From dcfb2683ebc9b9deea013c152d7c406ca24b2562 Mon Sep 17 00:00:00 2001 From: sozinov Date: Tue, 21 Jul 2026 11:40:01 +0300 Subject: [PATCH 1/2] MOBILE-284: Add shouldIncludeVersionCode option to exclude versionCode from User-Agent --- .../mobile_sdk/MindboxConfiguration.kt | 37 +++++++- .../mobile_sdk/models/Configuration.kt | 13 ++- .../mindbox/mobile_sdk/models/Requests.kt | 2 +- .../mobile_sdk/repository/MindboxDatabase.kt | 17 +++- .../mobile_sdk/MindboxConfigurationTest.kt | 89 +++++++++++++++++++ .../mobile_sdk/models/ConfigurationTest.kt | 70 +++++++++++++++ 6 files changed, 218 insertions(+), 10 deletions(-) create mode 100644 sdk/src/test/java/cloud/mindbox/mobile_sdk/MindboxConfigurationTest.kt create mode 100644 sdk/src/test/java/cloud/mindbox/mobile_sdk/models/ConfigurationTest.kt diff --git a/sdk/src/main/java/cloud/mindbox/mobile_sdk/MindboxConfiguration.kt b/sdk/src/main/java/cloud/mindbox/mobile_sdk/MindboxConfiguration.kt index b6c10f981..b684e6c31 100644 --- a/sdk/src/main/java/cloud/mindbox/mobile_sdk/MindboxConfiguration.kt +++ b/sdk/src/main/java/cloud/mindbox/mobile_sdk/MindboxConfiguration.kt @@ -21,6 +21,7 @@ public class MindboxConfiguration private constructor( internal val versionCode: String, internal val subscribeCustomerIfCreated: Boolean, internal val shouldCreateCustomer: Boolean, + internal val shouldIncludeVersionCode: Boolean, internal val uuidDebugEnabled: Boolean, internal val operationsDomain: String? = null, ) { @@ -35,6 +36,7 @@ public class MindboxConfiguration private constructor( versionCode = builder.versionCode, subscribeCustomerIfCreated = builder.subscribeCustomerIfCreated, shouldCreateCustomer = builder.shouldCreateCustomer, + shouldIncludeVersionCode = builder.shouldIncludeVersionCode, uuidDebugEnabled = builder.uuidDebugEnabled, operationsDomain = builder.operationsDomain, ) @@ -49,6 +51,7 @@ public class MindboxConfiguration private constructor( versionCode: String = this.versionCode, subscribeCustomerIfCreated: Boolean = this.subscribeCustomerIfCreated, shouldCreateCustomer: Boolean = this.shouldCreateCustomer, + shouldIncludeVersionCode: Boolean = this.shouldIncludeVersionCode, uuidDebugEnabled: Boolean = this.uuidDebugEnabled, operationsDomain: String? = this.operationsDomain, ) = MindboxConfiguration( @@ -61,6 +64,7 @@ public class MindboxConfiguration private constructor( versionCode = versionCode, subscribeCustomerIfCreated = subscribeCustomerIfCreated, shouldCreateCustomer = shouldCreateCustomer, + shouldIncludeVersionCode = shouldIncludeVersionCode, uuidDebugEnabled = uuidDebugEnabled, operationsDomain = operationsDomain, ) @@ -75,6 +79,7 @@ public class MindboxConfiguration private constructor( "versionCode = $versionCode, " + "subscribeCustomerIfCreated = $subscribeCustomerIfCreated, " + "shouldCreateCustomer = $shouldCreateCustomer, " + + "shouldIncludeVersionCode = $shouldIncludeVersionCode, " + "uuidDebugEnabled = $uuidDebugEnabled, " + "operationsDomain = $operationsDomain)" } @@ -89,6 +94,7 @@ public class MindboxConfiguration private constructor( private const val PLACEHOLDER_APP_PACKAGE_NAME = "Unknown package name" private const val PLACEHOLDER_APP_VERSION_NAME = "Unknown version" private const val PLACEHOLDER_APP_VERSION_CODE = "?" + private const val EMPTY_APP_VERSION_CODE = "" } internal var previousInstallationId: String = "" @@ -98,6 +104,7 @@ public class MindboxConfiguration private constructor( internal var versionName: String = PLACEHOLDER_APP_VERSION_NAME internal var versionCode: String = PLACEHOLDER_APP_VERSION_CODE internal var shouldCreateCustomer: Boolean = true + internal var shouldIncludeVersionCode: Boolean = true internal var uuidDebugEnabled: Boolean = true internal var operationsDomain: String? = null @@ -142,6 +149,21 @@ public class MindboxConfiguration private constructor( return this } + /** + * Specifies whether the app versionCode is included in the app version reported + * to Mindbox (the User-Agent header). When false, only versionName is reported. + * + * Note: starting from SDK version 3.0.0 the default value will change to false, + * and versionCode will not be reported unless explicitly enabled. + * + * @param shouldIncludeVersionCode - flag which determines if versionCode is reported. + * Default value is true. + */ + public fun shouldIncludeVersionCode(shouldIncludeVersionCode: Boolean): Builder { + this.shouldIncludeVersionCode = shouldIncludeVersionCode + return this + } + /** * Specifies if Mindbox UUID copy to clipboard functionality is enabled. If enabled - UUID * can be copied to clipboard by minimizing and maximizing your app 5 times in 10 seconds @@ -181,18 +203,25 @@ public class MindboxConfiguration private constructor( val packageInfo = packageManager.getPackageInfoCompat(context, 0) packageName = packageInfo.packageName.trim() this.versionName = packageInfo.versionName?.trim() - ?: PLACEHOLDER_APP_PACKAGE_NAME + ?: PLACEHOLDER_APP_VERSION_NAME this.versionCode = - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { - packageInfo.longVersionCode.toString().trim() + if (shouldIncludeVersionCode) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + packageInfo.longVersionCode.toString().trim() + } else { + PackageInfoCompat.getLongVersionCode(packageInfo).toString().trim() + } } else { - PackageInfoCompat.getLongVersionCode(packageInfo).toString().trim() + EMPTY_APP_VERSION_CODE } // need for scheduling and stopping one-time background service SharedPreferencesManager.with(context) MindboxPreferences.hostAppName = packageName } catch (_: Exception) { + if (!shouldIncludeVersionCode) { + versionCode = EMPTY_APP_VERSION_CODE + } MindboxLoggerImpl.e( this, "Getting app info failed. Identified as an unknown application", diff --git a/sdk/src/main/java/cloud/mindbox/mobile_sdk/models/Configuration.kt b/sdk/src/main/java/cloud/mindbox/mobile_sdk/models/Configuration.kt index c0b664e07..03f8795cc 100644 --- a/sdk/src/main/java/cloud/mindbox/mobile_sdk/models/Configuration.kt +++ b/sdk/src/main/java/cloud/mindbox/mobile_sdk/models/Configuration.kt @@ -20,6 +20,7 @@ internal data class Configuration( val subscribeCustomerIfCreated: Boolean, val shouldCreateCustomer: Boolean, val operationsDomain: String? = null, + val shouldIncludeVersionCode: Boolean = true, ) { internal constructor(mindboxConfiguration: MindboxConfiguration) : this( @@ -33,7 +34,14 @@ internal data class Configuration( subscribeCustomerIfCreated = mindboxConfiguration.subscribeCustomerIfCreated, shouldCreateCustomer = mindboxConfiguration.shouldCreateCustomer, operationsDomain = mindboxConfiguration.operationsDomain, + shouldIncludeVersionCode = mindboxConfiguration.shouldIncludeVersionCode, ) + + internal val hostAppVersion: String + get() = if (versionCode.isBlank()) versionName else "$versionName($versionCode)" + + internal val shortHostAppVersion: String + get() = if (versionCode.isBlank()) versionName else "$versionName-$versionCode" } internal fun Configuration.getUserAgent(): String { @@ -44,11 +52,10 @@ internal fun Configuration.getUserAgent(): String { Build.MANUFACTURER, Build.MODEL, packageName, - versionName, - versionCode, + hostAppVersion, ) } internal fun Configuration.getShortUserAgent(): String { - return "$packageName/$versionName-$versionCode mindbox.sdk/${BuildConfig.VERSION_NAME} " + return "$packageName/$shortHostAppVersion mindbox.sdk/${BuildConfig.VERSION_NAME}" } diff --git a/sdk/src/main/java/cloud/mindbox/mobile_sdk/models/Requests.kt b/sdk/src/main/java/cloud/mindbox/mobile_sdk/models/Requests.kt index 50cd3e650..555a93042 100644 --- a/sdk/src/main/java/cloud/mindbox/mobile_sdk/models/Requests.kt +++ b/sdk/src/main/java/cloud/mindbox/mobile_sdk/models/Requests.kt @@ -34,7 +34,7 @@ internal data class MindboxRequest( private const val VALUE_CONTENT_TYPE = "application/json; charset=utf-8" internal const val VALUE_USER_AGENT = - "mindbox.sdk/%1$1s (Android %2$1s; %3$1s; %4$1s) %5$1s/%6$1s(%7$1s)" // format: mindbox.sdk/{sdk.version} (Android {os_version}; {vendor}; {model}) {host_app_name}/{host_app_version} + "mindbox.sdk/%1$1s (Android %2$1s; %3$1s; %4$1s) %5$1s/%6$1s" // format: mindbox.sdk/{sdk.version} (Android {os_version}; {vendor}; {model}) {host_app_name}/{host_app_version} private const val VALUE_INTEGRATION = "Android-SDK" private const val VALUE_ACCEPT = "application/json" diff --git a/sdk/src/main/java/cloud/mindbox/mobile_sdk/repository/MindboxDatabase.kt b/sdk/src/main/java/cloud/mindbox/mobile_sdk/repository/MindboxDatabase.kt index ddf82ac7d..9b4c94feb 100644 --- a/sdk/src/main/java/cloud/mindbox/mobile_sdk/repository/MindboxDatabase.kt +++ b/sdk/src/main/java/cloud/mindbox/mobile_sdk/repository/MindboxDatabase.kt @@ -1,6 +1,7 @@ package cloud.mindbox.mobile_sdk.repository import android.content.Context +import androidx.annotation.VisibleForTesting import androidx.room.Database import androidx.room.Room import androidx.room.RoomDatabase @@ -14,7 +15,7 @@ import cloud.mindbox.mobile_sdk.managers.DbManager.CONFIGURATION_TABLE_NAME import cloud.mindbox.mobile_sdk.models.Configuration import cloud.mindbox.mobile_sdk.models.Event -@Database(entities = [Configuration::class, Event::class], exportSchema = false, version = 3) +@Database(entities = [Configuration::class, Event::class], exportSchema = false, version = 4) @TypeConverters(MindboxRoomConverter::class) internal abstract class MindboxDatabase : RoomDatabase() { @@ -40,6 +41,17 @@ internal abstract class MindboxDatabase : RoomDatabase() { } } + @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) + internal val MIGRATION_3_4 = object : Migration(3, 4) { + + override fun migrate(db: SupportSQLiteDatabase) { + db.execSQL( + "ALTER TABLE $CONFIGURATION_TABLE_NAME " + + "ADD COLUMN shouldIncludeVersionCode INTEGER NOT NULL DEFAULT 1" + ) + } + } + internal var isTestMode = false internal fun getInstance(context: Context) = if (!isTestMode) { @@ -50,7 +62,8 @@ internal abstract class MindboxDatabase : RoomDatabase() { DATABASE_NAME, ).addMigrations( MIGRATION_1_2, - MIGRATION_2_3 + MIGRATION_2_3, + MIGRATION_3_4 ) .build() } else { diff --git a/sdk/src/test/java/cloud/mindbox/mobile_sdk/MindboxConfigurationTest.kt b/sdk/src/test/java/cloud/mindbox/mobile_sdk/MindboxConfigurationTest.kt new file mode 100644 index 000000000..763ff512a --- /dev/null +++ b/sdk/src/test/java/cloud/mindbox/mobile_sdk/MindboxConfigurationTest.kt @@ -0,0 +1,89 @@ +package cloud.mindbox.mobile_sdk + +import android.content.Context +import androidx.test.core.app.ApplicationProvider +import io.mockk.every +import io.mockk.mockk +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.Shadows.shadowOf + +@RunWith(RobolectricTestRunner::class) +internal class MindboxConfigurationTest { + + companion object { + private const val DOMAIN = "api.mindbox.ru" + private const val ENDPOINT = "test-endpoint" + private const val VERSION_NAME = "1.2.3" + private const val VERSION_CODE = 123 + } + + private val context: Context = ApplicationProvider.getApplicationContext() + + @Before + fun setUp() { + val packageInfo = shadowOf(context.packageManager) + .getInternalMutablePackageInfo(context.packageName) + packageInfo.versionName = VERSION_NAME + @Suppress("DEPRECATION") + packageInfo.versionCode = VERSION_CODE + } + + @Test + fun `versionCode is read from PackageInfo by default`() { + val configuration = MindboxConfiguration.Builder(context, DOMAIN, ENDPOINT).build() + + assertEquals(VERSION_NAME, configuration.versionName) + assertEquals(VERSION_CODE.toString(), configuration.versionCode) + assertTrue(configuration.shouldIncludeVersionCode) + } + + @Test + fun `versionCode is read from PackageInfo when shouldIncludeVersionCode is true`() { + val configuration = MindboxConfiguration.Builder(context, DOMAIN, ENDPOINT) + .shouldIncludeVersionCode(true) + .build() + + assertEquals(VERSION_CODE.toString(), configuration.versionCode) + } + + @Test + fun `versionCode is empty when shouldIncludeVersionCode is false`() { + val configuration = MindboxConfiguration.Builder(context, DOMAIN, ENDPOINT) + .shouldIncludeVersionCode(false) + .build() + + assertEquals(VERSION_NAME, configuration.versionName) + assertEquals("", configuration.versionCode) + assertFalse(configuration.shouldIncludeVersionCode) + } + + @Test + fun `versionCode is placeholder when app info is unavailable`() { + val brokenContext = mockk { + every { packageManager } throws RuntimeException("no package manager") + } + + val configuration = MindboxConfiguration.Builder(brokenContext, DOMAIN, ENDPOINT).build() + + assertEquals("?", configuration.versionCode) + } + + @Test + fun `versionCode is empty when shouldIncludeVersionCode is false and app info is unavailable`() { + val brokenContext = mockk { + every { packageManager } throws RuntimeException("no package manager") + } + + val configuration = MindboxConfiguration.Builder(brokenContext, DOMAIN, ENDPOINT) + .shouldIncludeVersionCode(false) + .build() + + assertEquals("", configuration.versionCode) + } +} diff --git a/sdk/src/test/java/cloud/mindbox/mobile_sdk/models/ConfigurationTest.kt b/sdk/src/test/java/cloud/mindbox/mobile_sdk/models/ConfigurationTest.kt new file mode 100644 index 000000000..bd985c906 --- /dev/null +++ b/sdk/src/test/java/cloud/mindbox/mobile_sdk/models/ConfigurationTest.kt @@ -0,0 +1,70 @@ +package cloud.mindbox.mobile_sdk.models + +import cloud.mindbox.mobile_sdk.BuildConfig +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner + +@RunWith(RobolectricTestRunner::class) +internal class ConfigurationTest { + + private fun configuration(versionCode: String) = Configuration( + previousInstallationId = "", + previousDeviceUUID = "", + endpointId = "test-endpoint", + domain = "api.mindbox.ru", + packageName = "com.example.app", + versionName = "1.2.3", + versionCode = versionCode, + subscribeCustomerIfCreated = false, + shouldCreateCustomer = true, + ) + + @Test + fun `hostAppVersion contains versionCode in brackets when it is not blank`() { + assertEquals("1.2.3(123)", configuration(versionCode = "123").hostAppVersion) + } + + @Test + fun `hostAppVersion is versionName only when versionCode is blank`() { + assertEquals("1.2.3", configuration(versionCode = "").hostAppVersion) + } + + @Test + fun `user agent contains app version with versionCode when it is not blank`() { + val userAgent = configuration(versionCode = "123").getUserAgent() + + assertTrue( + "Unexpected User-Agent: $userAgent", + userAgent.endsWith(" com.example.app/1.2.3(123)"), + ) + } + + @Test + fun `user agent contains app version without versionCode when it is blank`() { + val userAgent = configuration(versionCode = "").getUserAgent() + + assertTrue( + "Unexpected User-Agent: $userAgent", + userAgent.endsWith(" com.example.app/1.2.3"), + ) + } + + @Test + fun `short user agent contains versionCode when it is not blank`() { + assertEquals( + "com.example.app/1.2.3-123 mindbox.sdk/${BuildConfig.VERSION_NAME}", + configuration(versionCode = "123").getShortUserAgent(), + ) + } + + @Test + fun `short user agent contains versionName only when versionCode is blank`() { + assertEquals( + "com.example.app/1.2.3 mindbox.sdk/${BuildConfig.VERSION_NAME}", + configuration(versionCode = "").getShortUserAgent(), + ) + } +} From ece757dd0f236ea7b385316608a70336a23e2ffc Mon Sep 17 00:00:00 2001 From: sozinov Date: Tue, 21 Jul 2026 16:00:41 +0300 Subject: [PATCH 2/2] MOBILE-284: follow review --- .../cloud/mindbox/mobile_sdk/repository/MindboxDatabase.kt | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/sdk/src/main/java/cloud/mindbox/mobile_sdk/repository/MindboxDatabase.kt b/sdk/src/main/java/cloud/mindbox/mobile_sdk/repository/MindboxDatabase.kt index 9b4c94feb..95af62d5b 100644 --- a/sdk/src/main/java/cloud/mindbox/mobile_sdk/repository/MindboxDatabase.kt +++ b/sdk/src/main/java/cloud/mindbox/mobile_sdk/repository/MindboxDatabase.kt @@ -1,7 +1,6 @@ package cloud.mindbox.mobile_sdk.repository import android.content.Context -import androidx.annotation.VisibleForTesting import androidx.room.Database import androidx.room.Room import androidx.room.RoomDatabase @@ -11,6 +10,7 @@ import androidx.sqlite.db.SupportSQLiteDatabase import cloud.mindbox.mobile_sdk.converters.MindboxRoomConverter import cloud.mindbox.mobile_sdk.data.ConfigurationsDao import cloud.mindbox.mobile_sdk.data.EventsDao +import cloud.mindbox.mobile_sdk.logger.mindboxLogI import cloud.mindbox.mobile_sdk.managers.DbManager.CONFIGURATION_TABLE_NAME import cloud.mindbox.mobile_sdk.models.Configuration import cloud.mindbox.mobile_sdk.models.Event @@ -41,14 +41,15 @@ internal abstract class MindboxDatabase : RoomDatabase() { } } - @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) - internal val MIGRATION_3_4 = object : Migration(3, 4) { + private val MIGRATION_3_4 = object : Migration(3, 4) { override fun migrate(db: SupportSQLiteDatabase) { + mindboxLogI("DB migration 3->4 started") db.execSQL( "ALTER TABLE $CONFIGURATION_TABLE_NAME " + "ADD COLUMN shouldIncludeVersionCode INTEGER NOT NULL DEFAULT 1" ) + mindboxLogI("DB migration 3->4 finished") } }