diff --git a/firebase-sessions/src/androidTest/kotlin/com/google/firebase/sessions/FirebaseSessionsTests.kt b/firebase-sessions/src/androidTest/kotlin/com/google/firebase/sessions/FirebaseSessionsTests.kt index c74b6e4e329..33799a64d77 100644 --- a/firebase-sessions/src/androidTest/kotlin/com/google/firebase/sessions/FirebaseSessionsTests.kt +++ b/firebase-sessions/src/androidTest/kotlin/com/google/firebase/sessions/FirebaseSessionsTests.kt @@ -42,7 +42,6 @@ class FirebaseSessionsTests { @Test fun firebaseSessionsDependenciesDoInitialize() { assertThat(SessionFirelogPublisher.instance).isNotNull() - assertThat(SessionGenerator.instance).isNotNull() assertThat(SessionsSettings.instance).isNotNull() } diff --git a/firebase-sessions/src/androidTest/kotlin/com/google/firebase/sessions/SessionLifecycleServiceBinderTest.kt b/firebase-sessions/src/androidTest/kotlin/com/google/firebase/sessions/SessionLifecycleServiceBinderTest.kt deleted file mode 100644 index 49106b742af..00000000000 --- a/firebase-sessions/src/androidTest/kotlin/com/google/firebase/sessions/SessionLifecycleServiceBinderTest.kt +++ /dev/null @@ -1,62 +0,0 @@ -/* - * Copyright 2024 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.content.ComponentName -import android.content.Context -import android.content.Intent -import android.content.ServiceConnection -import android.os.IBinder -import androidx.test.core.app.ApplicationProvider -import androidx.test.ext.junit.runners.AndroidJUnit4 -import androidx.test.rule.ServiceTestRule -import com.google.common.truth.Truth.assertThat -import org.junit.Rule -import org.junit.Test -import org.junit.runner.RunWith - -@RunWith(AndroidJUnit4::class) -class SessionLifecycleServiceBinderTest { - @get:Rule val serviceRule = ServiceTestRule() - - @Test - fun bindSessionLifecycleService() { - val serviceConnection = - object : ServiceConnection { - var connected: Boolean = false - - override fun onServiceConnected(className: ComponentName?, serviceBinder: IBinder?) { - connected = true - } - - override fun onServiceDisconnected(className: ComponentName?) { - connected = false - } - } - - val sessionLifecycleServiceIntent = - Intent(ApplicationProvider.getApplicationContext(), SessionLifecycleService::class.java) - - serviceRule.bindService( - sessionLifecycleServiceIntent, - serviceConnection, - Context.BIND_IMPORTANT or Context.BIND_AUTO_CREATE, - ) - - assertThat(serviceConnection.connected).isTrue() - } -} diff --git a/firebase-sessions/src/main/AndroidManifest.xml b/firebase-sessions/src/main/AndroidManifest.xml index 181dcb1eee7..f0f9609f4e7 100644 --- a/firebase-sessions/src/main/AndroidManifest.xml +++ b/firebase-sessions/src/main/AndroidManifest.xml @@ -1,5 +1,4 @@ - - + @@ -19,11 +18,8 @@ - + android:exported="false" + android:name="com.google.firebase.components.ComponentDiscoveryService"> 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 18b9961724b..b0bb9bca4c2 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 @@ -38,14 +38,14 @@ constructor( private val firebaseApp: FirebaseApp, private val settings: SessionsSettings, @Background backgroundDispatcher: CoroutineContext, - lifecycleServiceBinder: SessionLifecycleServiceBinder, + sessionsActivityLifecycleCallbacks: SessionsActivityLifecycleCallbacks, ) { init { Log.d(TAG, "Initializing Firebase Sessions SDK.") val appContext = firebaseApp.applicationContext.applicationContext if (appContext is Application) { - appContext.registerActivityLifecycleCallbacks(SessionsActivityLifecycleCallbacks) + appContext.registerActivityLifecycleCallbacks(sessionsActivityLifecycleCallbacks) CoroutineScope(backgroundDispatcher).launch { val subscribers = FirebaseSessionsDependencies.getRegisteredSubscribers() @@ -56,16 +56,12 @@ constructor( if (!settings.sessionsEnabled) { Log.d(TAG, "Sessions SDK disabled. Not listening to lifecycle events.") } else { - val lifecycleClient = SessionLifecycleClient(backgroundDispatcher) - lifecycleClient.bindToService(lifecycleServiceBinder) - SessionsActivityLifecycleCallbacks.lifecycleClient = lifecycleClient - firebaseApp.addLifecycleEventListener { _, _ -> - Log.w( - TAG, - "FirebaseApp instance deleted. Sessions library will stop collecting data.", - ) - SessionsActivityLifecycleCallbacks.lifecycleClient = null + // 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/FirebaseSessionsComponent.kt b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/FirebaseSessionsComponent.kt index 918782ef140..bdeb73af736 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 @@ -66,7 +66,6 @@ import kotlinx.coroutines.CoroutineScope internal interface FirebaseSessionsComponent { val firebaseSessions: FirebaseSessions - val sessionDatastore: SessionDatastore val sessionFirelogPublisher: SessionFirelogPublisher val sessionGenerator: SessionGenerator val sessionsSettings: SessionsSettings @@ -95,18 +94,10 @@ internal interface FirebaseSessionsComponent { interface MainModule { @Binds @Singleton fun eventGDTLoggerInterface(impl: EventGDTLogger): EventGDTLoggerInterface - @Binds @Singleton fun sessionDatastore(impl: SessionDatastoreImpl): SessionDatastore - @Binds @Singleton fun sessionFirelogPublisher(impl: SessionFirelogPublisherImpl): SessionFirelogPublisher - @Binds - @Singleton - fun sessionLifecycleServiceBinder( - impl: SessionLifecycleServiceBinderImpl - ): SessionLifecycleServiceBinder - @Binds @Singleton fun crashlyticsSettingsFetcher(impl: RemoteSettingsFetcher): CrashlyticsSettingsFetcher @@ -123,6 +114,10 @@ internal interface FirebaseSessionsComponent { @Binds @Singleton fun settingsCache(impl: SettingsCacheImpl): SettingsCache + @Binds + @Singleton + fun sharedSessionRepository(impl: SharedSessionRepositoryImpl): SharedSessionRepository + companion object { private const val TAG = "FirebaseSessions" @@ -157,13 +152,14 @@ internal interface FirebaseSessionsComponent { fun sessionDataStore( appContext: Context, @Blocking blockingDispatcher: CoroutineContext, + sessionDataSerializer: SessionDataSerializer, ): DataStore = createDataStore( - serializer = SessionDataSerializer, + serializer = sessionDataSerializer, corruptionHandler = ReplaceFileCorruptionHandler { ex -> Log.w(TAG, "CorruptionException in session data DataStore", ex) - SessionDataSerializer.defaultValue + sessionDataSerializer.defaultValue }, scope = CoroutineScope(blockingDispatcher), produceFile = { appContext.dataStoreFile("aqs/sessionDataStore.data") }, diff --git a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionData.kt b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionData.kt new file mode 100644 index 00000000000..3eaf1a6c011 --- /dev/null +++ b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionData.kt @@ -0,0 +1,58 @@ +/* + * 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 androidx.datastore.core.CorruptionException +import androidx.datastore.core.Serializer +import java.io.InputStream +import java.io.OutputStream +import javax.inject.Inject +import javax.inject.Singleton +import kotlinx.serialization.Serializable +import kotlinx.serialization.json.Json + +/** Session data to be persisted. */ +@Serializable +internal data class SessionData(val sessionDetails: SessionDetails, val backgroundTime: Time) + +/** DataStore json [Serializer] for [SessionData]. */ +@Singleton +internal class SessionDataSerializer +@Inject +constructor( + private val sessionGenerator: SessionGenerator, + private val timeProvider: TimeProvider, +) : Serializer { + override val defaultValue: SessionData + get() = + SessionData( + sessionDetails = sessionGenerator.generateNewSession(currentSession = null), + backgroundTime = timeProvider.currentTime(), + ) + + override suspend fun readFrom(input: InputStream): SessionData = + try { + Json.decodeFromString(input.readBytes().decodeToString()) + } catch (ex: Exception) { + throw CorruptionException("Cannot parse session data", ex) + } + + override suspend fun writeTo(t: SessionData, output: OutputStream) { + @Suppress("BlockingMethodInNonBlockingContext") // blockingDispatcher is safe for blocking calls + output.write(Json.encodeToString(SessionData.serializer(), t).encodeToByteArray()) + } +} diff --git a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionDatastore.kt b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionDatastore.kt deleted file mode 100644 index b3b72b4d4d7..00000000000 --- a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionDatastore.kt +++ /dev/null @@ -1,115 +0,0 @@ -/* - * Copyright 2023 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 androidx.datastore.core.CorruptionException -import androidx.datastore.core.DataStore -import androidx.datastore.core.Serializer -import com.google.firebase.Firebase -import com.google.firebase.annotations.concurrent.Background -import com.google.firebase.app -import java.io.IOException -import java.io.InputStream -import java.io.OutputStream -import java.util.concurrent.atomic.AtomicReference -import javax.inject.Inject -import javax.inject.Singleton -import kotlin.coroutines.CoroutineContext -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.catch -import kotlinx.coroutines.launch -import kotlinx.serialization.Serializable -import kotlinx.serialization.json.Json - -/** Data for sessions information */ -@Serializable internal data class SessionData(val sessionId: String?) - -/** DataStore json [Serializer] for [SessionData]. */ -internal object SessionDataSerializer : Serializer { - override val defaultValue = SessionData(sessionId = null) - - override suspend fun readFrom(input: InputStream): SessionData = - try { - Json.decodeFromString(input.readBytes().decodeToString()) - } catch (ex: Exception) { - throw CorruptionException("Cannot parse session data", ex) - } - - override suspend fun writeTo(t: SessionData, output: OutputStream) { - @Suppress("BlockingMethodInNonBlockingContext") // blockingDispatcher is safe for blocking calls - output.write(Json.encodeToString(SessionData.serializer(), t).encodeToByteArray()) - } -} - -/** Handles reading to and writing from the [DataStore]. */ -internal interface SessionDatastore { - /** Stores a new session ID value in the [DataStore] */ - fun updateSessionId(sessionId: String) - - /** - * Gets the currently stored session ID from the [DataStore]. This will be null if no session has - * been stored previously. - */ - fun getCurrentSessionId(): String? - - companion object { - val instance: SessionDatastore - get() = Firebase.app[FirebaseSessionsComponent::class.java].sessionDatastore - } -} - -@Singleton -internal class SessionDatastoreImpl -@Inject -constructor( - @Background private val backgroundDispatcher: CoroutineContext, - private val sessionDataStore: DataStore, -) : SessionDatastore { - - /** Most recent session from datastore is updated asynchronously whenever it changes */ - private val currentSessionFromDatastore = AtomicReference() - - private val firebaseSessionDataFlow: Flow = - sessionDataStore.data.catch { ex -> - Log.e(TAG, "Error reading stored session data.", ex) - emit(SessionDataSerializer.defaultValue) - } - - init { - CoroutineScope(backgroundDispatcher).launch { - firebaseSessionDataFlow.collect { currentSessionFromDatastore.set(it) } - } - } - - override fun updateSessionId(sessionId: String) { - CoroutineScope(backgroundDispatcher).launch { - try { - sessionDataStore.updateData { SessionData(sessionId) } - } catch (ex: IOException) { - Log.w(TAG, "Failed to update session Id", ex) - } - } - } - - override fun getCurrentSessionId(): String? = currentSessionFromDatastore.get()?.sessionId - - private companion object { - private const val TAG = "FirebaseSessionsRepo" - } -} diff --git a/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/testing/FakeSessionDatastore.kt b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionDetails.kt similarity index 50% rename from firebase-sessions/src/test/kotlin/com/google/firebase/sessions/testing/FakeSessionDatastore.kt rename to firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionDetails.kt index f98852032c8..c5f71fb65e5 100644 --- a/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/testing/FakeSessionDatastore.kt +++ b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionDetails.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. @@ -14,22 +14,15 @@ * limitations under the License. */ -package com.google.firebase.sessions.testing +package com.google.firebase.sessions -import com.google.firebase.sessions.SessionDatastore +import kotlinx.serialization.Serializable -/** - * Fake implementaiton of the [SessionDatastore] that allows for inspecting and modifying the - * currently stored values in unit tests. - */ -internal class FakeSessionDatastore : SessionDatastore { - - /** The currently stored value */ - private var currentSessionId: String? = null - - override fun updateSessionId(sessionId: String) { - currentSessionId = sessionId - } - - override fun getCurrentSessionId() = currentSessionId -} +/** Details about the current session. */ +@Serializable +internal data class SessionDetails( + val sessionId: String, + val firstSessionId: String, + val sessionIndex: Int, + val sessionStartTimestampUs: Long, +) diff --git a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionGenerator.kt b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionGenerator.kt index 409f9989348..888b3d4729b 100644 --- a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionGenerator.kt +++ b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionGenerator.kt @@ -16,22 +16,9 @@ package com.google.firebase.sessions -import com.google.errorprone.annotations.CanIgnoreReturnValue -import com.google.firebase.Firebase -import com.google.firebase.app import javax.inject.Inject import javax.inject.Singleton -/** - * [SessionDetails] is a data class responsible for storing information about the current Session. - */ -internal data class SessionDetails( - val sessionId: String, - val firstSessionId: String, - val sessionIndex: Int, - val sessionStartTimestampUs: Long, -) - /** * The [SessionGenerator] is responsible for generating the Session ID, and keeping the * [SessionDetails] up to date with the latest values. @@ -40,35 +27,20 @@ internal data class SessionDetails( internal class SessionGenerator @Inject constructor(private val timeProvider: TimeProvider, private val uuidGenerator: UuidGenerator) { - private val firstSessionId = generateSessionId() - private var sessionIndex = -1 - - /** The current generated session, must not be accessed before calling [generateNewSession]. */ - lateinit var currentSession: SessionDetails - private set - - /** Returns if a session has been generated. */ - val hasGenerateSession: Boolean - get() = ::currentSession.isInitialized - - /** Generates a new session. The first session's sessionId will match firstSessionId. */ - @CanIgnoreReturnValue - fun generateNewSession(): SessionDetails { - sessionIndex++ - currentSession = - SessionDetails( - sessionId = if (sessionIndex == 0) firstSessionId else generateSessionId(), - firstSessionId, - sessionIndex, - sessionStartTimestampUs = timeProvider.currentTime().us, - ) - return currentSession + /** + * Generates a new session. + * + * If a current session is provided, will maintain the first session id and appropriate index. + */ + fun generateNewSession(currentSession: SessionDetails?): SessionDetails { + val newSessionId = generateSessionId() + return SessionDetails( + sessionId = newSessionId, + firstSessionId = currentSession?.firstSessionId ?: newSessionId, + sessionIndex = currentSession?.sessionIndex?.inc() ?: 0, + sessionStartTimestampUs = timeProvider.currentTime().us, + ) } private fun generateSessionId() = uuidGenerator.next().toString().replace("-", "").lowercase() - - internal companion object { - val instance: SessionGenerator - get() = Firebase.app[FirebaseSessionsComponent::class.java].sessionGenerator - } } diff --git a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionLifecycleClient.kt b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionLifecycleClient.kt deleted file mode 100644 index 900068af00d..00000000000 --- a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionLifecycleClient.kt +++ /dev/null @@ -1,217 +0,0 @@ -/* - * Copyright 2023 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.content.ComponentName -import android.content.ServiceConnection -import android.os.Handler -import android.os.IBinder -import android.os.Looper -import android.os.Message -import android.os.Messenger -import android.os.RemoteException -import android.util.Log -import com.google.errorprone.annotations.CanIgnoreReturnValue -import com.google.firebase.sessions.api.FirebaseSessionsDependencies -import com.google.firebase.sessions.api.SessionSubscriber -import java.util.concurrent.LinkedBlockingDeque -import kotlin.coroutines.CoroutineContext -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.launch - -/** - * Client for binding to the [SessionLifecycleService]. This client will receive updated sessions - * through a callback whenever a new session is generated by the service, or after the initial - * binding. - * - * Note: this client will be connected in every application process that uses Firebase, and is - * intended to maintain that connection for the lifetime of the process. - */ -internal class SessionLifecycleClient(private val backgroundDispatcher: CoroutineContext) { - - private var service: Messenger? = null - private var serviceBound: Boolean = false - private val queuedMessages = LinkedBlockingDeque(MAX_QUEUED_MESSAGES) - - /** - * The callback class that will be used to receive updated session events from the - * [SessionLifecycleService]. - */ - internal class ClientUpdateHandler(private val backgroundDispatcher: CoroutineContext) : - Handler(Looper.getMainLooper()) { - - override fun handleMessage(msg: Message) { - when (msg.what) { - SessionLifecycleService.SESSION_UPDATED -> - handleSessionUpdate( - msg.data?.getString(SessionLifecycleService.SESSION_UPDATE_EXTRA) ?: "" - ) - else -> { - Log.w(TAG, "Received unexpected event from the SessionLifecycleService: $msg") - super.handleMessage(msg) - } - } - } - - private fun handleSessionUpdate(sessionId: String) { - Log.d(TAG, "Session update received.") - - CoroutineScope(backgroundDispatcher).launch { - 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") - } - } - } - } - - /** The connection object to the [SessionLifecycleService]. */ - private val serviceConnection = - object : ServiceConnection { - override fun onServiceConnected(className: ComponentName?, serviceBinder: IBinder?) { - Log.d(TAG, "Connected to SessionLifecycleService. Queue size ${queuedMessages.size}") - service = Messenger(serviceBinder) - serviceBound = true - sendLifecycleEvents(drainQueue()) - } - - override fun onServiceDisconnected(className: ComponentName?) { - Log.d(TAG, "Disconnected from SessionLifecycleService") - service = null - serviceBound = false - } - } - - /** - * Binds to the [SessionLifecycleService] and passes a callback [Messenger] that will be used to - * relay session updates to this client. - */ - fun bindToService(sessionLifecycleServiceBinder: SessionLifecycleServiceBinder) { - sessionLifecycleServiceBinder.bindToService( - Messenger(ClientUpdateHandler(backgroundDispatcher)), - serviceConnection, - ) - } - - /** - * Should be called when any activity in this application process goes to the foreground. This - * will relay the event to the [SessionLifecycleService] where it can make the determination of - * whether or not this foregrounding event should result in a new session being generated. - */ - fun foregrounded() { - sendLifecycleEvent(SessionLifecycleService.FOREGROUNDED) - } - - /** - * Should be called when any activity in this application process goes from the foreground to the - * background. This will relay the event to the [SessionLifecycleService] where it will be used to - * determine when a new session should be generated. - */ - fun backgrounded() { - sendLifecycleEvent(SessionLifecycleService.BACKGROUNDED) - } - - /** - * Sends a message to the [SessionLifecycleService] with the given event code. This will - * potentially also send any messages that have been queued up but not successfully delivered to - * this service since the previous send. - */ - private fun sendLifecycleEvent(messageCode: Int) { - val allMessages = drainQueue() - allMessages.add(Message.obtain(null, messageCode, 0, 0)) - sendLifecycleEvents(allMessages) - } - - /** - * Sends lifecycle events to the [SessionLifecycleService]. This will only send the latest - * FOREGROUND and BACKGROUND events to the service that are included in the given list. Running - * through the full backlog of messages is not useful since the service only cares about the - * current state and transitions from background -> foreground. - * - * Does not send events unless data collection is enabled for at least one subscriber. - */ - @CanIgnoreReturnValue - private fun sendLifecycleEvents(messages: List) = - CoroutineScope(backgroundDispatcher).launch { - val subscribers = FirebaseSessionsDependencies.getRegisteredSubscribers() - if (subscribers.isEmpty()) { - Log.d( - TAG, - "Sessions SDK did not have any dependent SDKs register as dependencies. Events will not be sent.", - ) - } else if (subscribers.values.none { it.isDataCollectionEnabled }) { - Log.d(TAG, "Data Collection is disabled for all subscribers. Skipping this Event") - } else { - mutableListOf( - getLatestByCode(messages, SessionLifecycleService.BACKGROUNDED), - getLatestByCode(messages, SessionLifecycleService.FOREGROUNDED), - ) - .filterNotNull() - .sortedBy { it.getWhen() } - .forEach { sendMessageToServer(it) } - } - } - - /** Sends the given [Message] to the [SessionLifecycleService]. */ - private fun sendMessageToServer(msg: Message) { - if (service != null) { - try { - Log.d(TAG, "Sending lifecycle ${msg.what} to service") - service?.send(msg) - } catch (e: RemoteException) { - Log.w(TAG, "Unable to deliver message: ${msg.what}", e) - queueMessage(msg) - } - } else { - queueMessage(msg) - } - } - - /** - * Queues the given [Message] up for delivery to the [SessionLifecycleService] once the connection - * is established. - */ - private fun queueMessage(msg: Message) { - if (queuedMessages.offer(msg)) { - Log.d(TAG, "Queued message ${msg.what}. Queue size ${queuedMessages.size}") - } else { - Log.d(TAG, "Failed to enqueue message ${msg.what}. Dropping.") - } - } - - /** Drains the queue of messages into a new list in a thread-safe manner. */ - private fun drainQueue(): MutableList { - val messages = mutableListOf() - queuedMessages.drainTo(messages) - return messages - } - - /** Gets the message in the given list with the given code that has the latest timestamp. */ - private fun getLatestByCode(messages: List, msgCode: Int): Message? = - messages.filter { it.what == msgCode }.maxByOrNull { it.getWhen() } - - companion object { - const val TAG = "SessionLifecycleClient" - - /** - * The maximum number of messages that we should queue up for delivery to the - * [SessionLifecycleService] in the event that we have lost the connection. - */ - private const val MAX_QUEUED_MESSAGES = 20 - } -} diff --git a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionLifecycleService.kt b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionLifecycleService.kt deleted file mode 100644 index 85930dc5455..00000000000 --- a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionLifecycleService.kt +++ /dev/null @@ -1,262 +0,0 @@ -/* - * Copyright 2023 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.Service -import android.content.Intent -import android.os.Build -import android.os.Bundle -import android.os.DeadObjectException -import android.os.Handler -import android.os.HandlerThread -import android.os.IBinder -import android.os.Looper -import android.os.Message -import android.os.Messenger -import android.util.Log -import com.google.firebase.sessions.settings.SessionsSettings - -/** - * Service for monitoring application lifecycle events and determining when/if a new session should - * be generated. When this happens, the service will broadcast the updated session id to all - * connected clients. - */ -internal class SessionLifecycleService : Service() { - - /** The thread that will be used to process all lifecycle messages from connected clients. */ - internal val handlerThread: HandlerThread = HandlerThread("FirebaseSessions_HandlerThread") - - /** The handler that will process all lifecycle messages from connected clients . */ - private var messageHandler: MessageHandler? = null - - /** The single messenger that will be sent to all connected clients of this service . */ - private var messenger: Messenger? = null - - /** - * Handler of incoming activity lifecycle events being received from [SessionLifecycleClient]s. - * All incoming communication from connected clients comes through this class and will be used to - * determine when new sessions should be created. - */ - internal class MessageHandler(looper: Looper) : Handler(looper) { - - /** - * Flag indicating whether or not the app has ever come into the foreground during the lifetime - * of the service. If it has not, we can infer that the first foreground event is a cold-start - * - * Note: this is made volatile because we attempt to send the current session ID to newly bound - * clients, and this binding happens - */ - private var hasForegrounded: Boolean = false - - /** - * The timestamp of the last activity lifecycle message we've received from a client. Used to - * determine when the app has been idle for long enough to require a new session. - */ - private var lastMsgTimeMs: Long = 0 - - /** Queue of connected clients. */ - private val boundClients = ArrayList() - - override fun handleMessage(msg: Message) { - if (lastMsgTimeMs > msg.getWhen()) { - Log.d(TAG, "Ignoring old message from ${msg.getWhen()} which is older than $lastMsgTimeMs.") - return - } - when (msg.what) { - FOREGROUNDED -> handleForegrounding(msg) - BACKGROUNDED -> handleBackgrounding(msg) - CLIENT_BOUND -> handleClientBound(msg) - else -> { - Log.w(TAG, "Received unexpected event from the SessionLifecycleClient: $msg") - super.handleMessage(msg) - } - } - } - - /** - * Handles a foregrounding event by any activity owned by the application as specified by the - * given [Message]. This will determine if the foregrounding should result in the creation of a - * new session. - */ - private fun handleForegrounding(msg: Message) { - Log.d(TAG, "Activity foregrounding at ${msg.getWhen()}.") - if (!hasForegrounded) { - Log.d(TAG, "Cold start detected.") - hasForegrounded = true - newSession() - } else if (isSessionRestart(msg.getWhen())) { - Log.d(TAG, "Session too long in background. Creating new session.") - newSession() - } - lastMsgTimeMs = msg.getWhen() - } - - /** - * Handles a backgrounding event by any activity owned by the application as specified by the - * given [Message]. This will keep track of the backgrounding and be used to determine if future - * foregrounding events should result in the creation of a new session. - */ - private fun handleBackgrounding(msg: Message) { - Log.d(TAG, "Activity backgrounding at ${msg.getWhen()}") - lastMsgTimeMs = msg.getWhen() - } - - /** - * Handles a newly bound client to this service by adding it to the list of callback clients and - * attempting to send it the latest session id immediately. - */ - private fun handleClientBound(msg: Message) { - boundClients.add(msg.replyTo) - maybeSendSessionToClient(msg.replyTo) - Log.d(TAG, "Client ${msg.replyTo} bound at ${msg.getWhen()}. Clients: ${boundClients.size}") - } - - /** Generates a new session id and sends it everywhere it's needed */ - private fun newSession() { - try { - SessionGenerator.instance.generateNewSession() - Log.d(TAG, "Generated new session.") - broadcastSession() - SessionDatastore.instance.updateSessionId( - SessionGenerator.instance.currentSession.sessionId - ) - } catch (ex: IllegalStateException) { - Log.w(TAG, "Failed to generate new session.", ex) - } - } - - /** - * Broadcasts the current session to by uploading to Firelog and all sending a message to all - * connected clients. - */ - private fun broadcastSession() { - Log.d(TAG, "Broadcasting new session") - SessionFirelogPublisher.instance.logSession(SessionGenerator.instance.currentSession) - // Create a defensive copy because DeadObjectExceptions on send will modify boundClients - val clientsToSend = ArrayList(boundClients) - clientsToSend.forEach { maybeSendSessionToClient(it) } - } - - private fun maybeSendSessionToClient(client: Messenger) { - try { - if (hasForegrounded) { - sendSessionToClient(client, SessionGenerator.instance.currentSession.sessionId) - } else { - // Send the value from the datastore before the first foregrounding it exists - val storedSession = SessionDatastore.instance.getCurrentSessionId() - Log.d(TAG, "App has not yet foregrounded. Using previously stored session.") - storedSession?.let { sendSessionToClient(client, it) } - } - } catch (ex: IllegalStateException) { - Log.w(TAG, "Failed to send session to client.", ex) - } - } - - /** Sends the current session id to the client connected through the given [Messenger]. */ - private fun sendSessionToClient(client: Messenger, sessionId: String) { - try { - val msgData = Bundle().also { it.putString(SESSION_UPDATE_EXTRA, sessionId) } - client.send(Message.obtain(null, SESSION_UPDATED, 0, 0).also { it.data = msgData }) - } catch (e: DeadObjectException) { - Log.d(TAG, "Removing dead client from list: $client") - boundClients.remove(client) - } catch (e: Exception) { - Log.w(TAG, "Unable to push new session to $client.", e) - } - } - - /** - * Determines if the foregrounding that occurred at the given time should trigger a new session - * because the app has been idle for too long. - */ - private fun isSessionRestart(foregroundTimeMs: Long) = - (foregroundTimeMs - lastMsgTimeMs) > - SessionsSettings.instance.sessionRestartTimeout.inWholeMilliseconds - } - - override fun onCreate() { - super.onCreate() - handlerThread.start() - messageHandler = MessageHandler(handlerThread.looper) - messenger = Messenger(messageHandler) - Log.d(TAG, "Service created on process ${android.os.Process.myPid()}") - } - - /** Called when a new [SessionLifecycleClient] binds to this service. */ - override fun onBind(intent: Intent?): IBinder? = - if (intent == null) { - Log.d(TAG, "Service bound with null intent. Ignoring.") - null - } else { - Log.d(TAG, "Service bound to new client on process ${intent.action}") - val callbackMessenger = getClientCallback(intent) - if (callbackMessenger != null) { - val clientBoundMsg = Message.obtain(null, CLIENT_BOUND, 0, 0) - clientBoundMsg.replyTo = callbackMessenger - messageHandler?.sendMessage(clientBoundMsg) - } - messenger?.binder - } - - override fun onDestroy() { - super.onDestroy() - handlerThread.quit() - } - - /** - * Extracts the callback [Messenger] from the given [Intent] which will be used to push session - * updates back to the [SessionLifecycleClient] that created this [Intent]. - */ - private fun getClientCallback(intent: Intent): Messenger? = - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - intent.getParcelableExtra(CLIENT_CALLBACK_MESSENGER, Messenger::class.java) - } else { - @Suppress("DEPRECATION") intent.getParcelableExtra(CLIENT_CALLBACK_MESSENGER) - } - - internal companion object { - const val TAG = "SessionLifecycleService" - - /** - * Key for the [Messenger] callback extra included in the [Intent] used by the - * [SessionLifecycleClient] to bind to this service. - */ - const val CLIENT_CALLBACK_MESSENGER = "ClientCallbackMessenger" - - /** - * Key for the extra String included in the [SESSION_UPDATED] message, sent to all connected - * clients, containing an updated session id. - */ - const val SESSION_UPDATE_EXTRA = "SessionUpdateExtra" - - /** [Message] code indicating that an application activity has gone to the foreground */ - const val FOREGROUNDED = 1 - /** [Message] code indicating that an application activity has gone to the background */ - const val BACKGROUNDED = 2 - /** - * [Message] code indicating that a new session has been started, and containing the new session - * id in the [SESSION_UPDATE_EXTRA] extra field. - */ - const val SESSION_UPDATED = 3 - - /** - * [Message] code indicating that a new client has been bound to the service. The - * [Message.replyTo] field will contain the new client callback interface. - */ - private const val CLIENT_BOUND = 4 - } -} diff --git a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionLifecycleServiceBinder.kt b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionLifecycleServiceBinder.kt deleted file mode 100644 index 094a76ee51c..00000000000 --- a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionLifecycleServiceBinder.kt +++ /dev/null @@ -1,77 +0,0 @@ -/* - * Copyright 2023 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.content.Context -import android.content.Intent -import android.content.ServiceConnection -import android.os.Messenger -import android.util.Log -import javax.inject.Inject -import javax.inject.Singleton - -/** Interface for binding with the [SessionLifecycleService]. */ -internal fun interface SessionLifecycleServiceBinder { - /** - * Binds the given client callback [Messenger] to the [SessionLifecycleService]. The given - * callback will be used to relay session updates to this client. - */ - fun bindToService(callback: Messenger, serviceConnection: ServiceConnection) -} - -@Singleton -internal class SessionLifecycleServiceBinderImpl -@Inject -constructor(private val appContext: Context) : SessionLifecycleServiceBinder { - - override fun bindToService(callback: Messenger, serviceConnection: ServiceConnection) { - Intent(appContext, SessionLifecycleService::class.java).also { intent -> - Log.d(TAG, "Binding service to application.") - // This is necessary for the onBind() to be called by each process - intent.action = android.os.Process.myPid().toString() - intent.putExtra(SessionLifecycleService.CLIENT_CALLBACK_MESSENGER, callback) - intent.setPackage(appContext.packageName) - - val isServiceBound = - try { - appContext.bindService( - intent, - serviceConnection, - Context.BIND_IMPORTANT or Context.BIND_AUTO_CREATE, - ) - } catch (ex: SecurityException) { - Log.w(TAG, "Failed to bind session lifecycle service to application.", ex) - false - } - if (!isServiceBound) { - unbindServiceSafely(appContext, serviceConnection) - Log.i(TAG, "Session lifecycle service binding failed.") - } - } - } - - private fun unbindServiceSafely(appContext: Context, serviceConnection: ServiceConnection) = - try { - appContext.unbindService(serviceConnection) - } catch (ex: IllegalArgumentException) { - Log.w(TAG, "Session lifecycle service binding failed.", ex) - } - - private companion object { - const val TAG = "LifecycleServiceBinder" - } -} diff --git a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionsActivityLifecycleCallbacks.kt b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionsActivityLifecycleCallbacks.kt index b72c1da5cf3..8b817316066 100644 --- a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionsActivityLifecycleCallbacks.kt +++ b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionsActivityLifecycleCallbacks.kt @@ -19,34 +19,22 @@ package com.google.firebase.sessions import android.app.Activity import android.app.Application.ActivityLifecycleCallbacks import android.os.Bundle -import androidx.annotation.VisibleForTesting +import javax.inject.Inject +import javax.inject.Singleton /** - * Lifecycle callbacks that will inform the [SessionLifecycleClient] whenever an [Activity] in this + * Lifecycle callbacks that will inform the [SharedSessionRepository] whenever an [Activity] in this * application process goes foreground or background. */ -internal object SessionsActivityLifecycleCallbacks : ActivityLifecycleCallbacks { - @VisibleForTesting internal var hasPendingForeground: Boolean = false - - var lifecycleClient: SessionLifecycleClient? = null - /** Sets the client and calls [SessionLifecycleClient.foregrounded] for pending foreground. */ - set(lifecycleClient) { - field = lifecycleClient - lifecycleClient?.let { - if (hasPendingForeground) { - hasPendingForeground = false - it.foregrounded() - } - } - } - - override fun onActivityResumed(activity: Activity) { - lifecycleClient?.foregrounded() ?: run { hasPendingForeground = true } - } - - override fun onActivityPaused(activity: Activity) { - lifecycleClient?.backgrounded() - } +@Singleton +internal class SessionsActivityLifecycleCallbacks +@Inject +constructor(private val sharedSessionRepository: SharedSessionRepository) : + ActivityLifecycleCallbacks { + + override fun onActivityResumed(activity: Activity) = sharedSessionRepository.appForeground() + + override fun onActivityPaused(activity: Activity) = sharedSessionRepository.appBackground() override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) = Unit 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 new file mode 100644 index 00000000000..234730985c0 --- /dev/null +++ b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SharedSessionRepository.kt @@ -0,0 +1,121 @@ +/* + * 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 androidx.datastore.core.DataStore +import com.google.firebase.annotations.concurrent.Background +import com.google.firebase.sessions.ProcessDetailsProvider.getProcessName +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 + +/** Repository to persist session data to be shared between all app processes. */ +internal interface SharedSessionRepository { + fun appBackground() + + fun appForeground() +} + +@Singleton +internal class SharedSessionRepositoryImpl +@Inject +constructor( + private val sessionsSettings: SessionsSettings, + private val sessionGenerator: SessionGenerator, + private val sessionFirelogPublisher: SessionFirelogPublisher, + private val timeProvider: TimeProvider, + private val sessionDataStore: DataStore, + @Background private val backgroundDispatcher: CoroutineContext, +) : SharedSessionRepository { + /** Local copy of the session data. Can get out of sync, must be double-checked in datastore. */ + private lateinit var localSessionData: SessionData + + init { + CoroutineScope(backgroundDispatcher).launch { + sessionDataStore.data.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") + } + } + } + } + + override fun appBackground() { + if (!::localSessionData.isInitialized) { + Log.d(TAG, "App backgrounded, but local SessionData not initialized") + return + } + val sessionData = localSessionData + Log.d(TAG, "App backgrounded on ${getProcessName()} - $sessionData") + + CoroutineScope(backgroundDispatcher).launch { + sessionDataStore.updateData { + // TODO(mrober): Double check time makes sense? + sessionData.copy(backgroundTime = timeProvider.currentTime()) + } + } + } + + override fun appForeground() { + if (!::localSessionData.isInitialized) { + Log.d(TAG, "App foregrounded, but local SessionData not initialized") + return + } + val sessionData = localSessionData + Log.d(TAG, "App foregrounded on ${getProcessName()} - $sessionData") + + if (shouldInitiateNewSession(sessionData)) { + // Generate new session details on main thread so the timestamp is as current as possible + val newSessionDetails = sessionGenerator.generateNewSession(sessionData.sessionDetails) + + CoroutineScope(backgroundDispatcher).launch { + sessionDataStore.updateData { currentSessionData -> + // Double-check pattern + if (shouldInitiateNewSession(currentSessionData)) { + currentSessionData.copy(sessionDetails = newSessionDetails) + } else { + currentSessionData + } + } + } + + // TODO(mrober): If data collection is enabled for at least one subscriber... + // https://github.com/firebase/firebase-android-sdk/blob/a53ab64150608c2eb3eafb17d81dfe217687d955/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/FirebaseSessions.kt#L110 + sessionFirelogPublisher.logSession(sessionDetails = newSessionDetails) + } + } + + private fun shouldInitiateNewSession(sessionData: SessionData): Boolean { + val interval = timeProvider.currentTime() - sessionData.backgroundTime + return interval > sessionsSettings.sessionRestartTimeout + } + + private companion object { + const val TAG = "SharedSessionRepository" + } +} diff --git a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/TimeProvider.kt b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/TimeProvider.kt index 869b64b2ff2..933decdd3e1 100644 --- a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/TimeProvider.kt +++ b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/TimeProvider.kt @@ -19,11 +19,15 @@ package com.google.firebase.sessions import android.os.SystemClock import kotlin.time.Duration import kotlin.time.Duration.Companion.milliseconds +import kotlinx.serialization.Serializable /** Time with accessors for microseconds, milliseconds, and seconds. */ +@Serializable internal data class Time(val ms: Long) { val us = ms * 1_000 val seconds = ms / 1_000 + + operator fun minus(time: Time): Duration = (ms - time.ms).milliseconds } /** Time provider interface, for testing purposes. */ diff --git a/firebase-sessions/src/test/AndroidManifest.xml b/firebase-sessions/src/test/AndroidManifest.xml deleted file mode 100644 index 4eccb7649da..00000000000 --- a/firebase-sessions/src/test/AndroidManifest.xml +++ /dev/null @@ -1,33 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/ApplicationInfoTest.kt b/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/ApplicationInfoTest.kt index 3c6ccf8644d..b7bdb7730e8 100644 --- a/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/ApplicationInfoTest.kt +++ b/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/ApplicationInfoTest.kt @@ -56,13 +56,13 @@ class ApplicationInfoTest { deviceManufacturer = Build.MANUFACTURER, actualCurrentProcessDetails, actualAppProcessDetails, - ) + ), ) ) } @Test - fun applicationInfo_missiongVersionCode_populatesInfoCorrectly() { + fun applicationInfo_missingVersionCode_populatesInfoCorrectly() { // Initialize Firebase with no version code set. val firebaseApp = Firebase.initialize( @@ -71,7 +71,7 @@ class ApplicationInfoTest { .setApplicationId(FakeFirebaseApp.MOCK_APP_ID) .setApiKey(FakeFirebaseApp.MOCK_API_KEY) .setProjectId(FakeFirebaseApp.MOCK_PROJECT_ID) - .build() + .build(), ) val actualCurrentProcessDetails = @@ -96,7 +96,7 @@ class ApplicationInfoTest { deviceManufacturer = Build.MANUFACTURER, actualCurrentProcessDetails, actualAppProcessDetails, - ) + ), ) ) } diff --git a/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/EventGDTLoggerTest.kt b/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/EventGDTLoggerTest.kt index b636d53e3dc..fb3e58f44d6 100644 --- a/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/EventGDTLoggerTest.kt +++ b/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/EventGDTLoggerTest.kt @@ -27,14 +27,12 @@ import com.google.firebase.sessions.testing.FakeProvider import com.google.firebase.sessions.testing.FakeSettingsProvider import com.google.firebase.sessions.testing.FakeTransportFactory import com.google.firebase.sessions.testing.TestSessionEventData -import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest import org.junit.After import org.junit.Test import org.junit.runner.RunWith import org.robolectric.RobolectricTestRunner -@OptIn(ExperimentalCoroutinesApi::class) @RunWith(RobolectricTestRunner::class) class EventGDTLoggerTest { diff --git a/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/SessionDatastoreTest.kt b/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/SessionDatastoreTest.kt deleted file mode 100644 index efe7bb27a97..00000000000 --- a/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/SessionDatastoreTest.kt +++ /dev/null @@ -1,59 +0,0 @@ -/* - * 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.content.Context -import androidx.datastore.core.DataStoreFactory -import androidx.datastore.dataStoreFile -import androidx.test.core.app.ApplicationProvider -import androidx.test.ext.junit.runners.AndroidJUnit4 -import com.google.common.truth.Truth.assertThat -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.test.StandardTestDispatcher -import kotlinx.coroutines.test.runCurrent -import kotlinx.coroutines.test.runTest -import org.junit.Test -import org.junit.runner.RunWith - -@OptIn(ExperimentalCoroutinesApi::class) -@RunWith(AndroidJUnit4::class) -class SessionDatastoreTest { - private val appContext: Context = ApplicationProvider.getApplicationContext() - - @Test - fun getCurrentSessionId_returnsLatest() = runTest { - val sessionDatastore = - SessionDatastoreImpl( - backgroundDispatcher = StandardTestDispatcher(testScheduler, "background"), - sessionDataStore = - DataStoreFactory.create( - serializer = SessionDataSerializer, - scope = CoroutineScope(StandardTestDispatcher(testScheduler, "blocking")), - produceFile = { appContext.dataStoreFile("sessionDataStore.data") }, - ), - ) - - sessionDatastore.updateSessionId("sessionId1") - sessionDatastore.updateSessionId("sessionId2") - sessionDatastore.updateSessionId("sessionId3") - - runCurrent() - - assertThat(sessionDatastore.getCurrentSessionId()).isEqualTo("sessionId3") - } -} diff --git a/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/SessionEventEncoderTest.kt b/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/SessionEventEncoderTest.kt index 6f919cf946b..70772e733cb 100644 --- a/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/SessionEventEncoderTest.kt +++ b/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/SessionEventEncoderTest.kt @@ -27,13 +27,11 @@ import com.google.firebase.sessions.testing.FakeFirebaseApp import com.google.firebase.sessions.testing.FakeSessionSubscriber import com.google.firebase.sessions.testing.FakeSettingsProvider import com.google.firebase.sessions.testing.TestSessionEventData -import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest import org.junit.After import org.junit.Test import org.junit.runner.RunWith -@OptIn(ExperimentalCoroutinesApi::class) @RunWith(AndroidJUnit4::class) class SessionEventEncoderTest { diff --git a/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/SessionEventTest.kt b/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/SessionEventTest.kt index 682371491df..16fe6547ca6 100644 --- a/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/SessionEventTest.kt +++ b/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/SessionEventTest.kt @@ -27,14 +27,12 @@ import com.google.firebase.sessions.testing.TestSessionEventData.TEST_DATA_COLLE import com.google.firebase.sessions.testing.TestSessionEventData.TEST_SESSION_DATA import com.google.firebase.sessions.testing.TestSessionEventData.TEST_SESSION_DETAILS import com.google.firebase.sessions.testing.TestSessionEventData.TEST_SESSION_EVENT -import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest import org.junit.After import org.junit.Test import org.junit.runner.RunWith import org.robolectric.RobolectricTestRunner -@OptIn(ExperimentalCoroutinesApi::class) @RunWith(RobolectricTestRunner::class) class SessionEventTest { @Test diff --git a/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/SessionGeneratorTest.kt b/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/SessionGeneratorTest.kt index bf260e73a4f..9aab92b766a 100644 --- a/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/SessionGeneratorTest.kt +++ b/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/SessionGeneratorTest.kt @@ -39,47 +39,21 @@ class SessionGeneratorTest { return true } - // This test case isn't important behavior. Nothing should access - // currentSession before generateNewSession has been called. - @Test(expected = UninitializedPropertyAccessException::class) - fun currentSession_beforeGenerate_throwsUninitialized() { - val sessionGenerator = - SessionGenerator(timeProvider = FakeTimeProvider(), uuidGenerator = UuidGeneratorImpl) - - sessionGenerator.currentSession - } - - @Test - fun hasGenerateSession_beforeGenerate_returnsFalse() { - val sessionGenerator = - SessionGenerator(timeProvider = FakeTimeProvider(), uuidGenerator = UuidGeneratorImpl) - - assertThat(sessionGenerator.hasGenerateSession).isFalse() - } - - @Test - fun hasGenerateSession_afterGenerate_returnsTrue() { - val sessionGenerator = - SessionGenerator(timeProvider = FakeTimeProvider(), uuidGenerator = UuidGeneratorImpl) - - sessionGenerator.generateNewSession() - - assertThat(sessionGenerator.hasGenerateSession).isTrue() - } - @Test fun generateNewSession_generatesValidSessionIds() { val sessionGenerator = SessionGenerator(timeProvider = FakeTimeProvider(), uuidGenerator = UuidGeneratorImpl) - sessionGenerator.generateNewSession() + val sessionDetails = sessionGenerator.generateNewSession(currentSession = null) - assertThat(isValidSessionId(sessionGenerator.currentSession.sessionId)).isTrue() - assertThat(isValidSessionId(sessionGenerator.currentSession.firstSessionId)).isTrue() + assertThat(isValidSessionId(sessionDetails.sessionId)).isTrue() + assertThat(isValidSessionId(sessionDetails.firstSessionId)).isTrue() // Validate several random session ids. + var currentSession = sessionDetails repeat(16) { - assertThat(isValidSessionId(sessionGenerator.generateNewSession().sessionId)).isTrue() + currentSession = sessionGenerator.generateNewSession(currentSession) + assertThat(isValidSessionId(currentSession.sessionId)).isTrue() } } @@ -88,12 +62,12 @@ class SessionGeneratorTest { val sessionGenerator = SessionGenerator(timeProvider = FakeTimeProvider(), uuidGenerator = FakeUuidGenerator()) - sessionGenerator.generateNewSession() + val sessionDetails = sessionGenerator.generateNewSession(currentSession = null) - assertThat(isValidSessionId(sessionGenerator.currentSession.sessionId)).isTrue() - assertThat(isValidSessionId(sessionGenerator.currentSession.firstSessionId)).isTrue() + assertThat(isValidSessionId(sessionDetails.sessionId)).isTrue() + assertThat(isValidSessionId(sessionDetails.firstSessionId)).isTrue() - assertThat(sessionGenerator.currentSession) + assertThat(sessionDetails) .isEqualTo( SessionDetails( sessionId = SESSION_ID_1, @@ -111,7 +85,7 @@ class SessionGeneratorTest { val sessionGenerator = SessionGenerator(timeProvider = FakeTimeProvider(), uuidGenerator = FakeUuidGenerator()) - val firstSessionDetails = sessionGenerator.generateNewSession() + val firstSessionDetails = sessionGenerator.generateNewSession(currentSession = null) assertThat(isValidSessionId(firstSessionDetails.sessionId)).isTrue() assertThat(isValidSessionId(firstSessionDetails.firstSessionId)).isTrue() @@ -126,7 +100,8 @@ class SessionGeneratorTest { ) ) - val secondSessionDetails = sessionGenerator.generateNewSession() + val secondSessionDetails = + sessionGenerator.generateNewSession(currentSession = firstSessionDetails) assertThat(isValidSessionId(secondSessionDetails.sessionId)).isTrue() assertThat(isValidSessionId(secondSessionDetails.firstSessionId)).isTrue() @@ -143,7 +118,8 @@ class SessionGeneratorTest { ) // Do a third round just in case - val thirdSessionDetails = sessionGenerator.generateNewSession() + val thirdSessionDetails = + sessionGenerator.generateNewSession(currentSession = secondSessionDetails) assertThat(isValidSessionId(thirdSessionDetails.sessionId)).isTrue() assertThat(isValidSessionId(thirdSessionDetails.firstSessionId)).isTrue() diff --git a/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/SessionLifecycleClientTest.kt b/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/SessionLifecycleClientTest.kt deleted file mode 100644 index 12a017a7462..00000000000 --- a/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/SessionLifecycleClientTest.kt +++ /dev/null @@ -1,281 +0,0 @@ -/* - * Copyright 2023 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.os.Looper -import androidx.test.core.app.ApplicationProvider -import androidx.test.filters.MediumTest -import com.google.common.truth.Truth.assertThat -import com.google.firebase.Firebase -import com.google.firebase.FirebaseApp -import com.google.firebase.FirebaseOptions -import com.google.firebase.concurrent.TestOnlyExecutors -import com.google.firebase.initialize -import com.google.firebase.sessions.api.FirebaseSessionsDependencies -import com.google.firebase.sessions.api.SessionSubscriber -import com.google.firebase.sessions.api.SessionSubscriber.SessionDetails -import com.google.firebase.sessions.testing.FakeFirebaseApp -import com.google.firebase.sessions.testing.FakeSessionLifecycleServiceBinder -import com.google.firebase.sessions.testing.FakeSessionSubscriber -import com.google.firebase.sessions.testing.FirebaseSessionsFakeComponent -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.asCoroutineDispatcher -import kotlinx.coroutines.test.UnconfinedTestDispatcher -import kotlinx.coroutines.test.runTest -import org.junit.After -import org.junit.Before -import org.junit.Test -import org.junit.runner.RunWith -import org.robolectric.RobolectricTestRunner -import org.robolectric.Shadows.shadowOf - -@OptIn(ExperimentalCoroutinesApi::class) -@MediumTest -@RunWith(RobolectricTestRunner::class) -internal class SessionLifecycleClientTest { - private lateinit var fakeService: FakeSessionLifecycleServiceBinder - private lateinit var lifecycleServiceBinder: SessionLifecycleServiceBinder - - @Before - fun setUp() { - Firebase.initialize( - ApplicationProvider.getApplicationContext(), - FirebaseOptions.Builder() - .setApplicationId(FakeFirebaseApp.MOCK_APP_ID) - .setApiKey(FakeFirebaseApp.MOCK_API_KEY) - .setProjectId(FakeFirebaseApp.MOCK_PROJECT_ID) - .build(), - ) - - fakeService = FirebaseSessionsFakeComponent.instance.fakeSessionLifecycleServiceBinder - lifecycleServiceBinder = FirebaseSessionsFakeComponent.instance.sessionLifecycleServiceBinder - } - - @After - fun cleanUp() { - fakeService.serviceDisconnected() - FirebaseApp.clearInstancesForTest() - fakeService.clearForTest() - FirebaseSessionsDependencies.reset() - } - - @Test - fun bindToService_registersCallbacks() = - runTest(UnconfinedTestDispatcher()) { - val client = SessionLifecycleClient(backgroundDispatcher() + coroutineContext) - addSubscriber(true, SessionSubscriber.Name.CRASHLYTICS) - client.bindToService(lifecycleServiceBinder) - - waitForMessages() - assertThat(fakeService.clientCallbacks).hasSize(1) - assertThat(fakeService.connectionCallbacks).hasSize(1) - } - - @Test - fun onServiceConnected_sendsQueuedMessages() = - runTest(UnconfinedTestDispatcher()) { - val client = SessionLifecycleClient(backgroundDispatcher() + coroutineContext) - addSubscriber(true, SessionSubscriber.Name.CRASHLYTICS) - client.bindToService(lifecycleServiceBinder) - client.foregrounded() - client.backgrounded() - - fakeService.serviceConnected() - - waitForMessages() - assertThat(fakeService.receivedMessageCodes) - .containsExactly(SessionLifecycleService.FOREGROUNDED, SessionLifecycleService.BACKGROUNDED) - } - - @Test - fun onServiceConnected_sendsOnlyLatestMessages() = - runTest(UnconfinedTestDispatcher()) { - val client = SessionLifecycleClient(backgroundDispatcher() + coroutineContext) - addSubscriber(true, SessionSubscriber.Name.CRASHLYTICS) - client.bindToService(lifecycleServiceBinder) - client.foregrounded() - client.backgrounded() - client.foregrounded() - client.backgrounded() - client.foregrounded() - client.backgrounded() - - fakeService.serviceConnected() - - waitForMessages() - assertThat(fakeService.receivedMessageCodes) - .containsExactly(SessionLifecycleService.FOREGROUNDED, SessionLifecycleService.BACKGROUNDED) - } - - @Test - fun onServiceDisconnected_noMoreEventsSent() = - runTest(UnconfinedTestDispatcher()) { - val client = SessionLifecycleClient(backgroundDispatcher() + coroutineContext) - addSubscriber(true, SessionSubscriber.Name.CRASHLYTICS) - client.bindToService(lifecycleServiceBinder) - - fakeService.serviceConnected() - fakeService.serviceDisconnected() - client.foregrounded() - client.backgrounded() - - waitForMessages() - assertThat(fakeService.receivedMessageCodes).isEmpty() - } - - @Test - fun serviceReconnection_handlesNewMessages() = - runTest(UnconfinedTestDispatcher()) { - val client = SessionLifecycleClient(backgroundDispatcher() + coroutineContext) - addSubscriber(true, SessionSubscriber.Name.CRASHLYTICS) - client.bindToService(lifecycleServiceBinder) - - fakeService.serviceConnected() - fakeService.serviceDisconnected() - fakeService.serviceConnected() - client.foregrounded() - client.backgrounded() - - waitForMessages() - assertThat(fakeService.receivedMessageCodes) - .containsExactly(SessionLifecycleService.FOREGROUNDED, SessionLifecycleService.BACKGROUNDED) - } - - @Test - fun serviceReconnection_queuesOldMessages() = - runTest(UnconfinedTestDispatcher()) { - val client = SessionLifecycleClient(backgroundDispatcher() + coroutineContext) - addSubscriber(true, SessionSubscriber.Name.CRASHLYTICS) - client.bindToService(lifecycleServiceBinder) - - fakeService.serviceConnected() - fakeService.serviceDisconnected() - client.foregrounded() - client.backgrounded() - fakeService.serviceConnected() - - waitForMessages() - assertThat(fakeService.receivedMessageCodes) - .containsExactly(SessionLifecycleService.FOREGROUNDED, SessionLifecycleService.BACKGROUNDED) - } - - @Test - fun doesNotSendLifecycleEventsWithoutSubscribers() = - runTest(UnconfinedTestDispatcher()) { - val client = SessionLifecycleClient(backgroundDispatcher() + coroutineContext) - client.bindToService(lifecycleServiceBinder) - - fakeService.serviceConnected() - client.foregrounded() - client.backgrounded() - - waitForMessages() - assertThat(fakeService.receivedMessageCodes).isEmpty() - } - - @Test - fun doesNotSendLifecycleEventsWithoutEnabledSubscribers() = - runTest(UnconfinedTestDispatcher()) { - val client = SessionLifecycleClient(backgroundDispatcher() + coroutineContext) - addSubscriber(collectionEnabled = false, SessionSubscriber.Name.CRASHLYTICS) - addSubscriber(collectionEnabled = false, SessionSubscriber.Name.MATT_SAYS_HI) - client.bindToService(lifecycleServiceBinder) - - fakeService.serviceConnected() - client.foregrounded() - client.backgrounded() - - waitForMessages() - assertThat(fakeService.receivedMessageCodes).isEmpty() - } - - @Test - fun sendsLifecycleEventsWhenAtLeastOneEnabledSubscriber() = - runTest(UnconfinedTestDispatcher()) { - val client = SessionLifecycleClient(backgroundDispatcher() + coroutineContext) - addSubscriber(collectionEnabled = true, SessionSubscriber.Name.CRASHLYTICS) - addSubscriber(collectionEnabled = false, SessionSubscriber.Name.MATT_SAYS_HI) - client.bindToService(lifecycleServiceBinder) - - fakeService.serviceConnected() - client.foregrounded() - client.backgrounded() - - waitForMessages() - assertThat(fakeService.receivedMessageCodes).hasSize(2) - } - - @Test - fun handleSessionUpdate_noSubscribers() = - runTest(UnconfinedTestDispatcher()) { - val client = SessionLifecycleClient(backgroundDispatcher() + coroutineContext) - client.bindToService(lifecycleServiceBinder) - - fakeService.serviceConnected() - fakeService.broadcastSession("123") - - waitForMessages() - } - - @Test - fun handleSessionUpdate_sendsToSubscribers() = - runTest(UnconfinedTestDispatcher()) { - val client = SessionLifecycleClient(backgroundDispatcher() + coroutineContext) - val crashlyticsSubscriber = addSubscriber(true, SessionSubscriber.Name.CRASHLYTICS) - val mattSaysHiSubscriber = addSubscriber(true, SessionSubscriber.Name.MATT_SAYS_HI) - client.bindToService(lifecycleServiceBinder) - - fakeService.serviceConnected() - fakeService.broadcastSession("123") - - waitForMessages() - assertThat(crashlyticsSubscriber.sessionChangedEvents).containsExactly(SessionDetails("123")) - assertThat(mattSaysHiSubscriber.sessionChangedEvents).containsExactly(SessionDetails("123")) - } - - @Test - fun handleSessionUpdate_sendsToAllSubscribersAsLongAsOneIsEnabled() = - runTest(UnconfinedTestDispatcher()) { - val client = SessionLifecycleClient(backgroundDispatcher() + coroutineContext) - val crashlyticsSubscriber = addSubscriber(true, SessionSubscriber.Name.CRASHLYTICS) - val mattSaysHiSubscriber = addSubscriber(false, SessionSubscriber.Name.MATT_SAYS_HI) - client.bindToService(lifecycleServiceBinder) - - fakeService.serviceConnected() - fakeService.broadcastSession("123") - - waitForMessages() - assertThat(crashlyticsSubscriber.sessionChangedEvents).containsExactly(SessionDetails("123")) - assertThat(mattSaysHiSubscriber.sessionChangedEvents).containsExactly(SessionDetails("123")) - } - - private fun addSubscriber( - collectionEnabled: Boolean, - name: SessionSubscriber.Name, - ): FakeSessionSubscriber { - val fakeSubscriber = FakeSessionSubscriber(collectionEnabled, sessionSubscriberName = name) - FirebaseSessionsDependencies.addDependency(name) - FirebaseSessionsDependencies.register(fakeSubscriber) - return fakeSubscriber - } - - private fun waitForMessages() { - shadowOf(Looper.getMainLooper()).idle() - } - - private fun backgroundDispatcher() = TestOnlyExecutors.background().asCoroutineDispatcher() -} diff --git a/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/SessionLifecycleServiceTest.kt b/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/SessionLifecycleServiceTest.kt deleted file mode 100644 index ccd933f1213..00000000000 --- a/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/SessionLifecycleServiceTest.kt +++ /dev/null @@ -1,234 +0,0 @@ -/* - * Copyright 2023 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.content.Intent -import android.os.Handler -import android.os.Looper -import android.os.Message -import android.os.Messenger -import androidx.test.core.app.ApplicationProvider -import androidx.test.filters.MediumTest -import com.google.common.truth.Truth.assertThat -import com.google.firebase.Firebase -import com.google.firebase.FirebaseApp -import com.google.firebase.FirebaseOptions -import com.google.firebase.initialize -import com.google.firebase.sessions.testing.FakeFirebaseApp -import com.google.firebase.sessions.testing.FirebaseSessionsFakeComponent -import java.time.Duration -import org.junit.After -import org.junit.Before -import org.junit.Test -import org.junit.runner.RunWith -import org.robolectric.Robolectric -import org.robolectric.RobolectricTestRunner -import org.robolectric.Shadows.shadowOf -import org.robolectric.android.controller.ServiceController -import org.robolectric.annotation.LooperMode -import org.robolectric.annotation.LooperMode.Mode.PAUSED -import org.robolectric.shadows.ShadowSystemClock - -@MediumTest -@LooperMode(PAUSED) -@RunWith(RobolectricTestRunner::class) -internal class SessionLifecycleServiceTest { - private lateinit var service: ServiceController - - data class CallbackMessage(val code: Int, val sessionId: String?) - - internal inner class TestCallbackHandler(looper: Looper = Looper.getMainLooper()) : - Handler(looper) { - val callbackMessages = ArrayList() - - override fun handleMessage(msg: Message) { - callbackMessages.add(CallbackMessage(msg.what, getSessionId(msg))) - } - } - - @Before - fun setUp() { - Firebase.initialize( - ApplicationProvider.getApplicationContext(), - FirebaseOptions.Builder() - .setApplicationId(FakeFirebaseApp.MOCK_APP_ID) - .setApiKey(FakeFirebaseApp.MOCK_API_KEY) - .setProjectId(FakeFirebaseApp.MOCK_PROJECT_ID) - .build(), - ) - service = createService() - } - - @After - fun cleanUp() { - FirebaseApp.clearInstancesForTest() - } - - @Test - fun binding_noCallbackOnInitialBindingWhenNoneStored() { - val client = TestCallbackHandler() - - bindToService(client) - - waitForAllMessages() - assertThat(client.callbackMessages).isEmpty() - } - - @Test - fun binding_callbackOnInitialBindWhenSessionIdSet() { - val client = TestCallbackHandler() - FirebaseSessionsFakeComponent.instance.fakeSessionDatastore.updateSessionId("123") - - bindToService(client) - - waitForAllMessages() - assertThat(client.callbackMessages).hasSize(1) - val msg = client.callbackMessages.first() - assertThat(msg.code).isEqualTo(SessionLifecycleService.SESSION_UPDATED) - assertThat(msg.sessionId).isNotEmpty() - // We should not send stored session IDs to firelog - assertThat(getUploadedSessions()).isEmpty() - } - - @Test - fun foregrounding_startsSessionOnFirstForegrounding() { - val client = TestCallbackHandler() - val messenger = bindToService(client) - - messenger.send(Message.obtain(null, SessionLifecycleService.FOREGROUNDED, 0, 0)) - - waitForAllMessages() - assertThat(client.callbackMessages).hasSize(1) - assertThat(getUploadedSessions()).hasSize(1) - assertThat(client.callbackMessages.first().code) - .isEqualTo(SessionLifecycleService.SESSION_UPDATED) - assertThat(client.callbackMessages.first().sessionId).isNotEmpty() - assertThat(getUploadedSessions().first().sessionId) - .isEqualTo(client.callbackMessages.first().sessionId) - } - - @Test - fun foregrounding_onlyOneSessionOnMultipleForegroundings() { - val client = TestCallbackHandler() - val messenger = bindToService(client) - - messenger.send(Message.obtain(null, SessionLifecycleService.FOREGROUNDED, 0, 0)) - messenger.send(Message.obtain(null, SessionLifecycleService.FOREGROUNDED, 0, 0)) - messenger.send(Message.obtain(null, SessionLifecycleService.FOREGROUNDED, 0, 0)) - - waitForAllMessages() - assertThat(client.callbackMessages).hasSize(1) - assertThat(getUploadedSessions()).hasSize(1) - } - - @Test - fun foregrounding_newSessionAfterLongDelay() { - val client = TestCallbackHandler() - val messenger = bindToService(client) - - messenger.send(Message.obtain(null, SessionLifecycleService.FOREGROUNDED, 0, 0)) - ShadowSystemClock.advanceBy(Duration.ofMinutes(31)) - messenger.send(Message.obtain(null, SessionLifecycleService.FOREGROUNDED, 0, 0)) - - waitForAllMessages() - assertThat(client.callbackMessages).hasSize(2) - assertThat(getUploadedSessions()).hasSize(2) - assertThat(client.callbackMessages.first().sessionId) - .isNotEqualTo(client.callbackMessages.last().sessionId) - assertThat(getUploadedSessions().first().sessionId) - .isEqualTo(client.callbackMessages.first().sessionId) - assertThat(getUploadedSessions().last().sessionId) - .isEqualTo(client.callbackMessages.last().sessionId) - } - - @Test - fun sendsSessionsToMultipleClients() { - val client1 = TestCallbackHandler() - val client2 = TestCallbackHandler() - val client3 = TestCallbackHandler() - bindToService(client1) - val messenger = bindToService(client2) - bindToService(client3) - waitForAllMessages() - - messenger.send(Message.obtain(null, SessionLifecycleService.FOREGROUNDED, 0, 0)) - - waitForAllMessages() - assertThat(client1.callbackMessages).hasSize(1) - assertThat(client1.callbackMessages).isEqualTo(client2.callbackMessages) - assertThat(client1.callbackMessages).isEqualTo(client3.callbackMessages) - assertThat(getUploadedSessions()).hasSize(1) - } - - @Test - fun onlyOneSessionForMultipleClientsForegrounding() { - val client1 = TestCallbackHandler() - val client2 = TestCallbackHandler() - val client3 = TestCallbackHandler() - val messenger1 = bindToService(client1) - val messenger2 = bindToService(client2) - val messenger3 = bindToService(client3) - waitForAllMessages() - - messenger1.send(Message.obtain(null, SessionLifecycleService.FOREGROUNDED, 0, 0)) - messenger1.send(Message.obtain(null, SessionLifecycleService.BACKGROUNDED, 0, 0)) - messenger2.send(Message.obtain(null, SessionLifecycleService.FOREGROUNDED, 0, 0)) - messenger2.send(Message.obtain(null, SessionLifecycleService.BACKGROUNDED, 0, 0)) - messenger3.send(Message.obtain(null, SessionLifecycleService.FOREGROUNDED, 0, 0)) - - waitForAllMessages() - assertThat(client1.callbackMessages).hasSize(1) - assertThat(client1.callbackMessages).isEqualTo(client2.callbackMessages) - assertThat(client1.callbackMessages).isEqualTo(client3.callbackMessages) - assertThat(getUploadedSessions()).hasSize(1) - } - - @Test - fun backgrounding_doesNotStartSession() { - val client = TestCallbackHandler() - val messenger = bindToService(client) - - messenger.send(Message.obtain(null, SessionLifecycleService.BACKGROUNDED, 0, 0)) - - waitForAllMessages() - assertThat(client.callbackMessages).isEmpty() - assertThat(getUploadedSessions()).isEmpty() - } - - private fun bindToService(client: TestCallbackHandler): Messenger { - return Messenger(service.get()?.onBind(createServiceLaunchIntent(client))) - } - - private fun createServiceLaunchIntent(client: TestCallbackHandler) = - Intent(ApplicationProvider.getApplicationContext(), SessionLifecycleService::class.java).apply { - putExtra(SessionLifecycleService.CLIENT_CALLBACK_MESSENGER, Messenger(client)) - } - - private fun createService() = - Robolectric.buildService(SessionLifecycleService::class.java).create() - - private fun waitForAllMessages() { - shadowOf(service.get()?.handlerThread?.getLooper()).idle() - shadowOf(Looper.getMainLooper()).idle() - } - - private fun getUploadedSessions() = - FirebaseSessionsFakeComponent.instance.fakeFirelogPublisher.loggedSessions - - private fun getSessionId(msg: Message) = - msg.data?.getString(SessionLifecycleService.SESSION_UPDATE_EXTRA) -} diff --git a/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/SessionsActivityLifecycleCallbacksTest.kt b/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/SessionsActivityLifecycleCallbacksTest.kt deleted file mode 100644 index 62e650d90c8..00000000000 --- a/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/SessionsActivityLifecycleCallbacksTest.kt +++ /dev/null @@ -1,132 +0,0 @@ -/* - * Copyright 2023 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.os.Looper -import androidx.test.core.app.ApplicationProvider -import androidx.test.ext.junit.runners.AndroidJUnit4 -import com.google.common.truth.Truth.assertThat -import com.google.firebase.Firebase -import com.google.firebase.FirebaseApp -import com.google.firebase.FirebaseOptions -import com.google.firebase.concurrent.TestOnlyExecutors -import com.google.firebase.initialize -import com.google.firebase.sessions.api.FirebaseSessionsDependencies -import com.google.firebase.sessions.api.SessionSubscriber -import com.google.firebase.sessions.testing.FakeFirebaseApp -import com.google.firebase.sessions.testing.FakeSessionLifecycleServiceBinder -import com.google.firebase.sessions.testing.FakeSessionSubscriber -import com.google.firebase.sessions.testing.FirebaseSessionsFakeComponent -import kotlin.coroutines.CoroutineContext -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.asCoroutineDispatcher -import kotlinx.coroutines.test.UnconfinedTestDispatcher -import kotlinx.coroutines.test.runTest -import org.junit.After -import org.junit.Before -import org.junit.Test -import org.junit.runner.RunWith -import org.robolectric.Shadows - -@OptIn(ExperimentalCoroutinesApi::class) -@RunWith(AndroidJUnit4::class) -internal class SessionsActivityLifecycleCallbacksTest { - private lateinit var fakeService: FakeSessionLifecycleServiceBinder - private lateinit var lifecycleServiceBinder: SessionLifecycleServiceBinder - private val fakeActivity = Activity() - - @Before - fun setUp() { - // Reset the state of the SessionsActivityLifecycleCallbacks object. - SessionsActivityLifecycleCallbacks.hasPendingForeground = false - SessionsActivityLifecycleCallbacks.lifecycleClient = null - - FirebaseSessionsDependencies.addDependency(SessionSubscriber.Name.MATT_SAYS_HI) - FirebaseSessionsDependencies.register( - FakeSessionSubscriber( - isDataCollectionEnabled = true, - sessionSubscriberName = SessionSubscriber.Name.MATT_SAYS_HI, - ) - ) - - Firebase.initialize( - ApplicationProvider.getApplicationContext(), - FirebaseOptions.Builder() - .setApplicationId(FakeFirebaseApp.MOCK_APP_ID) - .setApiKey(FakeFirebaseApp.MOCK_API_KEY) - .setProjectId(FakeFirebaseApp.MOCK_PROJECT_ID) - .build(), - ) - - fakeService = FirebaseSessionsFakeComponent.instance.fakeSessionLifecycleServiceBinder - lifecycleServiceBinder = FirebaseSessionsFakeComponent.instance.sessionLifecycleServiceBinder - } - - @After - fun cleanUp() { - fakeService.serviceDisconnected() - FirebaseApp.clearInstancesForTest() - fakeService.clearForTest() - FirebaseSessionsDependencies.reset() - } - - @Test - fun hasPendingForeground_thenSetLifecycleClient_callsBackgrounded() = - runTest(UnconfinedTestDispatcher()) { - val lifecycleClient = SessionLifecycleClient(backgroundDispatcher(coroutineContext)) - - // Activity comes to foreground before the lifecycle client was set due to no settings. - SessionsActivityLifecycleCallbacks.onActivityResumed(fakeActivity) - - // Settings fetched and set the lifecycle client. - lifecycleClient.bindToService(lifecycleServiceBinder) - fakeService.serviceConnected() - SessionsActivityLifecycleCallbacks.lifecycleClient = lifecycleClient - - // Assert lifecycleClient.foregrounded got called. - waitForMessages() - assertThat(fakeService.receivedMessageCodes).hasSize(1) - } - - @Test - fun noPendingForeground_thenSetLifecycleClient_doesNotCallBackgrounded() = - runTest(UnconfinedTestDispatcher()) { - val lifecycleClient = SessionLifecycleClient(backgroundDispatcher(coroutineContext)) - - // Set lifecycle client before any foreground happened. - lifecycleClient.bindToService(lifecycleServiceBinder) - fakeService.serviceConnected() - SessionsActivityLifecycleCallbacks.lifecycleClient = lifecycleClient - - // Assert lifecycleClient.foregrounded did not get called. - waitForMessages() - assertThat(fakeService.receivedMessageCodes).hasSize(0) - - // Activity comes to foreground. - SessionsActivityLifecycleCallbacks.onActivityResumed(fakeActivity) - - // Assert lifecycleClient.foregrounded did get called. - waitForMessages() - assertThat(fakeService.receivedMessageCodes).hasSize(1) - } - - private fun waitForMessages() = Shadows.shadowOf(Looper.getMainLooper()).idle() - - private fun backgroundDispatcher(coroutineContext: CoroutineContext) = - TestOnlyExecutors.background().asCoroutineDispatcher() + coroutineContext -} diff --git a/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/api/FirebaseSessionsDependenciesTest.kt b/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/api/FirebaseSessionsDependenciesTest.kt index f662fe13e90..d9095dbbd7d 100644 --- a/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/api/FirebaseSessionsDependenciesTest.kt +++ b/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/api/FirebaseSessionsDependenciesTest.kt @@ -23,7 +23,6 @@ import com.google.firebase.sessions.api.SessionSubscriber.Name.MATT_SAYS_HI import com.google.firebase.sessions.testing.FakeSessionSubscriber import kotlin.time.Duration.Companion.seconds import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.TimeoutCancellationException import kotlinx.coroutines.delay import kotlinx.coroutines.launch @@ -34,7 +33,6 @@ import org.junit.Assert.assertThrows import org.junit.Test import org.junit.runner.RunWith -@OptIn(ExperimentalCoroutinesApi::class) @RunWith(AndroidJUnit4::class) class FirebaseSessionsDependenciesTest { @After diff --git a/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/testing/FakeFirelogPublisher.kt b/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/testing/FakeFirelogPublisher.kt deleted file mode 100644 index 2975447bbaa..00000000000 --- a/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/testing/FakeFirelogPublisher.kt +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright 2023 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.testing - -import com.google.firebase.sessions.SessionDetails -import com.google.firebase.sessions.SessionFirelogPublisher - -/** - * Fake implementation of [SessionFirelogPublisher] that allows for inspecting the session details - * that were sent to it. - */ -internal class FakeFirelogPublisher : SessionFirelogPublisher { - - /** All the sessions that were uploaded via this fake [SessionFirelogPublisher] */ - val loggedSessions = ArrayList() - - override fun logSession(sessionDetails: SessionDetails) { - loggedSessions.add(sessionDetails) - } -} diff --git a/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/testing/FakeSessionLifecycleServiceBinder.kt b/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/testing/FakeSessionLifecycleServiceBinder.kt deleted file mode 100644 index 0d4e58e2014..00000000000 --- a/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/testing/FakeSessionLifecycleServiceBinder.kt +++ /dev/null @@ -1,88 +0,0 @@ -/* - * Copyright 2023 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.testing - -import android.content.ComponentName -import android.content.ServiceConnection -import android.os.Bundle -import android.os.Handler -import android.os.Looper -import android.os.Message -import android.os.Messenger -import com.google.firebase.sessions.SessionLifecycleService -import com.google.firebase.sessions.SessionLifecycleServiceBinder -import java.util.concurrent.LinkedBlockingQueue -import org.robolectric.Shadows.shadowOf - -/** - * Fake implementation of the [SessionLifecycleServiceBinder] that allows for inspecting the - * callbacks and received messages of the service in unit tests. - */ -internal class FakeSessionLifecycleServiceBinder : SessionLifecycleServiceBinder { - - val clientCallbacks = mutableListOf() - val connectionCallbacks = mutableListOf() - val receivedMessageCodes = LinkedBlockingQueue() - var service = Messenger(FakeServiceHandler()) - - internal inner class FakeServiceHandler() : Handler(Looper.getMainLooper()) { - override fun handleMessage(msg: Message) { - receivedMessageCodes.add(msg.what) - } - } - - override fun bindToService(callback: Messenger, serviceConnection: ServiceConnection) { - clientCallbacks.add(callback) - connectionCallbacks.add(serviceConnection) - } - - fun serviceConnected() { - connectionCallbacks.forEach { it.onServiceConnected(componentName, service.getBinder()) } - } - - fun serviceDisconnected() { - connectionCallbacks.forEach { it.onServiceDisconnected(componentName) } - } - - fun broadcastSession(sessionId: String) { - clientCallbacks.forEach { client -> - val msgData = - Bundle().also { it.putString(SessionLifecycleService.SESSION_UPDATE_EXTRA, sessionId) } - client.send( - Message.obtain(null, SessionLifecycleService.SESSION_UPDATED, 0, 0).also { - it.data = msgData - } - ) - } - } - - fun waitForAllMessages() { - shadowOf(Looper.getMainLooper()).idle() - } - - fun clearForTest() { - clientCallbacks.clear() - connectionCallbacks.clear() - receivedMessageCodes.clear() - service = Messenger(FakeServiceHandler()) - } - - companion object { - val componentName = - ComponentName("com.google.firebase.sessions.testing", "FakeSessionLifecycleServiceBinder") - } -} diff --git a/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/testing/FakeTransportFactory.kt b/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/testing/FakeTransportFactory.kt index da1d273a33f..d5f7dd8f510 100644 --- a/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/testing/FakeTransportFactory.kt +++ b/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/testing/FakeTransportFactory.kt @@ -16,7 +16,12 @@ package com.google.firebase.sessions.testing -import com.google.android.datatransport.* +import com.google.android.datatransport.Encoding +import com.google.android.datatransport.Event +import com.google.android.datatransport.Transformer +import com.google.android.datatransport.Transport +import com.google.android.datatransport.TransportFactory +import com.google.android.datatransport.TransportScheduleCallback import com.google.firebase.sessions.SessionEvent /** Fake [Transport] that implements [send]. */ @@ -34,7 +39,7 @@ internal class FakeTransport() : Transport { } /** Fake [TransportFactory] that implements [getTransport]. */ -internal class FakeTransportFactory() : TransportFactory { +internal class FakeTransportFactory : TransportFactory { var name: String? = null var payloadEncoding: Encoding? = null @@ -42,9 +47,9 @@ internal class FakeTransportFactory() : TransportFactory { override fun getTransport( name: String?, - payloadType: java.lang.Class?, + payloadType: Class?, payloadEncoding: Encoding?, - payloadTransformer: Transformer? + payloadTransformer: Transformer?, ): Transport? { this.name = name this.payloadEncoding = payloadEncoding @@ -54,11 +59,14 @@ internal class FakeTransportFactory() : TransportFactory { return fakeTransport } - @Deprecated("This is deprecated in the API. Don't use or expect on this function.") + @Deprecated( + "This is deprecated in the API. Don't use or expect on this function.", + ReplaceWith("null"), + ) override fun getTransport( name: String?, - payloadType: java.lang.Class?, - payloadTransformer: Transformer? + payloadType: Class?, + payloadTransformer: Transformer?, ): Transport? { return null } diff --git a/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/testing/FirebaseSessionsFakeComponent.kt b/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/testing/FirebaseSessionsFakeComponent.kt deleted file mode 100644 index b3431f71840..00000000000 --- a/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/testing/FirebaseSessionsFakeComponent.kt +++ /dev/null @@ -1,68 +0,0 @@ -/* - * 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.testing - -import com.google.firebase.Firebase -import com.google.firebase.app -import com.google.firebase.sessions.FirebaseSessions -import com.google.firebase.sessions.FirebaseSessionsComponent -import com.google.firebase.sessions.SessionDatastore -import com.google.firebase.sessions.SessionFirelogPublisher -import com.google.firebase.sessions.SessionGenerator -import com.google.firebase.sessions.SessionLifecycleServiceBinder -import com.google.firebase.sessions.settings.SessionsSettings -import com.google.firebase.sessions.settings.SettingsProvider - -/** Fake component to manage [FirebaseSessions] and related, often faked, dependencies. */ -@Suppress("MemberVisibilityCanBePrivate") // Keep access to fakes open for convenience -internal class FirebaseSessionsFakeComponent : FirebaseSessionsComponent { - // TODO(mrober): Move tests that need DI to integration tests, and remove this component. - - // Fakes, access these instances to setup test cases, e.g., add interval to fake time provider. - val fakeTimeProvider = FakeTimeProvider() - val fakeUuidGenerator = FakeUuidGenerator() - val fakeSessionDatastore = FakeSessionDatastore() - val fakeFirelogPublisher = FakeFirelogPublisher() - val fakeSessionLifecycleServiceBinder = FakeSessionLifecycleServiceBinder() - - // Settings providers, default to fake, set these to real instances for relevant test cases. - var localOverrideSettings: SettingsProvider = FakeSettingsProvider() - var remoteSettings: SettingsProvider = FakeSettingsProvider() - - override val firebaseSessions: FirebaseSessions - get() = throw NotImplementedError("FirebaseSessions not implemented, use integration tests.") - - override val sessionDatastore: SessionDatastore = fakeSessionDatastore - - override val sessionFirelogPublisher: SessionFirelogPublisher = fakeFirelogPublisher - - override val sessionGenerator: SessionGenerator by lazy { - SessionGenerator(timeProvider = fakeTimeProvider, uuidGenerator = fakeUuidGenerator) - } - - override val sessionsSettings: SessionsSettings by lazy { - SessionsSettings(localOverrideSettings, remoteSettings) - } - - val sessionLifecycleServiceBinder: SessionLifecycleServiceBinder - get() = fakeSessionLifecycleServiceBinder - - companion object { - val instance: FirebaseSessionsFakeComponent - get() = Firebase.app[FirebaseSessionsFakeComponent::class.java] - } -} diff --git a/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/testing/FirebaseSessionsFakeRegistrar.kt b/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/testing/FirebaseSessionsFakeRegistrar.kt deleted file mode 100644 index 8dc6454931e..00000000000 --- a/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/testing/FirebaseSessionsFakeRegistrar.kt +++ /dev/null @@ -1,52 +0,0 @@ -/* - * Copyright 2023 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.testing - -import com.google.firebase.components.Component -import com.google.firebase.components.ComponentRegistrar -import com.google.firebase.components.Dependency -import com.google.firebase.components.Qualified.unqualified -import com.google.firebase.platforminfo.LibraryVersionComponent -import com.google.firebase.sessions.BuildConfig -import com.google.firebase.sessions.FirebaseSessions -import com.google.firebase.sessions.FirebaseSessionsComponent - -/** - * [ComponentRegistrar] for setting up Fake components for [FirebaseSessions] and its internal - * dependencies for unit tests. - */ -internal class FirebaseSessionsFakeRegistrar : ComponentRegistrar { - override fun getComponents() = - listOf( - Component.builder(FirebaseSessionsComponent::class.java) - .name("fire-sessions-component") - .add(Dependency.required(firebaseSessionsFakeComponent)) - .factory { container -> container.get(firebaseSessionsFakeComponent) } - .build(), - Component.builder(FirebaseSessionsFakeComponent::class.java) - .name("fire-sessions-fake-component") - .factory { FirebaseSessionsFakeComponent() } - .build(), - LibraryVersionComponent.create(LIBRARY_NAME, BuildConfig.VERSION_NAME), - ) - - private companion object { - const val LIBRARY_NAME = "fire-sessions" - - val firebaseSessionsFakeComponent = unqualified(FirebaseSessionsFakeComponent::class.java) - } -}