Skip to content

Commit 3d3c5f2

Browse files
committed
fix ending an already ended session
# What Fixes an issue where invalid REST API requests were made to both outcomes/measure and User updates with session_time were bing made in the background. This is only present when location sharing is enabled in the SDK (in addition to the app and end-user allowing it) and the end-user opens the app at any point after receiving a notification. This resulted in 400 errors in the outcomes/measure case and no-op User updates that wasting resources, the later doesn't depend on receiving notifications. The outcomes/measure is also additive as the bad requests are cached and retried on app open. # How Add a state check to SessionService to prevent ending an already ended session. Also address a previous session never ending correctly when the app is cold started. Lastly, added comments pointing out the caveats of IBackgroundService to help prevent future wrong assumptions about when backgroundRun() is run. # Follow up Since there are cached invalid outcomes/measure requests a migration needs to be created in the SDK to remove them for apps who were affected by the bug.
1 parent 54ab98e commit 3d3c5f2

File tree

5 files changed

+55
-12
lines changed

5 files changed

+55
-12
lines changed

OneSignalSDK/onesignal/core/src/main/java/com/onesignal/core/internal/background/IBackgroundService.kt

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@ import androidx.annotation.WorkerThread
44

55
/**
66
* Implement and provide this interface as part of service registration to indicate the service
7-
* wants to be instantiated and its [backgroundRun] function called when the app is in the background. The
8-
* background process is initiated when the application is no longer in focus. Each background
9-
* service's [scheduleBackgroundRunIn] will be analyzed to determine when [backgroundRun] should be called.
7+
* wants to be instantiated and its [backgroundRun] function called when the app is in the background.
8+
* Each background service's [scheduleBackgroundRunIn] will be analyzed to determine when
9+
* [backgroundRun] should be called.
1010
*/
1111
interface IBackgroundService {
1212
/**
@@ -16,7 +16,12 @@ interface IBackgroundService {
1616
val scheduleBackgroundRunIn: Long?
1717

1818
/**
19-
* Run the background service
19+
* Run the background service.
20+
* WARNING: This may not follow your scheduleBackgroundRunIn schedule:
21+
* 1. May run more often as the lowest scheduleBackgroundRunIn
22+
* value is used across the SDK.
23+
* 2. Android doesn't guarantee exact timing on when the job is run,
24+
* so it's possible for it to be delayed by a few minutes.
2025
*/
2126
@WorkerThread
2227
suspend fun backgroundRun()

OneSignalSDK/onesignal/core/src/main/java/com/onesignal/session/internal/session/SessionModel.kt

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,10 @@ class SessionModel : Model() {
1616
}
1717

1818
/**
19-
* Whether the session is valid.
19+
* Indicates if there is an active session.
20+
* True when app is in the foreground.
21+
* Also true in the background for a short period of time (default 30s)
22+
* as a debouncing mechanism.
2023
*/
2124
var isValid: Boolean
2225
get() = getBooleanProperty(::isValid.name) { false }

OneSignalSDK/onesignal/core/src/main/java/com/onesignal/session/internal/session/impl/SessionService.kt

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -47,18 +47,28 @@ internal class SessionService(
4747
private var config: ConfigModel? = null
4848
private var shouldFireOnSubscribe = false
4949

50+
// True if app has been foregrounded at least once since the app started
51+
private var hasFocused = false
52+
5053
override fun start() {
5154
session = _sessionModelStore.model
5255
config = _configModelStore.model
53-
// Reset the session validity property to drive a new session
54-
session!!.isValid = false
5556
_applicationService.addApplicationLifecycleHandler(this)
5657
}
5758

59+
/** NOTE: This triggers more often than scheduleBackgroundRunIn defined above,
60+
* as it runs on the lowest IBackgroundService.scheduleBackgroundRunIn across
61+
* the SDK.
62+
*/
5863
override suspend fun backgroundRun() {
64+
endSession()
65+
}
66+
67+
private fun endSession() {
68+
if (!session!!.isValid) return
5969
val activeDuration = session!!.activeDuration
60-
// end the session
6170
Logging.debug("SessionService.backgroundRun: Session ended. activeDuration: $activeDuration")
71+
6272
session!!.isValid = false
6373
sessionLifeCycleNotifier.fire { it.onSessionEnded(activeDuration) }
6474
session!!.activeDuration = 0L
@@ -75,14 +85,20 @@ internal class SessionService(
7585
*/
7686
override fun onFocus(firedOnSubscribe: Boolean) {
7787
Logging.log(LogLevel.DEBUG, "SessionService.onFocus() - fired from start: $firedOnSubscribe")
88+
89+
// Treat app cold starts as a new session, we attempt to end any previous session to do this.
90+
if (!hasFocused) {
91+
hasFocused = true
92+
endSession()
93+
}
94+
7895
if (!session!!.isValid) {
7996
// As the old session was made inactive, we need to create a new session
8097
shouldFireOnSubscribe = firedOnSubscribe
8198
session!!.sessionId = UUID.randomUUID().toString()
8299
session!!.startTime = _time.currentTimeMillis
83100
session!!.focusTime = session!!.startTime
84101
session!!.isValid = true
85-
86102
Logging.debug("SessionService: New session started at ${session!!.startTime}")
87103
sessionLifeCycleNotifier.fire { it.onSessionStarted() }
88104
} else {

OneSignalSDK/onesignal/core/src/test/java/com/onesignal/session/internal/session/SessionServiceTests.kt

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -86,10 +86,10 @@ class SessionServiceTests : FunSpec({
8686

8787
// Then
8888
sessionModelStore.model.isValid shouldBe true
89-
sessionModelStore.model.startTime shouldBe startTime
89+
sessionModelStore.model.startTime shouldBe mocks.currentTime
9090
sessionModelStore.model.focusTime shouldBe mocks.currentTime
91-
verify(exactly = 1) { mocks.spyCallback.onSessionActive() }
92-
verify(exactly = 0) { mocks.spyCallback.onSessionStarted() }
91+
verify(exactly = 0) { mocks.spyCallback.onSessionActive() }
92+
verify(exactly = 1) { mocks.spyCallback.onSessionStarted() }
9393
}
9494

9595
test("session active duration updated when unfocused") {
@@ -140,4 +140,19 @@ class SessionServiceTests : FunSpec({
140140
sessionModelStore.model.isValid shouldBe false
141141
verify(exactly = 1) { mocks.spyCallback.onSessionEnded(activeDuration) }
142142
}
143+
144+
test("do not trigger onSessionEnd if session is not active") {
145+
// Given
146+
val mocks = Mocks()
147+
mocks.sessionModelStore { it.isValid = false }
148+
val sessionService = mocks.sessionService
149+
sessionService.subscribe(mocks.spyCallback)
150+
sessionService.start()
151+
152+
// When
153+
sessionService.backgroundRun()
154+
155+
// Then
156+
verify(exactly = 0) { mocks.spyCallback.onSessionEnded(any()) }
157+
}
143158
})

OneSignalSDK/onesignal/location/src/main/java/com/onesignal/location/internal/background/LocationBackgroundService.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,10 @@ internal class LocationBackgroundService(
3535
return scheduleTime
3636
}
3737

38+
/** NOTE: This triggers more often than scheduleBackgroundRunIn defined above,
39+
* as it runs on the lowest IBackgroundService.scheduleBackgroundRunIn across
40+
* the SDK.
41+
*/
3842
override suspend fun backgroundRun() {
3943
_capturer.captureLastLocation()
4044
}

0 commit comments

Comments
 (0)