Skip to content

dataconnect: Fixed occasional NullPointerException leading to erroneous UNAUTHENTICATED exceptions #7001

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 3 commits into from
May 30, 2025
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
4 changes: 3 additions & 1 deletion firebase-dataconnect/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# Unreleased

* [fixed] Fixed occasional `NullPointerException` when registering with
FirebaseAuth, leading to erroneous UNAUTHENTICATED exceptions.
([#7001](https://github.com/firebase/firebase-android-sdk/pull/7001))

# 16.0.2
* [changed] Improved code robustness related to state management in
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,13 +48,14 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.getAndUpdate
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch

/** Base class that shares logic for managing the Auth token and AppCheck token. */
internal sealed class DataConnectCredentialsTokenManager<T : Any>(
private val deferredProvider: com.google.firebase.inject.Deferred<T>,
parentCoroutineScope: CoroutineScope,
blockingDispatcher: CoroutineDispatcher,
private val blockingDispatcher: CoroutineDispatcher,
protected val logger: Logger,
) {
val instanceId: String
Expand All @@ -74,17 +75,23 @@ internal sealed class DataConnectCredentialsTokenManager<T : Any>(
}
)

init {
// Call `whenAvailable()` on a non-main thread because it accesses SharedPreferences, which
// performs disk i/o, violating the StrictMode policy android.os.strictmode.DiskReadViolation.
val coroutineName = CoroutineName("k6rwgqg9gh $instanceId whenAvailable")
coroutineScope.launch(coroutineName + blockingDispatcher) {
deferredProvider.whenAvailable(DeferredProviderHandlerImpl(weakThis))
}
}

private sealed interface State<out T> {

/**
* State indicating that the object has just been created and [initialize] has not yet been
* called.
*/
object New : State<Nothing>

/**
* State indicating that [initialize] has been invoked but the token provider is not (yet?)
* available.
*/
data class Initialized(override val forceTokenRefresh: Boolean) :
StateWithForceTokenRefresh<Nothing> {
constructor() : this(false)
}

/** State indicating that [close] has been invoked. */
object Closed : State<Nothing>

Expand All @@ -93,9 +100,6 @@ internal sealed class DataConnectCredentialsTokenManager<T : Any>(
val forceTokenRefresh: Boolean
}

/** State indicating that the token provider is not (yet?) available. */
data class New(override val forceTokenRefresh: Boolean) : StateWithForceTokenRefresh<Nothing>

sealed interface StateWithProvider<out T> : State<T> {
/** The token provider, [InternalAuthProvider] or [InteropAppCheckTokenProvider] */
val provider: T
Expand All @@ -115,7 +119,7 @@ internal sealed class DataConnectCredentialsTokenManager<T : Any>(
}

/** The current state of this object. */
private val state = MutableStateFlow<State<T>>(State.New(forceTokenRefresh = false))
private val state = MutableStateFlow<State<T>>(State.New)

/**
* Adds the token listener to the given provider.
Expand All @@ -137,6 +141,34 @@ internal sealed class DataConnectCredentialsTokenManager<T : Any>(
*/
protected abstract suspend fun getToken(provider: T, forceRefresh: Boolean): GetTokenResult

/**
* Initializes this object.
*
* Before calling this method, the _only_ other methods that are allowed to be called on this
* object are [awaitTokenProvider] and [close].
*
* This method may only be called once; subsequent calls result in an exception.
*/
fun initialize() {
logger.debug { "initialize()" }

state.update { currentState ->
when (currentState) {
is State.New -> State.Initialized()
is State.Closed ->
throw IllegalStateException("initialize() cannot be called after close()")
else -> throw IllegalStateException("initialize() has already been called")
}
}

// Call `whenAvailable()` on a non-main thread because it accesses SharedPreferences, which
// performs disk i/o, violating the StrictMode policy android.os.strictmode.DiskReadViolation.
val coroutineName = CoroutineName("k6rwgqg9gh $instanceId whenAvailable")
coroutineScope.launch(coroutineName + blockingDispatcher) {
deferredProvider.whenAvailable(DeferredProviderHandlerImpl(weakThis))
}
}

/**
* Closes this object, releasing its resources, unregistering any registered listeners, and
* cancelling any in-flight token requests.
Expand All @@ -155,8 +187,9 @@ internal sealed class DataConnectCredentialsTokenManager<T : Any>(

val oldState = state.getAndUpdate { State.Closed }
when (oldState) {
is State.Closed -> {}
is State.New -> {}
is State.Initialized -> {}
is State.Closed -> {}
is State.StateWithProvider -> {
removeTokenListener(oldState.provider)
}
Expand All @@ -166,6 +199,9 @@ internal sealed class DataConnectCredentialsTokenManager<T : Any>(
/**
* Suspends until the token provider becomes available to this object.
*
* This method _may_ be called before [initialize], which is the method that asynchronously gets
* the token provider.
*
* If [close] has been invoked, or is invoked _before_ a token provider becomes available, then
* this method returns normally, as if a token provider _had_ become available.
*/
Expand All @@ -177,6 +213,7 @@ internal sealed class DataConnectCredentialsTokenManager<T : Any>(
when (it) {
State.Closed -> true
is State.New -> false
is State.Initialized -> false
is State.Idle -> true
is State.Active -> true
}
Expand All @@ -197,25 +234,34 @@ internal sealed class DataConnectCredentialsTokenManager<T : Any>(
val newState =
when (currentState) {
is State.Closed -> State.Closed
is State.New -> currentState.copy(forceTokenRefresh = true)
is State.New -> currentState
is State.Initialized -> currentState.copy(forceTokenRefresh = true)
is State.Idle -> currentState.copy(forceTokenRefresh = true)
is State.Active -> State.Idle(currentState.provider, forceTokenRefresh = true)
}

check(newState is State.Closed || newState is State.StateWithForceTokenRefresh<T>) {
check(
newState is State.New ||
newState is State.Closed ||
newState is State.StateWithForceTokenRefresh<T>
) {
"internal error gbazc7qr66: newState should have been Closed or " +
"StateWithForceTokenRefresh, but got: $newState"
}
check((newState as? State.StateWithForceTokenRefresh<T>)?.forceTokenRefresh !== false) {
"internal error fnzwyrsez2: newState.forceTokenRefresh should have been true"
if (newState is State.StateWithForceTokenRefresh<T>) {
check(newState.forceTokenRefresh) {
"internal error fnzwyrsez2: newState.forceTokenRefresh should have been true"
}
}

newState
}

when (oldState) {
is State.Closed -> {}
is State.New -> {}
is State.New ->
throw IllegalStateException("initialize() must be called before forceRefresh()")
is State.Initialized -> {}
is State.Idle -> {}
is State.Active -> {
val message = "needs token refresh (wgrwbrvjxt)"
Expand Down Expand Up @@ -259,14 +305,16 @@ internal sealed class DataConnectCredentialsTokenManager<T : Any>(

val newState: State.Active<T> =
when (oldState) {
is State.New ->
throw IllegalStateException("initialize() must be called before getToken()")
is State.Closed -> {
logger.debug {
"$invocationId getToken() throws CredentialsTokenManagerClosedException" +
" because the DataConnectCredentialsTokenManager instance has been closed"
}
throw CredentialsTokenManagerClosedException(this)
}
is State.New -> {
is State.Initialized -> {
logger.debug {
"$invocationId getToken() returns null (token provider is not (yet?) available)"
}
Expand Down Expand Up @@ -353,22 +401,28 @@ internal sealed class DataConnectCredentialsTokenManager<T : Any>(
val oldState =
state.getAndUpdate { currentState ->
when (currentState) {
is State.New -> currentState
is State.Closed -> State.Closed
is State.New -> State.Idle(newProvider, currentState.forceTokenRefresh)
is State.Initialized -> State.Idle(newProvider, currentState.forceTokenRefresh)
is State.Idle -> State.Idle(newProvider, currentState.forceTokenRefresh)
is State.Active -> State.Idle(newProvider, forceTokenRefresh = false)
}
}

when (oldState) {
is State.New ->
throw IllegalStateException(
"internal error sdpzwhmhd3: " +
"initialize() should have been called before onProviderAvailable()"
)
is State.Closed -> {
logger.debug {
"onProviderAvailable(newProvider=$newProvider)" +
" unregistering token listener that was just added"
}
removeTokenListener(newProvider)
}
is State.New -> {}
is State.Initialized -> {}
is State.Idle -> {}
is State.Active -> {
val newProviderClassName = newProvider::class.qualifiedName
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -118,23 +118,25 @@ internal class FirebaseDataConnectImpl(

private val dataConnectAuth: DataConnectAuth =
DataConnectAuth(
deferredAuthProvider = deferredAuthProvider,
parentCoroutineScope = coroutineScope,
blockingDispatcher = blockingDispatcher,
logger = Logger("DataConnectAuth").apply { debug { "created by $instanceId" } },
)
deferredAuthProvider = deferredAuthProvider,
parentCoroutineScope = coroutineScope,
blockingDispatcher = blockingDispatcher,
logger = Logger("DataConnectAuth").apply { debug { "created by $instanceId" } },
)
.apply { initialize() }

override suspend fun awaitAuthReady() {
dataConnectAuth.awaitTokenProvider()
}

private val dataConnectAppCheck: DataConnectAppCheck =
DataConnectAppCheck(
deferredAppCheckTokenProvider = deferredAppCheckProvider,
parentCoroutineScope = coroutineScope,
blockingDispatcher = blockingDispatcher,
logger = Logger("DataConnectAppCheck").apply { debug { "created by $instanceId" } },
)
deferredAppCheckTokenProvider = deferredAppCheckProvider,
parentCoroutineScope = coroutineScope,
blockingDispatcher = blockingDispatcher,
logger = Logger("DataConnectAppCheck").apply { debug { "created by $instanceId" } },
)
.apply { initialize() }

override suspend fun awaitAppCheckReady() {
dataConnectAppCheck.awaitTokenProvider()
Expand Down
Loading
Loading