Skip to content
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
Original file line number Diff line number Diff line change
Expand Up @@ -17,22 +17,48 @@
package com.google.firebase.sessions

import android.content.Context
import android.util.Log
import androidx.datastore.core.DataStore
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 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.SessionsSettings
import com.google.firebase.sessions.settings.SettingsProvider
import dagger.Binds
import dagger.BindsInstance
import dagger.Component
import dagger.Module
import dagger.Provides
import javax.inject.Qualifier
import javax.inject.Singleton
import kotlin.coroutines.CoroutineContext

/** Dagger component to provide [FirebaseSessions] and its dependencies. */
@Qualifier internal annotation class SessionConfigsDataStore

@Qualifier internal annotation class SessionDetailsDataStore

@Qualifier internal annotation class LocalOverrideSettingsProvider

@Qualifier internal annotation class RemoteSettingsProvider

/**
* Dagger component to provide [FirebaseSessions] and its dependencies.
*
* This gets configured and built in [FirebaseSessionsRegistrar.getComponents].
*/
@Singleton
@Component(modules = [FirebaseSessionsComponent.MainModule::class])
internal interface FirebaseSessionsComponent {
Expand Down Expand Up @@ -79,8 +105,59 @@ internal interface FirebaseSessionsComponent {
impl: SessionLifecycleServiceBinderImpl
): SessionLifecycleServiceBinder

@Binds
@Singleton
fun crashlyticsSettingsFetcher(impl: RemoteSettingsFetcher): CrashlyticsSettingsFetcher

@Binds
@Singleton
@LocalOverrideSettingsProvider
fun localOverrideSettings(impl: LocalOverrideSettings): SettingsProvider

@Binds
@Singleton
@RemoteSettingsProvider
fun remoteSettings(impl: RemoteSettings): SettingsProvider

companion object {
@Provides @Singleton fun sessionGenerator() = SessionGenerator(timeProvider = WallClock)
private const val TAG = "FirebaseSessions"

@Provides @Singleton fun timeProvider(): TimeProvider = TimeProviderImpl

@Provides @Singleton fun uuidGenerator(): UuidGenerator = UuidGeneratorImpl

@Provides
@Singleton
fun applicationInfo(firebaseApp: FirebaseApp): ApplicationInfo =
SessionEvents.getApplicationInfo(firebaseApp)

@Provides
@Singleton
@SessionConfigsDataStore
fun sessionConfigsDataStore(appContext: Context): DataStore<Preferences> =
PreferenceDataStoreFactory.create(
corruptionHandler =
ReplaceFileCorruptionHandler { ex ->
Log.w(TAG, "CorruptionException in settings DataStore in ${getProcessName()}.", ex)
emptyPreferences()
}
) {
appContext.preferencesDataStoreFile(SessionDataStoreConfigs.SETTINGS_CONFIG_NAME)
}

@Provides
@Singleton
@SessionDetailsDataStore
fun sessionDetailsDataStore(appContext: Context): DataStore<Preferences> =
PreferenceDataStoreFactory.create(
corruptionHandler =
ReplaceFileCorruptionHandler { ex ->
Log.w(TAG, "CorruptionException in sessions DataStore in ${getProcessName()}.", ex)
emptyPreferences()
}
) {
appContext.preferencesDataStoreFile(SessionDataStoreConfigs.SESSIONS_CONFIG_NAME)
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,19 +16,15 @@

package com.google.firebase.sessions

import android.content.Context
import android.util.Log
import androidx.datastore.core.DataStore
import androidx.datastore.core.handlers.ReplaceFileCorruptionHandler
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.preferences.preferencesDataStore
import com.google.firebase.Firebase
import com.google.firebase.annotations.concurrent.Background
import com.google.firebase.app
import com.google.firebase.sessions.ProcessDetailsProvider.getProcessName
import java.io.IOException
import java.util.concurrent.atomic.AtomicReference
import javax.inject.Inject
Expand Down Expand Up @@ -64,8 +60,8 @@ internal interface SessionDatastore {
internal class SessionDatastoreImpl
@Inject
constructor(
private val appContext: Context,
@Background private val backgroundDispatcher: CoroutineContext,
@SessionDetailsDataStore private val dataStore: DataStore<Preferences>,
) : SessionDatastore {

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

private val firebaseSessionDataFlow: Flow<FirebaseSessionsData> =
appContext.dataStore.data
dataStore.data
.catch { exception ->
Log.e(TAG, "Error reading stored session data.", exception)
emit(emptyPreferences())
Expand All @@ -92,7 +88,7 @@ constructor(
override fun updateSessionId(sessionId: String) {
CoroutineScope(backgroundDispatcher).launch {
try {
appContext.dataStore.edit { preferences ->
dataStore.edit { preferences ->
preferences[FirebaseSessionDataKeys.SESSION_ID] = sessionId
}
} catch (e: IOException) {
Expand All @@ -108,15 +104,5 @@ constructor(

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

private val Context.dataStore: DataStore<Preferences> by
preferencesDataStore(
name = SessionDataStoreConfigs.SESSIONS_CONFIG_NAME,
corruptionHandler =
ReplaceFileCorruptionHandler { ex ->
Log.w(TAG, "CorruptionException in sessions DataStore in ${getProcessName()}.", ex)
emptyPreferences()
},
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ package com.google.firebase.sessions
import com.google.errorprone.annotations.CanIgnoreReturnValue
import com.google.firebase.Firebase
import com.google.firebase.app
import java.util.UUID
import javax.inject.Inject
import javax.inject.Singleton

/**
Expand All @@ -37,10 +37,9 @@ internal data class SessionDetails(
* [SessionDetails] up to date with the latest values.
*/
@Singleton
internal class SessionGenerator(
private val timeProvider: TimeProvider,
private val uuidGenerator: () -> UUID = UUID::randomUUID,
) {
internal class SessionGenerator
@Inject
constructor(private val timeProvider: TimeProvider, private val uuidGenerator: UuidGenerator) {
private val firstSessionId = generateSessionId()
private var sessionIndex = -1

Expand All @@ -66,7 +65,7 @@ internal class SessionGenerator(
return currentSession
}

private fun generateSessionId() = uuidGenerator().toString().replace("-", "").lowercase()
private fun generateSessionId() = uuidGenerator.next().toString().replace("-", "").lowercase()

internal companion object {
val instance: SessionGenerator
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,12 @@ import kotlin.time.Duration.Companion.milliseconds
/** Time provider interface, for testing purposes. */
internal interface TimeProvider {
fun elapsedRealtime(): Duration

fun currentTimeUs(): Long
}

/** "Wall clock" time provider. */
internal object WallClock : TimeProvider {
/** "Wall clock" time provider implementation. */
internal object TimeProviderImpl : TimeProvider {
/**
* Gets the [Duration] elapsed in "wall clock" time since device boot.
*
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/*
* Copyright 2025 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.google.firebase.sessions

import java.util.UUID

/** UUID generator interface. */
internal fun interface UuidGenerator {
fun next(): UUID
}

/** Generate random UUIDs using [UUID.randomUUID]. */
internal object UuidGeneratorImpl : UuidGenerator {
override fun next(): UUID = UUID.randomUUID()
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,20 +19,19 @@ package com.google.firebase.sessions.settings
import android.content.Context
import android.content.pm.PackageManager
import android.os.Bundle
import javax.inject.Inject
import javax.inject.Singleton
import kotlin.time.Duration
import kotlin.time.DurationUnit
import kotlin.time.toDuration

internal class LocalOverrideSettings(context: Context) : SettingsProvider {
@Suppress("DEPRECATION") // TODO(mrober): Use ApplicationInfoFlags when target sdk set to 33
@Singleton
internal class LocalOverrideSettings @Inject constructor(appContext: Context) : SettingsProvider {
private val metadata =
context.packageManager
.getApplicationInfo(
context.packageName,
PackageManager.GET_META_DATA,
)
appContext.packageManager
.getApplicationInfo(appContext.packageName, PackageManager.GET_META_DATA)
.metaData
?: Bundle.EMPTY // Default to an empty bundle, meaning no cached values.
?: Bundle.EMPTY // Default to an empty bundle

override val sessionEnabled: Boolean?
get() =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,13 @@ package com.google.firebase.sessions.settings
import android.os.Build
import android.util.Log
import androidx.annotation.VisibleForTesting
import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.Preferences
import com.google.firebase.annotations.concurrent.Background
import com.google.firebase.installations.FirebaseInstallationsApi
import com.google.firebase.sessions.ApplicationInfo
import com.google.firebase.sessions.InstallationId
import dagger.Lazy
import javax.inject.Inject
import javax.inject.Singleton
import kotlin.coroutines.CoroutineContext
import kotlin.time.Duration
import kotlin.time.Duration.Companion.seconds
Expand All @@ -34,14 +36,19 @@ import kotlinx.coroutines.sync.withLock
import org.json.JSONException
import org.json.JSONObject

internal class RemoteSettings(
private val backgroundDispatcher: CoroutineContext,
@Singleton
internal class RemoteSettings
@Inject
constructor(
@Background private val backgroundDispatcher: CoroutineContext,
private val firebaseInstallationsApi: FirebaseInstallationsApi,
private val appInfo: ApplicationInfo,
private val configsFetcher: CrashlyticsSettingsFetcher,
dataStore: DataStore<Preferences>,
private val lazySettingsCache: Lazy<SettingsCache>,
) : SettingsProvider {
private val settingsCache by lazy { SettingsCache(dataStore) }
private val settingsCache: SettingsCache
get() = lazySettingsCache.get()

private val fetchInProgress = Mutex()

override val sessionEnabled: Boolean?
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,13 @@
package com.google.firebase.sessions.settings

import android.net.Uri
import com.google.firebase.annotations.concurrent.Background
import com.google.firebase.sessions.ApplicationInfo
import java.io.BufferedReader
import java.io.InputStreamReader
import java.net.URL
import javax.inject.Inject
import javax.inject.Singleton
import javax.net.ssl.HttpsURLConnection
import kotlin.coroutines.CoroutineContext
import kotlinx.coroutines.withContext
Expand All @@ -30,20 +33,22 @@ internal fun interface CrashlyticsSettingsFetcher {
suspend fun doConfigFetch(
headerOptions: Map<String, String>,
onSuccess: suspend (JSONObject) -> Unit,
onFailure: suspend (msg: String) -> Unit
onFailure: suspend (msg: String) -> Unit,
)
}

internal class RemoteSettingsFetcher(
@Singleton
internal class RemoteSettingsFetcher
@Inject
constructor(
private val appInfo: ApplicationInfo,
private val blockingDispatcher: CoroutineContext,
private val baseUrl: String = FIREBASE_SESSIONS_BASE_URL_STRING,
@Background private val blockingDispatcher: CoroutineContext,
) : CrashlyticsSettingsFetcher {
@Suppress("BlockingMethodInNonBlockingContext") // blockingDispatcher is safe for blocking calls.
override suspend fun doConfigFetch(
headerOptions: Map<String, String>,
onSuccess: suspend (JSONObject) -> Unit,
onFailure: suspend (String) -> Unit
onFailure: suspend (String) -> Unit,
) =
withContext(blockingDispatcher) {
try {
Expand Down Expand Up @@ -78,7 +83,7 @@ internal class RemoteSettingsFetcher(
val uri =
Uri.Builder()
.scheme("https")
.authority(baseUrl)
.authority(FIREBASE_SESSIONS_BASE_URL_STRING)
.appendPath("spi")
.appendPath("v2")
.appendPath("platforms")
Expand Down
Loading
Loading