-
Notifications
You must be signed in to change notification settings - Fork 731
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[Device Manager] Parse user agents (PSG-762) #7247
Conversation
import org.amshove.kluent.shouldBeEqualTo | ||
import org.junit.Test | ||
|
||
private val A_USER_AGENT_LIST_FOR_ANDROID = listOf( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it would be good to test some weird user agents since they can be fully customised by other applications or browser extensions like empty string, curl/7.55.1
, some random string
etc
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Right, added. Thanks.
|
||
import im.vector.app.features.settings.devices.v2.list.DeviceType | ||
|
||
data class DeviceUserAgent( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe we should name it DeviceExtendedInfo
to avoid referencing the source of the data? We could decide in the future to use something different than the user agent but the info will still be the same.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Make sense, done.
} | ||
|
||
private fun isMobile(browserSegments: List<String>): Boolean { | ||
return browserSegments.lastOrNull()?.startsWith("Safari").orFalse() && |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Out of curiosity, why do we check for Safari
keyword for mobile browser case?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice catch! Fixed.
} else { | ||
deviceOperatingSystemSegments.getOrNull(0) | ||
} | ||
return DeviceUserAgent(DeviceType.DESKTOP, browserName, deviceOperatingSystem, null, null) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In the case of Web/Desktop, we cannot extract an app name and an app version?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, web browsers are generating it automatically without app name. Instead, web will put them inside account_data
. So we will need both extended device info and account data to be able to render session details screen.
@@ -110,21 +117,24 @@ class GetDeviceFullInfoListUseCaseTest { | |||
cryptoDeviceInfo = cryptoDeviceInfo1, | |||
roomEncryptionTrustLevel = RoomEncryptionTrustLevel.Trusted, | |||
isInactive = true, | |||
isCurrentDevice = true | |||
isCurrentDevice = true, | |||
deviceUserAgent = DeviceUserAgent(DeviceType.MOBILE) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This test should not pass normally. We expect to have the mocked result as deviceUseAgent
value:
DeviceUserAgent(
DeviceType.MOBILE,
"Xiaomi Mi 9T",
"Android 11",
"Element dbg",
"1.5.0-dev"
)
This means the assertion seems to be not correct in a certain way since it should not be equal to DeviceUserAgent(DeviceType.MOBILE)
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It is because filter use case is a mockk and returning expected result. Anyway, we don't need to test it, removed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice work! The code is clean and fairly easy to read regarding the complexity of the non-standardization of the data structure. I added some minor comments to look into.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Some thoughts.
val unstableLastSeenUserAgent: String? = null, | ||
|
||
@Json(name = "last_seen_user_agent") | ||
val lastSeenUserAgent: String? = null, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is strange to introduce both unstable and stable keys in the same PR.
You should use only unstable, then when the MSC is merged, use both, then after a while, remove unstable, that the servers should not used anymore.
If the MSC is already merged, you can directly use the stable key.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's say we used only unstable
one. MSC is merged and server is not using unstable
one anymore and sending us the stable
one. But I didn't updated EA in my device. So we will miss this field, no?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
From my understanding, the server has to use both stable and unstable field for a limited period of time.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
See in the doc:
Implementations MUST NOT use stable endpoints before the MSC has completed FCP. Once that has occurred, implementations are allowed to use stable endpoints, but are not required to.
This is about endpoint, but I think the same rule applies for Json key.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I will ask this to Backend team. I would be happy if we can get rid of this. For the record; we also have them for live location and polls.
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" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Note: this will probably change when we will use the Rust SDK.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Right, I couldn't find safer way for now :/ I can make this keyword in a const val to be safer. But then again we can't parse non-updated clients.
val deviceInfoSegments = userAgent.substringAfter("(").substringBeforeLast(")").split("; ") | ||
val deviceModel = deviceInfoSegments.getOrNull(0) | ||
val deviceOperatingSystem = deviceInfoSegments.getOrNull(1) | ||
return DeviceUserAgent(DeviceType.MOBILE, deviceModel, deviceOperatingSystem, appName, appVersion) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We do not distinguish iOS and Android devices? I thought the idea would be to render them differently in the UI.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, the design suggests only one icon for mobile devices. Instead, we will render device model info in session details.
AN_UNKNOWN_USER_AGENT_LIST.forEachIndexed { index, userAgent -> | ||
parseDeviceUserAgentUseCase.execute(userAgent) shouldBeEqualTo AN_UNKNOWN_USER_AGENT_EXPECTED_RESULT_LIST[index] | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe to avoid repeating the logic in all the test, you could extract it:
private fun doTest(userAgentList: List<String>, expectedResultList: List<DeviceUserAgent>) {
userAgentList.forEachIndexed { index, userAgent ->
parseDeviceUserAgentUseCase.execute(userAgent) shouldBeEqualTo expectedResultList[index]
}
}
and then call:
doTest(AN_UNKNOWN_USER_AGENT_LIST, AN_UNKNOWN_USER_AGENT_EXPECTED_RESULT_LIST)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It is easier to navigate to failed test case in Android Studio or see the which one is failing on CI by checking function name on logs. If we make this generic it would take some more time to detect which platform is failing.
/** | ||
* i.e. Chrome. | ||
*/ | ||
val browser: String? = null, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this field used? I am not seeing it is used in the parsing use case. I think we should use clientName
and clientVersion
field to set the browser info in case of a Web device.
deviceModel = deviceInfoSegments.getOrNull(0) | ||
deviceOperatingSystem = deviceInfoSegments.getOrNull(1) | ||
} | ||
return DeviceExtendedInfo(DeviceType.MOBILE, deviceModel, deviceOperatingSystem, appName, appVersion) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could we set the fields of the DeviceExtendedInfo
by naming them to avoid confusion? For example:
DeviceExtendedInfo(
deviceType = DeviceType.MOBILE,
deviceModel = deviceModel,
deviceOperatingSystem = deviceOperatingSystem,
clientName = appName,
clientVersion = appVersion,
)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, better. Done.
} else { | ||
deviceOperatingSystemSegments.getOrNull(0) | ||
} | ||
return DeviceExtendedInfo(DeviceType.DESKTOP, browserName, deviceOperatingSystem, null, null) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think in case of the Web device type, we should set browser info into the clientName
and clientVersion
. It seems we should only use these fields for the case of Web devices to render a "Browser" section. For the section "Application", we will use info coming from accountData
event.
Kudos, SonarCloud Quality Gate passed! |
Type of change
Content
We need to parse user agents of all platforms to be able to render them on new device manager screens.
Motivation and context
Screenshots / GIFs
Tests
Tested devices
Checklist