Skip to content

Commit 1f97344

Browse files
committed
Use multi-process DataStore instead of Preferences DataStore
1 parent f86d40d commit 1f97344

22 files changed

+576
-469
lines changed

build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ plugins {
2626
id("firebase-ci")
2727
id("smoke-tests")
2828
alias(libs.plugins.google.services)
29+
alias(libs.plugins.kotlinx.serialization) apply false
2930
}
3031

3132
extra["targetSdkVersion"] = 34

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)