Skip to content

Commit 4041333

Browse files
committed
Introduce remote config cache for storing configs fetched remotely.
1 parent 33f6660 commit 4041333

File tree

6 files changed

+465
-19
lines changed

6 files changed

+465
-19
lines changed

firebase-sessions/firebase-sessions.gradle.kts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ firebaseLibrary {
2626

2727
android {
2828
val targetSdkVersion: Int by rootProject
29-
compileSdk = targetSdkVersion
29+
compileSdk = 30
3030
defaultConfig {
3131
minSdk = 16
3232
targetSdk = targetSdkVersion
@@ -48,6 +48,7 @@ dependencies {
4848
implementation("com.google.firebase:firebase-encoders:17.0.0")
4949
implementation("com.google.firebase:firebase-installations-interop:17.1.0")
5050
implementation("com.google.android.datatransport:transport-api:3.0.0")
51+
implementation ("androidx.datastore:datastore-preferences:1.0.0")
5152
implementation(libs.androidx.annotation)
5253

5354
runtimeOnly("com.google.firebase:firebase-installations:17.1.3")

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

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -35,26 +35,32 @@ internal class LocalOverrideSettings(val context: Context) : SettingsProvider {
3535

3636
override val sessionEnabled: Boolean?
3737
get() {
38-
if (metadata != null && metadata.containsKey(sessions_metadata_flag_sessionsEnabled)) {
39-
return metadata.getBoolean(sessions_metadata_flag_sessionsEnabled)
38+
metadata?.let {
39+
if (it.containsKey(sessions_metadata_flag_sessionsEnabled)) {
40+
return it.getBoolean(sessions_metadata_flag_sessionsEnabled)
41+
}
4042
}
4143
return null
4244
}
4345

4446
override val sessionRestartTimeout: Duration?
4547
get() {
46-
if (metadata != null && metadata.containsKey(sessions_metadata_flag_sessionRestartTimeout)) {
47-
val timeoutInSeconds = metadata.getInt(sessions_metadata_flag_sessionRestartTimeout)
48-
val duration = timeoutInSeconds!!.toDuration(DurationUnit.SECONDS)
49-
return duration
48+
metadata?.let {
49+
if (it.containsKey(sessions_metadata_flag_sessionRestartTimeout)) {
50+
val timeoutInSeconds = it.getInt(sessions_metadata_flag_sessionRestartTimeout)
51+
val duration = timeoutInSeconds.toDuration(DurationUnit.SECONDS)
52+
return duration
53+
}
5054
}
5155
return null
5256
}
5357

5458
override val samplingRate: Double?
5559
get() {
56-
if (metadata != null && metadata.containsKey(sessions_metadata_flag_samplingRate)) {
57-
return metadata.getDouble(sessions_metadata_flag_samplingRate)
60+
metadata?.let {
61+
if (it.containsKey(sessions_metadata_flag_samplingRate)) {
62+
return it.getDouble(sessions_metadata_flag_samplingRate)
63+
}
5864
}
5965
return null
6066
}
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
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.content.Context
20+
import android.net.Uri
21+
import androidx.datastore.preferences.preferencesDataStore
22+
import java.net.URL
23+
import kotlin.time.Duration
24+
import kotlin.time.Duration.Companion.seconds
25+
26+
internal class RemoteSettings(val context: Context) : SettingsProvider {
27+
private val SESSION_CONFIGS_NAME = "firebase_session_settings"
28+
private val Context.dataStore by preferencesDataStore(name = SESSION_CONFIGS_NAME)
29+
private val settingsCache = SettingsCache(context.dataStore)
30+
31+
override val sessionEnabled: Boolean?
32+
get() {
33+
return settingsCache.sessionsEnabled()
34+
}
35+
36+
override val sessionRestartTimeout: Duration?
37+
get() {
38+
val durationInSeconds = settingsCache.sessionRestartTimeout()
39+
if (durationInSeconds != null) {
40+
return durationInSeconds.toLong().seconds
41+
}
42+
return null
43+
}
44+
45+
override val samplingRate: Double?
46+
get() {
47+
return settingsCache.sessionSamplingRate()
48+
}
49+
50+
override fun updateSettings() {
51+
fetchConfigs()
52+
}
53+
54+
override fun isSettingsStale(): Boolean {
55+
return settingsCache.hasCacheExpired()
56+
}
57+
58+
companion object SettingsFetcher {
59+
private val FIREBASE_SESSIONS_BASE_URL_STRING = "https://firebase-settings.crashlytics.com"
60+
private val FIREBASE_PLATFORM = "android"
61+
private val fetchInProgress = false
62+
private val settingsUrl: URL = run {
63+
var uri = Uri.Builder()
64+
uri.scheme("https")
65+
uri.authority(FIREBASE_SESSIONS_BASE_URL_STRING)
66+
uri.appendPath("spi/v2/platforms")
67+
uri.appendPath(FIREBASE_PLATFORM)
68+
uri.appendPath("gmp")
69+
// TODO(visum) Replace below with the GMP APPId
70+
uri.appendPath("GMP_APP_ID")
71+
uri.appendPath("settings")
72+
73+
uri.appendQueryParameter("build_version", "")
74+
uri.appendQueryParameter("display_version", "")
75+
76+
URL(uri.build().toString())
77+
}
78+
79+
fun fetchConfigs() {
80+
// Check if a fetch is in progress. If yes, return
81+
if (fetchInProgress) {
82+
return
83+
}
84+
85+
// Check if cache is expired. If not, return
86+
// Initiate a fetch. On successful response cache the fetched values
87+
}
88+
}
89+
}

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

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import kotlin.time.Duration.Companion.minutes
2828
internal class SessionsSettings(val context: Context) {
2929

3030
var localOverrideSettings = LocalOverrideSettings(context)
31+
var remoteSettings = RemoteSettings(context)
3132

3233
// Order of preference for all the configs below:
3334
// 1. Honor local overrides
@@ -37,40 +38,46 @@ internal class SessionsSettings(val context: Context) {
3738
// Setting to qualify if sessions service is enabled.
3839
val sessionsEnabled: Boolean
3940
get() {
40-
if (localOverrideSettings.sessionEnabled != null) {
41-
return localOverrideSettings.sessionEnabled!!
41+
localOverrideSettings.sessionEnabled?.let {
42+
return it
43+
}
44+
remoteSettings.sessionEnabled?.let {
45+
return it
4246
}
43-
4447
// SDK Default
4548
return true
4649
}
4750

4851
// Setting that provides the sessions sampling rate.
4952
val samplingRate: Double
5053
get() {
51-
if (localOverrideSettings.samplingRate != null) {
52-
return localOverrideSettings.samplingRate!!
54+
localOverrideSettings.samplingRate?.let {
55+
return it
56+
}
57+
remoteSettings.samplingRate?.let {
58+
return it
5359
}
54-
5560
// SDK Default
5661
return 1.0
5762
}
5863

5964
// Background timeout config value before which a new session is generated
6065
val sessionRestartTimeout: Duration
6166
get() {
62-
if (localOverrideSettings.sessionRestartTimeout != null) {
63-
return localOverrideSettings.sessionRestartTimeout!!
67+
localOverrideSettings.sessionRestartTimeout?.let {
68+
return it
69+
}
70+
remoteSettings.sessionRestartTimeout?.let {
71+
return it
6472
}
65-
6673
// SDK Default
6774
return 30.minutes
6875
}
6976

7077
// Update the settings for all the settings providers
7178
fun updateSettings() {
7279
// Placeholder to initiate settings update on different sources
73-
// Pending sources: RemoteSettings
7480
localOverrideSettings.updateSettings()
81+
remoteSettings.updateSettings()
7582
}
7683
}
Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
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 androidx.datastore.core.DataStore
20+
import androidx.datastore.preferences.core.Preferences
21+
import androidx.datastore.preferences.core.booleanPreferencesKey
22+
import androidx.datastore.preferences.core.doublePreferencesKey
23+
import androidx.datastore.preferences.core.edit
24+
import androidx.datastore.preferences.core.intPreferencesKey
25+
import androidx.datastore.preferences.core.longPreferencesKey
26+
import kotlinx.coroutines.flow.first
27+
28+
internal data class SessionConfigs(
29+
val sessionEnabled: Boolean?,
30+
val sessionSamplingRate: Double?,
31+
val cacheDuration: Int?,
32+
val sessionRestartTimeout: Int?,
33+
val cacheUpdatedTime: Long?
34+
)
35+
36+
internal class SettingsCache(private val store: DataStore<Preferences>) {
37+
private var sessionConfigs =
38+
SessionConfigs(
39+
sessionEnabled = null,
40+
sessionSamplingRate = null,
41+
sessionRestartTimeout = null,
42+
cacheDuration = null,
43+
cacheUpdatedTime = null
44+
)
45+
46+
private object SettingsCacheKeys {
47+
val SETTINGS_CACHE_SESSIONS_ENABLED = booleanPreferencesKey("firebase_sessions_enabled")
48+
val SETTINGS_CACHE_SAMPLING_RATE = doublePreferencesKey("firebase_sessions_sampling_rate")
49+
val SETTINGS_CACHE_SESSIONS_RESTART_TIMEOUT_SECONDS =
50+
intPreferencesKey("firebase_sessions_restart_timeout")
51+
val SETTINGS_CACHE_SESSIONS_CACHE_DURATION_SECONDS =
52+
intPreferencesKey("firebase_sessions_cache_duration")
53+
val SETTINGS_CACHE_SESSIONS_CACHE_UPDATED_TIME =
54+
longPreferencesKey("firebase_sessions_cache_updated_time")
55+
}
56+
57+
private suspend fun updateSessionConfigs() = mapSessionConfigs(store.data.first().toPreferences())
58+
59+
private fun mapSessionConfigs(settings: Preferences): SessionConfigs {
60+
val sessionEnabled = settings[SettingsCacheKeys.SETTINGS_CACHE_SESSIONS_ENABLED]
61+
val sessionSamplingRate = settings[SettingsCacheKeys.SETTINGS_CACHE_SAMPLING_RATE]
62+
val sessionRestartTimeout =
63+
settings[SettingsCacheKeys.SETTINGS_CACHE_SESSIONS_RESTART_TIMEOUT_SECONDS]
64+
val cacheDuration = settings[SettingsCacheKeys.SETTINGS_CACHE_SESSIONS_CACHE_DURATION_SECONDS]
65+
val cacheUpdatedTime = settings[SettingsCacheKeys.SETTINGS_CACHE_SESSIONS_CACHE_UPDATED_TIME]
66+
67+
sessionConfigs =
68+
SessionConfigs(
69+
sessionEnabled = sessionEnabled,
70+
sessionSamplingRate = sessionSamplingRate,
71+
sessionRestartTimeout = sessionRestartTimeout,
72+
cacheDuration = cacheDuration,
73+
cacheUpdatedTime = cacheUpdatedTime
74+
)
75+
return sessionConfigs
76+
}
77+
78+
internal fun hasCacheExpired(): Boolean {
79+
if (sessionConfigs.cacheUpdatedTime != null) {
80+
val currentTimestamp = System.currentTimeMillis()
81+
val timeDifferenceSeconds = (currentTimestamp - sessionConfigs.cacheUpdatedTime!!) / 1000
82+
if (timeDifferenceSeconds < sessionConfigs.cacheDuration!!) return false
83+
}
84+
return true
85+
}
86+
87+
fun sessionsEnabled(): Boolean? {
88+
return sessionConfigs.sessionEnabled
89+
}
90+
91+
fun sessionSamplingRate(): Double? {
92+
return sessionConfigs.sessionSamplingRate
93+
}
94+
95+
fun sessionRestartTimeout(): Int? {
96+
return sessionConfigs.sessionRestartTimeout
97+
}
98+
99+
suspend fun updateSettingsEnabled(enabled: Boolean?) {
100+
store.edit { preferences ->
101+
enabled?.run { preferences[SettingsCacheKeys.SETTINGS_CACHE_SESSIONS_ENABLED] = enabled }
102+
?: run { preferences.remove(SettingsCacheKeys.SETTINGS_CACHE_SESSIONS_ENABLED) }
103+
}
104+
updateSessionConfigs()
105+
}
106+
107+
suspend fun updateSamplingRate(rate: Double?) {
108+
store.edit { preferences ->
109+
rate?.run { preferences[SettingsCacheKeys.SETTINGS_CACHE_SAMPLING_RATE] = rate }
110+
?: run { preferences.remove(SettingsCacheKeys.SETTINGS_CACHE_SAMPLING_RATE) }
111+
}
112+
updateSessionConfigs()
113+
}
114+
115+
suspend fun updateSessionRestartTimeout(timeoutInSeconds: Int?) {
116+
store.edit { preferences ->
117+
timeoutInSeconds?.run {
118+
preferences[SettingsCacheKeys.SETTINGS_CACHE_SESSIONS_RESTART_TIMEOUT_SECONDS] =
119+
timeoutInSeconds
120+
}
121+
?: run {
122+
preferences.remove(SettingsCacheKeys.SETTINGS_CACHE_SESSIONS_RESTART_TIMEOUT_SECONDS)
123+
}
124+
}
125+
updateSessionConfigs()
126+
}
127+
128+
suspend fun updateSessionCacheDuration(cacheDurationInSeconds: Int?) {
129+
store.edit { preferences ->
130+
cacheDurationInSeconds?.run {
131+
preferences[SettingsCacheKeys.SETTINGS_CACHE_SESSIONS_CACHE_DURATION_SECONDS] =
132+
cacheDurationInSeconds
133+
}
134+
?: run {
135+
preferences.remove(SettingsCacheKeys.SETTINGS_CACHE_SESSIONS_CACHE_DURATION_SECONDS)
136+
}
137+
}
138+
updateSessionConfigs()
139+
}
140+
141+
suspend fun updateSessionCacheUpdatedTime(cacheUpdatedTime: Long?) {
142+
store.edit { preferences ->
143+
cacheUpdatedTime?.run {
144+
preferences[SettingsCacheKeys.SETTINGS_CACHE_SESSIONS_CACHE_UPDATED_TIME] = cacheUpdatedTime
145+
}
146+
?: run { preferences.remove(SettingsCacheKeys.SETTINGS_CACHE_SESSIONS_CACHE_UPDATED_TIME) }
147+
}
148+
updateSessionConfigs()
149+
}
150+
151+
suspend fun removeConfigs() {
152+
val configs = store
153+
store.edit { preferences ->
154+
preferences.clear()
155+
updateSessionConfigs()
156+
}
157+
}
158+
159+
companion object {
160+
private const val TAG = "SessionSettings"
161+
}
162+
}

0 commit comments

Comments
 (0)