From b49b68a5230de952120fedcf63871398c40d5065 Mon Sep 17 00:00:00 2001 From: Aleksandar Apostolov Date: Thu, 3 Sep 2026 15:49:00 +0200 Subject: [PATCH] fix(core): stop treating an unknown network state as offline `shouldDisconnect` read every non-Available network state as offline, `Unknown` included -- the state before any connectivity callback has fired. When the platform reports no viable network at registration time the monitor never seeds a state, so a foreground signal arriving while the socket is still connecting evaluates to Recovery.Disconnect and cancels the caller's connect() rather than letting it fail on its own. Only a reported loss counts as offline now, `Disconnected` or `Unavailable`, which matches how an `Unknown` lifecycle state is already handled. --- .../StreamConnectionRecoveryEvaluatorImpl.kt | 8 ++++- ...reamConnectionRecoveryEvaluatorImplTest.kt | 35 +++++++++++++++++++ 2 files changed, 42 insertions(+), 1 deletion(-) diff --git a/stream-android-core/src/main/java/io/getstream/android/core/internal/recovery/StreamConnectionRecoveryEvaluatorImpl.kt b/stream-android-core/src/main/java/io/getstream/android/core/internal/recovery/StreamConnectionRecoveryEvaluatorImpl.kt index d6812d25..99c09c8d 100644 --- a/stream-android-core/src/main/java/io/getstream/android/core/internal/recovery/StreamConnectionRecoveryEvaluatorImpl.kt +++ b/stream-android-core/src/main/java/io/getstream/android/core/internal/recovery/StreamConnectionRecoveryEvaluatorImpl.kt @@ -59,6 +59,12 @@ internal class StreamConnectionRecoveryEvaluatorImpl( val previousLifecycle = lastLifecycleState val previousNetwork = lastNetworkState val networkAvailable = networkState is StreamNetworkState.Available + // Only a reported loss counts as offline. `Unknown` means no callback has fired yet, + // which is not the same as "no network" and must not tear a connection down — the same + // way an `Unknown` lifecycle state is not treated as backgrounded below. + val networkLost = + networkState is StreamNetworkState.Disconnected || + networkState is StreamNetworkState.Unavailable val networkBecameAvailable = networkAvailable && previousNetwork !is StreamNetworkState.Available val lifecycleForeground = lifecycleState == StreamLifecycleState.Foreground @@ -67,7 +73,7 @@ internal class StreamConnectionRecoveryEvaluatorImpl( val shouldDisconnect = (isConnected || isConnecting) && - (!networkAvailable || lifecycleState == StreamLifecycleState.Background) + (networkLost || lifecycleState == StreamLifecycleState.Background) val shouldConnect = hasConnectedBefore && diff --git a/stream-android-core/src/test/java/io/getstream/android/core/internal/recovery/StreamConnectionRecoveryEvaluatorImplTest.kt b/stream-android-core/src/test/java/io/getstream/android/core/internal/recovery/StreamConnectionRecoveryEvaluatorImplTest.kt index 3fffb6ae..b47f7673 100644 --- a/stream-android-core/src/test/java/io/getstream/android/core/internal/recovery/StreamConnectionRecoveryEvaluatorImplTest.kt +++ b/stream-android-core/src/test/java/io/getstream/android/core/internal/recovery/StreamConnectionRecoveryEvaluatorImplTest.kt @@ -211,6 +211,41 @@ class StreamConnectionRecoveryEvaluatorImplTest { assertNull(recovery) } + // `Unknown` is the state before any connectivity callback has fired, which happens when the + // platform reports no viable network at registration time. Tearing the in-flight connect down + // there turns a socket failure into a cancellation of the caller's connect(). + @Test + fun `does not disconnect while connecting before the first network callback`() = runTest { + val evaluator = evaluator() + + val recovery = + evaluator + .evaluate( + connectionState = StreamConnectionState.Connecting.Opening(TEST_USER_ID), + lifecycleState = StreamLifecycleState.Foreground, + networkState = StreamNetworkState.Unknown, + ) + .getOrThrow() + + assertNull(recovery) + } + + @Test + fun `disconnects when the network is reported unavailable`() = runTest { + val evaluator = evaluator() + + val recovery = + evaluator + .evaluate( + connectionState = connectedState(), + lifecycleState = StreamLifecycleState.Foreground, + networkState = StreamNetworkState.Unavailable, + ) + .getOrThrow() + + assertIs>(recovery) + } + @Test fun `stays idle when returning foreground while already reconnecting`() = runTest { val evaluator = evaluator()