From 5f37a68a1687b9ac91001398236bf6aa785bc2ab Mon Sep 17 00:00:00 2001 From: Egor Kitselyuk Date: Fri, 9 Oct 2026 11:12:07 +0300 Subject: [PATCH 1/2] MOBILE-389: Remove ids for removed providers --- kmp-common-sdk | 2 +- .../java/cloud/mindbox/mobile_sdk/Mindbox.kt | 5 ++ .../managers/TrackingIdsResolver.kt | 16 ++++- .../managers/TrackingIdsResolverTest.kt | 67 ++++++++++++++++++- 4 files changed, 85 insertions(+), 5 deletions(-) diff --git a/kmp-common-sdk b/kmp-common-sdk index bf69265f..f03f1897 160000 --- a/kmp-common-sdk +++ b/kmp-common-sdk @@ -1 +1 @@ -Subproject commit bf69265fec8f09bd93d7b8d10eac3a41320591dd +Subproject commit f03f1897a35519502cbe513adc8dc3d4a3a352a9 diff --git a/sdk/src/main/java/cloud/mindbox/mobile_sdk/Mindbox.kt b/sdk/src/main/java/cloud/mindbox/mobile_sdk/Mindbox.kt index 29525e1d..d354b9c5 100644 --- a/sdk/src/main/java/cloud/mindbox/mobile_sdk/Mindbox.kt +++ b/sdk/src/main/java/cloud/mindbox/mobile_sdk/Mindbox.kt @@ -96,6 +96,11 @@ public object Mindbox : MindboxLog { private var pushConverters: List by SingleInitDelegate() + internal val configuredTrackingIdTypes: Set? + get() = (pushConverters.filterIsInstance() + pushServiceHandlers) + .takeIf { it.isNotEmpty() } + ?.mapNotNullTo(mutableSetOf()) { it.trackingIdType } + private val inAppMessageManager: InAppMessageManager by mindboxInject { inAppMessageManager } private val getDeviceIdMutex = Mutex() diff --git a/sdk/src/main/java/cloud/mindbox/mobile_sdk/managers/TrackingIdsResolver.kt b/sdk/src/main/java/cloud/mindbox/mobile_sdk/managers/TrackingIdsResolver.kt index 33bba8a0..6f350060 100644 --- a/sdk/src/main/java/cloud/mindbox/mobile_sdk/managers/TrackingIdsResolver.kt +++ b/sdk/src/main/java/cloud/mindbox/mobile_sdk/managers/TrackingIdsResolver.kt @@ -3,6 +3,7 @@ package cloud.mindbox.mobile_sdk.managers import android.content.Context import cloud.mindbox.mobile_sdk.Mindbox import cloud.mindbox.mobile_sdk.logger.mindboxLogI +import cloud.mindbox.mobile_sdk.logger.mindboxLogW import cloud.mindbox.mobile_sdk.models.TrackingId import cloud.mindbox.mobile_sdk.pushes.PushServiceHandler import cloud.mindbox.mobile_sdk.pushes.TrackingIdResult @@ -29,6 +30,7 @@ internal interface TrackingIdsResolver { internal class TrackingIdsResolverImpl( private val pushServiceHandlers: () -> List = { Mindbox.pushServiceHandlers }, + private val configuredTrackingIdTypes: () -> Set? = { Mindbox.configuredTrackingIdTypes }, private val scope: () -> CoroutineScope = { Mindbox.mindboxScope }, private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO, private val timeoutMillis: Long = READ_TRACKING_IDS_TIMEOUT, @@ -46,7 +48,7 @@ internal class TrackingIdsResolverImpl( if (results == null) { reading.cancel() mindboxLogI("Timed out reading tracking id providers, repeating the last reported set") - return lastSent() + return lastSentOfConfiguredProviders() } return results.mergeWithLastSent() @@ -66,12 +68,22 @@ internal class TrackingIdsResolverImpl( ?: emptyList() } + private fun lastSentOfConfiguredProviders(): List { + val stored = lastSent() + val configured = configuredTrackingIdTypes() ?: return stored + val (kept, removed) = stored.partition { it.type in configured } + if (removed.isNotEmpty()) { + mindboxLogW("Erasing tracking ids of removed providers: ${removed.joinToString { it.type }}") + } + return kept + } + private fun List.mergeWithLastSent(): List { val fresh = filterIsInstance().map { it.trackingId } val answered = fresh.map { it.type }.toSet() + filterIsInstance().map { it.type } - val kept = lastSent().filterNot { it.type in answered } + val kept = lastSentOfConfiguredProviders().filterNot { it.type in answered } if (kept.isNotEmpty()) { mindboxLogI("Keeping tracking ids we could not read: ${kept.joinToString { it.type }}") } diff --git a/sdk/src/test/java/cloud/mindbox/mobile_sdk/managers/TrackingIdsResolverTest.kt b/sdk/src/test/java/cloud/mindbox/mobile_sdk/managers/TrackingIdsResolverTest.kt index 868e8ea8..f63612a8 100644 --- a/sdk/src/test/java/cloud/mindbox/mobile_sdk/managers/TrackingIdsResolverTest.kt +++ b/sdk/src/test/java/cloud/mindbox/mobile_sdk/managers/TrackingIdsResolverTest.kt @@ -49,15 +49,23 @@ class TrackingIdsResolverImplTest { unmockkObject(MindboxPreferences) } - private fun resolver(vararg results: TrackingIdResult) = TrackingIdsResolverImpl( + private fun resolver( + vararg results: TrackingIdResult, + configured: Set? = null, + ) = TrackingIdsResolverImpl( pushServiceHandlers = { results.map(::handlerReturning) }, + configuredTrackingIdTypes = { configured }, scope = { CoroutineScope(UnconfinedTestDispatcher()) }, ioDispatcher = UnconfinedTestDispatcher(), ) /** Reads on a real dispatcher so a blocking provider actually blocks. */ - private fun resolverWithHangingProvider(vararg fast: TrackingIdResult) = TrackingIdsResolverImpl( + private fun resolverWithHangingProvider( + vararg fast: TrackingIdResult, + configured: Set? = null, + ) = TrackingIdsResolverImpl( pushServiceHandlers = { fast.map(::handlerReturning) + handlerThatHangs() }, + configuredTrackingIdTypes = { configured }, scope = { CoroutineScope(Dispatchers.IO) }, ioDispatcher = Dispatchers.IO, timeoutMillis = 100L, @@ -251,6 +259,61 @@ class TrackingIdsResolverImplTest { assertEquals(listOf(googleId), payload) } + @Test + fun `a provider removed from the integration is erased`() = runTest { + lastSent = STORED_GOOGLE + + // Firebase was dropped, the new build passes RuStore only. + val payload = resolver(TrackingIdResult.NotSupported, configured = emptySet()).resolve(context) + + assertEquals(emptyList(), payload) + } + + @Test + fun `a removed provider is erased next to a fresh one`() = runTest { + lastSent = STORED_GOOGLE + + val payload = resolver(success(HUAWEI, HUAWEI_VALUE), configured = setOf(HUAWEI)).resolve(context) + + assertEquals(listOf(huaweiId), payload) + } + + @Test + fun `a removed provider is erased while the remaining one is unreadable`() = runTest { + lastSent = STORED_BOTH + + val payload = resolver(TrackingIdResult.Unavailable, configured = setOf(HUAWEI)).resolve(context) + + assertEquals(listOf(huaweiId), payload) + } + + @Test + fun `a configured provider without a handler keeps its value`() = runTest { + lastSent = STORED_GOOGLE + + // Handlers of services unavailable at startup are filtered out, or not set up yet. + val payload = resolver(configured = setOf(GOOGLE)).resolve(context) + + assertEquals(listOf(googleId), payload) + } + + @Test + fun `a hanging provider does not keep a removed one`() = runTest { + lastSent = STORED_BOTH + + val payload = resolverWithHangingProvider(configured = setOf(HUAWEI)).resolve(context) + + assertEquals(listOf(huaweiId), payload) + } + + @Test + fun `erasing a removed provider is a change`() = runTest { + lastSent = STORED_GOOGLE + val resolver = resolver(TrackingIdResult.NotSupported, configured = emptySet()) + + assertTrue(resolver.hasChanged(resolver.resolve(context))) + } + @Test fun `a fresh value replaces the stored one of the same type`() = runTest { lastSent = STORED_BOTH From 358a9d31738e8736328d4880c8bc95696ee1c7ec Mon Sep 17 00:00:00 2001 From: Egor Kitselyuk Date: Fri, 9 Oct 2026 17:23:19 +0300 Subject: [PATCH 2/2] MOBILE-389: Add tests --- .../java/cloud/mindbox/mobile_sdk/Mindbox.kt | 4 +- .../managers/TrackingIdsResolver.kt | 8 +++ .../managers/TrackingIdTypesOfTest.kt | 52 +++++++++++++++++++ 3 files changed, 61 insertions(+), 3 deletions(-) create mode 100644 sdk/src/test/java/cloud/mindbox/mobile_sdk/managers/TrackingIdTypesOfTest.kt diff --git a/sdk/src/main/java/cloud/mindbox/mobile_sdk/Mindbox.kt b/sdk/src/main/java/cloud/mindbox/mobile_sdk/Mindbox.kt index d354b9c5..837c8c59 100644 --- a/sdk/src/main/java/cloud/mindbox/mobile_sdk/Mindbox.kt +++ b/sdk/src/main/java/cloud/mindbox/mobile_sdk/Mindbox.kt @@ -97,9 +97,7 @@ public object Mindbox : MindboxLog { private var pushConverters: List by SingleInitDelegate() internal val configuredTrackingIdTypes: Set? - get() = (pushConverters.filterIsInstance() + pushServiceHandlers) - .takeIf { it.isNotEmpty() } - ?.mapNotNullTo(mutableSetOf()) { it.trackingIdType } + get() = trackingIdTypesOf(configured = pushConverters, live = pushServiceHandlers) private val inAppMessageManager: InAppMessageManager by mindboxInject { inAppMessageManager } diff --git a/sdk/src/main/java/cloud/mindbox/mobile_sdk/managers/TrackingIdsResolver.kt b/sdk/src/main/java/cloud/mindbox/mobile_sdk/managers/TrackingIdsResolver.kt index 6f350060..65320b04 100644 --- a/sdk/src/main/java/cloud/mindbox/mobile_sdk/managers/TrackingIdsResolver.kt +++ b/sdk/src/main/java/cloud/mindbox/mobile_sdk/managers/TrackingIdsResolver.kt @@ -5,6 +5,7 @@ import cloud.mindbox.mobile_sdk.Mindbox import cloud.mindbox.mobile_sdk.logger.mindboxLogI import cloud.mindbox.mobile_sdk.logger.mindboxLogW import cloud.mindbox.mobile_sdk.models.TrackingId +import cloud.mindbox.mobile_sdk.pushes.PushConverter import cloud.mindbox.mobile_sdk.pushes.PushServiceHandler import cloud.mindbox.mobile_sdk.pushes.TrackingIdResult import cloud.mindbox.mobile_sdk.repository.MindboxPreferences @@ -19,6 +20,13 @@ import kotlin.time.Duration.Companion.milliseconds private const val READ_TRACKING_IDS_TIMEOUT = 5000L +internal fun trackingIdTypesOf( + configured: List, + live: List, +): Set? = (configured.filterIsInstance() + live) + .takeIf { it.isNotEmpty() } + ?.mapNotNullTo(mutableSetOf()) { it.trackingIdType } + internal interface TrackingIdsResolver { suspend fun resolve(context: Context): List diff --git a/sdk/src/test/java/cloud/mindbox/mobile_sdk/managers/TrackingIdTypesOfTest.kt b/sdk/src/test/java/cloud/mindbox/mobile_sdk/managers/TrackingIdTypesOfTest.kt new file mode 100644 index 00000000..8c617d01 --- /dev/null +++ b/sdk/src/test/java/cloud/mindbox/mobile_sdk/managers/TrackingIdTypesOfTest.kt @@ -0,0 +1,52 @@ +package cloud.mindbox.mobile_sdk.managers + +import cloud.mindbox.mobile_sdk.pushes.PushServiceHandler +import io.mockk.every +import io.mockk.mockk +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNull +import org.junit.Test + +class TrackingIdTypesOfTest { + + private fun handler(trackingIdType: String?): PushServiceHandler = + mockk().also { every { it.trackingIdType } returns trackingIdType } + + private val firebase = handler("google") + private val huawei = handler("huawei") + private val ruStore = handler(null) + + @Test + fun `nothing passed to init yet leaves the configuration unknown`() { + assertNull(trackingIdTypesOf(configured = emptyList(), live = emptyList())) + } + + @Test + fun `every passed push service with a tracking id is configured`() { + val types = trackingIdTypesOf(configured = listOf(firebase, huawei, ruStore), live = emptyList()) + + assertEquals(setOf("google", "huawei"), types) + } + + @Test + fun `a RuStore-only integration configures no tracking id types`() { + val types = trackingIdTypesOf(configured = listOf(ruStore), live = listOf(ruStore)) + + assertEquals(emptySet(), types) + } + + @Test + fun `a service filtered out as unavailable at startup stays configured`() { + val types = trackingIdTypesOf(configured = listOf(firebase), live = emptyList()) + + assertEquals(setOf("google"), types) + } + + @Test + fun `a live handler counts even if it came from another init call`() { + // initPushServices and init passed different lists; handlers were built from the second one. + val types = trackingIdTypesOf(configured = listOf(firebase), live = listOf(firebase, huawei)) + + assertEquals(setOf("google", "huawei"), types) + } +}