Skip to content

Commit b2fe881

Browse files
authored
dataconnect: Fixed occasional NullPointerException leading to erroneous UNAUTHENTICATED exceptions (#7001)
1 parent af24598 commit b2fe881

File tree

4 files changed

+183
-35
lines changed

4 files changed

+183
-35
lines changed

firebase-dataconnect/CHANGELOG.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
# Unreleased
2-
2+
* [fixed] Fixed occasional `NullPointerException` when registering with
3+
FirebaseAuth, leading to erroneous UNAUTHENTICATED exceptions.
4+
([#7001](https://github.com/firebase/firebase-android-sdk/pull/7001))
35

46
# 16.0.2
57
* [changed] Improved code robustness related to state management in

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

Lines changed: 77 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -48,13 +48,14 @@ import kotlinx.coroutines.flow.MutableStateFlow
4848
import kotlinx.coroutines.flow.filter
4949
import kotlinx.coroutines.flow.first
5050
import kotlinx.coroutines.flow.getAndUpdate
51+
import kotlinx.coroutines.flow.update
5152
import kotlinx.coroutines.launch
5253

5354
/** Base class that shares logic for managing the Auth token and AppCheck token. */
5455
internal sealed class DataConnectCredentialsTokenManager<T : Any>(
5556
private val deferredProvider: com.google.firebase.inject.Deferred<T>,
5657
parentCoroutineScope: CoroutineScope,
57-
blockingDispatcher: CoroutineDispatcher,
58+
private val blockingDispatcher: CoroutineDispatcher,
5859
protected val logger: Logger,
5960
) {
6061
val instanceId: String
@@ -74,17 +75,23 @@ internal sealed class DataConnectCredentialsTokenManager<T : Any>(
7475
}
7576
)
7677

77-
init {
78-
// Call `whenAvailable()` on a non-main thread because it accesses SharedPreferences, which
79-
// performs disk i/o, violating the StrictMode policy android.os.strictmode.DiskReadViolation.
80-
val coroutineName = CoroutineName("k6rwgqg9gh $instanceId whenAvailable")
81-
coroutineScope.launch(coroutineName + blockingDispatcher) {
82-
deferredProvider.whenAvailable(DeferredProviderHandlerImpl(weakThis))
83-
}
84-
}
85-
8678
private sealed interface State<out T> {
8779

80+
/**
81+
* State indicating that the object has just been created and [initialize] has not yet been
82+
* called.
83+
*/
84+
object New : State<Nothing>
85+
86+
/**
87+
* State indicating that [initialize] has been invoked but the token provider is not (yet?)
88+
* available.
89+
*/
90+
data class Initialized(override val forceTokenRefresh: Boolean) :
91+
StateWithForceTokenRefresh<Nothing> {
92+
constructor() : this(false)
93+
}
94+
8895
/** State indicating that [close] has been invoked. */
8996
object Closed : State<Nothing>
9097

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

96-
/** State indicating that the token provider is not (yet?) available. */
97-
data class New(override val forceTokenRefresh: Boolean) : StateWithForceTokenRefresh<Nothing>
98-
99103
sealed interface StateWithProvider<out T> : State<T> {
100104
/** The token provider, [InternalAuthProvider] or [InteropAppCheckTokenProvider] */
101105
val provider: T
@@ -115,7 +119,7 @@ internal sealed class DataConnectCredentialsTokenManager<T : Any>(
115119
}
116120

117121
/** The current state of this object. */
118-
private val state = MutableStateFlow<State<T>>(State.New(forceTokenRefresh = false))
122+
private val state = MutableStateFlow<State<T>>(State.New)
119123

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

144+
/**
145+
* Initializes this object.
146+
*
147+
* Before calling this method, the _only_ other methods that are allowed to be called on this
148+
* object are [awaitTokenProvider] and [close].
149+
*
150+
* This method may only be called once; subsequent calls result in an exception.
151+
*/
152+
fun initialize() {
153+
logger.debug { "initialize()" }
154+
155+
state.update { currentState ->
156+
when (currentState) {
157+
is State.New -> State.Initialized()
158+
is State.Closed ->
159+
throw IllegalStateException("initialize() cannot be called after close()")
160+
else -> throw IllegalStateException("initialize() has already been called")
161+
}
162+
}
163+
164+
// Call `whenAvailable()` on a non-main thread because it accesses SharedPreferences, which
165+
// performs disk i/o, violating the StrictMode policy android.os.strictmode.DiskReadViolation.
166+
val coroutineName = CoroutineName("k6rwgqg9gh $instanceId whenAvailable")
167+
coroutineScope.launch(coroutineName + blockingDispatcher) {
168+
deferredProvider.whenAvailable(DeferredProviderHandlerImpl(weakThis))
169+
}
170+
}
171+
140172
/**
141173
* Closes this object, releasing its resources, unregistering any registered listeners, and
142174
* cancelling any in-flight token requests.
@@ -155,8 +187,9 @@ internal sealed class DataConnectCredentialsTokenManager<T : Any>(
155187

156188
val oldState = state.getAndUpdate { State.Closed }
157189
when (oldState) {
158-
is State.Closed -> {}
159190
is State.New -> {}
191+
is State.Initialized -> {}
192+
is State.Closed -> {}
160193
is State.StateWithProvider -> {
161194
removeTokenListener(oldState.provider)
162195
}
@@ -166,6 +199,9 @@ internal sealed class DataConnectCredentialsTokenManager<T : Any>(
166199
/**
167200
* Suspends until the token provider becomes available to this object.
168201
*
202+
* This method _may_ be called before [initialize], which is the method that asynchronously gets
203+
* the token provider.
204+
*
169205
* If [close] has been invoked, or is invoked _before_ a token provider becomes available, then
170206
* this method returns normally, as if a token provider _had_ become available.
171207
*/
@@ -177,6 +213,7 @@ internal sealed class DataConnectCredentialsTokenManager<T : Any>(
177213
when (it) {
178214
State.Closed -> true
179215
is State.New -> false
216+
is State.Initialized -> false
180217
is State.Idle -> true
181218
is State.Active -> true
182219
}
@@ -197,25 +234,34 @@ internal sealed class DataConnectCredentialsTokenManager<T : Any>(
197234
val newState =
198235
when (currentState) {
199236
is State.Closed -> State.Closed
200-
is State.New -> currentState.copy(forceTokenRefresh = true)
237+
is State.New -> currentState
238+
is State.Initialized -> currentState.copy(forceTokenRefresh = true)
201239
is State.Idle -> currentState.copy(forceTokenRefresh = true)
202240
is State.Active -> State.Idle(currentState.provider, forceTokenRefresh = true)
203241
}
204242

205-
check(newState is State.Closed || newState is State.StateWithForceTokenRefresh<T>) {
243+
check(
244+
newState is State.New ||
245+
newState is State.Closed ||
246+
newState is State.StateWithForceTokenRefresh<T>
247+
) {
206248
"internal error gbazc7qr66: newState should have been Closed or " +
207249
"StateWithForceTokenRefresh, but got: $newState"
208250
}
209-
check((newState as? State.StateWithForceTokenRefresh<T>)?.forceTokenRefresh !== false) {
210-
"internal error fnzwyrsez2: newState.forceTokenRefresh should have been true"
251+
if (newState is State.StateWithForceTokenRefresh<T>) {
252+
check(newState.forceTokenRefresh) {
253+
"internal error fnzwyrsez2: newState.forceTokenRefresh should have been true"
254+
}
211255
}
212256

213257
newState
214258
}
215259

216260
when (oldState) {
217261
is State.Closed -> {}
218-
is State.New -> {}
262+
is State.New ->
263+
throw IllegalStateException("initialize() must be called before forceRefresh()")
264+
is State.Initialized -> {}
219265
is State.Idle -> {}
220266
is State.Active -> {
221267
val message = "needs token refresh (wgrwbrvjxt)"
@@ -259,14 +305,16 @@ internal sealed class DataConnectCredentialsTokenManager<T : Any>(
259305

260306
val newState: State.Active<T> =
261307
when (oldState) {
308+
is State.New ->
309+
throw IllegalStateException("initialize() must be called before getToken()")
262310
is State.Closed -> {
263311
logger.debug {
264312
"$invocationId getToken() throws CredentialsTokenManagerClosedException" +
265313
" because the DataConnectCredentialsTokenManager instance has been closed"
266314
}
267315
throw CredentialsTokenManagerClosedException(this)
268316
}
269-
is State.New -> {
317+
is State.Initialized -> {
270318
logger.debug {
271319
"$invocationId getToken() returns null (token provider is not (yet?) available)"
272320
}
@@ -353,22 +401,28 @@ internal sealed class DataConnectCredentialsTokenManager<T : Any>(
353401
val oldState =
354402
state.getAndUpdate { currentState ->
355403
when (currentState) {
404+
is State.New -> currentState
356405
is State.Closed -> State.Closed
357-
is State.New -> State.Idle(newProvider, currentState.forceTokenRefresh)
406+
is State.Initialized -> State.Idle(newProvider, currentState.forceTokenRefresh)
358407
is State.Idle -> State.Idle(newProvider, currentState.forceTokenRefresh)
359408
is State.Active -> State.Idle(newProvider, forceTokenRefresh = false)
360409
}
361410
}
362411

363412
when (oldState) {
413+
is State.New ->
414+
throw IllegalStateException(
415+
"internal error sdpzwhmhd3: " +
416+
"initialize() should have been called before onProviderAvailable()"
417+
)
364418
is State.Closed -> {
365419
logger.debug {
366420
"onProviderAvailable(newProvider=$newProvider)" +
367421
" unregistering token listener that was just added"
368422
}
369423
removeTokenListener(newProvider)
370424
}
371-
is State.New -> {}
425+
is State.Initialized -> {}
372426
is State.Idle -> {}
373427
is State.Active -> {
374428
val newProviderClassName = newProvider::class.qualifiedName

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

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -118,23 +118,25 @@ internal class FirebaseDataConnectImpl(
118118

119119
private val dataConnectAuth: DataConnectAuth =
120120
DataConnectAuth(
121-
deferredAuthProvider = deferredAuthProvider,
122-
parentCoroutineScope = coroutineScope,
123-
blockingDispatcher = blockingDispatcher,
124-
logger = Logger("DataConnectAuth").apply { debug { "created by $instanceId" } },
125-
)
121+
deferredAuthProvider = deferredAuthProvider,
122+
parentCoroutineScope = coroutineScope,
123+
blockingDispatcher = blockingDispatcher,
124+
logger = Logger("DataConnectAuth").apply { debug { "created by $instanceId" } },
125+
)
126+
.apply { initialize() }
126127

127128
override suspend fun awaitAuthReady() {
128129
dataConnectAuth.awaitTokenProvider()
129130
}
130131

131132
private val dataConnectAppCheck: DataConnectAppCheck =
132133
DataConnectAppCheck(
133-
deferredAppCheckTokenProvider = deferredAppCheckProvider,
134-
parentCoroutineScope = coroutineScope,
135-
blockingDispatcher = blockingDispatcher,
136-
logger = Logger("DataConnectAppCheck").apply { debug { "created by $instanceId" } },
137-
)
134+
deferredAppCheckTokenProvider = deferredAppCheckProvider,
135+
parentCoroutineScope = coroutineScope,
136+
blockingDispatcher = blockingDispatcher,
137+
logger = Logger("DataConnectAppCheck").apply { debug { "created by $instanceId" } },
138+
)
139+
.apply { initialize() }
138140

139141
override suspend fun awaitAppCheckReady() {
140142
dataConnectAppCheck.awaitTokenProvider()

0 commit comments

Comments
 (0)