-
Notifications
You must be signed in to change notification settings - Fork 754
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
Changes from 11 commits
c70b620
3e66a65
04a305b
2bcf0c3
41643ff
5666383
7a36b10
c16b5d6
4c173a7
8663fe8
38cd2be
6d459a0
81e8ddf
0f8637b
bf4576d
ea8dc45
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
[Device Manager] Parse user agents |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,46 @@ | ||
/* | ||
* 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 DeviceUserAgent( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe we should name it There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Make sense, done. |
||
/** | ||
* 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, | ||
/** | ||
* i.e. Chrome. | ||
*/ | ||
val browser: String? = null, | ||
) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,148 @@ | ||
/* | ||
* 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?): DeviceUserAgent { | ||
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): DeviceUserAgent { | ||
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 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("(").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 commentThe 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 commentThe 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. |
||
} | ||
|
||
private fun parseDesktopUserAgent(userAgent: String): DeviceUserAgent { | ||
val browserSegments = userAgent.split(" ") | ||
val browserName = when { | ||
isFirefox(browserSegments) -> { | ||
"Firefox" | ||
} | ||
isEdge(browserSegments) -> { | ||
"Edge" | ||
} | ||
isMobile(browserSegments) -> { | ||
getMobileBrowserName(browserSegments) | ||
} | ||
isSafari(browserSegments) -> { | ||
"Safari" | ||
} | ||
else -> { | ||
getRegularBrowserName(browserSegments) | ||
} | ||
} | ||
|
||
val deviceOperatingSystemSegments = userAgent.substringAfter("(").substringBefore(")").split("; ") | ||
val deviceOperatingSystem = if (deviceOperatingSystemSegments.getOrNull(1)?.startsWith("Android").orFalse()) { | ||
deviceOperatingSystemSegments.getOrNull(1) | ||
} 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 commentThe 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 commentThe 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 |
||
} | ||
|
||
private fun parseWebUserAgent(userAgent: String): DeviceUserAgent { | ||
return parseDesktopUserAgent(userAgent).copy( | ||
deviceType = DeviceType.WEB | ||
) | ||
} | ||
|
||
private fun createUnknownUserAgent(): DeviceUserAgent { | ||
return DeviceUserAgent(DeviceType.UNKNOWN) | ||
} | ||
|
||
private fun isFirefox(browserSegments: List<String>): Boolean { | ||
return browserSegments.lastOrNull()?.startsWith("Firefox").orFalse() | ||
} | ||
|
||
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.lastOrNull()?.startsWith("Safari").orFalse() && | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Out of curiosity, why do we check for There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nice catch! Fixed. |
||
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" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 commentThe 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. |
||
|
||
// 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/" | ||
} | ||
} |
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 usingunstable
one anymore and sending us thestable
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:
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.