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 index 3b56aff8f5c..f9cc5ab2a5a 100644 --- a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/ProcessDataManager.kt +++ b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/ProcessDataManager.kt @@ -23,32 +23,48 @@ 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. */ + /** This process's name. */ + val myProcessName: String + + /** This process's pid. */ + val myPid: Int + + /** An in-memory uuid to uniquely identify this instance of this process, not the uid. */ 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 + /** Checks if this process is stale. */ + fun isMyProcessStale(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()) + /** Generate a new mapping of process data about this process only. */ + fun generateProcessDataMap(): Map = updateProcessDataMap(emptyMap()) } -/** Manage process data, used for detecting cold app starts. */ @Singleton internal class ProcessDataManagerImpl @Inject -constructor(private val appContext: Context, private val uuidGenerator: UuidGenerator) : - ProcessDataManager { +constructor(private val appContext: Context, uuidGenerator: UuidGenerator) : ProcessDataManager { + /** + * This process's name. + * + * This value is cached, so will not reflect changes to the process name during runtime. + */ + override val myProcessName: String by lazy { myProcessDetails.processName } + + override val myPid = Process.myPid() + override val myUuid: String by lazy { uuidGenerator.next().toString() } - private val myProcessName: String by lazy { - ProcessDetailsProvider.getCurrentProcessDetails(appContext).processName + private val myProcessDetails by lazy { + ProcessDetailsProvider.getCurrentProcessDetails(appContext) } private var hasGeneratedSession: Boolean = false @@ -59,7 +75,8 @@ constructor(private val appContext: Context, private val uuidGenerator: UuidGene return false } - return ProcessDetailsProvider.getAppProcessDetails(appContext) + // A cold start is when all app processes are stale + return getAppProcessDetails() .mapNotNull { processDetails -> processDataMap[processDetails.processName]?.let { processData -> Pair(processDetails, processData) @@ -68,6 +85,11 @@ constructor(private val appContext: Context, private val uuidGenerator: UuidGene .all { (processDetails, processData) -> isProcessStale(processDetails, processData) } } + override fun isMyProcessStale(processDataMap: Map): Boolean { + val myProcessData = processDataMap[myProcessName] ?: return true + return myProcessData.pid != myPid || myProcessData.uuid != myUuid + } + override fun onSessionGenerated() { hasGeneratedSession = true } @@ -81,17 +103,22 @@ constructor(private val appContext: Context, private val uuidGenerator: UuidGene ?.toMap() ?: mapOf(myProcessName to ProcessData(Process.myPid(), myUuid)) + /** Gets the current details for all of the app's running processes. */ + private fun getAppProcessDetails() = ProcessDetailsProvider.getAppProcessDetails(appContext) + /** * Returns true if the process is stale, meaning the persisted process data does not match the * running process details. + * + * The [processDetails] is the running process details, and [processData] is the persisted data. */ - private fun isProcessStale( - runningProcessDetails: ProcessDetails, - persistedProcessData: ProcessData, - ): Boolean = - if (myProcessName == runningProcessDetails.processName) { - runningProcessDetails.pid != persistedProcessData.pid || myUuid != persistedProcessData.uuid + private fun isProcessStale(processDetails: ProcessDetails, processData: ProcessData): Boolean = + if (myProcessName == processDetails.processName) { + // For this process, check pid and uuid + processDetails.pid != processData.pid || myUuid != processData.uuid } else { - runningProcessDetails.pid != persistedProcessData.pid + // For other processes, only check pid to avoid inter-process communication + // It is very unlikely for there to be a pid collision + processDetails.pid != processData.pid } } 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 index 4bbeea6954c..9ac360aa9f4 100644 --- a/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/ProcessDataManagerTest.kt +++ b/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/ProcessDataManagerTest.kt @@ -100,6 +100,53 @@ internal class ProcessDataManagerTest { assertThat(coldStart).isFalse() } + @Test + fun isMyProcessStale() { + val appContext = + FakeFirebaseApp(processes = listOf(myProcessInfo)).firebaseApp.applicationContext + val processDataManager = ProcessDataManagerImpl(appContext, FakeUuidGenerator(UUID_1)) + + val myProcessStale = + processDataManager.isMyProcessStale(mapOf(MY_PROCESS_NAME to ProcessData(MY_PID, UUID_1))) + + assertThat(myProcessStale).isFalse() + } + + @Test + fun isMyProcessStale_otherProcessCurrent() { + val appContext = + FakeFirebaseApp(processes = listOf(myProcessInfo, otherProcessInfo)) + .firebaseApp + .applicationContext + val processDataManager = ProcessDataManagerImpl(appContext, FakeUuidGenerator(UUID_1)) + + val myProcessStale = + processDataManager.isMyProcessStale( + mapOf( + MY_PROCESS_NAME to ProcessData(OTHER_PID, UUID_1), + OTHER_PROCESS_NAME to ProcessData(OTHER_PID, UUID_2), + ) + ) + + assertThat(myProcessStale).isTrue() + } + + @Test + fun isMyProcessStale_missingMyProcessData() { + val appContext = + FakeFirebaseApp(processes = listOf(myProcessInfo, otherProcessInfo)) + .firebaseApp + .applicationContext + val processDataManager = ProcessDataManagerImpl(appContext, FakeUuidGenerator(UUID_1)) + + val myProcessStale = + processDataManager.isMyProcessStale( + mapOf(OTHER_PROCESS_NAME to ProcessData(OTHER_PID, UUID_2)) + ) + + assertThat(myProcessStale).isTrue() + } + @After fun cleanUp() { FirebaseApp.clearInstancesForTest() 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 index fc43a502476..8458c357f5d 100644 --- 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 @@ -25,6 +25,9 @@ import com.google.firebase.sessions.ProcessDataManager */ internal class FakeProcessDataManager( private val coldStart: Boolean = false, + private var myProcessStale: Boolean = false, + override val myProcessName: String = "com.google.firebase.sessions.test", + override var myPid: Int = 0, override var myUuid: String = FakeUuidGenerator.UUID_1, ) : ProcessDataManager { private var hasGeneratedSession: Boolean = false @@ -33,9 +36,12 @@ internal class FakeProcessDataManager( if (hasGeneratedSession) { return false } + return coldStart } + override fun isMyProcessStale(processDataMap: Map): Boolean = myProcessStale + override fun onSessionGenerated() { hasGeneratedSession = true }