Skip to content
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
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
package com.wire.android.notification

import androidx.annotation.VisibleForTesting
import com.wire.android.BuildConfig
import com.wire.android.R
import com.wire.android.appLogger
import com.wire.android.di.KaliumCoreLogic
Expand Down Expand Up @@ -47,6 +48,7 @@
import kotlinx.coroutines.Job
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.cancellable
Expand All @@ -62,10 +64,12 @@
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import java.net.UnknownHostException
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.atomic.AtomicReference
import javax.inject.Inject
import javax.inject.Singleton
import kotlin.time.Duration
import kotlin.time.Duration.Companion.seconds

@OptIn(ExperimentalCoroutinesApi::class)
Expand Down Expand Up @@ -165,13 +169,70 @@
val observeMessagesJob = observeMessageNotificationsOnceJob(userId)
val observeCallsJob = observeCallNotificationsOnceJob(userId)

appLogger.d("$TAG start syncing")
syncLifecycleManager.syncTemporarily(userId, STAY_ALIVE_TIME_ON_PUSH_DURATION)
val stayAliveDuration = BuildConfig.BACKGROUND_NOTIFICATION_STAY_ALIVE_SECONDS.seconds

if (BuildConfig.BACKGROUND_NOTIFICATION_RETRY_ENABLED) {
appLogger.d("$TAG start syncing with retry logic and extended duration (${stayAliveDuration.inWholeSeconds}s)")
retrySync(userId, stayAliveDuration)

Check warning on line 176 in app/src/main/kotlin/com/wire/android/notification/WireNotificationManager.kt

View check run for this annotation

Codecov / codecov/patch

app/src/main/kotlin/com/wire/android/notification/WireNotificationManager.kt#L175-L176

Added lines #L175 - L176 were not covered by tests
} else {
appLogger.d("$TAG start syncing without retry logic, default duration (${stayAliveDuration.inWholeSeconds}s)")
syncLifecycleManager.syncTemporarily(userId, stayAliveDuration)
}

observeMessagesJob?.cancel("$TAG checked the notifications once, canceling observing.")
observeCallsJob?.cancel("$TAG checked the calls once, canceling observing.")
}

/**
* Retries sync operation with exponential backoff for transient network failures.
* This is critical for background notifications during Doze mode where network
* may not be immediately available despite WorkManager constraints.
*/
@Suppress("TooGenericExceptionCaught")
private suspend fun retrySync(userId: UserId, stayAliveDuration: Duration) {
val maxRetries = MAX_SYNC_RETRY
var attempt = 0
var lastException: Exception? = null

Check warning on line 195 in app/src/main/kotlin/com/wire/android/notification/WireNotificationManager.kt

View check run for this annotation

Codecov / codecov/patch

app/src/main/kotlin/com/wire/android/notification/WireNotificationManager.kt#L193-L195

Added lines #L193 - L195 were not covered by tests

while (attempt < maxRetries) {
try {
appLogger.d("$TAG Sync attempt ${attempt + 1}/$maxRetries")
syncLifecycleManager.syncTemporarily(userId, stayAliveDuration)
appLogger.i("$TAG Sync succeeded on attempt ${attempt + 1}")

Check warning on line 201 in app/src/main/kotlin/com/wire/android/notification/WireNotificationManager.kt

View check run for this annotation

Codecov / codecov/patch

app/src/main/kotlin/com/wire/android/notification/WireNotificationManager.kt#L198-L201

Added lines #L198 - L201 were not covered by tests
return // Success, exit retry loop
} catch (e: Exception) {
lastException = e

Check warning on line 204 in app/src/main/kotlin/com/wire/android/notification/WireNotificationManager.kt

View check run for this annotation

Codecov / codecov/patch

app/src/main/kotlin/com/wire/android/notification/WireNotificationManager.kt#L203-L204

Added lines #L203 - L204 were not covered by tests

// Only retry on network-related errors
val isRetryable = when (e) {

Check warning on line 207 in app/src/main/kotlin/com/wire/android/notification/WireNotificationManager.kt

View check run for this annotation

Codecov / codecov/patch

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

Added line #L207 was not covered by tests
is UnknownHostException -> true
else -> e.cause is UnknownHostException

Check warning on line 209 in app/src/main/kotlin/com/wire/android/notification/WireNotificationManager.kt

View check run for this annotation

Codecov / codecov/patch

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

Added line #L209 was not covered by tests
}

if (!isRetryable) {
appLogger.w("$TAG Non-retryable error during sync: ${e.message}")
throw e

Check warning on line 214 in app/src/main/kotlin/com/wire/android/notification/WireNotificationManager.kt

View check run for this annotation

Codecov / codecov/patch

app/src/main/kotlin/com/wire/android/notification/WireNotificationManager.kt#L213-L214

Added lines #L213 - L214 were not covered by tests
}

attempt++

Check warning on line 217 in app/src/main/kotlin/com/wire/android/notification/WireNotificationManager.kt

View check run for this annotation

Codecov / codecov/patch

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

Added line #L217 was not covered by tests
if (attempt < maxRetries) {
// Exponential backoff: 1s, 2s, 4s
val delaySeconds = (1L shl (attempt - 1))
appLogger.w("$TAG Network error on attempt $attempt, retrying in ${delaySeconds}s: ${e.message}")
delay(delaySeconds.seconds)

Check warning on line 222 in app/src/main/kotlin/com/wire/android/notification/WireNotificationManager.kt

View check run for this annotation

Codecov / codecov/patch

app/src/main/kotlin/com/wire/android/notification/WireNotificationManager.kt#L220-L222

Added lines #L220 - L222 were not covered by tests
} else {
appLogger.e("$TAG All $maxRetries sync attempts failed with network errors")

Check warning on line 224 in app/src/main/kotlin/com/wire/android/notification/WireNotificationManager.kt

View check run for this annotation

Codecov / codecov/patch

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

Added line #L224 was not covered by tests
}
}
}

// If we exhausted all retries, log and rethrow
lastException?.let {
appLogger.e("$TAG Sync failed after $maxRetries attempts: ${it.message}")
throw it

Check warning on line 232 in app/src/main/kotlin/com/wire/android/notification/WireNotificationManager.kt

View check run for this annotation

Codecov / codecov/patch

app/src/main/kotlin/com/wire/android/notification/WireNotificationManager.kt#L231-L232

Added lines #L231 - L232 were not covered by tests
}
}

private suspend fun observeMessageNotificationsOnceJob(userId: UserId): Job? {
val isMessagesAlreadyObserving =
observingWhileRunningJobs.userJobs[userId]?.run { messagesJob.isActive }
Expand Down Expand Up @@ -527,6 +588,6 @@

companion object {
private const val TAG = "WireNotificationManager"
private val STAY_ALIVE_TIME_ON_PUSH_DURATION = 1.seconds
private const val MAX_SYNC_RETRY = 3
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@

package com.wire.android.services

import androidx.work.Constraints
import androidx.work.ExistingWorkPolicy
import androidx.work.NetworkType
import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.OutOfQuotaPolicy
import androidx.work.WorkManager
Expand Down Expand Up @@ -78,11 +80,22 @@
}

private fun enqueueNotificationFetchWorker(userId: String) {
val request = OneTimeWorkRequestBuilder<NotificationFetchWorker>()
val requestBuilder = OneTimeWorkRequestBuilder<NotificationFetchWorker>()

Check warning on line 83 in app/src/nonfree/kotlin/com/wire/android/services/WireFirebaseMessagingService.kt

View check run for this annotation

Codecov / codecov/patch

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

Added line #L83 was not covered by tests
.setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST)
.setInputData(workDataOf(NotificationFetchWorker.USER_ID_INPUT_DATA to userId))
.build()

// Only add network constraints if background notification retry feature is enabled
if (BuildConfig.BACKGROUND_NOTIFICATION_RETRY_ENABLED) {
val constraints = Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.build()
requestBuilder.setConstraints(constraints)
appLogger.d("$TAG: Enqueued NotificationFetchWorker with network constraints")

Check warning on line 93 in app/src/nonfree/kotlin/com/wire/android/services/WireFirebaseMessagingService.kt

View check run for this annotation

Codecov / codecov/patch

app/src/nonfree/kotlin/com/wire/android/services/WireFirebaseMessagingService.kt#L89-L93

Added lines #L89 - L93 were not covered by tests
} else {
appLogger.d("$TAG: Enqueued NotificationFetchWorker without network constraints")

Check warning on line 95 in app/src/nonfree/kotlin/com/wire/android/services/WireFirebaseMessagingService.kt

View check run for this annotation

Codecov / codecov/patch

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

Added line #L95 was not covered by tests
}

val request = requestBuilder.build()

Check warning on line 98 in app/src/nonfree/kotlin/com/wire/android/services/WireFirebaseMessagingService.kt

View check run for this annotation

Codecov / codecov/patch

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

Added line #L98 was not covered by tests
val workManager = WorkManager.getInstance(applicationContext)

workManager.enqueueUniqueWork(
Expand Down
12 changes: 12 additions & 0 deletions buildSrc/src/main/kotlin/customization/FeatureConfigs.kt
Original file line number Diff line number Diff line change
Expand Up @@ -125,4 +125,16 @@ enum class FeatureConfigs(val value: String, val configType: ConfigType) {
MEETINGS_ENABLED("meetings_enabled", ConfigType.BOOLEAN),

USE_ASYNC_FLUSH_LOGGING("use_async_flush_logging", ConfigType.BOOLEAN),

/**
* Background notification retry logic
* Enables retry with exponential backoff for background notification sync failures
*/
BACKGROUND_NOTIFICATION_RETRY_ENABLED("background_notification_retry_enabled", ConfigType.BOOLEAN),

/**
* Extended stay-alive duration (in seconds) when background notification retry is enabled
* Controls how long the sync connection stays alive after receiving a push notification
*/
BACKGROUND_NOTIFICATION_STAY_ALIVE_SECONDS("background_notification_stay_alive_seconds", ConfigType.INT),
}
9 changes: 7 additions & 2 deletions default.json
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,10 @@
"analytics_app_key": "8ffae535f1836ed5f58fd5c8a11c00eca07c5438",
"analytics_server_url": "https://wire.count.ly/",
"enable_new_registration": true,
"use_async_flush_logging" : true
"use_strict_mls_filter": false,
"use_async_flush_logging" : true,
"background_notification_retry_enabled": true,
"background_notification_stay_alive_seconds": 5
},
"fdroid": {
"application_id": "com.wire",
Expand Down Expand Up @@ -152,5 +155,7 @@
"is_mls_reset_enabled": true,
"use_strict_mls_filter": false,
"meetings_enabled": false,
"emm_support_enabled": true
"emm_support_enabled": true,
"background_notification_retry_enabled": false,
"background_notification_stay_alive_seconds": 1
}