diff --git a/changelog.d/7209.sdk b/changelog.d/7209.sdk new file mode 100644 index 0000000000..6375f5e495 --- /dev/null +++ b/changelog.d/7209.sdk @@ -0,0 +1 @@ +[Device Manager] Extend user agent to include device information diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/ComputeUserAgentUseCase.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/ComputeUserAgentUseCase.kt new file mode 100644 index 0000000000..6eb4d5b104 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/ComputeUserAgentUseCase.kt @@ -0,0 +1,81 @@ +/* + * Copyright 2022 The Matrix.org Foundation C.I.C. + * + * 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 org.matrix.android.sdk.internal.network + +import android.content.Context +import android.os.Build +import org.matrix.android.sdk.BuildConfig +import org.matrix.android.sdk.api.extensions.tryOrNull +import javax.inject.Inject + +class ComputeUserAgentUseCase @Inject constructor( + private val context: Context, +) { + + /** + * Create an user agent with the application version. + * Ex: Element/1.5.0 (Xiaomi Mi 9T; Android 11; RKQ1.200826.002; Flavour GooglePlay; MatrixAndroidSdk2 1.5.0) + * + * @param flavorDescription the flavor description + */ + fun execute(flavorDescription: String): String { + val appPackageName = context.applicationContext.packageName + val pm = context.packageManager + + val appName = tryOrNull { pm.getApplicationLabel(pm.getApplicationInfo(appPackageName, 0)).toString() } + ?.takeIf { + it.matches("\\A\\p{ASCII}*\\z".toRegex()) + } + ?: run { + // Use appPackageName instead of appName if appName is null or contains any non-ASCII character + appPackageName + } + val appVersion = tryOrNull { pm.getPackageInfo(context.applicationContext.packageName, 0).versionName } ?: FALLBACK_APP_VERSION + + val deviceManufacturer = Build.MANUFACTURER + val deviceModel = Build.MODEL + val androidVersion = Build.VERSION.RELEASE + val deviceBuildId = Build.DISPLAY + val matrixSdkVersion = BuildConfig.SDK_VERSION + + return buildString { + append(appName) + append("/") + append(appVersion) + append(" (") + append(deviceManufacturer) + append(" ") + append(deviceModel) + append("; ") + append("Android ") + append(androidVersion) + append("; ") + append(deviceBuildId) + append("; ") + append("Flavour ") + append(flavorDescription) + append("; ") + append("MatrixAndroidSdk2 ") + append(matrixSdkVersion) + append(")") + } + } + + companion object { + const val FALLBACK_APP_VERSION = "0.0.0" + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/UserAgentHolder.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/UserAgentHolder.kt index 28d96dfce7..4e83261277 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/UserAgentHolder.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/UserAgentHolder.kt @@ -16,73 +16,20 @@ package org.matrix.android.sdk.internal.network -import android.content.Context -import org.matrix.android.sdk.BuildConfig import org.matrix.android.sdk.api.MatrixConfiguration import org.matrix.android.sdk.internal.di.MatrixScope -import timber.log.Timber import javax.inject.Inject @MatrixScope internal class UserAgentHolder @Inject constructor( - private val context: Context, - matrixConfiguration: MatrixConfiguration + matrixConfiguration: MatrixConfiguration, + computeUserAgentUseCase: ComputeUserAgentUseCase, ) { var userAgent: String = "" private set init { - setApplicationFlavor(matrixConfiguration.applicationFlavor) - } - - /** - * Create an user agent with the application version. - * Ex: Element/1.0.0 (Linux; U; Android 6.0.1; SM-A510F Build/MMB29; Flavour GPlay; MatrixAndroidSdk2 1.0) - * - * @param flavorDescription the flavor description - */ - private fun setApplicationFlavor(flavorDescription: String) { - var appName = "" - var appVersion = "" - - try { - val appPackageName = context.applicationContext.packageName - val pm = context.packageManager - val appInfo = pm.getApplicationInfo(appPackageName, 0) - appName = pm.getApplicationLabel(appInfo).toString() - - val pkgInfo = pm.getPackageInfo(context.applicationContext.packageName, 0) - appVersion = pkgInfo.versionName ?: "" - - // Use appPackageName instead of appName if appName contains any non-ASCII character - if (!appName.matches("\\A\\p{ASCII}*\\z".toRegex())) { - appName = appPackageName - } - } catch (e: Exception) { - Timber.e(e, "## initUserAgent() : failed") - } - - val systemUserAgent = System.getProperty("http.agent") - - // cannot retrieve the application version - if (appName.isEmpty() || appVersion.isEmpty()) { - if (null == systemUserAgent) { - userAgent = "Java" + System.getProperty("java.version") - } - return - } - - // if there is no user agent or cannot parse it - if (null == systemUserAgent || systemUserAgent.lastIndexOf(")") == -1 || !systemUserAgent.contains("(")) { - userAgent = (appName + "/" + appVersion + " ( Flavour " + flavorDescription + - "; MatrixAndroidSdk2 " + BuildConfig.SDK_VERSION + ")") - } else { - // update - userAgent = appName + "/" + appVersion + " " + - systemUserAgent.substring(systemUserAgent.indexOf("("), systemUserAgent.lastIndexOf(")") - 1) + - "; Flavour " + flavorDescription + - "; MatrixAndroidSdk2 " + BuildConfig.SDK_VERSION + ")" - } + userAgent = computeUserAgentUseCase.execute(matrixConfiguration.applicationFlavor) } } diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/network/ComputeUserAgentUseCaseTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/network/ComputeUserAgentUseCaseTest.kt new file mode 100644 index 0000000000..9ed6f28d7e --- /dev/null +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/network/ComputeUserAgentUseCaseTest.kt @@ -0,0 +1,149 @@ +/* + * Copyright 2022 The Matrix.org Foundation C.I.C. + * + * 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 org.matrix.android.sdk.internal.network + +import android.content.Context +import android.content.pm.ApplicationInfo +import android.content.pm.PackageInfo +import android.content.pm.PackageManager +import android.os.Build +import io.mockk.every +import io.mockk.mockk +import org.amshove.kluent.shouldBeEqualTo +import org.junit.Before +import org.junit.Test +import org.matrix.android.sdk.BuildConfig +import java.lang.Exception + +private const val A_PACKAGE_NAME = "org.matrix.sdk" +private const val AN_APP_NAME = "Element" +private const val A_NON_ASCII_APP_NAME = "Élement" +private const val AN_APP_VERSION = "1.5.1" +private const val A_FLAVOUR = "GooglePlay" + +class ComputeUserAgentUseCaseTest { + + private val context = mockk() + private val packageManager = mockk() + private val applicationInfo = mockk() + private val packageInfo = mockk() + + private val computeUserAgentUseCase = ComputeUserAgentUseCase(context) + + @Before + fun setUp() { + every { context.applicationContext } returns context + every { context.packageName } returns A_PACKAGE_NAME + every { context.packageManager } returns packageManager + every { packageManager.getApplicationInfo(any(), any()) } returns applicationInfo + every { packageManager.getPackageInfo(any(), any()) } returns packageInfo + } + + @Test + fun `given a non-null app name and app version when computing user agent then returns expected user agent`() { + // Given + givenAppName(AN_APP_NAME) + givenAppVersion(AN_APP_VERSION) + + // When + val result = computeUserAgentUseCase.execute(A_FLAVOUR) + + // Then + val expectedUserAgent = constructExpectedUserAgent(AN_APP_NAME, AN_APP_VERSION) + result shouldBeEqualTo expectedUserAgent + } + + @Test + fun `given a null app name when computing user agent then returns user agent with package name instead of app name`() { + // Given + givenAppName(null) + givenAppVersion(AN_APP_VERSION) + + // When + val result = computeUserAgentUseCase.execute(A_FLAVOUR) + + // Then + val expectedUserAgent = constructExpectedUserAgent(A_PACKAGE_NAME, AN_APP_VERSION) + result shouldBeEqualTo expectedUserAgent + } + + @Test + fun `given a non-ascii app name when computing user agent then returns user agent with package name instead of app name`() { + // Given + givenAppName(A_NON_ASCII_APP_NAME) + givenAppVersion(AN_APP_VERSION) + + // When + val result = computeUserAgentUseCase.execute(A_FLAVOUR) + + // Then + val expectedUserAgent = constructExpectedUserAgent(A_PACKAGE_NAME, AN_APP_VERSION) + result shouldBeEqualTo expectedUserAgent + } + + @Test + fun `given a null app version when computing user agent then returns user agent with a fallback app version`() { + // Given + givenAppName(AN_APP_NAME) + givenAppVersion(null) + + // When + val result = computeUserAgentUseCase.execute(A_FLAVOUR) + + // Then + val expectedUserAgent = constructExpectedUserAgent(AN_APP_NAME, ComputeUserAgentUseCase.FALLBACK_APP_VERSION) + result shouldBeEqualTo expectedUserAgent + } + + private fun constructExpectedUserAgent(appName: String, appVersion: String): String { + return buildString { + append(appName) + append("/") + append(appVersion) + append(" (") + append(Build.MANUFACTURER) + append(" ") + append(Build.MODEL) + append("; ") + append("Android ") + append(Build.VERSION.RELEASE) + append("; ") + append(Build.DISPLAY) + append("; ") + append("Flavour ") + append(A_FLAVOUR) + append("; ") + append("MatrixAndroidSdk2 ") + append(BuildConfig.SDK_VERSION) + append(")") + } + } + + private fun givenAppName(deviceName: String?) { + if (deviceName == null) { + every { packageManager.getApplicationLabel(any()) } throws Exception("Cannot retrieve application name") + } else if (!deviceName.matches("\\A\\p{ASCII}*\\z".toRegex())) { + every { packageManager.getApplicationLabel(any()) } returns A_PACKAGE_NAME + } else { + every { packageManager.getApplicationLabel(any()) } returns deviceName + } + } + + private fun givenAppVersion(appVersion: String?) { + packageInfo.versionName = appVersion + } +}