Skip to content
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

Merged
merged 16 commits into from
Sep 30, 2022

Conversation

onurays
Copy link
Contributor

@onurays onurays commented Sep 28, 2022

Type of change

  • Feature
  • Bugfix
  • Technical
  • Other :

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

  • Physical
  • Emulator
  • OS version(s):

Checklist

@onurays onurays requested review from a team and mnaturel and removed request for a team September 28, 2022 15:22
import org.amshove.kluent.shouldBeEqualTo
import org.junit.Test

private val A_USER_AGENT_LIST_FOR_ANDROID = listOf(

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

Copy link
Contributor Author

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(
Copy link
Contributor

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.

Copy link
Contributor Author

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() &&
Copy link
Contributor

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?

Copy link
Contributor Author

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)
Copy link
Contributor

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?

Copy link
Contributor Author

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)
Copy link
Contributor

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).

Copy link
Contributor Author

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.

Copy link
Contributor

@mnaturel mnaturel left a 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.

Copy link
Member

@bmarty bmarty left a 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,
Copy link
Member

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.

Copy link
Contributor Author

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?

Copy link
Member

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.

Copy link
Member

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.

Copy link
Contributor Author

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"
Copy link
Member

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.

Copy link
Contributor Author

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)
Copy link
Member

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.

Copy link
Contributor Author

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]
}
}
Copy link
Member

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)

Copy link
Contributor Author

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,
Copy link
Contributor

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)
Copy link
Contributor

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,
)

Copy link
Contributor Author

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)
Copy link
Contributor

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.

@sonarcloud
Copy link

sonarcloud bot commented Sep 30, 2022

Kudos, SonarCloud Quality Gate passed!    Quality Gate passed

Bug A 0 Bugs
Vulnerability A 0 Vulnerabilities
Security Hotspot A 0 Security Hotspots
Code Smell A 0 Code Smells

87.8% 87.8% Coverage
0.0% 0.0% Duplication

@onurays onurays merged commit d0dd446 into develop Sep 30, 2022
@onurays onurays deleted the feature/ons/parse_user_agent branch September 30, 2022 15:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants