Skip to content

Commit 0038b58

Browse files
committed
Use multi-process DataStore instead of Preferences DataStore (#6781)
Use multi-process DataStore instead of Preferences DataStore. This change allows multiple processes to share the same datastore file safely. This reduces settings fetch to one per app run, not one per process. Also updated the TimeProvider to provide an object with explicit time units. This will make time less error prone. Removed all instances of `System.currentTimeMillis()` from tests, making them deterministic.
1 parent 5aff679 commit 0038b58

22 files changed

+575
-480
lines changed

firebase-sessions/CHANGELOG.md

Lines changed: 1 addition & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,15 @@
11
# Unreleased
2-
2+
* [changed] Use multi-process DataStore instead of Preferences DataStore
33

44
# 2.1.0
55
* [changed] Add warning for known issue b/328687152
66
* [changed] Use Dagger for dependency injection
77
* [changed] Updated datastore dependency to v1.1.3 to
88
fix [CVE-2024-7254](https://github.com/advisories/GHSA-735f-pc8j-v9w8).
99

10-
11-
## Kotlin
12-
The Kotlin extensions library transitively includes the updated
13-
`firebase-sessions` library. The Kotlin extensions library has no additional
14-
updates.
15-
1610
# 2.0.9
1711
* [fixed] Make AQS resilient to background init in multi-process apps.
1812

19-
20-
## Kotlin
21-
The Kotlin extensions library transitively includes the updated
22-
`firebase-sessions` library. The Kotlin extensions library has no additional
23-
updates.
24-
2513
# 2.0.7
2614
* [fixed] Removed extraneous logs that risk leaking internal identifiers.
2715

firebase-sessions/firebase-sessions.gradle.kts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ plugins {
2121
id("firebase-vendor")
2222
id("kotlin-android")
2323
id("kotlin-kapt")
24+
id("kotlinx-serialization")
2425
}
2526

2627
firebaseLibrary {
@@ -76,7 +77,8 @@ dependencies {
7677
implementation("com.google.android.datatransport:transport-api:3.2.0")
7778
implementation(libs.javax.inject)
7879
implementation(libs.androidx.annotation)
79-
implementation(libs.androidx.datastore.preferences)
80+
implementation(libs.androidx.datastore)
81+
implementation(libs.kotlinx.serialization.json)
8082

8183
vendor(libs.dagger.dagger) { exclude(group = "javax.inject", module = "javax.inject") }
8284

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

Lines changed: 33 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -19,23 +19,24 @@ package com.google.firebase.sessions
1919
import android.content.Context
2020
import android.util.Log
2121
import androidx.datastore.core.DataStore
22+
import androidx.datastore.core.MultiProcessDataStoreFactory
2223
import androidx.datastore.core.handlers.ReplaceFileCorruptionHandler
23-
import androidx.datastore.preferences.core.PreferenceDataStoreFactory
24-
import androidx.datastore.preferences.core.Preferences
25-
import androidx.datastore.preferences.core.emptyPreferences
26-
import androidx.datastore.preferences.preferencesDataStoreFile
24+
import androidx.datastore.dataStoreFile
2725
import com.google.android.datatransport.TransportFactory
2826
import com.google.firebase.FirebaseApp
2927
import com.google.firebase.annotations.concurrent.Background
3028
import com.google.firebase.annotations.concurrent.Blocking
3129
import com.google.firebase.inject.Provider
3230
import com.google.firebase.installations.FirebaseInstallationsApi
33-
import com.google.firebase.sessions.ProcessDetailsProvider.getProcessName
3431
import com.google.firebase.sessions.settings.CrashlyticsSettingsFetcher
3532
import com.google.firebase.sessions.settings.LocalOverrideSettings
3633
import com.google.firebase.sessions.settings.RemoteSettings
3734
import com.google.firebase.sessions.settings.RemoteSettingsFetcher
35+
import com.google.firebase.sessions.settings.SessionConfigs
36+
import com.google.firebase.sessions.settings.SessionConfigsSerializer
3837
import com.google.firebase.sessions.settings.SessionsSettings
38+
import com.google.firebase.sessions.settings.SettingsCache
39+
import com.google.firebase.sessions.settings.SettingsCacheImpl
3940
import com.google.firebase.sessions.settings.SettingsProvider
4041
import dagger.Binds
4142
import dagger.BindsInstance
@@ -45,10 +46,7 @@ import dagger.Provides
4546
import javax.inject.Qualifier
4647
import javax.inject.Singleton
4748
import kotlin.coroutines.CoroutineContext
48-
49-
@Qualifier internal annotation class SessionConfigsDataStore
50-
51-
@Qualifier internal annotation class SessionDetailsDataStore
49+
import kotlinx.coroutines.CoroutineScope
5250

5351
@Qualifier internal annotation class LocalOverrideSettingsProvider
5452

@@ -119,6 +117,8 @@ internal interface FirebaseSessionsComponent {
119117
@RemoteSettingsProvider
120118
fun remoteSettings(impl: RemoteSettings): SettingsProvider
121119

120+
@Binds @Singleton fun settingsCache(impl: SettingsCacheImpl): SettingsCache
121+
122122
companion object {
123123
private const val TAG = "FirebaseSessions"
124124

@@ -133,31 +133,37 @@ internal interface FirebaseSessionsComponent {
133133

134134
@Provides
135135
@Singleton
136-
@SessionConfigsDataStore
137-
fun sessionConfigsDataStore(appContext: Context): DataStore<Preferences> =
138-
PreferenceDataStoreFactory.create(
136+
fun sessionConfigsDataStore(
137+
appContext: Context,
138+
@Blocking blockingDispatcher: CoroutineContext,
139+
): DataStore<SessionConfigs> =
140+
MultiProcessDataStoreFactory.create(
141+
serializer = SessionConfigsSerializer,
139142
corruptionHandler =
140143
ReplaceFileCorruptionHandler { ex ->
141-
Log.w(TAG, "CorruptionException in settings DataStore in ${getProcessName()}.", ex)
142-
emptyPreferences()
143-
}
144-
) {
145-
appContext.preferencesDataStoreFile(SessionDataStoreConfigs.SETTINGS_CONFIG_NAME)
146-
}
144+
Log.w(TAG, "CorruptionException in session configs DataStore", ex)
145+
SessionConfigsSerializer.defaultValue
146+
},
147+
scope = CoroutineScope(blockingDispatcher),
148+
produceFile = { appContext.dataStoreFile("aqs/sessionConfigsDataStore.data") },
149+
)
147150

148151
@Provides
149152
@Singleton
150-
@SessionDetailsDataStore
151-
fun sessionDetailsDataStore(appContext: Context): DataStore<Preferences> =
152-
PreferenceDataStoreFactory.create(
153+
fun sessionDataStore(
154+
appContext: Context,
155+
@Blocking blockingDispatcher: CoroutineContext,
156+
): DataStore<SessionData> =
157+
MultiProcessDataStoreFactory.create(
158+
serializer = SessionDataSerializer,
153159
corruptionHandler =
154160
ReplaceFileCorruptionHandler { ex ->
155-
Log.w(TAG, "CorruptionException in sessions DataStore in ${getProcessName()}.", ex)
156-
emptyPreferences()
157-
}
158-
) {
159-
appContext.preferencesDataStoreFile(SessionDataStoreConfigs.SESSIONS_CONFIG_NAME)
160-
}
161+
Log.w(TAG, "CorruptionException in session data DataStore", ex)
162+
SessionDataSerializer.defaultValue
163+
},
164+
scope = CoroutineScope(blockingDispatcher),
165+
produceFile = { appContext.dataStoreFile("aqs/sessionDataStore.data") },
166+
)
161167
}
162168
}
163169
}

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ package com.google.firebase.sessions
1919
import android.content.Context
2020
import android.util.Log
2121
import androidx.annotation.Keep
22-
import androidx.datastore.preferences.preferencesDataStore
22+
import androidx.datastore.core.MultiProcessDataStoreFactory
2323
import com.google.android.datatransport.TransportFactory
2424
import com.google.firebase.FirebaseApp
2525
import com.google.firebase.annotations.concurrent.Background
@@ -84,7 +84,7 @@ internal class FirebaseSessionsRegistrar : ComponentRegistrar {
8484

8585
init {
8686
try {
87-
::preferencesDataStore.javaClass
87+
MultiProcessDataStoreFactory.javaClass
8888
} catch (ex: NoClassDefFoundError) {
8989
Log.w(
9090
TAG,

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

Lines changed: 0 additions & 40 deletions
This file was deleted.

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

Lines changed: 36 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -17,27 +17,45 @@
1717
package com.google.firebase.sessions
1818

1919
import android.util.Log
20+
import androidx.datastore.core.CorruptionException
2021
import androidx.datastore.core.DataStore
21-
import androidx.datastore.preferences.core.Preferences
22-
import androidx.datastore.preferences.core.edit
23-
import androidx.datastore.preferences.core.emptyPreferences
24-
import androidx.datastore.preferences.core.stringPreferencesKey
22+
import androidx.datastore.core.Serializer
2523
import com.google.firebase.Firebase
2624
import com.google.firebase.annotations.concurrent.Background
2725
import com.google.firebase.app
2826
import java.io.IOException
27+
import java.io.InputStream
28+
import java.io.OutputStream
2929
import java.util.concurrent.atomic.AtomicReference
3030
import javax.inject.Inject
3131
import javax.inject.Singleton
3232
import kotlin.coroutines.CoroutineContext
3333
import kotlinx.coroutines.CoroutineScope
3434
import kotlinx.coroutines.flow.Flow
3535
import kotlinx.coroutines.flow.catch
36-
import kotlinx.coroutines.flow.map
3736
import kotlinx.coroutines.launch
37+
import kotlinx.serialization.Serializable
38+
import kotlinx.serialization.json.Json
3839

39-
/** Datastore for sessions information */
40-
internal data class FirebaseSessionsData(val sessionId: String?)
40+
/** Data for sessions information */
41+
@Serializable internal data class SessionData(val sessionId: String?)
42+
43+
/** DataStore json [Serializer] for [SessionData]. */
44+
internal object SessionDataSerializer : Serializer<SessionData> {
45+
override val defaultValue = SessionData(sessionId = null)
46+
47+
override suspend fun readFrom(input: InputStream): SessionData =
48+
try {
49+
Json.decodeFromString<SessionData>(input.readBytes().decodeToString())
50+
} catch (ex: Exception) {
51+
throw CorruptionException("Cannot parse session data", ex)
52+
}
53+
54+
override suspend fun writeTo(t: SessionData, output: OutputStream) {
55+
@Suppress("BlockingMethodInNonBlockingContext") // blockingDispatcher is safe for blocking calls
56+
output.write(Json.encodeToString(SessionData.serializer(), t).encodeToByteArray())
57+
}
58+
}
4159

4260
/** Handles reading to and writing from the [DataStore]. */
4361
internal interface SessionDatastore {
@@ -61,23 +79,17 @@ internal class SessionDatastoreImpl
6179
@Inject
6280
constructor(
6381
@Background private val backgroundDispatcher: CoroutineContext,
64-
@SessionDetailsDataStore private val dataStore: DataStore<Preferences>,
82+
private val sessionDataStore: DataStore<SessionData>,
6583
) : SessionDatastore {
6684

6785
/** Most recent session from datastore is updated asynchronously whenever it changes */
68-
private val currentSessionFromDatastore = AtomicReference<FirebaseSessionsData>()
86+
private val currentSessionFromDatastore = AtomicReference<SessionData>()
6987

70-
private object FirebaseSessionDataKeys {
71-
val SESSION_ID = stringPreferencesKey("session_id")
72-
}
73-
74-
private val firebaseSessionDataFlow: Flow<FirebaseSessionsData> =
75-
dataStore.data
76-
.catch { exception ->
77-
Log.e(TAG, "Error reading stored session data.", exception)
78-
emit(emptyPreferences())
79-
}
80-
.map { preferences -> mapSessionsData(preferences) }
88+
private val firebaseSessionDataFlow: Flow<SessionData> =
89+
sessionDataStore.data.catch { ex ->
90+
Log.e(TAG, "Error reading stored session data.", ex)
91+
emit(SessionDataSerializer.defaultValue)
92+
}
8193

8294
init {
8395
CoroutineScope(backgroundDispatcher).launch {
@@ -88,19 +100,14 @@ constructor(
88100
override fun updateSessionId(sessionId: String) {
89101
CoroutineScope(backgroundDispatcher).launch {
90102
try {
91-
dataStore.edit { preferences ->
92-
preferences[FirebaseSessionDataKeys.SESSION_ID] = sessionId
93-
}
94-
} catch (e: IOException) {
95-
Log.w(TAG, "Failed to update session Id: $e")
103+
sessionDataStore.updateData { SessionData(sessionId) }
104+
} catch (ex: IOException) {
105+
Log.w(TAG, "Failed to update session Id", ex)
96106
}
97107
}
98108
}
99109

100-
override fun getCurrentSessionId() = currentSessionFromDatastore.get()?.sessionId
101-
102-
private fun mapSessionsData(preferences: Preferences): FirebaseSessionsData =
103-
FirebaseSessionsData(preferences[FirebaseSessionDataKeys.SESSION_ID])
110+
override fun getCurrentSessionId(): String? = currentSessionFromDatastore.get()?.sessionId
104111

105112
private companion object {
106113
private const val TAG = "FirebaseSessionsRepo"

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ constructor(private val timeProvider: TimeProvider, private val uuidGenerator: U
6060
sessionId = if (sessionIndex == 0) firstSessionId else generateSessionId(),
6161
firstSessionId,
6262
sessionIndex,
63-
sessionStartTimestampUs = timeProvider.currentTimeUs(),
63+
sessionStartTimestampUs = timeProvider.currentTime().us,
6464
)
6565
return currentSession
6666
}

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

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,17 @@ import android.os.SystemClock
2020
import kotlin.time.Duration
2121
import kotlin.time.Duration.Companion.milliseconds
2222

23+
/** Time with accessors for microseconds, milliseconds, and seconds. */
24+
internal data class Time(val ms: Long) {
25+
val us = ms * 1_000
26+
val seconds = ms / 1_000
27+
}
28+
2329
/** Time provider interface, for testing purposes. */
2430
internal interface TimeProvider {
2531
fun elapsedRealtime(): Duration
2632

27-
fun currentTimeUs(): Long
33+
fun currentTime(): Time
2834
}
2935

3036
/** "Wall clock" time provider implementation. */
@@ -38,14 +44,11 @@ internal object TimeProviderImpl : TimeProvider {
3844
override fun elapsedRealtime(): Duration = SystemClock.elapsedRealtime().milliseconds
3945

4046
/**
41-
* Gets the current "wall clock" time in microseconds.
47+
* Gets the current "wall clock" time.
4248
*
4349
* This clock can be set by the user or the phone network, so the time may jump backwards or
4450
* forwards unpredictably. This clock should only be used when correspondence with real-world
4551
* dates and times is important, such as in a calendar or alarm clock application.
4652
*/
47-
override fun currentTimeUs(): Long = System.currentTimeMillis() * US_PER_MILLIS
48-
49-
/** Microseconds per millisecond. */
50-
private const val US_PER_MILLIS = 1000L
53+
override fun currentTime(): Time = Time(ms = System.currentTimeMillis())
5154
}

0 commit comments

Comments
 (0)