1313import io .sentry .IConnectionStatusProvider ;
1414import io .sentry .ILogger ;
1515import io .sentry .ISentryLifecycleToken ;
16+ import io .sentry .Sentry ;
1617import io .sentry .SentryLevel ;
1718import io .sentry .SentryOptions ;
19+ import io .sentry .android .core .AppState ;
1820import io .sentry .android .core .BuildInfoProvider ;
1921import io .sentry .android .core .ContextUtils ;
2022import io .sentry .transport .ICurrentDateProvider ;
2123import io .sentry .util .AutoClosableReentrantLock ;
2224import java .util .ArrayList ;
2325import java .util .List ;
26+ import java .util .concurrent .ExecutorService ;
27+ import java .util .concurrent .Executors ;
28+ import java .util .concurrent .ThreadFactory ;
29+ import java .util .concurrent .atomic .AtomicBoolean ;
2430import org .jetbrains .annotations .ApiStatus ;
2531import org .jetbrains .annotations .NotNull ;
2632import org .jetbrains .annotations .Nullable ;
3238 * details
3339 */
3440@ ApiStatus .Internal
35- public final class AndroidConnectionStatusProvider implements IConnectionStatusProvider {
41+ public final class AndroidConnectionStatusProvider implements IConnectionStatusProvider ,
42+ AppState .AppStateListener {
3643
3744 private final @ NotNull Context context ;
3845 private final @ NotNull SentryOptions options ;
@@ -58,12 +65,12 @@ public final class AndroidConnectionStatusProvider implements IConnectionStatusP
5865
5966 private static final int [] capabilities = new int [2 ];
6067
61- private final @ NotNull Thread initThread ;
6268 private volatile @ Nullable NetworkCapabilities cachedNetworkCapabilities ;
6369 private volatile @ Nullable Network currentNetwork ;
6470 private volatile long lastCacheUpdateTime = 0 ;
6571 private static final long CACHE_TTL_MS = 2 * 60 * 1000L ; // 2 minutes
66-
72+ private final @ NotNull AtomicBoolean isConnected = new AtomicBoolean (false );
73+
6774 @ SuppressLint ("InlinedApi" )
6875 public AndroidConnectionStatusProvider (
6976 @ NotNull Context context ,
@@ -83,8 +90,9 @@ public AndroidConnectionStatusProvider(
8390
8491 // Register network callback immediately for caching
8592 //noinspection Convert2MethodRef
86- initThread = new Thread (() -> ensureNetworkCallbackRegistered ());
87- initThread .start ();
93+ submitSafe (() -> ensureNetworkCallbackRegistered ());
94+
95+ AppState .getInstance ().addAppStateListener (this );
8896 }
8997
9098 /**
@@ -148,6 +156,10 @@ private boolean isNetworkEffectivelyConnected(
148156 }
149157
150158 private void ensureNetworkCallbackRegistered () {
159+ if (!ContextUtils .isForegroundImportance ()) {
160+ return ;
161+ }
162+
151163 if (networkCallback != null ) {
152164 return ; // Already registered
153165 }
@@ -163,9 +175,13 @@ private void ensureNetworkCallbackRegistered() {
163175 public void onAvailable (final @ NotNull Network network ) {
164176 currentNetwork = network ;
165177
166- try (final @ NotNull ISentryLifecycleToken ignored = childCallbacksLock .acquire ()) {
167- for (final @ NotNull NetworkCallback cb : childCallbacks ) {
168- cb .onAvailable (network );
178+ // have to only dispatch this on first registration + when the connection got re-established
179+ // otherwise it would've been dispatched on every foreground
180+ if (!isConnected .getAndSet (true )) {
181+ try (final @ NotNull ISentryLifecycleToken ignored = childCallbacksLock .acquire ()) {
182+ for (final @ NotNull NetworkCallback cb : childCallbacks ) {
183+ cb .onAvailable (network );
184+ }
169185 }
170186 }
171187 }
@@ -197,6 +213,7 @@ public void onLost(final @NotNull Network network) {
197213 }
198214
199215 private void clearCacheAndNotifyObservers () {
216+ isConnected .set (false );
200217 // Clear cached capabilities and network reference atomically
201218 try (final @ NotNull ISentryLifecycleToken ignored = lock .acquire ()) {
202219 cachedNetworkCapabilities = null ;
@@ -413,42 +430,84 @@ public void removeConnectionStatusObserver(final @NotNull IConnectionStatusObser
413430 }
414431 }
415432
433+ private void unregisterNetworkCallback (final boolean clearObservers ) {
434+ try (final @ NotNull ISentryLifecycleToken ignored = lock .acquire ()) {
435+ if (clearObservers ) {
436+ connectionStatusObservers .clear ();
437+ }
438+
439+ final @ Nullable NetworkCallback callbackRef = networkCallback ;
440+ networkCallback = null ;
441+
442+ if (callbackRef != null ) {
443+ unregisterNetworkCallback (context , options .getLogger (), callbackRef );
444+ }
445+ // Clear cached state
446+ cachedNetworkCapabilities = null ;
447+ currentNetwork = null ;
448+ lastCacheUpdateTime = 0 ;
449+ }
450+ options .getLogger ().log (SentryLevel .DEBUG , "Network callback unregistered" );
451+ }
452+
416453 /** Clean up resources - should be called when the provider is no longer needed */
417454 @ Override
418455 public void close () {
419- try {
420- options
421- .getExecutorService ()
422- .submit (
423- () -> {
424- final NetworkCallback callbackRef ;
425- try (final @ NotNull ISentryLifecycleToken ignored = lock .acquire ()) {
426- connectionStatusObservers .clear ();
456+ submitSafe (
457+ () -> {
458+ unregisterNetworkCallback (/* clearObservers = */ true );
459+ try (final @ NotNull ISentryLifecycleToken ignored = childCallbacksLock .acquire ()) {
460+ childCallbacks .clear ();
461+ }
462+ try (final @ NotNull ISentryLifecycleToken ignored =
463+ connectivityManagerLock .acquire ()) {
464+ connectivityManager = null ;
465+ }
466+ });
467+ }
427468
428- callbackRef = networkCallback ;
429- networkCallback = null ;
469+ @ Override
470+ public void onForeground () {
471+ if (networkCallback != null ) {
472+ return ;
473+ }
430474
431- if (callbackRef != null ) {
432- unregisterNetworkCallback (context , options .getLogger (), callbackRef );
433- }
434- // Clear cached state
435- cachedNetworkCapabilities = null ;
436- currentNetwork = null ;
437- lastCacheUpdateTime = 0 ;
438- }
439- try (final @ NotNull ISentryLifecycleToken ignored = childCallbacksLock .acquire ()) {
440- childCallbacks .clear ();
441- }
442- try (final @ NotNull ISentryLifecycleToken ignored =
443- connectivityManagerLock .acquire ()) {
444- connectivityManager = null ;
445- }
446- });
447- } catch (Throwable t ) {
448- options
449- .getLogger ()
450- .log (SentryLevel .ERROR , "Error submitting AndroidConnectionStatusProvider task" , t );
475+ submitSafe (() -> {
476+ // proactively update cache and notify observers on foreground to ensure connectivity state is not stale
477+ updateCache (null );
478+
479+ final @ NotNull ConnectionStatus status = getConnectionStatusFromCache ();
480+ if (status == ConnectionStatus .DISCONNECTED ) {
481+ // onLost is not called retroactively when we registerNetworkCallback (as opposed to onAvailable), so we have to do it manually for the DISCONNECTED case
482+ isConnected .set (false );
483+ try (final @ NotNull ISentryLifecycleToken ignored = childCallbacksLock .acquire ()) {
484+ for (final @ NotNull NetworkCallback cb : childCallbacks ) {
485+ //noinspection DataFlowIssue
486+ cb .onLost (null );
487+ }
488+ }
489+ }
490+ try (final @ NotNull ISentryLifecycleToken ignored = lock .acquire ()) {
491+ for (final @ NotNull IConnectionStatusObserver observer :
492+ connectionStatusObservers ) {
493+ observer .onConnectionStatusChanged (status );
494+ }
495+ }
496+
497+ ensureNetworkCallbackRegistered ();
498+ });
499+ }
500+
501+ @ Override
502+ public void onBackground () {
503+ if (networkCallback == null ) {
504+ return ;
451505 }
506+
507+ submitSafe (() -> {
508+ //noinspection Convert2MethodRef
509+ unregisterNetworkCallback (/* clearObservers = */ false );
510+ });
452511 }
453512
454513 /**
@@ -595,7 +654,6 @@ public NetworkCapabilities getCachedNetworkCapabilities() {
595654 if (cellular ) {
596655 return "cellular" ;
597656 }
598-
599657 } catch (Throwable exception ) {
600658 logger .log (SentryLevel .ERROR , "Failed to retrieve network info" , exception );
601659 }
@@ -730,15 +788,30 @@ public NetworkCallback getNetworkCallback() {
730788 return networkCallback ;
731789 }
732790
733- @ TestOnly
734- @ NotNull
735- public Thread getInitThread () {
736- return initThread ;
737- }
738-
739791 @ TestOnly
740792 @ NotNull
741793 public static List <NetworkCallback > getChildCallbacks () {
742794 return childCallbacks ;
743795 }
796+
797+ private void submitSafe (@ NotNull Runnable r ) {
798+ try {
799+ options .getExecutorService ().submit (r );
800+ } catch (Throwable e ) {
801+ options .getLogger ()
802+ .log (SentryLevel .ERROR , "AndroidConnectionStatusProvider submit failed" , e );
803+ }
804+ }
805+
806+ private static class ConnectionStatusProviderThreadyFactory implements ThreadFactory {
807+
808+ private int cnt = 0 ;
809+
810+ @ Override
811+ public Thread newThread (Runnable r ) {
812+ final Thread ret = new Thread (r , "SentryConnectionStatusProvider-" + cnt ++);
813+ ret .setDaemon (true );
814+ return ret ;
815+ }
816+ }
744817}
0 commit comments