Skip to content

Commit 8e331cb

Browse files
authored
dataconnect(chore): add token StateFlow to observe token changes (#8273)
1 parent eb643b8 commit 8e331cb

5 files changed

Lines changed: 360 additions & 25 deletions

File tree

firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/core/DataConnectAppCheck.kt

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,8 @@ import com.google.firebase.appcheck.interop.AppCheckTokenListener
2222
import com.google.firebase.appcheck.interop.InteropAppCheckTokenProvider
2323
import com.google.firebase.dataconnect.core.DataConnectAppCheck.GetAppCheckTokenResult
2424
import com.google.firebase.dataconnect.core.Globals.toScrubbedAccessToken
25-
import com.google.firebase.dataconnect.core.LoggerGlobals.debug
2625
import com.google.firebase.dataconnect.util.IdStringGenerator
26+
import java.lang.ref.WeakReference
2727
import kotlinx.coroutines.CoroutineDispatcher
2828
import kotlinx.coroutines.CoroutineScope
2929
import kotlinx.coroutines.tasks.await
@@ -42,7 +42,10 @@ internal class DataConnectAppCheck(
4242
blockingDispatcher = blockingDispatcher,
4343
logger = logger,
4444
) {
45-
private val appCheckTokenListener = AppCheckTokenListenerImpl(logger)
45+
46+
@Suppress("LeakingThis") private val weakThis = WeakReference(this)
47+
48+
private val appCheckTokenListener = AppCheckTokenListenerImpl(weakThis)
4649

4750
@DeferredApi
4851
override fun addTokenListener(provider: InteropAppCheckTokenProvider) =
@@ -54,11 +57,19 @@ internal class DataConnectAppCheck(
5457
override suspend fun getToken(provider: InteropAppCheckTokenProvider, forceRefresh: Boolean) =
5558
provider.getToken(forceRefresh).await().let { GetAppCheckTokenResult(it.token) }
5659

57-
data class GetAppCheckTokenResult(override val token: String?) : GetTokenResult
60+
override fun onClose() {
61+
weakThis.clear()
62+
}
63+
64+
data class GetAppCheckTokenResult(override val token: String?) : GetTokenResult {
65+
override fun toString() = "GetAppCheckTokenResult(token=${token?.toScrubbedAccessToken()})"
66+
}
5867

59-
private class AppCheckTokenListenerImpl(private val logger: Logger) : AppCheckTokenListener {
68+
private class AppCheckTokenListenerImpl(
69+
private val dataConnectAppCheckRef: WeakReference<DataConnectAppCheck>,
70+
) : AppCheckTokenListener {
6071
override fun onAppCheckTokenChanged(tokenResult: AppCheckTokenResult) {
61-
logger.debug { "onAppCheckTokenChanged(token=${tokenResult.token.toScrubbedAccessToken()})" }
72+
dataConnectAppCheckRef.get()?.onTokenChanged(tokenResult.token)
6273
}
6374
}
6475
}

firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/core/DataConnectAuth.kt

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,9 @@ import com.google.firebase.auth.internal.IdTokenListener
2121
import com.google.firebase.auth.internal.InternalAuthProvider
2222
import com.google.firebase.dataconnect.core.DataConnectAuth.GetAuthTokenResult
2323
import com.google.firebase.dataconnect.core.Globals.toScrubbedAccessToken
24-
import com.google.firebase.dataconnect.core.LoggerGlobals.debug
2524
import com.google.firebase.dataconnect.util.IdStringGenerator
2625
import com.google.firebase.internal.InternalTokenResult
26+
import java.lang.ref.WeakReference
2727
import kotlinx.coroutines.CoroutineDispatcher
2828
import kotlinx.coroutines.CoroutineScope
2929
import kotlinx.coroutines.tasks.await
@@ -42,7 +42,10 @@ internal class DataConnectAuth(
4242
blockingDispatcher = blockingDispatcher,
4343
logger = logger,
4444
) {
45-
private val idTokenListener = IdTokenListenerImpl(logger)
45+
46+
@Suppress("LeakingThis") private val weakThis = WeakReference(this)
47+
48+
private val idTokenListener = IdTokenListenerImpl(weakThis)
4649

4750
@DeferredApi
4851
override fun addTokenListener(provider: InternalAuthProvider) =
@@ -56,17 +59,26 @@ internal class DataConnectAuth(
5659
GetAuthTokenResult(it.token, it.getAuthUid())
5760
}
5861

62+
override fun onClose() {
63+
weakThis.clear()
64+
}
65+
5966
@JvmInline
6067
value class AuthUid(val string: String) {
6168
override fun toString() = "AuthUid($string)"
6269
}
6370

6471
data class GetAuthTokenResult(override val token: String?, val authUid: AuthUid?) :
65-
GetTokenResult
72+
GetTokenResult {
73+
override fun toString() =
74+
"GetAuthTokenResult(authUid=$authUid, token=${token?.toScrubbedAccessToken()})"
75+
}
6676

67-
private class IdTokenListenerImpl(private val logger: Logger) : IdTokenListener {
77+
private class IdTokenListenerImpl(
78+
private val dataConnectAuthRef: WeakReference<DataConnectAuth>
79+
) : IdTokenListener {
6880
override fun onIdTokenChanged(tokenResult: InternalTokenResult) {
69-
logger.debug { "onIdTokenChanged(token=${tokenResult.token?.toScrubbedAccessToken()})" }
81+
dataConnectAuthRef.get()?.onTokenChanged(tokenResult.token)
7082
}
7183
}
7284

firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/core/DataConnectCredentialsTokenManager.kt

Lines changed: 54 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,6 @@ import com.google.firebase.inject.Deferred.DeferredHandler
3232
import com.google.firebase.inject.Provider
3333
import com.google.firebase.internal.api.FirebaseNoSignedInUserException
3434
import java.lang.ref.WeakReference
35-
import kotlin.coroutines.coroutineContext
3635
import kotlinx.coroutines.CancellationException
3736
import kotlinx.coroutines.CoroutineDispatcher
3837
import kotlinx.coroutines.CoroutineName
@@ -41,8 +40,11 @@ import kotlinx.coroutines.CoroutineStart
4140
import kotlinx.coroutines.Deferred
4241
import kotlinx.coroutines.async
4342
import kotlinx.coroutines.cancel
43+
import kotlinx.coroutines.currentCoroutineContext
4444
import kotlinx.coroutines.ensureActive
4545
import kotlinx.coroutines.flow.MutableStateFlow
46+
import kotlinx.coroutines.flow.StateFlow
47+
import kotlinx.coroutines.flow.asStateFlow
4648
import kotlinx.coroutines.flow.filter
4749
import kotlinx.coroutines.flow.first
4850
import kotlinx.coroutines.flow.getAndUpdate
@@ -110,6 +112,17 @@ internal sealed class DataConnectCredentialsTokenManager<T : Any, R : GetTokenRe
110112
/** The current state of this object. */
111113
private val state = MutableStateFlow<State<T, R>>(State.New)
112114

115+
private val _token = MutableStateFlow<R?>(null)
116+
117+
/**
118+
* The last token returned from [getToken]
119+
*
120+
* Returns null if [getToken] has never been called or never completed successfully.
121+
*
122+
* After [close] the value of this flow is _not_ cleared, and will remain unchanged indefinitely.
123+
*/
124+
val token: StateFlow<R?> = _token.asStateFlow()
125+
113126
/**
114127
* Adds the token listener to the given provider.
115128
*
@@ -130,6 +143,9 @@ internal sealed class DataConnectCredentialsTokenManager<T : Any, R : GetTokenRe
130143
*/
131144
protected abstract suspend fun getToken(provider: T, forceRefresh: Boolean): R
132145

146+
/** Invoked synchronously by [close]. */
147+
protected abstract fun onClose()
148+
133149
/**
134150
* Initializes this object.
135151
*
@@ -173,6 +189,7 @@ internal sealed class DataConnectCredentialsTokenManager<T : Any, R : GetTokenRe
173189

174190
weakThis.clear()
175191
coroutineScope.cancel()
192+
onClose()
176193

177194
val oldState = state.getAndUpdate { State.Closed }
178195
when (oldState) {
@@ -339,7 +356,7 @@ internal sealed class DataConnectCredentialsTokenManager<T : Any, R : GetTokenRe
339356

340357
// Ensure that any exception checking below is due to an exception that happened in the
341358
// coroutine that called getToken(), not from the calling coroutine being cancelled.
342-
coroutineContext.ensureActive()
359+
currentCoroutineContext().ensureActive()
343360

344361
val sequencedResult = jobResult.getOrNull()
345362
if (sequencedResult !== null && sequencedResult.sequenceNumber < attemptSequenceNumber) {
@@ -357,6 +374,7 @@ internal sealed class DataConnectCredentialsTokenManager<T : Any, R : GetTokenRe
357374
logger.debug {
358375
"$invocationId getToken() returns null (FirebaseAuth reports no signed-in user)"
359376
}
377+
_token.value = null
360378
return null
361379
} else if (exception is CancellationException) {
362380
logger.warn(exception) {
@@ -371,17 +389,16 @@ internal sealed class DataConnectCredentialsTokenManager<T : Any, R : GetTokenRe
371389
}
372390

373391
val tokenResult: R = sequencedResult!!.ref.getOrThrow()
374-
logger.debug {
375-
"$invocationId getToken() returns retrieved token: " +
376-
tokenResult.token?.toScrubbedAccessToken()
377-
}
392+
logger.debug { "$invocationId getToken() returns $tokenResult" }
393+
_token.value = tokenResult
378394
return tokenResult
379395
}
380396
}
381397

382398
private sealed class GetTokenRetry(message: String) : Exception(message)
383399
private class ForceRefresh(message: String) : GetTokenRetry(message)
384400
private class NewProvider(message: String) : GetTokenRetry(message)
401+
private class NewToken(message: String) : GetTokenRetry(message)
385402

386403
@DeferredApi
387404
private fun onProviderAvailable(newProvider: T) {
@@ -422,6 +439,37 @@ internal sealed class DataConnectCredentialsTokenManager<T : Any, R : GetTokenRe
422439
}
423440
}
424441

442+
protected fun onTokenChanged(newToken: String?) {
443+
if (token.value?.token == newToken) {
444+
return
445+
}
446+
447+
val invocationId = idStringGenerator.next("otc")
448+
logger.debug { "$invocationId onTokenChanged(newToken=${newToken?.toScrubbedAccessToken()})" }
449+
450+
while (true) {
451+
val currentState = state.value
452+
453+
val activeState =
454+
when (currentState) {
455+
State.New -> return
456+
is State.Initialized -> break
457+
is State.Idle -> break
458+
is State.Active -> currentState
459+
State.Closed -> return
460+
}
461+
462+
val newState = State.Idle(activeState.provider, forceTokenRefresh = false)
463+
if (state.compareAndSet(currentState, newState)) {
464+
val message = "$invocationId a new token is available (j567n2577q)"
465+
activeState.job.cancel(message, NewToken(message))
466+
break
467+
}
468+
}
469+
470+
coroutineScope.launch(CoroutineName(invocationId)) { getToken(invocationId) }
471+
}
472+
425473
/**
426474
* An implementation of [DeferredHandler] to be registered with the [Deferred] given to the
427475
* constructor.

0 commit comments

Comments
 (0)