Skip to content

Use multi-process DataStore instead of Preferences DataStore #6781

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Mar 20, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 3 additions & 7 deletions firebase-sessions/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,19 +1,15 @@
# Unreleased
* [changed] Use multi-process DataStore instead of Preferences DataStore

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


# 2.0.9
* [fixed] Make AQS resilient to background init in multi-process apps.


## Kotlin
The Kotlin extensions library transitively includes the updated
`firebase-sessions` library. The Kotlin extensions library has no additional
updates.

# 2.0.7
* [fixed] Removed extraneous logs that risk leaking internal identifiers.

Expand Down
4 changes: 3 additions & 1 deletion firebase-sessions/firebase-sessions.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ plugins {
id("firebase-vendor")
id("kotlin-android")
id("kotlin-kapt")
id("kotlinx-serialization")
}

firebaseLibrary {
Expand Down Expand Up @@ -76,7 +77,8 @@ dependencies {
implementation("com.google.android.datatransport:transport-api:3.2.0")
implementation(libs.javax.inject)
implementation(libs.androidx.annotation)
implementation(libs.androidx.datastore.preferences)
implementation(libs.androidx.datastore)
implementation(libs.kotlinx.serialization.json)

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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,23 +19,24 @@ package com.google.firebase.sessions
import android.content.Context
import android.util.Log
import androidx.datastore.core.DataStore
import androidx.datastore.core.MultiProcessDataStoreFactory
import androidx.datastore.core.handlers.ReplaceFileCorruptionHandler
import androidx.datastore.preferences.core.PreferenceDataStoreFactory
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.emptyPreferences
import androidx.datastore.preferences.preferencesDataStoreFile
import androidx.datastore.dataStoreFile
import com.google.android.datatransport.TransportFactory
import com.google.firebase.FirebaseApp
import com.google.firebase.annotations.concurrent.Background
import com.google.firebase.annotations.concurrent.Blocking
import com.google.firebase.inject.Provider
import com.google.firebase.installations.FirebaseInstallationsApi
import com.google.firebase.sessions.ProcessDetailsProvider.getProcessName
import com.google.firebase.sessions.settings.CrashlyticsSettingsFetcher
import com.google.firebase.sessions.settings.LocalOverrideSettings
import com.google.firebase.sessions.settings.RemoteSettings
import com.google.firebase.sessions.settings.RemoteSettingsFetcher
import com.google.firebase.sessions.settings.SessionConfigs
import com.google.firebase.sessions.settings.SessionConfigsSerializer
import com.google.firebase.sessions.settings.SessionsSettings
import com.google.firebase.sessions.settings.SettingsCache
import com.google.firebase.sessions.settings.SettingsCacheImpl
import com.google.firebase.sessions.settings.SettingsProvider
import dagger.Binds
import dagger.BindsInstance
Expand All @@ -45,10 +46,7 @@ import dagger.Provides
import javax.inject.Qualifier
import javax.inject.Singleton
import kotlin.coroutines.CoroutineContext

@Qualifier internal annotation class SessionConfigsDataStore

@Qualifier internal annotation class SessionDetailsDataStore
import kotlinx.coroutines.CoroutineScope

@Qualifier internal annotation class LocalOverrideSettingsProvider

Expand Down Expand Up @@ -119,6 +117,8 @@ internal interface FirebaseSessionsComponent {
@RemoteSettingsProvider
fun remoteSettings(impl: RemoteSettings): SettingsProvider

@Binds @Singleton fun settingsCache(impl: SettingsCacheImpl): SettingsCache

companion object {
private const val TAG = "FirebaseSessions"

Expand All @@ -133,31 +133,37 @@ internal interface FirebaseSessionsComponent {

@Provides
@Singleton
@SessionConfigsDataStore
fun sessionConfigsDataStore(appContext: Context): DataStore<Preferences> =
PreferenceDataStoreFactory.create(
fun sessionConfigsDataStore(
appContext: Context,
@Blocking blockingDispatcher: CoroutineContext,
): DataStore<SessionConfigs> =
MultiProcessDataStoreFactory.create(
serializer = SessionConfigsSerializer,
corruptionHandler =
ReplaceFileCorruptionHandler { ex ->
Log.w(TAG, "CorruptionException in settings DataStore in ${getProcessName()}.", ex)
emptyPreferences()
}
) {
appContext.preferencesDataStoreFile(SessionDataStoreConfigs.SETTINGS_CONFIG_NAME)
}
Log.w(TAG, "CorruptionException in session configs DataStore", ex)
SessionConfigsSerializer.defaultValue
},
scope = CoroutineScope(blockingDispatcher),
produceFile = { appContext.dataStoreFile("aqs/sessionConfigsDataStore.data") },
)

@Provides
@Singleton
@SessionDetailsDataStore
fun sessionDetailsDataStore(appContext: Context): DataStore<Preferences> =
PreferenceDataStoreFactory.create(
fun sessionDataStore(
appContext: Context,
@Blocking blockingDispatcher: CoroutineContext,
): DataStore<SessionData> =
MultiProcessDataStoreFactory.create(
serializer = SessionDataSerializer,
corruptionHandler =
ReplaceFileCorruptionHandler { ex ->
Log.w(TAG, "CorruptionException in sessions DataStore in ${getProcessName()}.", ex)
emptyPreferences()
}
) {
appContext.preferencesDataStoreFile(SessionDataStoreConfigs.SESSIONS_CONFIG_NAME)
}
Log.w(TAG, "CorruptionException in session data DataStore", ex)
SessionDataSerializer.defaultValue
},
scope = CoroutineScope(blockingDispatcher),
produceFile = { appContext.dataStoreFile("aqs/sessionDataStore.data") },
)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ package com.google.firebase.sessions
import android.content.Context
import android.util.Log
import androidx.annotation.Keep
import androidx.datastore.preferences.preferencesDataStore
import androidx.datastore.core.MultiProcessDataStoreFactory
import com.google.android.datatransport.TransportFactory
import com.google.firebase.FirebaseApp
import com.google.firebase.annotations.concurrent.Background
Expand Down Expand Up @@ -84,7 +84,7 @@ internal class FirebaseSessionsRegistrar : ComponentRegistrar {

init {
try {
::preferencesDataStore.javaClass
MultiProcessDataStoreFactory.javaClass
} catch (ex: NoClassDefFoundError) {
Log.w(
TAG,
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -17,27 +17,45 @@
package com.google.firebase.sessions

import android.util.Log
import androidx.datastore.core.CorruptionException
import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.emptyPreferences
import androidx.datastore.preferences.core.stringPreferencesKey
import androidx.datastore.core.Serializer
import com.google.firebase.Firebase
import com.google.firebase.annotations.concurrent.Background
import com.google.firebase.app
import java.io.IOException
import java.io.InputStream
import java.io.OutputStream
import java.util.concurrent.atomic.AtomicReference
import javax.inject.Inject
import javax.inject.Singleton
import kotlin.coroutines.CoroutineContext
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json

/** Datastore for sessions information */
internal data class FirebaseSessionsData(val sessionId: String?)
/** Data for sessions information */
@Serializable internal data class SessionData(val sessionId: String?)

/** DataStore json [Serializer] for [SessionData]. */
internal object SessionDataSerializer : Serializer<SessionData> {
override val defaultValue = SessionData(sessionId = null)

override suspend fun readFrom(input: InputStream): SessionData =
try {
Json.decodeFromString<SessionData>(input.readBytes().decodeToString())
} catch (ex: Exception) {
throw CorruptionException("Cannot parse session data", ex)
}

override suspend fun writeTo(t: SessionData, output: OutputStream) {
@Suppress("BlockingMethodInNonBlockingContext") // blockingDispatcher is safe for blocking calls
output.write(Json.encodeToString(SessionData.serializer(), t).encodeToByteArray())
}
}

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

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

private object FirebaseSessionDataKeys {
val SESSION_ID = stringPreferencesKey("session_id")
}

private val firebaseSessionDataFlow: Flow<FirebaseSessionsData> =
dataStore.data
.catch { exception ->
Log.e(TAG, "Error reading stored session data.", exception)
emit(emptyPreferences())
}
.map { preferences -> mapSessionsData(preferences) }
private val firebaseSessionDataFlow: Flow<SessionData> =
sessionDataStore.data.catch { ex ->
Log.e(TAG, "Error reading stored session data.", ex)
emit(SessionDataSerializer.defaultValue)
}

init {
CoroutineScope(backgroundDispatcher).launch {
Expand All @@ -88,19 +100,14 @@ constructor(
override fun updateSessionId(sessionId: String) {
CoroutineScope(backgroundDispatcher).launch {
try {
dataStore.edit { preferences ->
preferences[FirebaseSessionDataKeys.SESSION_ID] = sessionId
}
} catch (e: IOException) {
Log.w(TAG, "Failed to update session Id: $e")
sessionDataStore.updateData { SessionData(sessionId) }
} catch (ex: IOException) {
Log.w(TAG, "Failed to update session Id", ex)
}
}
}

override fun getCurrentSessionId() = currentSessionFromDatastore.get()?.sessionId

private fun mapSessionsData(preferences: Preferences): FirebaseSessionsData =
FirebaseSessionsData(preferences[FirebaseSessionDataKeys.SESSION_ID])
override fun getCurrentSessionId(): String? = currentSessionFromDatastore.get()?.sessionId

private companion object {
private const val TAG = "FirebaseSessionsRepo"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ constructor(private val timeProvider: TimeProvider, private val uuidGenerator: U
sessionId = if (sessionIndex == 0) firstSessionId else generateSessionId(),
firstSessionId,
sessionIndex,
sessionStartTimestampUs = timeProvider.currentTimeUs(),
sessionStartTimestampUs = timeProvider.currentTime().us,
)
return currentSession
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,17 @@ import android.os.SystemClock
import kotlin.time.Duration
import kotlin.time.Duration.Companion.milliseconds

/** Time with accessors for microseconds, milliseconds, and seconds. */
internal data class Time(val ms: Long) {
val us = ms * 1_000
val seconds = ms / 1_000
}

/** Time provider interface, for testing purposes. */
internal interface TimeProvider {
fun elapsedRealtime(): Duration

fun currentTimeUs(): Long
fun currentTime(): Time
}

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

/**
* Gets the current "wall clock" time in microseconds.
* Gets the current "wall clock" time.
*
* This clock can be set by the user or the phone network, so the time may jump backwards or
* forwards unpredictably. This clock should only be used when correspondence with real-world
* dates and times is important, such as in a calendar or alarm clock application.
*/
override fun currentTimeUs(): Long = System.currentTimeMillis() * US_PER_MILLIS

/** Microseconds per millisecond. */
private const val US_PER_MILLIS = 1000L
override fun currentTime(): Time = Time(ms = System.currentTimeMillis())
}
Loading
Loading