diff --git a/OneSignalSDK/onesignal/in-app-messages/src/main/java/com/onesignal/inAppMessages/internal/triggers/impl/DynamicTriggerController.kt b/OneSignalSDK/onesignal/in-app-messages/src/main/java/com/onesignal/inAppMessages/internal/triggers/impl/DynamicTriggerController.kt index b7dd35afd7..a37aa6a402 100644 --- a/OneSignalSDK/onesignal/in-app-messages/src/main/java/com/onesignal/inAppMessages/internal/triggers/impl/DynamicTriggerController.kt +++ b/OneSignalSDK/onesignal/in-app-messages/src/main/java/com/onesignal/inAppMessages/internal/triggers/impl/DynamicTriggerController.kt @@ -75,10 +75,12 @@ internal class DynamicTriggerController( return true } - val offset = requiredTimeInterval - currentTimeInterval - if (offset <= 0L) { + // Compare before subtracting: with an unbounded time since the last in-app + // message, a negative required interval would overflow the offset. + if (currentTimeInterval >= requiredTimeInterval) { return false } + val offset = requiredTimeInterval - currentTimeInterval // Prevents re-scheduling timers for messages that we're already waiting on if (scheduledMessages.contains(triggerId)) { @@ -149,8 +151,8 @@ internal class DynamicTriggerController( companion object { private const val REQUIRED_ACCURACY = 0.3 - // Assume last time an In-App Message was displayed a very very long time ago. - private const val DEFAULT_LAST_IN_APP_TIME_AGO: Long = 999999 + // If no In-App Message has been dismissed yet, treat the last one as infinitely long ago. + private const val DEFAULT_LAST_IN_APP_TIME_AGO: Long = Long.MAX_VALUE } override fun subscribe(handler: ITriggerHandler) = events.subscribe(handler) diff --git a/OneSignalSDK/onesignal/in-app-messages/src/test/java/com/onesignal/inAppMessages/internal/triggers/impl/DynamicTriggerControllerTests.kt b/OneSignalSDK/onesignal/in-app-messages/src/test/java/com/onesignal/inAppMessages/internal/triggers/impl/DynamicTriggerControllerTests.kt new file mode 100644 index 0000000000..5dee068eb2 --- /dev/null +++ b/OneSignalSDK/onesignal/in-app-messages/src/test/java/com/onesignal/inAppMessages/internal/triggers/impl/DynamicTriggerControllerTests.kt @@ -0,0 +1,64 @@ +package com.onesignal.inAppMessages.internal.triggers.impl + +import com.onesignal.inAppMessages.internal.Trigger +import com.onesignal.inAppMessages.internal.state.InAppStateService +import com.onesignal.mocks.MockHelper +import com.onesignal.session.internal.session.ISessionService +import io.kotest.core.spec.style.FunSpec +import io.kotest.matchers.shouldBe +import io.mockk.mockk +import org.json.JSONObject + +private fun timeSinceLastInAppTrigger( + seconds: Number, + operator: Trigger.OSTriggerOperator = Trigger.OSTriggerOperator.GREATER_THAN +): Trigger { + val json = JSONObject() + json.put("id", "trigger-id") + json.put("kind", Trigger.OSTriggerKind.TIME_SINCE_LAST_IN_APP.toString()) + json.put("operator", operator.toString()) + json.put("value", seconds) + return Trigger(json) +} + +class DynamicTriggerControllerTests : FunSpec({ + val oneHourInSeconds = 3600 + + test("time since last in-app trigger fires when no in-app message was dismissed yet") { + // Given + val state = InAppStateService() + val controller = DynamicTriggerController(state, mockk(relaxed = true), MockHelper.time(1_000_000)) + + // When + val result = controller.dynamicTriggerShouldFire(timeSinceLastInAppTrigger(oneHourInSeconds)) + + // Then + result shouldBe true + } + + test("time since last in-app trigger does not fire before the interval has elapsed") { + // Given + val now = 10_000_000L + val state = InAppStateService() + state.lastTimeInAppDismissed = now - 60_000L + val controller = DynamicTriggerController(state, mockk(relaxed = true), MockHelper.time(now)) + + // When + val result = controller.dynamicTriggerShouldFire(timeSinceLastInAppTrigger(oneHourInSeconds)) + + // Then + result shouldBe false + } + + test("time since last in-app trigger with a negative less-than value does not fire when no in-app message was dismissed yet") { + // Given + val state = InAppStateService() + val controller = DynamicTriggerController(state, mockk(relaxed = true), MockHelper.time(1_000_000)) + + // When + val result = controller.dynamicTriggerShouldFire(timeSinceLastInAppTrigger(-1, Trigger.OSTriggerOperator.LESS_THAN)) + + // Then + result shouldBe false + } +})