Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix ending an already ended session #2185

Merged
merged 2 commits into from
Sep 5, 2024
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
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ import androidx.annotation.WorkerThread

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

/**
* Run the background service
* Run the background service.
* WARNING: This may not follow your scheduleBackgroundRunIn schedule:
* 1. May run more often as the lowest scheduleBackgroundRunIn
* value is used across the SDK.
* 2. Android doesn't guarantee exact timing on when the job is run,
* so it's possible for it to be delayed by a few minutes.
*/
@WorkerThread
suspend fun backgroundRun()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import com.onesignal.session.internal.influence.Influence
import com.onesignal.session.internal.influence.InfluenceChannel
import com.onesignal.session.internal.influence.InfluenceType
import com.onesignal.session.internal.influence.InfluenceType.Companion.fromString
import com.onesignal.session.internal.outcomes.migrations.RemoveZeroSessionTimeRecords
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.json.JSONArray
Expand Down Expand Up @@ -101,6 +102,7 @@ internal class OutcomeEventsRepository(
override suspend fun getAllEventsToSend(): List<OutcomeEventParams> {
val events: MutableList<OutcomeEventParams> = ArrayList()
withContext(Dispatchers.IO) {
RemoveZeroSessionTimeRecords.run(_databaseProvider)
_databaseProvider.os.query(OutcomeEventsTable.TABLE_NAME) { cursor ->
if (cursor.moveToFirst()) {
do {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package com.onesignal.session.internal.outcomes.migrations

import com.onesignal.core.internal.database.IDatabaseProvider
import com.onesignal.session.internal.outcomes.impl.OutcomeEventsTable

/**
* Purpose: Clean up invalid cached os__session_duration outcome records
* with zero session_time produced in SDK versions 5.1.15 to 5.1.20 so we stop
* sending these requests to the backend.
*
* Issue: SessionService.backgroundRun() didn't account for it being run more
* than one time in the background, when this happened it would create a
* outcome record with zero time which is invalid.
*/
object RemoveZeroSessionTimeRecords {
fun run(databaseProvider: IDatabaseProvider) {
databaseProvider.os.delete(
OutcomeEventsTable.TABLE_NAME,
OutcomeEventsTable.COLUMN_NAME_NAME + " = \"os__session_duration\"" +
" AND " + OutcomeEventsTable.COLUMN_NAME_SESSION_TIME + " = 0",
null,
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,10 @@ class SessionModel : Model() {
}

/**
* Whether the session is valid.
* Indicates if there is an active session.
* True when app is in the foreground.
* Also true in the background for a short period of time (default 30s)
* as a debouncing mechanism.
*/
var isValid: Boolean
get() = getBooleanProperty(::isValid.name) { false }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,18 +47,28 @@ internal class SessionService(
private var config: ConfigModel? = null
private var shouldFireOnSubscribe = false

// True if app has been foregrounded at least once since the app started
private var hasFocused = false

override fun start() {
session = _sessionModelStore.model
config = _configModelStore.model
// Reset the session validity property to drive a new session
session!!.isValid = false
_applicationService.addApplicationLifecycleHandler(this)
}

/** NOTE: This triggers more often than scheduleBackgroundRunIn defined above,
* as it runs on the lowest IBackgroundService.scheduleBackgroundRunIn across
* the SDK.
*/
override suspend fun backgroundRun() {
endSession()
}

private fun endSession() {
if (!session!!.isValid) return
val activeDuration = session!!.activeDuration
// end the session
Logging.debug("SessionService.backgroundRun: Session ended. activeDuration: $activeDuration")

session!!.isValid = false
sessionLifeCycleNotifier.fire { it.onSessionEnded(activeDuration) }
session!!.activeDuration = 0L
Expand All @@ -75,14 +85,20 @@ internal class SessionService(
*/
override fun onFocus(firedOnSubscribe: Boolean) {
Logging.log(LogLevel.DEBUG, "SessionService.onFocus() - fired from start: $firedOnSubscribe")

// Treat app cold starts as a new session, we attempt to end any previous session to do this.
if (!hasFocused) {
hasFocused = true
endSession()
}

if (!session!!.isValid) {
// As the old session was made inactive, we need to create a new session
shouldFireOnSubscribe = firedOnSubscribe
session!!.sessionId = UUID.randomUUID().toString()
session!!.startTime = _time.currentTimeMillis
session!!.focusTime = session!!.startTime
session!!.isValid = true

Logging.debug("SessionService: New session started at ${session!!.startTime}")
sessionLifeCycleNotifier.fire { it.onSessionStarted() }
} else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,10 +86,10 @@ class SessionServiceTests : FunSpec({

// Then
sessionModelStore.model.isValid shouldBe true
sessionModelStore.model.startTime shouldBe startTime
sessionModelStore.model.startTime shouldBe mocks.currentTime
sessionModelStore.model.focusTime shouldBe mocks.currentTime
verify(exactly = 1) { mocks.spyCallback.onSessionActive() }
verify(exactly = 0) { mocks.spyCallback.onSessionStarted() }
verify(exactly = 0) { mocks.spyCallback.onSessionActive() }
verify(exactly = 1) { mocks.spyCallback.onSessionStarted() }
}

test("session active duration updated when unfocused") {
Expand Down Expand Up @@ -140,4 +140,19 @@ class SessionServiceTests : FunSpec({
sessionModelStore.model.isValid shouldBe false
verify(exactly = 1) { mocks.spyCallback.onSessionEnded(activeDuration) }
}

test("do not trigger onSessionEnd if session is not active") {
// Given
val mocks = Mocks()
mocks.sessionModelStore { it.isValid = false }
val sessionService = mocks.sessionService
sessionService.subscribe(mocks.spyCallback)
sessionService.start()

// When
sessionService.backgroundRun()

// Then
verify(exactly = 0) { mocks.spyCallback.onSessionEnded(any()) }
}
})
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,10 @@ internal class LocationBackgroundService(
return scheduleTime
}

/** NOTE: This triggers more often than scheduleBackgroundRunIn defined above,
* as it runs on the lowest IBackgroundService.scheduleBackgroundRunIn across
* the SDK.
*/
override suspend fun backgroundRun() {
_capturer.captureLastLocation()
}
Expand Down
Loading