@@ -23,12 +23,13 @@ import androidx.datastore.core.DataStore
23
23
import androidx.datastore.preferences.core.Preferences
24
24
import com.google.firebase.installations.FirebaseInstallationsApi
25
25
import com.google.firebase.sessions.ApplicationInfo
26
- import java.util.concurrent.atomic.AtomicBoolean
27
26
import kotlin.coroutines.CoroutineContext
28
27
import kotlin.time.Duration
29
28
import kotlin.time.Duration.Companion.seconds
30
29
import kotlinx.coroutines.CoroutineScope
31
30
import kotlinx.coroutines.launch
31
+ import kotlinx.coroutines.sync.Mutex
32
+ import kotlinx.coroutines.sync.withLock
32
33
import kotlinx.coroutines.tasks.await
33
34
import org.json.JSONException
34
35
import org.json.JSONObject
@@ -41,7 +42,7 @@ internal class RemoteSettings(
41
42
dataStore : DataStore <Preferences >,
42
43
) : SettingsProvider {
43
44
private val settingsCache = SettingsCache (dataStore)
44
- private val fetchInProgress = AtomicBoolean ( false )
45
+ private val fetchInProgress = Mutex ( )
45
46
46
47
override val sessionEnabled: Boolean?
47
48
get() = settingsCache.sessionsEnabled()
@@ -52,101 +53,99 @@ internal class RemoteSettings(
52
53
override val samplingRate: Double?
53
54
get() = settingsCache.sessionSamplingRate()
54
55
55
- override fun updateSettings () {
56
- val scope = CoroutineScope (backgroundDispatcher)
57
- scope.launch { fetchConfigs() }
58
- }
59
-
60
- override fun isSettingsStale (): Boolean = settingsCache.hasCacheExpired()
61
-
62
- @VisibleForTesting
63
- internal fun clearCachedSettings () {
64
- val scope = CoroutineScope (backgroundDispatcher)
65
- scope.launch { settingsCache.removeConfigs() }
66
- }
67
-
68
- private suspend fun fetchConfigs () {
69
- // Check if a fetch is in progress. If yes, return
70
- if (fetchInProgress.get()) {
56
+ /* *
57
+ * Fetch remote settings. This should only be called if data collection is enabled.
58
+ *
59
+ * This will exit early if the cache is not expired. Otherwise it will block while fetching, even
60
+ * if called multiple times. This may fetch the FID, so only call if data collection is enabled.
61
+ */
62
+ override suspend fun updateSettings () {
63
+ // Check if cache is expired. If not, return early.
64
+ if (! fetchInProgress.isLocked && ! settingsCache.hasCacheExpired()) {
71
65
return
72
66
}
73
67
74
- // Check if cache is expired. If not, return
75
- if (! settingsCache.hasCacheExpired()) {
76
- return
77
- }
78
-
79
- fetchInProgress.set(true )
80
-
81
- // TODO(mrober): Avoid sending the fid here, and avoid fetching it when data collection is off.
82
- // Get the installations ID before making a remote config fetch
83
- val installationId = firebaseInstallationsApi.id.await()
84
- if (installationId == null ) {
85
- fetchInProgress.set(false )
86
- return
87
- }
88
-
89
- // All the required fields are available, start making a network request.
90
- val options =
91
- mapOf (
92
- " X-Crashlytics-Installation-ID" to installationId,
93
- " X-Crashlytics-Device-Model" to
94
- removeForwardSlashesIn(String .format(" %s/%s" , Build .MANUFACTURER , Build .MODEL )),
95
- " X-Crashlytics-OS-Build-Version" to removeForwardSlashesIn(Build .VERSION .INCREMENTAL ),
96
- " X-Crashlytics-OS-Display-Version" to removeForwardSlashesIn(Build .VERSION .RELEASE ),
97
- " X-Crashlytics-API-Client-Version" to appInfo.sessionSdkVersion
98
- )
68
+ fetchInProgress.withLock {
69
+ // Double check if cache is expired. If not, return.
70
+ if (! settingsCache.hasCacheExpired()) {
71
+ return
72
+ }
99
73
100
- configsFetcher.doConfigFetch(
101
- headerOptions = options,
102
- onSuccess = {
103
- var sessionsEnabled: Boolean? = null
104
- var sessionSamplingRate: Double? = null
105
- var sessionTimeoutSeconds: Int? = null
106
- var cacheDuration: Int? = null
107
- if (it.has(" app_quality" )) {
108
- val aqsSettings = it.get(" app_quality" ) as JSONObject
109
- try {
110
- if (aqsSettings.has(" sessions_enabled" )) {
111
- sessionsEnabled = aqsSettings.get(" sessions_enabled" ) as Boolean?
112
- }
74
+ // Get the installations ID before making a remote config fetch.
75
+ val installationId = firebaseInstallationsApi.id.await()
76
+ if (installationId == null ) {
77
+ Log .w(TAG , " Error getting Firebase Installation ID. Skipping this Session Event." )
78
+ return
79
+ }
113
80
114
- if (aqsSettings.has(" sampling_rate" )) {
115
- sessionSamplingRate = aqsSettings.get(" sampling_rate" ) as Double?
81
+ // All the required fields are available, start making a network request.
82
+ val options =
83
+ mapOf (
84
+ " X-Crashlytics-Installation-ID" to installationId,
85
+ " X-Crashlytics-Device-Model" to
86
+ removeForwardSlashesIn(String .format(" %s/%s" , Build .MANUFACTURER , Build .MODEL )),
87
+ " X-Crashlytics-OS-Build-Version" to removeForwardSlashesIn(Build .VERSION .INCREMENTAL ),
88
+ " X-Crashlytics-OS-Display-Version" to removeForwardSlashesIn(Build .VERSION .RELEASE ),
89
+ " X-Crashlytics-API-Client-Version" to appInfo.sessionSdkVersion
90
+ )
91
+
92
+ configsFetcher.doConfigFetch(
93
+ headerOptions = options,
94
+ onSuccess = {
95
+ var sessionsEnabled: Boolean? = null
96
+ var sessionSamplingRate: Double? = null
97
+ var sessionTimeoutSeconds: Int? = null
98
+ var cacheDuration: Int? = null
99
+ if (it.has(" app_quality" )) {
100
+ val aqsSettings = it.get(" app_quality" ) as JSONObject
101
+ try {
102
+ if (aqsSettings.has(" sessions_enabled" )) {
103
+ sessionsEnabled = aqsSettings.get(" sessions_enabled" ) as Boolean?
104
+ }
105
+
106
+ if (aqsSettings.has(" sampling_rate" )) {
107
+ sessionSamplingRate = aqsSettings.get(" sampling_rate" ) as Double?
108
+ }
109
+
110
+ if (aqsSettings.has(" session_timeout_seconds" )) {
111
+ sessionTimeoutSeconds = aqsSettings.get(" session_timeout_seconds" ) as Int?
112
+ }
113
+
114
+ if (aqsSettings.has(" cache_duration" )) {
115
+ cacheDuration = aqsSettings.get(" cache_duration" ) as Int?
116
+ }
117
+ } catch (exception: JSONException ) {
118
+ Log .e(TAG , " Error parsing the configs remotely fetched: " , exception)
116
119
}
120
+ }
117
121
118
- if (aqsSettings.has(" session_timeout_seconds" )) {
119
- sessionTimeoutSeconds = aqsSettings.get(" session_timeout_seconds" ) as Int?
120
- }
122
+ sessionsEnabled?.let { settingsCache.updateSettingsEnabled(sessionsEnabled) }
121
123
122
- if (aqsSettings.has(" cache_duration" )) {
123
- cacheDuration = aqsSettings.get(" cache_duration" ) as Int?
124
- }
125
- } catch (exception: JSONException ) {
126
- Log .e(TAG , " Error parsing the configs remotely fetched: " , exception)
124
+ sessionTimeoutSeconds?.let {
125
+ settingsCache.updateSessionRestartTimeout(sessionTimeoutSeconds)
127
126
}
128
- }
129
127
130
- sessionsEnabled ?.let { settingsCache.updateSettingsEnabled(sessionsEnabled ) }
128
+ sessionSamplingRate ?.let { settingsCache.updateSamplingRate(sessionSamplingRate ) }
131
129
132
- sessionTimeoutSeconds?.let {
133
- settingsCache.updateSessionRestartTimeout(sessionTimeoutSeconds)
134
- }
130
+ cacheDuration?.let { settingsCache.updateSessionCacheDuration(cacheDuration) }
131
+ ? : let { settingsCache.updateSessionCacheDuration(86400 ) }
135
132
136
- sessionSamplingRate?.let { settingsCache.updateSamplingRate(sessionSamplingRate) }
133
+ settingsCache.updateSessionCacheUpdatedTime(System .currentTimeMillis())
134
+ },
135
+ onFailure = { msg ->
136
+ // Network request failed here.
137
+ Log .e(TAG , " Error failing to fetch the remote configs: $msg " )
138
+ }
139
+ )
140
+ }
141
+ }
137
142
138
- cacheDuration?.let { settingsCache.updateSessionCacheDuration(cacheDuration) }
139
- ? : let { settingsCache.updateSessionCacheDuration(86400 ) }
143
+ override fun isSettingsStale (): Boolean = settingsCache.hasCacheExpired()
140
144
141
- settingsCache.updateSessionCacheUpdatedTime(System .currentTimeMillis())
142
- fetchInProgress.set(false )
143
- },
144
- onFailure = { msg ->
145
- // Network request failed here.
146
- Log .e(TAG , " Error failing to fetch the remote configs: $msg " )
147
- fetchInProgress.set(false )
148
- }
149
- )
145
+ @VisibleForTesting
146
+ internal fun clearCachedSettings () {
147
+ val scope = CoroutineScope (backgroundDispatcher)
148
+ scope.launch { settingsCache.removeConfigs() }
150
149
}
151
150
152
151
private fun removeForwardSlashesIn (s : String ): String {
0 commit comments