diff --git a/service/src/main/java/com/github/kr328/clash/service/TunService.kt b/service/src/main/java/com/github/kr328/clash/service/TunService.kt index 8890d64d..06d843c8 100644 --- a/service/src/main/java/com/github/kr328/clash/service/TunService.kt +++ b/service/src/main/java/com/github/kr328/clash/service/TunService.kt @@ -62,9 +62,9 @@ class TunService : VpnService(), CoroutineScope by CoroutineScope(Dispatchers.De true } - network.onEvent { e -> + network.onEvent { n -> if (Build.VERSION.SDK_INT in 22..28) @TargetApi(22) { - setUnderlyingNetworks(e.network?.let { arrayOf(it) }) + setUnderlyingNetworks(n?.let { arrayOf(it) }) } false diff --git a/service/src/main/java/com/github/kr328/clash/service/clash/module/NetworkObserveModule.kt b/service/src/main/java/com/github/kr328/clash/service/clash/module/NetworkObserveModule.kt index 357b7abb..e8cef4a0 100644 --- a/service/src/main/java/com/github/kr328/clash/service/clash/module/NetworkObserveModule.kt +++ b/service/src/main/java/com/github/kr328/clash/service/clash/module/NetworkObserveModule.kt @@ -1,105 +1,127 @@ package com.github.kr328.clash.service.clash.module import android.app.Service -import android.content.Intent import android.net.* -import android.os.PowerManager +import android.os.Build import androidx.core.content.getSystemService import com.github.kr328.clash.common.log.Log import com.github.kr328.clash.core.Clash import com.github.kr328.clash.service.util.resolveDns import kotlinx.coroutines.NonCancellable import kotlinx.coroutines.channels.Channel -import kotlinx.coroutines.selects.select +import kotlinx.coroutines.channels.trySendBlocking import kotlinx.coroutines.withContext -class NetworkObserveModule(service: Service) : - Module(service) { - data class NetworkChanged(val network: Network?) +class NetworkObserveModule(service: Service) : Module(service) { + private data class Action(val type: Type, val network: Network) { + enum class Type { Available, Lost, Changed } + } private val connectivity = service.getSystemService()!! - private val networks: Channel = Channel(Channel.CONFLATED) + private val actions = Channel(Channel.UNLIMITED) private val request = NetworkRequest.Builder().apply { addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN) addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED) }.build() - private val callback = object : ConnectivityManager.NetworkCallback() { - private var network: Network? = null - override fun onAvailable(network: Network) { - if (this.network != network) - networks.trySend(network) + actions.trySendBlocking(Action(Action.Type.Available, network)) + } - this.network = network + override fun onLost(network: Network) { + actions.trySendBlocking(Action(Action.Type.Lost, network)) } override fun onLinkPropertiesChanged(network: Network, linkProperties: LinkProperties) { - if (this.network == network) - networks.trySend(network) - } - } - - private fun register(): Result { - return runCatching { - connectivity.registerNetworkCallback(request, callback) - }.onFailure { - Log.w("Observe network change: $it", it) - } - } - - private fun unregister(): Result { - return runCatching { - connectivity.unregisterNetworkCallback(callback) + actions.trySendBlocking(Action(Action.Type.Changed, network)) } } override suspend fun run() { - val screenToggle = receiveBroadcast(false, Channel.CONFLATED) { - addAction(Intent.ACTION_SCREEN_ON) - addAction(Intent.ACTION_SCREEN_OFF) - } + try { + connectivity.registerNetworkCallback(request, callback) + } catch (e: Exception) { + Log.w("Observe network failed: $e", e) - if (service.getSystemService()?.isInteractive != false) { - register() + return } try { + var current: Network? = null + val networks = mutableSetOf() + while (true) { - val quit = select { - screenToggle.onReceive { - when (it.action) { - Intent.ACTION_SCREEN_ON -> - register().isFailure - Intent.ACTION_SCREEN_OFF -> - unregister().isFailure - else -> - false + val action = actions.receive() + + when (action.type) { + Action.Type.Available -> { + networks.add(action.network) + } + Action.Type.Lost -> { + networks.remove(action.network) + } + Action.Type.Changed -> { + if (current == action.network) { + val dns = connectivity.resolveDns(action.network) + + Clash.notifyDnsChanged(dns) + + Log.d("Current network changed: ${action.network}: $dns") } - } - networks.onReceive { - val dns = connectivity.resolveDns(it) - Clash.notifyDnsChanged(dns) - - Log.d("Network changed, system dns = $dns") - - enqueueEvent(NetworkChanged(it)) - - false + continue } } - if (quit) { - return + + current = networks.maxByOrNull { + connectivity.getNetworkCapabilities(it)?.let { cap -> + TRANSPORT_PRIORITY.indexOfFirst { cap.hasTransport(it) } + } ?: -1 } + + val dns = connectivity.resolveDns(current) + + Clash.notifyDnsChanged(dns) + + enqueueEvent(current) + + Log.d("Available network changed: $current of $networks: $dns") } } finally { withContext(NonCancellable) { - unregister() + enqueueEvent(null) Clash.notifyDnsChanged(emptyList()) + + runCatching { + connectivity.unregisterNetworkCallback(callback) + } } } } + + companion object { + private val TRANSPORT_PRIORITY = sequence { + yield(NetworkCapabilities.TRANSPORT_CELLULAR) + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) { + yield(NetworkCapabilities.TRANSPORT_LOWPAN) + } + + yield(NetworkCapabilities.TRANSPORT_BLUETOOTH) + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + yield(NetworkCapabilities.TRANSPORT_WIFI_AWARE) + } + + yield(NetworkCapabilities.TRANSPORT_WIFI) + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + yield(NetworkCapabilities.TRANSPORT_USB) + } + + yield(NetworkCapabilities.TRANSPORT_ETHERNET) + }.toList() + } } \ No newline at end of file