Skip to content
4 changes: 0 additions & 4 deletions OneSignalSDK/detekt/detekt-baseline-core.xml
Original file line number Diff line number Diff line change
Expand Up @@ -120,10 +120,6 @@
<ID>ConstructorParameterNaming:SessionListener.kt$SessionListener$private val _propertiesModelStore: PropertiesModelStore</ID>
<ID>ConstructorParameterNaming:SessionListener.kt$SessionListener$private val _sessionService: ISessionService</ID>
<ID>ConstructorParameterNaming:SessionManager.kt$SessionManager$private val _outcomeController: IOutcomeEventsController</ID>
<ID>ConstructorParameterNaming:SessionService.kt$SessionService$private val _applicationService: IApplicationService</ID>
<ID>ConstructorParameterNaming:SessionService.kt$SessionService$private val _configModelStore: ConfigModelStore</ID>
<ID>ConstructorParameterNaming:SessionService.kt$SessionService$private val _sessionModelStore: SessionModelStore</ID>
<ID>ConstructorParameterNaming:SessionService.kt$SessionService$private val _time: ITime</ID>
<ID>ConstructorParameterNaming:SimpleModelStore.kt$SimpleModelStore$/** * Will be called whenever a new [TModel] needs to be instantiated. */ private val _create: () -> TModel</ID>
<ID>ConstructorParameterNaming:SimpleModelStore.kt$SimpleModelStore$_prefs: IPreferencesService? = null</ID>
<ID>ConstructorParameterNaming:SubscriptionBackendService.kt$SubscriptionBackendService$private val _httpClient: IHttpClient</ID>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,10 @@ interface ITime {
* Returns how long the app has been running.
*/
val processUptimeMillis: Long

/**
* Milliseconds since boot, including deep sleep. Monotonic, so safe for measuring
* durations when the wall clock is changed. Resets on reboot.
*/
val elapsedRealtimeMillis: Long
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,6 @@ internal class Time : ITime {
override val processUptimeMillis: Long
@RequiresApi(Build.VERSION_CODES.N)
get() = SystemClock.uptimeMillis() - android.os.Process.getStartUptimeMillis()
override val elapsedRealtimeMillis: Long
get() = SystemClock.elapsedRealtime()
}
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ import com.onesignal.user.UserModule
import com.onesignal.user.internal.LoginHelper
import com.onesignal.user.internal.LogoutHelper
import com.onesignal.user.internal.UserSwitcher
import com.onesignal.user.internal.applyResolvedAppId
import com.onesignal.user.internal.identity.IdentityModelStore
import com.onesignal.user.internal.jwt.JwtTokenStore
import com.onesignal.user.internal.properties.PropertiesModelStore
Expand Down Expand Up @@ -441,7 +442,7 @@ internal class OneSignalImp : IOneSignal,
completeInit(InitState.FAILED)
return false
}
configModel.appId = result.appId!! // safe because failed is false
applyResolvedAppId(configModel, result.appId!!) // safe because failed is false
val forceCreateUser = result.forceCreateUser

updateConfig()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,4 +54,54 @@ class SessionModel : Model() {
set(value) {
setLongProperty(::activeDuration.name, value)
}

/**
* [focusTime] from a monotonic clock ([com.onesignal.core.internal.time.ITime.elapsedRealtimeMillis]).
* Used instead of [focusTime] to measure [activeDuration] when [usesSessionsApi].
*/
var focusElapsedRealtime: Long
get() = getLongProperty(::focusElapsedRealtime.name) { 0L }
set(value) {
setLongProperty(::focusElapsedRealtime.name, value)
}

/**
* Whether this session reports through the sessions API instead of the legacy session paths.
* Decided by [com.onesignal.features.FeatureFlag.SDK_SESSIONS_V2_API_CUTOVER] when the session
* starts and fixed until it ends, so a session never mixes paths. Read this rather than the
* feature manager, whose value can change mid-session.
*/
var usesSessionsApi: Boolean
get() = getBooleanProperty(::usesSessionsApi.name) { false }
set(value) {
setBooleanProperty(::usesSessionsApi.name, value)
}

/**
* The OneSignal ID when this session started. Session updates keep using it after a
* login or user switch.
*/
var onesignalId: String?
get() = getOptStringProperty(::onesignalId.name)
set(value) {
setOptStringProperty(::onesignalId.name, value)
}

/**
* The push subscription ID when this session started. Pinned like [onesignalId].
*/
var subscriptionId: String?
get() = getOptStringProperty(::subscriptionId.name)
set(value) {
setOptStringProperty(::subscriptionId.name, value)
}

/**
* Null until the sessions API creates this session. Cleared when a new session starts.
*/
var serverSessionId: String?
get() = getOptStringProperty(::serverSessionId.name)
set(value) {
setOptStringProperty(::serverSessionId.name, value)
}
}
Original file line number Diff line number Diff line change
@@ -1,22 +1,31 @@
package com.onesignal.session.internal.session.impl

import com.onesignal.common.IDManager
import com.onesignal.common.events.EventProducer
import com.onesignal.common.modeling.ISingletonModelStoreChangeHandler
import com.onesignal.common.modeling.Model
import com.onesignal.common.modeling.ModelChangedArgs
import com.onesignal.common.threading.runOnSerialIO
import com.onesignal.core.internal.application.IApplicationLifecycleHandler
import com.onesignal.core.internal.application.IApplicationService
import com.onesignal.core.internal.background.IBackgroundService
import com.onesignal.core.internal.config.ConfigModel
import com.onesignal.core.internal.config.ConfigModelStore
import com.onesignal.core.internal.features.IFeatureManager
import com.onesignal.core.internal.startup.IBootstrapService
import com.onesignal.core.internal.startup.IStartableService
import com.onesignal.core.internal.time.ITime
import com.onesignal.debug.LogLevel
import com.onesignal.debug.internal.logging.Logging
import com.onesignal.features.FeatureFlag
import com.onesignal.session.internal.session.ISessionLifecycleHandler
import com.onesignal.session.internal.session.ISessionService
import com.onesignal.session.internal.session.SessionModel
import com.onesignal.session.internal.session.SessionModelStore
import com.onesignal.user.internal.backend.IdentityConstants
import com.onesignal.user.internal.identity.IdentityModelStore
import java.util.UUID
import kotlin.reflect.KMutableProperty1

/**
* The implementation for [ISessionService] will continue a session as long as the app remains
Expand All @@ -30,16 +39,18 @@ import java.util.UUID
* The time threshold for a session to expire is a configuration option: [ConfigModel.sessionFocusTimeout].
*/
internal class SessionService(
private val _applicationService: IApplicationService,
private val _configModelStore: ConfigModelStore,
private val _sessionModelStore: SessionModelStore,
private val _time: ITime,
private val applicationService: IApplicationService,
private val configModelStore: ConfigModelStore,
private val sessionModelStore: SessionModelStore,
private val time: ITime,
private val featureManager: IFeatureManager,
Comment thread
fadi-george marked this conversation as resolved.
private val identityModelStore: IdentityModelStore,
) : ISessionService, IBootstrapService, IStartableService, IBackgroundService, IApplicationLifecycleHandler {
override val startTime: Long
// Pre-bootstrap default returns "now" so call sites computing `_time.currentTimeMillis - startTime`
// (e.g. IAM session-duration / SESSION_TIME triggers) see ~0ms elapsed instead of ~58 years
// (which is what `0L` / Jan 1970 would produce).
get() = session?.startTime ?: _time.currentTimeMillis
get() = session?.startTime ?: time.currentTimeMillis

/**
* Run in the background when the session would time out, only if a session is currently active.
Expand All @@ -64,12 +75,53 @@ internal class SessionService(
private var hasFocused = false

override fun bootstrap() {
session = _sessionModelStore.model
config = _configModelStore.model
session = sessionModelStore.model
config = configModelStore.model
}

override fun start() {
_applicationService.addApplicationLifecycleHandler(this)
applicationService.addApplicationLifecycleHandler(this)
identityModelStore.subscribe(PinnedLocalIdTranslator(IdentityConstants.ONESIGNAL_ID, SessionModel::onesignalId))
Comment thread
fadi-george marked this conversation as resolved.
// Login before the anonymous user is created carries the local push subscription over to
// the new user, so its backend ID belongs to that user, not the one pinned on this session.
configModelStore.subscribe(
PinnedLocalIdTranslator(ConfigModel::pushSubscriptionId.name, SessionModel::subscriptionId) { session ->
val identityModel = identityModelStore.model
val currentOnesignalId = if (identityModel.hasProperty(IdentityConstants.ONESIGNAL_ID)) identityModel.onesignalId else null
session.onesignalId == currentOnesignalId
},
)
}

/**
* A session that starts before the user is created pins local IDs. When the backend replaces
* a local ID in place, carry it over to the pinned copy. Changes from a non-local ID, such as a
* login or user switch, leave the pinned IDs alone.
*/
private inner class PinnedLocalIdTranslator<TModel : Model>(
private val sourceProperty: String,
private val pinnedId: KMutableProperty1<SessionModel, String?>,
private val appliesTo: (SessionModel) -> Boolean = { true },
) : ISingletonModelStoreChangeHandler<TModel> {
override fun onModelReplaced(
model: TModel,
tag: String,
) = Unit

override fun onModelUpdated(
args: ModelChangedArgs,
tag: String,
) {
val localId =
(args.oldValue as? String)?.takeIf { args.property == sourceProperty && IDManager.isLocalId(it) }
val backendId = (args.newValue as? String)?.takeUnless { IDManager.isLocalId(it) }
if (localId == null || backendId == null) return

runOnSerialIO {
val session = this@SessionService.session ?: return@runOnSerialIO
if (pinnedId.get(session) == localId && appliesTo(session)) pinnedId.set(session, backendId)
}
}
}

/** NOTE: This triggers more often than scheduleBackgroundRunIn defined above,
Expand Down Expand Up @@ -106,15 +158,17 @@ internal class SessionService(
override fun onFocus(firedOnSubscribe: Boolean) {
// Capture focus time on the caller's thread so session timestamps reflect lifecycle
// arrival, not dispatcher latency (SDK-4506).
val focusTimeMs = _time.currentTimeMillis
val focusTimeMs = time.currentTimeMillis
val focusElapsedMs = time.elapsedRealtimeMillis
runOnSerialIO {
handleOnFocus(firedOnSubscribe, focusTimeMs)
handleOnFocus(firedOnSubscribe, focusTimeMs, focusElapsedMs)
}
}

private fun handleOnFocus(
firedOnSubscribe: Boolean,
focusTimeMs: Long,
focusElapsedMs: Long,
) {
Logging.log(LogLevel.DEBUG, "SessionService.onFocus() - fired from start: $firedOnSubscribe")

Expand All @@ -136,33 +190,52 @@ internal class SessionService(
session.sessionId = UUID.randomUUID().toString()
session.startTime = focusTimeMs
session.focusTime = session.startTime
session.focusElapsedRealtime = focusElapsedMs
session.usesSessionsApi = featureManager.isEnabled(FeatureFlag.SDK_SESSIONS_V2_API_CUTOVER)
Comment thread
fadi-george marked this conversation as resolved.
session.onesignalId =
identityModelStore.model
.takeIf { it.hasProperty(IdentityConstants.ONESIGNAL_ID) }
?.onesignalId
session.subscriptionId = config?.pushSubscriptionId
session.serverSessionId = null
session.isValid = true
Logging.debug("SessionService: New session started at ${session.startTime}")
sessionLifeCycleNotifier.fire { it.onSessionStarted() }
} else {
// existing session: just remember the focus time so we can calculate the active time
// when onUnfocused is called.
session.focusTime = focusTimeMs
session.focusElapsedRealtime = focusElapsedMs
Comment thread
fadi-george marked this conversation as resolved.
sessionLifeCycleNotifier.fire { it.onSessionActive() }
}
}

override fun onUnfocused() {
// Capture on the caller's thread so activeDuration is unaffected by dispatcher latency.
val unfocusTimeMs = _time.currentTimeMillis
val unfocusTimeMs = time.currentTimeMillis
val unfocusElapsedMs = time.elapsedRealtimeMillis
runOnSerialIO {
handleOnUnfocused(unfocusTimeMs)
handleOnUnfocused(unfocusTimeMs, unfocusElapsedMs)
}
}

private fun handleOnUnfocused(unfocusTimeMs: Long) {
private fun handleOnUnfocused(
unfocusTimeMs: Long,
unfocusElapsedMs: Long,
) {
val session = this.session
if (session == null) {
Logging.warn("SessionService.onUnfocused called before bootstrap; ignoring.")
return
}
// capture the amount of time the app was focused
val dt = unfocusTimeMs - session.focusTime
val dt =
if (session.usesSessionsApi) {
// elapsedRealtime resets on reboot; drop the interval rather than count a negative one.
(unfocusElapsedMs - session.focusElapsedRealtime).coerceAtLeast(0L)
Comment thread
fadi-george marked this conversation as resolved.
} else {
unfocusTimeMs - session.focusTime
}
session.activeDuration += dt
Logging.log(LogLevel.DEBUG, "SessionService.onUnfocused adding time $dt for total: ${session.activeDuration}")
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,21 @@ data class AppIdResolution(
val failed: Boolean,
)

/**
* Sets [appId] on [configModel]. Cached remote feature flags belong to the previous app, so they
* are dropped when the app changes; otherwise they apply until the new app's flags are fetched.
*/
fun applyResolvedAppId(
configModel: ConfigModel,
appId: String,
) {
if (configModel.hasProperty(ConfigModel::appId.name) && configModel.appId != appId) {
configModel.sdkRemoteFeatureFlags = emptyList()
configModel.sdkRemoteFeatureFlagMetadata = null
}
configModel.appId = appId
}

fun resolveAppId(
inputAppId: String?,
configModel: ConfigModel,
Expand Down
Loading
Loading