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] Extend user agent to include device information (PSG-755) #7209

Merged
merged 8 commits into from
Sep 29, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog.d/7209.sdk
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
[Device Manager] Extend user agent to include device information
Original file line number Diff line number Diff line change
@@ -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"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the removal of this part.


// 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)
}
}
Original file line number Diff line number Diff line change
@@ -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<Context>()
private val packageManager = mockk<PackageManager>()
private val applicationInfo = mockk<ApplicationInfo>()
private val packageInfo = mockk<PackageInfo>()

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<String>(), 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
}
}