From 53a10544459052b5472e490c62ce0fa17f7822e2 Mon Sep 17 00:00:00 2001 From: Stefano Date: Thu, 23 Oct 2025 17:25:10 +0200 Subject: [PATCH 01/11] AndroidConnectionStatusProvider cache is now updated in the background, so lock is not acquired indefinitely --- .../util/AndroidConnectionStatusProvider.java | 127 +++++++++--------- 1 file changed, 66 insertions(+), 61 deletions(-) diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/internal/util/AndroidConnectionStatusProvider.java b/sentry-android-core/src/main/java/io/sentry/android/core/internal/util/AndroidConnectionStatusProvider.java index 3f05beeceb..956df4b1b8 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/internal/util/AndroidConnectionStatusProvider.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/internal/util/AndroidConnectionStatusProvider.java @@ -268,7 +268,16 @@ private void updateCacheAndNotifyObservers( // Only notify observers if something meaningful changed if (shouldUpdate) { - updateCache(networkCapabilities); + cachedNetworkCapabilities = networkCapabilities; + lastCacheUpdateTime = timeProvider.getCurrentTimeMillis(); + options + .getLogger() + .log( + SentryLevel.DEBUG, + "Cache updated - Status: " + + getConnectionStatusFromCache() + + ", Type: " + + getConnectionTypeFromCache()); final @NotNull ConnectionStatus status = getConnectionStatusFromCache(); try (final @NotNull ISentryLifecycleToken ignored = lock.acquire()) { @@ -349,59 +358,54 @@ private boolean hasSignificantTransportChanges( } @SuppressLint({"NewApi", "MissingPermission"}) - private void updateCache(@Nullable NetworkCapabilities networkCapabilities) { + private void updateCache() { try (final @NotNull ISentryLifecycleToken ignored = lock.acquire()) { - try { - if (networkCapabilities != null) { - cachedNetworkCapabilities = networkCapabilities; - } else { - if (!Permissions.hasPermission(context, Manifest.permission.ACCESS_NETWORK_STATE)) { - options - .getLogger() - .log( - SentryLevel.INFO, - "No permission (ACCESS_NETWORK_STATE) to check network status."); - cachedNetworkCapabilities = null; - lastCacheUpdateTime = timeProvider.getCurrentTimeMillis(); - return; - } - - if (buildInfoProvider.getSdkInfoVersion() < Build.VERSION_CODES.M) { - cachedNetworkCapabilities = null; - lastCacheUpdateTime = timeProvider.getCurrentTimeMillis(); - return; - } - - // Fallback: query current active network - final ConnectivityManager connectivityManager = - getConnectivityManager(context, options.getLogger()); - if (connectivityManager != null) { - final Network activeNetwork = connectivityManager.getActiveNetwork(); - - cachedNetworkCapabilities = - activeNetwork != null - ? connectivityManager.getNetworkCapabilities(activeNetwork) - : null; - } else { - cachedNetworkCapabilities = - null; // Clear cached capabilities if connectivity manager is null - } - } - lastCacheUpdateTime = timeProvider.getCurrentTimeMillis(); - + cachedNetworkCapabilities = null; + lastCacheUpdateTime = timeProvider.getCurrentTimeMillis(); + } + try { + if (!Permissions.hasPermission(context, Manifest.permission.ACCESS_NETWORK_STATE)) { options .getLogger() - .log( - SentryLevel.DEBUG, - "Cache updated - Status: " - + getConnectionStatusFromCache() - + ", Type: " - + getConnectionTypeFromCache()); - } catch (Throwable t) { - options.getLogger().log(SentryLevel.WARNING, "Failed to update connection status cache", t); - cachedNetworkCapabilities = null; - lastCacheUpdateTime = timeProvider.getCurrentTimeMillis(); + .log(SentryLevel.INFO, "No permission (ACCESS_NETWORK_STATE) to check network status."); + return; } + + if (buildInfoProvider.getSdkInfoVersion() < Build.VERSION_CODES.M) { + return; + } + + // Fallback: query current active network in the background + submitSafe( + () -> { + final ConnectivityManager connectivityManager = + getConnectivityManager(context, options.getLogger()); + if (connectivityManager != null) { + final @Nullable NetworkCapabilities capabilities = + getNetworkCapabilities(connectivityManager); + + try (final @NotNull ISentryLifecycleToken ignored2 = lock.acquire()) { + cachedNetworkCapabilities = capabilities; + lastCacheUpdateTime = timeProvider.getCurrentTimeMillis(); + + if (capabilities != null) { + options + .getLogger() + .log( + SentryLevel.DEBUG, + "Cache updated - Status: " + + getConnectionStatusFromCache() + + ", Type: " + + getConnectionTypeFromCache()); + } + } + } + }); + + } catch (Throwable t) { + options.getLogger().log(SentryLevel.WARNING, "Failed to update connection status cache", t); + cachedNetworkCapabilities = null; + lastCacheUpdateTime = timeProvider.getCurrentTimeMillis(); } } @@ -412,7 +416,7 @@ private boolean isCacheValid() { @Override public @NotNull ConnectionStatus getConnectionStatus() { if (!isCacheValid()) { - updateCache(null); + updateCache(); } return getConnectionStatusFromCache(); } @@ -420,7 +424,7 @@ private boolean isCacheValid() { @Override public @Nullable String getConnectionType() { if (!isCacheValid()) { - updateCache(null); + updateCache(); } return getConnectionTypeFromCache(); } @@ -490,7 +494,7 @@ public void onForeground() { () -> { // proactively update cache and notify observers on foreground to ensure connectivity // state is not stale - updateCache(null); + updateCache(); final @NotNull ConnectionStatus status = getConnectionStatusFromCache(); if (status == ConnectionStatus.DISCONNECTED) { @@ -575,6 +579,14 @@ public NetworkCapabilities getCachedNetworkCapabilities() { } } + @RequiresApi(Build.VERSION_CODES.M) + @SuppressLint("MissingPermission") + private static @Nullable NetworkCapabilities getNetworkCapabilities( + final @NotNull ConnectivityManager connectivityManager) { + final Network activeNetwork = connectivityManager.getActiveNetwork(); + return activeNetwork != null ? connectivityManager.getNetworkCapabilities(activeNetwork) : null; + } + /** * Check the connection type of the active network * @@ -603,14 +615,7 @@ public NetworkCapabilities getCachedNetworkCapabilities() { boolean cellular = false; if (buildInfoProvider.getSdkInfoVersion() >= Build.VERSION_CODES.M) { - - final Network activeNetwork = connectivityManager.getActiveNetwork(); - if (activeNetwork == null) { - logger.log(SentryLevel.INFO, "Network is null and cannot check network status"); - return null; - } - final NetworkCapabilities networkCapabilities = - connectivityManager.getNetworkCapabilities(activeNetwork); + final NetworkCapabilities networkCapabilities = getNetworkCapabilities(connectivityManager); if (networkCapabilities == null) { logger.log(SentryLevel.INFO, "NetworkCapabilities is null and cannot check network type"); return null; From e520ff7dcfb5bb1ac69f9d2a8510f8c55a6da5ad Mon Sep 17 00:00:00 2001 From: Stefano Date: Thu, 23 Oct 2025 17:29:45 +0200 Subject: [PATCH 02/11] updated changelog --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9c2eb2a58e..fc5d7da6fa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## Unreleased + +### Miscellaneous + +- [ANR] Update Connection Status cache in the background ([#4832](https://github.com/getsentry/sentry-java/pull/4832)) + ## 8.24.0 ### Features From 7ae75a26c00fdc6bfcdc6975c844e7880cbf5306 Mon Sep 17 00:00:00 2001 From: Stefano Date: Thu, 23 Oct 2025 17:56:29 +0200 Subject: [PATCH 03/11] update cache inside lock avoid concurrent cache updates --- .../util/AndroidConnectionStatusProvider.java | 40 ++++++++++--------- 1 file changed, 21 insertions(+), 19 deletions(-) diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/internal/util/AndroidConnectionStatusProvider.java b/sentry-android-core/src/main/java/io/sentry/android/core/internal/util/AndroidConnectionStatusProvider.java index 956df4b1b8..0a9efd6d0e 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/internal/util/AndroidConnectionStatusProvider.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/internal/util/AndroidConnectionStatusProvider.java @@ -54,6 +54,7 @@ public final class AndroidConnectionStatusProvider private static final @NotNull AutoClosableReentrantLock childCallbacksLock = new AutoClosableReentrantLock(); private static final @NotNull List childCallbacks = new ArrayList<>(); + private static final AtomicBoolean isUpdatingCache = new AtomicBoolean(false); private static final int[] transports = { NetworkCapabilities.TRANSPORT_WIFI, @@ -268,19 +269,16 @@ private void updateCacheAndNotifyObservers( // Only notify observers if something meaningful changed if (shouldUpdate) { + try (final @NotNull ISentryLifecycleToken ignored = lock.acquire()) { cachedNetworkCapabilities = networkCapabilities; lastCacheUpdateTime = timeProvider.getCurrentTimeMillis(); + final @NotNull ConnectionStatus status = getConnectionStatusFromCache(); options .getLogger() .log( SentryLevel.DEBUG, - "Cache updated - Status: " - + getConnectionStatusFromCache() - + ", Type: " - + getConnectionTypeFromCache()); + "Cache updated - Status: " + status + ", Type: " + getConnectionTypeFromCache()); - final @NotNull ConnectionStatus status = getConnectionStatusFromCache(); - try (final @NotNull ISentryLifecycleToken ignored = lock.acquire()) { for (final @NotNull IConnectionStatusObserver observer : connectionStatusObservers) { observer.onConnectionStatusChanged(status); @@ -378,27 +376,31 @@ private void updateCache() { // Fallback: query current active network in the background submitSafe( () -> { - final ConnectivityManager connectivityManager = + // Avoid concurrent updates + if (!isUpdatingCache.getAndSet(true)) { + final ConnectivityManager connectivityManager = getConnectivityManager(context, options.getLogger()); - if (connectivityManager != null) { - final @Nullable NetworkCapabilities capabilities = + if (connectivityManager != null) { + final @Nullable NetworkCapabilities capabilities = getNetworkCapabilities(connectivityManager); - try (final @NotNull ISentryLifecycleToken ignored2 = lock.acquire()) { - cachedNetworkCapabilities = capabilities; - lastCacheUpdateTime = timeProvider.getCurrentTimeMillis(); + try (final @NotNull ISentryLifecycleToken ignored2 = lock.acquire()) { + cachedNetworkCapabilities = capabilities; + lastCacheUpdateTime = timeProvider.getCurrentTimeMillis(); - if (capabilities != null) { - options + if (capabilities != null) { + options .getLogger() .log( - SentryLevel.DEBUG, - "Cache updated - Status: " - + getConnectionStatusFromCache() - + ", Type: " - + getConnectionTypeFromCache()); + SentryLevel.DEBUG, + "Cache updated - Status: " + + getConnectionStatusFromCache() + + ", Type: " + + getConnectionTypeFromCache()); + } } } + isUpdatingCache.set(false); } }); From e44381d9bd7c6173c3fcea80228963c7756f0873 Mon Sep 17 00:00:00 2001 From: Sentry Github Bot Date: Thu, 23 Oct 2025 16:01:07 +0000 Subject: [PATCH 04/11] Format code --- .../util/AndroidConnectionStatusProvider.java | 35 ++++++++++--------- 1 file changed, 19 insertions(+), 16 deletions(-) diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/internal/util/AndroidConnectionStatusProvider.java b/sentry-android-core/src/main/java/io/sentry/android/core/internal/util/AndroidConnectionStatusProvider.java index 0a9efd6d0e..c9a4202988 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/internal/util/AndroidConnectionStatusProvider.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/internal/util/AndroidConnectionStatusProvider.java @@ -270,14 +270,17 @@ private void updateCacheAndNotifyObservers( // Only notify observers if something meaningful changed if (shouldUpdate) { try (final @NotNull ISentryLifecycleToken ignored = lock.acquire()) { - cachedNetworkCapabilities = networkCapabilities; - lastCacheUpdateTime = timeProvider.getCurrentTimeMillis(); + cachedNetworkCapabilities = networkCapabilities; + lastCacheUpdateTime = timeProvider.getCurrentTimeMillis(); final @NotNull ConnectionStatus status = getConnectionStatusFromCache(); - options - .getLogger() - .log( - SentryLevel.DEBUG, - "Cache updated - Status: " + status + ", Type: " + getConnectionTypeFromCache()); + options + .getLogger() + .log( + SentryLevel.DEBUG, + "Cache updated - Status: " + + status + + ", Type: " + + getConnectionTypeFromCache()); for (final @NotNull IConnectionStatusObserver observer : connectionStatusObservers) { @@ -379,10 +382,10 @@ private void updateCache() { // Avoid concurrent updates if (!isUpdatingCache.getAndSet(true)) { final ConnectivityManager connectivityManager = - getConnectivityManager(context, options.getLogger()); + getConnectivityManager(context, options.getLogger()); if (connectivityManager != null) { final @Nullable NetworkCapabilities capabilities = - getNetworkCapabilities(connectivityManager); + getNetworkCapabilities(connectivityManager); try (final @NotNull ISentryLifecycleToken ignored2 = lock.acquire()) { cachedNetworkCapabilities = capabilities; @@ -390,13 +393,13 @@ private void updateCache() { if (capabilities != null) { options - .getLogger() - .log( - SentryLevel.DEBUG, - "Cache updated - Status: " - + getConnectionStatusFromCache() - + ", Type: " - + getConnectionTypeFromCache()); + .getLogger() + .log( + SentryLevel.DEBUG, + "Cache updated - Status: " + + getConnectionStatusFromCache() + + ", Type: " + + getConnectionTypeFromCache()); } } } From 8dba1d8cf70ee4a2c7847c3a6e2cbaf3cf2c006b Mon Sep 17 00:00:00 2001 From: Stefano Date: Fri, 24 Oct 2025 12:32:14 +0200 Subject: [PATCH 05/11] update cache inside lock avoid concurrent cache updates --- .../internal/util/AndroidConnectionStatusProvider.java | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/internal/util/AndroidConnectionStatusProvider.java b/sentry-android-core/src/main/java/io/sentry/android/core/internal/util/AndroidConnectionStatusProvider.java index c9a4202988..df5d17052f 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/internal/util/AndroidConnectionStatusProvider.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/internal/util/AndroidConnectionStatusProvider.java @@ -401,16 +401,18 @@ private void updateCache() { + ", Type: " + getConnectionTypeFromCache()); } + isUpdatingCache.set(false); } } - isUpdatingCache.set(false); } }); } catch (Throwable t) { options.getLogger().log(SentryLevel.WARNING, "Failed to update connection status cache", t); - cachedNetworkCapabilities = null; - lastCacheUpdateTime = timeProvider.getCurrentTimeMillis(); + try (final @NotNull ISentryLifecycleToken ignored = lock.acquire()) { + cachedNetworkCapabilities = null; + lastCacheUpdateTime = timeProvider.getCurrentTimeMillis(); + } } } From 5e5a8d5b4fb34ace2926ab196d38ae8d20d7ac29 Mon Sep 17 00:00:00 2001 From: Stefano Date: Fri, 24 Oct 2025 12:36:47 +0200 Subject: [PATCH 06/11] update cache inside lock avoid concurrent cache updates --- .../core/internal/util/AndroidConnectionStatusProvider.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/internal/util/AndroidConnectionStatusProvider.java b/sentry-android-core/src/main/java/io/sentry/android/core/internal/util/AndroidConnectionStatusProvider.java index df5d17052f..78b82d227b 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/internal/util/AndroidConnectionStatusProvider.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/internal/util/AndroidConnectionStatusProvider.java @@ -401,9 +401,9 @@ private void updateCache() { + ", Type: " + getConnectionTypeFromCache()); } - isUpdatingCache.set(false); } } + isUpdatingCache.set(false); } }); From 7a980e83271ac887f71261718dde7df137fc905d Mon Sep 17 00:00:00 2001 From: Stefano Date: Tue, 4 Nov 2025 12:33:53 +0100 Subject: [PATCH 07/11] AndroidConnectionStatusProvider.updateCache() now doesn't reset cache status synchronously --- .../util/AndroidConnectionStatusProvider.java | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/internal/util/AndroidConnectionStatusProvider.java b/sentry-android-core/src/main/java/io/sentry/android/core/internal/util/AndroidConnectionStatusProvider.java index 78b82d227b..5124541c4b 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/internal/util/AndroidConnectionStatusProvider.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/internal/util/AndroidConnectionStatusProvider.java @@ -360,19 +360,23 @@ private boolean hasSignificantTransportChanges( @SuppressLint({"NewApi", "MissingPermission"}) private void updateCache() { - try (final @NotNull ISentryLifecycleToken ignored = lock.acquire()) { - cachedNetworkCapabilities = null; - lastCacheUpdateTime = timeProvider.getCurrentTimeMillis(); - } try { if (!Permissions.hasPermission(context, Manifest.permission.ACCESS_NETWORK_STATE)) { options .getLogger() .log(SentryLevel.INFO, "No permission (ACCESS_NETWORK_STATE) to check network status."); + try (final @NotNull ISentryLifecycleToken ignored = lock.acquire()) { + cachedNetworkCapabilities = null; + lastCacheUpdateTime = timeProvider.getCurrentTimeMillis(); + } return; } if (buildInfoProvider.getSdkInfoVersion() < Build.VERSION_CODES.M) { + try (final @NotNull ISentryLifecycleToken ignored = lock.acquire()) { + cachedNetworkCapabilities = null; + lastCacheUpdateTime = timeProvider.getCurrentTimeMillis(); + } return; } @@ -387,7 +391,7 @@ private void updateCache() { final @Nullable NetworkCapabilities capabilities = getNetworkCapabilities(connectivityManager); - try (final @NotNull ISentryLifecycleToken ignored2 = lock.acquire()) { + try (final @NotNull ISentryLifecycleToken ignored = lock.acquire()) { cachedNetworkCapabilities = capabilities; lastCacheUpdateTime = timeProvider.getCurrentTimeMillis(); From f3585483eae3c24abdbfa025bbf5c602bdf4ec9b Mon Sep 17 00:00:00 2001 From: Stefano Date: Fri, 7 Nov 2025 14:55:35 +0100 Subject: [PATCH 08/11] check main thread before offloading cache update --- .../util/AndroidConnectionStatusProvider.java | 66 ++++++++++--------- 1 file changed, 36 insertions(+), 30 deletions(-) diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/internal/util/AndroidConnectionStatusProvider.java b/sentry-android-core/src/main/java/io/sentry/android/core/internal/util/AndroidConnectionStatusProvider.java index 5124541c4b..6e773ac2d6 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/internal/util/AndroidConnectionStatusProvider.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/internal/util/AndroidConnectionStatusProvider.java @@ -380,36 +380,15 @@ private void updateCache() { return; } - // Fallback: query current active network in the background - submitSafe( - () -> { - // Avoid concurrent updates - if (!isUpdatingCache.getAndSet(true)) { - final ConnectivityManager connectivityManager = - getConnectivityManager(context, options.getLogger()); - if (connectivityManager != null) { - final @Nullable NetworkCapabilities capabilities = - getNetworkCapabilities(connectivityManager); - - try (final @NotNull ISentryLifecycleToken ignored = lock.acquire()) { - cachedNetworkCapabilities = capabilities; - lastCacheUpdateTime = timeProvider.getCurrentTimeMillis(); - - if (capabilities != null) { - options - .getLogger() - .log( - SentryLevel.DEBUG, - "Cache updated - Status: " - + getConnectionStatusFromCache() - + ", Type: " - + getConnectionTypeFromCache()); - } - } - } - isUpdatingCache.set(false); - } - }); + // Avoid concurrent updates + if (!isUpdatingCache.getAndSet(true)) { + // Fallback: query current active network in the background + if (options.getThreadChecker().isMainThread()) { + submitSafe(() -> updateCacheFromConnectivityManager()); + } else { + updateCacheFromConnectivityManager(); + } + } } catch (Throwable t) { options.getLogger().log(SentryLevel.WARNING, "Failed to update connection status cache", t); @@ -420,6 +399,33 @@ private void updateCache() { } } + @RequiresApi(api = Build.VERSION_CODES.M) + private void updateCacheFromConnectivityManager() { + final ConnectivityManager connectivityManager = + getConnectivityManager(context, options.getLogger()); + if (connectivityManager != null) { + final @Nullable NetworkCapabilities capabilities = + getNetworkCapabilities(connectivityManager); + + try (final @NotNull ISentryLifecycleToken ignored = lock.acquire()) { + cachedNetworkCapabilities = capabilities; + lastCacheUpdateTime = timeProvider.getCurrentTimeMillis(); + + if (capabilities != null) { + options + .getLogger() + .log( + SentryLevel.DEBUG, + "Cache updated - Status: " + + getConnectionStatusFromCache() + + ", Type: " + + getConnectionTypeFromCache()); + } + } + } + isUpdatingCache.set(false); + } + private boolean isCacheValid() { return (timeProvider.getCurrentTimeMillis() - lastCacheUpdateTime) < CACHE_TTL_MS; } From c7698bcb77a5eee3d9639cbea7e8c0fc3424edfa Mon Sep 17 00:00:00 2001 From: Stefano Date: Fri, 7 Nov 2025 16:22:37 +0100 Subject: [PATCH 09/11] cache is now updated directly if not on the main thread while cache is updated, if no cache data exists, connection status is unknown and type is null --- .../util/AndroidConnectionStatusProvider.java | 30 ++++++--- .../AndroidConnectionStatusProviderTest.kt | 67 +++++++++++++++++++ 2 files changed, 88 insertions(+), 9 deletions(-) diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/internal/util/AndroidConnectionStatusProvider.java b/sentry-android-core/src/main/java/io/sentry/android/core/internal/util/AndroidConnectionStatusProvider.java index 6e773ac2d6..de69ca7a0f 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/internal/util/AndroidConnectionStatusProvider.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/internal/util/AndroidConnectionStatusProvider.java @@ -147,6 +147,12 @@ private boolean isNetworkEffectivelyConnected( : ConnectionStatus.DISCONNECTED; } + // If cache is being updated, and there's no cached data, return UNKNOWN status until cache is + // updated + if (isUpdatingCache.get()) { + return ConnectionStatus.UNKNOWN; + } + // Fallback to legacy method when NetworkCapabilities not available final ConnectivityManager connectivityManager = getConnectivityManager(context, options.getLogger()); @@ -164,6 +170,12 @@ private boolean isNetworkEffectivelyConnected( return getConnectionType(capabilities); } + // If cache is being updated, and there's no cached data, return UNKNOWN status until cache is + // updated + if (isUpdatingCache.get()) { + return null; + } + // Fallback to legacy method when NetworkCapabilities not available return getConnectionType(context, options.getLogger(), buildInfoProvider); } @@ -402,10 +414,10 @@ private void updateCache() { @RequiresApi(api = Build.VERSION_CODES.M) private void updateCacheFromConnectivityManager() { final ConnectivityManager connectivityManager = - getConnectivityManager(context, options.getLogger()); + getConnectivityManager(context, options.getLogger()); if (connectivityManager != null) { final @Nullable NetworkCapabilities capabilities = - getNetworkCapabilities(connectivityManager); + getNetworkCapabilities(connectivityManager); try (final @NotNull ISentryLifecycleToken ignored = lock.acquire()) { cachedNetworkCapabilities = capabilities; @@ -413,13 +425,13 @@ private void updateCacheFromConnectivityManager() { if (capabilities != null) { options - .getLogger() - .log( - SentryLevel.DEBUG, - "Cache updated - Status: " - + getConnectionStatusFromCache() - + ", Type: " - + getConnectionTypeFromCache()); + .getLogger() + .log( + SentryLevel.DEBUG, + "Cache updated - Status: " + + getConnectionStatusFromCache() + + ", Type: " + + getConnectionTypeFromCache()); } } } diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/internal/util/AndroidConnectionStatusProviderTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/internal/util/AndroidConnectionStatusProviderTest.kt index 4dd8062464..b8f0a2b6aa 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/internal/util/AndroidConnectionStatusProviderTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/internal/util/AndroidConnectionStatusProviderTest.kt @@ -25,8 +25,10 @@ import io.sentry.android.core.AppState import io.sentry.android.core.BuildInfoProvider import io.sentry.android.core.ContextUtils import io.sentry.android.core.SystemEventsBreadcrumbsIntegration +import io.sentry.test.DeferredExecutorService import io.sentry.test.ImmediateExecutorService import io.sentry.transport.ICurrentDateProvider +import io.sentry.util.thread.IThreadChecker import kotlin.test.AfterTest import kotlin.test.BeforeTest import kotlin.test.Test @@ -38,6 +40,7 @@ import kotlin.test.assertTrue import org.junit.runner.RunWith import org.mockito.MockedStatic import org.mockito.Mockito.mockStatic +import org.mockito.Mockito.never import org.mockito.kotlin.any import org.mockito.kotlin.anyOrNull import org.mockito.kotlin.argThat @@ -205,6 +208,41 @@ class AndroidConnectionStatusProviderTest { providerWithNullConnectivity.close() } + @Test + fun `When cache is updating, return UNKNOWN for connectionStatus on main thread`() { + whenever(networkInfo.isConnected).thenReturn(true) + // When we are on the main thread + val mockThreadChecker = mock() + options.threadChecker = mockThreadChecker + whenever(mockThreadChecker.isMainThread()).thenReturn(true) + + // The update is done on the background + val executorService = DeferredExecutorService() + options.executorService = executorService + + // Advance time beyond TTL (2 minutes) + currentTime += 2 * 60 * 1000L + + // Connection status is unknown while we update the cache + assertEquals( + IConnectionStatusProvider.ConnectionStatus.UNKNOWN, + connectionStatusProvider.connectionStatus, + ) + + verify(connectivityManager, never()).activeNetworkInfo + verify(connectivityManager, never()).activeNetwork + + // When background cache update is done + executorService.runAll() + + // Connection status is updated + verify(connectivityManager).activeNetwork + assertEquals( + IConnectionStatusProvider.ConnectionStatus.CONNECTED, + connectionStatusProvider.connectionStatus, + ) + } + @Test fun `When there's no permission, return null for getConnectionType`() { whenever(contextMock.checkPermission(any(), any(), any())).thenReturn(PERMISSION_DENIED) @@ -219,6 +257,35 @@ class AndroidConnectionStatusProviderTest { assertNull(connectionStatusProvider.connectionType) } + @Test + fun `When cache is updating, return null for getConnectionType on main thread`() { + whenever(networkInfo.isConnected).thenReturn(true) + // When we are on the main thread + val mockThreadChecker = mock() + options.threadChecker = mockThreadChecker + whenever(mockThreadChecker.isMainThread()).thenReturn(true) + + // The update is done on the background + val executorService = DeferredExecutorService() + options.executorService = executorService + + // Advance time beyond TTL (2 minutes) + currentTime += 2 * 60 * 1000L + + // Connection type is null while we update the cache + assertNull(connectionStatusProvider.connectionType) + + verify(connectivityManager, never()).activeNetworkInfo + verify(connectivityManager, never()).activeNetwork + + // When background cache update is done + executorService.runAll() + + // Connection type is updated + verify(connectivityManager).activeNetwork + assertNotNull(connectionStatusProvider.connectionType) + } + @Test fun `When network capabilities are not available, return null for getConnectionType`() { whenever(connectivityManager.getNetworkCapabilities(any())).thenReturn(null) From 3d90c0765dd384654969e74ecb1a566f7731d156 Mon Sep 17 00:00:00 2001 From: Stefano Date: Fri, 7 Nov 2025 16:47:30 +0100 Subject: [PATCH 10/11] fixed tests --- .../internal/util/AndroidConnectionStatusProvider.java | 10 +++++++++- .../util/AndroidConnectionStatusProviderTest.kt | 2 ++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/internal/util/AndroidConnectionStatusProvider.java b/sentry-android-core/src/main/java/io/sentry/android/core/internal/util/AndroidConnectionStatusProvider.java index de69ca7a0f..20c96dcc9e 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/internal/util/AndroidConnectionStatusProvider.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/internal/util/AndroidConnectionStatusProvider.java @@ -396,7 +396,7 @@ private void updateCache() { if (!isUpdatingCache.getAndSet(true)) { // Fallback: query current active network in the background if (options.getThreadChecker().isMainThread()) { - submitSafe(() -> updateCacheFromConnectivityManager()); + submitSafe(() -> updateCacheFromConnectivityManager(), () -> isUpdatingCache.set(false)); } else { updateCacheFromConnectivityManager(); } @@ -854,12 +854,20 @@ public static List getChildCallbacks() { } private void submitSafe(@NotNull Runnable r) { + submitSafe(r, null); + } + + private void submitSafe(final @NotNull Runnable r, final @Nullable Runnable onFinally) { try { options.getExecutorService().submit(r); } catch (Throwable e) { options .getLogger() .log(SentryLevel.ERROR, "AndroidConnectionStatusProvider submit failed", e); + } finally { + if (onFinally != null) { + onFinally.run(); + } } } } diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/internal/util/AndroidConnectionStatusProviderTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/internal/util/AndroidConnectionStatusProviderTest.kt index b8f0a2b6aa..a1a1e6550a 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/internal/util/AndroidConnectionStatusProviderTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/internal/util/AndroidConnectionStatusProviderTest.kt @@ -106,6 +106,7 @@ class AndroidConnectionStatusProviderTest { options = SentryOptions() options.setLogger(logger) options.executorService = ImmediateExecutorService() + options.threadChecker = AndroidThreadChecker.getInstance() // Reset current time for each test to ensure cache isolation currentTime = 1000L @@ -128,6 +129,7 @@ class AndroidConnectionStatusProviderTest { @AfterTest fun `tear down`() { + options.executorService = ImmediateExecutorService() // clear the cache and ensure proper cleanup connectionStatusProvider.close() contextUtilsStaticMock.close() From e69534d24c5675f917c134dfee34b7008f39dd74 Mon Sep 17 00:00:00 2001 From: Stefano Date: Fri, 7 Nov 2025 16:50:55 +0100 Subject: [PATCH 11/11] fixed tests --- .../internal/util/AndroidConnectionStatusProvider.java | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/internal/util/AndroidConnectionStatusProvider.java b/sentry-android-core/src/main/java/io/sentry/android/core/internal/util/AndroidConnectionStatusProvider.java index 20c96dcc9e..9c8ed2feb3 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/internal/util/AndroidConnectionStatusProvider.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/internal/util/AndroidConnectionStatusProvider.java @@ -857,16 +857,15 @@ private void submitSafe(@NotNull Runnable r) { submitSafe(r, null); } - private void submitSafe(final @NotNull Runnable r, final @Nullable Runnable onFinally) { + private void submitSafe(final @NotNull Runnable r, final @Nullable Runnable onError) { try { options.getExecutorService().submit(r); } catch (Throwable e) { options .getLogger() .log(SentryLevel.ERROR, "AndroidConnectionStatusProvider submit failed", e); - } finally { - if (onFinally != null) { - onFinally.run(); + if (onError != null) { + onError.run(); } } }