1919package com.wire.android.notification
2020
2121import androidx.annotation.VisibleForTesting
22+ import com.wire.android.BuildConfig
2223import com.wire.android.R
2324import com.wire.android.appLogger
2425import com.wire.android.di.KaliumCoreLogic
@@ -47,6 +48,7 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi
4748import kotlinx.coroutines.Job
4849import kotlinx.coroutines.SupervisorJob
4950import kotlinx.coroutines.cancel
51+ import kotlinx.coroutines.delay
5052import kotlinx.coroutines.flow.MutableStateFlow
5153import kotlinx.coroutines.flow.StateFlow
5254import kotlinx.coroutines.flow.cancellable
@@ -62,10 +64,12 @@ import kotlinx.coroutines.flow.stateIn
6264import kotlinx.coroutines.launch
6365import kotlinx.coroutines.sync.Mutex
6466import kotlinx.coroutines.sync.withLock
67+ import java.net.UnknownHostException
6568import java.util.concurrent.ConcurrentHashMap
6669import java.util.concurrent.atomic.AtomicReference
6770import javax.inject.Inject
6871import javax.inject.Singleton
72+ import kotlin.time.Duration
6973import kotlin.time.Duration.Companion.seconds
7074
7175@OptIn(ExperimentalCoroutinesApi ::class )
@@ -165,13 +169,70 @@ class WireNotificationManager @Inject constructor(
165169 val observeMessagesJob = observeMessageNotificationsOnceJob(userId)
166170 val observeCallsJob = observeCallNotificationsOnceJob(userId)
167171
168- appLogger.d(" $TAG start syncing" )
169- syncLifecycleManager.syncTemporarily(userId, STAY_ALIVE_TIME_ON_PUSH_DURATION )
172+ val stayAliveDuration = BuildConfig .BACKGROUND_NOTIFICATION_STAY_ALIVE_SECONDS .seconds
173+
174+ if (BuildConfig .BACKGROUND_NOTIFICATION_RETRY_ENABLED ) {
175+ appLogger.d(" $TAG start syncing with retry logic and extended duration (${stayAliveDuration.inWholeSeconds} s)" )
176+ retrySync(userId, stayAliveDuration)
177+ } else {
178+ appLogger.d(" $TAG start syncing without retry logic, default duration (${stayAliveDuration.inWholeSeconds} s)" )
179+ syncLifecycleManager.syncTemporarily(userId, stayAliveDuration)
180+ }
170181
171182 observeMessagesJob?.cancel(" $TAG checked the notifications once, canceling observing." )
172183 observeCallsJob?.cancel(" $TAG checked the calls once, canceling observing." )
173184 }
174185
186+ /* *
187+ * Retries sync operation with exponential backoff for transient network failures.
188+ * This is critical for background notifications during Doze mode where network
189+ * may not be immediately available despite WorkManager constraints.
190+ */
191+ @Suppress(" TooGenericExceptionCaught" )
192+ private suspend fun retrySync (userId : UserId , stayAliveDuration : Duration ) {
193+ val maxRetries = MAX_SYNC_RETRY
194+ var attempt = 0
195+ var lastException: Exception ? = null
196+
197+ while (attempt < maxRetries) {
198+ try {
199+ appLogger.d(" $TAG Sync attempt ${attempt + 1 } /$maxRetries " )
200+ syncLifecycleManager.syncTemporarily(userId, stayAliveDuration)
201+ appLogger.i(" $TAG Sync succeeded on attempt ${attempt + 1 } " )
202+ return // Success, exit retry loop
203+ } catch (e: Exception ) {
204+ lastException = e
205+
206+ // Only retry on network-related errors
207+ val isRetryable = when (e) {
208+ is UnknownHostException -> true
209+ else -> e.cause is UnknownHostException
210+ }
211+
212+ if (! isRetryable) {
213+ appLogger.w(" $TAG Non-retryable error during sync: ${e.message} " )
214+ throw e
215+ }
216+
217+ attempt++
218+ if (attempt < maxRetries) {
219+ // Exponential backoff: 1s, 2s, 4s
220+ val delaySeconds = (1L shl (attempt - 1 ))
221+ appLogger.w(" $TAG Network error on attempt $attempt , retrying in ${delaySeconds} s: ${e.message} " )
222+ delay(delaySeconds.seconds)
223+ } else {
224+ appLogger.e(" $TAG All $maxRetries sync attempts failed with network errors" )
225+ }
226+ }
227+ }
228+
229+ // If we exhausted all retries, log and rethrow
230+ lastException?.let {
231+ appLogger.e(" $TAG Sync failed after $maxRetries attempts: ${it.message} " )
232+ throw it
233+ }
234+ }
235+
175236 private suspend fun observeMessageNotificationsOnceJob (userId : UserId ): Job ? {
176237 val isMessagesAlreadyObserving =
177238 observingWhileRunningJobs.userJobs[userId]?.run { messagesJob.isActive }
@@ -527,6 +588,6 @@ class WireNotificationManager @Inject constructor(
527588
528589 companion object {
529590 private const val TAG = " WireNotificationManager"
530- private val STAY_ALIVE_TIME_ON_PUSH_DURATION = 1 .seconds
591+ private const val MAX_SYNC_RETRY = 3
531592 }
532593}
0 commit comments