Check if network access if blocked when fetching notifications (#6247)
* Add `NetworkMonitor.isNetworkBlocked()`, use it to check if Doze prevented us from loading notifications * Only check if network is blocked after checking if we have a network available, otherwise it's always `true` * Extract `NetworkBlockedChecker` to handle deprecations more carefully
This commit is contained in:
parent
dc11430a73
commit
fe4554703c
5 changed files with 72 additions and 14 deletions
|
|
@ -20,4 +20,9 @@ interface NetworkMonitor {
|
||||||
* A flow containing the current network connectivity status.
|
* A flow containing the current network connectivity status.
|
||||||
*/
|
*/
|
||||||
val connectivity: StateFlow<NetworkStatus>
|
val connectivity: StateFlow<NetworkStatus>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if the active network is being blocked by Doze, even if it's available.
|
||||||
|
*/
|
||||||
|
fun isNetworkBlocked(): Boolean
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -43,6 +43,9 @@ class DefaultNetworkMonitor(
|
||||||
appCoroutineScope: CoroutineScope,
|
appCoroutineScope: CoroutineScope,
|
||||||
) : NetworkMonitor {
|
) : NetworkMonitor {
|
||||||
private val connectivityManager: ConnectivityManager = context.getSystemService(ConnectivityManager::class.java)
|
private val connectivityManager: ConnectivityManager = context.getSystemService(ConnectivityManager::class.java)
|
||||||
|
private val blockedNetworkBlockedChecker = NetworkBlockedChecker(connectivityManager)
|
||||||
|
|
||||||
|
override fun isNetworkBlocked(): Boolean = blockedNetworkBlockedChecker.isNetworkBlocked()
|
||||||
|
|
||||||
override val connectivity: StateFlow<NetworkStatus> = callbackFlow {
|
override val connectivity: StateFlow<NetworkStatus> = callbackFlow {
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,31 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2026 Element Creations Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||||
|
* Please see LICENSE files in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
@file:Suppress("DEPRECATION")
|
||||||
|
|
||||||
|
package io.element.android.features.networkmonitor.impl
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.net.ConnectivityManager
|
||||||
|
import android.net.NetworkInfo
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper to check if the active network in [ConnectivityManager] is blocked.
|
||||||
|
*
|
||||||
|
* This is extracted to its own class because it uses deprecated APIs (but the only ones that are reliable)
|
||||||
|
* and we don't want to suppress deprecations everywhere.
|
||||||
|
*/
|
||||||
|
class NetworkBlockedChecker(
|
||||||
|
private val connectivityManager: ConnectivityManager,
|
||||||
|
) {
|
||||||
|
// The permission is granted by the manifest, false positive
|
||||||
|
@SuppressLint("MissingPermission")
|
||||||
|
fun isNetworkBlocked(): Boolean {
|
||||||
|
// This call is deprecated, but it seems like it's the only reliable way to tell if doze has blocked network access
|
||||||
|
return connectivityManager.activeNetworkInfo?.detailedState == NetworkInfo.DetailedState.BLOCKED
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -12,6 +12,10 @@ import io.element.android.features.networkmonitor.api.NetworkMonitor
|
||||||
import io.element.android.features.networkmonitor.api.NetworkStatus
|
import io.element.android.features.networkmonitor.api.NetworkStatus
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
|
||||||
class FakeNetworkMonitor(initialStatus: NetworkStatus = NetworkStatus.Connected) : NetworkMonitor {
|
class FakeNetworkMonitor(
|
||||||
|
initialStatus: NetworkStatus = NetworkStatus.Connected,
|
||||||
|
private val isNetworkBlockedLambda: () -> Boolean = { false },
|
||||||
|
) : NetworkMonitor {
|
||||||
override val connectivity = MutableStateFlow(initialStatus)
|
override val connectivity = MutableStateFlow(initialStatus)
|
||||||
|
override fun isNetworkBlocked(): Boolean = isNetworkBlockedLambda()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,7 @@ import io.element.android.services.analytics.api.AnalyticsLongRunningTransaction
|
||||||
import io.element.android.services.analytics.api.AnalyticsService
|
import io.element.android.services.analytics.api.AnalyticsService
|
||||||
import io.element.android.services.analytics.api.finishLongRunningTransaction
|
import io.element.android.services.analytics.api.finishLongRunningTransaction
|
||||||
import io.element.android.services.analytics.api.recordTransaction
|
import io.element.android.services.analytics.api.recordTransaction
|
||||||
|
import io.element.android.services.analyticsproviders.api.AnalyticsTransaction
|
||||||
import io.element.android.services.toolbox.api.sdk.BuildVersionSdkIntProvider
|
import io.element.android.services.toolbox.api.sdk.BuildVersionSdkIntProvider
|
||||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||||
import kotlinx.coroutines.flow.first
|
import kotlinx.coroutines.flow.first
|
||||||
|
|
@ -57,28 +58,21 @@ class FetchNotificationsWorker(
|
||||||
override suspend fun doWork(): Result {
|
override suspend fun doWork(): Result {
|
||||||
Timber.d("FetchNotificationsWorker started")
|
Timber.d("FetchNotificationsWorker started")
|
||||||
val requests = workerDataConverter.deserialize(inputData) ?: return Result.failure()
|
val requests = workerDataConverter.deserialize(inputData) ?: return Result.failure()
|
||||||
// Wait for network to be available, but not more than 10 seconds
|
|
||||||
val networkTimeoutSpans = requests.mapNotNull { request ->
|
val networkTimeoutSpans = requests.mapNotNull { request ->
|
||||||
val parent = analyticsService.getLongRunningTransaction(AnalyticsLongRunningTransaction.PushToWorkManager(request.eventId.value))
|
val parent = analyticsService.getLongRunningTransaction(AnalyticsLongRunningTransaction.PushToWorkManager(request.eventId.value))
|
||||||
parent?.startChild("Waiting for network connectivity", "await_network")
|
parent?.startChild("Waiting for network connectivity", "await_network")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Wait for network to be available, but not more than 10 seconds
|
||||||
val hasNetwork = withTimeoutOrNull(10.seconds) {
|
val hasNetwork = withTimeoutOrNull(10.seconds) {
|
||||||
networkMonitor.connectivity.first { it == NetworkStatus.Connected }
|
networkMonitor.connectivity.first { it == NetworkStatus.Connected }
|
||||||
} != null
|
} != null
|
||||||
|
|
||||||
for (span in networkTimeoutSpans) {
|
networkTimeoutSpans.finish()
|
||||||
span.finish()
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!hasNetwork) {
|
// If there is a problem with the updated network values, report it and retry if needed
|
||||||
Timber.w("No network, retrying later")
|
if (reportConnectivityError(requests = requests, hasNetwork = hasNetwork, isNetworkBlocked = networkMonitor.isNetworkBlocked())) {
|
||||||
for (request in requests) {
|
|
||||||
val eventId = request.eventId.value
|
|
||||||
analyticsService.finishLongRunningTransaction(AnalyticsLongRunningTransaction.PushToWorkManager(eventId))
|
|
||||||
val parent = analyticsService.getLongRunningTransaction(AnalyticsLongRunningTransaction.PushToNotification(eventId))
|
|
||||||
// Since we're retrying, start a new transaction
|
|
||||||
analyticsService.startLongRunningTransaction(AnalyticsLongRunningTransaction.PushToWorkManager(eventId), parent)
|
|
||||||
}
|
|
||||||
return Result.retry()
|
return Result.retry()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -157,6 +151,25 @@ class FetchNotificationsWorker(
|
||||||
return Result.success()
|
return Result.success()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun reportConnectivityError(requests: List<NotificationEventRequest>, hasNetwork: Boolean, isNetworkBlocked: Boolean): Boolean {
|
||||||
|
return if (!hasNetwork || isNetworkBlocked) {
|
||||||
|
for (request in requests) {
|
||||||
|
val eventId = request.eventId.value
|
||||||
|
analyticsService.finishLongRunningTransaction(AnalyticsLongRunningTransaction.PushToWorkManager(eventId)) {
|
||||||
|
it.putExtraData("has_network_connection", hasNetwork.toString())
|
||||||
|
it.putExtraData("is_network_blocked", isNetworkBlocked.toString())
|
||||||
|
}
|
||||||
|
val parent = analyticsService.getLongRunningTransaction(AnalyticsLongRunningTransaction.PushToNotification(eventId))
|
||||||
|
// Since we're retrying, start a new transaction
|
||||||
|
analyticsService.startLongRunningTransaction(AnalyticsLongRunningTransaction.PushToWorkManager(eventId), parent)
|
||||||
|
}
|
||||||
|
Timber.w("FetchNotificationsWorker will retry. Has network connectivity: $hasNetwork. Is network blocked: $isNetworkBlocked")
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private suspend fun performOpportunisticSyncIfNeeded(
|
private suspend fun performOpportunisticSyncIfNeeded(
|
||||||
groupedRequests: Map<SessionId, List<NotificationEventRequest>>,
|
groupedRequests: Map<SessionId, List<NotificationEventRequest>>,
|
||||||
) {
|
) {
|
||||||
|
|
@ -174,3 +187,5 @@ class FetchNotificationsWorker(
|
||||||
@AssistedFactory
|
@AssistedFactory
|
||||||
interface Factory : MetroWorkerFactory.WorkerInstanceFactory<FetchNotificationsWorker>
|
interface Factory : MetroWorkerFactory.WorkerInstanceFactory<FetchNotificationsWorker>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun <T : AnalyticsTransaction> Collection<T>.finish() = forEach { it.finish() }
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue