Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,16 @@ object DefaultNetworkListener {
is NetworkMessage.Start -> {
if (listeners.isEmpty()) register()
listeners[message.key] = message.listener
if (network != null) message.listener(network)
if (network != null) {
message.listener(network)
} else if (fallback) {
val activeNetwork = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
SagerNet.connectivity.activeNetwork
} else {
null
}
message.listener(activeNetwork)
}
}
is NetworkMessage.Get -> {
check(listeners.isNotEmpty()) { "Getting network without any listeners is not supported" }
Expand Down Expand Up @@ -124,6 +133,7 @@ object DefaultNetworkListener {
}

private var fallback = false
private val callbackRegistration = NetworkCallbackRegistration()
private val request = NetworkRequest.Builder().apply {
addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED)
Expand All @@ -145,8 +155,19 @@ object DefaultNetworkListener {
* Source: https://android.googlesource.com/platform/frameworks/base/+/2df4c7d/services/core/java/com/android/server/ConnectivityService.java#887
*/
private fun register() {
try {
fallback = false
if (callbackRegistration.requiresFallback) {
callbackRegistration.unregister {
SagerNet.connectivity.unregisterNetworkCallback(Callback)
}.onFailure {
Logs.w("DefaultNetworkListener: retry unregister failed", it)
}
}
if (callbackRegistration.requiresFallback) {
fallback = true
return
Comment on lines +166 to +167

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Fallback Still Stalls When unregister cleanup keeps failing, this branch returns before installing a new Android network callback. The next Start only receives the current activeNetwork, so later Wi-Fi/mobile switches or network loss are not delivered through onAvailable or onLost. After a service restart in this state, underlyingNetwork can stay stale for the rest of the process unless the old callback is still active.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No change. After repeated platform cleanup failures, callback ownership is unknown. Registering again could create duplicate callbacks and conflicting stale events. The listener retries cleanup on each restart, delivers the current active network while uncertain, and keeps the actor alive; conservative fallback is the safe residual behavior.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fair point — when unregister fails, the old callback's ownership is genuinely ambiguous, and a blind re-register in that state could produce duplicates or conflicting onAvailable/onLost events that are harder to reason about than a known-stale snapshot. The retry-on-restart + current-network delivery is a coherent conservative contract.

Withdrawing the concern. The fallback behavior is intentional and the reasoning is sound.

}
Comment thread
greptile-apps[bot] marked this conversation as resolved.
Comment thread
greptile-apps[bot] marked this conversation as resolved.
fallback = false
callbackRegistration.register {
when (Build.VERSION.SDK_INT) {
in 31..Int.MAX_VALUE ->
@TargetApi(31)
Expand Down Expand Up @@ -177,11 +198,18 @@ object DefaultNetworkListener {
// known bug on API 23: https://stackoverflow.com/a/33509180/2245107
}
}
} catch (e: Exception) {
Logs.w(e)
}.onFailure {
Logs.w(it)
fallback = true
}
}

private fun unregister() = SagerNet.connectivity.unregisterNetworkCallback(Callback)
private fun unregister() {
callbackRegistration.unregister {
SagerNet.connectivity.unregisterNetworkCallback(Callback)
}.onFailure {
fallback = true
Logs.w("DefaultNetworkListener: failed to unregister network callback", it)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package io.nekohasekai.sagernet.utils

internal class NetworkCallbackRegistration {
internal var isRegistered = false
private set
internal var requiresFallback = false
private set

fun register(block: () -> Unit): Result<Unit> {
if (isRegistered) return Result.success(Unit)
return runCatching(block).onSuccess {
isRegistered = true
requiresFallback = false
}
}

fun unregister(block: () -> Unit): Result<Unit> {
if (!isRegistered) return Result.success(Unit)
return runCatching(block)
.onSuccess {
isRegistered = false
requiresFallback = false
}
.onFailure { throwable ->
if (throwable is IllegalArgumentException) {
isRegistered = false
requiresFallback = false
} else {
requiresFallback = true
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
package io.nekohasekai.sagernet.utils

import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertSame
import org.junit.Assert.assertTrue
import org.junit.Test

class NetworkCallbackRegistrationTest {

@Test
fun successfulRegisterThenUnregisterInvokesEachLambdaOnce() {
val registration = NetworkCallbackRegistration()
var registerCalls = 0
var unregisterCalls = 0

val registerResult = registration.register { registerCalls++ }
val unregisterResult = registration.unregister { unregisterCalls++ }

assertTrue(registerResult.isSuccess)
assertTrue(unregisterResult.isSuccess)
assertEquals(1, registerCalls)
assertEquals(1, unregisterCalls)
assertFalse(registration.isRegistered)
}

@Test
fun failedRegisterLeavesStateFalseAndSkipsUnregisterLambda() {
val registration = NetworkCallbackRegistration()
val failure = IllegalStateException("registration failed")
var unregisterCalls = 0

val registerResult = registration.register { throw failure }
val unregisterResult = registration.unregister { unregisterCalls++ }

assertSame(failure, registerResult.exceptionOrNull())
assertTrue(unregisterResult.isSuccess)
assertEquals(0, unregisterCalls)
assertFalse(registration.isRegistered)
}

@Test
fun failedUnregisterKeepsStateForRetry() {
val registration = NetworkCallbackRegistration()
val failure = IllegalStateException("unregistration failed")
var successfulUnregisterCalls = 0
registration.register {}

val failedResult = registration.unregister { throw failure }
val retryResult = registration.unregister { successfulUnregisterCalls++ }

assertSame(failure, failedResult.exceptionOrNull())
assertTrue(retryResult.isSuccess)
assertEquals(1, successfulUnregisterCalls)
assertFalse(registration.isRegistered)
assertFalse(registration.requiresFallback)
}

@Test
fun alreadyUnregisteredFailureAllowsFreshRegistration() {
val registration = NetworkCallbackRegistration()
var registerCalls = 0
registration.register {}

val unregisterResult = registration.unregister {
throw IllegalArgumentException("callback was not registered")
}
val registerResult = registration.register { registerCalls++ }

assertTrue(unregisterResult.isFailure)
assertTrue(registerResult.isSuccess)
assertEquals(1, registerCalls)
assertTrue(registration.isRegistered)
assertFalse(registration.requiresFallback)
}

@Test
fun registerAfterEitherFailureDoesNotDuplicateARegisteredCallback() {
val registration = NetworkCallbackRegistration()
var successfulRegisterCalls = 0

registration.register { throw IllegalStateException("registration failed") }
val afterRegisterFailure = registration.register { successfulRegisterCalls++ }
registration.unregister { throw IllegalStateException("unregistration failed") }
val afterUnregisterFailure = registration.register { successfulRegisterCalls++ }

assertTrue(afterRegisterFailure.isSuccess)
assertTrue(afterUnregisterFailure.isSuccess)
assertEquals(1, successfulRegisterCalls)
assertTrue(registration.isRegistered)
assertTrue(registration.requiresFallback)
}

@Test
fun repeatedUnregisterWhileFalseIsHarmless() {
val registration = NetworkCallbackRegistration()
var unregisterCalls = 0

val firstResult = registration.unregister { unregisterCalls++ }
val secondResult = registration.unregister { unregisterCalls++ }

assertTrue(firstResult.isSuccess)
assertTrue(secondResult.isSuccess)
assertEquals(0, unregisterCalls)
assertFalse(registration.isRegistered)
}
}
Loading