Skip to content

Commit 4c04731

Browse files
authored
feat: add background notification retry logic with configurable stayalive duration [WPB-22041] (#4391)
1 parent 863132d commit 4c04731

File tree

4 files changed

+98
-7
lines changed

4 files changed

+98
-7
lines changed

app/src/main/kotlin/com/wire/android/notification/WireNotificationManager.kt

Lines changed: 64 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
package com.wire.android.notification
2020

2121
import androidx.annotation.VisibleForTesting
22+
import com.wire.android.BuildConfig
2223
import com.wire.android.R
2324
import com.wire.android.appLogger
2425
import com.wire.android.di.KaliumCoreLogic
@@ -47,6 +48,7 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi
4748
import kotlinx.coroutines.Job
4849
import kotlinx.coroutines.SupervisorJob
4950
import kotlinx.coroutines.cancel
51+
import kotlinx.coroutines.delay
5052
import kotlinx.coroutines.flow.MutableStateFlow
5153
import kotlinx.coroutines.flow.StateFlow
5254
import kotlinx.coroutines.flow.cancellable
@@ -62,10 +64,12 @@ import kotlinx.coroutines.flow.stateIn
6264
import kotlinx.coroutines.launch
6365
import kotlinx.coroutines.sync.Mutex
6466
import kotlinx.coroutines.sync.withLock
67+
import java.net.UnknownHostException
6568
import java.util.concurrent.ConcurrentHashMap
6669
import java.util.concurrent.atomic.AtomicReference
6770
import javax.inject.Inject
6871
import javax.inject.Singleton
72+
import kotlin.time.Duration
6973
import 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
}

app/src/nonfree/kotlin/com/wire/android/services/WireFirebaseMessagingService.kt

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,9 @@
1818

1919
package com.wire.android.services
2020

21+
import androidx.work.Constraints
2122
import androidx.work.ExistingWorkPolicy
23+
import androidx.work.NetworkType
2224
import androidx.work.OneTimeWorkRequestBuilder
2325
import androidx.work.OutOfQuotaPolicy
2426
import androidx.work.WorkManager
@@ -78,11 +80,22 @@ class WireFirebaseMessagingService : FirebaseMessagingService() {
7880
}
7981

8082
private fun enqueueNotificationFetchWorker(userId: String) {
81-
val request = OneTimeWorkRequestBuilder<NotificationFetchWorker>()
83+
val requestBuilder = OneTimeWorkRequestBuilder<NotificationFetchWorker>()
8284
.setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST)
8385
.setInputData(workDataOf(NotificationFetchWorker.USER_ID_INPUT_DATA to userId))
84-
.build()
8586

87+
// Only add network constraints if background notification retry feature is enabled
88+
if (BuildConfig.BACKGROUND_NOTIFICATION_RETRY_ENABLED) {
89+
val constraints = Constraints.Builder()
90+
.setRequiredNetworkType(NetworkType.CONNECTED)
91+
.build()
92+
requestBuilder.setConstraints(constraints)
93+
appLogger.d("$TAG: Enqueued NotificationFetchWorker with network constraints")
94+
} else {
95+
appLogger.d("$TAG: Enqueued NotificationFetchWorker without network constraints")
96+
}
97+
98+
val request = requestBuilder.build()
8699
val workManager = WorkManager.getInstance(applicationContext)
87100

88101
workManager.enqueueUniqueWork(

buildSrc/src/main/kotlin/customization/FeatureConfigs.kt

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,4 +125,16 @@ enum class FeatureConfigs(val value: String, val configType: ConfigType) {
125125
MEETINGS_ENABLED("meetings_enabled", ConfigType.BOOLEAN),
126126

127127
USE_ASYNC_FLUSH_LOGGING("use_async_flush_logging", ConfigType.BOOLEAN),
128+
129+
/**
130+
* Background notification retry logic
131+
* Enables retry with exponential backoff for background notification sync failures
132+
*/
133+
BACKGROUND_NOTIFICATION_RETRY_ENABLED("background_notification_retry_enabled", ConfigType.BOOLEAN),
134+
135+
/**
136+
* Extended stay-alive duration (in seconds) when background notification retry is enabled
137+
* Controls how long the sync connection stays alive after receiving a push notification
138+
*/
139+
BACKGROUND_NOTIFICATION_STAY_ALIVE_SECONDS("background_notification_stay_alive_seconds", ConfigType.INT),
128140
}

default.json

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,10 @@
8181
"analytics_app_key": "8ffae535f1836ed5f58fd5c8a11c00eca07c5438",
8282
"analytics_server_url": "https://wire.count.ly/",
8383
"enable_new_registration": true,
84-
"use_async_flush_logging" : true
84+
"use_strict_mls_filter": false,
85+
"use_async_flush_logging" : true,
86+
"background_notification_retry_enabled": true,
87+
"background_notification_stay_alive_seconds": 5
8588
},
8689
"fdroid": {
8790
"application_id": "com.wire",
@@ -152,5 +155,7 @@
152155
"is_mls_reset_enabled": true,
153156
"use_strict_mls_filter": false,
154157
"meetings_enabled": false,
155-
"emm_support_enabled": true
158+
"emm_support_enabled": true,
159+
"background_notification_retry_enabled": false,
160+
"background_notification_stay_alive_seconds": 1
156161
}

0 commit comments

Comments
 (0)