Skip to content

Commit f86d40d

Browse files
authored
Simplify settings package and a few more classes using DI (#6778)
Simplify settings package and a few more classes using DI. The biggest example is `SessionsSettings`, which used to have multiple secondary constructors. This change lets us avoid plumbing dependencies like `DataStore<>` through multiple classes. Moved DataStore construction into provides, which will be needed for `MultiProcessDataStoreFactory`. Also cleaned up tests a bit. We shouldn't use Dagger in the unit tests on Android. Once we remove the bound service, it will be easy to remove DI from the unit tests. Instead, unit tests will just call the constructors directly and pass in real or fake instances.
1 parent 76990d9 commit f86d40d

19 files changed

+354
-332
lines changed

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

Lines changed: 79 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,22 +17,48 @@
1717
package com.google.firebase.sessions
1818

1919
import android.content.Context
20+
import android.util.Log
21+
import androidx.datastore.core.DataStore
22+
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
2027
import com.google.android.datatransport.TransportFactory
2128
import com.google.firebase.FirebaseApp
2229
import com.google.firebase.annotations.concurrent.Background
2330
import com.google.firebase.annotations.concurrent.Blocking
2431
import com.google.firebase.inject.Provider
2532
import com.google.firebase.installations.FirebaseInstallationsApi
33+
import com.google.firebase.sessions.ProcessDetailsProvider.getProcessName
34+
import com.google.firebase.sessions.settings.CrashlyticsSettingsFetcher
35+
import com.google.firebase.sessions.settings.LocalOverrideSettings
36+
import com.google.firebase.sessions.settings.RemoteSettings
37+
import com.google.firebase.sessions.settings.RemoteSettingsFetcher
2638
import com.google.firebase.sessions.settings.SessionsSettings
39+
import com.google.firebase.sessions.settings.SettingsProvider
2740
import dagger.Binds
2841
import dagger.BindsInstance
2942
import dagger.Component
3043
import dagger.Module
3144
import dagger.Provides
45+
import javax.inject.Qualifier
3246
import javax.inject.Singleton
3347
import kotlin.coroutines.CoroutineContext
3448

35-
/** Dagger component to provide [FirebaseSessions] and its dependencies. */
49+
@Qualifier internal annotation class SessionConfigsDataStore
50+
51+
@Qualifier internal annotation class SessionDetailsDataStore
52+
53+
@Qualifier internal annotation class LocalOverrideSettingsProvider
54+
55+
@Qualifier internal annotation class RemoteSettingsProvider
56+
57+
/**
58+
* Dagger component to provide [FirebaseSessions] and its dependencies.
59+
*
60+
* This gets configured and built in [FirebaseSessionsRegistrar.getComponents].
61+
*/
3662
@Singleton
3763
@Component(modules = [FirebaseSessionsComponent.MainModule::class])
3864
internal interface FirebaseSessionsComponent {
@@ -79,8 +105,59 @@ internal interface FirebaseSessionsComponent {
79105
impl: SessionLifecycleServiceBinderImpl
80106
): SessionLifecycleServiceBinder
81107

108+
@Binds
109+
@Singleton
110+
fun crashlyticsSettingsFetcher(impl: RemoteSettingsFetcher): CrashlyticsSettingsFetcher
111+
112+
@Binds
113+
@Singleton
114+
@LocalOverrideSettingsProvider
115+
fun localOverrideSettings(impl: LocalOverrideSettings): SettingsProvider
116+
117+
@Binds
118+
@Singleton
119+
@RemoteSettingsProvider
120+
fun remoteSettings(impl: RemoteSettings): SettingsProvider
121+
82122
companion object {
83-
@Provides @Singleton fun sessionGenerator() = SessionGenerator(timeProvider = WallClock)
123+
private const val TAG = "FirebaseSessions"
124+
125+
@Provides @Singleton fun timeProvider(): TimeProvider = TimeProviderImpl
126+
127+
@Provides @Singleton fun uuidGenerator(): UuidGenerator = UuidGeneratorImpl
128+
129+
@Provides
130+
@Singleton
131+
fun applicationInfo(firebaseApp: FirebaseApp): ApplicationInfo =
132+
SessionEvents.getApplicationInfo(firebaseApp)
133+
134+
@Provides
135+
@Singleton
136+
@SessionConfigsDataStore
137+
fun sessionConfigsDataStore(appContext: Context): DataStore<Preferences> =
138+
PreferenceDataStoreFactory.create(
139+
corruptionHandler =
140+
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+
}
147+
148+
@Provides
149+
@Singleton
150+
@SessionDetailsDataStore
151+
fun sessionDetailsDataStore(appContext: Context): DataStore<Preferences> =
152+
PreferenceDataStoreFactory.create(
153+
corruptionHandler =
154+
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+
}
84161
}
85162
}
86163
}

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

Lines changed: 3 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -16,19 +16,15 @@
1616

1717
package com.google.firebase.sessions
1818

19-
import android.content.Context
2019
import android.util.Log
2120
import androidx.datastore.core.DataStore
22-
import androidx.datastore.core.handlers.ReplaceFileCorruptionHandler
2321
import androidx.datastore.preferences.core.Preferences
2422
import androidx.datastore.preferences.core.edit
2523
import androidx.datastore.preferences.core.emptyPreferences
2624
import androidx.datastore.preferences.core.stringPreferencesKey
27-
import androidx.datastore.preferences.preferencesDataStore
2825
import com.google.firebase.Firebase
2926
import com.google.firebase.annotations.concurrent.Background
3027
import com.google.firebase.app
31-
import com.google.firebase.sessions.ProcessDetailsProvider.getProcessName
3228
import java.io.IOException
3329
import java.util.concurrent.atomic.AtomicReference
3430
import javax.inject.Inject
@@ -64,8 +60,8 @@ internal interface SessionDatastore {
6460
internal class SessionDatastoreImpl
6561
@Inject
6662
constructor(
67-
private val appContext: Context,
6863
@Background private val backgroundDispatcher: CoroutineContext,
64+
@SessionDetailsDataStore private val dataStore: DataStore<Preferences>,
6965
) : SessionDatastore {
7066

7167
/** Most recent session from datastore is updated asynchronously whenever it changes */
@@ -76,7 +72,7 @@ constructor(
7672
}
7773

7874
private val firebaseSessionDataFlow: Flow<FirebaseSessionsData> =
79-
appContext.dataStore.data
75+
dataStore.data
8076
.catch { exception ->
8177
Log.e(TAG, "Error reading stored session data.", exception)
8278
emit(emptyPreferences())
@@ -92,7 +88,7 @@ constructor(
9288
override fun updateSessionId(sessionId: String) {
9389
CoroutineScope(backgroundDispatcher).launch {
9490
try {
95-
appContext.dataStore.edit { preferences ->
91+
dataStore.edit { preferences ->
9692
preferences[FirebaseSessionDataKeys.SESSION_ID] = sessionId
9793
}
9894
} catch (e: IOException) {
@@ -108,15 +104,5 @@ constructor(
108104

109105
private companion object {
110106
private const val TAG = "FirebaseSessionsRepo"
111-
112-
private val Context.dataStore: DataStore<Preferences> by
113-
preferencesDataStore(
114-
name = SessionDataStoreConfigs.SESSIONS_CONFIG_NAME,
115-
corruptionHandler =
116-
ReplaceFileCorruptionHandler { ex ->
117-
Log.w(TAG, "CorruptionException in sessions DataStore in ${getProcessName()}.", ex)
118-
emptyPreferences()
119-
},
120-
)
121107
}
122108
}

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

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ package com.google.firebase.sessions
1919
import com.google.errorprone.annotations.CanIgnoreReturnValue
2020
import com.google.firebase.Firebase
2121
import com.google.firebase.app
22-
import java.util.UUID
22+
import javax.inject.Inject
2323
import javax.inject.Singleton
2424

2525
/**
@@ -37,10 +37,9 @@ internal data class SessionDetails(
3737
* [SessionDetails] up to date with the latest values.
3838
*/
3939
@Singleton
40-
internal class SessionGenerator(
41-
private val timeProvider: TimeProvider,
42-
private val uuidGenerator: () -> UUID = UUID::randomUUID,
43-
) {
40+
internal class SessionGenerator
41+
@Inject
42+
constructor(private val timeProvider: TimeProvider, private val uuidGenerator: UuidGenerator) {
4443
private val firstSessionId = generateSessionId()
4544
private var sessionIndex = -1
4645

@@ -66,7 +65,7 @@ internal class SessionGenerator(
6665
return currentSession
6766
}
6867

69-
private fun generateSessionId() = uuidGenerator().toString().replace("-", "").lowercase()
68+
private fun generateSessionId() = uuidGenerator.next().toString().replace("-", "").lowercase()
7069

7170
internal companion object {
7271
val instance: SessionGenerator

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

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,12 @@ import kotlin.time.Duration.Companion.milliseconds
2323
/** Time provider interface, for testing purposes. */
2424
internal interface TimeProvider {
2525
fun elapsedRealtime(): Duration
26+
2627
fun currentTimeUs(): Long
2728
}
2829

29-
/** "Wall clock" time provider. */
30-
internal object WallClock : TimeProvider {
30+
/** "Wall clock" time provider implementation. */
31+
internal object TimeProviderImpl : TimeProvider {
3132
/**
3233
* Gets the [Duration] elapsed in "wall clock" time since device boot.
3334
*
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
/*
2+
* Copyright 2025 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.google.firebase.sessions
18+
19+
import java.util.UUID
20+
21+
/** UUID generator interface. */
22+
internal fun interface UuidGenerator {
23+
fun next(): UUID
24+
}
25+
26+
/** Generate random UUIDs using [UUID.randomUUID]. */
27+
internal object UuidGeneratorImpl : UuidGenerator {
28+
override fun next(): UUID = UUID.randomUUID()
29+
}

firebase-sessions/src/main/kotlin/com/google/firebase/sessions/settings/LocalOverrideSettings.kt

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -19,20 +19,19 @@ package com.google.firebase.sessions.settings
1919
import android.content.Context
2020
import android.content.pm.PackageManager
2121
import android.os.Bundle
22+
import javax.inject.Inject
23+
import javax.inject.Singleton
2224
import kotlin.time.Duration
2325
import kotlin.time.DurationUnit
2426
import kotlin.time.toDuration
2527

26-
internal class LocalOverrideSettings(context: Context) : SettingsProvider {
27-
@Suppress("DEPRECATION") // TODO(mrober): Use ApplicationInfoFlags when target sdk set to 33
28+
@Singleton
29+
internal class LocalOverrideSettings @Inject constructor(appContext: Context) : SettingsProvider {
2830
private val metadata =
29-
context.packageManager
30-
.getApplicationInfo(
31-
context.packageName,
32-
PackageManager.GET_META_DATA,
33-
)
31+
appContext.packageManager
32+
.getApplicationInfo(appContext.packageName, PackageManager.GET_META_DATA)
3433
.metaData
35-
?: Bundle.EMPTY // Default to an empty bundle, meaning no cached values.
34+
?: Bundle.EMPTY // Default to an empty bundle
3635

3736
override val sessionEnabled: Boolean?
3837
get() =

firebase-sessions/src/main/kotlin/com/google/firebase/sessions/settings/RemoteSettings.kt

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,13 @@ package com.google.firebase.sessions.settings
1919
import android.os.Build
2020
import android.util.Log
2121
import androidx.annotation.VisibleForTesting
22-
import androidx.datastore.core.DataStore
23-
import androidx.datastore.preferences.core.Preferences
22+
import com.google.firebase.annotations.concurrent.Background
2423
import com.google.firebase.installations.FirebaseInstallationsApi
2524
import com.google.firebase.sessions.ApplicationInfo
2625
import com.google.firebase.sessions.InstallationId
26+
import dagger.Lazy
27+
import javax.inject.Inject
28+
import javax.inject.Singleton
2729
import kotlin.coroutines.CoroutineContext
2830
import kotlin.time.Duration
2931
import kotlin.time.Duration.Companion.seconds
@@ -34,14 +36,19 @@ import kotlinx.coroutines.sync.withLock
3436
import org.json.JSONException
3537
import org.json.JSONObject
3638

37-
internal class RemoteSettings(
38-
private val backgroundDispatcher: CoroutineContext,
39+
@Singleton
40+
internal class RemoteSettings
41+
@Inject
42+
constructor(
43+
@Background private val backgroundDispatcher: CoroutineContext,
3944
private val firebaseInstallationsApi: FirebaseInstallationsApi,
4045
private val appInfo: ApplicationInfo,
4146
private val configsFetcher: CrashlyticsSettingsFetcher,
42-
dataStore: DataStore<Preferences>,
47+
private val lazySettingsCache: Lazy<SettingsCache>,
4348
) : SettingsProvider {
44-
private val settingsCache by lazy { SettingsCache(dataStore) }
49+
private val settingsCache: SettingsCache
50+
get() = lazySettingsCache.get()
51+
4552
private val fetchInProgress = Mutex()
4653

4754
override val sessionEnabled: Boolean?

firebase-sessions/src/main/kotlin/com/google/firebase/sessions/settings/RemoteSettingsFetcher.kt

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,13 @@
1717
package com.google.firebase.sessions.settings
1818

1919
import android.net.Uri
20+
import com.google.firebase.annotations.concurrent.Background
2021
import com.google.firebase.sessions.ApplicationInfo
2122
import java.io.BufferedReader
2223
import java.io.InputStreamReader
2324
import java.net.URL
25+
import javax.inject.Inject
26+
import javax.inject.Singleton
2427
import javax.net.ssl.HttpsURLConnection
2528
import kotlin.coroutines.CoroutineContext
2629
import kotlinx.coroutines.withContext
@@ -30,20 +33,22 @@ internal fun interface CrashlyticsSettingsFetcher {
3033
suspend fun doConfigFetch(
3134
headerOptions: Map<String, String>,
3235
onSuccess: suspend (JSONObject) -> Unit,
33-
onFailure: suspend (msg: String) -> Unit
36+
onFailure: suspend (msg: String) -> Unit,
3437
)
3538
}
3639

37-
internal class RemoteSettingsFetcher(
40+
@Singleton
41+
internal class RemoteSettingsFetcher
42+
@Inject
43+
constructor(
3844
private val appInfo: ApplicationInfo,
39-
private val blockingDispatcher: CoroutineContext,
40-
private val baseUrl: String = FIREBASE_SESSIONS_BASE_URL_STRING,
45+
@Background private val blockingDispatcher: CoroutineContext,
4146
) : CrashlyticsSettingsFetcher {
4247
@Suppress("BlockingMethodInNonBlockingContext") // blockingDispatcher is safe for blocking calls.
4348
override suspend fun doConfigFetch(
4449
headerOptions: Map<String, String>,
4550
onSuccess: suspend (JSONObject) -> Unit,
46-
onFailure: suspend (String) -> Unit
51+
onFailure: suspend (String) -> Unit,
4752
) =
4853
withContext(blockingDispatcher) {
4954
try {
@@ -78,7 +83,7 @@ internal class RemoteSettingsFetcher(
7883
val uri =
7984
Uri.Builder()
8085
.scheme("https")
81-
.authority(baseUrl)
86+
.authority(FIREBASE_SESSIONS_BASE_URL_STRING)
8287
.appendPath("spi")
8388
.appendPath("v2")
8489
.appendPath("platforms")

0 commit comments

Comments
 (0)