Skip to content
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 @@ -4,6 +4,7 @@ import com.google.firebase.remoteconfig.FirebaseRemoteConfig
import com.google.firebase.remoteconfig.FirebaseRemoteConfigValue
import dev.androidbroadcast.featured.ConfigParam
import dev.androidbroadcast.featured.ConfigValue
import dev.androidbroadcast.featured.InitializableConfigValueProvider
import dev.androidbroadcast.featured.RemoteConfigValueProvider
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.tasks.await
Expand All @@ -28,7 +29,8 @@ import kotlin.reflect.KClass
*/
public class FirebaseConfigValueProvider(
private val remoteConfig: FirebaseRemoteConfig = FirebaseRemoteConfig.getInstance(),
) : RemoteConfigValueProvider {
) : RemoteConfigValueProvider,
InitializableConfigValueProvider {
/**
* Mutable registry of type converters used to extract typed values from Firebase.
*
Expand Down Expand Up @@ -81,6 +83,30 @@ public class FirebaseConfigValueProvider(
throw IllegalStateException("No converter registered for type: $type")
}

/**
* Loads the persisted Remote Config data into memory so that values are immediately
* readable via [get] without a network round-trip.
*
* Internally calls [FirebaseRemoteConfig.ensureInitialized], which warms Firebase's
* on-disk caches (activated, fetched, and defaults) into the in-process cache.
* This does NOT perform a network fetch — call [fetch] for that.
* This does NOT activate fetched-but-unactivated config — use [fetch] with `activate = true`
* or call `FirebaseRemoteConfig.activate()` directly when activation is desired.
*
* @throws FetchException if [FirebaseRemoteConfig.ensureInitialized] fails.
* @throws kotlinx.coroutines.CancellationException if the coroutine is cancelled; propagated
* without wrapping.
*/
override suspend fun initialize() {
try {
remoteConfig.ensureInitialized().await()
} catch (e: CancellationException) {
throw e
} catch (e: Exception) {
throw FetchException("Firebase Remote Config initialize failed", e)
}
}

/**
* Fetches the latest values from Firebase Remote Config and optionally activates them.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,14 @@ import dev.androidbroadcast.featured.ConfigValue
import io.mockk.every
import io.mockk.mockk
import io.mockk.verify
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.test.runTest
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test
import kotlin.test.assertFailsWith
import kotlin.test.assertSame

class FirebaseConfigValueProviderTest {
private lateinit var remoteConfig: FirebaseRemoteConfig
Expand Down Expand Up @@ -234,6 +236,47 @@ class FirebaseConfigValueProviderTest {
assertFailsWith<IllegalStateException> { provider.get(param) }
}

// --- initialize() behaviour ---

@Test
fun `initialize calls ensureInitialized exactly once`() =
runTest {
every { remoteConfig.ensureInitialized() } returns Tasks.forResult(mockk(relaxed = true))

provider.initialize()

verify(exactly = 1) { remoteConfig.ensureInitialized() }
}

@Test
fun `initialize does not call fetch or fetchAndActivate`() =
runTest {
every { remoteConfig.ensureInitialized() } returns Tasks.forResult(mockk(relaxed = true))

provider.initialize()

verify(exactly = 0) { remoteConfig.fetch() }
verify(exactly = 0) { remoteConfig.fetchAndActivate() }
}

@Test
fun `initialize wraps task failure in FetchException`() =
runTest {
val cause = RuntimeException("disk read error")
every { remoteConfig.ensureInitialized() } returns Tasks.forException(cause)

val ex = assertFailsWith<FetchException> { provider.initialize() }
assertSame(cause, ex.cause)
}

@Test
fun `initialize rethrows CancellationException without wrapping in FetchException`() =
runTest {
every { remoteConfig.ensureInitialized() } returns Tasks.forException(CancellationException("cancelled"))

assertFailsWith<CancellationException> { provider.initialize() }
}

// --- fetch() behaviour ---

@Test
Expand Down
Loading