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
2 changes: 1 addition & 1 deletion kmp-common-sdk
Submodule kmp-common-sdk updated 0 files
3 changes: 3 additions & 0 deletions sdk/src/main/java/cloud/mindbox/mobile_sdk/Mindbox.kt
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,9 @@ public object Mindbox : MindboxLog {

private var pushConverters: List<PushConverter> by SingleInitDelegate()

internal val configuredTrackingIdTypes: Set<String>?
Comment thread
enotniy marked this conversation as resolved.
get() = trackingIdTypesOf(configured = pushConverters, live = pushServiceHandlers)

private val inAppMessageManager: InAppMessageManager by mindboxInject { inAppMessageManager }

private val getDeviceIdMutex = Mutex()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ 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.PushConverter
import cloud.mindbox.mobile_sdk.pushes.PushServiceHandler
import cloud.mindbox.mobile_sdk.pushes.TrackingIdResult
import cloud.mindbox.mobile_sdk.repository.MindboxPreferences
Expand All @@ -18,6 +20,13 @@ import kotlin.time.Duration.Companion.milliseconds

private const val READ_TRACKING_IDS_TIMEOUT = 5000L

internal fun trackingIdTypesOf(
configured: List<PushConverter>,
live: List<PushServiceHandler>,
): Set<String>? = (configured.filterIsInstance<PushServiceHandler>() + live)
.takeIf { it.isNotEmpty() }
?.mapNotNullTo(mutableSetOf()) { it.trackingIdType }

internal interface TrackingIdsResolver {

suspend fun resolve(context: Context): List<TrackingId>
Expand All @@ -29,6 +38,7 @@ internal interface TrackingIdsResolver {

internal class TrackingIdsResolverImpl(
private val pushServiceHandlers: () -> List<PushServiceHandler> = { Mindbox.pushServiceHandlers },
private val configuredTrackingIdTypes: () -> Set<String>? = { Mindbox.configuredTrackingIdTypes },
private val scope: () -> CoroutineScope = { Mindbox.mindboxScope },
private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO,
private val timeoutMillis: Long = READ_TRACKING_IDS_TIMEOUT,
Expand All @@ -46,7 +56,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()
Expand All @@ -66,12 +76,22 @@ internal class TrackingIdsResolverImpl(
?: emptyList()
}

private fun lastSentOfConfiguredProviders(): List<TrackingId> {
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<TrackingIdResult>.mergeWithLastSent(): List<TrackingId> {
val fresh = filterIsInstance<TrackingIdResult.Success>().map { it.trackingId }
val answered = fresh.map { it.type }.toSet() +
filterIsInstance<TrackingIdResult.Denied>().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 }}")
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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<PushServiceHandler>().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<String>(), 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)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -49,15 +49,23 @@ class TrackingIdsResolverImplTest {
unmockkObject(MindboxPreferences)
}

private fun resolver(vararg results: TrackingIdResult) = TrackingIdsResolverImpl(
private fun resolver(
vararg results: TrackingIdResult,
configured: Set<String>? = 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<String>? = null,
) = TrackingIdsResolverImpl(
pushServiceHandlers = { fast.map(::handlerReturning) + handlerThatHangs() },
configuredTrackingIdTypes = { configured },
scope = { CoroutineScope(Dispatchers.IO) },
ioDispatcher = Dispatchers.IO,
timeoutMillis = 100L,
Expand Down Expand Up @@ -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<TrackingId>(), 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
Expand Down
Loading