Skip to content

Commit 1238f31

Browse files
authored
Merge pull request #7209 from vector-im/feature/ons/extend_user_agent
[Device Manager] Extend user agent to include device information (PSG-755)
2 parents 214867a + 0f0ec54 commit 1238f31

File tree

4 files changed

+234
-56
lines changed

4 files changed

+234
-56
lines changed

changelog.d/7209.sdk

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
[Device Manager] Extend user agent to include device information
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
/*
2+
* Copyright 2022 The Matrix.org Foundation C.I.C.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.matrix.android.sdk.internal.network
18+
19+
import android.content.Context
20+
import android.os.Build
21+
import org.matrix.android.sdk.BuildConfig
22+
import org.matrix.android.sdk.api.extensions.tryOrNull
23+
import javax.inject.Inject
24+
25+
class ComputeUserAgentUseCase @Inject constructor(
26+
private val context: Context,
27+
) {
28+
29+
/**
30+
* Create an user agent with the application version.
31+
* Ex: Element/1.5.0 (Xiaomi Mi 9T; Android 11; RKQ1.200826.002; Flavour GooglePlay; MatrixAndroidSdk2 1.5.0)
32+
*
33+
* @param flavorDescription the flavor description
34+
*/
35+
fun execute(flavorDescription: String): String {
36+
val appPackageName = context.applicationContext.packageName
37+
val pm = context.packageManager
38+
39+
val appName = tryOrNull { pm.getApplicationLabel(pm.getApplicationInfo(appPackageName, 0)).toString() }
40+
?.takeIf {
41+
it.matches("\\A\\p{ASCII}*\\z".toRegex())
42+
}
43+
?: run {
44+
// Use appPackageName instead of appName if appName is null or contains any non-ASCII character
45+
appPackageName
46+
}
47+
val appVersion = tryOrNull { pm.getPackageInfo(context.applicationContext.packageName, 0).versionName } ?: FALLBACK_APP_VERSION
48+
49+
val deviceManufacturer = Build.MANUFACTURER
50+
val deviceModel = Build.MODEL
51+
val androidVersion = Build.VERSION.RELEASE
52+
val deviceBuildId = Build.DISPLAY
53+
val matrixSdkVersion = BuildConfig.SDK_VERSION
54+
55+
return buildString {
56+
append(appName)
57+
append("/")
58+
append(appVersion)
59+
append(" (")
60+
append(deviceManufacturer)
61+
append(" ")
62+
append(deviceModel)
63+
append("; ")
64+
append("Android ")
65+
append(androidVersion)
66+
append("; ")
67+
append(deviceBuildId)
68+
append("; ")
69+
append("Flavour ")
70+
append(flavorDescription)
71+
append("; ")
72+
append("MatrixAndroidSdk2 ")
73+
append(matrixSdkVersion)
74+
append(")")
75+
}
76+
}
77+
78+
companion object {
79+
const val FALLBACK_APP_VERSION = "0.0.0"
80+
}
81+
}

matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/UserAgentHolder.kt

+3-56
Original file line numberDiff line numberDiff line change
@@ -16,73 +16,20 @@
1616

1717
package org.matrix.android.sdk.internal.network
1818

19-
import android.content.Context
20-
import org.matrix.android.sdk.BuildConfig
2119
import org.matrix.android.sdk.api.MatrixConfiguration
2220
import org.matrix.android.sdk.internal.di.MatrixScope
23-
import timber.log.Timber
2421
import javax.inject.Inject
2522

2623
@MatrixScope
2724
internal class UserAgentHolder @Inject constructor(
28-
private val context: Context,
29-
matrixConfiguration: MatrixConfiguration
25+
matrixConfiguration: MatrixConfiguration,
26+
computeUserAgentUseCase: ComputeUserAgentUseCase,
3027
) {
3128

3229
var userAgent: String = ""
3330
private set
3431

3532
init {
36-
setApplicationFlavor(matrixConfiguration.applicationFlavor)
37-
}
38-
39-
/**
40-
* Create an user agent with the application version.
41-
* Ex: Element/1.0.0 (Linux; U; Android 6.0.1; SM-A510F Build/MMB29; Flavour GPlay; MatrixAndroidSdk2 1.0)
42-
*
43-
* @param flavorDescription the flavor description
44-
*/
45-
private fun setApplicationFlavor(flavorDescription: String) {
46-
var appName = ""
47-
var appVersion = ""
48-
49-
try {
50-
val appPackageName = context.applicationContext.packageName
51-
val pm = context.packageManager
52-
val appInfo = pm.getApplicationInfo(appPackageName, 0)
53-
appName = pm.getApplicationLabel(appInfo).toString()
54-
55-
val pkgInfo = pm.getPackageInfo(context.applicationContext.packageName, 0)
56-
appVersion = pkgInfo.versionName ?: ""
57-
58-
// Use appPackageName instead of appName if appName contains any non-ASCII character
59-
if (!appName.matches("\\A\\p{ASCII}*\\z".toRegex())) {
60-
appName = appPackageName
61-
}
62-
} catch (e: Exception) {
63-
Timber.e(e, "## initUserAgent() : failed")
64-
}
65-
66-
val systemUserAgent = System.getProperty("http.agent")
67-
68-
// cannot retrieve the application version
69-
if (appName.isEmpty() || appVersion.isEmpty()) {
70-
if (null == systemUserAgent) {
71-
userAgent = "Java" + System.getProperty("java.version")
72-
}
73-
return
74-
}
75-
76-
// if there is no user agent or cannot parse it
77-
if (null == systemUserAgent || systemUserAgent.lastIndexOf(")") == -1 || !systemUserAgent.contains("(")) {
78-
userAgent = (appName + "/" + appVersion + " ( Flavour " + flavorDescription +
79-
"; MatrixAndroidSdk2 " + BuildConfig.SDK_VERSION + ")")
80-
} else {
81-
// update
82-
userAgent = appName + "/" + appVersion + " " +
83-
systemUserAgent.substring(systemUserAgent.indexOf("("), systemUserAgent.lastIndexOf(")") - 1) +
84-
"; Flavour " + flavorDescription +
85-
"; MatrixAndroidSdk2 " + BuildConfig.SDK_VERSION + ")"
86-
}
33+
userAgent = computeUserAgentUseCase.execute(matrixConfiguration.applicationFlavor)
8734
}
8835
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
/*
2+
* Copyright 2022 The Matrix.org Foundation C.I.C.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.matrix.android.sdk.internal.network
18+
19+
import android.content.Context
20+
import android.content.pm.ApplicationInfo
21+
import android.content.pm.PackageInfo
22+
import android.content.pm.PackageManager
23+
import android.os.Build
24+
import io.mockk.every
25+
import io.mockk.mockk
26+
import org.amshove.kluent.shouldBeEqualTo
27+
import org.junit.Before
28+
import org.junit.Test
29+
import org.matrix.android.sdk.BuildConfig
30+
import java.lang.Exception
31+
32+
private const val A_PACKAGE_NAME = "org.matrix.sdk"
33+
private const val AN_APP_NAME = "Element"
34+
private const val A_NON_ASCII_APP_NAME = "Élement"
35+
private const val AN_APP_VERSION = "1.5.1"
36+
private const val A_FLAVOUR = "GooglePlay"
37+
38+
class ComputeUserAgentUseCaseTest {
39+
40+
private val context = mockk<Context>()
41+
private val packageManager = mockk<PackageManager>()
42+
private val applicationInfo = mockk<ApplicationInfo>()
43+
private val packageInfo = mockk<PackageInfo>()
44+
45+
private val computeUserAgentUseCase = ComputeUserAgentUseCase(context)
46+
47+
@Before
48+
fun setUp() {
49+
every { context.applicationContext } returns context
50+
every { context.packageName } returns A_PACKAGE_NAME
51+
every { context.packageManager } returns packageManager
52+
every { packageManager.getApplicationInfo(any(), any()) } returns applicationInfo
53+
every { packageManager.getPackageInfo(any<String>(), any()) } returns packageInfo
54+
}
55+
56+
@Test
57+
fun `given a non-null app name and app version when computing user agent then returns expected user agent`() {
58+
// Given
59+
givenAppName(AN_APP_NAME)
60+
givenAppVersion(AN_APP_VERSION)
61+
62+
// When
63+
val result = computeUserAgentUseCase.execute(A_FLAVOUR)
64+
65+
// Then
66+
val expectedUserAgent = constructExpectedUserAgent(AN_APP_NAME, AN_APP_VERSION)
67+
result shouldBeEqualTo expectedUserAgent
68+
}
69+
70+
@Test
71+
fun `given a null app name when computing user agent then returns user agent with package name instead of app name`() {
72+
// Given
73+
givenAppName(null)
74+
givenAppVersion(AN_APP_VERSION)
75+
76+
// When
77+
val result = computeUserAgentUseCase.execute(A_FLAVOUR)
78+
79+
// Then
80+
val expectedUserAgent = constructExpectedUserAgent(A_PACKAGE_NAME, AN_APP_VERSION)
81+
result shouldBeEqualTo expectedUserAgent
82+
}
83+
84+
@Test
85+
fun `given a non-ascii app name when computing user agent then returns user agent with package name instead of app name`() {
86+
// Given
87+
givenAppName(A_NON_ASCII_APP_NAME)
88+
givenAppVersion(AN_APP_VERSION)
89+
90+
// When
91+
val result = computeUserAgentUseCase.execute(A_FLAVOUR)
92+
93+
// Then
94+
val expectedUserAgent = constructExpectedUserAgent(A_PACKAGE_NAME, AN_APP_VERSION)
95+
result shouldBeEqualTo expectedUserAgent
96+
}
97+
98+
@Test
99+
fun `given a null app version when computing user agent then returns user agent with a fallback app version`() {
100+
// Given
101+
givenAppName(AN_APP_NAME)
102+
givenAppVersion(null)
103+
104+
// When
105+
val result = computeUserAgentUseCase.execute(A_FLAVOUR)
106+
107+
// Then
108+
val expectedUserAgent = constructExpectedUserAgent(AN_APP_NAME, ComputeUserAgentUseCase.FALLBACK_APP_VERSION)
109+
result shouldBeEqualTo expectedUserAgent
110+
}
111+
112+
private fun constructExpectedUserAgent(appName: String, appVersion: String): String {
113+
return buildString {
114+
append(appName)
115+
append("/")
116+
append(appVersion)
117+
append(" (")
118+
append(Build.MANUFACTURER)
119+
append(" ")
120+
append(Build.MODEL)
121+
append("; ")
122+
append("Android ")
123+
append(Build.VERSION.RELEASE)
124+
append("; ")
125+
append(Build.DISPLAY)
126+
append("; ")
127+
append("Flavour ")
128+
append(A_FLAVOUR)
129+
append("; ")
130+
append("MatrixAndroidSdk2 ")
131+
append(BuildConfig.SDK_VERSION)
132+
append(")")
133+
}
134+
}
135+
136+
private fun givenAppName(deviceName: String?) {
137+
if (deviceName == null) {
138+
every { packageManager.getApplicationLabel(any()) } throws Exception("Cannot retrieve application name")
139+
} else if (!deviceName.matches("\\A\\p{ASCII}*\\z".toRegex())) {
140+
every { packageManager.getApplicationLabel(any()) } returns A_PACKAGE_NAME
141+
} else {
142+
every { packageManager.getApplicationLabel(any()) } returns deviceName
143+
}
144+
}
145+
146+
private fun givenAppVersion(appVersion: String?) {
147+
packageInfo.versionName = appVersion
148+
}
149+
}

0 commit comments

Comments
 (0)