Skip to content
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
6 changes: 6 additions & 0 deletions id/src/main/java/com/simprints/id/Application.kt
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,10 @@ import com.simprints.infra.enrolment.records.repository.local.migration.RealmToR
import com.simprints.infra.eventsync.BuildConfig.DB_ENCRYPTION
import com.simprints.infra.logging.LoggingConstants.CrashReportTag.APPLICATION
import com.simprints.infra.logging.LoggingConstants.CrashReportingCustomKeys.DEVICE_ID
import com.simprints.infra.logging.LoggingConstants.CrashReportingCustomKeys.VERSION_HISTORY
import com.simprints.infra.logging.Simber
import com.simprints.infra.logging.SimberBuilder
import com.simprints.infra.logging.usecases.UpdateAndGetVersionHistoryUseCase
import com.simprints.infra.sync.SyncOrchestrator
import dagger.hilt.android.HiltAndroidApp
import kotlinx.coroutines.CoroutineScope
Expand All @@ -35,6 +37,8 @@ open class Application :
@Inject
lateinit var realmToRoomMigrationScheduler: RealmToRoomMigrationScheduler

private val getVersionNames = UpdateAndGetVersionHistoryUseCase()

@AppScope
@Inject
lateinit var appScope: CoroutineScope
Expand Down Expand Up @@ -76,6 +80,8 @@ open class Application :
open fun initApplication() {
SimberBuilder.initialize(this)
Simber.setUserProperty(DEVICE_ID, deviceHardwareId)
Simber.setUserProperty(VERSION_HISTORY, getVersionNames(this, BuildConfig.VERSION_NAME))

appScope.launch {
realmToRoomMigrationScheduler.scheduleMigrationWorkerIfNeeded()
syncOrchestrator.cleanupWorkers()
Expand Down
1 change: 1 addition & 0 deletions infra/logging/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ dependencies {
implementation(libs.kermit.io)

testImplementation(libs.testing.junit)
testImplementation(libs.testing.truth)
testImplementation(libs.testing.mockk.core)
testImplementation(libs.testing.robolectric.core)
testImplementation(libs.testing.coroutines)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ object LoggingConstants {
const val DEVICE_ID = "Device_ID"
const val SUBJECTS_DOWN_SYNC_TRIGGERS = "Down_sync_triggers"
const val SESSION_ID = "Session_ID"

const val VERSION_HISTORY = "Version_History"
}

object AnalyticsUserProperties {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package com.simprints.infra.logging.usecases

import android.content.Context
import android.content.Context.MODE_PRIVATE
import androidx.core.content.edit

class UpdateAndGetVersionHistoryUseCase {
operator fun invoke(
context: Context,
currentVersion: String,
): String {
// Keeping the preferences initialised only in the scope of the function call
val preference = context.getSharedPreferences(VERSION_CACHE_NAME, MODE_PRIVATE)

val versions = preference.getString(VERSIONS_KEY, null).orEmpty()
if (versions.startsWith(currentVersion)) return versions

val newVersions =
if (versions.isEmpty()) currentVersion else "$currentVersion$VERSION_DELIMITER$versions"
return newVersions
.let {
// Keep the total length within custom key constrains
if (it.length > MAX_VALUE_LENGTH) it.substringBeforeLast(VERSION_DELIMITER) else it
}.also { preference.edit { putString(VERSIONS_KEY, it) } }
}

companion object Companion {
private const val VERSION_CACHE_NAME = "574b2793-7087-42ff-a80f-f9ad7cc3e54e"
private const val VERSIONS_KEY = "versions"

private const val VERSION_DELIMITER = ";"

// This limit is based on the firebase/crashlytics message limit and
// gives us ~6 version names or ~10 version codes of tracking
private const val MAX_VALUE_LENGTH = 95
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
package com.simprints.infra.logging.usecases

import android.content.Context
import android.content.SharedPreferences
import com.google.common.truth.Truth.*
import io.mockk.*
import io.mockk.impl.annotations.MockK
import org.junit.Before
import org.junit.Test

class UpdateAndGetVersionHistoryTest {
@MockK
lateinit var mockContext: Context

@MockK
private lateinit var mockSharedPreferences: SharedPreferences

@MockK
private lateinit var mockEditor: SharedPreferences.Editor
private lateinit var updateAndGetVersionHistoryUseCase: UpdateAndGetVersionHistoryUseCase

private lateinit var slot: CapturingSlot<String>

@Before
fun setUp() {
MockKAnnotations.init(this, relaxed = true, relaxUnitFun = true)

every { mockContext.getSharedPreferences(any(), Context.MODE_PRIVATE) } returns mockSharedPreferences
every { mockSharedPreferences.edit() } returns mockEditor

slot = slot()
every { mockEditor.putString(any(), capture(slot)) } returns mockEditor

updateAndGetVersionHistoryUseCase = UpdateAndGetVersionHistoryUseCase()
}

@Test
fun `adds current version and returns it when cache is empty `() {
verifyCorrectCachedVersions(
currentVersion = "10010701",
cachedVersions = null,
expectedVersions = "10010701",
)
}

@Test
fun `returns existing versions without change when current version is already cached and is the first`() {
verifyCorrectCachedVersions(
currentVersion = "10010701",
cachedVersions = "10010701;10010601",
expectedVersions = "10010701;10010601",
shouldWriteCache = false,
)
}

@Test
fun `prepends current version when cache is not empty and current version is new`() {
verifyCorrectCachedVersions(
currentVersion = "10010701",
cachedVersions = "10010601;10010501",
expectedVersions = "10010701;10010601;10010501",
)
}

@Test
fun `truncates oldest versions when current version is new and adding it exceeds MAX_VALUE_LENGTH `() {
verifyCorrectCachedVersions(
currentVersion = "2025.3.0+107.1",
// Cached is already at the limit of the analytics keys
cachedVersions = "2025.2.0+107.1;2025.1.0+107.1;2024.4.0+107.1;2024.3.0+107.1;2024.2.0+107.1;2024.1.0+107.1",
expectedVersions = "2025.3.0+107.1;2025.2.0+107.1;2025.1.0+107.1;2024.4.0+107.1;2024.3.0+107.1;2024.2.0+107.1",
)
}

private fun verifyCorrectCachedVersions(
currentVersion: String,
cachedVersions: String?,
expectedVersions: String,
shouldWriteCache: Boolean = true,
) {
every { mockSharedPreferences.getString(any(), any()) } returns cachedVersions

val result = updateAndGetVersionHistoryUseCase(mockContext, currentVersion)

assertThat(result).isEqualTo(expectedVersions)
verify(exactly = if (shouldWriteCache) 1 else 0) { mockEditor.putString(any(), eq(expectedVersions)) }
}
}