Add network constraints for fetching notifications with WorkManager (#6305)

* Add `isNetworkBlocked` and `isInAirGappedEnvironment` to `NetworkMonitor`.

* Improve the DI of `SyncPendingNotificationsRequestBuilder` to simplify its usage.

* Only update `isInAirGappedEnvironment` in `DefaultNetworkManager` if the current build is an enterprise one.

* Add network constraints to `DefaultSyncPendingNotificationsRequestBuilder` based on the air-gapped status.

* Add a feature flag to disable the new check, in case it doesn't work as expected.
This commit is contained in:
Jorge Martin Espinosa 2026-03-10 13:44:31 +01:00 committed by GitHub
parent de7f2990ae
commit f77098ed47
15 changed files with 298 additions and 100 deletions

View file

@ -30,7 +30,6 @@ import io.element.android.libraries.workmanager.api.WorkManagerRequestType
import io.element.android.libraries.workmanager.api.WorkManagerScheduler
import io.element.android.services.analytics.api.AnalyticsLongRunningTransaction
import io.element.android.services.analytics.api.AnalyticsService
import io.element.android.services.toolbox.api.sdk.BuildVersionSdkIntProvider
import io.element.android.services.toolbox.api.systemclock.SystemClock
import kotlinx.coroutines.flow.first
import timber.log.Timber
@ -49,7 +48,7 @@ class DefaultPushHandler(
private val analyticsService: AnalyticsService,
private val systemClock: SystemClock,
private val workManagerScheduler: WorkManagerScheduler,
private val buildVersionSdkIntProvider: BuildVersionSdkIntProvider,
private val syncPendingNotificationsRequestFactory: SyncPendingNotificationsRequestBuilder.Factory,
resultProcessor: NotificationResultProcessor,
) : PushHandler {
init {
@ -134,12 +133,7 @@ class DefaultPushHandler(
if (!workManagerScheduler.hasPendingWork(userId, WorkManagerRequestType.NOTIFICATION_SYNC)) {
Timber.d("No pending worker for push notifications found")
workManagerScheduler.submit(
SyncPendingNotificationsRequestBuilder(
sessionId = userId,
buildVersionSdkIntProvider = buildVersionSdkIntProvider,
)
)
workManagerScheduler.submit(syncPendingNotificationsRequestFactory.create(userId))
}
} catch (e: Exception) {
Timber.tag(loggerTag.value).e(e, "## handleInternal() failed")

View file

@ -160,7 +160,8 @@ class FetchPendingNotificationsWorker(
networkTimeoutSpans.finish()
// If there is a problem with the updated network values, report it and retry if needed
if (reportConnectivityError(requests = requests, hasNetwork = hasNetwork, isNetworkBlocked = networkMonitor.isNetworkBlocked())) {
val isNetworkBlocked = networkMonitor.isNetworkBlocked.first()
if (reportConnectivityError(requests = requests, hasNetwork = hasNetwork, isNetworkBlocked = isNetworkBlocked)) {
pushHistoryService.insertOrUpdatePushRequests(requests.map { request ->
request.copy(retries = request.retries + 1)
})

View file

@ -8,32 +8,87 @@
package io.element.android.libraries.push.impl.workmanager
import android.net.NetworkCapabilities
import android.net.NetworkRequest
import android.os.Build
import androidx.work.Constraints
import androidx.work.ExistingWorkPolicy
import androidx.work.NetworkType
import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.OutOfQuotaPolicy
import androidx.work.workDataOf
import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.AssistedFactory
import dev.zacsweers.metro.AssistedInject
import dev.zacsweers.metro.ContributesBinding
import io.element.android.features.networkmonitor.api.NetworkMonitor
import io.element.android.libraries.featureflag.api.FeatureFlagService
import io.element.android.libraries.featureflag.api.FeatureFlags
import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.push.impl.workmanager.SyncPendingNotificationsRequestBuilder.Companion.SESSION_ID
import io.element.android.libraries.workmanager.api.WorkManagerRequestBuilder
import io.element.android.libraries.workmanager.api.WorkManagerRequestType
import io.element.android.libraries.workmanager.api.WorkManagerRequestWrapper
import io.element.android.libraries.workmanager.api.WorkManagerWorkerType
import io.element.android.libraries.workmanager.api.workManagerTag
import io.element.android.services.toolbox.api.sdk.BuildVersionSdkIntProvider
import kotlinx.coroutines.flow.first
import timber.log.Timber
interface SyncPendingNotificationsRequestBuilder : WorkManagerRequestBuilder {
fun interface Factory {
fun create(sessionId: SessionId): SyncPendingNotificationsRequestBuilder
}
class SyncPendingNotificationsRequestBuilder(
private val sessionId: SessionId,
private val buildVersionSdkIntProvider: BuildVersionSdkIntProvider,
) : WorkManagerRequestBuilder {
companion object {
const val SESSION_ID = "session_id"
}
}
@AssistedInject
class DefaultSyncPendingNotificationsRequestBuilder(
@Assisted private val sessionId: SessionId,
private val buildVersionSdkIntProvider: BuildVersionSdkIntProvider,
private val networkMonitor: NetworkMonitor,
private val featureFlagService: FeatureFlagService,
) : SyncPendingNotificationsRequestBuilder {
@AssistedFactory
@ContributesBinding(AppScope::class)
interface Factory : SyncPendingNotificationsRequestBuilder.Factory {
override fun create(sessionId: SessionId): DefaultSyncPendingNotificationsRequestBuilder
}
override suspend fun build(): Result<List<WorkManagerRequestWrapper>> {
val type = WorkManagerWorkerType.Unique(
name = workManagerTag(sessionId = sessionId, requestType = WorkManagerRequestType.NOTIFICATION_SYNC),
policy = ExistingWorkPolicy.APPEND_OR_REPLACE,
)
val networkRequestBuilder = NetworkRequest.Builder()
// Allow any kind of network that can have internet connectivity.
.addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR)
.addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
.addTransportType(NetworkCapabilities.TRANSPORT_VPN)
.addTransportType(NetworkCapabilities.TRANSPORT_ETHERNET)
// By default, the network request will require the device to not be in VPN, but since some customers use a VPN to connect to their homeserver,
// we need to allow VPN networks.
.removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN)
// If we're in an air-gapped environment, we shouldn't validate internet connectivity, as the checker will fail and the worker won't run at all.
// Note this will always be false for FOSS, since the feature is only enabled in Element Pro.
if (networkMonitor.isInAirGappedEnvironment.first()) {
Timber.d("In an air-gapped environment, not adding NET_CAPABILITY_VALIDATED to the network request")
networkRequestBuilder.removeCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED)
} else if (featureFlagService.isFeatureEnabled(FeatureFlags.ValidateNetworkWhenSchedulingNotificationFetching)) {
Timber.d("Not in an air-gapped environment, adding NET_CAPABILITY_VALIDATED to the network request")
networkRequestBuilder.addCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED)
}
val networkConstraints = Constraints.Builder()
.setRequiredNetworkRequest(networkRequestBuilder.build(), NetworkType.NOT_REQUIRED)
.build()
val request = OneTimeWorkRequestBuilder<FetchPendingNotificationsWorker>()
.setInputData(workDataOf(SESSION_ID to sessionId.value))
.apply {
@ -44,8 +99,10 @@ class SyncPendingNotificationsRequestBuilder(
setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST)
}
}
.setConstraints(networkConstraints)
.setTraceTag(workManagerTag(sessionId, WorkManagerRequestType.NOTIFICATION_SYNC))
.build()
return Result.success(listOf(WorkManagerRequestWrapper(request, type)))
}
}