Skip to content

Implement local manifest override configs #4877

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Apr 10, 2023
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 @@ -25,6 +25,7 @@ import com.google.firebase.inject.Provider
import com.google.firebase.installations.FirebaseInstallationsApi
import com.google.firebase.ktx.Firebase
import com.google.firebase.ktx.app
import com.google.firebase.sessions.settings.SessionsSettings
import kotlinx.coroutines.CoroutineDispatcher

class FirebaseSessions
Expand All @@ -38,9 +39,11 @@ internal constructor(
private val eventGDTLogger = EventGDTLogger(transportFactoryProvider)
private val sessionCoordinator =
SessionCoordinator(firebaseInstallations, backgroundDispatcher, eventGDTLogger)
private val sessionSettings = SessionsSettings(firebaseApp.applicationContext)

init {
val sessionInitiator = SessionInitiator(WallClock::elapsedRealtime, this::initiateSessionStart)
val sessionInitiator =
SessionInitiator(WallClock::elapsedRealtime, this::initiateSessionStart, sessionSettings)
val appContext = firebaseApp.applicationContext.applicationContext
if (appContext is Application) {
appContext.registerActivityLifecycleCallbacks(sessionInitiator.activityLifecycleCallbacks)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ package com.google.firebase.sessions
import android.app.Activity
import android.app.Application.ActivityLifecycleCallbacks
import android.os.Bundle
import com.google.firebase.sessions.settings.SessionsSettings
import kotlin.time.Duration

/**
Expand All @@ -30,7 +31,8 @@ import kotlin.time.Duration
*/
internal class SessionInitiator(
private val elapsedRealtime: () -> Duration,
private val initiateSessionStart: () -> Unit
private val initiateSessionStart: () -> Unit,
private val sessionsSettings: SessionsSettings
) {
private var backgroundTime = elapsedRealtime()

Expand All @@ -44,7 +46,7 @@ internal class SessionInitiator(

fun appForegrounded() {
val interval = elapsedRealtime() - backgroundTime
val sessionTimeout = SessionsSettings().sessionRestartTimeout
val sessionTimeout = sessionsSettings.sessionRestartTimeout
if (interval > sessionTimeout) {
initiateSessionStart()
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
/*
* Copyright 2023 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.settings

import android.content.Context
import android.content.pm.PackageManager
import kotlin.time.Duration
import kotlin.time.DurationUnit
import kotlin.time.toDuration

internal class LocalOverrideSettings(val context: Context) : SettingsProvider {

private val sessions_metadata_flag_sessionsEnabled = "firebase_sessions_enabled"
private val sessions_metadata_flag_sessionRestartTimeout =
"firebase_sessions_sessions_restart_timeout"
private val sessions_metadata_flag_samplingRate = "firebase_sessions_sampling_rate"
private val metadata =
context.packageManager
.getApplicationInfo(context.packageName, PackageManager.GET_META_DATA)
.metaData

override val sessionEnabled: Boolean?
get() {
if (metadata != null && metadata.containsKey(sessions_metadata_flag_sessionsEnabled)) {
return metadata.getBoolean(sessions_metadata_flag_sessionsEnabled)
}
return null
}

override val sessionRestartTimeout: Duration?
get() {
if (metadata != null && metadata.containsKey(sessions_metadata_flag_sessionRestartTimeout)) {
val timeoutInSeconds = metadata.getInt(sessions_metadata_flag_sessionRestartTimeout)
val duration = timeoutInSeconds!!.toDuration(DurationUnit.SECONDS)
return duration
}
return null
}

override val samplingRate: Double?
get() {
if (metadata != null && metadata.containsKey(sessions_metadata_flag_samplingRate)) {
return metadata.getDouble(sessions_metadata_flag_samplingRate)
}
return null
}

override fun updateSettings() {
// Nothing to be done here since there is nothing to be updated.
}

override fun isSettingsStale(): Boolean {
// Settings are never stale since all of these are from Manifest file.
return false
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,9 @@
* limitations under the License.
*/

package com.google.firebase.sessions
package com.google.firebase.sessions.settings

import android.content.Context
import kotlin.time.Duration
import kotlin.time.Duration.Companion.minutes

Expand All @@ -24,26 +25,52 @@ import kotlin.time.Duration.Companion.minutes
*
* @hide
*/
internal class SessionsSettings {
internal class SessionsSettings(val context: Context) {

var localOverrideSettings = LocalOverrideSettings(context)

// Order of preference for all the configs below:
// 1. Honor local overrides
// 2. If no local overrides, use remote config
// 3. If no remote config, fall back to SDK defaults.

// Setting to qualify if sessions service is enabled.
val sessionsEnabled: Boolean
get() {
if (localOverrideSettings.sessionEnabled != null) {
return localOverrideSettings.sessionEnabled!!
Copy link
Contributor

@mrober mrober Apr 11, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of this pattern and using !!, Kotlin has nice syntax for dealing with null like:

Elvis if it's just 1 or the other:
get() = localOverrideSettings.sessionEnabled ?: true

There is also this for when we have multiple cases:

localOverrideSettings.sessionEnabled?.let { return it }

someOtherCases?.let { return it }

return true

}

// SDK Default
return true
}

// Setting that provides the sessions sampling rate.
val samplingRate: Double
get() {
if (localOverrideSettings.samplingRate != null) {
return localOverrideSettings.samplingRate!!
}

// SDK Default
return 1.0
}

// Background timeout config value before which a new session is generated
val sessionRestartTimeout: Duration
get() = 30.minutes
get() {
if (localOverrideSettings.sessionRestartTimeout != null) {
return localOverrideSettings.sessionRestartTimeout!!
}

// SDK Default
return 30.minutes
}

// Update the settings for all the settings providers
fun updateSettings() {
// Placeholder to initiate settings update on different sources
// Expected sources: RemoteSettings, ManifestOverrides, SDK Defaults
// Pending sources: RemoteSettings
localOverrideSettings.updateSettings()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/*
* Copyright 2023 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.settings

import kotlin.time.Duration

interface SettingsProvider {
// Setting to control if session collection is enabled
val sessionEnabled: Boolean?

// Setting to represent when to restart a new session after app backgrounding.
val sessionRestartTimeout: Duration?

// Setting denoting the percentage of the sessions data that should be collected
val samplingRate: Double?

// Function to initiate refresh of the settings for the provider
fun updateSettings()

// Function representing if the settings are stale.
fun isSettingsStale(): Boolean
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
/*
* Copyright 2023 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 android.os.Bundle
import com.google.common.truth.Truth.assertThat
import com.google.firebase.FirebaseApp
import com.google.firebase.sessions.settings.LocalOverrideSettings
import com.google.firebase.sessions.testing.FakeFirebaseApp
import kotlin.time.Duration.Companion.minutes
import org.junit.After
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner

@RunWith(RobolectricTestRunner::class)
class LocalOverrideSettingsTest {

@Test
fun localOverrides_returnsNullByDefault() {
val context = FakeFirebaseApp.fakeFirebaseApp().applicationContext

val localSettings = LocalOverrideSettings(context)
assertThat(localSettings.sessionEnabled).isNull()
assertThat(localSettings.sessionRestartTimeout).isNull()
assertThat(localSettings.samplingRate).isNull()
}

@Test
fun localOverrides_validateIfOverrideValuesAreFetchedCorrectly() {
val metadata = Bundle()
metadata.putBoolean("firebase_sessions_enabled", false)
metadata.putDouble("firebase_sessions_sampling_rate", 0.5)
metadata.putInt("firebase_sessions_sessions_restart_timeout", 180)
val context = FakeFirebaseApp.fakeFirebaseApp(metadata).applicationContext

val localSettings = LocalOverrideSettings(context)
assertThat(localSettings.sessionEnabled).isFalse()
assertThat(localSettings.sessionRestartTimeout).isEqualTo(3.minutes)
assertThat(localSettings.samplingRate).isEqualTo(0.5)
}

@Test
fun localOverridesForSomeFields_validateIfOverrideValuesAreFetchedCorrectly() {
val metadata = Bundle()
metadata.putBoolean("firebase_sessions_enabled", false)
metadata.putInt("firebase_sessions_sessions_restart_timeout", 180)
val context = FakeFirebaseApp.fakeFirebaseApp(metadata).applicationContext

val localSettings = LocalOverrideSettings(context)
assertThat(localSettings.sessionEnabled).isFalse()
assertThat(localSettings.sessionRestartTimeout).isEqualTo(3.minutes)
assertThat(localSettings.samplingRate).isNull()
}

@After
fun cleanUp() {
FirebaseApp.clearInstancesForTest()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,17 @@
package com.google.firebase.sessions

import com.google.common.truth.Truth.assertThat
import com.google.firebase.FirebaseApp
import com.google.firebase.sessions.settings.SessionsSettings
import com.google.firebase.sessions.testing.FakeFirebaseApp
import kotlin.time.Duration
import kotlin.time.Duration.Companion.minutes
import org.junit.After
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner

@RunWith(RobolectricTestRunner::class)
class SessionInitiatorTest {
class FakeClock {
var elapsed = Duration.ZERO
Expand All @@ -43,9 +50,11 @@ class SessionInitiatorTest {
@Test
fun coldStart_initiatesSession() {
val sessionStartCounter = SessionStartCounter()
val context = FakeFirebaseApp.fakeFirebaseApp().applicationContext
val settings = SessionsSettings(context)

// Simulate a cold start by simply constructing the SessionInitiator object
SessionInitiator(Duration::ZERO, sessionStartCounter::initiateSessionStart)
SessionInitiator(Duration::ZERO, sessionStartCounter::initiateSessionStart, settings)

assertThat(sessionStartCounter.count).isEqualTo(1)
}
Expand All @@ -54,9 +63,11 @@ class SessionInitiatorTest {
fun appForegrounded_largeInterval_initiatesSession() {
val fakeClock = FakeClock()
val sessionStartCounter = SessionStartCounter()
val context = FakeFirebaseApp.fakeFirebaseApp().applicationContext
val settings = SessionsSettings(context)

val sessionInitiator =
SessionInitiator(fakeClock::elapsed, sessionStartCounter::initiateSessionStart)
SessionInitiator(fakeClock::elapsed, sessionStartCounter::initiateSessionStart, settings)

// First session on cold start
assertThat(sessionStartCounter.count).isEqualTo(1)
Expand All @@ -73,9 +84,11 @@ class SessionInitiatorTest {
fun appForegrounded_smallInterval_doesNotInitiatesSession() {
val fakeClock = FakeClock()
val sessionStartCounter = SessionStartCounter()
val context = FakeFirebaseApp.fakeFirebaseApp().applicationContext
val settings = SessionsSettings(context)

val sessionInitiator =
SessionInitiator(fakeClock::elapsed, sessionStartCounter::initiateSessionStart)
SessionInitiator(fakeClock::elapsed, sessionStartCounter::initiateSessionStart, settings)

// First session on cold start
assertThat(sessionStartCounter.count).isEqualTo(1)
Expand All @@ -92,9 +105,11 @@ class SessionInitiatorTest {
fun appForegrounded_background_foreground_largeIntervals_initiatesSessions() {
val fakeClock = FakeClock()
val sessionStartCounter = SessionStartCounter()
val context = FakeFirebaseApp.fakeFirebaseApp().applicationContext
val settings = SessionsSettings(context)

val sessionInitiator =
SessionInitiator(fakeClock::elapsed, sessionStartCounter::initiateSessionStart)
SessionInitiator(fakeClock::elapsed, sessionStartCounter::initiateSessionStart, settings)

assertThat(sessionStartCounter.count).isEqualTo(1)

Expand All @@ -114,9 +129,11 @@ class SessionInitiatorTest {
fun appForegrounded_background_foreground_smallIntervals_doesNotInitiateNewSessions() {
val fakeClock = FakeClock()
val sessionStartCounter = SessionStartCounter()
val context = FakeFirebaseApp.fakeFirebaseApp().applicationContext
val settings = SessionsSettings(context)

val sessionInitiator =
SessionInitiator(fakeClock::elapsed, sessionStartCounter::initiateSessionStart)
SessionInitiator(fakeClock::elapsed, sessionStartCounter::initiateSessionStart, settings)

// First session on cold start
assertThat(sessionStartCounter.count).isEqualTo(1)
Expand All @@ -135,6 +152,11 @@ class SessionInitiatorTest {
assertThat(sessionStartCounter.count).isEqualTo(1)
}

@After
fun cleanUp() {
FirebaseApp.clearInstancesForTest()
}

companion object {
private val SMALL_INTERVAL = 29.minutes // not enough time to initiate a new session
private val LARGE_INTERVAL = 31.minutes // enough to initiate another session
Expand Down
Loading