mirror of
https://github.com/pezkuwichain/pezkuwi-wallet-android.git
synced 2026-08-01 15:05:41 +00:00
fix(push): retry FCM token requests instead of failing on the first timeout
Enabling notifications started failing with java.util.concurrent.ExecutionException: java.io.IOException: SERVICE_NOT_AVAILABLE SERVICE_NOT_AVAILABLE is Firebase saying its endpoint is momentarily unreachable. It is transient, and Firebase's own guidance is to retry with backoff. requestToken() made a single attempt with no catch, so a few seconds of that ended the flow and put a raw stack trace in front of the user. Nothing about the app or its Firebase project changed — the project is healthy and the API key still authenticates. The app simply had no tolerance for a service that is documented as intermittently unavailable, so it worked until the first time Google was slow to answer. Now four attempts with 1s/2s/4s backoff. The wrapping matters: the IOException arrives inside an ExecutionException, so catching IOException directly would never have matched and the retry would have been dead code. The check walks the cause chain instead. Failures that are not network-related — a wrong google-services.json, a missing Play Services install — are rethrown on the first attempt, since retrying those only delays the same error.
This commit is contained in:
+62
-1
@@ -8,11 +8,13 @@ import io.novafoundation.nova.feature_push_notifications.data.PushNotificationsS
|
||||
import io.novafoundation.nova.feature_push_notifications.di.PushNotificationsFeatureApi
|
||||
import io.novafoundation.nova.feature_push_notifications.di.PushNotificationsFeatureComponent
|
||||
import io.novafoundation.nova.feature_push_notifications.presentation.handling.NotificationHandler
|
||||
import java.io.IOException
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.tasks.await
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
@@ -56,12 +58,71 @@ class NovaFirebaseMessagingService : FirebaseMessagingService(), CoroutineScope
|
||||
|
||||
companion object {
|
||||
|
||||
// Three attempts, 1s + 2s of backoff. The caller wraps this in
|
||||
// withTimeout(SAVING_TIMEOUT = 15s), so the retries plus the attempts
|
||||
// themselves have to fit inside that budget: a fourth attempt would add
|
||||
// 4s of backoff and, if FCM answers slowly rather than failing fast,
|
||||
// could be cut off mid-retry and report a timeout instead of the real
|
||||
// error. Observed failures return in ~130ms, so this leaves ample room.
|
||||
private const val TOKEN_RETRY_ATTEMPTS = 3
|
||||
private const val TOKEN_RETRY_BASE_DELAY_MS = 1_000L
|
||||
|
||||
suspend fun getToken(): String? {
|
||||
return runCatching { FirebaseMessaging.getInstance().token.await() }.getOrNull()
|
||||
}
|
||||
|
||||
/**
|
||||
* FCM token, retrying on the transient failures Firebase expects callers to
|
||||
* handle themselves.
|
||||
*
|
||||
* getToken() surfaces SERVICE_NOT_AVAILABLE when Google's endpoint is
|
||||
* momentarily unreachable — an IOException, not a configuration problem, and
|
||||
* Firebase's own guidance is to retry with backoff. A single attempt meant a
|
||||
* few seconds of that landed in front of the user as
|
||||
* "java.util.concurrent.ExecutionException: java.io.IOException:
|
||||
* SERVICE_NOT_AVAILABLE" and enabling notifications simply failed.
|
||||
*
|
||||
* Retries three times with 1s/2s backoff. Only network failures qualify;
|
||||
* a wrong google-services.json or a missing Play Services install fails
|
||||
* differently and will not start working on the second try, so those are
|
||||
* rethrown on the first attempt.
|
||||
*/
|
||||
suspend fun requestToken(): String {
|
||||
return FirebaseMessaging.getInstance().token.await()
|
||||
var lastError: Exception? = null
|
||||
|
||||
repeat(TOKEN_RETRY_ATTEMPTS) { attempt ->
|
||||
try {
|
||||
return FirebaseMessaging.getInstance().token.await()
|
||||
} catch (e: Exception) {
|
||||
// The IOException arrives wrapped — the message users saw was
|
||||
// "java.util.concurrent.ExecutionException: java.io.IOException:
|
||||
// SERVICE_NOT_AVAILABLE" — so catching IOException alone would
|
||||
// never match and the retry would be dead code.
|
||||
if (!e.isTransientNetworkFailure()) throw e
|
||||
|
||||
lastError = e
|
||||
if (attempt < TOKEN_RETRY_ATTEMPTS - 1) {
|
||||
delay(TOKEN_RETRY_BASE_DELAY_MS shl attempt)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
throw lastError ?: IOException("Failed to obtain FCM token")
|
||||
}
|
||||
|
||||
/**
|
||||
* True when the failure is Firebase being momentarily unreachable, which is
|
||||
* worth another attempt. A misconfigured google-services.json or a missing
|
||||
* Play Services install fails differently and will not start working on a
|
||||
* retry, so those are rethrown immediately.
|
||||
*/
|
||||
private fun Throwable.isTransientNetworkFailure(): Boolean {
|
||||
var cause: Throwable? = this
|
||||
while (cause != null) {
|
||||
if (cause is IOException) return true
|
||||
cause = cause.cause
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
suspend fun deleteToken() {
|
||||
|
||||
Reference in New Issue
Block a user