refactor clash core context

This commit is contained in:
Kr328 committed 2020-04-08 00:42:22 +08:00
1 parent c69c41c57f
commit 3152bcaaa6
17 files changed
+328 -439

No files matched your search

@@ -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()
}
@@ -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
@@ -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
@@ -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
@@ -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()
@@ -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<Module> = 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 <T : Module> 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<Intent>(Channel.UNLIMITED)
val tickerChannel = Channel<Unit>()
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<Unit> {
broadcastChannel.onReceive { intent ->
modules.forEach {
it.onBroadcastReceived(intent)
}
}
if (tickerEnabled) {
tickerChannel.onReceive {
modules.forEach {
it.onTick()
}
}
}
}
}
} finally {
modules.reversed().forEach {
it.onStop()
}
Clash.stop()
}
}
}
}
@@ -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<String>
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()
}
}
}
@@ -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() {
}
}
@@ -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<String>
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<Unit>(Channel.CONFLATED)
private val screenChannel = Channel<Boolean>(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<Unit>()
backgroundJob = launch {
launch {
while (isActive) {
tickerChannel.send(Unit)
delay(1000)
}
}
var refreshEnabled =
service.getSystemService(PowerManager::class.java).isInteractive
while (isActive) {
select<Unit> {
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"
}
}
@@ -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<String> = emptySet()
var enableTicker: Boolean = false
}
@@ -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<InetAddress>) -> 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<Network, Boolean>()
private val dns = mutableMapOf<Network, List<InetAddress>>()
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<InetAddress>) -> 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()
}
}
@@ -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<Unit>(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<String>
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))
}
}
@@ -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<String>
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<Unit>(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()
@@ -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()
}
}
}
@@ -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<Pair<Network, LinkProperties?>?> 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<Network, NetworkCapabilities?>()
private val dnsServerCache = mutableMapOf<Network, List<InetAddress>>()
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
}
}
}
@@ -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) {
@@ -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