diff --git a/firebase-sessions/CHANGELOG.md b/firebase-sessions/CHANGELOG.md index 55e15d56850..7b9be5d2acd 100644 --- a/firebase-sessions/CHANGELOG.md +++ b/firebase-sessions/CHANGELOG.md @@ -1,15 +1,10 @@ # Unreleased * [changed] Use multi-process DataStore instead of Preferences DataStore +* [changed] Update the heuristic to detect cold app starts # 2.1.1 * [unchanged] Updated to keep SDK versions aligned. - -## Kotlin -The Kotlin extensions library transitively includes the updated -`firebase-sessions` library. The Kotlin extensions library has no additional -updates. - # 2.1.0 * [changed] Add warning for known issue b/328687152 * [changed] Use Dagger for dependency injection diff --git a/firebase-sessions/firebase-sessions.gradle.kts b/firebase-sessions/firebase-sessions.gradle.kts index 23edc952d5e..6c4b56a1c3b 100644 --- a/firebase-sessions/firebase-sessions.gradle.kts +++ b/firebase-sessions/firebase-sessions.gradle.kts @@ -29,7 +29,11 @@ firebaseLibrary { testLab.enabled = true publishJavadoc = false - releaseNotes { enabled.set(false) } + + releaseNotes { + enabled = false + hasKTX = false + } } android { diff --git a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/FirebaseSessionsComponent.kt b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/FirebaseSessionsComponent.kt index bdeb73af736..6eb15149cb3 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 @@ -118,6 +118,8 @@ internal interface FirebaseSessionsComponent { @Singleton fun sharedSessionRepository(impl: SharedSessionRepositoryImpl): SharedSessionRepository + @Binds @Singleton fun processDataManager(impl: ProcessDataManagerImpl): ProcessDataManager + companion object { private const val TAG = "FirebaseSessions" diff --git a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/ProcessDataManager.kt b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/ProcessDataManager.kt new file mode 100644 index 00000000000..3b56aff8f5c --- /dev/null +++ b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/ProcessDataManager.kt @@ -0,0 +1,97 @@ +/* + * 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 android.os.Process +import javax.inject.Inject +import javax.inject.Singleton + +/** Manage process data, used for detecting cold app starts. */ +internal interface ProcessDataManager { + /** An in-memory uuid to uniquely identify this instance of this process. */ + val myUuid: String + + /** Checks if this is a cold app start, meaning all processes in the mapping table are stale. */ + fun isColdStart(processDataMap: Map): Boolean + + /** Call to notify the process data manager that a session has been generated. */ + fun onSessionGenerated() + + /** Update the mapping of the current processes with data about this process. */ + fun updateProcessDataMap(processDataMap: Map?): Map + + /** Generate a new mapping of process data with the current process only. */ + fun generateProcessDataMap() = updateProcessDataMap(mapOf()) +} + +/** Manage process data, used for detecting cold app starts. */ +@Singleton +internal class ProcessDataManagerImpl +@Inject +constructor(private val appContext: Context, private val uuidGenerator: UuidGenerator) : + ProcessDataManager { + override val myUuid: String by lazy { uuidGenerator.next().toString() } + + private val myProcessName: String by lazy { + ProcessDetailsProvider.getCurrentProcessDetails(appContext).processName + } + + private var hasGeneratedSession: Boolean = false + + override fun isColdStart(processDataMap: Map): Boolean { + if (hasGeneratedSession) { + // This process has been notified that a session was generated, so cannot be a cold start + return false + } + + return ProcessDetailsProvider.getAppProcessDetails(appContext) + .mapNotNull { processDetails -> + processDataMap[processDetails.processName]?.let { processData -> + Pair(processDetails, processData) + } + } + .all { (processDetails, processData) -> isProcessStale(processDetails, processData) } + } + + override fun onSessionGenerated() { + hasGeneratedSession = true + } + + override fun updateProcessDataMap( + processDataMap: Map? + ): Map = + processDataMap + ?.toMutableMap() + ?.apply { this[myProcessName] = ProcessData(Process.myPid(), myUuid) } + ?.toMap() + ?: mapOf(myProcessName to ProcessData(Process.myPid(), myUuid)) + + /** + * Returns true if the process is stale, meaning the persisted process data does not match the + * running process details. + */ + private fun isProcessStale( + runningProcessDetails: ProcessDetails, + persistedProcessData: ProcessData, + ): Boolean = + if (myProcessName == runningProcessDetails.processName) { + runningProcessDetails.pid != persistedProcessData.pid || myUuid != persistedProcessData.uuid + } else { + runningProcessDetails.pid != persistedProcessData.pid + } +} 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 index 84e05b89ed9..8af2eee544d 100644 --- a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionData.kt +++ b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionData.kt @@ -29,17 +29,18 @@ import kotlinx.serialization.json.Json @Serializable internal data class SessionData( val sessionDetails: SessionDetails, - val backgroundTime: Time? = null + val backgroundTime: Time? = null, + val processDataMap: Map? = null, ) +/** Data about a process, for persistence. */ +@Serializable internal data class ProcessData(val pid: Int, val uuid: String) + /** DataStore json [Serializer] for [SessionData]. */ @Singleton internal class SessionDataSerializer @Inject -constructor( - private val sessionGenerator: SessionGenerator, - private val timeProvider: TimeProvider, -) : Serializer { +constructor(private val sessionGenerator: SessionGenerator) : Serializer { override val defaultValue: SessionData get() = SessionData(sessionGenerator.generateNewSession(currentSession = null)) diff --git a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SharedSessionRepository.kt b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SharedSessionRepository.kt index 5606342a428..cec9362725f 100644 --- a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SharedSessionRepository.kt +++ b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SharedSessionRepository.kt @@ -46,6 +46,7 @@ constructor( private val sessionFirelogPublisher: SessionFirelogPublisher, private val timeProvider: TimeProvider, private val sessionDataStore: DataStore, + private val processDataManager: ProcessDataManager, @Background private val backgroundDispatcher: CoroutineContext, ) : SharedSessionRepository { /** Local copy of the session data. Can get out of sync, must be double-checked in datastore. */ @@ -57,8 +58,9 @@ constructor( */ internal enum class NotificationType { GENERAL, - FALLBACK + FALLBACK, } + internal var previousNotificationType: NotificationType = NotificationType.GENERAL init { @@ -68,11 +70,11 @@ constructor( val newSession = SessionData( sessionDetails = sessionGenerator.generateNewSession(null), - backgroundTime = null + backgroundTime = null, ) Log.d( TAG, - "Init session datastore failed with exception message: ${it.message}. Emit fallback session ${newSession.sessionDetails.sessionId}" + "Init session datastore failed with exception message: ${it.message}. Emit fallback session ${newSession.sessionDetails.sessionId}", ) emit(newSession) } @@ -122,6 +124,7 @@ constructor( val newSessionDetails = sessionGenerator.generateNewSession(sessionData.sessionDetails) sessionFirelogPublisher.mayLogSession(sessionDetails = newSessionDetails) + processDataManager.onSessionGenerated() currentSessionData.copy(sessionDetails = newSessionDetails, backgroundTime = null) } else { currentSessionData @@ -153,17 +156,26 @@ constructor( "Notified ${subscriber.sessionSubscriberName} of new session $sessionId" NotificationType.FALLBACK -> "Notified ${subscriber.sessionSubscriberName} of new fallback session $sessionId" - } + }, ) } } private fun shouldInitiateNewSession(sessionData: SessionData): Boolean { - sessionData.backgroundTime?.let { - val interval = timeProvider.currentTime() - it - return interval > sessionsSettings.sessionRestartTimeout + sessionData.backgroundTime?.let { backgroundTime -> + val interval = timeProvider.currentTime() - backgroundTime + if (interval > sessionsSettings.sessionRestartTimeout) { + Log.d(TAG, "Passed session restart timeout, so initiate a new session") + return true + } } - Log.d(TAG, "No process has backgrounded yet, should not change the session.") + + sessionData.processDataMap?.let { processDataMap -> + Log.d(TAG, "Has not passed session restart timeout, so check for cold app start") + return processDataManager.isColdStart(processDataMap) + } + + Log.d(TAG, "No process has backgrounded yet and no process data, should not change the session") return false } diff --git a/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/ProcessDataManagerTest.kt b/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/ProcessDataManagerTest.kt new file mode 100644 index 00000000000..4bbeea6954c --- /dev/null +++ b/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/ProcessDataManagerTest.kt @@ -0,0 +1,120 @@ +/* + * 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.test.ext.junit.runners.AndroidJUnit4 +import com.google.common.truth.Truth.assertThat +import com.google.firebase.FirebaseApp +import com.google.firebase.sessions.testing.FakeFirebaseApp +import com.google.firebase.sessions.testing.FakeRunningAppProcessInfo +import com.google.firebase.sessions.testing.FakeUuidGenerator +import com.google.firebase.sessions.testing.FakeUuidGenerator.Companion.UUID_1 +import com.google.firebase.sessions.testing.FakeUuidGenerator.Companion.UUID_2 +import org.junit.After +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +internal class ProcessDataManagerTest { + @Test + fun isColdStart_myProcess() { + val appContext = FakeFirebaseApp().firebaseApp.applicationContext + val processDataManager = ProcessDataManagerImpl(appContext, FakeUuidGenerator(UUID_1)) + + val coldStart = + processDataManager.isColdStart(mapOf(MY_PROCESS_NAME to ProcessData(MY_PID, UUID_1))) + + assertThat(coldStart).isFalse() + } + + fun isColdStart_myProcessCurrent_otherProcessCurrent() { + val appContext = + FakeFirebaseApp(processes = listOf(myProcessInfo, otherProcessInfo)) + .firebaseApp + .applicationContext + val processDataManager = ProcessDataManagerImpl(appContext, FakeUuidGenerator(UUID_1)) + + val coldStart = + processDataManager.isColdStart( + mapOf( + MY_PROCESS_NAME to ProcessData(MY_PID, UUID_1), + OTHER_PROCESS_NAME to ProcessData(OTHER_PID, UUID_2), + ) + ) + + assertThat(coldStart).isFalse() + } + + @Test + fun isColdStart_staleProcessPid() { + val appContext = FakeFirebaseApp().firebaseApp.applicationContext + val processDataManager = ProcessDataManagerImpl(appContext, FakeUuidGenerator(UUID_1)) + + val coldStart = + processDataManager.isColdStart(mapOf(MY_PROCESS_NAME to ProcessData(OTHER_PID, UUID_1))) + + assertThat(coldStart).isTrue() + } + + @Test + fun isColdStart_staleProcessUuid() { + val appContext = FakeFirebaseApp().firebaseApp.applicationContext + val processDataManager = ProcessDataManagerImpl(appContext, FakeUuidGenerator(UUID_1)) + + val coldStart = + processDataManager.isColdStart(mapOf(MY_PROCESS_NAME to ProcessData(MY_PID, UUID_2))) + + assertThat(coldStart).isTrue() + } + + @Test + fun isColdStart_myProcessStale_otherProcessCurrent() { + val appContext = + FakeFirebaseApp(processes = listOf(myProcessInfo, otherProcessInfo)) + .firebaseApp + .applicationContext + val processDataManager = ProcessDataManagerImpl(appContext, FakeUuidGenerator(UUID_1)) + + val coldStart = + processDataManager.isColdStart( + mapOf( + MY_PROCESS_NAME to ProcessData(OTHER_PID, UUID_1), + OTHER_PROCESS_NAME to ProcessData(OTHER_PID, UUID_2), + ) + ) + + assertThat(coldStart).isFalse() + } + + @After + fun cleanUp() { + FirebaseApp.clearInstancesForTest() + } + + private companion object { + const val MY_PROCESS_NAME = "com.google.firebase.sessions.test" + const val OTHER_PROCESS_NAME = "not.my.process" + + const val MY_PID = 0 + const val OTHER_PID = 4 + + val myProcessInfo = FakeRunningAppProcessInfo(pid = MY_PID, processName = MY_PROCESS_NAME) + + val otherProcessInfo = + FakeRunningAppProcessInfo(pid = OTHER_PID, processName = OTHER_PROCESS_NAME) + } +} diff --git a/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/SharedSessionRepositoryTest.kt b/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/SharedSessionRepositoryTest.kt index 135b8df4f22..38ab0ca37a4 100644 --- a/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/SharedSessionRepositoryTest.kt +++ b/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/SharedSessionRepositoryTest.kt @@ -27,6 +27,7 @@ import com.google.firebase.sessions.testing.FakeDataStore import com.google.firebase.sessions.testing.FakeEventGDTLogger import com.google.firebase.sessions.testing.FakeFirebaseApp import com.google.firebase.sessions.testing.FakeFirebaseInstallations +import com.google.firebase.sessions.testing.FakeProcessDataManager import com.google.firebase.sessions.testing.FakeSettingsProvider import com.google.firebase.sessions.testing.FakeTimeProvider import com.google.firebase.sessions.testing.FakeUuidGenerator @@ -67,14 +68,9 @@ class SharedSessionRepositoryTest { TestOnlyExecutors.background().asCoroutineDispatcher() + coroutineContext, ) val fakeDataStore = - FakeDataStore( + FakeDataStore( SessionData( - SessionDetails( - SESSION_ID_INIT, - SESSION_ID_INIT, - 0, - fakeTimeProvider.currentTime().ms, - ), + SessionDetails(SESSION_ID_INIT, SESSION_ID_INIT, 0, fakeTimeProvider.currentTime().ms), fakeTimeProvider.currentTime(), ) ) @@ -85,8 +81,9 @@ class SharedSessionRepositoryTest { sessionFirelogPublisher = publisher, timeProvider = fakeTimeProvider, sessionDataStore = fakeDataStore, + processDataManager = FakeProcessDataManager(), backgroundDispatcher = - TestOnlyExecutors.background().asCoroutineDispatcher() + coroutineContext + TestOnlyExecutors.background().asCoroutineDispatcher() + coroutineContext, ) runCurrent() fakeDataStore.close() @@ -94,6 +91,47 @@ class SharedSessionRepositoryTest { .isEqualTo(SESSION_ID_INIT) } + @Test + fun initSharedSessionRepo_coldStart() = runTest { + val publisher = + SessionFirelogPublisherImpl( + fakeFirebaseApp.firebaseApp, + firebaseInstallations, + sessionsSettings, + eventGDTLogger = fakeEventGDTLogger, + TestOnlyExecutors.background().asCoroutineDispatcher() + coroutineContext, + ) + val fakeDataStore = + FakeDataStore( + SessionData( + SessionDetails(SESSION_ID_INIT, SESSION_ID_INIT, 0, fakeTimeProvider.currentTime().ms), + backgroundTime = fakeTimeProvider.currentTime(), + processDataMap = mapOf("" to ProcessData(pid = 10, uuid = "uuid")), + ) + ) + val sharedSessionRepository = + SharedSessionRepositoryImpl( + sessionsSettings = sessionsSettings, + sessionGenerator = sessionGenerator, + sessionFirelogPublisher = publisher, + timeProvider = fakeTimeProvider, + sessionDataStore = fakeDataStore, + processDataManager = FakeProcessDataManager(coldStart = true), + backgroundDispatcher = + TestOnlyExecutors.background().asCoroutineDispatcher() + coroutineContext, + ) + runCurrent() + + sharedSessionRepository.appForeground() + runCurrent() + fakeDataStore.close() + + assertThat(sharedSessionRepository.localSessionData.sessionDetails) + .isEqualTo( + SessionDetails(SESSION_ID_1, SESSION_ID_INIT, 1, fakeTimeProvider.currentTime().us) + ) + } + @Test fun initSharedSessionRepo_initException() = runTest { val sessionFirelogPublisher = @@ -105,17 +143,12 @@ class SharedSessionRepositoryTest { TestOnlyExecutors.background().asCoroutineDispatcher() + coroutineContext, ) val fakeDataStore = - FakeDataStore( + FakeDataStore( SessionData( - SessionDetails( - SESSION_ID_INIT, - SESSION_ID_INIT, - 0, - fakeTimeProvider.currentTime().ms, - ), + SessionDetails(SESSION_ID_INIT, SESSION_ID_INIT, 0, fakeTimeProvider.currentTime().ms), fakeTimeProvider.currentTime(), ), - IllegalArgumentException("Datastore init failed") + IllegalArgumentException("Datastore init failed"), ) val sharedSessionRepository = SharedSessionRepositoryImpl( @@ -124,8 +157,9 @@ class SharedSessionRepositoryTest { sessionFirelogPublisher, fakeTimeProvider, fakeDataStore, + processDataManager = FakeProcessDataManager(), backgroundDispatcher = - TestOnlyExecutors.background().asCoroutineDispatcher() + coroutineContext + TestOnlyExecutors.background().asCoroutineDispatcher() + coroutineContext, ) runCurrent() fakeDataStore.close() @@ -144,15 +178,11 @@ class SharedSessionRepositoryTest { TestOnlyExecutors.background().asCoroutineDispatcher() + coroutineContext, ) val fakeDataStore = - FakeDataStore( + FakeDataStore( SessionData( - SessionDetails( - SESSION_ID_INIT, - SESSION_ID_INIT, - 0, - fakeTimeProvider.currentTime().ms, - ), - fakeTimeProvider.currentTime(), + SessionDetails(SESSION_ID_INIT, SESSION_ID_INIT, 0, fakeTimeProvider.currentTime().ms), + backgroundTime = fakeTimeProvider.currentTime(), + processDataMap = mapOf("" to ProcessData(pid = 10, uuid = "uuid")), ) ) val sharedSessionRepository = @@ -162,8 +192,9 @@ class SharedSessionRepositoryTest { sessionFirelogPublisher, fakeTimeProvider, fakeDataStore, + processDataManager = FakeProcessDataManager(), backgroundDispatcher = - TestOnlyExecutors.background().asCoroutineDispatcher() + coroutineContext + TestOnlyExecutors.background().asCoroutineDispatcher() + coroutineContext, ) runCurrent() @@ -190,17 +221,12 @@ class SharedSessionRepositoryTest { TestOnlyExecutors.background().asCoroutineDispatcher() + coroutineContext, ) val fakeDataStore = - FakeDataStore( + FakeDataStore( SessionData( - SessionDetails( - SESSION_ID_INIT, - SESSION_ID_INIT, - 0, - fakeTimeProvider.currentTime().ms, - ), + SessionDetails(SESSION_ID_INIT, SESSION_ID_INIT, 0, fakeTimeProvider.currentTime().ms), fakeTimeProvider.currentTime(), ), - IllegalArgumentException("Datastore init failed") + IllegalArgumentException("Datastore init failed"), ) val sharedSessionRepository = SharedSessionRepositoryImpl( @@ -209,8 +235,9 @@ class SharedSessionRepositoryTest { sessionFirelogPublisher, fakeTimeProvider, fakeDataStore, + processDataManager = FakeProcessDataManager(), backgroundDispatcher = - TestOnlyExecutors.background().asCoroutineDispatcher() + coroutineContext + TestOnlyExecutors.background().asCoroutineDispatcher() + coroutineContext, ) runCurrent() diff --git a/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/testing/FakeFirebaseApp.kt b/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/testing/FakeFirebaseApp.kt index eea9114b3b8..e934ada6bf0 100644 --- a/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/testing/FakeFirebaseApp.kt +++ b/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/testing/FakeFirebaseApp.kt @@ -29,7 +29,10 @@ import org.robolectric.Shadows import org.robolectric.shadow.api.Shadow import org.robolectric.shadows.ShadowActivityManager -internal class FakeFirebaseApp(metadata: Bundle? = null) { +internal class FakeFirebaseApp( + metadata: Bundle? = null, + processes: List = emptyList(), +) { val firebaseApp: FirebaseApp init { @@ -45,12 +48,16 @@ internal class FakeFirebaseApp(metadata: Bundle? = null) { val activityManager = context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager val shadowActivityManager: ShadowActivityManager = Shadow.extract(activityManager) - val runningAppProcessInfo = ActivityManager.RunningAppProcessInfo() - runningAppProcessInfo.pid = 0 - runningAppProcessInfo.uid = 313 - runningAppProcessInfo.processName = context.packageName - runningAppProcessInfo.importance = 100 - shadowActivityManager.setProcesses(listOf(runningAppProcessInfo)) + if (processes.isEmpty()) { + val runningAppProcessInfo = ActivityManager.RunningAppProcessInfo() + runningAppProcessInfo.pid = 0 + runningAppProcessInfo.uid = 313 + runningAppProcessInfo.processName = context.packageName + runningAppProcessInfo.importance = 100 + shadowActivityManager.setProcesses(listOf(runningAppProcessInfo)) + } else { + shadowActivityManager.setProcesses(processes) + } firebaseApp = Firebase.initialize( @@ -59,7 +66,7 @@ internal class FakeFirebaseApp(metadata: Bundle? = null) { .setApplicationId(MOCK_APP_ID) .setApiKey(MOCK_API_KEY) .setProjectId(MOCK_PROJECT_ID) - .build() + .build(), ) } diff --git a/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/testing/FakeProcessDataManager.kt b/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/testing/FakeProcessDataManager.kt new file mode 100644 index 00000000000..fc43a502476 --- /dev/null +++ b/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/testing/FakeProcessDataManager.kt @@ -0,0 +1,48 @@ +/* + * 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.sessions.ProcessData +import com.google.firebase.sessions.ProcessDataManager + +/** + * Fake implementation of ProcessDataManager that returns the provided [coldStart] value for + * [isColdStart] until [onSessionGenerated] gets called, then returns false. + */ +internal class FakeProcessDataManager( + private val coldStart: Boolean = false, + override var myUuid: String = FakeUuidGenerator.UUID_1, +) : ProcessDataManager { + private var hasGeneratedSession: Boolean = false + + override fun isColdStart(processDataMap: Map): Boolean { + if (hasGeneratedSession) { + return false + } + return coldStart + } + + override fun onSessionGenerated() { + hasGeneratedSession = true + } + + override fun updateProcessDataMap( + processDataMap: Map? + ): Map { + TODO("Not yet implemented") + } +} diff --git a/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/testing/FakeRunningAppProcessInfo.kt b/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/testing/FakeRunningAppProcessInfo.kt new file mode 100644 index 00000000000..1afebb2d0bb --- /dev/null +++ b/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/testing/FakeRunningAppProcessInfo.kt @@ -0,0 +1,34 @@ +/* + * 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 android.app.ActivityManager + +/** Fake [ActivityManager.RunningAppProcessInfo] that is easy to construct. */ +internal class FakeRunningAppProcessInfo( + pid: Int = 0, + uid: Int = 313, + processName: String = "fake.process.name", + importance: Int = 100, +) : ActivityManager.RunningAppProcessInfo() { + init { + this.pid = pid + this.uid = uid + this.processName = processName + this.importance = importance + } +} diff --git a/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/testing/FakeUuidGenerator.kt b/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/testing/FakeUuidGenerator.kt index 88f1f816c12..5fb2cd47785 100644 --- a/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/testing/FakeUuidGenerator.kt +++ b/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/testing/FakeUuidGenerator.kt @@ -24,6 +24,8 @@ internal class FakeUuidGenerator(private val names: List = listOf(UUID_1 UuidGenerator { private var index = -1 + constructor(vararg names: String) : this(names.toList()) + override fun next(): UUID { index = (index + 1).coerceAtMost(names.size - 1) return UUID.fromString(names[index])