diff --git a/.gitignore b/.gitignore index fc46e7b..bf25eae 100644 --- a/.gitignore +++ b/.gitignore @@ -41,3 +41,9 @@ keystore.properties .cxx *.hprof + +# firebase +google-services.json + +# Dolphin +.directory diff --git a/app/build.gradle b/app/build.gradle index 11ce45c..8329fab 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -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' diff --git a/app/src/main/java/com/github/kr328/clash/BaseActivity.kt b/app/src/main/java/com/github/kr328/clash/BaseActivity.kt new file mode 100644 index 0000000..a24ed41 --- /dev/null +++ b/app/src/main/java/com/github/kr328/clash/BaseActivity.kt @@ -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 + } +} \ No newline at end of file diff --git a/app/src/main/java/com/github/kr328/clash/ClashStatusActivity.kt b/app/src/main/java/com/github/kr328/clash/ClashStatusActivity.kt index 9012223..34cb81e 100644 --- a/app/src/main/java/com/github/kr328/clash/ClashStatusActivity.kt +++ b/app/src/main/java/com/github/kr328/clash/ClashStatusActivity.kt @@ -1,7 +1,5 @@ package com.github.kr328.clash -import androidx.appcompat.app.AppCompatActivity - -class ClashStatusActivity : AppCompatActivity() { +class ClashStatusActivity : BaseActivity() { } \ No newline at end of file diff --git a/app/src/main/java/com/github/kr328/clash/CreateProfileActivity.kt b/app/src/main/java/com/github/kr328/clash/CreateProfileActivity.kt index 9952e42..e5a32cb 100644 --- a/app/src/main/java/com/github/kr328/clash/CreateProfileActivity.kt +++ b/app/src/main/java/com/github/kr328/clash/CreateProfileActivity.kt @@ -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, diff --git a/app/src/main/java/com/github/kr328/clash/ImportFileActivity.kt b/app/src/main/java/com/github/kr328/clash/ImportFileActivity.kt index 873d1b8..ef242d5 100644 --- a/app/src/main/java/com/github/kr328/clash/ImportFileActivity.kt +++ b/app/src/main/java/com/github/kr328/clash/ImportFileActivity.kt @@ -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 diff --git a/app/src/main/java/com/github/kr328/clash/MainActivity.kt b/app/src/main/java/com/github/kr328/clash/MainActivity.kt index dd86a73..ad5a009 100644 --- a/app/src/main/java/com/github/kr328/clash/MainActivity.kt +++ b/app/src/main/java/com/github/kr328/clash/MainActivity.kt @@ -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 } diff --git a/app/src/main/java/com/github/kr328/clash/ProfilesActivity.kt b/app/src/main/java/com/github/kr328/clash/ProfilesActivity.kt index 63f6b01..b1488c6 100644 --- a/app/src/main/java/com/github/kr328/clash/ProfilesActivity.kt +++ b/app/src/main/java/com/github/kr328/clash/ProfilesActivity.kt @@ -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) diff --git a/app/src/main/java/com/github/kr328/clash/adapter/ProfileAdapter.kt b/app/src/main/java/com/github/kr328/clash/adapter/ProfileAdapter.kt index ec5bdd5..607314a 100644 --- a/app/src/main/java/com/github/kr328/clash/adapter/ProfileAdapter.kt +++ b/app/src/main/java/com/github/kr328/clash/adapter/ProfileAdapter.kt @@ -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) } diff --git a/build.gradle b/build.gradle index dd71c44..bc04311 100644 --- a/build.gradle +++ b/build.gradle @@ -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 } diff --git a/core/src/main/java/com/github/kr328/clash/core/Clash.kt b/core/src/main/java/com/github/kr328/clash/core/Clash.kt index 70bfef4..e4e4f06 100644 --- a/core/src/main/java/com/github/kr328/clash/core/Clash.kt +++ b/core/src/main/java/com/github/kr328/clash/core/Clash.kt @@ -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, 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) } } diff --git a/core/src/main/java/com/github/kr328/clash/core/ClashProcess.kt b/core/src/main/java/com/github/kr328/clash/core/ClashProcess.kt index 3dd8286..4cd08d5 100644 --- a/core/src/main/java/com/github/kr328/clash/core/ClashProcess.kt +++ b/core/src/main/java/com/github/kr328/clash/core/ClashProcess.kt @@ -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() { diff --git a/core/src/main/java/com/github/kr328/clash/core/event/ErrorEvent.kt b/core/src/main/java/com/github/kr328/clash/core/event/ErrorEvent.kt index 37be929..71f5a2f 100644 --- a/core/src/main/java/com/github/kr328/clash/core/event/ErrorEvent.kt +++ b/core/src/main/java/com/github/kr328/clash/core/event/ErrorEvent.kt @@ -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) { diff --git a/core/src/main/java/com/github/kr328/clash/core/event/ProfileChangedEvent.kt b/core/src/main/java/com/github/kr328/clash/core/event/ProfileChangedEvent.kt new file mode 100644 index 0000000..3a9d53d --- /dev/null +++ b/core/src/main/java/com/github/kr328/clash/core/event/ProfileChangedEvent.kt @@ -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 { + override fun createFromParcel(parcel: Parcel): ProfileChangedEvent { + return Parcels.load(serializer(), parcel) + } + + override fun newArray(size: Int): Array { + return arrayOfNulls(size) + } + } +} \ No newline at end of file diff --git a/core/src/main/java/com/github/kr328/clash/core/utils/Log.kt b/core/src/main/java/com/github/kr328/clash/core/utils/Log.kt new file mode 100644 index 0000000..0393386 --- /dev/null +++ b/core/src/main/java/com/github/kr328/clash/core/utils/Log.kt @@ -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) +} diff --git a/service/build.gradle b/service/build.gradle index ef64f68..b28830c 100644 --- a/service/build.gradle +++ b/service/build.gradle @@ -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' -} +} \ No newline at end of file diff --git a/service/src/main/aidl/com/github/kr328/clash/core/event/Event.aidl b/service/src/main/aidl/com/github/kr328/clash/core/event/Event.aidl index 8c1a067..094c402 100644 --- a/service/src/main/aidl/com/github/kr328/clash/core/event/Event.aidl +++ b/service/src/main/aidl/com/github/kr328/clash/core/event/Event.aidl @@ -4,4 +4,5 @@ parcelable ProcessEvent; parcelable LogEvent; parcelable ProxyChangedEvent; parcelable TrafficEvent; -parcelable ErrorEvent; \ No newline at end of file +parcelable ErrorEvent; +parcelable ProfileChangedEvent; \ No newline at end of file diff --git a/service/src/main/aidl/com/github/kr328/clash/service/IClashEventObserver.aidl b/service/src/main/aidl/com/github/kr328/clash/service/IClashEventObserver.aidl index ad9bebc..3347882 100644 --- a/service/src/main/aidl/com/github/kr328/clash/service/IClashEventObserver.aidl +++ b/service/src/main/aidl/com/github/kr328/clash/service/IClashEventObserver.aidl @@ -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); } diff --git a/service/src/main/aidl/com/github/kr328/clash/service/IClashEventService.aidl b/service/src/main/aidl/com/github/kr328/clash/service/IClashEventService.aidl index 373bf3b..bb0beea 100644 --- a/service/src/main/aidl/com/github/kr328/clash/service/IClashEventService.aidl +++ b/service/src/main/aidl/com/github/kr328/clash/service/IClashEventService.aidl @@ -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); } diff --git a/service/src/main/aidl/com/github/kr328/clash/service/IClashService.aidl b/service/src/main/aidl/com/github/kr328/clash/service/IClashService.aidl index ddc541d..4afc6e5 100644 --- a/service/src/main/aidl/com/github/kr328/clash/service/IClashService.aidl +++ b/service/src/main/aidl/com/github/kr328/clash/service/IClashService.aidl @@ -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(); diff --git a/service/src/main/java/com/github/kr328/clash/service/ClashEventService.kt b/service/src/main/java/com/github/kr328/clash/service/ClashEventService.kt index c0c48bc..aa1139c 100644 --- a/service/src/main/java/com/github/kr328/clash/service/ClashEventService.kt +++ b/service/src/main/java/com/github/kr328/clash/service/ClashEventService.kt @@ -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) + private data class EventObserverRecord(val observer: IClashEventObserver, val acquiredEvent: Set) private val observers = mutableMapOf() 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 diff --git a/service/src/main/java/com/github/kr328/clash/service/ClashService.kt b/service/src/main/java/com/github/kr328/clash/service/ClashService.kt index a43a02b..1eae425 100644 --- a/service/src/main/java/com/github/kr328/clash/service/ClashService.kt +++ b/service/src/main/java/com/github/kr328/clash/service/ClashService.kt @@ -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 - - 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() - - private val defaultProfileObserver = object: Observer { - 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") } \ No newline at end of file diff --git a/service/src/main/java/com/github/kr328/clash/service/TunService.kt b/service/src/main/java/com/github/kr328/clash/service/TunService.kt index 46890cd..97e34b5 100644 --- a/service/src/main/java/com/github/kr328/clash/service/TunService.kt +++ b/service/src/main/java/com/github/kr328/clash/service/TunService.kt @@ -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") } \ No newline at end of file diff --git a/service/src/main/java/com/github/kr328/clash/service/data/ClashProfileDao.kt b/service/src/main/java/com/github/kr328/clash/service/data/ClashProfileDao.kt index 5c023f8..401244d 100644 --- a/service/src/main/java/com/github/kr328/clash/service/data/ClashProfileDao.kt +++ b/service/src/main/java/com/github/kr328/clash/service/data/ClashProfileDao.kt @@ -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 - @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 @Query("SELECT * FROM profiles ORDER BY name") diff --git a/service/src/main/java/com/github/kr328/clash/service/data/ClashProfileEntity.kt b/service/src/main/java/com/github/kr328/clash/service/data/ClashProfileEntity.kt index 0950db3..f2ee571 100644 --- a/service/src/main/java/com/github/kr328/clash/service/data/ClashProfileEntity.kt +++ b/service/src/main/java/com/github/kr328/clash/service/data/ClashProfileEntity.kt @@ -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,