refactor clash core context
This commit is contained in:
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
+1
-8
@@ -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() {
|
||||
|
||||
}
|
||||
}
|
||||
+46
-84
@@ -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
|
||||
}
|
||||
+103
@@ -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))
|
||||
}
|
||||
}
|
||||
+15
-40
@@ -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
|
||||
|
||||
|
||||
Reference in new issue
Block a user