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 f9cc5ab2a5a..295b6550ed7 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 @@ -63,9 +63,7 @@ constructor(private val appContext: Context, uuidGenerator: UuidGenerator) : Pro override val myUuid: String by lazy { uuidGenerator.next().toString() } - private val myProcessDetails by lazy { - ProcessDetailsProvider.getCurrentProcessDetails(appContext) - } + private val myProcessDetails by lazy { ProcessDetailsProvider.getMyProcessDetails(appContext) } private var hasGeneratedSession: Boolean = false diff --git a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/ProcessDetailsProvider.kt b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/ProcessDetailsProvider.kt index 65d1dfbbc60..39a3c03ed17 100644 --- a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/ProcessDetailsProvider.kt +++ b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/ProcessDetailsProvider.kt @@ -23,13 +23,9 @@ import android.os.Build import android.os.Process import com.google.android.gms.common.util.ProcessUtils -/** - * Provider of ProcessDetails. - * - * @hide - */ +/** Provide [ProcessDetails] for all app processes. */ internal object ProcessDetailsProvider { - /** Gets the details for all of this app's running processes. */ + /** Gets the details for all the app's running processes. */ fun getAppProcessDetails(context: Context): List { val appUid = context.applicationInfo.uid val defaultProcessName = context.applicationInfo.processName @@ -53,27 +49,19 @@ internal object ProcessDetailsProvider { } /** - * Gets this app's current process details. + * Gets this process's details. * - * If the current process details are not found for whatever reason, returns process details with - * just the current process name and pid set. + * If this process's full details are not found for whatever reason, returns process details with + * just the process name and pid set. */ - fun getCurrentProcessDetails(context: Context): ProcessDetails { + fun getMyProcessDetails(context: Context): ProcessDetails { val pid = Process.myPid() return getAppProcessDetails(context).find { it.pid == pid } - ?: buildProcessDetails(getProcessName(), pid) + ?: ProcessDetails(getProcessName(), pid, importance = 0, isDefaultProcess = false) } - /** Builds a ProcessDetails object. */ - private fun buildProcessDetails( - processName: String, - pid: Int = 0, - importance: Int = 0, - isDefaultProcess: Boolean = false - ) = ProcessDetails(processName, pid, importance, isDefaultProcess) - /** Gets the app's current process name. If it could not be found, returns an empty string. */ - internal fun getProcessName(): String { + private fun getProcessName(): String { if (Build.VERSION.SDK_INT > Build.VERSION_CODES.TIRAMISU) { return Process.myProcessName() } diff --git a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionEvents.kt b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionEvents.kt index 6a540fd0104..864b393d64b 100644 --- a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionEvents.kt +++ b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionEvents.kt @@ -83,7 +83,7 @@ internal object SessionEvents { versionName = packageInfo.versionName ?: buildVersion, appBuildVersion = buildVersion, deviceManufacturer = Build.MANUFACTURER, - ProcessDetailsProvider.getCurrentProcessDetails(firebaseApp.applicationContext), + ProcessDetailsProvider.getMyProcessDetails(firebaseApp.applicationContext), ProcessDetailsProvider.getAppProcessDetails(firebaseApp.applicationContext), ), ) 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 cec9362725f..6cbaea417d3 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 @@ -19,7 +19,6 @@ 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 @@ -92,7 +91,7 @@ constructor( return } val sessionData = localSessionData - Log.d(TAG, "App backgrounded on ${getProcessName()} - $sessionData") + Log.d(TAG, "App backgrounded on ${processDataManager.myProcessName} - $sessionData") CoroutineScope(backgroundDispatcher).launch { try { @@ -113,32 +112,58 @@ constructor( return } val sessionData = localSessionData - Log.d(TAG, "App foregrounded on ${getProcessName()} - $sessionData") + Log.d(TAG, "App foregrounded on ${processDataManager.myProcessName} - $sessionData") - if (shouldInitiateNewSession(sessionData)) { + // Check if maybe the session data needs to be updated + if (isSessionExpired(sessionData) || isMyProcessStale(sessionData)) { CoroutineScope(backgroundDispatcher).launch { try { sessionDataStore.updateData { currentSessionData -> - // Double-check pattern - if (shouldInitiateNewSession(currentSessionData)) { + // Check again using the current session data on disk + val isSessionExpired = isSessionExpired(currentSessionData) + val isColdStart = isColdStart(currentSessionData) + val isMyProcessStale = isMyProcessStale(currentSessionData) + + val newProcessDataMap = + if (isColdStart) { + // Generate a new process data map for cold app start + processDataManager.generateProcessDataMap() + } else if (isMyProcessStale) { + // Update the data map with this process if stale + processDataManager.updateProcessDataMap(currentSessionData.processDataMap) + } else { + // No change + currentSessionData.processDataMap + } + + // This is an expression, and returns the updated session data + if (isSessionExpired || isColdStart) { val newSessionDetails = - sessionGenerator.generateNewSession(sessionData.sessionDetails) + sessionGenerator.generateNewSession(currentSessionData.sessionDetails) sessionFirelogPublisher.mayLogSession(sessionDetails = newSessionDetails) processDataManager.onSessionGenerated() - currentSessionData.copy(sessionDetails = newSessionDetails, backgroundTime = null) + currentSessionData.copy( + sessionDetails = newSessionDetails, + backgroundTime = null, + processDataMap = newProcessDataMap, + ) + } else if (isMyProcessStale) { + currentSessionData.copy( + processDataMap = processDataManager.updateProcessDataMap(newProcessDataMap) + ) } else { currentSessionData } } } catch (ex: Exception) { Log.d(TAG, "App appForegrounded, failed to update data. Message: ${ex.message}") - val newSessionDetails = sessionGenerator.generateNewSession(sessionData.sessionDetails) - localSessionData = - localSessionData.copy(sessionDetails = newSessionDetails, backgroundTime = null) - sessionFirelogPublisher.mayLogSession(sessionDetails = newSessionDetails) - - val sessionId = newSessionDetails.sessionId - notifySubscribers(sessionId, NotificationType.FALLBACK) + if (isSessionExpired(sessionData)) { + val newSessionDetails = sessionGenerator.generateNewSession(sessionData.sessionDetails) + localSessionData = + sessionData.copy(sessionDetails = newSessionDetails, backgroundTime = null) + sessionFirelogPublisher.mayLogSession(sessionDetails = newSessionDetails) + notifySubscribers(newSessionDetails.sessionId, NotificationType.FALLBACK) + } } } } @@ -161,22 +186,47 @@ constructor( } } - private fun shouldInitiateNewSession(sessionData: SessionData): Boolean { + /** Checks if the session has expired. If no background time, consider it not expired. */ + private fun isSessionExpired(sessionData: SessionData): Boolean { 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 + val sessionExpired = (interval > sessionsSettings.sessionRestartTimeout) + if (sessionExpired) { + Log.d(TAG, "Session ${sessionData.sessionDetails.sessionId} is expired") } + return sessionExpired } + Log.d(TAG, "Session ${sessionData.sessionDetails.sessionId} has not backgrounded yet") + return false + } + + /** Checks for cold app start. If no process data map, consider it a cold start. */ + private fun isColdStart(sessionData: SessionData): Boolean { sessionData.processDataMap?.let { processDataMap -> - Log.d(TAG, "Has not passed session restart timeout, so check for cold app start") - return processDataManager.isColdStart(processDataMap) + val coldStart = processDataManager.isColdStart(processDataMap) + if (coldStart) { + Log.d(TAG, "Cold app start detected") + } + return coldStart } - Log.d(TAG, "No process has backgrounded yet and no process data, should not change the session") - return false + Log.d(TAG, "No process data map") + return true + } + + /** Checks if this process is stale. If no process data map, consider the process stale. */ + private fun isMyProcessStale(sessionData: SessionData): Boolean { + sessionData.processDataMap?.let { processDataMap -> + val myProcessStale = processDataManager.isMyProcessStale(processDataMap) + if (myProcessStale) { + Log.d(TAG, "Process ${processDataManager.myProcessName} is stale") + } + return myProcessStale + } + + Log.d(TAG, "No process data for ${processDataManager.myProcessName}") + return true } private companion object { 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 b7bdb7730e8..b026b7f33bc 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 @@ -36,9 +36,9 @@ class ApplicationInfoTest { @Test fun applicationInfo_populatesInfoCorrectly() { val firebaseApp = FakeFirebaseApp().firebaseApp - val actualCurrentProcessDetails = - ProcessDetailsProvider.getCurrentProcessDetails(firebaseApp.applicationContext) - val actualAppProcessDetails = + val myProcessDetails = + ProcessDetailsProvider.getMyProcessDetails(firebaseApp.applicationContext) + val appProcessDetails = ProcessDetailsProvider.getAppProcessDetails(firebaseApp.applicationContext) val applicationInfo = SessionEvents.getApplicationInfo(firebaseApp) assertThat(applicationInfo) @@ -54,8 +54,8 @@ class ApplicationInfoTest { versionName = FakeFirebaseApp.MOCK_APP_VERSION, appBuildVersion = FakeFirebaseApp.MOCK_APP_BUILD_VERSION, deviceManufacturer = Build.MANUFACTURER, - actualCurrentProcessDetails, - actualAppProcessDetails, + myProcessDetails, + appProcessDetails, ), ) ) @@ -74,9 +74,9 @@ class ApplicationInfoTest { .build(), ) - val actualCurrentProcessDetails = - ProcessDetailsProvider.getCurrentProcessDetails(firebaseApp.applicationContext) - val actualAppProcessDetails = + val myProcessDetails = + ProcessDetailsProvider.getMyProcessDetails(firebaseApp.applicationContext) + val appProcessDetails = ProcessDetailsProvider.getAppProcessDetails(firebaseApp.applicationContext) val applicationInfo = SessionEvents.getApplicationInfo(firebaseApp) @@ -94,8 +94,8 @@ class ApplicationInfoTest { versionName = "0", appBuildVersion = "0", deviceManufacturer = Build.MANUFACTURER, - actualCurrentProcessDetails, - actualAppProcessDetails, + myProcessDetails, + appProcessDetails, ), ) ) 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 9ac360aa9f4..3eddd371a0f 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 @@ -22,8 +22,8 @@ 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 com.google.firebase.sessions.testing.FakeUuidGenerator.Companion.UUID_1 as MY_UUID +import com.google.firebase.sessions.testing.FakeUuidGenerator.Companion.UUID_2 as OTHER_UUID import org.junit.After import org.junit.Test import org.junit.runner.RunWith @@ -33,26 +33,36 @@ internal class ProcessDataManagerTest { @Test fun isColdStart_myProcess() { val appContext = FakeFirebaseApp().firebaseApp.applicationContext - val processDataManager = ProcessDataManagerImpl(appContext, FakeUuidGenerator(UUID_1)) + val processDataManager = ProcessDataManagerImpl(appContext, FakeUuidGenerator(MY_UUID)) val coldStart = - processDataManager.isColdStart(mapOf(MY_PROCESS_NAME to ProcessData(MY_PID, UUID_1))) + processDataManager.isColdStart(mapOf(MY_PROCESS_NAME to ProcessData(MY_PID, MY_UUID))) assertThat(coldStart).isFalse() } + @Test + fun isColdStart_emptyProcessDataMap() { + val appContext = FakeFirebaseApp().firebaseApp.applicationContext + val processDataManager = ProcessDataManagerImpl(appContext, FakeUuidGenerator(MY_UUID)) + + val coldStart = processDataManager.isColdStart(processDataMap = emptyMap()) + + assertThat(coldStart).isTrue() + } + fun isColdStart_myProcessCurrent_otherProcessCurrent() { val appContext = FakeFirebaseApp(processes = listOf(myProcessInfo, otherProcessInfo)) .firebaseApp .applicationContext - val processDataManager = ProcessDataManagerImpl(appContext, FakeUuidGenerator(UUID_1)) + val processDataManager = ProcessDataManagerImpl(appContext, FakeUuidGenerator(MY_UUID)) val coldStart = processDataManager.isColdStart( mapOf( - MY_PROCESS_NAME to ProcessData(MY_PID, UUID_1), - OTHER_PROCESS_NAME to ProcessData(OTHER_PID, UUID_2), + MY_PROCESS_NAME to ProcessData(MY_PID, MY_UUID), + OTHER_PROCESS_NAME to ProcessData(OTHER_PID, OTHER_UUID), ) ) @@ -62,10 +72,10 @@ internal class ProcessDataManagerTest { @Test fun isColdStart_staleProcessPid() { val appContext = FakeFirebaseApp().firebaseApp.applicationContext - val processDataManager = ProcessDataManagerImpl(appContext, FakeUuidGenerator(UUID_1)) + val processDataManager = ProcessDataManagerImpl(appContext, FakeUuidGenerator(MY_UUID)) val coldStart = - processDataManager.isColdStart(mapOf(MY_PROCESS_NAME to ProcessData(OTHER_PID, UUID_1))) + processDataManager.isColdStart(mapOf(MY_PROCESS_NAME to ProcessData(OTHER_PID, MY_UUID))) assertThat(coldStart).isTrue() } @@ -73,10 +83,10 @@ internal class ProcessDataManagerTest { @Test fun isColdStart_staleProcessUuid() { val appContext = FakeFirebaseApp().firebaseApp.applicationContext - val processDataManager = ProcessDataManagerImpl(appContext, FakeUuidGenerator(UUID_1)) + val processDataManager = ProcessDataManagerImpl(appContext, FakeUuidGenerator(MY_UUID)) val coldStart = - processDataManager.isColdStart(mapOf(MY_PROCESS_NAME to ProcessData(MY_PID, UUID_2))) + processDataManager.isColdStart(mapOf(MY_PROCESS_NAME to ProcessData(MY_PID, OTHER_UUID))) assertThat(coldStart).isTrue() } @@ -87,13 +97,13 @@ internal class ProcessDataManagerTest { FakeFirebaseApp(processes = listOf(myProcessInfo, otherProcessInfo)) .firebaseApp .applicationContext - val processDataManager = ProcessDataManagerImpl(appContext, FakeUuidGenerator(UUID_1)) + val processDataManager = ProcessDataManagerImpl(appContext, FakeUuidGenerator(MY_UUID)) val coldStart = processDataManager.isColdStart( mapOf( - MY_PROCESS_NAME to ProcessData(OTHER_PID, UUID_1), - OTHER_PROCESS_NAME to ProcessData(OTHER_PID, UUID_2), + MY_PROCESS_NAME to ProcessData(OTHER_PID, MY_UUID), + OTHER_PROCESS_NAME to ProcessData(OTHER_PID, OTHER_UUID), ) ) @@ -104,10 +114,10 @@ internal class ProcessDataManagerTest { fun isMyProcessStale() { val appContext = FakeFirebaseApp(processes = listOf(myProcessInfo)).firebaseApp.applicationContext - val processDataManager = ProcessDataManagerImpl(appContext, FakeUuidGenerator(UUID_1)) + val processDataManager = ProcessDataManagerImpl(appContext, FakeUuidGenerator(MY_UUID)) val myProcessStale = - processDataManager.isMyProcessStale(mapOf(MY_PROCESS_NAME to ProcessData(MY_PID, UUID_1))) + processDataManager.isMyProcessStale(mapOf(MY_PROCESS_NAME to ProcessData(MY_PID, MY_UUID))) assertThat(myProcessStale).isFalse() } @@ -118,13 +128,13 @@ internal class ProcessDataManagerTest { FakeFirebaseApp(processes = listOf(myProcessInfo, otherProcessInfo)) .firebaseApp .applicationContext - val processDataManager = ProcessDataManagerImpl(appContext, FakeUuidGenerator(UUID_1)) + val processDataManager = ProcessDataManagerImpl(appContext, FakeUuidGenerator(MY_UUID)) val myProcessStale = processDataManager.isMyProcessStale( mapOf( - MY_PROCESS_NAME to ProcessData(OTHER_PID, UUID_1), - OTHER_PROCESS_NAME to ProcessData(OTHER_PID, UUID_2), + MY_PROCESS_NAME to ProcessData(OTHER_PID, MY_UUID), + OTHER_PROCESS_NAME to ProcessData(OTHER_PID, OTHER_UUID), ) ) @@ -137,11 +147,11 @@ internal class ProcessDataManagerTest { FakeFirebaseApp(processes = listOf(myProcessInfo, otherProcessInfo)) .firebaseApp .applicationContext - val processDataManager = ProcessDataManagerImpl(appContext, FakeUuidGenerator(UUID_1)) + val processDataManager = ProcessDataManagerImpl(appContext, FakeUuidGenerator(MY_UUID)) val myProcessStale = processDataManager.isMyProcessStale( - mapOf(OTHER_PROCESS_NAME to ProcessData(OTHER_PID, UUID_2)) + mapOf(OTHER_PROCESS_NAME to ProcessData(OTHER_PID, OTHER_UUID)) ) assertThat(myProcessStale).isTrue() diff --git a/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/ProcessDetailsProviderTest.kt b/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/ProcessDetailsProviderTest.kt index b41b33e3361..2517157c7e2 100644 --- a/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/ProcessDetailsProviderTest.kt +++ b/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/ProcessDetailsProviderTest.kt @@ -40,9 +40,8 @@ class ProcessDetailsProviderTest { } @Test - fun getCurrentProcessDetails() { - val processDetails = - ProcessDetailsProvider.getCurrentProcessDetails(firebaseApp.applicationContext) + fun getMyProcessDetails() { + val processDetails = ProcessDetailsProvider.getMyProcessDetails(firebaseApp.applicationContext) assertThat(processDetails) .isEqualTo(ProcessDetails("com.google.firebase.sessions.test", 0, 100, false)) } 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 8458c357f5d..d6e287196d4 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 @@ -21,11 +21,12 @@ 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. + * [isColdStart], and similar for [isMyProcessStale], until [onSessionGenerated] gets called then + * returns false. */ internal class FakeProcessDataManager( private val coldStart: Boolean = false, - private var myProcessStale: Boolean = false, + private var myProcessStale: Boolean = coldStart, override val myProcessName: String = "com.google.firebase.sessions.test", override var myPid: Int = 0, override var myUuid: String = FakeUuidGenerator.UUID_1, @@ -40,7 +41,13 @@ internal class FakeProcessDataManager( return coldStart } - override fun isMyProcessStale(processDataMap: Map): Boolean = myProcessStale + override fun isMyProcessStale(processDataMap: Map): Boolean { + if (hasGeneratedSession) { + return false + } + + return myProcessStale + } override fun onSessionGenerated() { hasGeneratedSession = true @@ -48,7 +55,5 @@ internal class FakeProcessDataManager( override fun updateProcessDataMap( processDataMap: Map? - ): Map { - TODO("Not yet implemented") - } + ): Map = processDataMap ?: emptyMap() }