Skip to content

Commit e497817

Browse files
committed
Implement cold start detection logic
1 parent 248fd92 commit e497817

File tree

8 files changed

+137
-89
lines changed

8 files changed

+137
-89
lines changed

firebase-sessions/src/main/kotlin/com/google/firebase/sessions/ProcessDataManager.kt

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -63,9 +63,7 @@ constructor(private val appContext: Context, uuidGenerator: UuidGenerator) : Pro
6363

6464
override val myUuid: String by lazy { uuidGenerator.next().toString() }
6565

66-
private val myProcessDetails by lazy {
67-
ProcessDetailsProvider.getCurrentProcessDetails(appContext)
68-
}
66+
private val myProcessDetails by lazy { ProcessDetailsProvider.getMyProcessDetails(appContext) }
6967

7068
private var hasGeneratedSession: Boolean = false
7169

@@ -100,8 +98,7 @@ constructor(private val appContext: Context, uuidGenerator: UuidGenerator) : Pro
10098
processDataMap
10199
?.toMutableMap()
102100
?.apply { this[myProcessName] = ProcessData(Process.myPid(), myUuid) }
103-
?.toMap()
104-
?: mapOf(myProcessName to ProcessData(Process.myPid(), myUuid))
101+
?.toMap() ?: mapOf(myProcessName to ProcessData(Process.myPid(), myUuid))
105102

106103
/** Gets the current details for all of the app's running processes. */
107104
private fun getAppProcessDetails() = ProcessDetailsProvider.getAppProcessDetails(appContext)

firebase-sessions/src/main/kotlin/com/google/firebase/sessions/ProcessDetailsProvider.kt

Lines changed: 8 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -23,13 +23,9 @@ import android.os.Build
2323
import android.os.Process
2424
import com.google.android.gms.common.util.ProcessUtils
2525

26-
/**
27-
* Provider of ProcessDetails.
28-
*
29-
* @hide
30-
*/
26+
/** Provide [ProcessDetails] for all app processes. */
3127
internal object ProcessDetailsProvider {
32-
/** Gets the details for all of this app's running processes. */
28+
/** Gets the details for all the app's running processes. */
3329
fun getAppProcessDetails(context: Context): List<ProcessDetails> {
3430
val appUid = context.applicationInfo.uid
3531
val defaultProcessName = context.applicationInfo.processName
@@ -53,27 +49,19 @@ internal object ProcessDetailsProvider {
5349
}
5450

5551
/**
56-
* Gets this app's current process details.
52+
* Gets this process's details.
5753
*
58-
* If the current process details are not found for whatever reason, returns process details with
59-
* just the current process name and pid set.
54+
* If this process's full details are not found for whatever reason, returns process details with
55+
* just the process name and pid set.
6056
*/
61-
fun getCurrentProcessDetails(context: Context): ProcessDetails {
57+
fun getMyProcessDetails(context: Context): ProcessDetails {
6258
val pid = Process.myPid()
6359
return getAppProcessDetails(context).find { it.pid == pid }
64-
?: buildProcessDetails(getProcessName(), pid)
60+
?: ProcessDetails(getProcessName(), pid, importance = 0, isDefaultProcess = false)
6561
}
6662

67-
/** Builds a ProcessDetails object. */
68-
private fun buildProcessDetails(
69-
processName: String,
70-
pid: Int = 0,
71-
importance: Int = 0,
72-
isDefaultProcess: Boolean = false
73-
) = ProcessDetails(processName, pid, importance, isDefaultProcess)
74-
7563
/** Gets the app's current process name. If it could not be found, returns an empty string. */
76-
internal fun getProcessName(): String {
64+
private fun getProcessName(): String {
7765
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.TIRAMISU) {
7866
return Process.myProcessName()
7967
}

firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionEvents.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ internal object SessionEvents {
8383
versionName = packageInfo.versionName ?: buildVersion,
8484
appBuildVersion = buildVersion,
8585
deviceManufacturer = Build.MANUFACTURER,
86-
ProcessDetailsProvider.getCurrentProcessDetails(firebaseApp.applicationContext),
86+
ProcessDetailsProvider.getMyProcessDetails(firebaseApp.applicationContext),
8787
ProcessDetailsProvider.getAppProcessDetails(firebaseApp.applicationContext),
8888
),
8989
)

firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SharedSessionRepository.kt

Lines changed: 72 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@ package com.google.firebase.sessions
1919
import android.util.Log
2020
import androidx.datastore.core.DataStore
2121
import com.google.firebase.annotations.concurrent.Background
22-
import com.google.firebase.sessions.ProcessDetailsProvider.getProcessName
2322
import com.google.firebase.sessions.api.FirebaseSessionsDependencies
2423
import com.google.firebase.sessions.api.SessionSubscriber
2524
import com.google.firebase.sessions.settings.SessionsSettings
@@ -92,7 +91,7 @@ constructor(
9291
return
9392
}
9493
val sessionData = localSessionData
95-
Log.d(TAG, "App backgrounded on ${getProcessName()} - $sessionData")
94+
Log.d(TAG, "App backgrounded on ${processDataManager.myProcessName} - $sessionData")
9695

9796
CoroutineScope(backgroundDispatcher).launch {
9897
try {
@@ -113,32 +112,57 @@ constructor(
113112
return
114113
}
115114
val sessionData = localSessionData
116-
Log.d(TAG, "App foregrounded on ${getProcessName()} - $sessionData")
115+
Log.d(TAG, "App foregrounded on ${processDataManager.myProcessName} - $sessionData")
117116

118-
if (shouldInitiateNewSession(sessionData)) {
117+
// Check if maybe the session data needs to be updated
118+
if (isSessionExpired(sessionData) || isMyProcessStale(sessionData)) {
119119
CoroutineScope(backgroundDispatcher).launch {
120120
try {
121121
sessionDataStore.updateData { currentSessionData ->
122-
// Double-check pattern
123-
if (shouldInitiateNewSession(currentSessionData)) {
122+
val isSessionExpired = isSessionExpired(currentSessionData)
123+
val isColdStart = isColdStart(currentSessionData)
124+
val isMyProcessStale = isMyProcessStale(currentSessionData)
125+
126+
val newProcessDataMap =
127+
if (isColdStart) {
128+
// Generate a new process data map for cold app start
129+
processDataManager.generateProcessDataMap()
130+
} else if (isMyProcessStale) {
131+
// Update the data map with this process if stale
132+
processDataManager.updateProcessDataMap(currentSessionData.processDataMap)
133+
} else {
134+
// No change
135+
currentSessionData.processDataMap
136+
}
137+
138+
// This is an expression, and returns the updated session data
139+
if (isSessionExpired || isColdStart) {
124140
val newSessionDetails =
125-
sessionGenerator.generateNewSession(sessionData.sessionDetails)
141+
sessionGenerator.generateNewSession(currentSessionData.sessionDetails)
126142
sessionFirelogPublisher.mayLogSession(sessionDetails = newSessionDetails)
127143
processDataManager.onSessionGenerated()
128-
currentSessionData.copy(sessionDetails = newSessionDetails, backgroundTime = null)
144+
currentSessionData.copy(
145+
sessionDetails = newSessionDetails,
146+
backgroundTime = null,
147+
processDataMap = newProcessDataMap,
148+
)
149+
} else if (isMyProcessStale) {
150+
currentSessionData.copy(
151+
processDataMap = processDataManager.updateProcessDataMap(newProcessDataMap)
152+
)
129153
} else {
130154
currentSessionData
131155
}
132156
}
133157
} catch (ex: Exception) {
134158
Log.d(TAG, "App appForegrounded, failed to update data. Message: ${ex.message}")
135-
val newSessionDetails = sessionGenerator.generateNewSession(sessionData.sessionDetails)
136-
localSessionData =
137-
localSessionData.copy(sessionDetails = newSessionDetails, backgroundTime = null)
138-
sessionFirelogPublisher.mayLogSession(sessionDetails = newSessionDetails)
139-
140-
val sessionId = newSessionDetails.sessionId
141-
notifySubscribers(sessionId, NotificationType.FALLBACK)
159+
if (isSessionExpired(sessionData)) {
160+
val newSessionDetails = sessionGenerator.generateNewSession(sessionData.sessionDetails)
161+
localSessionData =
162+
sessionData.copy(sessionDetails = newSessionDetails, backgroundTime = null)
163+
sessionFirelogPublisher.mayLogSession(sessionDetails = newSessionDetails)
164+
notifySubscribers(newSessionDetails.sessionId, NotificationType.FALLBACK)
165+
}
142166
}
143167
}
144168
}
@@ -161,22 +185,47 @@ constructor(
161185
}
162186
}
163187

164-
private fun shouldInitiateNewSession(sessionData: SessionData): Boolean {
188+
/** Checks if the session has expired. If no background time, consider it not expired. */
189+
private fun isSessionExpired(sessionData: SessionData): Boolean {
165190
sessionData.backgroundTime?.let { backgroundTime ->
166191
val interval = timeProvider.currentTime() - backgroundTime
167-
if (interval > sessionsSettings.sessionRestartTimeout) {
168-
Log.d(TAG, "Passed session restart timeout, so initiate a new session")
169-
return true
192+
val sessionExpired = (interval > sessionsSettings.sessionRestartTimeout)
193+
if (sessionExpired) {
194+
Log.d(TAG, "Session ${sessionData.sessionDetails.sessionId} is expired")
170195
}
196+
return sessionExpired
171197
}
172198

199+
Log.d(TAG, "Session ${sessionData.sessionDetails.sessionId} has not backgrounded yet")
200+
return false
201+
}
202+
203+
/** Checks for cold app start. If no process data map, consider it a cold start. */
204+
private fun isColdStart(sessionData: SessionData): Boolean {
173205
sessionData.processDataMap?.let { processDataMap ->
174-
Log.d(TAG, "Has not passed session restart timeout, so check for cold app start")
175-
return processDataManager.isColdStart(processDataMap)
206+
val coldStart = processDataManager.isColdStart(processDataMap)
207+
if (coldStart) {
208+
Log.d(TAG, "Cold app start detected")
209+
}
210+
return coldStart
176211
}
177212

178-
Log.d(TAG, "No process has backgrounded yet and no process data, should not change the session")
179-
return false
213+
Log.d(TAG, "No process data map")
214+
return true
215+
}
216+
217+
/** Checks if this process is stale. If no process data map, consider the process stale. */
218+
private fun isMyProcessStale(sessionData: SessionData): Boolean {
219+
sessionData.processDataMap?.let { processDataMap ->
220+
val myProcessStale = processDataManager.isMyProcessStale(processDataMap)
221+
if (myProcessStale) {
222+
Log.d(TAG, "Process ${processDataManager.myProcessName} is stale")
223+
}
224+
return myProcessStale
225+
}
226+
227+
Log.d(TAG, "No process data for ${processDataManager.myProcessName}")
228+
return true
180229
}
181230

182231
private companion object {

firebase-sessions/src/test/kotlin/com/google/firebase/sessions/ApplicationInfoTest.kt

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -36,9 +36,9 @@ class ApplicationInfoTest {
3636
@Test
3737
fun applicationInfo_populatesInfoCorrectly() {
3838
val firebaseApp = FakeFirebaseApp().firebaseApp
39-
val actualCurrentProcessDetails =
40-
ProcessDetailsProvider.getCurrentProcessDetails(firebaseApp.applicationContext)
41-
val actualAppProcessDetails =
39+
val myProcessDetails =
40+
ProcessDetailsProvider.getMyProcessDetails(firebaseApp.applicationContext)
41+
val appProcessDetails =
4242
ProcessDetailsProvider.getAppProcessDetails(firebaseApp.applicationContext)
4343
val applicationInfo = SessionEvents.getApplicationInfo(firebaseApp)
4444
assertThat(applicationInfo)
@@ -54,8 +54,8 @@ class ApplicationInfoTest {
5454
versionName = FakeFirebaseApp.MOCK_APP_VERSION,
5555
appBuildVersion = FakeFirebaseApp.MOCK_APP_BUILD_VERSION,
5656
deviceManufacturer = Build.MANUFACTURER,
57-
actualCurrentProcessDetails,
58-
actualAppProcessDetails,
57+
myProcessDetails,
58+
appProcessDetails,
5959
),
6060
)
6161
)
@@ -74,9 +74,9 @@ class ApplicationInfoTest {
7474
.build(),
7575
)
7676

77-
val actualCurrentProcessDetails =
78-
ProcessDetailsProvider.getCurrentProcessDetails(firebaseApp.applicationContext)
79-
val actualAppProcessDetails =
77+
val myProcessDetails =
78+
ProcessDetailsProvider.getMyProcessDetails(firebaseApp.applicationContext)
79+
val appProcessDetails =
8080
ProcessDetailsProvider.getAppProcessDetails(firebaseApp.applicationContext)
8181

8282
val applicationInfo = SessionEvents.getApplicationInfo(firebaseApp)
@@ -94,8 +94,8 @@ class ApplicationInfoTest {
9494
versionName = "0",
9595
appBuildVersion = "0",
9696
deviceManufacturer = Build.MANUFACTURER,
97-
actualCurrentProcessDetails,
98-
actualAppProcessDetails,
97+
myProcessDetails,
98+
appProcessDetails,
9999
),
100100
)
101101
)

0 commit comments

Comments
 (0)