main activity
This commit is contained in:
18 files changed
+276
-139
No files matched your search
+15
-4
@@ -19,6 +19,13 @@ android {
|
||||
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
||||
}
|
||||
}
|
||||
compileOptions {
|
||||
sourceCompatibility = 1.8
|
||||
targetCompatibility = 1.8
|
||||
}
|
||||
kotlinOptions {
|
||||
jvmTarget = "1.8"
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
@@ -26,12 +33,16 @@ dependencies {
|
||||
|
||||
implementation project(":core")
|
||||
implementation project(":service")
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
|
||||
implementation "androidx.appcompat:appcompat:1.1.0"
|
||||
implementation "androidx.core:core-ktx:1.1.0"
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
|
||||
implementation "androidx.lifecycle:lifecycle-livedata:2.1.0"
|
||||
implementation "androidx.lifecycle:lifecycle-common-java8:2.1.0"
|
||||
implementation "androidx.constraintlayout:constraintlayout:1.1.3"
|
||||
implementation 'androidx.browser:browser:1.0.0'
|
||||
implementation 'androidx.preference:preference:1.1.0'
|
||||
implementation 'androidx.recyclerview:recyclerview:1.1.0-rc01'
|
||||
implementation 'androidx.core:core-ktx:1.2.0-beta02'
|
||||
implementation 'androidx.fragment:fragment-ktx:1.2.0-rc02'
|
||||
implementation 'androidx.appcompat:appcompat:1.1.0'
|
||||
implementation 'androidx.constraintlayout:constraintlayout:2.0.0-beta3'
|
||||
implementation "androidx.room:room-runtime:$room_version"
|
||||
implementation "com.google.android.material:material:1.2.0-alpha02"
|
||||
}
|
||||
@@ -1,10 +1,56 @@
|
||||
package com.github.kr328.clash
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.ServiceConnection
|
||||
import android.net.VpnService
|
||||
import android.os.Bundle
|
||||
import android.os.Handler
|
||||
import android.os.IBinder
|
||||
import android.view.View
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.observe
|
||||
import com.github.kr328.clash.core.ClashProcessStatus
|
||||
import com.github.kr328.clash.service.ClashService
|
||||
import com.github.kr328.clash.service.IClashObserver
|
||||
import com.github.kr328.clash.service.IClashService
|
||||
import com.github.kr328.clash.service.TunService
|
||||
import com.github.kr328.clash.service.data.ClashDatabase
|
||||
import kotlinx.android.synthetic.main.activity_main.*
|
||||
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() {
|
||||
companion object {
|
||||
const val VPN_REQUEST_CODE = 233
|
||||
}
|
||||
|
||||
private val handler = Handler()
|
||||
private var clash: IClashService? = null
|
||||
private val connection = object: ServiceConnection {
|
||||
override fun onServiceDisconnected(name: ComponentName?) {
|
||||
finish()
|
||||
}
|
||||
|
||||
override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
|
||||
clash = IClashService.Stub.asInterface(service)
|
||||
|
||||
clash?.apply {
|
||||
registerObserver("main_activity",true , object: IClashObserver.Stub() {
|
||||
override fun onStatusChanged(status: ClashProcessStatus?) {
|
||||
clashStatus.postValue(status ?: ClashProcessStatus(ClashProcessStatus.STATUS_STOPPED_INT))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val clashStatus = MutableLiveData<ClashProcessStatus>(ClashProcessStatus(ClashProcessStatus.STATUS_STOPPED_INT))
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.activity_main)
|
||||
@@ -12,5 +58,74 @@ class MainActivity : AppCompatActivity() {
|
||||
setSupportActionBar(activity_main_toolbar.apply {
|
||||
setLogo(R.mipmap.ic_launcher_foreground)
|
||||
})
|
||||
|
||||
clashStatus.observe(this) {
|
||||
handler.removeMessages(0)
|
||||
handler.postDelayed({
|
||||
when ( it.status ) {
|
||||
ClashProcessStatus.STATUS_STARTED_INT -> {
|
||||
activity_main_clash_status_icon.setImageResource(R.drawable.ic_clash_started)
|
||||
activity_main_clash_status_title.text = getString(R.string.clash_status_started)
|
||||
activity_main_clash_status_summary.text = getString(R.string.clash_status_forwarded_traffic, "1.4GiB")
|
||||
activity_main_clash_proxies.visibility = View.VISIBLE
|
||||
activity_main_clash_logs.visibility = View.VISIBLE
|
||||
}
|
||||
else -> {
|
||||
activity_main_clash_status_icon.setImageResource(R.drawable.ic_clash_stopped)
|
||||
activity_main_clash_status_title.text = getString(R.string.clash_status_stopped)
|
||||
activity_main_clash_status_summary.text = getString(R.string.clash_status_click_to_start)
|
||||
activity_main_clash_proxies.visibility = View.GONE
|
||||
activity_main_clash_logs.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
}, 300)
|
||||
}
|
||||
|
||||
activity_main_clash_status.setOnClickListener {
|
||||
when ( clashStatus.value?.status ?: ClashProcessStatus.STATUS_STOPPED_INT ) {
|
||||
ClashProcessStatus.STATUS_STARTED_INT -> {}
|
||||
ClashProcessStatus.STATUS_STOPPED_INT -> {
|
||||
VpnService.prepare(this)?.apply {
|
||||
startActivityForResult(this, VPN_REQUEST_CODE)
|
||||
} ?: startService(Intent(this, TunService::class.java))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ClashDatabase.getInstance(this)
|
||||
.openClashProfileDao()
|
||||
.observeDefaultProfileUrl()
|
||||
.observe(this) {
|
||||
activity_main_clash_profiles_summary.text =
|
||||
if ( it == null )
|
||||
getString(R.string.clash_profiles_summary_unselected)
|
||||
else
|
||||
getString(R.string.clash_profiles_summary_selected, it)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
|
||||
bindService(Intent(this, ClashService::class.java),
|
||||
connection,
|
||||
Context.BIND_AUTO_CREATE)
|
||||
}
|
||||
|
||||
override fun onStop() {
|
||||
super.onStop()
|
||||
|
||||
unbindService(connection)
|
||||
}
|
||||
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
when ( requestCode ) {
|
||||
VPN_REQUEST_CODE -> {
|
||||
if ( resultCode == Activity.RESULT_OK )
|
||||
startService(Intent(this, TunService::class.java))
|
||||
}
|
||||
}
|
||||
|
||||
super.onActivityResult(requestCode, resultCode, data)
|
||||
}
|
||||
}
|
||||
@@ -16,8 +16,8 @@
|
||||
<RelativeLayout
|
||||
android:paddingStart="18dp"
|
||||
android:paddingEnd="18dp"
|
||||
android:paddingTop="24dp"
|
||||
android:paddingBottom="24dp"
|
||||
android:paddingTop="20dp"
|
||||
android:paddingBottom="20dp"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
<ImageView
|
||||
@@ -42,7 +42,7 @@
|
||||
android:text="@string/clash_status_click_to_start"
|
||||
android:textColor="@color/light_gray"
|
||||
android:textAppearance="@style/TextAppearance.AppCompat.Small"
|
||||
android:layout_marginTop="2dp"
|
||||
android:layout_marginTop="5dp"
|
||||
android:layout_alignStart="@id/activity_main_clash_status_title"
|
||||
android:layout_below="@id/activity_main_clash_status_title"
|
||||
android:layout_width="wrap_content"
|
||||
|
||||
@@ -40,7 +40,7 @@
|
||||
|
||||
<TextView
|
||||
android:id="@+id/activity_main_clash_profiles_summary"
|
||||
android:text="@string/clash_profiles_summary"
|
||||
android:text="@string/clash_profiles_summary_unselected"
|
||||
android:textColor="@android:color/darker_gray"
|
||||
android:textAppearance="@style/TextAppearance.AppCompat.Small"
|
||||
android:layout_marginTop="2dp"
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<com.google.android.material.card.MaterialCardView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:id="@+id/activity_main_clash_proxies"
|
||||
android:layout_marginTop="18dp"
|
||||
android:layout_marginTop="10dp"
|
||||
android:layout_marginStart="30dp"
|
||||
android:layout_marginEnd="30dp"
|
||||
android:layout_width="match_parent"
|
||||
|
||||
@@ -3,14 +3,15 @@
|
||||
|
||||
<string name="clash_status_stopped">Clash Stopped</string>
|
||||
<string name="clash_status_click_to_start">Click to start</string>
|
||||
<string name="clash_status_running">Clash Started</string>
|
||||
<string name="clash_status_started">Clash Started</string>
|
||||
<string name="clash_status_forwarded_traffic">%s Traffic Forwarded</string>
|
||||
|
||||
<string name="clash_proxy_manage">Proxy Manage</string>
|
||||
<string name="clash_proxy_manage_summary">%d Proxies</string>
|
||||
|
||||
<string name="clash_profiles">Profiles</string>
|
||||
<string name="clash_profiles_summary">%s Selected</string>
|
||||
<string name="clash_profiles_summary_selected">%s Selected</string>
|
||||
<string name="clash_profiles_summary_unselected">Unselected</string>
|
||||
|
||||
<string name="clash_logs">Logs</string>
|
||||
<string name="clash_settings">Settings</string>
|
||||
|
||||
+7
-2
@@ -28,11 +28,15 @@ android {
|
||||
jniLibs.srcDirs += ["$buildDir/intermediates/native_output"]
|
||||
}
|
||||
}
|
||||
compileOptions {
|
||||
sourceCompatibility = 1.8
|
||||
targetCompatibility = 1.8
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation "androidx.core:core-ktx:1.1.0"
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
|
||||
}
|
||||
|
||||
afterEvaluate {
|
||||
@@ -52,7 +56,7 @@ afterEvaluate {
|
||||
|
||||
def ds = tasks.register("downloadMMDB", MMDBDowloadTask.class) {
|
||||
onlyIf {
|
||||
System.currentTimeMillis() - file("$buildDir/intermediates/cache/Country.tar.gz").lastModified() > 30 * 24 * 3600L
|
||||
System.currentTimeMillis() - file("$buildDir/intermediates/cache/Country.tar.gz").lastModified() > 24 * 3600L
|
||||
}
|
||||
|
||||
output = "$buildDir/intermediates/cache/Country.tar.gz"
|
||||
@@ -69,6 +73,7 @@ afterEvaluate {
|
||||
into "$buildDir/intermediates/cache/mmdb"
|
||||
|
||||
doLast {
|
||||
file("$buildDir/intermediates/dynamic_assets").mkdirs()
|
||||
fileTree("$buildDir/intermediates/cache/mmdb").visit { FileVisitDetails details ->
|
||||
if ( details.path.endsWith("Country.mmdb") )
|
||||
details.file.renameTo(file("$buildDir/intermediates/dynamic_assets/Country.mmdb"))
|
||||
|
||||
@@ -29,6 +29,7 @@ func main() {
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Println("[PID]", os.Getpid())
|
||||
fmt.Println("[CONTROLLER] STARTED")
|
||||
|
||||
profile.LoadDefault()
|
||||
|
||||
@@ -87,7 +87,7 @@ func newTunProxy(fd int, mtu int) (*tun, error) {
|
||||
|
||||
// IPv6 [::]/0
|
||||
subnet, _ = tcpip.NewSubnet(tcpip.Address(strings.Repeat("\x00", 16)), tcpip.AddressMask(strings.Repeat("\x00", 16)))
|
||||
ipstack.AddAddressRange(1, ipv4.ProtocolNumber, subnet)
|
||||
ipstack.AddAddressRange(1, ipv6.ProtocolNumber, subnet)
|
||||
|
||||
// TCP handler
|
||||
tcpFwd := tcp.NewForwarder(ipstack, 0, 16, func(r *tcp.ForwarderRequest) {
|
||||
|
||||
@@ -5,6 +5,7 @@ import android.util.Log
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
import java.io.IOException
|
||||
import java.lang.Exception
|
||||
import kotlin.concurrent.thread
|
||||
|
||||
abstract class ClashProcess(private val context: Context,
|
||||
@@ -14,11 +15,9 @@ abstract class ClashProcess(private val context: Context,
|
||||
companion object {
|
||||
const val TAG = "ClashForAndroid"
|
||||
|
||||
private const val CLASH_COMMAND = "\"{CLASH}\" \"{CONTROLLER}\" 2>&1"
|
||||
|
||||
private const val CONTROLLER_STATUS_PREFIX = "[CONTROLLER]"
|
||||
|
||||
private val PID_PATTERN = Regex("PID=(\\d+)")
|
||||
private val PID_PATTERN = Regex("\\[PID]\\s*(\\d+)")
|
||||
private val CONTROLLER_ERROR_PATTERN = Regex("\\[CONTROLLER] ERROR=\\{(.+)\\}")
|
||||
}
|
||||
|
||||
@@ -30,71 +29,72 @@ abstract class ClashProcess(private val context: Context,
|
||||
if ( pid > 0 )
|
||||
return
|
||||
|
||||
extractMMDB()
|
||||
try {
|
||||
clashDir.mkdirs()
|
||||
controllerPath.parentFile?.mkdirs()
|
||||
controllerPath.delete()
|
||||
|
||||
clashDir.mkdirs()
|
||||
controllerPath.parentFile?.mkdirs()
|
||||
controllerPath.delete()
|
||||
extractMMDB()
|
||||
|
||||
val p = ProcessBuilder().apply {
|
||||
command("sh")
|
||||
directory(clashDir)
|
||||
}.start()
|
||||
val clashPath = File(context.applicationInfo.nativeLibraryDir,"libclash.so").absolutePath
|
||||
|
||||
val clashPath = File(context.applicationInfo.nativeLibraryDir,"libclash.so").absolutePath
|
||||
val command = CLASH_COMMAND
|
||||
.replace("{CLASH}", clashPath)
|
||||
.replace("{CONTROLLER}", controllerPath.absolutePath)
|
||||
val p = ProcessBuilder().apply {
|
||||
command(clashPath, controllerPath.absolutePath)
|
||||
directory(clashDir)
|
||||
}.start()
|
||||
|
||||
Log.i(TAG, "Starting clash [$command]")
|
||||
Log.i(TAG, "Starting clash [$clashPath]")
|
||||
|
||||
p.outputStream.use {
|
||||
it.write("echo PID=$$\n".toByteArray())
|
||||
it.write("exec $command\n".toByteArray())
|
||||
it.write("exit\n".toByteArray())
|
||||
it.flush()
|
||||
}
|
||||
val reader = p.inputStream.bufferedReader()
|
||||
var line = ""
|
||||
|
||||
var line = ""
|
||||
// Parse pid
|
||||
var currentPid = 0
|
||||
while ( reader.readLine()?.apply { line = this.trim() } != null ) {
|
||||
Log.d(TAG, line)
|
||||
|
||||
// Parse pid
|
||||
var currentPid = 0
|
||||
while ( p.inputStream.bufferedReader().readLine()?.apply { line = this.trim() } != null ) {
|
||||
if ( PID_PATTERN.matchEntire(line)?.apply { currentPid = groups[1]!!.value.toInt() } != null )
|
||||
break
|
||||
}
|
||||
|
||||
while ( p.inputStream.bufferedReader().readLine()?.apply { line = this.trim() } != null ) {
|
||||
if ( line.startsWith(CONTROLLER_STATUS_PREFIX) ) {
|
||||
val error = CONTROLLER_ERROR_PATTERN.matchEntire(line)?.groups?.get(1)?.value
|
||||
|
||||
if ( error != null )
|
||||
throw IOException("Controller: $error")
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
process = p
|
||||
pid = currentPid
|
||||
|
||||
listener(ClashProcessStatus(ClashProcessStatus.STATUS_STARTED))
|
||||
|
||||
Log.i(TAG, "Clash started pid = $pid")
|
||||
|
||||
thread {
|
||||
// Redirect stdout to log
|
||||
while ( p.inputStream.bufferedReader().readLine()?.apply { line = this } != null ) {
|
||||
Log.i(TAG, line.trim())
|
||||
if ( PID_PATTERN.matchEntire(line)?.apply { currentPid = groups[1]!!.value.toInt() } != null )
|
||||
break
|
||||
}
|
||||
|
||||
synchronized(this@ClashProcess) {
|
||||
p.destroy()
|
||||
process = null
|
||||
pid = -1
|
||||
while ( reader.readLine()?.apply { line = this.trim() } != null ) {
|
||||
Log.d(TAG, line)
|
||||
|
||||
if ( line.startsWith(CONTROLLER_STATUS_PREFIX) ) {
|
||||
val error = CONTROLLER_ERROR_PATTERN.matchEntire(line)?.groups?.get(1)?.value
|
||||
|
||||
if ( error != null )
|
||||
throw IOException("Controller: $error")
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
listener(ClashProcessStatus(ClashProcessStatus.STATUS_STOPPED))
|
||||
process = p
|
||||
pid = currentPid
|
||||
|
||||
listener(ClashProcessStatus.STATUS_STARTED)
|
||||
|
||||
Log.i(TAG, "Clash started pid = $pid")
|
||||
|
||||
thread {
|
||||
// Redirect stdout to log
|
||||
while ( reader.readLine()?.apply { line = this } != null ) {
|
||||
Log.i(TAG, line.trim())
|
||||
}
|
||||
|
||||
synchronized(this@ClashProcess) {
|
||||
p.destroy()
|
||||
process = null
|
||||
pid = -1
|
||||
}
|
||||
|
||||
listener(ClashProcessStatus.STATUS_STOPPED)
|
||||
}
|
||||
}
|
||||
catch (e: Exception) {
|
||||
listener(ClashProcessStatus.STATUS_STOPPED)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,8 +5,10 @@ import android.os.Parcelable
|
||||
|
||||
data class ClashProcessStatus(val status: Int) : Parcelable {
|
||||
companion object {
|
||||
const val STATUS_STOPPED = 0
|
||||
const val STATUS_STARTED = 1
|
||||
const val STATUS_STOPPED_INT = 0
|
||||
const val STATUS_STARTED_INT = 1
|
||||
val STATUS_STOPPED = ClashProcessStatus(STATUS_STOPPED_INT)
|
||||
val STATUS_STARTED = ClashProcessStatus(STATUS_STARTED_INT)
|
||||
|
||||
@JvmField
|
||||
val CREATOR = object: Parcelable.Creator<ClashProcessStatus> {
|
||||
|
||||
@@ -36,7 +36,6 @@ dependencies {
|
||||
implementation project(":core")
|
||||
implementation "androidx.lifecycle:lifecycle-livedata:2.1.0"
|
||||
implementation "androidx.lifecycle:lifecycle-common-java8:2.1.0"
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
|
||||
implementation "androidx.core:core-ktx:1.1.0"
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
|
||||
implementation "androidx.room:room-runtime:$room_version"
|
||||
}
|
||||
@@ -1,10 +1,6 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.github.kr328.clash.service">
|
||||
<application>
|
||||
<activity
|
||||
android:name=".VpnRequestActivity"
|
||||
android:exported="false"
|
||||
android:theme="@android:style/Theme.NoDisplay" />
|
||||
<service
|
||||
android:name="ClashService"
|
||||
android:exported="false"
|
||||
|
||||
@@ -3,6 +3,7 @@ package com.github.kr328.clash.service
|
||||
import android.app.Service
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Handler
|
||||
import android.os.IBinder
|
||||
import android.os.ParcelFileDescriptor
|
||||
import android.os.RemoteException
|
||||
@@ -19,11 +20,12 @@ class ClashService : Service() {
|
||||
private val TAG = "ClashForAndroid"
|
||||
}
|
||||
|
||||
private val handler = Handler()
|
||||
private val executor = Executors.newSingleThreadExecutor()
|
||||
private lateinit var clash: Clash
|
||||
private var lastProfile: String? = null
|
||||
|
||||
private var status = ClashProcessStatus(ClashProcessStatus.STATUS_STOPPED)
|
||||
private var status = ClashProcessStatus(ClashProcessStatus.STATUS_STOPPED_INT)
|
||||
set(value) {
|
||||
field = value
|
||||
|
||||
@@ -38,6 +40,8 @@ class ClashService : Service() {
|
||||
private val defaultProfileObserver = object: Observer<String?> {
|
||||
override fun onChanged(url: String?) {
|
||||
executor.submit {
|
||||
Log.i(TAG, "Loading profile $url")
|
||||
|
||||
if (url == null)
|
||||
clash.stop()
|
||||
if (url == lastProfile)
|
||||
@@ -55,7 +59,7 @@ class ClashService : Service() {
|
||||
lastProfile = null
|
||||
ClashDatabase.getInstance(this@ClashService)
|
||||
.openClashProfileDao()
|
||||
.setDefaultProfile(p ?: "")
|
||||
.setSelectedProfile(p ?: "")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -97,6 +101,8 @@ class ClashService : Service() {
|
||||
try {
|
||||
clash.start()
|
||||
} catch (e: IOException) {
|
||||
Log.e(TAG, "Start failure", e)
|
||||
|
||||
throw RemoteException(e.message)
|
||||
}
|
||||
}
|
||||
@@ -128,12 +134,19 @@ class ClashService : Service() {
|
||||
) {
|
||||
this.status = it
|
||||
|
||||
when (it.status) {
|
||||
ClashProcessStatus.STATUS_STARTED ->
|
||||
ClashDatabase.getInstance(this)
|
||||
.openClashProfileDao()
|
||||
.observeDefaultProfileUrl()
|
||||
.observeForever(defaultProfileObserver)
|
||||
handler.post {
|
||||
when (it.status) {
|
||||
ClashProcessStatus.STATUS_STARTED_INT ->
|
||||
ClashDatabase.getInstance(this)
|
||||
.openClashProfileDao()
|
||||
.observeDefaultProfileUrl()
|
||||
.observeForever(defaultProfileObserver)
|
||||
ClashProcessStatus.STATUS_STOPPED_INT ->
|
||||
ClashDatabase.getInstance(this)
|
||||
.openClashProfileDao()
|
||||
.observeDefaultProfileUrl()
|
||||
.removeObserver(defaultProfileObserver)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -154,11 +167,6 @@ class ClashService : Service() {
|
||||
clash.stop()
|
||||
executor.shutdown()
|
||||
|
||||
ClashDatabase.getInstance(this)
|
||||
.openClashProfileDao()
|
||||
.observeDefaultProfileUrl()
|
||||
.removeObserver(defaultProfileObserver)
|
||||
|
||||
super.onDestroy()
|
||||
}
|
||||
}
|
||||
@@ -20,7 +20,7 @@ class TunService : VpnService() {
|
||||
private const val PRIVATE_VLAN6_ROUTER = "fdfe:dcba:9876::2"
|
||||
}
|
||||
|
||||
private lateinit var fileDescriptor: ParcelFileDescriptor
|
||||
private var fileDescriptor: ParcelFileDescriptor? = null
|
||||
private var clash: IClashService? = null
|
||||
private val connection = object: ServiceConnection {
|
||||
override fun onServiceDisconnected(name: ComponentName?) {
|
||||
@@ -40,9 +40,9 @@ class TunService : VpnService() {
|
||||
return
|
||||
|
||||
when ( status.status ) {
|
||||
ClashProcessStatus.STATUS_STOPPED ->
|
||||
ClashProcessStatus.STATUS_STOPPED_INT ->
|
||||
stopSelf()
|
||||
ClashProcessStatus.STATUS_STARTED ->
|
||||
ClashProcessStatus.STATUS_STARTED_INT ->
|
||||
clash.startTunDevice(fileDescriptor, VPN_MTU)
|
||||
}
|
||||
}
|
||||
@@ -57,7 +57,6 @@ class TunService : VpnService() {
|
||||
super.onCreate()
|
||||
|
||||
if ( prepare(this) != null ) {
|
||||
startActivity(Intent(this, VpnRequestActivity::class.java))
|
||||
stopSelf()
|
||||
return
|
||||
}
|
||||
@@ -84,7 +83,7 @@ class TunService : VpnService() {
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
fileDescriptor.close()
|
||||
fileDescriptor?.close()
|
||||
|
||||
clash?.stopTunDevice()
|
||||
clash?.stop()
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
package com.github.kr328.clash.service
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Intent
|
||||
import android.net.VpnService
|
||||
import android.os.Bundle
|
||||
|
||||
class VpnRequestActivity : Activity() {
|
||||
companion object {
|
||||
const val REQUEST_CODE = 124
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
val intent = VpnService.prepare(this)
|
||||
|
||||
if ( intent == null )
|
||||
finish()
|
||||
else
|
||||
startActivityForResult(intent, REQUEST_CODE)
|
||||
}
|
||||
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
if ( requestCode == REQUEST_CODE && resultCode == RESULT_OK ) {
|
||||
startService(Intent(this, TunService::class.java))
|
||||
}
|
||||
else {
|
||||
super.onActivityResult(requestCode, resultCode, data)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,9 +6,9 @@ import androidx.room.Query
|
||||
|
||||
@Dao
|
||||
interface ClashProfileDao {
|
||||
@Query("UPDATE profiles SET `default` = CASE WHEN url = :url THEN 1 ELSE 0 END")
|
||||
fun setDefaultProfile(url: String)
|
||||
@Query("UPDATE profiles SET selected = CASE WHEN url = :url THEN 1 ELSE 0 END")
|
||||
fun setSelectedProfile(url: String)
|
||||
|
||||
@Query("SELECT url FROM profiles WHERE `default` = 1 LIMIT 1")
|
||||
@Query("SELECT url FROM profiles WHERE selected = 1 LIMIT 1")
|
||||
fun observeDefaultProfileUrl(): LiveData<String?>
|
||||
}
|
||||
@@ -1,10 +1,42 @@
|
||||
package com.github.kr328.clash.service.data
|
||||
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Entity
|
||||
import androidx.room.*
|
||||
|
||||
@Entity(tableName = "profiles", primaryKeys = ["url"])
|
||||
data class ClashProfileEntity(@ColumnInfo(name = "url") val url: String,
|
||||
@ColumnInfo(name = "default") val defaultProfile: Boolean,
|
||||
@ColumnInfo(name = "writable") val writable: Boolean,
|
||||
@ColumnInfo(name = "last_update") val lastUpdate: Long)
|
||||
@Entity(tableName = "profiles")
|
||||
@TypeConverters(ClashProfileEntity.Converters::class)
|
||||
data class ClashProfileEntity(
|
||||
@ColumnInfo(name = "name") val name: String,
|
||||
@ColumnInfo(name = "url") val url: String,
|
||||
@ColumnInfo(name = "selected") val selected: Boolean,
|
||||
@ColumnInfo(name = "type") val writable: Type,
|
||||
@ColumnInfo(name = "last_update") val lastUpdate: Long,
|
||||
@ColumnInfo(name = "id") @PrimaryKey(autoGenerate = true) val id: Int = 0) {
|
||||
|
||||
enum class Type {
|
||||
UNKNOWN, URL, FILE
|
||||
}
|
||||
|
||||
class Converters {
|
||||
@TypeConverter
|
||||
fun typeToInt(type: Type?): Int? {
|
||||
return type?.run {
|
||||
when ( this ) {
|
||||
Type.URL -> 1
|
||||
Type.FILE -> 2
|
||||
Type.UNKNOWN -> 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
fun intToType(i: Int?): Type? {
|
||||
return i?.run {
|
||||
when (this) {
|
||||
1 -> Type.URL
|
||||
2 -> Type.FILE
|
||||
else -> Type.UNKNOWN
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user