Skip to content

Commit c3b0dcc

Browse files
authored
Implement remote config fetcher to fetch and cache configs. (#4967)
1 parent d4a30e1 commit c3b0dcc

15 files changed

+716
-74
lines changed

firebase-sessions/firebase-sessions.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ dependencies {
5050
implementation("com.google.firebase:firebase-encoders:17.0.0")
5151
implementation("com.google.firebase:firebase-installations-interop:17.1.0")
5252
implementation(libs.androidx.annotation)
53+
testImplementation(project(mapOf("path" to ":integ-testing")))
5354

5455
runtimeOnly("com.google.firebase:firebase-installations:17.1.3")
5556
runtimeOnly(project(":firebase-datatransport"))

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

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,16 +33,26 @@ internal constructor(
3333
private val firebaseApp: FirebaseApp,
3434
firebaseInstallations: FirebaseInstallationsApi,
3535
backgroundDispatcher: CoroutineDispatcher,
36+
blockingDispatcher: CoroutineDispatcher,
3637
transportFactoryProvider: Provider<TransportFactory>,
3738
) {
38-
private val sessionSettings = SessionsSettings(firebaseApp.applicationContext)
39+
private val applicationInfo = SessionEvents.getApplicationInfo(firebaseApp)
40+
private val sessionSettings =
41+
SessionsSettings(
42+
firebaseApp.applicationContext,
43+
blockingDispatcher,
44+
backgroundDispatcher,
45+
firebaseInstallations,
46+
applicationInfo
47+
)
3948
private val sessionGenerator = SessionGenerator(collectEvents = shouldCollectEvents())
4049
private val eventGDTLogger = EventGDTLogger(transportFactoryProvider)
4150
private val sessionCoordinator =
4251
SessionCoordinator(firebaseInstallations, backgroundDispatcher, eventGDTLogger)
4352
private val timeProvider: TimeProvider = Time()
4453

4554
init {
55+
sessionSettings.updateSettings()
4656
val sessionInitiator =
4757
SessionInitiator(timeProvider, this::initiateSessionStart, sessionSettings)
4858
val appContext = firebaseApp.applicationContext.applicationContext

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import androidx.annotation.Keep
1818
import com.google.android.datatransport.TransportFactory
1919
import com.google.firebase.FirebaseApp
2020
import com.google.firebase.annotations.concurrent.Background
21+
import com.google.firebase.annotations.concurrent.Blocking
2122
import com.google.firebase.components.*
2223
import com.google.firebase.components.Qualified.qualified
2324
import com.google.firebase.components.Qualified.unqualified
@@ -45,6 +46,7 @@ internal class FirebaseSessionsRegistrar : ComponentRegistrar {
4546
.add(Dependency.required(firebaseApp))
4647
.add(Dependency.required(firebaseInstallationsApi))
4748
.add(Dependency.required(backgroundDispatcher))
49+
.add(Dependency.required(blockingDispatcher))
4850
.add(Dependency.requiredProvider(transportFactory))
4951
.factory { container ->
5052
// Make sure FirebaseSessionsEarly has started up
@@ -53,6 +55,7 @@ internal class FirebaseSessionsRegistrar : ComponentRegistrar {
5355
container.get(firebaseApp),
5456
container.get(firebaseInstallationsApi),
5557
container.get(backgroundDispatcher),
58+
container.get(blockingDispatcher),
5659
container.getProvider(transportFactory),
5760
)
5861
}
@@ -69,6 +72,8 @@ internal class FirebaseSessionsRegistrar : ComponentRegistrar {
6972
private val firebaseInstallationsApi = unqualified(FirebaseInstallationsApi::class.java)
7073
private val backgroundDispatcher =
7174
qualified(Background::class.java, CoroutineDispatcher::class.java)
75+
private val blockingDispatcher =
76+
qualified(Blocking::class.java, CoroutineDispatcher::class.java)
7277
private val transportFactory = unqualified(TransportFactory::class.java)
7378
}
7479
}

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

Lines changed: 112 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -17,15 +17,34 @@
1717
package com.google.firebase.sessions.settings
1818

1919
import android.content.Context
20-
import android.net.Uri
20+
import android.util.Log
2121
import androidx.datastore.preferences.preferencesDataStore
22-
import java.net.URL
22+
import com.google.firebase.installations.FirebaseInstallationsApi
23+
import com.google.firebase.sessions.ApplicationInfo
24+
import java.util.concurrent.atomic.AtomicBoolean
25+
import kotlin.coroutines.CoroutineContext
2326
import kotlin.time.Duration
2427
import kotlin.time.Duration.Companion.seconds
28+
import kotlinx.coroutines.CoroutineScope
29+
import kotlinx.coroutines.Dispatchers
30+
import kotlinx.coroutines.launch
31+
import kotlinx.coroutines.runBlocking
32+
import kotlinx.coroutines.tasks.await
33+
import org.json.JSONException
34+
import org.json.JSONObject
2535

26-
internal class RemoteSettings(val context: Context) : SettingsProvider {
27-
private val Context.dataStore by preferencesDataStore(name = SESSION_CONFIGS_NAME)
36+
internal class RemoteSettings(
37+
val context: Context,
38+
val blockingDispatcher: CoroutineContext,
39+
val backgroundDispatcher: CoroutineContext,
40+
val firebaseInstallationsApi: FirebaseInstallationsApi,
41+
val appInfo: ApplicationInfo,
42+
private val configsFetcher: CrashlyticsSettingsFetcher = RemoteSettingsFetcher(appInfo),
43+
dataStoreName: String = SESSION_CONFIGS_NAME
44+
) : SettingsProvider {
45+
private val Context.dataStore by preferencesDataStore(name = dataStoreName)
2846
private val settingsCache = SettingsCache(context.dataStore)
47+
private var fetchInProgress = AtomicBoolean(false)
2948

3049
override val sessionEnabled: Boolean?
3150
get() {
@@ -47,45 +66,105 @@ internal class RemoteSettings(val context: Context) : SettingsProvider {
4766
}
4867

4968
override fun updateSettings() {
50-
fetchConfigs()
69+
// TODO: Move to blocking coroutine dispatcher.
70+
runBlocking(Dispatchers.Default) { launch { fetchConfigs() } }
5171
}
5272

5373
override fun isSettingsStale(): Boolean {
5474
return settingsCache.hasCacheExpired()
5575
}
5676

57-
companion object SettingsFetcher {
58-
private const val SESSION_CONFIGS_NAME = "firebase_session_settings"
59-
private const val FIREBASE_SESSIONS_BASE_URL_STRING =
60-
"https://firebase-settings.crashlytics.com"
61-
private const val FIREBASE_PLATFORM = "android"
62-
private const val fetchInProgress = false
63-
private val settingsUrl: URL = run {
64-
var uri =
65-
Uri.Builder()
66-
.scheme("https")
67-
.authority(FIREBASE_SESSIONS_BASE_URL_STRING)
68-
.appendPath("spi/v2/platforms")
69-
.appendPath(FIREBASE_PLATFORM)
70-
.appendPath("gmp")
71-
// TODO(visum) Replace below with the GMP APPId
72-
.appendPath("GMP_APP_ID")
73-
.appendPath("settings")
74-
.appendQueryParameter("build_version", "")
75-
.appendQueryParameter("display_version", "")
76-
77-
URL(uri.build().toString())
77+
internal fun clearCachedSettings() {
78+
val scope = CoroutineScope(backgroundDispatcher)
79+
scope.launch { settingsCache.removeConfigs() }
80+
}
81+
82+
suspend private fun fetchConfigs() {
83+
// Check if a fetch is in progress. If yes, return
84+
if (fetchInProgress.get()) {
85+
return
7886
}
7987

80-
fun fetchConfigs() {
81-
// Check if a fetch is in progress. If yes, return
82-
if (fetchInProgress) {
83-
return
84-
}
88+
// Check if cache is expired. If not, return
89+
if (!settingsCache.hasCacheExpired()) {
90+
return
91+
}
8592

86-
// Check if cache is expired. If not, return
87-
// Initiate a fetch. On successful response cache the fetched values
93+
fetchInProgress.set(true)
8894

95+
// Get the installations ID before making a remote config fetch
96+
var installationId = firebaseInstallationsApi.id.await()
97+
if (installationId == null) {
98+
fetchInProgress.set(false)
99+
return
89100
}
101+
102+
// All the required fields are available, start making a network request.
103+
val options =
104+
mapOf(
105+
"X-Crashlytics-Installation-ID" to installationId as String,
106+
"X-Crashlytics-Device-Model" to appInfo.deviceModel,
107+
// TODO(visum) Add OS version parameters
108+
// "X-Crashlytics-OS-Build-Version" to "",
109+
// "X-Crashlytics-OS-Display-Version" to "",
110+
"X-Crashlytics-API-Client-Version" to appInfo.sessionSdkVersion
111+
)
112+
113+
configsFetcher.doConfigFetch(
114+
headerOptions = options,
115+
onSuccess = {
116+
var sessionsEnabled: Boolean? = null
117+
var sessionSamplingRate: Double? = null
118+
var sessionTimeoutSeconds: Int? = null
119+
var cacheDuration: Int? = null
120+
if (it.has("app_quality")) {
121+
val aqsSettings = it.get("app_quality") as JSONObject
122+
try {
123+
if (aqsSettings.has("sessions_enabled")) {
124+
sessionsEnabled = aqsSettings.get("sessions_enabled") as Boolean?
125+
}
126+
127+
if (aqsSettings.has("sampling_rate")) {
128+
sessionSamplingRate = aqsSettings.get("sampling_rate") as Double?
129+
}
130+
131+
if (aqsSettings.has("session_timeout_seconds")) {
132+
sessionTimeoutSeconds = aqsSettings.get("session_timeout_seconds") as Int?
133+
}
134+
135+
if (aqsSettings.has("cache_duration")) {
136+
cacheDuration = aqsSettings.get("cache_duration") as Int?
137+
}
138+
} catch (exception: JSONException) {
139+
Log.e(TAG, "Error parsing the configs remotely fetched: ", exception)
140+
}
141+
}
142+
143+
val scope = CoroutineScope(backgroundDispatcher)
144+
sessionsEnabled?.let { settingsCache.updateSettingsEnabled(sessionsEnabled) }
145+
146+
sessionTimeoutSeconds?.let {
147+
settingsCache.updateSessionRestartTimeout(sessionTimeoutSeconds)
148+
}
149+
150+
sessionSamplingRate?.let { settingsCache.updateSamplingRate(sessionSamplingRate) }
151+
152+
cacheDuration?.let { settingsCache.updateSessionCacheDuration(cacheDuration) }
153+
?: let { settingsCache.updateSessionCacheDuration(86400) }
154+
155+
settingsCache.updateSessionCacheUpdatedTime(System.currentTimeMillis())
156+
fetchInProgress.set(false)
157+
},
158+
onFailure = {
159+
// Network request failed here.
160+
Log.e(TAG, "Error failing to fetch the remote configs")
161+
fetchInProgress.set(false)
162+
}
163+
)
164+
}
165+
166+
companion object {
167+
private const val SESSION_CONFIGS_NAME = "firebase_session_settings"
168+
private const val TAG = "SessionConfigFetcher"
90169
}
91170
}
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
/*
2+
* Copyright 2023 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.settings
18+
19+
import android.net.Uri
20+
import com.google.firebase.sessions.ApplicationInfo
21+
import java.io.BufferedReader
22+
import java.io.InputStreamReader
23+
import java.net.URL
24+
import javax.net.ssl.HttpsURLConnection
25+
import org.json.JSONObject
26+
27+
internal interface CrashlyticsSettingsFetcher {
28+
suspend fun doConfigFetch(
29+
headerOptions: Map<String, String>,
30+
onSuccess: suspend ((JSONObject)) -> Unit,
31+
onFailure: suspend () -> Unit
32+
)
33+
}
34+
35+
internal class RemoteSettingsFetcher(val appInfo: ApplicationInfo) : CrashlyticsSettingsFetcher {
36+
override suspend fun doConfigFetch(
37+
headerOptions: Map<String, String>,
38+
onSuccess: suspend ((JSONObject)) -> Unit,
39+
onFailure: suspend () -> Unit
40+
) {
41+
val connection = settingsUrl().openConnection() as HttpsURLConnection
42+
connection.requestMethod = "GET"
43+
connection.setRequestProperty("Accept", "application/json")
44+
headerOptions.forEach { connection.setRequestProperty(it.key, it.value) }
45+
46+
val responseCode = connection.responseCode
47+
if (responseCode == HttpsURLConnection.HTTP_OK) {
48+
val inputStream = connection.inputStream
49+
val bufferedReader = BufferedReader(InputStreamReader(inputStream))
50+
val response = StringBuilder()
51+
var inputLine: String?
52+
while (bufferedReader.readLine().also { inputLine = it } != null) {
53+
response.append(inputLine)
54+
}
55+
bufferedReader.close()
56+
inputStream.close()
57+
58+
val responseJson = JSONObject(response.toString())
59+
onSuccess(responseJson)
60+
} else {
61+
onFailure()
62+
}
63+
}
64+
65+
fun settingsUrl(): URL {
66+
var uri =
67+
Uri.Builder()
68+
.scheme("https")
69+
.authority(FIREBASE_SESSIONS_BASE_URL_STRING)
70+
.appendPath("spi")
71+
.appendPath("v2")
72+
.appendPath("platforms")
73+
.appendPath(FIREBASE_PLATFORM)
74+
.appendPath("gmp")
75+
.appendPath(appInfo.appId)
76+
.appendPath("settings")
77+
// TODO(visum) Setup build version and display version
78+
// .appendQueryParameter("build_version", "")
79+
// .appendQueryParameter("display_version", "")
80+
81+
return URL(uri.build().toString())
82+
}
83+
84+
companion object {
85+
private const val FIREBASE_SESSIONS_BASE_URL_STRING = "firebase-settings.crashlytics.com"
86+
private const val FIREBASE_PLATFORM = "android"
87+
}
88+
}

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

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

1919
import android.content.Context
20+
import com.google.firebase.installations.FirebaseInstallationsApi
21+
import com.google.firebase.sessions.ApplicationInfo
22+
import kotlin.coroutines.CoroutineContext
2023
import kotlin.time.Duration
2124
import kotlin.time.Duration.Companion.minutes
2225

@@ -25,11 +28,22 @@ import kotlin.time.Duration.Companion.minutes
2528
*
2629
* @hide
2730
*/
28-
internal class SessionsSettings(val context: Context) {
29-
30-
private var localOverrideSettings = LocalOverrideSettings(context)
31-
private var remoteSettings = RemoteSettings(context)
32-
31+
internal class SessionsSettings(
32+
val context: Context,
33+
val blockingDispatcher: CoroutineContext,
34+
val backgroundDispatcher: CoroutineContext,
35+
val firebaseInstallationsApi: FirebaseInstallationsApi,
36+
val appInfo: ApplicationInfo,
37+
private val localOverrideSettings: LocalOverrideSettings = LocalOverrideSettings(context),
38+
private val remoteSettings: RemoteSettings =
39+
RemoteSettings(
40+
context,
41+
blockingDispatcher,
42+
backgroundDispatcher,
43+
firebaseInstallationsApi,
44+
appInfo
45+
)
46+
) {
3347
// Order of preference for all the configs below:
3448
// 1. Honor local overrides
3549
// 2. If no local overrides, use remote config

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ internal class SettingsCache(private val store: DataStore<Preferences>) {
6969
}
7070

7171
internal fun hasCacheExpired(): Boolean {
72-
if (sessionConfigs.cacheUpdatedTime != null) {
72+
if (sessionConfigs.cacheUpdatedTime != null && sessionConfigs.cacheDuration != null) {
7373
val currentTimestamp = System.currentTimeMillis()
7474
val timeDifferenceSeconds = (currentTimestamp - sessionConfigs.cacheUpdatedTime!!) / 1000
7575
if (timeDifferenceSeconds < sessionConfigs.cacheDuration!!) return false

0 commit comments

Comments
 (0)