Skip to content
Closed
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 @@ -38,14 +38,15 @@ constructor(
private val firebaseApp: FirebaseApp,
private val settings: SessionsSettings,
@Background backgroundDispatcher: CoroutineContext,
sessionsActivityLifecycleCallbacks: SessionsActivityLifecycleCallbacks,
sessionsActivityLifecycleCallbacks: SharedSessionRepository,
sessionsFallbackActivityLifecycleCallbacks: SessionsFallbackActivityLifecycleCallbacks,
) {

init {
Log.d(TAG, "Initializing Firebase Sessions SDK.")
val appContext = firebaseApp.applicationContext.applicationContext
if (appContext is Application) {
appContext.registerActivityLifecycleCallbacks(sessionsActivityLifecycleCallbacks)
SessionInitiator.lifecycleClient = sessionsActivityLifecycleCallbacks
appContext.registerActivityLifecycleCallbacks(SessionInitiator)

CoroutineScope(backgroundDispatcher).launch {
val subscribers = FirebaseSessionsDependencies.getRegisteredSubscribers()
Expand All @@ -55,14 +56,15 @@ constructor(
settings.updateSettings()
if (!settings.sessionsEnabled) {
Log.d(TAG, "Sessions SDK disabled. Not listening to lifecycle events.")
} else {
firebaseApp.addLifecycleEventListener { _, _ ->
// Log.w(
// TAG,
// "FirebaseApp instance deleted. Sessions library will stop collecting data.",
// )
// TODO(mrober): Clean up on firebase app delete
}
sessionsActivityLifecycleCallbacks.unregister()
SessionInitiator.lifecycleClient = sessionsFallbackActivityLifecycleCallbacks
}
firebaseApp.addLifecycleEventListener { _, _ ->
// Log.w(
// TAG,
// "FirebaseApp instance deleted. Sessions library will stop collecting data.",
// )
// TODO(mrober): Clean up on firebase app delete
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -114,10 +114,6 @@ internal interface FirebaseSessionsComponent {

@Binds @Singleton fun settingsCache(impl: SettingsCacheImpl): SettingsCache

@Binds
@Singleton
fun sharedSessionRepository(impl: SharedSessionRepositoryImpl): SharedSessionRepository

companion object {
private const val TAG = "FirebaseSessions"

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2023 Google LLC
* Copyright 2025 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand All @@ -19,22 +19,31 @@ package com.google.firebase.sessions
import android.app.Activity
import android.app.Application.ActivityLifecycleCallbacks
import android.os.Bundle
import javax.inject.Inject
import javax.inject.Singleton

/**
* Lifecycle callbacks that will inform the [SharedSessionRepository] whenever an [Activity] in this
* application process goes foreground or background.
*/
@Singleton
internal class SessionsActivityLifecycleCallbacks
@Inject
constructor(private val sharedSessionRepository: SharedSessionRepository) :
ActivityLifecycleCallbacks {

override fun onActivityResumed(activity: Activity) = sharedSessionRepository.appForeground()
internal interface SessionLifecycleClient {
var localSessionData: SessionData
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

var localSessionData: SessionData
private set()

fun appForegrounded()
fun appBackgrounded()
fun unregister() = Unit
}

override fun onActivityPaused(activity: Activity) = sharedSessionRepository.appBackground()
internal object SessionInitiator : ActivityLifecycleCallbacks {
var currentLocalSession: SessionDetails? = null
get() {
return lifecycleClient?.localSessionData?.sessionDetails
}
var lifecycleClient: SessionLifecycleClient? = null
set(lifecycleClient) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remove the setter

field = lifecycleClient
}

override fun onActivityResumed(activity: Activity) {
lifecycleClient?.appForegrounded()
}

override fun onActivityPaused(activity: Activity) {
lifecycleClient?.appBackgrounded()
}

override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) = Unit

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
/*
* Copyright 2025 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.google.firebase.sessions

import android.util.Log
import com.google.firebase.annotations.concurrent.Background
import com.google.firebase.sessions.api.FirebaseSessionsDependencies
import com.google.firebase.sessions.api.SessionSubscriber
import com.google.firebase.sessions.settings.SessionsSettings
import javax.inject.Inject
import javax.inject.Singleton
import kotlin.coroutines.CoroutineContext
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch

/**
* This is the fallback module for datastore implementation (SharedSessionRepository). We are
* fallback to pre multi-process support behavior.
*/
@Singleton
internal class SessionsFallbackActivityLifecycleCallbacks
@Inject
constructor(
private val sessionsSettings: SessionsSettings,
private val sessionGenerator: SessionGenerator,
private val timeProvider: TimeProvider,
@Background private val backgroundDispatcher: CoroutineContext,
) : SessionLifecycleClient {

override var localSessionData: SessionData =
SessionData(
sessionDetails = sessionGenerator.generateNewSession(null),
timeProvider.currentTime()
)

init {
notifySubscribers(localSessionData.sessionDetails.sessionId)
}

override fun appBackgrounded() {
localSessionData = localSessionData.copy(backgroundTime = timeProvider.currentTime())
}

override fun appForegrounded() {
if (shouldInitiateNewSession(localSessionData)) {
val newSessionDetails = sessionGenerator.generateNewSession(localSessionData.sessionDetails)
localSessionData = localSessionData.copy(sessionDetails = newSessionDetails)
notifySubscribers(localSessionData.sessionDetails.sessionId)
}
}

private fun notifySubscribers(sessionId: String) {
CoroutineScope(backgroundDispatcher).launch {
// Only notify subscriber for session change, not send to event to firelog
FirebaseSessionsDependencies.getRegisteredSubscribers().values.forEach { subscriber ->
subscriber.onSessionChanged(SessionSubscriber.SessionDetails(sessionId))
Log.d(
TAG,
"Notified ${subscriber.sessionSubscriberName} of new session $sessionId with fallback mode"
)
}
}
}

private fun shouldInitiateNewSession(sessionData: SessionData): Boolean {
val interval = timeProvider.currentTime() - sessionData.backgroundTime
return interval > sessionsSettings.sessionRestartTimeout
}

private companion object {
const val TAG = "SessionsFallbackALC"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,17 +27,13 @@ import javax.inject.Inject
import javax.inject.Singleton
import kotlin.coroutines.CoroutineContext
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.cancellable
import kotlinx.coroutines.launch

/** Repository to persist session data to be shared between all app processes. */
internal interface SharedSessionRepository {
fun appBackground()

fun appForeground()
}

@Singleton
internal class SharedSessionRepositoryImpl
internal class SharedSessionRepository
@Inject
constructor(
private val sessionsSettings: SessionsSettings,
Expand All @@ -46,26 +42,29 @@ constructor(
private val timeProvider: TimeProvider,
private val sessionDataStore: DataStore<SessionData>,
@Background private val backgroundDispatcher: CoroutineContext,
) : SharedSessionRepository {
) : SessionLifecycleClient {
/** Local copy of the session data. Can get out of sync, must be double-checked in datastore. */
private lateinit var localSessionData: SessionData
override lateinit var localSessionData: SessionData

private var jobForCancel: Job? = null
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

dataFlowJob


init {
CoroutineScope(backgroundDispatcher).launch {
sessionDataStore.data.collect { sessionData ->
localSessionData = sessionData
val sessionId = sessionData.sessionDetails.sessionId
jobForCancel =
CoroutineScope(backgroundDispatcher).launch {
sessionDataStore.data.cancellable().collect { sessionData ->
localSessionData = sessionData
val sessionId = sessionData.sessionDetails.sessionId

FirebaseSessionsDependencies.getRegisteredSubscribers().values.forEach { subscriber ->
// Notify subscribers, regardless of sampling and data collection state
subscriber.onSessionChanged(SessionSubscriber.SessionDetails(sessionId))
Log.d(TAG, "Notified ${subscriber.sessionSubscriberName} of new session $sessionId")
FirebaseSessionsDependencies.getRegisteredSubscribers().values.forEach { subscriber ->
// Notify subscribers, regardless of sampling and data collection state
subscriber.onSessionChanged(SessionSubscriber.SessionDetails(sessionId))
Log.d(TAG, "Notified ${subscriber.sessionSubscriberName} of new session $sessionId")
}
}
}
}
}

override fun appBackground() {
override fun appBackgrounded() {
if (!::localSessionData.isInitialized) {
Log.d(TAG, "App backgrounded, but local SessionData not initialized")
return
Expand All @@ -81,7 +80,7 @@ constructor(
}
}

override fun appForeground() {
override fun appForegrounded() {
if (!::localSessionData.isInitialized) {
Log.d(TAG, "App foregrounded, but local SessionData not initialized")
return
Expand All @@ -105,6 +104,10 @@ constructor(
}
}

override fun unregister() {
jobForCancel?.cancel("Datastore turned off, stop flow")
}

private fun shouldInitiateNewSession(sessionData: SessionData): Boolean {
val interval = timeProvider.currentTime() - sessionData.backgroundTime
return interval > sessionsSettings.sessionRestartTimeout
Expand Down
Loading