From 0507bc22b6a770f9b9253b1d9b343b454b84a399 Mon Sep 17 00:00:00 2001 From: Themis wang Date: Thu, 10 Apr 2025 15:05:31 -0400 Subject: [PATCH 1/2] fallback module --- .../firebase/sessions/FirebaseSessions.kt | 24 ++-- ...sionsFallbackActivityLifecycleCallbacks.kt | 107 ++++++++++++++++++ 2 files changed, 121 insertions(+), 10 deletions(-) create mode 100644 firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionsFallbackActivityLifecycleCallbacks.kt diff --git a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/FirebaseSessions.kt b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/FirebaseSessions.kt index b0bb9bca4c2..f30821d9e3d 100644 --- a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/FirebaseSessions.kt +++ b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/FirebaseSessions.kt @@ -24,6 +24,7 @@ import com.google.firebase.annotations.concurrent.Background import com.google.firebase.app import com.google.firebase.sessions.api.FirebaseSessionsDependencies import com.google.firebase.sessions.settings.SessionsSettings +import dagger.Lazy import javax.inject.Inject import javax.inject.Singleton import kotlin.coroutines.CoroutineContext @@ -38,14 +39,13 @@ constructor( private val firebaseApp: FirebaseApp, private val settings: SessionsSettings, @Background backgroundDispatcher: CoroutineContext, - sessionsActivityLifecycleCallbacks: SessionsActivityLifecycleCallbacks, + sessionsActivityLifecycleCallbacks: Lazy, + sessionsFallbackActivityLifecycleCallbacks: Lazy, ) { - init { Log.d(TAG, "Initializing Firebase Sessions SDK.") val appContext = firebaseApp.applicationContext.applicationContext if (appContext is Application) { - appContext.registerActivityLifecycleCallbacks(sessionsActivityLifecycleCallbacks) CoroutineScope(backgroundDispatcher).launch { val subscribers = FirebaseSessionsDependencies.getRegisteredSubscribers() @@ -55,14 +55,18 @@ constructor( settings.updateSettings() if (!settings.sessionsEnabled) { Log.d(TAG, "Sessions SDK disabled. Not listening to lifecycle events.") + appContext.registerActivityLifecycleCallbacks( + sessionsFallbackActivityLifecycleCallbacks.get().activityLifecycleCallbacks + ) } else { - firebaseApp.addLifecycleEventListener { _, _ -> - // Log.w( - // TAG, - // "FirebaseApp instance deleted. Sessions library will stop collecting data.", - // ) - // TODO(mrober): Clean up on firebase app delete - } + appContext.registerActivityLifecycleCallbacks(sessionsActivityLifecycleCallbacks.get()) + } + firebaseApp.addLifecycleEventListener { _, _ -> + // Log.w( + // TAG, + // "FirebaseApp instance deleted. Sessions library will stop collecting data.", + // ) + // TODO(mrober): Clean up on firebase app delete } } } diff --git a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionsFallbackActivityLifecycleCallbacks.kt b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionsFallbackActivityLifecycleCallbacks.kt new file mode 100644 index 00000000000..64c423e5aec --- /dev/null +++ b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionsFallbackActivityLifecycleCallbacks.kt @@ -0,0 +1,107 @@ +/* + * 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.app.Activity +import android.app.Application.ActivityLifecycleCallbacks +import android.os.Bundle +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, +) { + private var localSessionData: SessionData + init { + localSessionData = + SessionData( + sessionDetails = sessionGenerator.generateNewSession(null), + backgroundTime = timeProvider.currentTime() + ) + val sessionId = localSessionData.sessionDetails.sessionId + notifySubscribers(sessionId) + } + + fun appBackground() { + localSessionData.copy(backgroundTime = timeProvider.currentTime()) + } + + fun appForeground() { + if (shouldInitiateNewSession(localSessionData)) { + val newSessionDetails = sessionGenerator.generateNewSession(localSessionData.sessionDetails) + localSessionData.copy(sessionDetails = newSessionDetails) + notifySubscribers(localSessionData.sessionDetails.sessionId) + } + } + + internal val activityLifecycleCallbacks = + object : ActivityLifecycleCallbacks { + override fun onActivityResumed(activity: Activity) = appForeground() + + override fun onActivityPaused(activity: Activity) = appBackground() + + override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) = Unit + + override fun onActivityStarted(activity: Activity) = Unit + + override fun onActivityStopped(activity: Activity) = Unit + + override fun onActivityDestroyed(activity: Activity) = Unit + + override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) = Unit + } + + 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" + } +} From 3913038c068f0ad13f9573e2ac3853d3b35289c9 Mon Sep 17 00:00:00 2001 From: Themis wang Date: Mon, 14 Apr 2025 10:58:02 -0400 Subject: [PATCH 2/2] wip --- .../firebase/sessions/FirebaseSessions.kt | 14 +++--- .../sessions/FirebaseSessionsComponent.kt | 4 -- ...ecycleCallbacks.kt => SessionInitiator.kt} | 39 ++++++++++------ ...sionsFallbackActivityLifecycleCallbacks.kt | 46 ++++++------------- .../sessions/SharedSessionRepository.kt | 45 +++++++++--------- 5 files changed, 67 insertions(+), 81 deletions(-) rename firebase-sessions/src/main/kotlin/com/google/firebase/sessions/{SessionsActivityLifecycleCallbacks.kt => SessionInitiator.kt} (61%) diff --git a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/FirebaseSessions.kt b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/FirebaseSessions.kt index f30821d9e3d..ed0d485c6de 100644 --- a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/FirebaseSessions.kt +++ b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/FirebaseSessions.kt @@ -24,7 +24,6 @@ import com.google.firebase.annotations.concurrent.Background import com.google.firebase.app import com.google.firebase.sessions.api.FirebaseSessionsDependencies import com.google.firebase.sessions.settings.SessionsSettings -import dagger.Lazy import javax.inject.Inject import javax.inject.Singleton import kotlin.coroutines.CoroutineContext @@ -39,13 +38,15 @@ constructor( private val firebaseApp: FirebaseApp, private val settings: SessionsSettings, @Background backgroundDispatcher: CoroutineContext, - sessionsActivityLifecycleCallbacks: Lazy, - sessionsFallbackActivityLifecycleCallbacks: Lazy, + sessionsActivityLifecycleCallbacks: SharedSessionRepository, + sessionsFallbackActivityLifecycleCallbacks: SessionsFallbackActivityLifecycleCallbacks, ) { init { Log.d(TAG, "Initializing Firebase Sessions SDK.") val appContext = firebaseApp.applicationContext.applicationContext if (appContext is Application) { + SessionInitiator.lifecycleClient = sessionsActivityLifecycleCallbacks + appContext.registerActivityLifecycleCallbacks(SessionInitiator) CoroutineScope(backgroundDispatcher).launch { val subscribers = FirebaseSessionsDependencies.getRegisteredSubscribers() @@ -55,11 +56,8 @@ constructor( settings.updateSettings() if (!settings.sessionsEnabled) { Log.d(TAG, "Sessions SDK disabled. Not listening to lifecycle events.") - appContext.registerActivityLifecycleCallbacks( - sessionsFallbackActivityLifecycleCallbacks.get().activityLifecycleCallbacks - ) - } else { - appContext.registerActivityLifecycleCallbacks(sessionsActivityLifecycleCallbacks.get()) + sessionsActivityLifecycleCallbacks.unregister() + SessionInitiator.lifecycleClient = sessionsFallbackActivityLifecycleCallbacks } firebaseApp.addLifecycleEventListener { _, _ -> // Log.w( diff --git a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/FirebaseSessionsComponent.kt b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/FirebaseSessionsComponent.kt index bdeb73af736..3b6fe179974 100644 --- a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/FirebaseSessionsComponent.kt +++ b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/FirebaseSessionsComponent.kt @@ -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" diff --git a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionsActivityLifecycleCallbacks.kt b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionInitiator.kt similarity index 61% rename from firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionsActivityLifecycleCallbacks.kt rename to firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionInitiator.kt index 8b817316066..3f6b75ced5b 100644 --- a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionsActivityLifecycleCallbacks.kt +++ b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionInitiator.kt @@ -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. @@ -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 + 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) { + field = lifecycleClient + } + + override fun onActivityResumed(activity: Activity) { + lifecycleClient?.appForegrounded() + } + + override fun onActivityPaused(activity: Activity) { + lifecycleClient?.appBackgrounded() + } override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) = Unit diff --git a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionsFallbackActivityLifecycleCallbacks.kt b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionsFallbackActivityLifecycleCallbacks.kt index 64c423e5aec..44e9ab654ad 100644 --- a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionsFallbackActivityLifecycleCallbacks.kt +++ b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionsFallbackActivityLifecycleCallbacks.kt @@ -16,9 +16,6 @@ package com.google.firebase.sessions -import android.app.Activity -import android.app.Application.ActivityLifecycleCallbacks -import android.os.Bundle import android.util.Log import com.google.firebase.annotations.concurrent.Background import com.google.firebase.sessions.api.FirebaseSessionsDependencies @@ -42,47 +39,30 @@ constructor( private val sessionGenerator: SessionGenerator, private val timeProvider: TimeProvider, @Background private val backgroundDispatcher: CoroutineContext, -) { - private var localSessionData: SessionData +) : SessionLifecycleClient { + + override var localSessionData: SessionData = + SessionData( + sessionDetails = sessionGenerator.generateNewSession(null), + timeProvider.currentTime() + ) + init { - localSessionData = - SessionData( - sessionDetails = sessionGenerator.generateNewSession(null), - backgroundTime = timeProvider.currentTime() - ) - val sessionId = localSessionData.sessionDetails.sessionId - notifySubscribers(sessionId) + notifySubscribers(localSessionData.sessionDetails.sessionId) } - fun appBackground() { - localSessionData.copy(backgroundTime = timeProvider.currentTime()) + override fun appBackgrounded() { + localSessionData = localSessionData.copy(backgroundTime = timeProvider.currentTime()) } - fun appForeground() { + override fun appForegrounded() { if (shouldInitiateNewSession(localSessionData)) { val newSessionDetails = sessionGenerator.generateNewSession(localSessionData.sessionDetails) - localSessionData.copy(sessionDetails = newSessionDetails) + localSessionData = localSessionData.copy(sessionDetails = newSessionDetails) notifySubscribers(localSessionData.sessionDetails.sessionId) } } - internal val activityLifecycleCallbacks = - object : ActivityLifecycleCallbacks { - override fun onActivityResumed(activity: Activity) = appForeground() - - override fun onActivityPaused(activity: Activity) = appBackground() - - override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) = Unit - - override fun onActivityStarted(activity: Activity) = Unit - - override fun onActivityStopped(activity: Activity) = Unit - - override fun onActivityDestroyed(activity: Activity) = Unit - - override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) = Unit - } - private fun notifySubscribers(sessionId: String) { CoroutineScope(backgroundDispatcher).launch { // Only notify subscriber for session change, not send to event to firelog diff --git a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SharedSessionRepository.kt b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SharedSessionRepository.kt index fee1acd737e..34be95181e4 100644 --- a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SharedSessionRepository.kt +++ b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SharedSessionRepository.kt @@ -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, @@ -46,26 +42,29 @@ constructor( private val timeProvider: TimeProvider, private val sessionDataStore: DataStore, @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 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 @@ -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 @@ -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