diff --git a/providers/firebase/src/main/kotlin/dev/androidbroadcast/featured/firebase/FirebaseConfigValueProvider.kt b/providers/firebase/src/main/kotlin/dev/androidbroadcast/featured/firebase/FirebaseConfigValueProvider.kt index 80582068..9c4d9942 100644 --- a/providers/firebase/src/main/kotlin/dev/androidbroadcast/featured/firebase/FirebaseConfigValueProvider.kt +++ b/providers/firebase/src/main/kotlin/dev/androidbroadcast/featured/firebase/FirebaseConfigValueProvider.kt @@ -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 @@ -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. * @@ -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. * diff --git a/providers/firebase/src/test/kotlin/dev/androidbroadcast/featured/firebase/FirebaseConfigValueProviderTest.kt b/providers/firebase/src/test/kotlin/dev/androidbroadcast/featured/firebase/FirebaseConfigValueProviderTest.kt index 092182d8..214f02e8 100644 --- a/providers/firebase/src/test/kotlin/dev/androidbroadcast/featured/firebase/FirebaseConfigValueProviderTest.kt +++ b/providers/firebase/src/test/kotlin/dev/androidbroadcast/featured/firebase/FirebaseConfigValueProviderTest.kt @@ -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 @@ -234,6 +236,47 @@ class FirebaseConfigValueProviderTest { assertFailsWith { 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 { 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 { provider.initialize() } + } + // --- fetch() behaviour --- @Test