service event framework
This commit is contained in:
25 files changed
+317
-251
No files matched your search
@@ -41,3 +41,9 @@ keystore.properties
|
||||
.cxx
|
||||
|
||||
*.hprof
|
||||
|
||||
# firebase
|
||||
google-services.json
|
||||
|
||||
# Dolphin
|
||||
.directory
|
||||
@@ -48,4 +48,7 @@ dependencies {
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-serialization-runtime:0.14.0"
|
||||
implementation "com.charleskorn.kaml:kaml:0.14.0"
|
||||
implementation "com.google.android.material:material:1.2.0-alpha02"
|
||||
implementation 'com.google.firebase:firebase-analytics:17.2.1'
|
||||
}
|
||||
|
||||
apply plugin: 'com.google.gms.google-services'
|
||||
@@ -0,0 +1,62 @@
|
||||
package com.github.kr328.clash
|
||||
|
||||
import android.os.IBinder
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import com.github.kr328.clash.core.event.*
|
||||
import com.github.kr328.clash.service.IClashEventObserver
|
||||
|
||||
abstract class BaseActivity : AppCompatActivity(), IClashEventObserver {
|
||||
private val observerBinder = object: IClashEventObserver.Stub() {
|
||||
override fun onLogEvent(event: LogEvent?) {
|
||||
this@BaseActivity.onLogEvent(event)
|
||||
}
|
||||
|
||||
override fun onProcessEvent(event: ProcessEvent?) {
|
||||
this@BaseActivity.onProcessEvent(event)
|
||||
}
|
||||
|
||||
override fun onProxyChangedEvent(event: ProxyChangedEvent?) {
|
||||
this@BaseActivity.onProxyChangedEvent(event)
|
||||
}
|
||||
|
||||
override fun onErrorEvent(event: ErrorEvent?) {
|
||||
this@BaseActivity.onErrorEvent(event)
|
||||
}
|
||||
|
||||
override fun onTrafficEvent(event: TrafficEvent?) {
|
||||
this@BaseActivity.onTrafficEvent(event)
|
||||
}
|
||||
|
||||
override fun onProfileChanged(event: ProfileChangedEvent?) {
|
||||
this@BaseActivity.onProfileChanged(event)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onLogEvent(event: LogEvent?) {
|
||||
|
||||
}
|
||||
|
||||
override fun onErrorEvent(event: ErrorEvent?) {
|
||||
|
||||
}
|
||||
|
||||
override fun onProfileChanged(event: ProfileChangedEvent?) {
|
||||
|
||||
}
|
||||
|
||||
override fun onProcessEvent(event: ProcessEvent?) {
|
||||
|
||||
}
|
||||
|
||||
override fun onProxyChangedEvent(event: ProxyChangedEvent?) {
|
||||
|
||||
}
|
||||
|
||||
override fun onTrafficEvent(event: TrafficEvent?) {
|
||||
|
||||
}
|
||||
|
||||
override fun asBinder(): IBinder {
|
||||
return observerBinder
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,5 @@
|
||||
package com.github.kr328.clash
|
||||
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
|
||||
class ClashStatusActivity : AppCompatActivity() {
|
||||
class ClashStatusActivity : BaseActivity() {
|
||||
|
||||
}
|
||||
@@ -15,7 +15,7 @@ import androidx.fragment.app.commit
|
||||
import com.github.kr328.clash.view.FatItem
|
||||
import kotlinx.android.synthetic.main.activity_new_profile.*
|
||||
|
||||
class CreateProfileActivity : AppCompatActivity() {
|
||||
class CreateProfileActivity : BaseActivity() {
|
||||
companion object {
|
||||
val NEW_PROFILE_SOURCE = listOf(
|
||||
AdapterData(R.drawable.ic_new_profile_file,
|
||||
|
||||
@@ -8,7 +8,6 @@ import android.view.View
|
||||
import android.webkit.MimeTypeMap
|
||||
import android.widget.EditText
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import com.charleskorn.kaml.Yaml
|
||||
import com.charleskorn.kaml.YamlConfiguration
|
||||
import com.github.kr328.clash.model.ClashProfile
|
||||
@@ -18,13 +17,12 @@ import com.github.kr328.clash.utils.FileUtils
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import kotlinx.android.synthetic.main.activity_import_file.*
|
||||
import kotlinx.serialization.parse
|
||||
import java.io.FileOutputStream
|
||||
import java.lang.Exception
|
||||
import java.lang.NullPointerException
|
||||
import kotlin.concurrent.thread
|
||||
|
||||
class ImportFileActivity : AppCompatActivity() {
|
||||
class ImportFileActivity : BaseActivity() {
|
||||
companion object {
|
||||
const val REQUEST_FILE_CODE = 185
|
||||
}
|
||||
@@ -137,7 +135,7 @@ class ImportFileActivity : AppCompatActivity() {
|
||||
name = activity_import_file_name_text.text.toString(),
|
||||
token = "file|" + activity_import_file_path_text.tag.toString(),
|
||||
cache = cache.absolutePath,
|
||||
selected = false,
|
||||
active = false,
|
||||
proxies = parsed.proxies.size,
|
||||
proxyGroups = parsed.proxyGroups.size,
|
||||
rules = parsed.rules.size
|
||||
|
||||
@@ -25,7 +25,7 @@ import kotlinx.android.synthetic.main.activity_main_clash_status.*
|
||||
import kotlinx.android.synthetic.main.activity_main_profiles.*
|
||||
import kotlinx.android.synthetic.main.activity_main_proxy_manage.*
|
||||
|
||||
class MainActivity : AppCompatActivity() {
|
||||
class MainActivity : BaseActivity() {
|
||||
companion object {
|
||||
const val VPN_REQUEST_CODE = 233
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ import com.github.kr328.clash.service.data.ClashProfileEntity
|
||||
import kotlinx.android.synthetic.main.activity_profiles.*
|
||||
import kotlin.concurrent.thread
|
||||
|
||||
class ProfilesActivity : AppCompatActivity() {
|
||||
class ProfilesActivity : BaseActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.activity_profiles)
|
||||
|
||||
@@ -1,13 +1,7 @@
|
||||
package com.github.kr328.clash.adapter
|
||||
|
||||
import android.content.Context
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.CheckBox
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import androidx.appcompat.widget.AppCompatRadioButton
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.github.kr328.clash.R
|
||||
import com.github.kr328.clash.service.data.ClashProfileEntity
|
||||
@@ -39,7 +33,7 @@ class ProfileAdapter(private val context: Context, private val listener: (Int) -
|
||||
R.string.clash_profile_item_summary,
|
||||
current.proxies, current.proxyGroups, current.rules
|
||||
)
|
||||
holder.view.isChecked = current.selected
|
||||
holder.view.isChecked = current.active
|
||||
holder.view.setOnClickListener {
|
||||
listener(current.id)
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ buildscript {
|
||||
classpath 'com.android.tools.build:gradle:3.5.2'
|
||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||
classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version"
|
||||
classpath 'com.google.gms:google-services:4.3.3'
|
||||
// NOTE: Do not place your application dependencies here; they belong
|
||||
// in the individual module build.gradle files
|
||||
}
|
||||
|
||||
@@ -66,7 +66,8 @@ class Clash(
|
||||
|
||||
fun pollEvent(listener: (Event) -> Unit) {
|
||||
runControl(COMMAND_POLL_EVENT) { _, input, _ ->
|
||||
loop@while ( process.isRunning() ) {
|
||||
loop@while ( process.getProcessStatus() == ProcessEvent.STARTED
|
||||
&& !Thread.currentThread().isInterrupted ) {
|
||||
when (val type = input.readInt()) {
|
||||
Event.EVENT_CLOSE -> {
|
||||
break@loop
|
||||
@@ -75,7 +76,7 @@ class Clash(
|
||||
listener(Json(JsonConfiguration.Stable).parse(LogEvent.serializer(), input.readString()))
|
||||
}
|
||||
Event.EVENT_PROXY_CHANGED -> {
|
||||
listener(ProxyChangedEvent())
|
||||
listener(Json(JsonConfiguration.Stable).parse(ProxyChangedEvent.serializer(), input.readString()))
|
||||
}
|
||||
Event.EVENT_TRAFFIC -> {
|
||||
listener(Json(JsonConfiguration.Stable).parse(TrafficEvent.serializer(), input.readString()))
|
||||
@@ -86,19 +87,9 @@ class Clash(
|
||||
}
|
||||
}
|
||||
|
||||
fun setEventEnabled(type: KClass<Event>, enabled: Boolean) {
|
||||
fun setEventEnabled(type: Int, enabled: Boolean) {
|
||||
runControl(COMMAND_SET_EVENT_ENABLED) { _, _, output ->
|
||||
output.writeInt(
|
||||
when ( type ) {
|
||||
LogEvent::class ->
|
||||
Event.EVENT_LOG
|
||||
ProxyChangedEvent::class ->
|
||||
Event.EVENT_PROXY_CHANGED
|
||||
TrafficEvent::class ->
|
||||
Event.EVENT_TRAFFIC
|
||||
else -> throw IllegalArgumentException("Invalid event type $type")
|
||||
}
|
||||
)
|
||||
output.writeInt(type)
|
||||
output.writeInt(if ( enabled ) 1 else 0)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -99,8 +99,11 @@ class ClashProcess(private val context: Context,
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun isRunning(): Boolean {
|
||||
return pid > 0
|
||||
fun getProcessStatus(): ProcessEvent {
|
||||
return if ( pid > 0 )
|
||||
ProcessEvent.STARTED
|
||||
else
|
||||
ProcessEvent.STOPPED
|
||||
}
|
||||
|
||||
fun stop() {
|
||||
|
||||
@@ -8,7 +8,7 @@ import kotlinx.serialization.Serializable
|
||||
@Serializable
|
||||
data class ErrorEvent(val type: Type, val message: String): Event, Parcelable {
|
||||
enum class Type {
|
||||
|
||||
START_FAILURE
|
||||
}
|
||||
|
||||
override fun writeToParcel(parcel: Parcel, flags: Int) {
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
package com.github.kr328.clash.core.event
|
||||
|
||||
import android.os.Parcel
|
||||
import android.os.Parcelable
|
||||
import com.github.kr328.clash.core.serialization.Parcels
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
class ProfileChangedEvent : Event, Parcelable {
|
||||
override fun writeToParcel(parcel: Parcel, flags: Int) {
|
||||
Parcels.dump(serializer(), this, parcel)
|
||||
}
|
||||
|
||||
override fun describeContents(): Int {
|
||||
return 0
|
||||
}
|
||||
|
||||
companion object CREATOR : Parcelable.Creator<ProfileChangedEvent> {
|
||||
override fun createFromParcel(parcel: Parcel): ProfileChangedEvent {
|
||||
return Parcels.load(serializer(), parcel)
|
||||
}
|
||||
|
||||
override fun newArray(size: Int): Array<ProfileChangedEvent?> {
|
||||
return arrayOfNulls(size)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package com.github.kr328.clash.core.utils
|
||||
|
||||
import com.github.kr328.clash.core.Constants.TAG
|
||||
|
||||
object Log {
|
||||
var handler: LogHandler = object: LogHandler {
|
||||
override fun info(message: String, throwable: Throwable?) {
|
||||
android.util.Log.i(TAG, message, throwable)
|
||||
}
|
||||
|
||||
override fun warn(message: String, throwable: Throwable?) {
|
||||
android.util.Log.w(TAG, message, throwable)
|
||||
}
|
||||
|
||||
override fun error(message: String, throwable: Throwable?) {
|
||||
android.util.Log.e(TAG, message, throwable)
|
||||
}
|
||||
|
||||
override fun wtf(message: String, throwable: Throwable?) {
|
||||
android.util.Log.wtf(TAG, message, throwable)
|
||||
}
|
||||
|
||||
override fun debug(message: String, throwable: Throwable?) {
|
||||
android.util.Log.d(TAG, message, throwable)
|
||||
}
|
||||
}
|
||||
|
||||
interface LogHandler {
|
||||
fun info(message: String, throwable: Throwable?)
|
||||
fun warn(message: String, throwable: Throwable?)
|
||||
fun error(message: String, throwable: Throwable?)
|
||||
fun wtf(message: String, throwable: Throwable?)
|
||||
fun debug(message: String, throwable: Throwable?)
|
||||
}
|
||||
|
||||
fun i(message: String, throwable: Throwable? = null) = handler.info(message, throwable)
|
||||
fun w(message: String, throwable: Throwable? = null) = handler.warn(message, throwable)
|
||||
fun e(message: String, throwable: Throwable? = null) = handler.error(message, throwable)
|
||||
fun wtf(message: String, throwable: Throwable? = null) = handler.wtf(message, throwable)
|
||||
fun debug(message: String, throwable: Throwable? = null) = handler.debug(message, throwable)
|
||||
}
|
||||
@@ -39,4 +39,4 @@ dependencies {
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
|
||||
implementation "androidx.room:room-runtime:$room_version"
|
||||
implementation 'androidx.core:core-ktx:1.2.0-rc01'
|
||||
}
|
||||
}
|
||||
@@ -4,4 +4,5 @@ parcelable ProcessEvent;
|
||||
parcelable LogEvent;
|
||||
parcelable ProxyChangedEvent;
|
||||
parcelable TrafficEvent;
|
||||
parcelable ErrorEvent;
|
||||
parcelable ErrorEvent;
|
||||
parcelable ProfileChangedEvent;
|
||||
@@ -8,4 +8,5 @@ interface IClashEventObserver {
|
||||
void onLogEvent(in LogEvent event);
|
||||
void onErrorEvent(in ErrorEvent event);
|
||||
void onTrafficEvent(in TrafficEvent event);
|
||||
void onProfileChanged(in ProfileChangedEvent event);
|
||||
}
|
||||
@@ -3,9 +3,6 @@ package com.github.kr328.clash.service;
|
||||
import com.github.kr328.clash.service.IClashEventObserver;
|
||||
|
||||
interface IClashEventService {
|
||||
void registerEventObserver(String id, in IClashEventObserver observer);
|
||||
void registerEventObserver(String id, in IClashEventObserver observer, in int[] events);
|
||||
void unregisterEventObserver(String id);
|
||||
|
||||
void acquireEvent(String id, int event);
|
||||
void releaseEvent(String id, int event);
|
||||
}
|
||||
@@ -5,7 +5,13 @@ import com.github.kr328.clash.service.IClashEventService;
|
||||
import com.github.kr328.clash.core.event.Event;
|
||||
|
||||
interface IClashService {
|
||||
// Services
|
||||
IClashEventService getEventService();
|
||||
|
||||
// Status
|
||||
ProcessEvent getCurrentProcessStatus();
|
||||
|
||||
// Control
|
||||
void startTunDevice(in ParcelFileDescriptor fd, int mtu);
|
||||
void stopTunDevice();
|
||||
void start();
|
||||
|
||||
@@ -16,7 +16,7 @@ class ClashEventService(private val master: Master) : IClashEventService.Stub()
|
||||
private val EVENT_SET = setOf(Event.EVENT_LOG, Event.EVENT_TRAFFIC, Event.EVENT_PROXY_CHANGED)
|
||||
}
|
||||
|
||||
private data class EventObserverRecord(val observer: IClashEventObserver, val acquiredEvent: MutableSet<Int>)
|
||||
private data class EventObserverRecord(val observer: IClashEventObserver, val acquiredEvent: Set<Int>)
|
||||
|
||||
private val observers = mutableMapOf<String, EventObserverRecord>()
|
||||
private lateinit var handler: Handler
|
||||
@@ -42,7 +42,8 @@ class ClashEventService(private val master: Master) : IClashEventService.Stub()
|
||||
fun preformLogEvent(event: LogEvent) {
|
||||
handler.post {
|
||||
observers.values.forEach {
|
||||
it.observer.onLogEvent(event)
|
||||
if ( it.acquiredEvent.contains(Event.EVENT_LOG) )
|
||||
it.observer.onLogEvent(event)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -50,7 +51,8 @@ class ClashEventService(private val master: Master) : IClashEventService.Stub()
|
||||
fun preformProxyChangedEvent(event: ProxyChangedEvent) {
|
||||
handler.post {
|
||||
observers.values.forEach {
|
||||
it.observer.onProxyChangedEvent(event)
|
||||
if ( it.acquiredEvent.contains(Event.EVENT_PROXY_CHANGED) )
|
||||
it.observer.onProxyChangedEvent(event)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -58,7 +60,8 @@ class ClashEventService(private val master: Master) : IClashEventService.Stub()
|
||||
fun preformTrafficEvent(event: TrafficEvent) {
|
||||
handler.post {
|
||||
observers.values.forEach {
|
||||
it.observer.onTrafficEvent(event)
|
||||
if ( it.acquiredEvent.contains(Event.EVENT_TRAFFIC) )
|
||||
it.observer.onTrafficEvent(event)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -71,23 +74,11 @@ class ClashEventService(private val master: Master) : IClashEventService.Stub()
|
||||
}
|
||||
}
|
||||
|
||||
override fun acquireEvent(id: String?, event: Int) {
|
||||
fun preformProfileChangedEvent(event: ProfileChangedEvent) {
|
||||
handler.post {
|
||||
require(id != null)
|
||||
|
||||
observers[id]?.acquiredEvent?.add(event)
|
||||
|
||||
recastEventRequirement()
|
||||
}
|
||||
}
|
||||
|
||||
override fun releaseEvent(id: String?, event: Int) {
|
||||
handler.post {
|
||||
require(id != null)
|
||||
|
||||
observers[id]?.acquiredEvent?.remove(event)
|
||||
|
||||
recastEventRequirement()
|
||||
observers.values.forEach {
|
||||
it.observer.onProfileChanged(event)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -101,15 +92,25 @@ class ClashEventService(private val master: Master) : IClashEventService.Stub()
|
||||
}
|
||||
}
|
||||
|
||||
override fun registerEventObserver(id: String?, observer: IClashEventObserver?) {
|
||||
override fun registerEventObserver(
|
||||
id: String?,
|
||||
observer: IClashEventObserver?,
|
||||
events: IntArray?
|
||||
) {
|
||||
handler.post {
|
||||
require(id != null && observer != null)
|
||||
require(id != null && observer != null && events != null)
|
||||
|
||||
observers[id] = EventObserverRecord(observer, mutableSetOf())
|
||||
observers[id] = EventObserverRecord(observer, events.toSet())
|
||||
|
||||
observer.asBinder().linkToDeath({
|
||||
unregisterEventObserver(id)
|
||||
}, 0)
|
||||
|
||||
recastEventRequirement()
|
||||
}
|
||||
}
|
||||
|
||||
fun recastEventRequirement() {
|
||||
private fun recastEventRequirement() {
|
||||
handler.post {
|
||||
val req = observers.values.flatMap {
|
||||
it.acquiredEvent
|
||||
|
||||
@@ -1,122 +1,43 @@
|
||||
package com.github.kr328.clash.service
|
||||
|
||||
import android.app.NotificationChannel
|
||||
import android.app.NotificationManager
|
||||
import android.app.PendingIntent
|
||||
import android.app.Service
|
||||
import android.content.ComponentName
|
||||
import android.content.Intent
|
||||
import android.os.*
|
||||
import android.util.Log
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.Observer
|
||||
import android.os.Binder
|
||||
import android.os.IBinder
|
||||
import android.os.ParcelFileDescriptor
|
||||
import com.github.kr328.clash.core.Clash
|
||||
import com.github.kr328.clash.core.event.ProcessEvent
|
||||
import com.github.kr328.clash.core.Constants
|
||||
import com.github.kr328.clash.core.event.*
|
||||
import com.github.kr328.clash.core.utils.Log
|
||||
import com.github.kr328.clash.service.data.ClashDatabase
|
||||
import com.github.kr328.clash.service.data.ClashProfileEntity
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
import java.lang.Exception
|
||||
import java.lang.IllegalArgumentException
|
||||
import java.util.concurrent.Executors
|
||||
import kotlin.concurrent.thread
|
||||
|
||||
class ClashService : Service() {
|
||||
companion object {
|
||||
private const val TAG = "ClashForAndroid"
|
||||
}
|
||||
|
||||
private val handler = Handler()
|
||||
class ClashService : Service(), IClashEventObserver, ClashEventService.Master {
|
||||
private val executor = Executors.newSingleThreadExecutor()
|
||||
private var pollThread: Thread? = null
|
||||
|
||||
private val eventService = ClashEventService(this)
|
||||
|
||||
private lateinit var clash: Clash
|
||||
private lateinit var currentProfile: LiveData<ClashProfileEntity?>
|
||||
|
||||
private var tunEnabled = false
|
||||
|
||||
private var status =
|
||||
ProcessEvent(ProcessEvent.STATUS_STOPPED_INT)
|
||||
set(value) {
|
||||
field = value
|
||||
|
||||
for (observer in observers.values) {
|
||||
runCatching {
|
||||
observer.onStatusChanged(status)
|
||||
}
|
||||
}
|
||||
}
|
||||
private val observers = mutableMapOf<String, IClashObserver>()
|
||||
|
||||
private val defaultProfileObserver = object: Observer<ClashProfileEntity?> {
|
||||
override fun onChanged(file: ClashProfileEntity?) {
|
||||
executor.submit {
|
||||
if (file == null){
|
||||
clash.process.stop()
|
||||
return@submit
|
||||
}
|
||||
|
||||
Log.i(TAG, "Loading profile ${file.cache}")
|
||||
|
||||
try {
|
||||
clash.loadProfile(File(file.cache))
|
||||
} catch (e: IOException) {
|
||||
clash.process.stop()
|
||||
Log.w(TAG, "Load profile failure", e)
|
||||
}
|
||||
|
||||
try {
|
||||
Log.d(Constants.TAG, clash.queryProxies().toString())
|
||||
}
|
||||
catch (e: Exception) {
|
||||
Log.e(Constants.TAG, "NMSL" ,e)
|
||||
}
|
||||
|
||||
handler.post(this@ClashService::updateNotification)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private inner class ClashServiceImpl : IClashService.Stub() {
|
||||
override fun registerObserver(
|
||||
id: String?,
|
||||
notifyCurrent: Boolean,
|
||||
observer: IClashObserver?
|
||||
) {
|
||||
if (id == null || observer == null)
|
||||
throw RemoteException()
|
||||
|
||||
observers[id] = observer
|
||||
|
||||
if (notifyCurrent)
|
||||
observer.onStatusChanged(status)
|
||||
|
||||
|
||||
observer.asBinder().linkToDeath({
|
||||
observers.remove(id)
|
||||
}, 0)
|
||||
}
|
||||
|
||||
override fun unregisterObserver(id: String?) {
|
||||
if (id == null)
|
||||
throw RemoteException()
|
||||
|
||||
observers.remove(id)
|
||||
}
|
||||
private lateinit var database: ClashDatabase
|
||||
|
||||
private val clashService = object: IClashService.Stub() {
|
||||
override fun stopTunDevice() {
|
||||
clash.stopTunDevice()
|
||||
|
||||
handler.post(this@ClashService::updateNotification)
|
||||
}
|
||||
|
||||
override fun start() {
|
||||
try {
|
||||
clash.process.start()
|
||||
} catch (e: IOException) {
|
||||
Log.e(TAG, "Start failure", e)
|
||||
Log.e("Start failure", e)
|
||||
|
||||
throw RemoteException(e.message)
|
||||
this@ClashService.eventService.preformErrorEvent(
|
||||
ErrorEvent(ErrorEvent.Type.START_FAILURE, e.toString())
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -124,48 +45,50 @@ class ClashService : Service() {
|
||||
clash.process.stop()
|
||||
}
|
||||
|
||||
override fun getClashProcessStatus(): ProcessEvent {
|
||||
return status
|
||||
}
|
||||
|
||||
override fun startTunDevice(fd: ParcelFileDescriptor, mtu: Int) {
|
||||
try {
|
||||
clash.startTunDevice(fd.fileDescriptor, mtu)
|
||||
fd.close()
|
||||
tunEnabled = true
|
||||
} catch (e: IOException) {
|
||||
throw RemoteException(e.message)
|
||||
}
|
||||
Log.e("Start tun failure", e)
|
||||
|
||||
handler.post(this@ClashService::updateNotification)
|
||||
this@ClashService.eventService.preformErrorEvent(
|
||||
ErrorEvent(ErrorEvent.Type.START_FAILURE, e.toString())
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getEventService(): IClashEventService {
|
||||
return this@ClashService.eventService
|
||||
}
|
||||
|
||||
override fun getCurrentProcessStatus(): ProcessEvent {
|
||||
return clash.process.getProcessStatus()
|
||||
}
|
||||
}
|
||||
|
||||
override fun requireEvent(event: Int) {
|
||||
clash.setEventEnabled(event, true)
|
||||
}
|
||||
|
||||
override fun releaseEvent(event: Int) {
|
||||
clash.setEventEnabled(event, false)
|
||||
}
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
|
||||
currentProfile = ClashDatabase.getInstance(this)
|
||||
.openClashProfileDao()
|
||||
.observeSelectedProfile()
|
||||
database = ClashDatabase.getInstance(this)
|
||||
|
||||
eventService.registerEventObserver(ClashService::class.java.simpleName,
|
||||
this,
|
||||
intArrayOf(Event.EVENT_TRAFFIC))
|
||||
|
||||
clash = Clash(
|
||||
this,
|
||||
filesDir.resolve("clash"),
|
||||
cacheDir.resolve("clash_controller")
|
||||
) {
|
||||
this.status = it
|
||||
|
||||
handler.post {
|
||||
when (it.status) {
|
||||
ProcessEvent.STATUS_STARTED_INT ->
|
||||
currentProfile.observeForever(defaultProfileObserver)
|
||||
ProcessEvent.STATUS_STOPPED_INT ->
|
||||
currentProfile.removeObserver(defaultProfileObserver)
|
||||
}
|
||||
}
|
||||
|
||||
handler.post(this::updateNotification)
|
||||
}
|
||||
cacheDir.resolve("clash_controller"),
|
||||
eventService::preformProcessEvent)
|
||||
}
|
||||
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
@@ -177,7 +100,7 @@ class ClashService : Service() {
|
||||
}
|
||||
|
||||
override fun onBind(intent: Intent?): IBinder? {
|
||||
return ClashServiceImpl()
|
||||
return clashService
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
@@ -187,43 +110,58 @@ class ClashService : Service() {
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
private fun updateNotification() {
|
||||
if ( status == ProcessEvent.STATUS_STARTED ) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
NotificationManagerCompat.from(this)
|
||||
.createNotificationChannel(NotificationChannel(CLASH_STATUS_NOTIFICATION_CHANNEL,
|
||||
getString(R.string.clash_service_status_channel),
|
||||
NotificationManager.IMPORTANCE_MIN))
|
||||
override fun onProfileChanged(event: ProfileChangedEvent?) {
|
||||
executor.submit {
|
||||
val active = database.openClashProfileDao().queryActiveProfile()
|
||||
|
||||
if (active == null){
|
||||
clash.process.stop()
|
||||
return@submit
|
||||
}
|
||||
|
||||
val notification = NotificationCompat.Builder(this, CLASH_STATUS_NOTIFICATION_CHANNEL)
|
||||
.setSmallIcon(R.drawable.ic_notification_icon)
|
||||
.setMode()
|
||||
.setOngoing(true)
|
||||
.setColorized(true)
|
||||
.setColor(getColor(R.color.colorAccentService))
|
||||
.setShowWhen(false)
|
||||
.setContentTitle(getString(R.string.clash_service_running))
|
||||
.setContentText(getString(R.string.clash_service_running_message, currentProfile.value?.name))
|
||||
.setContentIntent(PendingIntent.getActivity(
|
||||
this,
|
||||
CLASH_STATUS_NOTIFICATION_ID,
|
||||
Intent.makeMainActivity(ComponentName.createRelative(packageName, MAIN_ACTIVITY_NAME))
|
||||
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK),
|
||||
PendingIntent.FLAG_UPDATE_CURRENT
|
||||
))
|
||||
.build()
|
||||
Log.i("Loading profile ${active.cache}")
|
||||
|
||||
NotificationManagerCompat.from(this).notify(CLASH_STATUS_NOTIFICATION_ID, notification)
|
||||
}
|
||||
else {
|
||||
NotificationManagerCompat.from(this).cancel(CLASH_STATUS_NOTIFICATION_ID)
|
||||
try {
|
||||
clash.loadProfile(File(active.cache))
|
||||
} catch (e: IOException) {
|
||||
clash.process.stop()
|
||||
Log.w("Load profile failure", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun NotificationCompat.Builder.setMode(): NotificationCompat.Builder {
|
||||
if ( tunEnabled )
|
||||
setSubText(getString(R.string.clash_service_vpn_mode))
|
||||
return this
|
||||
override fun onProcessEvent(event: ProcessEvent?) {
|
||||
when ( event!! ) {
|
||||
ProcessEvent.STARTED -> {
|
||||
pollThread = thread {
|
||||
clash.pollEvent {
|
||||
try {
|
||||
when ( it ) {
|
||||
is LogEvent ->
|
||||
eventService.preformLogEvent(it)
|
||||
is TrafficEvent ->
|
||||
eventService.preformTrafficEvent(it)
|
||||
is ProxyChangedEvent ->
|
||||
eventService.preformProxyChangedEvent(it)
|
||||
}
|
||||
}
|
||||
catch (e: Exception) {}
|
||||
}
|
||||
}
|
||||
}
|
||||
ProcessEvent.STOPPED -> {
|
||||
pollThread?.interrupt()
|
||||
pollThread = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onTrafficEvent(event: TrafficEvent?) {
|
||||
|
||||
}
|
||||
|
||||
override fun onProxyChangedEvent(event: ProxyChangedEvent?) {}
|
||||
override fun onLogEvent(event: LogEvent?) {}
|
||||
override fun onErrorEvent(event: ErrorEvent?) {}
|
||||
override fun asBinder(): IBinder = throw IllegalArgumentException("asBinder Unsupported")
|
||||
}
|
||||
@@ -3,14 +3,15 @@ package com.github.kr328.clash.service
|
||||
import android.app.Service
|
||||
import android.content.*
|
||||
import android.net.*
|
||||
import android.os.Binder
|
||||
import android.os.Build
|
||||
import android.os.IBinder
|
||||
import android.os.ParcelFileDescriptor
|
||||
import android.util.Log
|
||||
import com.github.kr328.clash.core.event.ProcessEvent
|
||||
import com.github.kr328.clash.core.event.*
|
||||
import com.github.kr328.clash.service.net.DefaultNetworkObserver
|
||||
import java.lang.IllegalArgumentException
|
||||
|
||||
class TunService : VpnService() {
|
||||
class TunService : VpnService(), IClashEventObserver {
|
||||
companion object {
|
||||
const val TAG = "ClashForAndroid"
|
||||
|
||||
@@ -42,29 +43,16 @@ class TunService : VpnService() {
|
||||
service
|
||||
) ?: throw NullPointerException()
|
||||
|
||||
this@TunService.clash = clash
|
||||
|
||||
clash.registerObserver("tun", false, object: IClashObserver.Stub() {
|
||||
override fun onStatusChanged(event: ProcessEvent?) {
|
||||
if ( event == null )
|
||||
return
|
||||
clash.eventService.registerEventObserver(TunService::class.java.simpleName,
|
||||
this@TunService,
|
||||
intArrayOf())
|
||||
|
||||
Log.d(TAG, "New clash status $event")
|
||||
|
||||
when ( event ) {
|
||||
ProcessEvent.STATUS_STOPPED ->
|
||||
stopSelf()
|
||||
ProcessEvent.STATUS_STARTED ->
|
||||
clash.startTunDevice(fileDescriptor, VPN_MTU)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
if ( clash.clashProcessStatus == ProcessEvent.STATUS_STARTED )
|
||||
if ( clash.currentProcessStatus == ProcessEvent.STARTED )
|
||||
clash.startTunDevice(fileDescriptor, VPN_MTU)
|
||||
else
|
||||
clash.start()
|
||||
|
||||
this@TunService.clash = clash
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,7 +70,6 @@ class TunService : VpnService() {
|
||||
.addAddress(PRIVATE_VLAN6_CLIENT, 126)
|
||||
.addDefaultDns()
|
||||
.addDisallowedApplication(packageName)
|
||||
.addDns()
|
||||
.addBypassPrivateRoute()
|
||||
.setMtu(VPN_MTU)
|
||||
.setBlocking(false)
|
||||
@@ -117,11 +104,13 @@ class TunService : VpnService() {
|
||||
defaultNetworkObserver.unregister()
|
||||
}
|
||||
|
||||
private fun Builder.addDefaultDns(): Builder {
|
||||
DEFAULT_DNS.forEach {
|
||||
addDnsServer(it)
|
||||
override fun onProcessEvent(event: ProcessEvent?) {
|
||||
when ( event ) {
|
||||
ProcessEvent.STOPPED ->
|
||||
stopSelf()
|
||||
ProcessEvent.STARTED ->
|
||||
clash.startTunDevice(fileDescriptor, VPN_MTU)
|
||||
}
|
||||
return this
|
||||
}
|
||||
|
||||
private fun Builder.setMeteredCompat(isMetered: Boolean): Builder {
|
||||
@@ -134,7 +123,6 @@ class TunService : VpnService() {
|
||||
// IPv4
|
||||
resources.getStringArray(R.array.bypass_private_route).forEach {
|
||||
val address = it.split("/")
|
||||
Log.d(TAG, "$address")
|
||||
addRoute(address[0], address[1].toInt())
|
||||
}
|
||||
|
||||
@@ -144,10 +132,17 @@ class TunService : VpnService() {
|
||||
return this
|
||||
}
|
||||
|
||||
private fun Builder.addDns(): Builder {
|
||||
private fun Builder.addDefaultDns(): Builder {
|
||||
resources.getStringArray(R.array.default_dns).forEach {
|
||||
addDnsServer(it)
|
||||
}
|
||||
return this
|
||||
}
|
||||
|
||||
override fun onLogEvent(event: LogEvent?) {}
|
||||
override fun onErrorEvent(event: ErrorEvent?) {}
|
||||
override fun onProfileChanged(event: ProfileChangedEvent?) {}
|
||||
override fun onProxyChangedEvent(event: ProxyChangedEvent?) {}
|
||||
override fun onTrafficEvent(event: TrafficEvent?) {}
|
||||
override fun asBinder(): IBinder = throw IllegalArgumentException("asBinder Unsupported")
|
||||
}
|
||||
@@ -8,13 +8,16 @@ import androidx.room.Query
|
||||
|
||||
@Dao
|
||||
interface ClashProfileDao {
|
||||
@Query("UPDATE profiles SET selected = CASE WHEN id = :id THEN 1 ELSE 0 END")
|
||||
@Query("UPDATE profiles SET active = CASE WHEN id = :id THEN 1 ELSE 0 END")
|
||||
fun setSelectedProfile(id: Int)
|
||||
|
||||
@Query("SELECT * FROM profiles WHERE selected = 1 LIMIT 1")
|
||||
@Query("SELECT * FROM profiles WHERE active = 1 LIMIT 1")
|
||||
fun observeSelectedProfile(): LiveData<ClashProfileEntity?>
|
||||
|
||||
@Query("SELECT name FROM profiles WHERE selected = 1 LIMIT 1")
|
||||
@Query("SELECT * FROM profiles WHERE active = 1 LIMIT 1")
|
||||
fun queryActiveProfile(): ClashProfileEntity?
|
||||
|
||||
@Query("SELECT name FROM profiles WHERE active = 1 LIMIT 1")
|
||||
fun observeDefaultProfileName(): LiveData<String?>
|
||||
|
||||
@Query("SELECT * FROM profiles ORDER BY name")
|
||||
|
||||
@@ -8,7 +8,7 @@ data class ClashProfileEntity(
|
||||
@ColumnInfo(name = "name") val name: String,
|
||||
@ColumnInfo(name = "token") val token: String,
|
||||
@ColumnInfo(name = "cache") val cache: String,
|
||||
@ColumnInfo(name = "selected") val selected: Boolean,
|
||||
@ColumnInfo(name = "active") val active: Boolean,
|
||||
@ColumnInfo(name = "proxies") val proxies: Int,
|
||||
@ColumnInfo(name = "proxy_groups") val proxyGroups: Int,
|
||||
@ColumnInfo(name = "rules") val rules: Int,
|
||||
|
||||
Reference in new issue
Block a user