Skip to content

Commit a220260

Browse files
committed
Implement remote config fetcher to cache values.
1 parent 5402c30 commit a220260

File tree

13 files changed

+530
-57
lines changed

13 files changed

+530
-57
lines changed

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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,14 +35,17 @@ internal constructor(
3535
backgroundDispatcher: CoroutineDispatcher,
3636
transportFactoryProvider: Provider<TransportFactory>,
3737
) {
38-
private val sessionSettings = SessionsSettings(firebaseApp.applicationContext)
38+
private val applicationInfo = SessionEvents.getApplicationInfo(firebaseApp)
39+
private val sessionSettings =
40+
SessionsSettings(firebaseApp.applicationContext, firebaseInstallations, applicationInfo)
3941
private val sessionGenerator = SessionGenerator(collectEvents = shouldCollectEvents())
4042
private val eventGDTLogger = EventGDTLogger(transportFactoryProvider)
4143
private val sessionCoordinator =
4244
SessionCoordinator(firebaseInstallations, backgroundDispatcher, eventGDTLogger)
4345
private val timeProvider: TimeProvider = Time()
4446

4547
init {
48+
sessionSettings.updateSettings()
4649
val sessionInitiator =
4750
SessionInitiator(timeProvider, this::initiateSessionStart, sessionSettings)
4851
val appContext = firebaseApp.applicationContext.applicationContext

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

Lines changed: 105 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,24 @@
1717
package com.google.firebase.sessions.settings
1818

1919
import android.content.Context
20-
import android.net.Uri
2120
import androidx.datastore.preferences.preferencesDataStore
22-
import java.net.URL
21+
import com.google.firebase.installations.FirebaseInstallationsApi
22+
import com.google.firebase.sessions.ApplicationInfo
2323
import kotlin.time.Duration
2424
import kotlin.time.Duration.Companion.seconds
25+
import kotlinx.coroutines.runBlocking
26+
import kotlinx.coroutines.tasks.await
27+
import org.json.JSONException
28+
import org.json.JSONObject
2529

26-
internal class RemoteSettings(val context: Context) : SettingsProvider {
27-
private val Context.dataStore by preferencesDataStore(name = SESSION_CONFIGS_NAME)
30+
internal class RemoteSettings(
31+
val context: Context,
32+
val firebaseInstallationsApi: FirebaseInstallationsApi,
33+
val appInfo: ApplicationInfo,
34+
private val configsFetcher: CrashlyticsSettingsFetcher = RemoteSettingsFetcher(appInfo),
35+
private val dataStoreName: String = SESSION_CONFIGS_NAME
36+
) : SettingsProvider {
37+
private val Context.dataStore by preferencesDataStore(name = dataStoreName)
2838
private val settingsCache = SettingsCache(context.dataStore)
2939

3040
override val sessionEnabled: Boolean?
@@ -54,38 +64,102 @@ internal class RemoteSettings(val context: Context) : SettingsProvider {
5464
return settingsCache.hasCacheExpired()
5565
}
5666

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())
67+
internal fun clearCachedSettings() {
68+
runBlocking { settingsCache.removeConfigs() }
69+
}
70+
71+
private fun fetchConfigs() {
72+
// Check if a fetch is in progress. If yes, return
73+
if (fetchInProgress) {
74+
return
75+
}
76+
77+
// Check if cache is expired. If not, return
78+
if (!settingsCache.hasCacheExpired()) {
79+
return
7880
}
7981

80-
fun fetchConfigs() {
81-
// Check if a fetch is in progress. If yes, return
82-
if (fetchInProgress) {
83-
return
82+
fetchInProgress = true
83+
// Get the installations ID before making a remote config fetch
84+
var installationId = runBlocking {
85+
try {
86+
firebaseInstallationsApi.id.await()
87+
} catch (ex: Exception) {
88+
// TODO(visum) Failed to get installations ID
8489
}
90+
}
91+
92+
if (installationId == null) {
93+
fetchInProgress = false
94+
return
95+
}
96+
97+
val options =
98+
mapOf(
99+
"X-Crashlytics-Installation-ID" to installationId as String,
100+
"X-Crashlytics-Device-Model" to appInfo.deviceModel,
101+
// TODO(visum) Add OS version parameters
102+
// "X-Crashlytics-OS-Build-Version" to "",
103+
// "X-Crashlytics-OS-Display-Version" to "",
104+
"X-Crashlytics-API-Client-Version" to appInfo.sessionSdkVersion
105+
)
106+
runBlocking {
107+
configsFetcher.doConfigFetch(
108+
headerOptions = options,
109+
onSuccess = {
110+
var sessionsEnabled: Boolean? = null
111+
var sessionSamplingRate: Double? = null
112+
var sessionTimeoutSeconds: Int? = null
113+
var cacheDuration: Int? = null
114+
if (it.has("app_quality")) {
115+
val aqsSettings = it.get("app_quality") as JSONObject
116+
try {
117+
if (aqsSettings.has("sessions_enabled")) {
118+
sessionsEnabled = aqsSettings.get("sessions_enabled") as Boolean?
119+
}
85120

86-
// Check if cache is expired. If not, return
87-
// Initiate a fetch. On successful response cache the fetched values
121+
if (aqsSettings.has("sampling_rate")) {
122+
sessionSamplingRate = aqsSettings.get("sampling_rate") as Double?
123+
}
88124

125+
if (aqsSettings.has("session_timeout_seconds")) {
126+
sessionTimeoutSeconds = aqsSettings.get("session_timeout_seconds") as Int?
127+
}
128+
129+
if (aqsSettings.has("cache_duration")) {
130+
cacheDuration = aqsSettings.get("cache_duration") as Int?
131+
}
132+
} catch (exception: JSONException) {
133+
// TODO(visum) Log failure to parse the configs fetched.
134+
}
135+
}
136+
137+
sessionsEnabled?.let { settingsCache.updateSettingsEnabled(sessionsEnabled) }
138+
139+
sessionTimeoutSeconds?.let {
140+
settingsCache.updateSessionRestartTimeout(sessionTimeoutSeconds)
141+
}
142+
143+
sessionSamplingRate?.let { settingsCache.updateSamplingRate(sessionSamplingRate) }
144+
145+
cacheDuration?.let { settingsCache.updateSessionCacheDuration(cacheDuration) }
146+
?: let { settingsCache.updateSessionCacheDuration(86400) }
147+
148+
settingsCache.updateSessionCacheUpdatedTime(System.currentTimeMillis())
149+
fetchInProgress = false
150+
},
151+
onFailure = {
152+
// Network request failed here.
153+
// TODO(visum) Log error in fetching configs
154+
// Logger.logError("[Settings] Fetching newest settings failed with error: \(error)")
155+
fetchInProgress = false
156+
}
157+
)
89158
}
90159
}
160+
161+
companion object {
162+
private const val SESSION_CONFIGS_NAME = "firebase_session_settings"
163+
private var fetchInProgress = false
164+
}
91165
}
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: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@
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
2022
import kotlin.time.Duration
2123
import kotlin.time.Duration.Companion.minutes
2224

@@ -25,10 +27,14 @@ import kotlin.time.Duration.Companion.minutes
2527
*
2628
* @hide
2729
*/
28-
internal class SessionsSettings(val context: Context) {
30+
internal class SessionsSettings(
31+
val context: Context,
32+
val firebaseInstallationsApi: FirebaseInstallationsApi,
33+
val appInfo: ApplicationInfo
34+
) {
2935

3036
private var localOverrideSettings = LocalOverrideSettings(context)
31-
private var remoteSettings = RemoteSettings(context)
37+
private var remoteSettings = RemoteSettings(context, firebaseInstallationsApi, appInfo)
3238

3339
// Order of preference for all the configs below:
3440
// 1. Honor local overrides

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

firebase-sessions/src/test/kotlin/com/google/firebase/sessions/EventGDTLoggerTest.kt

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import com.google.common.truth.Truth.assertThat
2323
import com.google.firebase.FirebaseApp
2424
import com.google.firebase.sessions.settings.SessionsSettings
2525
import com.google.firebase.sessions.testing.FakeFirebaseApp
26+
import com.google.firebase.sessions.testing.FakeFirebaseInstallations
2627
import com.google.firebase.sessions.testing.FakeProvider
2728
import com.google.firebase.sessions.testing.FakeTimeProvider
2829
import com.google.firebase.sessions.testing.FakeTransportFactory
@@ -38,11 +39,16 @@ class EventGDTLoggerTest {
3839
@Test
3940
fun event_logsToGoogleDataTransport() {
4041
val fakeFirebaseApp = FakeFirebaseApp()
42+
val firebaseInstallations = FakeFirebaseInstallations("FaKeFiD")
4143
val sessionEvent =
4244
SessionEvents.startSession(
4345
fakeFirebaseApp.firebaseApp,
4446
TestSessionEventData.TEST_SESSION_DETAILS,
45-
SessionsSettings(fakeFirebaseApp.firebaseApp.applicationContext),
47+
SessionsSettings(
48+
fakeFirebaseApp.firebaseApp.applicationContext,
49+
firebaseInstallations,
50+
SessionEvents.getApplicationInfo(fakeFirebaseApp.firebaseApp)
51+
),
4652
FakeTimeProvider(),
4753
)
4854
val fakeTransportFactory = FakeTransportFactory()

0 commit comments

Comments
 (0)