diff --git a/service/src/main/java/com/github/kr328/clash/service/ClashService.kt b/service/src/main/java/com/github/kr328/clash/service/ClashService.kt index d7ec76a..f3fdfd0 100644 --- a/service/src/main/java/com/github/kr328/clash/service/ClashService.kt +++ b/service/src/main/java/com/github/kr328/clash/service/ClashService.kt @@ -12,11 +12,10 @@ import com.github.kr328.clash.service.settings.ServiceSettings import com.github.kr328.clash.service.util.broadcastClashStarted import com.github.kr328.clash.service.util.broadcastClashStopped import kotlinx.coroutines.launch -import kotlinx.coroutines.runBlocking class ClashService : BaseService() { private val service = this - private val runtime = ClashRuntime() + private val runtime = ClashRuntime(this) private var reason: String? = null override fun onCreate() { @@ -34,13 +33,13 @@ class ClashService : BaseService() { val settings = ServiceSettings(service) runtime.install(ReloadModule(service)) { - onEmptyProfile { + onEmpty { reason = "No profile selected" stopSelf() } } - runtime.install(CloseModule(service)) { + runtime.install(CloseModule()) { onClose { reason = null @@ -53,7 +52,7 @@ class ClashService : BaseService() { else runtime.install(StaticNotificationModule(service)) - runtime.start() + runtime.exec() } } @@ -68,13 +67,9 @@ class ClashService : BaseService() { } override fun onDestroy() { - runBlocking { - ServiceStatusProvider.serviceRunning = false + ServiceStatusProvider.serviceRunning = false - runtime.stop() - - service.broadcastClashStopped(reason) - } + service.broadcastClashStopped(reason) super.onDestroy() } diff --git a/service/src/main/java/com/github/kr328/clash/service/ProfileBackgroundService.kt b/service/src/main/java/com/github/kr328/clash/service/ProfileBackgroundService.kt index c3a946c..64e43a4 100644 --- a/service/src/main/java/com/github/kr328/clash/service/ProfileBackgroundService.kt +++ b/service/src/main/java/com/github/kr328/clash/service/ProfileBackgroundService.kt @@ -11,7 +11,7 @@ import android.os.Build import android.os.IBinder import androidx.core.app.NotificationCompat import androidx.core.app.NotificationManagerCompat -import com.github.kr328.clash.component.ids.Intents +import com.github.kr328.clash.common.ids.Intents import com.github.kr328.clash.service.data.ClashDatabase import com.github.kr328.clash.service.data.ClashProfileDao import com.github.kr328.clash.service.ipc.IStreamCallback diff --git a/service/src/main/java/com/github/kr328/clash/service/ProfileRequestReceiver.kt b/service/src/main/java/com/github/kr328/clash/service/ProfileRequestReceiver.kt index b093994..520fcac 100644 --- a/service/src/main/java/com/github/kr328/clash/service/ProfileRequestReceiver.kt +++ b/service/src/main/java/com/github/kr328/clash/service/ProfileRequestReceiver.kt @@ -3,7 +3,7 @@ package com.github.kr328.clash.service import android.content.BroadcastReceiver import android.content.Context import android.content.Intent -import com.github.kr328.clash.component.ids.Intents +import com.github.kr328.clash.common.ids.Intents import com.github.kr328.clash.service.transact.ProfileRequest import com.github.kr328.clash.service.util.componentName import com.github.kr328.clash.service.util.startForegroundServiceCompat diff --git a/service/src/main/java/com/github/kr328/clash/service/ProfileService.kt b/service/src/main/java/com/github/kr328/clash/service/ProfileService.kt index df33bd4..cca6698 100644 --- a/service/src/main/java/com/github/kr328/clash/service/ProfileService.kt +++ b/service/src/main/java/com/github/kr328/clash/service/ProfileService.kt @@ -4,7 +4,7 @@ import android.content.Intent import android.net.Uri import android.os.IBinder import androidx.core.content.FileProvider -import com.github.kr328.clash.component.ids.Intents +import com.github.kr328.clash.common.ids.Intents import com.github.kr328.clash.core.Global import com.github.kr328.clash.core.utils.Log import com.github.kr328.clash.service.data.ClashDatabase 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 c4586f0..9765eb6 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 @@ -1,14 +1,11 @@ package com.github.kr328.clash.service -import android.content.BroadcastReceiver -import android.content.Context import android.content.Intent import android.net.VpnService import com.github.kr328.clash.core.Clash import com.github.kr328.clash.core.utils.Log import com.github.kr328.clash.service.clash.ClashRuntime import com.github.kr328.clash.service.clash.module.* -import com.github.kr328.clash.service.net.DefaultNetworkChannel import com.github.kr328.clash.service.settings.ServiceSettings import com.github.kr328.clash.service.util.asSocketAddressText import com.github.kr328.clash.service.util.broadcastClashStarted @@ -30,7 +27,7 @@ class TunService : VpnService(), CoroutineScope by MainScope() { } private val service = this - private val runtime = ClashRuntime() + private val runtime = ClashRuntime(this) private var reason: String? = null override fun onCreate() { @@ -51,20 +48,24 @@ class TunService : VpnService(), CoroutineScope by MainScope() { val dnsInject = DnsInjectModule() runtime.install(ReloadModule(service)) { - onEmptyProfile { + onEmpty { launch { reason = "No selected profile" + stopSelf() - runtime.stop() + + TunModule.requestStop() } } } - runtime.install(CloseModule(service)) { + runtime.install(CloseModule()) { onClose { launch { reason = null + stopSelf() - runtime.stop() + + TunModule.requestStop() } } } @@ -82,41 +83,23 @@ class TunService : VpnService(), CoroutineScope by MainScope() { dnsOverride = settings.get(ServiceSettings.OVERRIDE_DNS) } - runtime.start() - - val defaultNetworkChannel = DefaultNetworkChannel(service, this) - - try { - defaultNetworkChannel.register() - - while (isActive) { - val d = defaultNetworkChannel.receive() - - if (d == null) { - setUnderlyingNetworks(null) - continue - } - - setUnderlyingNetworks(arrayOf(d.first)) + runtime.install(NetworkObserveModule(service)) { + onNetworkChanged { network, dnsServers -> + setUnderlyingNetworks(network?.let { arrayOf(it) }) if (settings.get(ServiceSettings.AUTO_ADD_SYSTEM_DNS)) { - withContext(Dispatchers.Default) { - val dnsServers = d.second?.dnsServers ?: emptyList() - - val dnsStrings = dnsServers.map { - it.asSocketAddressText(53) - } - - dnsInject.appendDns = dnsStrings + val dnsStrings = dnsServers.map { + it.asSocketAddressText(53) } + + dnsInject.appendDns = dnsStrings } broadcastNetworkChanged() } } - finally { - defaultNetworkChannel.unregister() - } + + runtime.exec() } } @@ -127,13 +110,9 @@ class TunService : VpnService(), CoroutineScope by MainScope() { } override fun onDestroy() { - runBlocking { - ServiceStatusProvider.serviceRunning = false + ServiceStatusProvider.serviceRunning = false - runtime.stop() - - broadcastClashStopped(reason) - } + broadcastClashStopped(reason) cancel() diff --git a/service/src/main/java/com/github/kr328/clash/service/clash/ClashRuntime.kt b/service/src/main/java/com/github/kr328/clash/service/clash/ClashRuntime.kt index a5ef3ce..9732880 100644 --- a/service/src/main/java/com/github/kr328/clash/service/clash/ClashRuntime.kt +++ b/service/src/main/java/com/github/kr328/clash/service/clash/ClashRuntime.kt @@ -1,62 +1,97 @@ package com.github.kr328.clash.service.clash +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.IntentFilter import com.github.kr328.clash.core.Clash import com.github.kr328.clash.service.clash.module.Module +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.delay +import kotlinx.coroutines.isActive +import kotlinx.coroutines.launch +import kotlinx.coroutines.selects.select import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock -class ClashRuntime { +class ClashRuntime(private val context: Context) { companion object { private val GIL = Mutex() // :) } private val modules: MutableList = mutableListOf() private val mutex = Mutex() - private var started = false - private var stopped = false - - suspend fun start() = mutex.withLock { - if (!GIL.tryLock()) - throw IllegalStateException("ClashRuntime running") - - if (started || stopped) - return - - started = true - - Clash.start() - - modules.forEach { - it.onStart() - } - } - - suspend fun stop() = mutex.withLock { - if (!started || stopped) - return - - modules.forEach { - it.onStop() - it.onDestroy() - } - - Clash.stop() - - GIL.unlock() - } suspend fun install(module: T, configure: T.() -> Unit = {}) = mutex.withLock { - if (stopped) - return - - stopped = true - modules.add(module) module.onCreate() module.configure() + } - if (started) - module.onStart() + suspend fun exec() { + GIL.withLock { + execLocked() + } + } + + private suspend fun execLocked() { + val broadcastChannel = Channel(Channel.UNLIMITED) + val tickerChannel = Channel() + val receiver = object : BroadcastReceiver() { + override fun onReceive(context: Context?, intent: Intent?) { + broadcastChannel.offer(intent ?: return) + } + } + var tickerEnabled: Boolean + + coroutineScope { + launch { + while (isActive) { + tickerChannel.offer(Unit) + delay(1000) + } + } + + try { + Clash.start() + + modules.forEach { + it.onStart() + } + + context.registerReceiver(receiver, IntentFilter().apply { + modules.flatMap { it.receiveBroadcasts }.distinct().forEach { + addAction(it) + } + }) + + while (isActive) { + tickerEnabled = modules.any { it.enableTicker } + + select { + broadcastChannel.onReceive { intent -> + modules.forEach { + it.onBroadcastReceived(intent) + } + } + if (tickerEnabled) { + tickerChannel.onReceive { + modules.forEach { + it.onTick() + } + } + } + } + } + } finally { + modules.reversed().forEach { + it.onStop() + } + + Clash.stop() + } + } } } \ No newline at end of file diff --git a/service/src/main/java/com/github/kr328/clash/service/clash/module/CloseModule.kt b/service/src/main/java/com/github/kr328/clash/service/clash/module/CloseModule.kt index 7de77c9..ffe121e 100644 --- a/service/src/main/java/com/github/kr328/clash/service/clash/module/CloseModule.kt +++ b/service/src/main/java/com/github/kr328/clash/service/clash/module/CloseModule.kt @@ -1,35 +1,22 @@ package com.github.kr328.clash.service.clash.module -import android.app.Service -import android.content.BroadcastReceiver -import android.content.Context import android.content.Intent -import android.content.IntentFilter -import com.github.kr328.clash.component.ids.Intents +import com.github.kr328.clash.common.ids.Intents + +class CloseModule : Module() { + override val receiveBroadcasts: Set + get() = setOf(Intents.INTENT_ACTION_REQUEST_STOP) -class CloseModule(private val service: Service) : Module { private var callback: () -> Unit = {} - private val receiver = object : BroadcastReceiver() { - override fun onReceive(context: Context?, intent: Intent?) { - callback() - } - } fun onClose(cb: () -> Unit) { callback = cb } - override suspend fun onCreate() {} - - override suspend fun onStart() { - service.registerReceiver(receiver, IntentFilter(Intents.INTENT_ACTION_REQUEST_STOP)) - } - - override suspend fun onStop() { - service.unregisterReceiver(receiver) - } - - override suspend fun onDestroy() { - + override suspend fun onBroadcastReceived(intent: Intent) { + when (intent.action) { + Intents.INTENT_ACTION_REQUEST_STOP -> + callback() + } } } \ No newline at end of file diff --git a/service/src/main/java/com/github/kr328/clash/service/clash/module/DnsInjectModule.kt b/service/src/main/java/com/github/kr328/clash/service/clash/module/DnsInjectModule.kt index 9a74fd5..07feebf 100644 --- a/service/src/main/java/com/github/kr328/clash/service/clash/module/DnsInjectModule.kt +++ b/service/src/main/java/com/github/kr328/clash/service/clash/module/DnsInjectModule.kt @@ -2,7 +2,7 @@ package com.github.kr328.clash.service.clash.module import com.github.kr328.clash.core.Clash -class DnsInjectModule : Module { +class DnsInjectModule : Module() { var dnsOverride: Boolean = false set(value) { Clash.setDnsOverrideEnabled(value) @@ -14,10 +14,6 @@ class DnsInjectModule : Module { field = value } - override suspend fun onCreate() { - - } - override suspend fun onStart() { Clash.setDnsOverrideEnabled(dnsOverride) Clash.appendDns(appendDns) @@ -28,7 +24,4 @@ class DnsInjectModule : Module { Clash.appendDns(emptyList()) } - override suspend fun onDestroy() { - - } } \ No newline at end of file diff --git a/service/src/main/java/com/github/kr328/clash/service/clash/module/DynamicNotificationModule.kt b/service/src/main/java/com/github/kr328/clash/service/clash/module/DynamicNotificationModule.kt index 565557f..83a5661 100644 --- a/service/src/main/java/com/github/kr328/clash/service/clash/module/DynamicNotificationModule.kt +++ b/service/src/main/java/com/github/kr328/clash/service/clash/module/DynamicNotificationModule.kt @@ -2,27 +2,27 @@ package com.github.kr328.clash.service.clash.module import android.app.PendingIntent import android.app.Service -import android.content.BroadcastReceiver -import android.content.Context import android.content.Intent -import android.content.IntentFilter import android.os.PowerManager import androidx.core.app.NotificationCompat -import com.github.kr328.clash.component.ids.Intents -import com.github.kr328.clash.component.ids.NotificationChannels -import com.github.kr328.clash.component.ids.NotificationIds +import com.github.kr328.clash.common.ids.Intents +import com.github.kr328.clash.common.ids.NotificationChannels +import com.github.kr328.clash.common.ids.NotificationIds import com.github.kr328.clash.core.Clash +import com.github.kr328.clash.core.utils.Log import com.github.kr328.clash.core.utils.asBytesString import com.github.kr328.clash.service.R import com.github.kr328.clash.service.data.ClashDatabase -import kotlinx.coroutines.Job -import kotlinx.coroutines.channels.Channel -import kotlinx.coroutines.delay -import kotlinx.coroutines.isActive -import kotlinx.coroutines.launch -import kotlinx.coroutines.selects.select +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext -class DynamicNotificationModule(private val service: Service) : Module { +class DynamicNotificationModule(private val service: Service) : Module() { + override val receiveBroadcasts: Set + get() = setOf( + Intent.ACTION_SCREEN_ON, + Intent.ACTION_SCREEN_OFF, + Intents.INTENT_ACTION_PROFILE_CHANGED + ) private val contentIntent = Intent(Intent.ACTION_MAIN) .addCategory(Intent.CATEGORY_DEFAULT) .addCategory(Intent.CATEGORY_LAUNCHER) @@ -43,76 +43,30 @@ class DynamicNotificationModule(private val service: Service) : Module { PendingIntent.FLAG_UPDATE_CURRENT ) ) - private val reloadChannel = Channel(Channel.CONFLATED) - private val screenChannel = Channel(Channel.CONFLATED) - private val receiver = object : BroadcastReceiver() { - override fun onReceive(context: Context?, intent: Intent?) { - when (intent?.action) { - Intent.ACTION_SCREEN_ON -> - screenChannel.offer(true) - Intent.ACTION_SCREEN_OFF -> - screenChannel.offer(false) - Intents.INTENT_ACTION_PROFILE_CHANGED -> - reloadChannel.offer(Unit) - } - } - } - private lateinit var backgroundJob: Job private var currentProfile = "Not selected" - override suspend fun onCreate() { - val database = ClashDatabase.getInstance(service).openClashProfileDao() - val tickerChannel = Channel() - - backgroundJob = launch { - launch { - while (isActive) { - tickerChannel.send(Unit) - delay(1000) - } - } - - var refreshEnabled = - service.getSystemService(PowerManager::class.java).isInteractive - - while (isActive) { - select { - reloadChannel.onReceive { - currentProfile = database.queryActiveProfile()?.name ?: "Not selected" - } - screenChannel.onReceive { - refreshEnabled = it - } - if (refreshEnabled) { - tickerChannel.onReceive - } - } - - update() - } + override suspend fun onBroadcastReceived(intent: Intent) { + when (intent.action) { + Intent.ACTION_SCREEN_ON -> + enableTicker = true + Intent.ACTION_SCREEN_OFF -> + enableTicker = false + Intents.INTENT_ACTION_PROFILE_CHANGED -> + reload() } } override suspend fun onStart() { - reloadChannel.offer(Unit) + enableTicker = service.getSystemService(PowerManager::class.java).isInteractive - service.registerReceiver(receiver, IntentFilter().apply { - addAction(Intents.INTENT_ACTION_PROFILE_CHANGED) - addAction(Intent.ACTION_SCREEN_OFF) - addAction(Intent.ACTION_SCREEN_ON) - }) + reload() } override suspend fun onStop() { - service.unregisterReceiver(receiver) service.stopForeground(true) } - override suspend fun onDestroy() { - backgroundJob.cancel() - } - - private fun update() { + override suspend fun onTick() { val traffic = Clash.queryTraffic() val bandwidth = Clash.queryBandwidth() @@ -121,22 +75,30 @@ class DynamicNotificationModule(private val service: Service) : Module { val uploaded = bandwidth.upload.asBytesString() val downloaded = bandwidth.download.asBytesString() - val notification = builder - .setContentTitle(currentProfile) - .setContentText( - service.getString( - R.string.clash_notification_content, - uploading, downloading + withContext(Dispatchers.Default) { + val notification = builder + .setContentTitle(currentProfile) + .setContentText( + service.getString( + R.string.clash_notification_content, + uploading, downloading + ) ) - ) - .setSubText( - service.getString( - R.string.clash_notification_content, - uploaded, downloaded + .setSubText( + service.getString( + R.string.clash_notification_content, + uploaded, downloaded + ) ) - ) - .build() + .build() - service.startForeground(NotificationIds.CLASH_STATUS, notification) + service.startForeground(NotificationIds.CLASH_STATUS, notification) + } + } + + private suspend fun reload() { + val active = + ClashDatabase.getInstance(service).openClashProfileDao().queryActiveProfile() + currentProfile = active?.name ?: "Not selected" } } \ No newline at end of file diff --git a/service/src/main/java/com/github/kr328/clash/service/clash/module/Module.kt b/service/src/main/java/com/github/kr328/clash/service/clash/module/Module.kt index c509a97..eace7ac 100644 --- a/service/src/main/java/com/github/kr328/clash/service/clash/module/Module.kt +++ b/service/src/main/java/com/github/kr328/clash/service/clash/module/Module.kt @@ -1,16 +1,14 @@ package com.github.kr328.clash.service.clash.module -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlin.coroutines.CoroutineContext -import kotlin.coroutines.coroutineContext +import android.content.Intent -interface Module : CoroutineScope { - override val coroutineContext: CoroutineContext - suspend fun get() = Dispatchers.Unconfined +abstract class Module { + open suspend fun onCreate() {} + open suspend fun onStart() {} + open suspend fun onStop() {} + open suspend fun onTick() {} + open suspend fun onBroadcastReceived(intent: Intent) {} - suspend fun onCreate() - suspend fun onStart() - suspend fun onStop() - suspend fun onDestroy() + open val receiveBroadcasts: Set = emptySet() + var enableTicker: Boolean = false } \ No newline at end of file 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 new file mode 100644 index 0000000..bc42bbf --- /dev/null +++ b/service/src/main/java/com/github/kr328/clash/service/clash/module/NetworkObserveModule.kt @@ -0,0 +1,103 @@ +package com.github.kr328.clash.service.clash.module + +import android.content.Context +import android.net.* +import kotlinx.coroutines.sync.Mutex +import java.net.InetAddress + +class NetworkObserveModule(context: Context) : Module() { + private var networkChanged: (Network?, List) -> Unit = { _, _ -> } + private var network: Network? = null + private val lock = Mutex() + private val connectivity = context.getSystemService(ConnectivityManager::class.java) + private val callback = object : ConnectivityManager.NetworkCallback() { + private val internet = mutableMapOf() + private val dns = mutableMapOf>() + + override fun onAvailable(network: Network) { + detectDefaultNetwork() + } + + override fun onLosing(network: Network, maxMsToLive: Int) { + internet.remove(network) + dns.remove(network) + + detectDefaultNetwork() + } + + override fun onLinkPropertiesChanged(network: Network, linkProperties: LinkProperties) { + val old = dns[network] + val new = linkProperties.dnsServers + + if (old != new) { + dns[network] = new + detectDefaultNetwork() + } + } + + override fun onCapabilitiesChanged( + network: Network, + networkCapabilities: NetworkCapabilities + ) { + val old = internet[network] + val new = networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) + + if (old != new) { + internet[network] = new + detectDefaultNetwork() + } + } + } + + override suspend fun onStart() { + connectivity.registerNetworkCallback(NetworkRequest.Builder().build(), callback) + } + + override suspend fun onStop() { + connectivity.unregisterNetworkCallback(callback) + } + + fun onNetworkChanged(callback: (Network?, List) -> Unit) { + networkChanged = callback + } + + private fun detectDefaultNetwork() { + if (!lock.tryLock()) + return + + val def = connectivity.allNetworks + .asSequence() + .mapNotNull { network -> + connectivity.getNetworkCapabilities(network)?.let { it to network } + } + .filterNot { + it.first.hasTransport(NetworkCapabilities.TRANSPORT_VPN) || + !it.first.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) + } + .sortedBy { + when { + it.first.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET) -> 0 + it.first.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) -> 1 + it.first.hasTransport(NetworkCapabilities.TRANSPORT_BLUETOOTH) -> 2 + it.first.hasTransport(NetworkCapabilities.TRANSPORT_LOWPAN) -> 3 + it.first.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) -> 4 + else -> 5 + } + if (it.first.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED)) + -1000 + else + 0 + } + .map { + it.second + } + .firstOrNull() + + if (def != network) { + network = def + + networkChanged(def, connectivity.getLinkProperties(def)?.dnsServers ?: emptyList()) + } + + lock.unlock() + } +} \ No newline at end of file diff --git a/service/src/main/java/com/github/kr328/clash/service/clash/module/ReloadModule.kt b/service/src/main/java/com/github/kr328/clash/service/clash/module/ReloadModule.kt index 535d4ff..db160a4 100644 --- a/service/src/main/java/com/github/kr328/clash/service/clash/module/ReloadModule.kt +++ b/service/src/main/java/com/github/kr328/clash/service/clash/module/ReloadModule.kt @@ -1,68 +1,51 @@ package com.github.kr328.clash.service.clash.module -import android.content.BroadcastReceiver import android.content.Context import android.content.Intent -import android.content.IntentFilter -import com.github.kr328.clash.component.ids.Intents +import com.github.kr328.clash.common.ids.Intents import com.github.kr328.clash.core.Clash import com.github.kr328.clash.service.data.ClashDatabase import com.github.kr328.clash.service.util.resolveBase import com.github.kr328.clash.service.util.resolveProfile -import kotlinx.coroutines.Job -import kotlinx.coroutines.channels.Channel -import kotlinx.coroutines.isActive -import kotlinx.coroutines.launch +import kotlinx.coroutines.sync.Mutex -class ReloadModule(private val context: Context) : Module { - private lateinit var backgroundJob: Job - private var emptyProfileCallback: () -> Unit = {} - private val reloadChannel = Channel(Channel.CONFLATED) - private val reloadBroadcastReceiver = object : BroadcastReceiver() { - override fun onReceive(context: Context?, intent: Intent?) { - reloadChannel.offer(Unit) - } - } - - override suspend fun onCreate() { - backgroundJob = launch { - val database = ClashDatabase.getInstance(context).openClashProfileDao() - - while (isActive) { - reloadChannel.receive() - - val id = database.queryActiveProfile()?.id - if (id == null) { - emptyProfileCallback() - continue - } - - Clash.loadProfile(resolveProfile(id), resolveBase(id)) - } - } - } +class ReloadModule(private val context: Context) : Module() { + override val receiveBroadcasts: Set + get() = setOf(Intents.INTENT_ACTION_NETWORK_CHANGED, Intents.INTENT_ACTION_PROFILE_CHANGED) + private val reloadMutex = Mutex() + private var emptyCallback: () -> Unit = {} override suspend fun onStart() { - context.registerReceiver( - reloadBroadcastReceiver, - IntentFilter().apply { - addAction(Intents.INTENT_ACTION_PROFILE_CHANGED) - addAction(Intents.INTENT_ACTION_NETWORK_CHANGED) + reload() + } + + override suspend fun onBroadcastReceived(intent: Intent) { + if (!reloadMutex.tryLock()) + return + + when (intent.action) { + Intents.INTENT_ACTION_NETWORK_CHANGED, Intents.INTENT_ACTION_PROFILE_CHANGED -> { + reload() } - ) + } - reloadChannel.offer(Unit) + reloadMutex.unlock() } - override suspend fun onStop() { - context.unregisterReceiver(reloadBroadcastReceiver) + fun onEmpty(callback: () -> Unit) { + emptyCallback = callback } - override suspend fun onDestroy() { - backgroundJob.cancel() - } + private suspend fun reload() { + val database = ClashDatabase.getInstance(context).openClashProfileDao() - fun onEmptyProfile(callback: () -> Unit) { - emptyProfileCallback = callback + val id = database.queryActiveProfile()?.id + if (id == null) { + emptyCallback() + reloadMutex.unlock() + return + } + + Clash.loadProfile(resolveProfile(id), resolveBase(id)) } } \ No newline at end of file diff --git a/service/src/main/java/com/github/kr328/clash/service/clash/module/StaticNotificationModule.kt b/service/src/main/java/com/github/kr328/clash/service/clash/module/StaticNotificationModule.kt index 79dfa12..e87c5ff 100644 --- a/service/src/main/java/com/github/kr328/clash/service/clash/module/StaticNotificationModule.kt +++ b/service/src/main/java/com/github/kr328/clash/service/clash/module/StaticNotificationModule.kt @@ -4,24 +4,19 @@ import android.app.NotificationChannel import android.app.NotificationManager import android.app.PendingIntent import android.app.Service -import android.content.BroadcastReceiver -import android.content.Context import android.content.Intent -import android.content.IntentFilter import android.os.Build import androidx.core.app.NotificationCompat import androidx.core.app.NotificationManagerCompat -import com.github.kr328.clash.component.ids.Intents -import com.github.kr328.clash.component.ids.NotificationChannels -import com.github.kr328.clash.component.ids.NotificationIds +import com.github.kr328.clash.common.ids.Intents +import com.github.kr328.clash.common.ids.NotificationChannels +import com.github.kr328.clash.common.ids.NotificationIds import com.github.kr328.clash.service.R import com.github.kr328.clash.service.data.ClashDatabase -import kotlinx.coroutines.Job -import kotlinx.coroutines.channels.Channel -import kotlinx.coroutines.isActive -import kotlinx.coroutines.launch -class StaticNotificationModule(private val service: Service) : Module { +class StaticNotificationModule(private val service: Service) : Module() { + override val receiveBroadcasts: Set + get() = setOf(Intents.INTENT_ACTION_PROFILE_CHANGED) private val contentIntent = Intent(Intent.ACTION_MAIN) .addCategory(Intent.CATEGORY_DEFAULT) .addCategory(Intent.CATEGORY_LAUNCHER) @@ -42,50 +37,30 @@ class StaticNotificationModule(private val service: Service) : Module { PendingIntent.FLAG_UPDATE_CURRENT ) ) - private val reloadChannel = Channel(Channel.CONFLATED) - private val receiver = object : BroadcastReceiver() { - override fun onReceive(context: Context?, intent: Intent?) { - when (intent?.action) { - Intents.INTENT_ACTION_PROFILE_CHANGED -> - reloadChannel.offer(Unit) - } - } - } - private var currentProfile: String = "Not selected" - private lateinit var backgroundJob: Job - - override suspend fun onCreate() { - val database = ClashDatabase.getInstance(service).openClashProfileDao() - - backgroundJob = launch { - while (isActive) { - reloadChannel.receive() - - currentProfile = database.queryActiveProfile()?.name ?: "Not selected" + override suspend fun onBroadcastReceived(intent: Intent) { + when (intent.action) { + Intents.INTENT_ACTION_PROFILE_CHANGED -> { update() } } } override suspend fun onStart() { - service.registerReceiver(receiver, IntentFilter(Intents.INTENT_ACTION_PROFILE_CHANGED)) - - reloadChannel.offer(Unit) + update() } override suspend fun onStop() { - service.unregisterReceiver(receiver) service.stopForeground(true) } - override suspend fun onDestroy() { - backgroundJob.cancel() - } + private suspend fun update() { + val database = ClashDatabase.getInstance(service).openClashProfileDao() + + val profileName = database.queryActiveProfile()?.name ?: "Not selected" - private fun update() { val notification = builder - .setContentTitle(currentProfile) + .setContentTitle(profileName) .setContentText(service.getText(R.string.running)) .build() diff --git a/service/src/main/java/com/github/kr328/clash/service/clash/module/TunModule.kt b/service/src/main/java/com/github/kr328/clash/service/clash/module/TunModule.kt index 94155ac..3e551e1 100644 --- a/service/src/main/java/com/github/kr328/clash/service/clash/module/TunModule.kt +++ b/service/src/main/java/com/github/kr328/clash/service/clash/module/TunModule.kt @@ -4,13 +4,13 @@ import android.app.PendingIntent import android.content.Intent import android.net.VpnService import android.os.Build -import com.github.kr328.clash.component.ids.NotificationIds +import com.github.kr328.clash.common.ids.NotificationIds import com.github.kr328.clash.core.Clash import com.github.kr328.clash.service.util.parseCIDR import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext -class TunModule(private val service: VpnService) : Module { +class TunModule(private val service: VpnService) : Module() { interface Configure { val builder: VpnService.Builder val mtu: Int @@ -34,7 +34,6 @@ class TunModule(private val service: VpnService) : Module { var configure: Configure? = null - override suspend fun onCreate() {} override suspend fun onStart() { withContext(Dispatchers.IO) { val c = configure ?: throw IllegalArgumentException("Configure required") @@ -107,5 +106,9 @@ class TunModule(private val service: VpnService) : Module { Clash.stopTunDevice() } - override suspend fun onDestroy() {} + companion object { + fun requestStop() { + Clash.stopTunDevice() + } + } } \ No newline at end of file diff --git a/service/src/main/java/com/github/kr328/clash/service/net/DefaultNetworkChannel.kt b/service/src/main/java/com/github/kr328/clash/service/net/DefaultNetworkChannel.kt deleted file mode 100644 index c0d0a86..0000000 --- a/service/src/main/java/com/github/kr328/clash/service/net/DefaultNetworkChannel.kt +++ /dev/null @@ -1,124 +0,0 @@ -package com.github.kr328.clash.service.net - -import android.content.Context -import android.net.* -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.channels.Channel -import kotlinx.coroutines.launch -import kotlinx.coroutines.sync.Mutex -import kotlinx.coroutines.withContext -import java.net.InetAddress - -class DefaultNetworkChannel(val context: Context, scope: CoroutineScope) : - CoroutineScope by scope, - Channel?> by Channel(Channel.CONFLATED) { - private val sendLock = Mutex() - private val connectivity = context.getSystemService(ConnectivityManager::class.java)!! - - private var currentNetwork: Network? = null - - private val callback = object : ConnectivityManager.NetworkCallback() { - private val capabilitiesCache = mutableMapOf() - private val dnsServerCache = mutableMapOf>() - - override fun onAvailable(network: Network) { - sendDefaultNetwork(true) - } - - override fun onLost(network: Network) { - sendDefaultNetwork(true) - - capabilitiesCache.remove(network) - dnsServerCache.remove(network) - } - - override fun onCapabilitiesChanged( - network: Network, - networkCapabilities: NetworkCapabilities? - ) { - val cap = capabilitiesCache[network] - - if (cap?.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) - != networkCapabilities?.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) - ) { - sendDefaultNetwork(true) - } - - capabilitiesCache[network] = networkCapabilities - } - - override fun onLinkPropertiesChanged(network: Network, linkProperties: LinkProperties) { - val cache = dnsServerCache[network] - - if (cache != linkProperties.dnsServers) { - sendDefaultNetwork(false) - } - - dnsServerCache[network] = linkProperties.dnsServers - } - } - - fun register() { - connectivity.registerNetworkCallback(NetworkRequest.Builder().build(), callback) - } - - fun unregister() { - connectivity.unregisterNetworkCallback(callback) - } - - private fun sendDefaultNetwork(ignoreSame: Boolean) { - if (!sendLock.tryLock()) - return - - launch { - val network = detectDefaultNetwork() - val link = network?.let(connectivity::getLinkProperties) - - if (ignoreSame && network == currentNetwork) - return@launch sendLock.unlock() - - currentNetwork = network - - if (network != null) - send(network to link) - else - send(null) - - sendLock.unlock() - } - } - - private suspend fun detectDefaultNetwork() = withContext(Dispatchers.Default) { - try { - connectivity.allNetworks - .asSequence() - .mapNotNull { network -> - connectivity.getNetworkCapabilities(network)?.let { it to network } - } - .filterNot { - it.first.hasTransport(NetworkCapabilities.TRANSPORT_VPN) || - !it.first.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) - } - .sortedBy { - when { - it.first.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET) -> 0 - it.first.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) -> 1 - it.first.hasTransport(NetworkCapabilities.TRANSPORT_BLUETOOTH) -> 2 - it.first.hasTransport(NetworkCapabilities.TRANSPORT_LOWPAN) -> 3 - it.first.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) -> 4 - else -> 5 - } + if (it.first.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED)) - -1000 - else - 0 - } - .map { - it.second - } - .firstOrNull() - } catch (e: Exception) { - null - } - } -} \ No newline at end of file diff --git a/service/src/main/java/com/github/kr328/clash/service/util/BroadcastUtils.kt b/service/src/main/java/com/github/kr328/clash/service/util/BroadcastUtils.kt index db3fdeb..4cffcff 100644 --- a/service/src/main/java/com/github/kr328/clash/service/util/BroadcastUtils.kt +++ b/service/src/main/java/com/github/kr328/clash/service/util/BroadcastUtils.kt @@ -2,7 +2,7 @@ package com.github.kr328.clash.service.util import android.content.Context import android.content.Intent -import com.github.kr328.clash.component.ids.Intents +import com.github.kr328.clash.common.ids.Intents import com.github.kr328.clash.core.Global fun Context.sendBroadcastSelf(intent: Intent) { diff --git a/service/src/main/java/com/github/kr328/clash/service/util/UpdateUtils.kt b/service/src/main/java/com/github/kr328/clash/service/util/UpdateUtils.kt index 91f6a40..89ea1f6 100644 --- a/service/src/main/java/com/github/kr328/clash/service/util/UpdateUtils.kt +++ b/service/src/main/java/com/github/kr328/clash/service/util/UpdateUtils.kt @@ -4,7 +4,7 @@ import android.app.AlarmManager import android.app.PendingIntent import android.content.Context import android.content.Intent -import com.github.kr328.clash.component.ids.Intents +import com.github.kr328.clash.common.ids.Intents import com.github.kr328.clash.service.ProfileRequestReceiver import com.github.kr328.clash.service.data.ClashProfileEntity