Compare commits

..

104 Commits

Author SHA1 Message Date
Kr328
1d21368385 Fix database migrations 2020-04-15 13:16:50 +08:00
Kr328
2a5865397a fix DatabaseMigrations.kt 2020-04-15 12:25:28 +08:00
Kr328
f5f378f3b0 update clash core 2020-04-15 11:14:37 +08:00
Kr328
861d5fa871 Update tun2socket 2020-04-15 11:07:02 +08:00
Kr328
4518825aca update tun2socket & update build tools 2020-04-15 01:29:50 +08:00
Kr328
260ebc83ff fix access control & remove context support in fetch.go 2020-04-14 23:44:41 +08:00
Kr328
640cf3e75c add scroll view to support activity 2020-04-14 20:25:13 +08:00
Kr328
7466990e0c update version 1.1.11 -> 1.2.0 2020-04-14 20:07:07 +08:00
Kr328
b41715686c cleanup code & rename util -> utils 2020-04-14 19:50:56 +08:00
Kr328
bca2a7a520 decrease waiting time 2020-04-14 19:47:32 +08:00
Kr328
545c43fe2c fix external profile 2020-04-14 19:47:16 +08:00
Kr328
eaf16318de fix external intent lost & replace application observer 2020-04-14 19:17:24 +08:00
Kr328
a809f94a02 add profile notification intent & detect is modified 2020-04-14 18:47:43 +08:00
Kr328
541862cda8 change routing system traffic string 2020-04-14 14:28:54 +08:00
Kr328
7ab2df9afe fix profile reload detect 2020-04-14 14:25:18 +08:00
Kr328
c45c4a1ad8 fix profile edit 2020-04-14 14:18:41 +08:00
Kr328
2b71f9a9f7 remove channels on migration 2020-04-14 13:57:28 +08:00
Kr328
3db2e1c618 fix profile edit & cleanup code 2020-04-14 12:25:20 +08:00
Kr328
d24617d842 add clone profile detect 2020-04-14 11:03:30 +08:00
Kr328
29dc8dc492 fix as Entity.Profile 2020-04-14 10:48:29 +08:00
Kr328
57b8ce3d7f rename ProfileMetadata.kt -> Profile.kt 2020-04-14 10:47:14 +08:00
Kr328
1536c9f056 fix auto update 2020-04-14 10:45:55 +08:00
Kr328
c2280555d3 fix dns hijacking 2020-04-14 02:17:07 +08:00
Kr328
532760e71a add loading notification 2020-04-14 02:16:58 +08:00
Kr328
6da7eff62c fix reload logic 2020-04-14 02:07:19 +08:00
Kr328
a75192b6bf improve code by Inspect Code 2020-04-14 01:23:15 +08:00
Kr328
f4ceebb12c rename ipc -> transact 2020-04-14 01:03:29 +08:00
Kr328
151327e9ba add waiting status for profile service 2020-04-14 00:58:07 +08:00
Kr328
a343678bad fix auto restart 2020-04-14 00:55:24 +08:00
Kr328
1b2c97d381 bug fix 2020-04-13 23:48:40 +08:00
Kr328
d034eb6f4b remove unused resources 2020-04-13 16:35:09 +08:00
Kr328
08dc165450 rename makeSnackbar & remove modified detect 2020-04-13 16:28:59 +08:00
Kr328
50eef54ab6 update support activity 2020-04-13 16:28:13 +08:00
Kr328
a75bdd457f profile ui refactored 2020-04-13 14:59:24 +08:00
Kr328
d5d219789f add clone support 2020-04-13 12:15:37 +08:00
Kr328
04eed1a768 broadcast with permission 2020-04-13 12:00:48 +08:00
Kr328
87d17bcb9a remove providers only on new profile 2020-04-13 11:58:01 +08:00
Kr328
2d3bd5c5cf add temp acquire 2020-04-13 11:45:05 +08:00
Kr328
12d76a2ad1 cleanup code 2020-04-13 11:13:04 +08:00
Kr328
a0026b1bb3 add timeout to profile processor 2020-04-13 11:12:45 +08:00
Kr328
761f11fa5f move utils to common 2020-04-13 11:12:28 +08:00
Kr328
a7b28692ea remove sync map 2020-04-13 10:48:05 +08:00
Kr328
daac6e1e5c service refactored 2020-04-13 10:46:02 +08:00
Kr328
8c9b7e4a01 decrease mtu 2020-04-11 19:49:20 +08:00
Kr328
1b0778141c update README.md 2020-04-10 14:35:27 +08:00
Kr328
96ed63b704 update README.md 2020-04-10 14:34:32 +08:00
Kr328
bb07bc639d update README.md 2020-04-10 13:49:25 +08:00
Kr328
c3eb4b2f51 update clash core 2020-04-10 13:45:51 +08:00
Kr328
9deeb37f21 add auto signing with properties 2020-04-10 13:45:16 +08:00
Kr328
0befdc0d34 clean up code 2020-04-09 23:19:59 +08:00
Kr328
606a21dff0 add documents provider 2020-04-09 14:39:57 +08:00
Kr328
ef6a37fb7c remove default network detect logs 2020-04-08 16:23:37 +08:00
Kr328
2f7b8c4b59 fix default network detect 2020-04-08 16:21:22 +08:00
Kr328
968e82b072 move load.go init to patch.go 2020-04-08 16:14:09 +08:00
Kr328
0240eea776 android: add fake pool host override 2020-04-08 11:13:40 +08:00
Kr328
dc8d94fa31 remove ipv6 for VpnService 2020-04-08 11:11:38 +08:00
Kr328
f6ec8cc882 update tun2socket & remove ipv6 for VpnService 2020-04-08 11:10:13 +08:00
Kr328
b648d21068 use ServiceStatusProvider instead of requery database 2020-04-08 02:05:05 +08:00
Kr328
2b63997b22 cleanup code 2020-04-08 01:58:14 +08:00
Kr328
198daaf720 add restore selected proxy 2020-04-08 01:57:50 +08:00
Kr328
a1847dc6f2 update core & broadcast on loaded & stop on load failure 2020-04-08 01:42:21 +08:00
Kr328
8ebb3a5f31 fix dns hijack not working 2020-04-08 01:00:06 +08:00
Kr328
7cf3dc2bf2 clean up code 2020-04-08 00:44:28 +08:00
Kr328
2346095323 increase mtu 2020-04-08 00:43:47 +08:00
Kr328
40ae2d456c rename module component -> common 2020-04-08 00:42:41 +08:00
Kr328
3152bcaaa6 refactor clash core context 2020-04-08 00:42:22 +08:00
Kr328
c69c41c57f refactor clash core context 2020-04-03 01:08:04 +08:00
Kr328
ea1f424b33 refactor clash core context 2020-04-02 00:44:26 +08:00
Kr328
1ef828497c remove gc period 2020-04-01 14:45:53 +08:00
Kr328
88b28faa48 add component module & clean up code 2020-04-01 14:45:06 +08:00
Kr328
7bfaacf936 call gc period to decrease ram usage 2020-04-01 01:12:50 +08:00
Kr328
05d8dae4e0 fix notification not show profile 2020-04-01 00:52:36 +08:00
Kr328
27cd92b4b1 recycle dns udp packet 2020-03-31 23:21:02 +08:00
Kr328
4b6b5b5d95 update core 2020-03-30 16:51:39 +08:00
Kr328
6468575a42 fix fake ip filter not working 2020-03-30 16:38:33 +08:00
Kr328
5ea22cecad update tun2socket & fix crash on tcp dns query 2020-03-30 13:09:36 +08:00
Kr328
c8205d3f95 update tun2socket 2020-03-29 20:14:45 +08:00
Kr328
639377760c update tun2socket 2020-03-29 15:25:39 +08:00
Kr328
a9323f0528 update tun2socket & core & mtu 2020-03-28 23:41:52 +08:00
Kr328
2e31e90225 update core 2020-03-28 14:49:11 +08:00
Kr328
9d5b8188eb fix patch bug 2020-03-28 14:39:28 +08:00
Kr328
1f9e330f6a fix patch bug 2020-03-28 13:56:54 +08:00
Kr328
a69776fe33 update clash core 2020-03-28 12:31:48 +08:00
Kr328
d42dc46b8c use tun2socket instead of netstack 2020-03-28 12:30:26 +08:00
Kr328
c37dc2f874 fix delete geoip database on rebuild 2020-03-28 12:29:28 +08:00
Kr328
9c20f13f95 update dependencies 2020-03-27 23:59:33 +08:00
Kr328
88aec66ef8 Merge branch 'master' into dev 2020-03-21 16:13:55 +08:00
Kr328
f500596621 update README.md 2020-03-21 16:11:03 +08:00
Kr328
df5bafd0bb update README.md 2020-03-21 16:01:35 +08:00
Kr328
01fe9deb20 update issue template 2020-03-20 13:16:43 +08:00
Kr328
4e44298e98 sorted github issue 2020-03-19 19:46:12 +08:00
Kr328
a7c3a05c23 add application info request 2020-03-19 19:09:20 +08:00
Kr328
05fa36497b rename issues template file 2020-03-19 18:56:41 +08:00
Kr328
e6f8ae265e Add issue templates 2020-03-19 18:53:05 +08:00
Kr328
d107847747 remove unused General.Mode serializer 2020-03-19 15:21:20 +08:00
Kr328
ea8e270df6 add auto activity name detect 2020-03-19 15:16:50 +08:00
Kr328
4bc40e0663 update dependencies 2020-03-19 15:16:39 +08:00
Kr328
5e6ccd990a update dependencies 2020-03-19 12:29:34 +08:00
Kr328
1a8d673742 update clash core 2020-03-03 22:48:54 +08:00
Kr328
a4069aa6f6 remove unused var 2020-03-03 22:01:07 +08:00
Kr328
3cfb90c078 use kotlin build script 2020-03-03 21:50:30 +08:00
Kr328
594949d3f0 format code & update core 2020-02-28 23:52:58 +08:00
Kr328
8fd34b5258 improve apk broken detect & fix udp crash 2020-02-28 14:00:38 +08:00
Kr328
1161482a5b fix udp crash 2020-02-28 11:19:06 +08:00
184 changed files with 4841 additions and 3420 deletions

View File

@@ -0,0 +1,43 @@
---
name: "[English] Bug report"
about: Create a report to help us improve
title: "[BUG] "
labels: ''
assignees: ''
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Logs**
if applicable, add logs to help detect problem
**Device Info (please complete the following information):**
- Device: [e.g. Pixel 4]
- ROM: [e.g: AOSP]
- ROM Version:
- Android Version [e.g. Oreo]
**Application Info (please complete the following information):**
- Version: [e.g. 1.1.10]
- Apk File Name: [e.g. app-release-arm64-v8a.apk]
- Distribution Channel: [e.g. Google Play]
**Additional context**
Add any other context about the problem here.

View File

@@ -0,0 +1,20 @@
---
name: "[English] Feature request"
about: Suggest an idea for this app
title: "[Feature Request] "
labels: ''
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.

View File

@@ -0,0 +1,42 @@
---
name: "[简体中文] 创建错误报告"
about: 创建错误报告以帮助我们改进应用
title: "[BUG] "
labels: ''
assignees: ''
---
**描述出现的错误**
请简洁的描述你遇到的错误
**如何复现该错误**
复现步骤:
1. ...
2. ...
3. ...
4. ...
**预期行为**
清晰简单的描述你预期的应用应该表现的行为
**屏幕截图**
如果适用, 上传屏幕截图以帮助描述错误
**日志**
如果适用, 上传日志以帮助侦测错误
**设备信息 (请完成一下信息):**
- 机型: [例如: Pixel 4]
- 系统/ROM: [例如: MIUI 11]
- Android 版本 [例如: Oreo]
- ROM版本 [例如: 20.3.19]
**应用信息**
- 版本: [例如: 1.1.10]
- 安装包文件名: [例如: app-release-arm64-v8a.apk]
- 应用来源: [例如: Google Play]
**附加信息**
其他的可能与改错误相关的信息

View File

@@ -0,0 +1,17 @@
---
name: "[简体中文] 功能请求"
about: 你希望的能够在应用中增加的功能
title: "[Feature Request] "
labels: ''
assignees: ''
---
**功能描述**
请清晰的描述你想要的功能
**描述你希望的实现方式**
清晰的描述应用应该如何实现该功能
**附加信息**
其他的与改功能相关的附加信息

3
.gitignore vendored
View File

@@ -23,9 +23,6 @@ gradle-app.setting
*.keystore
*.jks
# gradle
.gradle
# clion cmake build
cmake-build-*

View File

@@ -1,10 +1,8 @@
## Clash for Android
A GUI for [clash](https://github.com/Dreamacro/clash) on Android
> NOTICE: Early testing currently
A Graphical user interface of [clash](https://github.com/Dreamacro/clash) for Android
<a href="https://play.google.com/store/apps/details?id=com.github.kr328.clash"><img width="200px" alt="Get it on Google Play" src="https://play.google.com/intl/en_us/badges/static/images/badges/en_badge_web_generic.png"/></a> or [Releases](https://github.com/Kr328/ClashForAndroid/releases)
### Feature
@@ -15,9 +13,7 @@ Fully feature of [clash](https://github.com/Dreamacro/clash) ~~(Exclude `externa
### Requirement
* Android 7.0+
* `arm64` or `x86_64` architecture
* `armeabi-v7a` , `arm64-v8a`, `x86` or `x86_64` Architecture
### License
@@ -39,9 +35,9 @@ See also [PRIVACY_POLICY.md](./PRIVACY_POLICY.md)
git submodule update --init --recursive
```
2. Install `Android SDK (include JDK)` ,`Android NDK` and `Golang`
2. Install `JDK 1.8`, `Android SDK` ,`Android NDK` and `Golang`
3. Configure `local.properties`
3. Create `local.properties` in project root with
```properties
sdk.dir=/path/to/android-sdk
@@ -49,18 +45,19 @@ See also [PRIVACY_POLICY.md](./PRIVACY_POLICY.md)
appcenter.key=<AppCenter Key> # Optional, from "appcenter.ms"
```
4. Build
4. Create `keystore.properties` in project root with
on Linux
```properties
storeFile=/path/to/keystore/file
storePassword=<key store password>
keyAlias=<key alias>
keyPassword=<key password>
```
5. Build
```bash
./gradlew build
./gradlew app:assembleRelease
```
on Windows
```bash
.\gradlew.bat build
```
6. Pick `app-release-<arch>.apk` in `app/build/outputs/apks`

View File

@@ -1,76 +0,0 @@
apply plugin: 'com.android.application'
apply plugin: 'kotlinx-serialization'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
apply plugin: 'kotlin-kapt'
android {
compileSdkVersion gCompileSdkVersion
buildToolsVersion gBuildToolsVersion
defaultConfig {
applicationId "com.github.kr328.clash"
minSdkVersion gMinSdkVersion
targetSdkVersion gTargetSdkVersion
versionCode gVersionCode
versionName gVersionName
}
buildTypes {
release {
minifyEnabled true
shrinkResources true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility = 1.8
targetCompatibility = 1.8
}
kotlinOptions {
jvmTarget = "1.8"
}
splits {
abi {
enable true
universalApk true
}
}
}
dependencies {
kapt "androidx.room:room-compiler:$gRoomVersion"
implementation project(":core")
implementation project(":service")
implementation project(":design")
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$gKotlinVersion"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$gKotlinCoroutineVersion"
implementation "androidx.lifecycle:lifecycle-extensions:$gLifecycleVersion"
implementation "androidx.lifecycle:lifecycle-common-java8:$gLifecycleVersion"
implementation "androidx.recyclerview:recyclerview:$gRecyclerviewVersion"
implementation "androidx.core:core-ktx:$gAndroidKtxVersion"
implementation "androidx.appcompat:appcompat:$gAppCompatVersion"
implementation "androidx.room:room-runtime:$gRoomVersion"
implementation "org.jetbrains.kotlinx:kotlinx-serialization-runtime:$gKotlinSerializationVersion"
implementation "com.google.android.material:material:$gMaterialDesignVersion"
implementation "moe.shizuku.preference:preference-appcompat:$gShizukuPreferenceVersion"
implementation "moe.shizuku.preference:preference-simplemenu-appcompat:$gShizukuPreferenceVersion"
implementation "com.microsoft.appcenter:appcenter-analytics:$gAppCenterVersion"
implementation "com.microsoft.appcenter:appcenter-crashes:$gAppCenterVersion"
}
task injectAppCenterKey() {
doFirst {
Properties properties = new Properties()
properties.load(rootProject.file('local.properties').newDataInputStream())
def key = properties.getProperty("appcenter.key", "")
android.buildTypes.each {
it.buildConfigField 'String', 'APP_CENTER_KEY', "\"$key\""
}
}
}
afterEvaluate {
preBuild.dependsOn(injectAppCenterKey)
}

126
app/build.gradle.kts Normal file
View File

@@ -0,0 +1,126 @@
import java.util.*
plugins {
id("com.android.application")
id("kotlin-android")
id("kotlin-android-extensions")
}
val rootExtra = rootProject.extra
val gCompileSdkVersion: Int by rootExtra
val gBuildToolsVersion: String by rootExtra
val gMinSdkVersion: Int by rootExtra
val gTargetSdkVersion: Int by rootExtra
val gVersionCode: Int by rootExtra
val gVersionName: String by rootExtra
val gKotlinVersion: String by rootExtra
val gKotlinCoroutineVersion: String by rootExtra
val gAppCenterVersion: String by rootExtra
val gAndroidKtxVersion: String by rootExtra
val gRecyclerviewVersion: String by rootExtra
val gAppCompatVersion: String by rootExtra
val gMaterialDesignVersion: String by rootExtra
val gShizukuPreferenceVersion: String by rootExtra
val gMultiprocessPreferenceVersion: String by rootExtra
android {
compileSdkVersion(gCompileSdkVersion)
buildToolsVersion(gBuildToolsVersion)
defaultConfig {
applicationId = "com.github.kr328.clash"
minSdkVersion(gMinSdkVersion)
targetSdkVersion(gTargetSdkVersion)
versionCode = gVersionCode
versionName = gVersionName
}
buildTypes {
maybeCreate("release").apply {
isMinifyEnabled = true
isShrinkResources = true
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = "1.8"
}
splits {
abi {
isEnable = true
isUniversalApk = true
}
}
val signingFile = rootProject.file("keystore.properties")
if ( signingFile.exists() ) {
val properties = Properties().apply {
signingFile.inputStream().use {
load(it)
}
}
signingConfigs {
maybeCreate("release").apply {
storeFile = rootProject.file(Objects.requireNonNull(properties.getProperty("storeFile")))
storePassword = Objects.requireNonNull(properties.getProperty("storePassword"))
keyAlias = Objects.requireNonNull(properties.getProperty("keyAlias"))
keyPassword = Objects.requireNonNull(properties.getProperty("keyPassword"))
}
}
buildTypes {
maybeCreate("release").apply {
this.signingConfig = signingConfigs.findByName("release")
}
}
}
}
dependencies {
implementation(project(":core"))
implementation(project(":service"))
implementation(project(":design"))
implementation(project(":common"))
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8:$gKotlinVersion")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$gKotlinCoroutineVersion")
implementation("androidx.recyclerview:recyclerview:$gRecyclerviewVersion")
implementation("androidx.core:core-ktx:$gAndroidKtxVersion")
implementation("androidx.appcompat:appcompat:$gAppCompatVersion")
implementation("com.google.android.material:material:$gMaterialDesignVersion")
implementation("moe.shizuku.preference:preference-appcompat:$gShizukuPreferenceVersion")
implementation("moe.shizuku.preference:preference-simplemenu-appcompat:$gShizukuPreferenceVersion")
implementation("com.microsoft.appcenter:appcenter-analytics:$gAppCenterVersion")
implementation("com.microsoft.appcenter:appcenter-crashes:$gAppCenterVersion")
}
task("injectAppCenterKey") {
doFirst {
val properties = Properties().apply {
rootProject.file("local.properties").inputStream().use {
load(it)
}
}
val key = properties.getProperty("appcenter.key", "")
android.buildTypes.forEach {
it.buildConfigField("String", "APP_CENTER_KEY", "\"$key\"")
}
}
}
afterEvaluate {
tasks["preBuild"].dependsOn(tasks["injectAppCenterKey"])
}

View File

@@ -109,12 +109,5 @@
<action android:name="android.service.quicksettings.action.QS_TILE" />
</intent-filter>
</service>
<receiver
android:name=".OnBootReceiver"
android:enabled="false">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
</intent-filter>
</receiver>
</application>
</manifest>

View File

@@ -65,7 +65,4 @@ class ApkBrokenActivity : BaseActivity() {
override fun shouldDisplayHomeAsUpEnabled(): Boolean {
return false
}
override val activityLabel: CharSequence
get() = getText(R.string.application_broken)
}

View File

@@ -13,9 +13,9 @@ import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.app.AppCompatDelegate
import androidx.appcompat.widget.Toolbar
import androidx.coordinatorlayout.widget.CoordinatorLayout
import com.github.kr328.clash.common.utils.createLanguageConfigurationContext
import com.github.kr328.clash.preference.UiSettings
import com.github.kr328.clash.remote.Broadcasts
import com.github.kr328.clash.service.util.createLanguageConfigurationContext
import com.google.android.material.snackbar.Snackbar
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.MainScope
@@ -109,6 +109,8 @@ abstract class BaseActivity : AppCompatActivity(), CoroutineScope by MainScope()
resetDarkMode()
resetLightNavigationBar()
title = resolveActivityTitle()
}
override fun onStart() {
@@ -148,10 +150,6 @@ abstract class BaseActivity : AppCompatActivity(), CoroutineScope by MainScope()
supportActionBar?.apply {
setDisplayHomeAsUpEnabled(shouldDisplayHomeAsUpEnabled())
activityLabel?.let {
title = it
}
}
}
@@ -159,8 +157,6 @@ abstract class BaseActivity : AppCompatActivity(), CoroutineScope by MainScope()
return true
}
abstract val activityLabel: CharSequence?
override fun onSupportNavigateUp(): Boolean {
this.onBackPressed()
@@ -179,7 +175,7 @@ abstract class BaseActivity : AppCompatActivity(), CoroutineScope by MainScope()
recreate()
}
protected fun makeSnackbarException(title: String, detail: String?) {
protected fun showSnackbarException(title: String, detail: String?) {
Snackbar.make(rootView, title, Snackbar.LENGTH_LONG).setAction(R.string.detail) {
AlertDialog.Builder(this).setTitle(R.string.detail).setMessage(detail ?: "Unknown")
.show()
@@ -213,4 +209,13 @@ abstract class BaseActivity : AppCompatActivity(), CoroutineScope by MainScope()
window.navigationBarColor = getColor(R.color.backgroundColor)
}
private fun resolveActivityTitle(): CharSequence {
val info = packageManager.getActivityInfo(componentName, 0)
if (info.labelRes <= 0)
return title
return resources.getText(info.labelRes)
}
}

View File

@@ -6,11 +6,6 @@ object Constants {
const val LOG_DIR_NAME = "logs"
const val URL_PROVIDER_TYPE_FILE = "file"
const val URL_PROVIDER_TYPE_URL = "url"
const val URL_PROVIDER_TYPE_EXTERNAL = "external"
const val URL_PROVIDER_INTENT_ACTION = "com.github.kr328.clash.action.PROVIDE_URL"
const val URL_PROVIDER_INTENT_EXTRA_NAME = "name"
}

View File

@@ -13,7 +13,9 @@ import android.view.View
import android.view.ViewGroup
import android.widget.BaseAdapter
import android.widget.TextView
import com.github.kr328.clash.service.util.intent
import com.github.kr328.clash.common.utils.intent
import com.github.kr328.clash.remote.withProfile
import com.github.kr328.clash.service.model.Profile.Type
import kotlinx.android.synthetic.main.activity_create_profile.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@@ -24,6 +26,8 @@ class CreateProfileActivity : BaseActivity() {
const val REQUEST_CODE = 20000
}
private val self = this
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@@ -41,12 +45,22 @@ class CreateProfileActivity : BaseActivity() {
mainList.setOnItemClickListener { _, _, position, _ ->
val item = providers[position]
startActivityForResult(
ProfileEditActivity::class.intent
.putExtra("type", item.type)
.putExtra("intent", item.intent),
REQUEST_CODE
)
self.launch {
val id = withProfile {
acquireUnused(item.type, item.intent?.toUri(0))
}
startActivityForResult(
ProfileEditActivity::class.intent.setData(
Uri.fromParts(
"id",
id.toString(),
null
)
),
REQUEST_CODE
)
}
}
mainList.setOnItemLongClickListener { _, _, position, _ ->
val item = providers[position]
@@ -66,9 +80,6 @@ class CreateProfileActivity : BaseActivity() {
}
}
override val activityLabel: CharSequence
get() = getText(R.string.create_profile)
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
if (requestCode == REQUEST_CODE && resultCode == Activity.RESULT_OK)
return finish()
@@ -83,14 +94,14 @@ class CreateProfileActivity : BaseActivity() {
getText(R.string.file),
getText(R.string.import_from_file),
getDrawable(R.drawable.ic_file)!!,
Constants.URL_PROVIDER_TYPE_FILE,
Type.FILE,
null
),
UrlProvider(
getText(R.string.url),
getText(R.string.import_from_url),
getDrawable(R.drawable.ic_download)!!,
Constants.URL_PROVIDER_TYPE_URL,
Type.URL,
null
)
)
@@ -104,7 +115,7 @@ class CreateProfileActivity : BaseActivity() {
val name = activity.applicationInfo.loadLabel(packageManager)
val summary = activity.loadLabel(packageManager)
val icon = activity.loadIcon(packageManager)
val type = Constants.URL_PROVIDER_TYPE_EXTERNAL
val type = Type.EXTERNAL
val intent = Intent(Constants.URL_PROVIDER_INTENT_ACTION)
.setComponent(
ComponentName.createRelative(
@@ -123,7 +134,7 @@ class CreateProfileActivity : BaseActivity() {
val name: CharSequence,
val summary: CharSequence,
val icon: Drawable,
val type: String,
val type: Type,
val intent: Intent?
)

View File

@@ -10,8 +10,8 @@ import androidx.core.net.toFile
import androidx.recyclerview.widget.LinearLayoutManager
import com.github.kr328.clash.adapter.LiveLogAdapter
import com.github.kr328.clash.adapter.LogAdapter
import com.github.kr328.clash.common.utils.intent
import com.github.kr328.clash.core.event.LogEvent
import com.github.kr328.clash.service.util.intent
import kotlinx.android.synthetic.main.activity_log_viewer.*
import kotlinx.coroutines.*
import kotlinx.coroutines.sync.Mutex
@@ -63,9 +63,6 @@ class LogViewerActivity : BaseActivity() {
}
}
override val activityLabel: CharSequence
get() = getText(R.string.log_viewer)
override fun onStart() {
super.onStart()
@@ -105,7 +102,7 @@ class LogViewerActivity : BaseActivity() {
.map { LogEvent(LogEvent.Level.valueOf(it[1]), it[2], it[0].toLong()) }
.toList()
} catch (e: Exception) {
makeSnackbarException(getString(R.string.open_log_failure), e.message)
showSnackbarException(getString(R.string.open_log_failure), e.message)
throw CancellationException()
}

View File

@@ -15,16 +15,16 @@ import android.os.IInterface
import androidx.collection.CircularArray
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import com.github.kr328.clash.common.utils.createLanguageConfigurationContext
import com.github.kr328.clash.common.utils.intent
import com.github.kr328.clash.common.utils.Log
import com.github.kr328.clash.core.event.LogEvent
import com.github.kr328.clash.core.utils.Log
import com.github.kr328.clash.model.LogFile
import com.github.kr328.clash.preference.UiSettings
import com.github.kr328.clash.service.ClashManagerService
import com.github.kr328.clash.service.IClashManager
import com.github.kr328.clash.service.ipc.IStreamCallback
import com.github.kr328.clash.service.ipc.ParcelableContainer
import com.github.kr328.clash.service.util.createLanguageConfigurationContext
import com.github.kr328.clash.service.util.intent
import com.github.kr328.clash.service.transact.IStreamCallback
import com.github.kr328.clash.service.transact.ParcelableContainer
import com.github.kr328.clash.utils.format
import com.github.kr328.clash.utils.logsDir
import kotlinx.coroutines.*

View File

@@ -11,11 +11,11 @@ import androidx.appcompat.app.AlertDialog
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.LinearLayoutManager
import com.github.kr328.clash.adapter.LogFileAdapter
import com.github.kr328.clash.common.utils.intent
import com.github.kr328.clash.common.utils.startForegroundServiceCompat
import com.github.kr328.clash.design.common.Category
import com.github.kr328.clash.design.view.CommonUiLayout
import com.github.kr328.clash.model.LogFile
import com.github.kr328.clash.service.util.intent
import com.github.kr328.clash.service.util.startForegroundServiceCompat
import com.github.kr328.clash.utils.format
import com.github.kr328.clash.utils.logsDir
import com.google.android.material.bottomsheet.BottomSheetDialog
@@ -92,9 +92,6 @@ class LogsActivity : BaseActivity() {
refreshList()
}
override val activityLabel: CharSequence
get() = getText(R.string.logs)
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
if (requestCode == REQUEST_CODE) {
if (resultCode == Activity.RESULT_OK) {
@@ -186,6 +183,7 @@ class LogsActivity : BaseActivity() {
ViewGroup.LayoutParams.WRAP_CONTENT
)
}
@ColorInt
val errorColor = TypedValue().run {
theme.resolveAttribute(R.attr.colorError, this, true)

View File

@@ -8,13 +8,13 @@ import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.appcompat.app.AlertDialog
import com.github.kr328.clash.common.utils.intent
import com.github.kr328.clash.common.utils.asBytesString
import com.github.kr328.clash.core.model.General
import com.github.kr328.clash.core.utils.asBytesString
import com.github.kr328.clash.remote.withClash
import com.github.kr328.clash.remote.withProfile
import com.github.kr328.clash.service.util.intent
import com.github.kr328.clash.utils.startClashService
import com.github.kr328.clash.utils.stopClashService
import com.github.kr328.clash.service.util.startClashService
import com.github.kr328.clash.service.util.stopClashService
import kotlinx.android.synthetic.main.activity_main.*
import kotlinx.coroutines.*
@@ -37,11 +37,10 @@ class MainActivity : BaseActivity() {
val vpnRequest = startClashService()
if (vpnRequest != null) {
val resolved = packageManager.resolveActivity(vpnRequest, 0)
if ( resolved != null ) {
if (resolved != null) {
startActivityForResult(vpnRequest, REQUEST_CODE)
}
else {
makeSnackbarException(getString(R.string.missing_vpn_component), null)
} else {
showSnackbarException(getString(R.string.missing_vpn_component), null)
}
}
}
@@ -84,8 +83,6 @@ class MainActivity : BaseActivity() {
stopBandwidthPolling()
}
override val activityLabel: CharSequence? = null
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
if (requestCode == REQUEST_CODE) {
if (resultCode == Activity.RESULT_OK)
@@ -104,7 +101,7 @@ class MainActivity : BaseActivity() {
updateClashStatus()
if (reason != null)
makeSnackbarException(getString(R.string.clash_start_failure), reason)
showSnackbarException(getString(R.string.clash_start_failure), reason)
}
override suspend fun onClashProfileLoaded() {
@@ -162,7 +159,7 @@ class MainActivity : BaseActivity() {
queryGeneral()
}
val active = withProfile {
queryActiveProfile()
queryActive()
}
val modeResId = when (general.mode) {

View File

@@ -2,7 +2,10 @@ package com.github.kr328.clash
import android.app.Application
import android.content.Context
import com.github.kr328.clash.core.Global
import android.content.Intent
import android.net.Uri
import com.github.kr328.clash.common.Global
import com.github.kr328.clash.common.utils.componentName
import com.github.kr328.clash.dump.LogcatDumper
import com.github.kr328.clash.remote.Broadcasts
import com.github.kr328.clash.remote.Remote
@@ -48,6 +51,20 @@ class MainApplication : Application() {
})
}
Global.openMainIntent = {
Intent(Intent.ACTION_MAIN).apply {
component = MainActivity::class.componentName
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
}
}
Global.openProfileIntent = {
Intent(Intent.ACTION_MAIN).apply {
component = ProfileEditActivity::class.componentName
data = Uri.fromParts("id", it.toString(), null)
flags = Intent.FLAG_ACTIVITY_NEW_TASK
}
}
Remote.init(this)
Broadcasts.init(this)
}

View File

@@ -1,23 +0,0 @@
package com.github.kr328.clash
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import com.github.kr328.clash.service.Intents
import com.github.kr328.clash.service.ProfileBackgroundService
import com.github.kr328.clash.service.util.componentName
import com.github.kr328.clash.service.util.startForegroundServiceCompat
import com.github.kr328.clash.utils.startClashService
class OnBootReceiver : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
if (intent?.action != Intent.ACTION_BOOT_COMPLETED || context == null)
return
context.startClashService()
context.startForegroundServiceCompat(
Intent(Intents.INTENT_ACTION_PROFILE_SETUP)
.setComponent(ProfileBackgroundService::class.componentName)
)
}
}

View File

@@ -15,8 +15,6 @@ import kotlinx.coroutines.channels.Channel
import kotlin.streams.toList
class PackagesActivity : BaseActivity() {
override val activityLabel: CharSequence?
get() = getText(R.string.access_control_packages)
private val activity: PackagesActivity
get() = this
private val adapter: PackagesAdapter?

View File

@@ -1,37 +1,19 @@
package com.github.kr328.clash
import android.app.Activity
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.text.Html
import android.view.View
import android.webkit.MimeTypeMap
import androidx.appcompat.app.AlertDialog
import com.github.kr328.clash.core.utils.Log
import com.github.kr328.clash.design.common.TextInput
import com.github.kr328.clash.fragment.ProfileEditFragment
import com.github.kr328.clash.remote.withProfile
import com.github.kr328.clash.service.data.ClashProfileEntity
import com.github.kr328.clash.service.ipc.IStreamCallback
import com.github.kr328.clash.service.ipc.ParcelableContainer
import com.github.kr328.clash.service.transact.ProfileRequest
import com.github.kr328.clash.service.model.Profile
import com.google.android.material.snackbar.Snackbar
import kotlinx.android.synthetic.main.activity_profile_edit.*
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
class ProfileEditActivity : BaseActivity() {
companion object {
private const val REQUEST_CODE = 10000
private const val KEY_NAME = "name"
private const val KEY_URL = "url"
private const val KEY_AUTO_UPDATE = "auto_update"
private val TYPE_YAML = MimeTypeMap.getSingleton()
.getMimeTypeFromExtension("yaml") ?: "*/*"
}
private var modified = false
private var editor: ProfileEditFragment? = null
private var processing = false
set(value) {
field = value
@@ -45,170 +27,73 @@ class ProfileEditActivity : BaseActivity() {
}
}
private val requestCallback = object : IStreamCallback.Stub() {
override fun complete() {
launch {
setResult(Activity.RESULT_OK)
finish()
}
}
override fun completeExceptionally(reason: String?) {
launch {
makeSnackbarException(getString(R.string.download_failure), reason ?: "Unknown")
processing = false
}
}
override fun send(data: ParcelableContainer?) {}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_profile_edit)
setSupportActionBar(toolbar)
settings.build {
tips(icon = getDrawable(R.drawable.ic_info)) {
title = Html.fromHtml(getString(R.string.tips_profile), Html.FROM_HTML_MODE_LEGACY)
toolbar.setTitle(R.string.loading)
launch {
val id = intent.data?.schemeSpecificPart?.toLongOrNull() ?: return@launch finish()
val metadata = withProfile {
queryById(id)
} ?: return@launch finish()
when {
metadata.lastModified > 0 ->
toolbar.setTitle(R.string.edit_profile)
metadata.name.isBlank() ->
toolbar.setTitle(R.string.new_profile)
else ->
toolbar.setTitle(R.string.clone_profile)
}
textInput(
title = getString(R.string.name),
icon = getDrawable(R.drawable.ic_label_outline),
hint = getString(R.string.profile_name),
content = intent.getStringExtra("name") ?: "",
id = KEY_NAME
) {
onTextChanged {
modified = true
}
}
textInput(
title = getString(R.string.url),
icon = getDrawable(R.drawable.ic_content),
hint = getString(R.string.profile_url),
content = intent.getStringExtra("url") ?: "",
id = KEY_URL
) {
onOpenInput {
if (!openUrlProvider())
openDialogInput()
}
onDisplayContent {
it.split("/").last()
}
onTextChanged {
modified = true
}
}
textInput(
title = getString(R.string.auto_update),
icon = getDrawable(R.drawable.ic_update),
hint = getString(R.string.seconds),
id = KEY_AUTO_UPDATE,
content = intent.getStringExtra("interval") ?: ""
) {
onDisplayContent {
val interval = it.toString().toIntOrNull() ?: 0
val fragment = ProfileEditFragment(
metadata.id,
metadata.name, metadata.uri, metadata.interval,
metadata.type, metadata.source
)
if (interval <= 0)
getString(R.string.disabled)
else
getString(R.string.format_seconds, interval)
}
onTextChanged {
val s = it.toString()
editor = fragment
if (s.isNotEmpty() && s.toIntOrNull() == null) {
content = ""
Snackbar.make(rootView, R.string.invalid_interval, Snackbar.LENGTH_LONG)
.show()
} else {
modified = true
}
}
supportFragmentManager.beginTransaction()
.replace(R.id.fragment, fragment)
.commit()
if (intent.getStringExtra("type") == Constants.URL_PROVIDER_TYPE_FILE)
isHidden = true
}
}
settings.screen.restoreState(savedInstanceState)
save.setOnClickListener {
with(settings.screen) {
val name = requireElement<TextInput>(KEY_NAME).content.toString()
val url = Uri.parse(requireElement<TextInput>(KEY_URL).content.toString())
val interval = requireElement<TextInput>(KEY_AUTO_UPDATE).content.toString()
.toLongOrNull() ?: 0
save.setOnClickListener {
val name = fragment.name
val uri = fragment.uri
val interval = fragment.interval
if (name.isBlank()) {
Snackbar.make(rootView, R.string.empty_name, Snackbar.LENGTH_LONG).show()
return@setOnClickListener
}
if (url == null || url == Uri.EMPTY ||
(!url.scheme.equals("http", ignoreCase = true)
&& !url.scheme.equals("https", ignoreCase = true)
&& !url.scheme.equals("content", ignoreCase = true)
&& !url.scheme.equals("file", ignoreCase = true))
) {
Snackbar.make(rootView, R.string.invalid_url, Snackbar.LENGTH_LONG).show()
return@setOnClickListener
}
val newMetadata = metadata.copy(
name = name,
uri = uri,
interval = interval
)
processing = true
sendProfileRequest(name, url, interval)
commit(newMetadata)
}
}
when (intent.extras?.getLong("id", Long.MIN_VALUE)) {
Long.MIN_VALUE -> {
openUrlProvider()
setTitle(R.string.new_profile)
}
-1L -> {
setTitle(R.string.new_profile)
}
else -> {
setTitle(R.string.edit_profile)
}
}
}
override val activityLabel: CharSequence? = null
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
if (requestCode == REQUEST_CODE) {
if (resultCode != Activity.RESULT_OK || data == null)
return
data.data?.apply {
settings.screen.requireElement<TextInput>(KEY_URL).content = this.toString()
}
data.getStringExtra(Constants.URL_PROVIDER_INTENT_EXTRA_NAME)?.also {
settings.screen.requireElement<TextInput>(KEY_NAME).apply {
if (content.isBlank())
content = it
}
}
}
super.onActivityResult(requestCode, resultCode, data)
}
override fun onBackPressed() {
if (!modified)
return super.onBackPressed()
if (processing) {
Snackbar.make(rootView, R.string.processing, Snackbar.LENGTH_LONG).show()
return
}
if ( editor?.isModified != true)
return finish()
AlertDialog.Builder(this)
.setTitle(R.string.exit_without_save)
.setMessage(R.string.exit_without_save_warning)
@@ -217,65 +102,34 @@ class ProfileEditActivity : BaseActivity() {
.show()
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
override fun onDestroy() {
runBlocking {
withProfile {
val id = editor?.id ?: return@withProfile
settings.screen.saveState(outState)
}
private fun openUrlProvider(): Boolean {
val type = intent.getStringExtra("type")
val externalIntent = intent.getParcelableExtra<Intent>("intent")
try {
when (type) {
Constants.URL_PROVIDER_TYPE_FILE ->
startActivityForResult(
Intent(Intent.ACTION_GET_CONTENT).setType(TYPE_YAML),
REQUEST_CODE
)
Constants.URL_PROVIDER_TYPE_EXTERNAL ->
startActivityForResult(
externalIntent ?: throw NullPointerException(),
REQUEST_CODE
)
else -> return false
release(id)
}
} catch (e: Exception) {
makeSnackbarException(getString(R.string.start_url_provider_failure), e.message)
}
return true
super.onDestroy()
}
private fun sendProfileRequest(name: String, url: Uri, interval: Long) {
private fun commit(metadata: Profile) {
launch {
val source = intent?.getParcelableExtra<Intent>("intent")?.toUri(0)?.run(Uri::parse)
val type = when (intent?.getStringExtra("type")) {
Constants.URL_PROVIDER_TYPE_FILE ->
ClashProfileEntity.TYPE_FILE
Constants.URL_PROVIDER_TYPE_URL ->
ClashProfileEntity.TYPE_URL
Constants.URL_PROVIDER_TYPE_EXTERNAL ->
ClashProfileEntity.TYPE_EXTERNAL
else -> throw IllegalArgumentException()
try {
withProfile {
update(metadata.id, metadata)
commitAsync(metadata.id).await()
}
setResult(Activity.RESULT_OK)
finish()
} catch (e: Exception) {
showSnackbarException(getString(R.string.download_failure), e.message)
}
Log.d(interval.toString())
val request = ProfileRequest()
.action(ProfileRequest.Action.UPDATE_OR_CREATE)
.withId(intent.getLongExtra("id", -1L))
.withName(name)
.withURL(url)
.withUpdateInterval(interval)
.withCallback(requestCallback)
.withType(type)
.withSource(source)
withProfile {
enqueueRequest(request)
}
processing = false
}
}
}

View File

@@ -5,21 +5,20 @@ import android.net.Uri
import android.os.Bundle
import androidx.recyclerview.widget.LinearLayoutManager
import com.github.kr328.clash.adapter.ProfileAdapter
import com.github.kr328.clash.common.utils.intent
import com.github.kr328.clash.remote.withProfile
import com.github.kr328.clash.service.Intents
import com.github.kr328.clash.service.ProfileBackgroundService
import com.github.kr328.clash.service.data.ClashProfileEntity
import com.github.kr328.clash.service.transact.ProfileRequest
import com.github.kr328.clash.service.util.componentName
import com.github.kr328.clash.service.util.intent
import com.github.kr328.clash.service.util.startForegroundServiceCompat
import com.github.kr328.clash.service.ProfileReceiver
import com.github.kr328.clash.service.model.Profile
import com.github.kr328.clash.service.util.sendBroadcastSelf
import com.github.kr328.clash.weight.ProfilesMenu
import com.google.android.material.snackbar.Snackbar
import kotlinx.android.synthetic.main.activity_profiles.*
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import java.io.FileNotFoundException
import java.util.*
class ProfilesActivity : BaseActivity(), ProfileAdapter.Callback, ProfilesMenu.Callback {
@@ -29,7 +28,7 @@ class ProfilesActivity : BaseActivity(), ProfileAdapter.Callback, ProfilesMenu.C
private var backgroundJob: Job? = null
private val reloadMutex = Mutex()
private val editorStack = Stack<String>()
private val editorStack = Stack<Profile>()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@@ -62,16 +61,14 @@ class ProfilesActivity : BaseActivity(), ProfileAdapter.Callback, ProfilesMenu.C
backgroundJob = null
}
override val activityLabel: CharSequence?
get() = getText(R.string.profiles)
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
if (requestCode == EDITOR_REQUEST_CODE) {
launch {
val uri = editorStack.pop()
val profile = editorStack.pop()
withProfile {
commitProfileEditUri(uri)
update(profile.id, profile)
startUpdate(profile.id)
}
}
@@ -90,24 +87,23 @@ class ProfilesActivity : BaseActivity(), ProfileAdapter.Callback, ProfilesMenu.C
return
val profiles = withProfile {
queryProfiles()
queryAll()
}
(mainList.adapter as ProfileAdapter)
.setEntitiesAsync(profiles.toList())
(mainList.adapter as ProfileAdapter).setEntitiesAsync(profiles.toList())
reloadMutex.unlock()
}
override fun onProfileClicked(entity: ClashProfileEntity) {
override fun onProfileClicked(entity: Profile) {
launch {
withProfile {
setActiveProfile(entity.id)
setActive(entity.id)
}
}
}
override fun onMenuClicked(entity: ClashProfileEntity) {
override fun onMenuClicked(entity: Profile) {
ProfilesMenu(this, entity, this).show()
}
@@ -115,96 +111,70 @@ class ProfilesActivity : BaseActivity(), ProfileAdapter.Callback, ProfilesMenu.C
startActivity(CreateProfileActivity::class.intent)
}
private fun deleteProfile(entity: ClashProfileEntity) = launch {
val request = ProfileRequest().action(ProfileRequest.Action.REMOVE).withId(entity.id)
withProfile {
enqueueRequest(request)
}
}
private fun resetProviders(entity: ClashProfileEntity) = launch {
val request = ProfileRequest().action(ProfileRequest.Action.CLEAR).withId(entity.id)
withProfile {
enqueueRequest(request)
}
}
private fun openPropertiesEditor(entity: ClashProfileEntity, duplicate: Boolean) {
val type = when (entity.type) {
ClashProfileEntity.TYPE_FILE ->
Constants.URL_PROVIDER_TYPE_FILE
ClashProfileEntity.TYPE_URL ->
Constants.URL_PROVIDER_TYPE_URL
ClashProfileEntity.TYPE_EXTERNAL ->
Constants.URL_PROVIDER_TYPE_EXTERNAL
else -> throw IllegalArgumentException("Invalid type ${entity.type}")
}
val intent = entity.source?.run { Intent.parseUri(this, 0) }
val name = entity.name
val uri = entity.uri
val interval = entity.updateInterval.toString()
val editor = ProfileEditActivity::class.intent
.putExtra("id", if (duplicate) -1L else entity.id)
.putExtra("type", if (duplicate) Constants.URL_PROVIDER_TYPE_FILE else type)
.putExtra("intent", intent)
.putExtra("name", name)
.putExtra("url", uri)
.putExtra("interval", if (duplicate) "0" else interval)
startActivity(editor)
}
private fun openEditor(entity: ClashProfileEntity) = launch {
val uri = withProfile {
requestProfileEditUri(entity.id)
} ?: return@launch
editorStack.push(uri)
startActivityForResult(
Intent(Intent.ACTION_VIEW)
.setDataAndType(Uri.parse(uri), "text/plain")
.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION),
EDITOR_REQUEST_CODE
private fun openProperties(id: Long) {
startActivity(
ProfileEditActivity::class.intent
.setData(Uri.fromParts("id", id.toString(), null))
)
}
private fun startUpdate(entity: ClashProfileEntity) {
val request = ProfileRequest()
.action(ProfileRequest.Action.UPDATE_OR_CREATE)
.withId(entity.id)
private fun openEditor(profile: Profile) = launch {
try {
val uri = withProfile {
acquireTempUri(profile.id)
} ?: throw FileNotFoundException()
val intent = Intent(Intents.INTENT_ACTION_PROFILE_ENQUEUE_REQUEST)
.setComponent(ProfileBackgroundService::class.componentName)
.putExtra(Intents.INTENT_EXTRA_PROFILE_REQUEST, request)
editorStack.push(profile.copy(uri = Uri.parse(uri)))
startForegroundServiceCompat(intent)
startActivityForResult(
Intent(Intent.ACTION_VIEW)
.setDataAndType(Uri.parse(uri), "text/plain")
.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION),
EDITOR_REQUEST_CODE
)
} catch (e: Exception) {
Snackbar.make(rootView, getText(R.string.profile_not_found), Snackbar.LENGTH_LONG)
.show()
}
}
override fun onOpenEditor(entity: ClashProfileEntity) {
private fun startUpdate(id: Long) {
sendBroadcastSelf(ProfileReceiver.buildUpdateIntentForId(id))
}
override fun onOpenEditor(entity: Profile) {
openEditor(entity)
}
override fun onUpdate(entity: ClashProfileEntity) {
startUpdate(entity)
override fun onUpdate(entity: Profile) {
startUpdate(entity.id)
}
override fun onOpenProperties(entity: ClashProfileEntity) {
openPropertiesEditor(entity, false)
override fun onOpenProperties(entity: Profile) {
openProperties(entity.id)
}
override fun onDuplicate(entity: ClashProfileEntity) {
openPropertiesEditor(entity, true)
override fun onDuplicate(entity: Profile) {
launch {
withProfile {
openProperties(acquireCloned(entity.id))
}
}
}
override fun onResetProvider(entity: ClashProfileEntity) {
resetProviders(entity)
override fun onResetProvider(entity: Profile) {
launch {
withProfile {
clear(entity.id)
}
}
}
override fun onDelete(entity: ClashProfileEntity) {
deleteProfile(entity)
override fun onDelete(entity: Profile) {
launch {
withProfile {
delete(entity.id)
}
}
}
}

View File

@@ -161,9 +161,6 @@ class ProxiesActivity : BaseActivity(), ScrollBinding.Callback {
return true
}
override val activityLabel: CharSequence?
get() = getText(R.string.proxy)
override suspend fun onClashStopped(reason: String?) {
finish()
}

View File

@@ -1,7 +1,7 @@
package com.github.kr328.clash
import android.os.Bundle
import com.github.kr328.clash.service.util.intent
import com.github.kr328.clash.common.utils.intent
import kotlinx.android.synthetic.main.activity_settings.*
class SettingsActivity : BaseActivity() {
@@ -43,7 +43,4 @@ class SettingsActivity : BaseActivity() {
}
}
}
override val activityLabel: CharSequence?
get() = getText(R.string.settings)
}

View File

@@ -14,7 +14,4 @@ class SettingsBehaviorActivity : BaseActivity() {
.replace(R.id.fragment, BehaviorFragment())
.commit()
}
override val activityLabel: CharSequence?
get() = getText(R.string.behavior)
}

View File

@@ -14,7 +14,4 @@ class SettingsInterfaceActivity : BaseActivity() {
.replace(R.id.fragment, InterfaceFragment())
.commit()
}
override val activityLabel: CharSequence?
get() = getText(R.string.interface_)
}

View File

@@ -26,7 +26,4 @@ class SettingsNetworkActivity : BaseActivity() {
override suspend fun onClashStarted() {
recreate()
}
override val activityLabel: CharSequence?
get() = getText(R.string.network)
}

View File

@@ -1,18 +1,12 @@
package com.github.kr328.clash
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import androidx.core.content.getSystemService
import com.google.android.material.snackbar.Snackbar
import android.text.Html
import kotlinx.android.synthetic.main.activity_support.*
class SupportActivity : BaseActivity() {
override val activityLabel: CharSequence?
get() = getText(R.string.support)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@@ -20,6 +14,11 @@ class SupportActivity : BaseActivity() {
setSupportActionBar(toolbar)
commonUi.build {
tips {
icon = getDrawable(R.drawable.ic_info)
title = Html.fromHtml(getString(R.string.tips_support), Html.FROM_HTML_MODE_LEGACY)
}
category(text = getString(R.string.sources))
option(
@@ -45,20 +44,8 @@ class SupportActivity : BaseActivity() {
}
}
category(text = getString(R.string.contacts))
category(text = getString(R.string.feedback))
option(
title = getString(R.string.email),
summary = getString(R.string.email_url)
) {
onClick {
val data =
ClipData.newPlainText("email", getText(R.string.email_url))
getSystemService<ClipboardManager>()?.setPrimaryClip(data)
Snackbar.make(rootView, getText(R.string.copied), Snackbar.LENGTH_SHORT).show()
}
}
option(
title = getString(R.string.github_issues),
summary = getString(R.string.github_issues_url)
@@ -71,9 +58,11 @@ class SupportActivity : BaseActivity() {
}
}
if (resources.configuration.locales.get(0)
.language.equals("zh", true)
) {
val firstLanguage = resources.configuration.locales.get(0).language
if (firstLanguage.equals("zh", true)) {
category(getString(R.string.donate))
option(
title = getString(R.string.telegram_channel),
summary = getString(R.string.telegram_channel_url)

View File

@@ -7,10 +7,11 @@ import android.content.IntentFilter
import android.graphics.drawable.Icon
import android.service.quicksettings.Tile
import android.service.quicksettings.TileService
import com.github.kr328.clash.common.Permissions
import com.github.kr328.clash.common.ids.Intents
import com.github.kr328.clash.remote.RemoteUtils
import com.github.kr328.clash.service.Intents
import com.github.kr328.clash.utils.startClashService
import com.github.kr328.clash.utils.stopClashService
import com.github.kr328.clash.service.util.startClashService
import com.github.kr328.clash.service.util.stopClashService
class TileService : TileService() {
private var currentProfile = ""
@@ -84,7 +85,9 @@ class TileService : TileService() {
addAction(Intents.INTENT_ACTION_CLASH_STARTED)
addAction(Intents.INTENT_ACTION_CLASH_STOPPED)
addAction(Intents.INTENT_ACTION_PROFILE_LOADED)
}
},
Permissions.PERMISSION_RECEIVE_BROADCASTS,
null
)
val name = RemoteUtils.getCurrentClashProfileName(this)

View File

@@ -9,7 +9,7 @@ import android.widget.TextView
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView
import com.github.kr328.clash.R
import com.github.kr328.clash.service.data.ClashProfileEntity
import com.github.kr328.clash.service.model.Profile
import com.github.kr328.clash.utils.IntervalUtils
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
@@ -17,12 +17,12 @@ import kotlinx.coroutines.withContext
class ProfileAdapter(private val context: Context, private val callback: Callback) :
RecyclerView.Adapter<RecyclerView.ViewHolder>() {
interface Callback {
fun onProfileClicked(entity: ClashProfileEntity)
fun onMenuClicked(entity: ClashProfileEntity)
fun onProfileClicked(entity: Profile)
fun onMenuClicked(entity: Profile)
fun onNewProfile()
}
private var entities: List<ClashProfileEntity> = emptyList()
private var entities: List<Profile> = emptyList()
class EntityHolder(view: View) : RecyclerView.ViewHolder(view) {
val root: View = view.findViewById(R.id.root)
@@ -37,7 +37,7 @@ class ProfileAdapter(private val context: Context, private val callback: Callbac
val root: View = view.findViewById(R.id.root)
}
suspend fun setEntitiesAsync(new: List<ClashProfileEntity>) {
suspend fun setEntitiesAsync(new: List<Profile>) {
val old = withContext(Dispatchers.Main) {
entities
}
@@ -101,7 +101,7 @@ class ProfileAdapter(private val context: Context, private val callback: Callbac
holder.radio.isChecked = current.active
holder.name.text = current.name
holder.type.text = getTypeName(current.type)
holder.interval.text = offsetDate(current.lastUpdate)
holder.interval.text = offsetDate(current.lastModified)
holder.root.setOnClickListener {
callback.onProfileClicked(current)
@@ -118,13 +118,13 @@ class ProfileAdapter(private val context: Context, private val callback: Callbac
}
}
private fun getTypeName(type: Int): CharSequence {
private fun getTypeName(type: Profile.Type): CharSequence {
return when (type) {
ClashProfileEntity.TYPE_FILE ->
Profile.Type.FILE ->
context.getText(R.string.file)
ClashProfileEntity.TYPE_URL ->
Profile.Type.URL ->
context.getText(R.string.url)
ClashProfileEntity.TYPE_EXTERNAL ->
Profile.Type.EXTERNAL ->
context.getText(R.string.external)
else ->
context.getText(R.string.unknown)

View File

@@ -66,8 +66,10 @@ class ProxyAdapter(
private var renderList = mutableListOf<RenderInfo>()
private var activeList: MutableMap<String, Int> = mutableMapOf()
private var groupPosition: MutableMap<String, Int> = mutableMapOf()
@ColorInt
private val colorSurface: Int
@ColorInt
private val colorOnSurface: Int

View File

@@ -0,0 +1,206 @@
package com.github.kr328.clash.fragment
import android.app.Activity
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.text.Html
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.ViewGroup.LayoutParams
import android.webkit.MimeTypeMap
import android.webkit.URLUtil
import androidx.fragment.app.Fragment
import com.github.kr328.clash.Constants
import com.github.kr328.clash.R
import com.github.kr328.clash.design.common.TextInput
import com.github.kr328.clash.design.view.CommonUiLayout
import com.github.kr328.clash.service.model.Profile.Type
import com.google.android.material.snackbar.Snackbar
class ProfileEditFragment(
val id: Long,
var name: String,
var uri: Uri,
var interval: Long,
private val type: Type,
private val source: String?
) : Fragment() {
var isModified = false
companion object {
private const val REQUEST_CODE = 10000
private const val KEY_NAME = "name"
private const val KEY_URL = "url"
private const val KEY_AUTO_UPDATE = "auto_update"
private val TYPE_YAML = MimeTypeMap.getSingleton()
.getMimeTypeFromExtension("yaml") ?: "*/*"
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return CommonUiLayout(requireContext()).apply {
layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)
build {
tips(icon = requireContext().getDrawable(R.drawable.ic_info)) {
title =
Html.fromHtml(getString(R.string.tips_profile), Html.FROM_HTML_MODE_LEGACY)
}
textInput(
title = getString(R.string.name),
icon = requireContext().getDrawable(R.drawable.ic_label_outline),
hint = getString(R.string.profile_name),
id = KEY_NAME,
content = name
) {
onTextChanged {
name = content.toString()
isModified = true
}
}
textInput(
title = getString(R.string.url),
icon = requireContext().getDrawable(R.drawable.ic_content),
hint = getString(R.string.profile_url),
id = KEY_URL,
content = uri.toString()
) {
onOpenInput {
if (!openUrlProvider())
openDialogInput()
}
onDisplayContent {
it.split("/").last()
}
onTextChanged {
if (!URLUtil.isValidUrl(content.toString())) {
content = ""
Snackbar.make(view, R.string.invalid_url, Snackbar.LENGTH_LONG).show()
return@onTextChanged
}
uri = Uri.parse(content.toString())
isModified = true
}
}
textInput(
title = getString(R.string.auto_update),
icon = requireContext().getDrawable(R.drawable.ic_update),
hint = getString(R.string.more_than_15_minutes),
id = KEY_AUTO_UPDATE,
content = (interval / 1000 / 60).toStringIfNonZero()
) {
onDisplayContent {
val interval = it.toString().toIntOrNull() ?: 0
if (interval <= 0)
getString(R.string.disabled)
else
getString(R.string.format_minutes, interval)
}
onTextChanged {
val s = it.toString()
if (s.isBlank()) {
content = ""
return@onTextChanged
}
val value = s.toIntOrNull()
if (value == null || value < 15) {
content = ""
Snackbar.make(view, R.string.invalid_interval, Snackbar.LENGTH_LONG)
.show()
return@onTextChanged
}
interval = content.toString().toLong() * 1000 * 60
isModified = true
}
if ( type == Type.FILE )
isHidden = true
}
screen.restoreState(savedInstanceState)
if (type == Type.EXTERNAL)
openUrlProvider()
}
}
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
if (requestCode == REQUEST_CODE) {
if (resultCode != Activity.RESULT_OK || data == null)
return
val layout = view as CommonUiLayout
data.data?.apply {
layout.screen.requireElement<TextInput>(KEY_URL).content = this.toString()
}
data.getStringExtra(Constants.URL_PROVIDER_INTENT_EXTRA_NAME)?.also {
layout.screen.requireElement<TextInput>(KEY_NAME).apply {
if (content.isBlank())
content = it
}
}
}
super.onActivityResult(requestCode, resultCode, data)
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
(view as CommonUiLayout?)?.screen?.saveState(outState)
}
private fun openUrlProvider(): Boolean {
try {
when (type) {
Type.FILE ->
startActivityForResult(
Intent(Intent.ACTION_GET_CONTENT).setType(TYPE_YAML),
REQUEST_CODE
)
Type.EXTERNAL ->
startActivityForResult(
source?.toIntent() ?: return false,
REQUEST_CODE
)
else -> return false
}
} catch (e: Exception) {
Snackbar.make(
view as ViewGroup,
R.string.start_url_provider_failure,
Snackbar.LENGTH_LONG
).show()
}
return true
}
private fun Long.toStringIfNonZero(): String {
return if ( this == 0L ) "" else this.toString()
}
private fun String.toIntent(): Intent? {
return try {
Intent.parseUri(this, 0)
} catch (e: Exception) {
null
}
}
}

View File

@@ -1,5 +1,5 @@
package com.github.kr328.clash.pipeline
import com.github.kr328.clash.service.settings.BaseSettings
import com.github.kr328.clash.common.settings.BaseSettings
data class Pipeline<T>(val input: T, val settings: BaseSettings)

View File

@@ -72,8 +72,7 @@ suspend fun Pipeline<List<ProxyGroup>>.sort(): Pipeline<List<ProxyGroup>> {
suspend fun Pipeline<List<ProxyGroup>>.toAdapterElement(
prefixMerged: Map<ProxyEntry, ProxyMerged>,
general: General
):
List<ProxyAdapter.ProxyGroupInfo> {
): List<ProxyAdapter.ProxyGroupInfo> {
return input.map { group ->
val proxies = group.proxies.map { proxy ->
val merged = prefixMerged[ProxyEntry(group.name, proxy.name)]?.takeIf {

View File

@@ -1,7 +1,7 @@
package com.github.kr328.clash.preference
import android.content.Context
import com.github.kr328.clash.service.settings.BaseSettings
import com.github.kr328.clash.common.settings.BaseSettings
class UiSettings(context: Context) :
BaseSettings(context.getSharedPreferences(FILE_NAME, Context.MODE_PRIVATE)) {
@@ -16,7 +16,6 @@ class UiSettings(context: Context) :
const val DARK_MODE_DARK = "dark"
const val DARK_MODE_LIGHT = "light"
val ENABLE_VPN = BooleanEntry("enable_vpn", true)
val PROXY_GROUP_SORT = StringEntry("proxy_group_sort", PROXY_SORT_DEFAULT)
val PROXY_PROXY_SORT = StringEntry("proxy_proxy_sort", PROXY_SORT_DEFAULT)
val PROXY_LAST_SELECT_GROUP = StringEntry("proxy_last_select_group", "")

View File

@@ -5,10 +5,11 @@ import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.ProcessLifecycleOwner
import com.github.kr328.clash.service.Intents
import com.github.kr328.clash.common.Global
import com.github.kr328.clash.common.Permissions
import com.github.kr328.clash.common.ids.Intents
import com.github.kr328.clash.common.utils.Log
import com.github.kr328.clash.utils.ApplicationObserver
object Broadcasts {
interface Receiver {
@@ -62,36 +63,37 @@ object Broadcasts {
receivers.remove(receiver)
}
fun init(application: Application) {
ProcessLifecycleOwner.get().lifecycle.addObserver(object : DefaultLifecycleObserver {
override fun onStart(owner: LifecycleOwner) {
application.registerReceiver(broadcastReceiver, IntentFilter().apply {
addAction(Intents.INTENT_ACTION_PROFILE_CHANGED)
addAction(Intents.INTENT_ACTION_CLASH_STOPPED)
addAction(Intents.INTENT_ACTION_CLASH_STARTED)
addAction(Intents.INTENT_ACTION_PROFILE_LOADED)
})
private val observer = ApplicationObserver {
Log.d("Global Broadcast Receiver State = $it")
if ( it ) {
Global.application.registerReceiver(broadcastReceiver, IntentFilter().apply {
addAction(Intents.INTENT_ACTION_PROFILE_CHANGED)
addAction(Intents.INTENT_ACTION_CLASH_STOPPED)
addAction(Intents.INTENT_ACTION_CLASH_STARTED)
addAction(Intents.INTENT_ACTION_PROFILE_LOADED)
}, Permissions.PERMISSION_RECEIVE_BROADCASTS, null)
val current = RemoteUtils.detectClashRunning(application)
if (current != clashRunning) {
clashRunning = current
val current = RemoteUtils.detectClashRunning(Global.application)
if (current != clashRunning) {
clashRunning = current
if (current) {
receivers.forEach {
it.onStarted()
}
} else {
receivers.forEach {
it.onStopped(null)
}
if (current) {
receivers.forEach { receiver ->
receiver.onStarted()
}
} else {
receivers.forEach { receiver ->
receiver.onStopped(null)
}
}
}
} else {
Global.application.unregisterReceiver(broadcastReceiver)
}
}
override fun onStop(owner: LifecycleOwner) {
application.unregisterReceiver(broadcastReceiver)
}
})
fun init(application: Application) {
observer.register(application)
}
}

View File

@@ -4,8 +4,8 @@ import android.os.RemoteException
import com.github.kr328.clash.core.model.General
import com.github.kr328.clash.core.model.ProxyGroup
import com.github.kr328.clash.service.IClashManager
import com.github.kr328.clash.service.ipc.IStreamCallback
import com.github.kr328.clash.service.ipc.ParcelableContainer
import com.github.kr328.clash.service.transact.IStreamCallback
import com.github.kr328.clash.service.transact.ParcelableContainer
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext

View File

@@ -1,33 +1,72 @@
package com.github.kr328.clash.remote
import android.os.RemoteException
import com.github.kr328.clash.service.IProfileService
import com.github.kr328.clash.service.data.ClashProfileEntity
import com.github.kr328.clash.service.transact.ProfileRequest
import com.github.kr328.clash.service.transact.IStreamCallback
import com.github.kr328.clash.service.transact.ParcelableContainer
import com.github.kr328.clash.service.model.Profile
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
class ProfileClient(private val service: IProfileService) {
suspend fun queryProfiles(): Array<ClashProfileEntity> = withContext(Dispatchers.IO) {
service.queryProfiles()
suspend fun acquireUnused(type: Profile.Type, source: String?) = withContext(Dispatchers.IO) {
service.acquireUnused(type.name, source)
}
suspend fun queryActiveProfile(): ClashProfileEntity? = withContext(Dispatchers.IO) {
service.queryActiveProfile()
suspend fun acquireCloned(id: Long) = withContext(Dispatchers.IO) {
service.acquireCloned(id)
}
suspend fun enqueueRequest(request: ProfileRequest) = withContext(Dispatchers.IO) {
service.enqueueRequest(request)
suspend fun acquireTempUri(id: Long): String? = withContext(Dispatchers.IO) {
service.acquireTempUri(id)
}
suspend fun setActiveProfile(id: Long) = withContext(Dispatchers.IO) {
service.setActiveProfile(id)
suspend fun update(id: Long, metadata: Profile) = withContext(Dispatchers.IO) {
service.update(id, metadata)
}
suspend fun requestProfileEditUri(id: Long): String? = withContext(Dispatchers.IO) {
service.requestProfileEditUri(id)
suspend fun commitAsync(id: Long) = withContext(Dispatchers.IO) {
CompletableDeferred<Unit>().apply {
service.commit(id, object : IStreamCallback.Stub() {
override fun complete() {
complete(Unit)
}
override fun completeExceptionally(reason: String?) {
completeExceptionally(RemoteException(reason))
}
override fun send(data: ParcelableContainer?) {}
})
}
}
suspend fun commitProfileEditUri(uri: String) = withContext(Dispatchers.IO) {
service.commitProfileEditUri(uri)
suspend fun release(id: Long) = withContext(Dispatchers.IO) {
service.release(id)
}
suspend fun delete(id: Long) = withContext(Dispatchers.IO) {
service.delete(id)
}
suspend fun clear(id: Long) = withContext(Dispatchers.IO) {
service.clear(id)
}
suspend fun queryAll(): Array<Profile> = withContext(Dispatchers.IO) {
service.queryAll()
}
suspend fun queryActive(): Profile? = withContext(Dispatchers.IO) {
service.queryActive()
}
suspend fun queryById(id: Long): Profile? = withContext(Dispatchers.IO) {
service.queryById(id)
}
suspend fun setActive(id: Long) = withContext(Dispatchers.IO) {
service.setActive(id)
}
}

View File

@@ -8,26 +8,21 @@ import android.content.ServiceConnection
import android.os.Build
import android.os.Handler
import android.os.IBinder
import android.os.RemoteException
import androidx.core.content.edit
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.ProcessLifecycleOwner
import com.github.kr328.clash.ApkBrokenActivity
import com.github.kr328.clash.Constants
import com.github.kr328.clash.core.utils.Log
import com.github.kr328.clash.dump.LogcatDumper
import com.github.kr328.clash.common.Global
import com.github.kr328.clash.common.utils.intent
import com.github.kr328.clash.common.utils.Log
import com.github.kr328.clash.service.ClashManagerService
import com.github.kr328.clash.service.IClashManager
import com.github.kr328.clash.service.IProfileService
import com.github.kr328.clash.service.ProfileService
import com.github.kr328.clash.service.util.intent
import com.github.kr328.clash.utils.ApplicationObserver
import com.microsoft.appcenter.crashes.Crashes
import com.microsoft.appcenter.crashes.ingestion.models.ErrorAttachmentLog
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.Channel
import java.io.File
import java.lang.Exception
import java.util.zip.ZipFile
object Remote {
@@ -87,97 +82,103 @@ object Remote {
}
}
fun init(application: Application) {
val handler = Handler()
private val handler = Handler()
private val observer = ApplicationObserver {
Log.d("Remote Connection State = $it")
ProcessLifecycleOwner.get().lifecycle.addObserver(object : DefaultLifecycleObserver {
override fun onStart(owner: LifecycleOwner) {
handler.removeMessages(0)
val application = Global.application
GlobalScope.launch {
if (!verifyApk(application)) {
application.startActivity(
ApkBrokenActivity::class.intent
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
)
return@launch
}
if ( it ) {
handler.removeMessages(0)
clashConnection = ClashConnection().apply {
application.bindService(
ClashManagerService::class.intent,
this,
Context.BIND_AUTO_CREATE
)
}
GlobalScope.launch {
val valid = withContext(Dispatchers.IO) {
verifyApk(application)
}
profileConnection = ProfileConnection().apply {
application.bindService(
ProfileService::class.intent,
this,
Context.BIND_AUTO_CREATE
)
}
if (!valid) {
application.startActivity(
ApkBrokenActivity::class.intent
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
)
return@launch
}
clashConnection = ClashConnection().apply {
application.bindService(
ClashManagerService::class.intent,
this,
Context.BIND_AUTO_CREATE
)
}
profileConnection = ProfileConnection().apply {
application.bindService(
ProfileService::class.intent,
this,
Context.BIND_AUTO_CREATE
)
}
}
} else {
handler.postDelayed({
clashConnection?.also {
application.unbindService(it)
it.onServiceDisconnected(null)
}
profileConnection?.also {
application.unbindService(it)
it.onServiceDisconnected(null)
}
override fun onStop(owner: LifecycleOwner) {
handler.postDelayed({
clashConnection?.also {
application.unbindService(it)
it.onServiceDisconnected(null)
}
profileConnection?.also {
application.unbindService(it)
it.onServiceDisconnected(null)
}
clashConnection = null
profileConnection = null
}, 5000)
}
})
clashConnection = null
profileConnection = null
}, 5000)
}
}
private suspend fun verifyApk(application: Application) = withContext(Dispatchers.IO) {
val sp = application.getSharedPreferences(
Constants.PREFERENCE_NAME_APP,
Context.MODE_PRIVATE
)
val pkg = application.packageManager.getPackageInfo(application.packageName, 0)
fun init(application: Application) {
observer.register(application)
}
if (sp.getLong(Constants.PREFERENCE_KEY_LAST_INSTALL, 0) == pkg.lastUpdateTime)
return@withContext true
private fun verifyApk(application: Application): Boolean {
return try {
val sp = application.getSharedPreferences(
Constants.PREFERENCE_NAME_APP,
Context.MODE_PRIVATE
)
val pkg = application.packageManager.getPackageInfo(application.packageName, 0)
val info = application.applicationInfo
val sources =
info.splitSourceDirs ?: arrayOf(info.sourceDir) ?: return@withContext false
if (sp.getLong(Constants.PREFERENCE_KEY_LAST_INSTALL, 0) == pkg.lastUpdateTime)
return true
val regexNativeLibrary = Regex("lib/(\\S+)/libgojni.so")
val availableAbi = Build.SUPPORTED_ABIS.toSet()
val apkAbi = try {
sources
.asSequence()
.filter { File(it).exists() }
.flatMap { ZipFile(it).entries().asSequence() }
.mapNotNull { regexNativeLibrary.matchEntire(it.name) }
.mapNotNull { it.groups[1]?.value }
.toSet()
}
catch (e: Exception) {
val info = application.applicationInfo
val sources =
info.splitSourceDirs ?: arrayOf(info.sourceDir) ?: return false
val regexNativeLibrary = Regex("lib/(\\S+)/libgojni.so")
val availableAbi = Build.SUPPORTED_ABIS.toSet()
val apkAbi =
sources
.asSequence()
.filter { File(it).exists() }
.flatMap { ZipFile(it).entries().asSequence() }
.mapNotNull { regexNativeLibrary.matchEntire(it.name) }
.mapNotNull { it.groups[1]?.value }
.toSet()
if (availableAbi.intersect(apkAbi).isNotEmpty()) {
sp.edit {
putLong(Constants.PREFERENCE_KEY_LAST_INSTALL, pkg.lastUpdateTime)
}
true
} else {
false
}
} catch (e: Exception) {
Crashes.trackError(e)
emptySet<String>()
}
if (availableAbi.intersect(apkAbi).isNotEmpty()) {
sp.edit {
putLong(Constants.PREFERENCE_KEY_LAST_INSTALL, pkg.lastUpdateTime)
}
true
}
else {
false
}
}

View File

@@ -4,9 +4,9 @@ import android.content.Context
import android.content.Intent
import android.net.Uri
import com.github.kr328.clash.ApkBrokenActivity
import com.github.kr328.clash.common.utils.intent
import com.github.kr328.clash.service.Constants
import com.github.kr328.clash.service.ServiceStatusProvider
import com.github.kr328.clash.service.util.intent
object RemoteUtils {
fun detectClashRunning(context: Context): Boolean {

View File

@@ -2,11 +2,11 @@ package com.github.kr328.clash.settings
import android.content.pm.PackageManager
import android.os.Bundle
import com.github.kr328.clash.OnBootReceiver
import com.github.kr328.clash.R
import com.github.kr328.clash.common.utils.componentName
import com.github.kr328.clash.remote.Broadcasts
import com.github.kr328.clash.service.RestartReceiver
import com.github.kr328.clash.service.settings.ServiceSettings
import com.github.kr328.clash.service.util.componentName
class BehaviorFragment : BaseSettingFragment() {
companion object {
@@ -40,7 +40,7 @@ class BehaviorFragment : BaseSettingFragment() {
PackageManager.COMPONENT_ENABLED_STATE_DISABLED
requireActivity().packageManager.setComponentEnabledSetting(
OnBootReceiver::class.componentName,
RestartReceiver::class.componentName,
status,
PackageManager.DONT_KILL_APP
)
@@ -48,7 +48,7 @@ class BehaviorFragment : BaseSettingFragment() {
override fun get(): Any? {
val status = requireActivity().packageManager
.getComponentEnabledSetting(OnBootReceiver::class.componentName)
.getComponentEnabledSetting(RestartReceiver::class.componentName)
return status == PackageManager.COMPONENT_ENABLED_STATE_ENABLED
}

View File

@@ -3,15 +3,13 @@ package com.github.kr328.clash.settings
import android.os.Bundle
import com.github.kr328.clash.PackagesActivity
import com.github.kr328.clash.R
import com.github.kr328.clash.preference.UiSettings
import com.github.kr328.clash.common.utils.intent
import com.github.kr328.clash.remote.Broadcasts
import com.github.kr328.clash.service.settings.ServiceSettings
import com.github.kr328.clash.service.util.intent
class NetworkFragment : BaseSettingFragment() {
companion object {
private const val KEY_ENABLE_VPN_SERVICE = "enable_vpn_service"
private const val KEY_IPV6 = "ipv6"
private const val BYPASS_PRIVATE_NETWORK = "bypass_private_network"
private const val KEY_DNS_HIJACKING = "dns_hijacking"
private const val KEY_DNS_OVERRIDE = "dns_override"
@@ -33,8 +31,7 @@ class NetworkFragment : BaseSettingFragment() {
override fun onCreateDataStore(): SettingsDataStore {
return SettingsDataStore().apply {
on(KEY_ENABLE_VPN_SERVICE, UiSettings.ENABLE_VPN.asSource(ui))
on(KEY_IPV6, ServiceSettings.IPV6_SUPPORT.asSource(service))
on(KEY_ENABLE_VPN_SERVICE, ServiceSettings.ENABLE_VPN.asSource(service))
on(BYPASS_PRIVATE_NETWORK, ServiceSettings.BYPASS_PRIVATE_NETWORK.asSource(service))
on(KEY_DNS_HIJACKING, ServiceSettings.DNS_HIJACKING.asSource(service))
on(KEY_DNS_OVERRIDE, ServiceSettings.OVERRIDE_DNS.asSource(service))

View File

@@ -1,6 +1,6 @@
package com.github.kr328.clash.settings
import com.github.kr328.clash.service.settings.BaseSettings
import com.github.kr328.clash.common.settings.BaseSettings
import moe.shizuku.preference.PreferenceDataStore
class SettingsDataStore : PreferenceDataStore() {

View File

@@ -0,0 +1,40 @@
package com.github.kr328.clash.utils
import android.app.Activity
import android.app.Application
import android.os.Bundle
class ApplicationObserver(val stateChanged: (Boolean) -> Unit) {
private var applicationRunning = false
private set(value) {
if ( field != value )
stateChanged(value)
field = value
}
private var activityCount: Int = 0
private val activityObserver = object: Application.ActivityLifecycleCallbacks {
override fun onActivityPaused(activity: Activity) {}
override fun onActivityStarted(activity: Activity) {}
override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) {}
override fun onActivityStopped(activity: Activity) {}
override fun onActivityResumed(activity: Activity) {}
override fun onActivityDestroyed(activity: Activity) {
synchronized(this) {
activityCount--
applicationRunning = activityCount > 0
}
}
override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {
synchronized(this) {
activityCount++
applicationRunning = activityCount > 0
}
}
}
fun register(application: Application) {
application.registerActivityLifecycleCallbacks(activityObserver)
}
}

View File

@@ -1,32 +0,0 @@
package com.github.kr328.clash.utils
import android.content.Context
import android.content.Intent
import android.net.VpnService
import com.github.kr328.clash.preference.UiSettings
import com.github.kr328.clash.service.ClashService
import com.github.kr328.clash.service.Intents
import com.github.kr328.clash.service.TunService
import com.github.kr328.clash.service.util.intent
import com.github.kr328.clash.service.util.sendBroadcastSelf
import com.github.kr328.clash.service.util.startForegroundServiceCompat
fun Context.startClashService(): Intent? {
val startTun = UiSettings(this).get(UiSettings.ENABLE_VPN)
if (startTun) {
val vpnRequest = VpnService.prepare(this)
if (vpnRequest != null)
return vpnRequest
startForegroundServiceCompat(TunService::class.intent)
} else {
startForegroundServiceCompat(ClashService::class.intent)
}
return null
}
fun Context.stopClashService() {
sendBroadcastSelf(Intent(Intents.INTENT_ACTION_REQUEST_STOP))
}

View File

@@ -6,21 +6,21 @@ import android.view.ViewGroup
import androidx.annotation.ColorInt
import com.github.kr328.clash.R
import com.github.kr328.clash.design.view.CommonUiLayout
import com.github.kr328.clash.service.data.ClashProfileEntity
import com.github.kr328.clash.service.model.Profile
import com.google.android.material.bottomsheet.BottomSheetDialog
class ProfilesMenu(
context: Context,
private val entity: ClashProfileEntity,
private val entity: Profile,
private val callback: Callback
) : BottomSheetDialog(context) {
interface Callback {
fun onOpenEditor(entity: ClashProfileEntity)
fun onUpdate(entity: ClashProfileEntity)
fun onOpenProperties(entity: ClashProfileEntity)
fun onDuplicate(entity: ClashProfileEntity)
fun onResetProvider(entity: ClashProfileEntity)
fun onDelete(entity: ClashProfileEntity)
fun onOpenEditor(entity: Profile)
fun onUpdate(entity: Profile)
fun onOpenProperties(entity: Profile)
fun onDuplicate(entity: Profile)
fun onResetProvider(entity: Profile)
fun onDelete(entity: Profile)
}
init {
@@ -30,6 +30,7 @@ class ProfilesMenu(
ViewGroup.LayoutParams.WRAP_CONTENT
)
}
@ColorInt
val errorColor = TypedValue().run {
context.theme.resolveAttribute(R.attr.colorError, this, true)
@@ -37,7 +38,7 @@ class ProfilesMenu(
}
menu.build {
if (entity.type != ClashProfileEntity.TYPE_FILE) {
if (entity.type != Profile.Type.FILE) {
option(
title = context.getString(R.string.update),
icon = context.getDrawable(R.drawable.ic_update)

View File

@@ -45,8 +45,8 @@
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.github.kr328.clash.design.view.CommonUiLayout
android:id="@+id/settings"
<FrameLayout
android:id="@+id/fragment"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</ScrollView>

View File

@@ -16,8 +16,12 @@
android:background="@color/toolbarColor" />
</com.google.android.material.appbar.AppBarLayout>
<com.github.kr328.clash.design.view.CommonUiLayout
android:id="@+id/commonUi"
<ScrollView
android:layout_width="match_parent"
android:layout_height="match_parent" />
android:layout_height="match_parent">
<com.github.kr328.clash.design.view.CommonUiLayout
android:id="@+id/commonUi"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</ScrollView>
</LinearLayout>

View File

@@ -8,9 +8,9 @@
<string name="append_system_dns_summary">自动追加系统 DNS 到 Clash</string>
<string name="application_broken">应用损坏</string>
<string name="application_name">Clash for Android</string>
<string name="auto_update">自动更新 ()</string>
<string name="auto_update">自动更新 (分钟)</string>
<string name="behavior">行为</string>
<string name="boot"></string>
<string name="restart"></string>
<string name="bypass_private_network">绕过私有网络</string>
<string name="bypass_private_network_summary">绕过私有网络地址</string>
<string name="cancel">取消</string>
@@ -39,7 +39,6 @@
<string name="edit">编辑</string>
<string name="edit_profile">编辑配置</string>
<string name="empty_name">空名称</string>
<string name="enable_ipv6">启用 IPv6 支持 (不建议)</string>
<string name="exit_without_save">退出而不保存</string>
<string name="exit_without_save_warning">所有变更将会丢失</string>
<string name="export">导出</string>
@@ -59,11 +58,9 @@
<string name="history">历史</string>
<string name="import_from_file">从文件导入</string>
<string name="import_from_url">从 URL 导入</string>
<string name="seconds"></string>
<string name="interface_">界面</string>
<string name="invalid_interval">无效的间隔</string>
<string name="invalid_url">无效的 URL</string>
<string name="ipv6">IPv6</string>
<string name="language">语言</string>
<string name="launch_name">Clash</string>
<string name="log_viewer">日志查看器</string>
@@ -87,8 +84,8 @@
<string name="recently">近期</string>
<string name="refresh">刷新</string>
<string name="reset_provider">重置外部引用</string>
<string name="route_system_traffic">路由系统流量</string>
<string name="routing_via_vpn_service">通过 VpnService 路由所有系统流量</string>
<string name="route_system_traffic">自动路由系统流量</string>
<string name="routing_via_vpn_service">通过 VpnService 自动路由所有系统流量</string>
<string name="rule">规则</string>
<string name="rule_mode">规则模式</string>
<string name="running">运行中</string>
@@ -97,9 +94,9 @@
<string name="show_traffic_summary">在通知中自动刷新流量</string>
<string name="sort_group">代理组排序</string>
<string name="sort_proxy">代理排序</string>
<string name="start_clash_on_system_boot">在系统启动时启动 Clash</string>
<string name="start_on_boot">开机时启动</string>
<string name="start_url_provider_failure">打开 URL 提供失败</string>
<string name="allow_clash_auto_restart">允许 Clash 自动重启</string>
<string name="auto_restart">自动重启</string>
<string name="start_url_provider_failure">打开 URL 提供程序失败</string>
<string name="stopped">已停止</string>
<string name="support">支持</string>
<string name="tap_to_start">点此启动</string>
@@ -118,17 +115,20 @@
<string name="reverse">反转</string>
<string name="clash">Clash</string>
<string name="clash_for_android">Clash for Android</string>
<string name="contacts">联系我们</string>
<string name="email">E-Mail</string>
<string name="feedback">反馈</string>
<string name="sources">源代码</string>
<string name="telegram_channel">Telegram 频道</string>
<string name="github_issues">Github Issues</string>
<string name="format_seconds">%d 秒</string>
<string name="copied">已复制</string>
<string name="tips_profile"><![CDATA[仅接受 <strong>Clash 配置文件</strong> <br />其中包含了 <strong>代理, 代理组 和 规则</strong>]]></string>
<string name="application_broken_description"><![CDATA[这通常意味着您使用来自 <strong>Google Play</strong> 的副本, 但是生成该副本的应用未能正确处理 <strong>分包机制</strong><br />这意味着您获取的是 <strong>应用的一部分</strong>]]></string>
<string name="learn_more_about_split_apks">了解更多关于分包机制</string>
<string name="reinstall_from_google_play">重新从 Google Play 安装</string>
<string name="download_from_github_releases">从 Github Release 下载</string>
<string name="missing_vpn_component">系统 VPN 组件缺失</string>
<string name="profile_not_found">配置文件丢失</string>
<string name="more_than_15_minutes">大于15分钟</string>
<string name="loading">载入中</string>
<string name="tips_support"><![CDATA[Clash for Android 是一个<strong>免费开源</strong>的项目<br /> 我们<strong>不提供</strong>任何代理服务<br />请务必<strong>不要</strong>反馈非应用自身引起的问题]]></string>
<string name="donate">捐赠</string>
<string name="clone_profile">复制配置</string>
</resources>

View File

@@ -25,7 +25,6 @@
<string name="recently">Recently</string>
<string name="format_seconds">%d seconds</string>
<string name="format_minutes">%d minutes</string>
<string name="format_hours">%d hours</string>
<string name="format_days">%d days</string>
@@ -46,8 +45,8 @@
<string name="profile_name">Profile Name</string>
<string name="profile_url">Profile URL</string>
<string name="auto_update">Auto Update (Seconds)</string>
<string name="seconds">Seconds</string>
<string name="auto_update">Auto Update (Minutes)</string>
<string name="more_than_15_minutes">More than 15 minutes</string>
<string name="invalid_interval">Invalid Interval</string>
<string name="download_failure">Download Failure</string>
<string name="detail">Detail</string>
@@ -66,12 +65,13 @@
<string name="empty_name">Empty Name</string>
<string name="invalid_url">Invalid URL</string>
<string name="processing">Processing</string>
<string name="loading">Loading</string>
<string name="new_profile">New Profile</string>
<string name="clone_profile">Clone Profile</string>
<string name="edit_profile">Edit Profile</string>
<string name="clash_start_failure">Clash Start Failure</string>
<string name="refresh">Refresh</string>
<string name="copied">Copied</string>
<string name="delay">Delay</string>
<string name="direct">Direct</string>
@@ -107,20 +107,18 @@
<string name="network">Network</string>
<string name="interface_">Interface</string>
<string name="boot">Boot</string>
<string name="start_on_boot">Start on Boot</string>
<string name="start_clash_on_system_boot">Start Clash on system boot</string>
<string name="restart">Restart</string>
<string name="auto_restart">Auto Restart</string>
<string name="allow_clash_auto_restart">Allow clash auto restart</string>
<string name="notification">Notification</string>
<string name="show_traffic">Show Traffic</string>
<string name="show_traffic_summary">Auto refresh traffic in notification</string>
<string name="route_system_traffic">Route System Traffic</string>
<string name="routing_via_vpn_service">Routing all system traffic via VpnService</string>
<string name="routing_via_vpn_service">Auto routing all system traffic via VpnService</string>
<string name="vpn_service">VPN Service</string>
<string name="ipv6">IPv6</string>
<string name="enable_ipv6">Enable IPv6 support (not recommend)</string>
<string name="bypass_private_network">Bypass Private Network</string>
<string name="bypass_private_network_summary">Bypass private network addresses</string>
<string name="dns_hijacking">DNS Hijacking</string>
@@ -152,15 +150,15 @@
<string name="clash_url" translatable="false">https://github.com/Dreamacro/clash</string>
<string name="clash_for_android_url" translatable="false">https://github.com/Kr328/ClashForAndroid</string>
<string name="contacts">Contacts</string>
<string name="email">E-Mail</string>
<string name="feedback">Feedback</string>
<string name="donate">Donate</string>
<string name="github_issues">Github Issues</string>
<string name="telegram_channel">Telegram Channel</string>
<string name="email_url" translatable="false">kr328app@outlook.com</string>
<string name="github_issues_url" translatable="false">https://github.com/Kr328/ClashForAndroid/issues</string>
<string name="telegram_channel_url" translatable="false">https://t.me/clash_for_android_channel</string>
<string name="tips_profile"><![CDATA[Accept Only <strong>Clash Config</strong> which contains <br /><strong>Proxies, Proxy Groups and Rules</strong>]]></string>
<string name="tips_support"><![CDATA[Clash for Android is <strong>free</strong> and <strong>open source</strong> <br /> It does <strong>NOT</strong> provide any proxy services]]></string>
<string name="application_broken_description"><![CDATA[This is usually because you used a copy from <strong>Google Play</strong>, but the app that generated the copy did not handle <strong>Split Apks</strong> correctly <br />This means you get <strong>part of the app</strong>, not all of it]]></string>
<string name="learn_more_about_split_apks">Learn more about Split Apks</string>
@@ -173,4 +171,6 @@
<string name="format_proxy_group_title" translatable="false">%s - %s</string>
<string name="missing_vpn_component">Missing VPN Components</string>
<string name="profile_not_found">Profile not found</string>
</resources>

View File

@@ -1,11 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<!--suppress DeprecatedClassUsageInspection -->
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
<PreferenceCategory android:title="@string/boot">
<PreferenceCategory android:title="@string/restart">
<SwitchPreference
android:key="start_on_boot"
android:title="@string/start_on_boot"
android:summary="@string/start_clash_on_system_boot" />
android:title="@string/auto_restart"
android:summary="@string/allow_clash_auto_restart" />
</PreferenceCategory>
<PreferenceCategory android:title="@string/notification">
<SwitchPreference

View File

@@ -7,11 +7,6 @@
android:summary="@string/routing_via_vpn_service"
android:defaultValue="true" />
<PreferenceCategory android:title="@string/vpn_service" android:dependency="enable_vpn_service">
<SwitchPreference
android:key="ipv6"
android:title="@string/ipv6"
android:summary="@string/enable_ipv6"
android:defaultValue="false"/>
<SwitchPreference
android:key="bypass_private_network"
android:title="@string/bypass_private_network"

View File

@@ -1,53 +0,0 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
ext {
gBuildToolsVersion = "29.0.3"
gCompileSdkVersion = 29
gMinSdkVersion = 24
gTargetSdkVersion = 29
gVersionCode = 10108
gVersionName = "1.1.8"
gKotlinVersion = '1.3.61'
gKotlinCoroutineVersion = '1.3.3'
gKotlinSerializationVersion = '0.14.0'
gRoomVersion = '2.2.4'
gAppCenterVersion = '2.5.1'
gAndroidKtxVersion = "1.2.0"
gLifecycleVersion = "2.2.0"
gRecyclerviewVersion = "1.1.0"
gAppCompatVersion = "1.1.0"
gMaterialDesignVersion = '1.2.0-alpha05'
gShizukuPreferenceVersion = "4.2.0"
gMultiprocessPreferenceVersion = "1.0.0"
}
repositories {
google()
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:3.6.0'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$gKotlinVersion"
classpath "org.jetbrains.kotlin:kotlin-serialization:$gKotlinVersion"
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
}
}
allprojects {
repositories {
google()
jcenter()
maven {
url "https://dl.bintray.com/rikkaw/Libraries"
}
}
}
task clean(type: Delete) {
delete rootProject.buildDir
}

52
build.gradle.kts Normal file
View File

@@ -0,0 +1,52 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
val kotlinVersion = "1.3.71"
rootProject.extra.apply {
this["gBuildToolsVersion"] = "29.0.3"
this["gCompileSdkVersion"] = 29
this["gMinSdkVersion"] = 24
this["gTargetSdkVersion"] = 29
this["gVersionCode"] = 10204
this["gVersionName"] = "1.2.4"
this["gKotlinVersion"] = kotlinVersion
this["gKotlinCoroutineVersion"] = "1.3.5"
this["gKotlinSerializationVersion"] = "0.20.0"
this["gRoomVersion"] = "2.2.5"
this["gAppCenterVersion"] = "2.5.1"
this["gAndroidKtxVersion"] = "1.2.0"
this["gRecyclerviewVersion"] = "1.1.0"
this["gAppCompatVersion"] = "1.1.0"
this["gMaterialDesignVersion"] = "1.1.0"
this["gShizukuPreferenceVersion"] = "4.2.0"
this["gMultiprocessPreferenceVersion"] = "1.0.0"
}
repositories {
google()
jcenter()
}
dependencies {
classpath("com.android.tools.build:gradle:4.0.0-beta04")
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion")
classpath("org.jetbrains.kotlin:kotlin-serialization:$kotlinVersion")
}
}
allprojects {
repositories {
google()
jcenter()
maven {
url = java.net.URI("https://dl.bintray.com/rikkaw/Libraries")
}
}
}
task("clean", type = Delete::class) {
delete(rootProject.buildDir)
}

View File

@@ -1,32 +0,0 @@
apply plugin: 'java'
apply plugin: 'kotlin'
buildscript {
ext.kotlin_version = '1.3.61'
repositories {
mavenCentral()
}
dependencies {
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
}
}
repositories {
mavenCentral()
}
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
}
compileKotlin {
kotlinOptions {
jvmTarget = "1.8"
}
}
compileTestKotlin {
kotlinOptions {
jvmTarget = "1.8"
}
}

View File

@@ -1,197 +0,0 @@
import org.apache.tools.ant.taskdefs.condition.Os
import org.gradle.api.DefaultTask
import org.gradle.api.GradleException
import org.gradle.api.tasks.TaskAction
import java.io.File
import java.io.FileOutputStream
import java.io.FileReader
import java.io.FileWriter
import java.util.*
import java.util.zip.ZipFile
open class GolangBindTask : DefaultTask() {
companion object {
private val STUB_GO_MOD_CONTENT = """
module github.com/kr328/cfa-bind
require (
github.com/kr328/cfa v0.0.0 // redirect
)
replace github.com/kr328/cfa v0.0.0 => {SOURCE_PATH}
""".trimIndent()
private val STUB_GO_FILE_CONTENT = """
package main
import "github.com/kr328/cfa/bridge"
func main() {}
""".trimIndent()
private val REGEX_REPLACE_TARGET_LOCAL = Regex("=>\\s+\\./")
private val REGEX_REPLACE_SOURCE_VERSION = Regex("v.+\\s+=>")
}
private val javaOutput: File
get() {
return project.buildDir.resolve("intermediates/go_output/generate_java")
}
private val nativeOutput: File
get() {
return project.buildDir.resolve("intermediates/go_output/native_library")
}
private val goBuildPath: File
get() {
return project.buildDir.resolve("intermediates/go_build")
}
private val goPath: File
get() {
return goBuildPath.resolve("go_path")
}
private val goBindPath: File
get() {
return goBuildPath.resolve("go_bind_path")
}
private val sourcePath: File
get() {
return project.file("src/main/golang")
}
private val properties by lazy {
FileReader(project.rootProject.file("local.properties")).use {
Properties().apply { load(it) }
}
}
private val environment = mutableMapOf<String, String>()
init {
onlyIf {
val lastModify = sourcePath.walk()
.filter { it.extension == "go" || it.extension == "mod" }
.map { it.lastModified() }
.max() ?: 0L
return@onlyIf goBuildPath.resolve("bridge.aar").lastModified() < lastModify
}
}
@TaskAction
fun process() {
environment["GOPATH"] = goPath.absolutePath
environment["ANDROID_HOME"] = findAndroidSdkPath().absolutePath
environment["ANDROID_NDK_HOME"] = findAndroidNdkPath().absolutePath
if (Os.isFamily(Os.FAMILY_WINDOWS))
environment["Path"] = System.getenv("Path") + ";" + goPath.resolve("bin")
else
environment["PATH"] = System.getenv("PATH") + ":" + goPath.resolve("bin")
goPath.resolve("src/github.com/kr328").deleteRecursively()
goBindPath.deleteRecursively()
goBindPath.mkdirs()
"go get golang.org/x/mobile/cmd/gomobile".exec()
FileWriter(goBindPath.resolve("go.mod")).use {
it.write(buildStubGoModule(sourcePath))
}
FileWriter(goBindPath.resolve("main.go")).use {
it.write(STUB_GO_FILE_CONTENT)
}
"go mod vendor".exec(goBindPath)
goBindPath.resolve("vendor")
.copyRecursively(goPath.resolve("src"), overwrite = true)
"gomobile init".exec(goBuildPath)
"gomobile bind -target=android \"-gcflags=all=-trimpath=$goPath\" \"-ldflags=-w -s\" github.com/kr328/cfa/bridge".exec(goBuildPath)
nativeOutput.deleteRecursively()
javaOutput.deleteRecursively()
with(ZipFile(goBuildPath.resolve("bridge.aar"))) {
stream()
.filter { !it.isDirectory }
.filter { it.name.startsWith("jni") }
.forEach {
val target = nativeOutput.resolve(it.name.removePrefix("jni/"))
target.parentFile.mkdirs()
FileOutputStream(target).use { output ->
getInputStream(it).use { input ->
input.copyTo(output)
}
}
}
}
with(ZipFile(goBuildPath.resolve("bridge-sources.jar"))) {
stream()
.filter { !it.isDirectory }
.filter { it.name.endsWith(".java") }
.forEach {
val target = javaOutput.resolve(it.name)
target.parentFile.mkdirs()
FileOutputStream(target).use { output ->
getInputStream(it).use { input ->
input.copyTo(output)
}
}
}
}
}
private fun findAndroidNdkPath(): File {
return properties.getProperty("ndk.dir")?.let { File(it) }?.takeIf { it.exists() }
?: throw GradleException("Android NDK not found.")
}
private fun findAndroidSdkPath(): File {
return properties.getProperty("sdk.dir")?.let { File(it) }?.takeIf { it.exists() }
?: throw GradleException("Android SDK not found.")
}
private fun buildStubGoModule(source: File): String {
val replaces = source.walk()
.filter { it.name == "go.mod" }
.flatMap { file ->
file.readLines()
.asSequence()
.filter { line -> line.startsWith("replace") }
.map { replace ->
replace.replace(REGEX_REPLACE_TARGET_LOCAL, "=> " + file.parentFile.absolutePath.replace('\\','/') + "/")
}
.map { replace ->
replace.replace(REGEX_REPLACE_SOURCE_VERSION, " =>")
}
}
.joinToString("\n")
return STUB_GO_MOD_CONTENT.replace("{SOURCE_PATH}", source.absolutePath) +
"\n\n" + replaces
}
private fun String.exec(pwd: File = File(".")) {
val process = with(ProcessBuilder()) {
if (Os.isFamily(Os.FAMILY_WINDOWS))
command("cmd.exe", "/c", this@exec)
else
command("bash", "-c", this@exec)
environment().putAll(environment)
directory(pwd)
redirectErrorStream(true)
start()
}
process.inputStream.copyTo(System.out)
System.out.flush()
if (process.waitFor() != 0)
throw GradleException("Run command $this failure")
}
}

View File

@@ -1,41 +0,0 @@
import org.gradle.api.DefaultTask
import org.gradle.api.GradleException
import org.gradle.api.tasks.TaskAction
import java.io.File
import java.io.FileOutputStream
import java.net.HttpURLConnection
import java.net.URL
open class MMDBDowloadTask : DefaultTask() {
companion object {
const val URL = "https://github.com/Dreamacro/maxmind-geoip/releases/latest/download/Country.mmdb"
}
var output: String = ""
@TaskAction
fun exec() {
val file = File(output).apply {
parentFile?.mkdirs()
}
try {
(URL(URL).openConnection() as HttpURLConnection).apply {
instanceFollowRedirects = true
connect()
require(responseCode / 100 == 2)
FileOutputStream(file).use {
inputStream.copyTo(it)
}
disconnect()
}
}
catch (e: Throwable) {
e.printStackTrace()
throw GradleException("Download failure", e)
}
}
}

63
common/build.gradle.kts Normal file
View File

@@ -0,0 +1,63 @@
plugins {
id("com.android.library")
id("kotlin-android")
id("kotlin-android-extensions")
}
val rootExtra = rootProject.extra
val gCompileSdkVersion: Int by rootExtra
val gBuildToolsVersion: String by rootExtra
val gMinSdkVersion: Int by rootExtra
val gTargetSdkVersion: Int by rootExtra
val gVersionCode: Int by rootExtra
val gVersionName: String by rootExtra
val gKotlinVersion: String by rootExtra
val gKotlinCoroutineVersion: String by rootExtra
val gAndroidKtxVersion: String by rootExtra
val gKotlinSerializationVersion: String by rootExtra
android {
compileSdkVersion(gCompileSdkVersion)
buildToolsVersion(gBuildToolsVersion)
defaultConfig {
minSdkVersion(gMinSdkVersion)
targetSdkVersion(gTargetSdkVersion)
versionCode = gVersionCode
versionName = gVersionName
consumerProguardFiles("consumer-rules.pro")
}
buildTypes {
maybeCreate("release").apply {
isMinifyEnabled = false
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = "1.8"
}
}
dependencies {
implementation("androidx.core:core-ktx:$gAndroidKtxVersion")
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8:$gKotlinVersion")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$gKotlinCoroutineVersion")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-runtime:$gKotlinSerializationVersion")
}
repositories {
mavenCentral()
}

View File

21
common/proguard-rules.pro vendored Normal file
View File

@@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile

View File

@@ -0,0 +1,9 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.github.kr328.clash.common">
<permission
android:name="${applicationId}.permission.RECEIVE_BROADCASTS"
android:label="@string/receive_clash_broadcasts"
android:description="@string/receive_broadcasts_of_clash"
android:protectionLevel="privileged|signature" />
<uses-permission android:name="${applicationId}.permission.RECEIVE_BROADCASTS" />
</manifest>

View File

@@ -1,4 +1,4 @@
package com.github.kr328.clash.core
package com.github.kr328.clash.common
object Constants {
const val TAG = "ClashForAndroid"

View File

@@ -0,0 +1,16 @@
package com.github.kr328.clash.common
import android.app.Application
import android.content.Intent
object Global {
var openMainIntent: () -> Intent = { Intent() }
var openProfileIntent: (Long) -> Intent = { Intent() }
lateinit var application: Application
private set
fun init(application: Application) {
Global.application = application
}
}

View File

@@ -0,0 +1,6 @@
package com.github.kr328.clash.common
object Permissions {
val PERMISSION_RECEIVE_BROADCASTS: String
get() = Global.application.packageName + ".permission.RECEIVE_BROADCASTS"
}

View File

@@ -0,0 +1,23 @@
package com.github.kr328.clash.common.ids
import com.github.kr328.clash.common.BuildConfig
object Intents {
const val INTENT_ACTION_CLASH_STARTED =
"${BuildConfig.LIBRARY_PACKAGE_NAME}.intent.action.clash.STARTED"
const val INTENT_ACTION_CLASH_STOPPED =
"${BuildConfig.LIBRARY_PACKAGE_NAME}.intent.action.clash.STOPPED"
const val INTENT_ACTION_CLASH_REQUEST_STOP =
"${BuildConfig.LIBRARY_PACKAGE_NAME}.intent.action.clash.REQUEST_STOP"
const val INTENT_ACTION_PROFILE_CHANGED =
"${BuildConfig.LIBRARY_PACKAGE_NAME}.intent.action.profile.CHANGED"
const val INTENT_ACTION_PROFILE_REQUEST_UPDATE =
"${BuildConfig.LIBRARY_PACKAGE_NAME}.intent.action.profile.REQUEST_UPDATE"
const val INTENT_ACTION_PROFILE_LOADED =
"${BuildConfig.LIBRARY_PACKAGE_NAME}.intent.action.profile.LOADED"
const val INTENT_ACTION_NETWORK_CHANGED =
"${BuildConfig.LIBRARY_PACKAGE_NAME}.intent.action.network.CHANGED"
const val INTENT_EXTRA_CLASH_STOP_REASON =
"${BuildConfig.LIBRARY_PACKAGE_NAME}.intent.extra.clash.STOP_REASON"
}

View File

@@ -0,0 +1,7 @@
package com.github.kr328.clash.common.ids
object NotificationChannels {
const val CLASH_STATUS = "clash_status_channel"
const val PROFILE_STATUS = "profile_status_channel"
const val PROFILE_RESULT = "profile_result_channel"
}

View File

@@ -0,0 +1,12 @@
package com.github.kr328.clash.common.ids
object NotificationIds {
const val CLASH_STATUS = 1
const val PROFILE_STATUS = 2
private val PROFILE_RESULT = 10000..20000
fun generateProfileResultId(profileId: Long): Int {
val bound = PROFILE_RESULT.last - PROFILE_RESULT.first
return (profileId % bound + PROFILE_RESULT.first).toInt()
}
}

View File

@@ -0,0 +1,9 @@
package com.github.kr328.clash.common.ids
object PendingIds {
const val CLASH_VPN = 1
fun generateProfileResultId(profileId: Long): Int {
return NotificationIds.generateProfileResultId(profileId)
}
}

View File

@@ -1,11 +1,11 @@
package com.github.kr328.clash.core.serialization
package com.github.kr328.clash.common.serialization
import android.os.Parcel
import kotlinx.serialization.*
import kotlinx.serialization.modules.EmptyModule
import kotlinx.serialization.modules.SerialModule
object MergedParcels : AbstractSerialFormat(EmptyModule) {
object MergedParcels: SerialFormat {
fun <T> dump(serializer: SerializationStrategy<T>, obj: T, parcel: Parcel) {
val data = Parcel.obtain()
val encoder = ParcelsEncoder(data)
@@ -45,64 +45,63 @@ object MergedParcels : AbstractSerialFormat(EmptyModule) {
get() = EmptyModule
override fun beginCollection(
desc: SerialDescriptor,
descriptor: SerialDescriptor,
collectionSize: Int,
vararg typeParams: KSerializer<*>
vararg typeSerializers: KSerializer<*>
): CompositeEncoder {
encodeInt(collectionSize)
return super.beginCollection(desc, collectionSize, *typeParams)
return super.beginCollection(descriptor, collectionSize, *typeSerializers)
}
override fun encodeBooleanElement(desc: SerialDescriptor, index: Int, value: Boolean) =
override fun encodeBooleanElement(descriptor: SerialDescriptor, index: Int, value: Boolean) =
encodeBoolean(value)
override fun encodeByteElement(desc: SerialDescriptor, index: Int, value: Byte) =
override fun encodeByteElement(descriptor: SerialDescriptor, index: Int, value: Byte) =
encodeByte(value)
override fun encodeCharElement(desc: SerialDescriptor, index: Int, value: Char) =
override fun encodeCharElement(descriptor: SerialDescriptor, index: Int, value: Char) =
encodeChar(value)
override fun encodeDoubleElement(desc: SerialDescriptor, index: Int, value: Double) =
override fun encodeDoubleElement(descriptor: SerialDescriptor, index: Int, value: Double) =
encodeDouble(value)
override fun encodeFloatElement(desc: SerialDescriptor, index: Int, value: Float) =
override fun encodeFloatElement(descriptor: SerialDescriptor, index: Int, value: Float) =
encodeFloat(value)
override fun encodeIntElement(desc: SerialDescriptor, index: Int, value: Int) =
override fun encodeIntElement(descriptor: SerialDescriptor, index: Int, value: Int) =
encodeInt(value)
override fun encodeLongElement(desc: SerialDescriptor, index: Int, value: Long) =
override fun encodeLongElement(descriptor: SerialDescriptor, index: Int, value: Long) =
encodeLong(value)
override fun encodeShortElement(desc: SerialDescriptor, index: Int, value: Short) =
override fun encodeShortElement(descriptor: SerialDescriptor, index: Int, value: Short) =
encodeShort(value)
override fun encodeStringElement(desc: SerialDescriptor, index: Int, value: String) =
override fun encodeStringElement(descriptor: SerialDescriptor, index: Int, value: String) =
encodeString(value)
override fun encodeUnitElement(desc: SerialDescriptor, index: Int) =
override fun encodeUnitElement(descriptor: SerialDescriptor, index: Int) =
encodeUnit()
override fun encodeNonSerializableElement(desc: SerialDescriptor, index: Int, value: Any) =
throw IllegalArgumentException("Unsupported")
override fun endStructure(descriptor: SerialDescriptor) {}
override fun <T : Any> encodeNullableSerializableElement(
desc: SerialDescriptor,
descriptor: SerialDescriptor,
index: Int,
serializer: SerializationStrategy<T>,
value: T?
) = encodeNullableSerializableValue(serializer, value)
override fun <T> encodeSerializableElement(
desc: SerialDescriptor,
descriptor: SerialDescriptor,
index: Int,
serializer: SerializationStrategy<T>,
value: T
) = encodeSerializableValue(serializer, value)
override fun beginStructure(
desc: SerialDescriptor,
vararg typeParams: KSerializer<*>
descriptor: SerialDescriptor,
vararg typeSerializers: KSerializer<*>
): CompositeEncoder = this
override fun encodeBoolean(value: Boolean) =
@@ -117,8 +116,8 @@ object MergedParcels : AbstractSerialFormat(EmptyModule) {
override fun encodeDouble(value: Double) =
parcel.writeDouble(value)
override fun encodeEnum(enumDescription: SerialDescriptor, ordinal: Int) =
parcel.writeInt(ordinal)
override fun encodeEnum(enumDescriptor: SerialDescriptor, index: Int) =
parcel.writeInt(index)
override fun encodeFloat(value: Float) =
parcel.writeFloat(value)
@@ -155,70 +154,75 @@ object MergedParcels : AbstractSerialFormat(EmptyModule) {
override val updateMode: UpdateMode
get() = UpdateMode.BANNED
override fun decodeElementIndex(desc: SerialDescriptor) =
CompositeDecoder.READ_ALL
override fun decodeSequentially() =
true
override fun decodeCollectionSize(desc: SerialDescriptor) =
override fun decodeElementIndex(descriptor: SerialDescriptor) =
CompositeDecoder.UNKNOWN_NAME
override fun decodeCollectionSize(descriptor: SerialDescriptor) =
decodeInt()
override fun decodeBooleanElement(desc: SerialDescriptor, index: Int) =
override fun decodeBooleanElement(descriptor: SerialDescriptor, index: Int) =
decodeBoolean()
override fun decodeByteElement(desc: SerialDescriptor, index: Int) =
override fun decodeByteElement(descriptor: SerialDescriptor, index: Int) =
decodeByte()
override fun decodeCharElement(desc: SerialDescriptor, index: Int) =
override fun decodeCharElement(descriptor: SerialDescriptor, index: Int) =
decodeChar()
override fun decodeDoubleElement(desc: SerialDescriptor, index: Int) =
override fun decodeDoubleElement(descriptor: SerialDescriptor, index: Int) =
decodeDouble()
override fun decodeFloatElement(desc: SerialDescriptor, index: Int) =
override fun decodeFloatElement(descriptor: SerialDescriptor, index: Int) =
decodeFloat()
override fun decodeIntElement(desc: SerialDescriptor, index: Int) =
override fun decodeIntElement(descriptor: SerialDescriptor, index: Int) =
decodeInt()
override fun decodeShortElement(desc: SerialDescriptor, index: Int) =
override fun decodeShortElement(descriptor: SerialDescriptor, index: Int) =
decodeShort()
override fun decodeLongElement(desc: SerialDescriptor, index: Int) =
override fun decodeLongElement(descriptor: SerialDescriptor, index: Int) =
decodeLong()
override fun decodeStringElement(desc: SerialDescriptor, index: Int) =
override fun decodeStringElement(descriptor: SerialDescriptor, index: Int) =
decodeString()
override fun decodeUnitElement(desc: SerialDescriptor, index: Int) =
override fun decodeUnitElement(descriptor: SerialDescriptor, index: Int) =
decodeUnit()
override fun endStructure(descriptor: SerialDescriptor) {}
override fun <T : Any> decodeNullableSerializableElement(
desc: SerialDescriptor,
descriptor: SerialDescriptor,
index: Int,
deserializer: DeserializationStrategy<T?>
) = decodeNullableSerializableValue(deserializer)
override fun <T> decodeSerializableElement(
desc: SerialDescriptor,
descriptor: SerialDescriptor,
index: Int,
deserializer: DeserializationStrategy<T>
) = decodeSerializableValue(deserializer)
override fun <T : Any> updateNullableSerializableElement(
desc: SerialDescriptor,
descriptor: SerialDescriptor,
index: Int,
deserializer: DeserializationStrategy<T?>,
old: T?
) = updateNullableSerializableValue(deserializer, old)
override fun <T> updateSerializableElement(
desc: SerialDescriptor,
descriptor: SerialDescriptor,
index: Int,
deserializer: DeserializationStrategy<T>,
old: T
) = updateSerializableValue(deserializer, old)
override fun beginStructure(
desc: SerialDescriptor,
descriptor: SerialDescriptor,
vararg typeParams: KSerializer<*>
): CompositeDecoder = this
@@ -234,7 +238,7 @@ object MergedParcels : AbstractSerialFormat(EmptyModule) {
override fun decodeDouble() =
parcel.readDouble()
override fun decodeEnum(enumDescription: SerialDescriptor) =
override fun decodeEnum(enumDescriptor: SerialDescriptor) =
parcel.readInt()
override fun decodeFloat() =
@@ -263,6 +267,8 @@ object MergedParcels : AbstractSerialFormat(EmptyModule) {
}
}
override val context: SerialModule = EmptyModule
}

View File

@@ -1,11 +1,11 @@
package com.github.kr328.clash.core.serialization
package com.github.kr328.clash.common.serialization
import android.os.Parcel
import kotlinx.serialization.*
import kotlinx.serialization.modules.EmptyModule
import kotlinx.serialization.modules.SerialModule
object Parcels : AbstractSerialFormat(EmptyModule) {
object Parcels : SerialFormat {
fun <T> dump(serializer: SerializationStrategy<T>, obj: T, parcel: Parcel) {
serializer.serialize(ParcelsEncoder(parcel), obj)
}
@@ -20,64 +20,63 @@ object Parcels : AbstractSerialFormat(EmptyModule) {
get() = EmptyModule
override fun beginCollection(
desc: SerialDescriptor,
descriptor: SerialDescriptor,
collectionSize: Int,
vararg typeParams: KSerializer<*>
vararg typeSerializers: KSerializer<*>
): CompositeEncoder {
encodeInt(collectionSize)
return super.beginCollection(desc, collectionSize, *typeParams)
return super.beginCollection(descriptor, collectionSize, *typeSerializers)
}
override fun encodeBooleanElement(desc: SerialDescriptor, index: Int, value: Boolean) =
override fun encodeBooleanElement(descriptor: SerialDescriptor, index: Int, value: Boolean) =
encodeBoolean(value)
override fun encodeByteElement(desc: SerialDescriptor, index: Int, value: Byte) =
override fun encodeByteElement(descriptor: SerialDescriptor, index: Int, value: Byte) =
encodeByte(value)
override fun encodeCharElement(desc: SerialDescriptor, index: Int, value: Char) =
override fun encodeCharElement(descriptor: SerialDescriptor, index: Int, value: Char) =
encodeChar(value)
override fun encodeDoubleElement(desc: SerialDescriptor, index: Int, value: Double) =
override fun encodeDoubleElement(descriptor: SerialDescriptor, index: Int, value: Double) =
encodeDouble(value)
override fun encodeFloatElement(desc: SerialDescriptor, index: Int, value: Float) =
override fun encodeFloatElement(descriptor: SerialDescriptor, index: Int, value: Float) =
encodeFloat(value)
override fun encodeIntElement(desc: SerialDescriptor, index: Int, value: Int) =
override fun encodeIntElement(descriptor: SerialDescriptor, index: Int, value: Int) =
encodeInt(value)
override fun encodeLongElement(desc: SerialDescriptor, index: Int, value: Long) =
override fun encodeLongElement(descriptor: SerialDescriptor, index: Int, value: Long) =
encodeLong(value)
override fun encodeShortElement(desc: SerialDescriptor, index: Int, value: Short) =
override fun encodeShortElement(descriptor: SerialDescriptor, index: Int, value: Short) =
encodeShort(value)
override fun encodeStringElement(desc: SerialDescriptor, index: Int, value: String) =
override fun encodeStringElement(descriptor: SerialDescriptor, index: Int, value: String) =
encodeString(value)
override fun encodeUnitElement(desc: SerialDescriptor, index: Int) =
override fun encodeUnitElement(descriptor: SerialDescriptor, index: Int) =
encodeUnit()
override fun encodeNonSerializableElement(desc: SerialDescriptor, index: Int, value: Any) =
throw IllegalArgumentException("Unsupported")
override fun endStructure(descriptor: SerialDescriptor) {}
override fun <T : Any> encodeNullableSerializableElement(
desc: SerialDescriptor,
descriptor: SerialDescriptor,
index: Int,
serializer: SerializationStrategy<T>,
value: T?
) = encodeNullableSerializableValue(serializer, value)
override fun <T> encodeSerializableElement(
desc: SerialDescriptor,
descriptor: SerialDescriptor,
index: Int,
serializer: SerializationStrategy<T>,
value: T
) = encodeSerializableValue(serializer, value)
override fun beginStructure(
desc: SerialDescriptor,
vararg typeParams: KSerializer<*>
descriptor: SerialDescriptor,
vararg typeSerializers: KSerializer<*>
): CompositeEncoder = this
override fun encodeBoolean(value: Boolean) =
@@ -92,8 +91,8 @@ object Parcels : AbstractSerialFormat(EmptyModule) {
override fun encodeDouble(value: Double) =
parcel.writeDouble(value)
override fun encodeEnum(enumDescription: SerialDescriptor, ordinal: Int) =
parcel.writeInt(ordinal)
override fun encodeEnum(enumDescriptor: SerialDescriptor, index: Int) =
parcel.writeInt(index)
override fun encodeFloat(value: Float) =
parcel.writeFloat(value)
@@ -125,70 +124,75 @@ object Parcels : AbstractSerialFormat(EmptyModule) {
override val updateMode: UpdateMode
get() = UpdateMode.BANNED
override fun decodeElementIndex(desc: SerialDescriptor) =
CompositeDecoder.READ_ALL
override fun decodeSequentially() =
true
override fun decodeCollectionSize(desc: SerialDescriptor) =
override fun decodeElementIndex(descriptor: SerialDescriptor) =
CompositeDecoder.UNKNOWN_NAME
override fun decodeCollectionSize(descriptor: SerialDescriptor) =
decodeInt()
override fun decodeBooleanElement(desc: SerialDescriptor, index: Int) =
override fun decodeBooleanElement(descriptor: SerialDescriptor, index: Int) =
decodeBoolean()
override fun decodeByteElement(desc: SerialDescriptor, index: Int) =
override fun decodeByteElement(descriptor: SerialDescriptor, index: Int) =
decodeByte()
override fun decodeCharElement(desc: SerialDescriptor, index: Int) =
override fun decodeCharElement(descriptor: SerialDescriptor, index: Int) =
decodeChar()
override fun decodeDoubleElement(desc: SerialDescriptor, index: Int) =
override fun decodeDoubleElement(descriptor: SerialDescriptor, index: Int) =
decodeDouble()
override fun decodeFloatElement(desc: SerialDescriptor, index: Int) =
override fun decodeFloatElement(descriptor: SerialDescriptor, index: Int) =
decodeFloat()
override fun decodeIntElement(desc: SerialDescriptor, index: Int) =
override fun decodeIntElement(descriptor: SerialDescriptor, index: Int) =
decodeInt()
override fun decodeShortElement(desc: SerialDescriptor, index: Int) =
override fun decodeShortElement(descriptor: SerialDescriptor, index: Int) =
decodeShort()
override fun decodeLongElement(desc: SerialDescriptor, index: Int) =
override fun decodeLongElement(descriptor: SerialDescriptor, index: Int) =
decodeLong()
override fun decodeStringElement(desc: SerialDescriptor, index: Int) =
override fun decodeStringElement(descriptor: SerialDescriptor, index: Int) =
decodeString()
override fun decodeUnitElement(desc: SerialDescriptor, index: Int) =
override fun decodeUnitElement(descriptor: SerialDescriptor, index: Int) =
decodeUnit()
override fun endStructure(descriptor: SerialDescriptor) {}
override fun <T : Any> decodeNullableSerializableElement(
desc: SerialDescriptor,
descriptor: SerialDescriptor,
index: Int,
deserializer: DeserializationStrategy<T?>
) = decodeNullableSerializableValue(deserializer)
override fun <T> decodeSerializableElement(
desc: SerialDescriptor,
descriptor: SerialDescriptor,
index: Int,
deserializer: DeserializationStrategy<T>
) = decodeSerializableValue(deserializer)
override fun <T : Any> updateNullableSerializableElement(
desc: SerialDescriptor,
descriptor: SerialDescriptor,
index: Int,
deserializer: DeserializationStrategy<T?>,
old: T?
) = updateNullableSerializableValue(deserializer, old)
override fun <T> updateSerializableElement(
desc: SerialDescriptor,
descriptor: SerialDescriptor,
index: Int,
deserializer: DeserializationStrategy<T>,
old: T
) = updateSerializableValue(deserializer, old)
override fun beginStructure(
desc: SerialDescriptor,
descriptor: SerialDescriptor,
vararg typeParams: KSerializer<*>
): CompositeDecoder = this
@@ -204,7 +208,7 @@ object Parcels : AbstractSerialFormat(EmptyModule) {
override fun decodeDouble() =
parcel.readDouble()
override fun decodeEnum(enumDescription: SerialDescriptor) =
override fun decodeEnum(enumDescriptor: SerialDescriptor) =
parcel.readInt()
override fun decodeFloat() =
@@ -230,6 +234,8 @@ object Parcels : AbstractSerialFormat(EmptyModule) {
override fun decodeUnit() {}
}
override val context: SerialModule = EmptyModule
}

View File

@@ -1,4 +1,4 @@
package com.github.kr328.clash.service.settings
package com.github.kr328.clash.common.settings
import android.content.SharedPreferences
@@ -8,7 +8,8 @@ abstract class BaseSettings(private val preferences: SharedPreferences) {
fun put(editor: SharedPreferences.Editor, value: T)
}
class StringEntry(private val key: String, private val defaultValue: String) : Entry<String> {
class StringEntry(private val key: String, private val defaultValue: String) :
Entry<String> {
override fun get(preferences: SharedPreferences): String {
return preferences.getString(key, defaultValue)!!
}

View File

@@ -1,4 +1,4 @@
package com.github.kr328.clash.core.utils
package com.github.kr328.clash.common.utils
object ByteFormatter {
fun byteToString(bytes: Long): String {

View File

@@ -1,8 +1,8 @@
package com.github.kr328.clash.service.util
package com.github.kr328.clash.common.utils
import android.content.ComponentName
import android.content.Intent
import com.github.kr328.clash.core.Global
import com.github.kr328.clash.common.Global
import kotlin.reflect.KClass
val KClass<*>.componentName: ComponentName

View File

@@ -1,4 +1,4 @@
package com.github.kr328.clash.service.util
package com.github.kr328.clash.common.utils
import android.content.Context
import android.content.res.Configuration

View File

@@ -1,6 +1,6 @@
package com.github.kr328.clash.core.utils
package com.github.kr328.clash.common.utils
import com.github.kr328.clash.core.Constants.TAG
import com.github.kr328.clash.common.Constants.TAG
object Log {
fun i(message: String, throwable: Throwable? = null) =

View File

@@ -0,0 +1,13 @@
package com.github.kr328.clash.common.utils
import android.content.Context
import android.content.Intent
import android.os.Build
fun Context.startForegroundServiceCompat(intent: Intent) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
startForegroundService(intent)
} else {
startService(intent)
}
}

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="receive_clash_broadcasts">接收 Clash 广播</string>
<string name="receive_broadcasts_of_clash">接收来自 Clash 内部的广播</string>
</resources>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="receive_clash_broadcasts">Receive Clash Broadcasts</string>
<string name="receive_broadcasts_of_clash">Receive broadcasts of clash services</string>
</resources>

View File

@@ -1,66 +0,0 @@
apply plugin: 'com.android.library'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
apply plugin: 'kotlinx-serialization'
android {
compileSdkVersion gCompileSdkVersion
buildToolsVersion gBuildToolsVersion
defaultConfig {
minSdkVersion gMinSdkVersion
targetSdkVersion gTargetSdkVersion
versionCode gVersionCode
versionName gVersionName
consumerProguardFiles 'consumer-rules.pro'
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
sourceSets {
main {
assets.srcDirs += ["$buildDir/intermediates/dynamic_assets"]
jniLibs.srcDirs += ["$buildDir/intermediates/go_output/native_library"]
java.srcDirs += ["$buildDir/intermediates/go_output/generate_java"]
}
}
compileOptions {
sourceCompatibility = 1.8
targetCompatibility = 1.8
}
kotlinOptions {
jvmTarget = "1.8"
}
}
dependencies {
implementation "androidx.core:core-ktx:$gAndroidKtxVersion"
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$gKotlinVersion"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$gKotlinCoroutineVersion"
implementation "org.jetbrains.kotlinx:kotlinx-serialization-runtime:$gKotlinSerializationVersion"
}
afterEvaluate {
def ds = tasks.register("downloadMMDB", MMDBDowloadTask.class) {
onlyIf {
System.currentTimeMillis() - file("$buildDir/intermediates/dynamic_assets/Country.mmdb").lastModified() > 7 * 24 * 3600 * 1000L
}
output = "$buildDir/intermediates/dynamic_assets/Country.mmdb"
}
def gs = tasks.register("golangBind", GolangBindTask.class)
preBuild.dependsOn(ds, gs)
}
repositories {
mavenCentral()
}

82
core/build.gradle.kts Normal file
View File

@@ -0,0 +1,82 @@
apply(from = "clash.gradle.kts")
plugins {
id("com.android.library")
id("kotlin-android")
id("kotlin-android-extensions")
id("kotlinx-serialization")
}
val rootExtra = rootProject.extra
val gCompileSdkVersion: Int by rootExtra
val gBuildToolsVersion: String by rootExtra
val gMinSdkVersion: Int by rootExtra
val gTargetSdkVersion: Int by rootExtra
val gVersionCode: Int by rootExtra
val gVersionName: String by rootExtra
val gKotlinVersion: String by rootExtra
val gKotlinCoroutineVersion: String by rootExtra
val gKotlinSerializationVersion: String by rootExtra
val gAndroidKtxVersion: String by rootExtra
val clashCoreOutput = buildDir.resolve("extraSources")
android {
compileSdkVersion(gCompileSdkVersion)
buildToolsVersion(gBuildToolsVersion)
defaultConfig {
minSdkVersion(gMinSdkVersion)
targetSdkVersion(gTargetSdkVersion)
versionCode = gVersionCode
versionName = gVersionName
consumerProguardFiles("consumer-rules.pro")
}
buildTypes {
maybeCreate("release").apply {
isMinifyEnabled = false
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
}
}
sourceSets {
maybeCreate("main").apply {
assets.srcDir(clashCoreOutput.resolve("assets"))
jniLibs.srcDir(clashCoreOutput.resolve("jniLibs"))
java.srcDir(clashCoreOutput.resolve("classes"))
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = "1.8"
}
}
dependencies {
implementation(project(":common"))
implementation("androidx.core:core-ktx:$gAndroidKtxVersion")
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8:$gKotlinVersion")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$gKotlinCoroutineVersion")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-runtime:$gKotlinSerializationVersion")
}
repositories {
mavenCentral()
}
afterEvaluate {
tasks["clean"].dependsOn(tasks["resetGolangMode"])
tasks["preBuild"].dependsOn(tasks["extractSources"], tasks["downloadGeoipDatabase"])
}

222
core/clash.gradle.kts Normal file
View File

@@ -0,0 +1,222 @@
import org.apache.tools.ant.taskdefs.condition.Os
import java.io.*
import java.util.*
import java.net.*
object Constants {
const val GEOIP_DATABASE_URL = "https://github.com/Dreamacro/maxmind-geoip/releases/latest/download/Country.mmdb"
const val GEOIP_INVALID_INTERVAL = 1000L * 60 * 60 * 24 * 7
const val SOURCE_PATH = "src/main/golang"
const val OUTPUT_PATH = "extraSources"
const val GOLANG_BASE = "intermediates/golang"
const val GOLANG_PATH = "$GOLANG_BASE/path"
const val GOLANG_BIND = "$GOLANG_BASE/bind"
const val GOLANG_BINARY = "$GOLANG_PATH/bin"
const val GOLANG_OUTPUT = "$GOLANG_BASE/bridge.aar"
const val GOLANG_OUTPUT_SOURCES = "$GOLANG_BASE/bridge-sources.jar"
val STUB_GO_FILE_CONTENT = """
package main
import "github.com/kr328/cfa/bridge"
func main() {}
""".trimIndent()
val STUB_GO_MOD_CONTENT = """
module github.com/kr328/cfa-bind
require github.com/kr328/cfa v0.0.0 // redirect
replace github.com/kr328/cfa => {SOURCE_PATH}
""".trimIndent()
val REGEX_REPLACE = Regex("replace\\s+(\\S+)\\s+(\\S*)\\s*=>\\s*(\\S+)\\s*(\\S*)\\s*")
val REGEX_JNI = Regex("^jni/")
}
fun generateGolangBuildEnvironment(vararg pathAppend: String): Map<String, String> {
val environment = TreeMap<String, String>(String.CASE_INSENSITIVE_ORDER).apply { putAll(System.getenv()) }
val properties = Properties().apply { load(rootProject.file("local.properties").inputStream()) }
val sdkPath = properties.getProperty("sdk.dir")
?: throw GradleScriptException("sdk.dir not found", FileNotFoundException())
val ndkPath = properties.getProperty("ndk.dir")
?: throw GradleScriptException("ndk.dir not found", FileNotFoundException())
val pathSeparator = if ( Os.isFamily(Os.FAMILY_WINDOWS) ) ";" else ":"
environment["GOPATH"] = listOf(buildDir.resolve(Constants.GOLANG_PATH).absolutePath, *pathAppend)
.joinToString(separator = pathSeparator)
environment["ANDROID_HOME"] = sdkPath
environment["ANDROID_NDK_HOME"] = ndkPath
environment["PATH"] += "$pathSeparator${buildDir.resolve(Constants.GOLANG_BINARY)}"
return environment
}
fun generateGolangModule(): String {
val moduleFile = file(Constants.SOURCE_PATH).resolve("go.mod")
val replaces = moduleFile
.readLines()
.asSequence()
.map { line -> Constants.REGEX_REPLACE.matchEntire(line) }
.filterNotNull()
.map { match ->
val source = match.groupValues[1].trim()
val sVersion = match.groupValues[2].trim()
val target = match.groupValues[3].trim()
val tVersion = match.groupValues[4].trim()
val resolvedTarget = if ( target.startsWith("./") )
moduleFile.parentFile!!.resolve(target).canonicalPath
else
target
"replace $source $sVersion => $resolvedTarget $tVersion"
}.joinToString(separator = "\n")
return Constants.STUB_GO_MOD_CONTENT
.replace("{SOURCE_PATH}", file(Constants.SOURCE_PATH).absolutePath) + replaces
}
fun String.exec(pwd: File = buildDir, env: Map<String, String> = System.getenv()): String {
val process = ProcessBuilder().run {
if ( Os.isFamily(Os.FAMILY_WINDOWS) )
command("cmd.exe", "/c", this@exec)
else
command("bash", "-c", this@exec)
environment().putAll(env)
directory(pwd)
redirectErrorStream(true)
start()
}
val outputStream = ByteArrayOutputStream()
process.inputStream.copyTo(outputStream)
if ( process.waitFor() != 0 ) {
println(outputStream.toString("utf-8"))
throw GradleScriptException("Exec $this failure", IOException())
}
return outputStream.toString("utf-8")
}
task("generateClashBindSources") {
onlyIf {
val lastModified = file(Constants.SOURCE_PATH).walk()
.filter { it.extension == "go" || it.extension == "mod" }
.map { it.lastModified() }
.max() ?: 0L
return@onlyIf lastModified > buildDir.resolve(Constants.GOLANG_OUTPUT).lastModified()
}
doFirst {
buildDir.resolve(Constants.GOLANG_BIND).apply {
deleteRecursively()
mkdirs()
}
}
doLast {
val environment = generateGolangBuildEnvironment()
val bind = buildDir.resolve(Constants.GOLANG_BIND).apply {
resolve("main.go").writeText(Constants.STUB_GO_FILE_CONTENT)
resolve("go.mod").writeText(generateGolangModule())
}
"go mod vendor".exec(pwd = bind, env = environment)
buildDir.resolve(Constants.GOLANG_BIND).apply {
resolve("vendor").renameTo(resolve("src"))
resolve("go.mod").delete()
resolve("main.go").delete()
resolve("go.sum").delete()
}
}
}
task("bindClashCore") {
dependsOn(tasks["generateClashBindSources"])
onlyIf {
!tasks["generateClashBindSources"].state.skipped
}
doFirst {
val environment = generateGolangBuildEnvironment()
"go get golang.org/x/mobile/cmd/gomobile".exec(env = environment)
}
doLast {
val bind = buildDir.resolve(Constants.GOLANG_BIND)
val environment = generateGolangBuildEnvironment(bind.absolutePath)
"gomobile init".exec(pwd = bind, env = environment)
"gomobile bind -target=android -trimpath github.com/kr328/cfa/bridge"
.exec(pwd = buildDir.resolve(Constants.GOLANG_BASE), env = environment)
}
}
task("extractSources", type = Copy::class) {
dependsOn(tasks["bindClashCore"])
doFirst {
buildDir.resolve(Constants.OUTPUT_PATH).apply {
resolve("jniLibs").deleteRecursively()
resolve("classes").deleteRecursively()
}
}
from(zipTree(buildDir.resolve(Constants.GOLANG_OUTPUT))) {
include("**/*.so")
eachFile {
path = path.replace(Constants.REGEX_JNI, "jniLibs/")
}
}
from(zipTree(buildDir.resolve(Constants.GOLANG_OUTPUT_SOURCES))) {
include("**/*.java")
into("classes")
}
destinationDir = buildDir.resolve(Constants.OUTPUT_PATH)
}
task("downloadGeoipDatabase") {
onlyIf {
val file = buildDir.resolve(Constants.OUTPUT_PATH).resolve("assets/Country.mmdb")
System.currentTimeMillis() - file.lastModified() > Constants.GEOIP_INVALID_INTERVAL
}
doLast {
val assets = buildDir.resolve(Constants.OUTPUT_PATH).resolve("assets")
assets.mkdirs()
URL(Constants.GEOIP_DATABASE_URL).openConnection().getInputStream().use { input ->
FileOutputStream(assets.resolve("Country.mmdb")).use { output ->
input.copyTo(output)
}
}
}
}
task("resetGolangMode", type = Exec::class) {
onlyIf {
!Os.isFamily(Os.FAMILY_WINDOWS)
}
commandLine("chmod", "-R", "777", buildDir.resolve(Constants.GOLANG_PATH))
isIgnoreExitValue = true
}

View File

@@ -1,13 +1,12 @@
package bridge
import (
"sync"
"github.com/Dreamacro/clash/component/mmdb"
C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/log"
"github.com/Dreamacro/clash/tunnel"
"github.com/kr328/cfa/profile"
"github.com/kr328/cfa/config"
"sync"
)
var (
@@ -19,26 +18,24 @@ type LogCallback interface {
OnLogEvent(level, payload string)
}
func LoadMMDB(data []byte) {
dataClone := make([]byte, len(data))
copy(dataClone, data)
func InitCore(geoipDatabase[] byte, homeDir string, version string) {
dataClone := make([]byte, len(geoipDatabase))
copy(dataClone, geoipDatabase)
mmdb.LoadFromBytes(dataClone)
}
func SetHome(homeDir string) {
C.SetHomeDir(homeDir)
config.ApplicationVersion = version
Reset()
log.Infoln("Initialed")
}
func Reset() {
profile.LoadDefault()
config.LoadDefault()
tunnel.DefaultManager.ResetStatistic()
}
func SetApplicationVersion(version string) {
profile.ApplicationVersion = version
}
func SetLogCallback(callback LogCallback) {
logSubscribe.Do(func() {
go func() {

View File

@@ -3,40 +3,40 @@ package bridge
import (
"strings"
"github.com/kr328/cfa/profile"
"github.com/kr328/cfa/config"
)
func ResetDnsAppend(dns string) {
if len(dns) == 0 {
profile.NameServersAppend = make([]string, 0)
config.NameServersAppend = make([]string, 0)
} else {
profile.NameServersAppend = strings.Split(dns, ",")
config.NameServersAppend = strings.Split(dns, ",")
}
}
func SetDnsOverrideEnabled(enabled bool) {
if enabled {
profile.DnsPatch = profile.OptionalDnsPatch
config.DnsPatch = config.OptionalDnsPatch
} else {
profile.DnsPatch = nil
config.DnsPatch = nil
}
}
func LoadProfileFile(path, baseDir string, callback DoneCallback) {
go func() {
call(profile.LoadFromFile(path, baseDir), callback)
call(config.LoadFromFile(path, baseDir), callback)
}()
}
func DownloadProfileAndCheck(url, output, baseDir string, callback DoneCallback) {
go func() {
call(profile.DownloadAndCheck(url, output, baseDir), callback)
call(config.PullRemote(url, output, baseDir), callback)
}()
}
func ReadProfileAndCheck(fd int, output, baseDir string, callback DoneCallback) {
go func() {
call(profile.ReadAndCheck(fd, output, baseDir), callback)
call(config.PullLocal(fd, output, baseDir), callback)
}()
}

View File

@@ -53,26 +53,18 @@ func StartUrlTest(group string, callback DoneCallback) {
p := tunnel.Proxies()[group]
pa, ok := p.(*outbound.Proxy)
pi, ok := p.(*outbound.Proxy)
if !ok {
return
}
var providers []provider.ProxyProvider
switch group := pa.ProxyAdapter.(type) {
case *outboundgroup.Fallback:
providers = group.GetProviders()
case *outboundgroup.URLTest:
providers = group.GetProviders()
case *outboundgroup.LoadBalance:
providers = group.GetProviders()
case *outboundgroup.Selector:
providers = group.GetProviders()
default:
group, ok := pi.ProxyAdapter.(outboundgroup.ProxyGroup)
if !ok {
return
}
providers := group.GetProxyProviders()
wg := &sync.WaitGroup{}
wg.Add(len(providers))
@@ -91,55 +83,23 @@ func QueryAllProxyGroups(collection ProxyGroupCollection) {
ps := tunnel.Proxies()
for _, p := range ps {
pa, ok := p.(*outbound.Proxy)
pi, ok := p.(*outbound.Proxy)
if !ok {
continue
}
switch group := pa.ProxyAdapter.(type) {
case *outboundgroup.Fallback:
collection.Add(
&ProxyGroupItem{
Name: group.Name(),
Type: group.Type().String(),
Current: group.Now(),
Delay: int(p.LastDelay()),
providers: group.GetProviders(),
},
)
case *outboundgroup.URLTest:
collection.Add(
&ProxyGroupItem{
Name: group.Name(),
Type: group.Type().String(),
Current: group.Now(),
Delay: int(p.LastDelay()),
providers: group.GetProviders(),
},
)
case *outboundgroup.LoadBalance:
collection.Add(
&ProxyGroupItem{
Name: group.Name(),
Type: group.Type().String(),
Current: "",
Delay: int(p.LastDelay()),
providers: group.GetProviders(),
},
)
case *outboundgroup.Selector:
collection.Add(
&ProxyGroupItem{
Name: group.Name(),
Type: group.Type().String(),
Current: group.Now(),
Delay: int(p.LastDelay()),
providers: group.GetProviders(),
},
)
default:
group, ok := pi.ProxyAdapter.(outboundgroup.ProxyGroup)
if !ok {
continue
}
collection.Add(&ProxyGroupItem{
Name: group.Name(),
Type: group.Type().String(),
Current: group.Now(),
Delay: 0,
providers: group.GetProxyProviders(),
})
}
}

View File

@@ -28,9 +28,9 @@ func onNewListenConfig(listen *net.ListenConfig) {
listen.Control = onNewSocket
}
func onNewSocket(network, address string, c syscall.RawConn) error {
func onNewSocket(_, _ string, c syscall.RawConn) error {
if cb := callback; cb != nil {
c.Control(func(fd uintptr) {
_ = c.Control(func(fd uintptr) {
cb.OnCreateSocket(int(fd))
})
}
@@ -38,10 +38,10 @@ func onNewSocket(network, address string, c syscall.RawConn) error {
return nil
}
func StartTunDevice(fd, mtu int, dns string, cb TunCallback) error {
func StartTunDevice(fd, mtu int, gateway, mirror, dns string, cb TunCallback) error {
callback = cb
return tun.StartTunDevice(fd, mtu, dns)
return tun.StartTunDevice(fd, mtu, gateway, mirror, dns)
}
func StopTunDevice() {

View File

@@ -1,8 +1,9 @@
package profile
package config
import (
"context"
"errors"
"github.com/kr328/cfa/utils"
"io/ioutil"
"net"
"net/http"
@@ -30,66 +31,69 @@ var client = &http.Client{
tunnel.Add(inbound.NewSocket(socks5.ParseAddr(address), server, constant.HTTP, constant.TCP))
go func() {
if ctx == nil || ctx.Done() == nil {
return
}
<-ctx.Done()
client.Close()
server.Close()
}()
return client, nil
},
},
}
func DownloadAndCheck(url, output, baseDir string) error {
func fetchRemote(url string) ([]byte, error) {
request, err := http.NewRequest("GET", url, nil)
if err != nil {
return err
return nil, err
}
request.Header.Set("User-Agent", "ClashForAndroid/"+ApplicationVersion)
response, err := client.Do(request)
if err != nil {
return err
return nil, err
}
defer response.Body.Close()
defer utils.CloseSilent(response.Body)
data, err := ioutil.ReadAll(response.Body)
if err != nil {
return err
return nil, err
}
return SaveAndCheck(data, output, baseDir)
return data, nil
}
func ReadAndCheck(fd int, output, baseDir string) error {
syscall.SetNonblock(fd, true)
func fetchLocal(fd int) ([]byte, error) {
_ = syscall.SetNonblock(fd, true)
file := os.NewFile(uintptr(fd), "/dev/null")
defer file.Close()
defer utils.CloseSilent(file)
data, err := ioutil.ReadAll(file)
return ioutil.ReadAll(file)
}
func PullRemote(url, output, baseDir string) error {
data, err := fetchRemote(url)
if err != nil {
return err
}
return SaveAndCheck(data, output, baseDir)
return save(data, output, baseDir)
}
func SaveAndCheck(data []byte, output, baseDir string) error {
func PullLocal(fd int, output, baseDir string) error {
data, err := fetchLocal(fd)
if err != nil {
return err
}
return save(data, output, baseDir)
}
func save(data []byte, output, baseDir string) error {
cfg, err := parseConfig(data, baseDir)
if err != nil {
return err
}
for _, v := range cfg.Providers {
v.Destroy()
_ = v.Destroy()
}
return ioutil.WriteFile(output, data, defaultFileMode)

View File

@@ -0,0 +1,75 @@
package config
import (
"errors"
"io/ioutil"
"github.com/Dreamacro/clash/config"
"github.com/Dreamacro/clash/hub/executor"
"github.com/Dreamacro/clash/log"
"github.com/kr328/cfa/tun"
)
// LoadDefault - load default configure
func LoadDefault() {
DnsPatch = nil
NameServersAppend = make([]string, 0)
defaultC, err := config.Parse([]byte{})
if err != nil {
log.Warnln("Load Default Failure " + err.Error())
return
}
executor.ApplyConfig(defaultC, true)
tun.InitialResolver()
}
// LoadFromFile - load file
func LoadFromFile(path, baseDir string) error {
data, err := ioutil.ReadFile(path)
if err != nil {
return err
}
cfg, err := parseConfig(data, baseDir)
if err != nil {
return err
}
for _, ns := range cfg.DNS.NameServer {
log.Infoln("DNS: %s", ns.Addr)
}
executor.ApplyConfig(cfg, true)
tun.InitialResolver()
log.Infoln("Profile " + path + " loaded")
return nil
}
func parseConfig(data []byte, baseDir string) (*config.Config, error) {
raw, err := config.UnmarshalRawConfig(data)
if err != nil {
return nil, err
}
patchRawConfig(raw)
if len(raw.Proxy) == 0 && len(raw.ProxyProvider) == 0 &&
len(raw.ProxyOld) == 0 && len(raw.ProxyProviderOld) == 0 {
return nil, errors.New("Empty Profile")
}
cfg, err := config.ParseRawConfig(raw, baseDir)
if err != nil {
return nil, err
}
patchConfig(cfg)
return cfg, nil
}

View File

@@ -0,0 +1,93 @@
package config
import (
"github.com/Dreamacro/clash/component/fakeip"
"github.com/Dreamacro/clash/config"
"github.com/Dreamacro/clash/dns"
"net/url"
)
var (
OptionalDnsPatch *config.RawDNS
DnsPatch *config.RawDNS
NameServersAppend []string
cachedPool *fakeip.Pool
)
func init() {
defaultNameServers := []string{
"223.5.5.5",
"119.29.29.29",
"1.1.1.1",
"208.67.222.222",
}
OptionalDnsPatch = &config.RawDNS{
Enable: true,
IPv6: true,
NameServer: defaultNameServers,
Fallback: []string{},
FallbackFilter: config.RawFallbackFilter{
GeoIP: false,
IPCIDR: []string{},
},
Listen: ":0",
EnhancedMode: dns.FAKEIP,
FakeIPRange: "198.18.0.0/16",
FakeIPFilter: []string{},
DefaultNameserver: defaultNameServers,
}
}
func patchRawConfig(rawConfig *config.RawConfig) {
rawConfig.DNS.FakeIPRange = "198.18.0.0/16"
rawConfig.Experimental.Interface = ""
rawConfig.ExternalUI = ""
rawConfig.ExternalController = ""
if d := DnsPatch; d != nil {
rawConfig.DNS = *d
} else if d := OptionalDnsPatch; d != nil {
if !rawConfig.DNS.Enable {
rawConfig.DNS = *d
}
}
if nameServersAppend := NameServersAppend; len(nameServersAppend) > 0 {
d := &rawConfig.DNS
nameServers := make([]string, len(nameServersAppend)+len(d.NameServer))
copy(nameServers, nameServersAppend)
copy(nameServers[len(nameServersAppend):], d.NameServer)
d.NameServer = nameServers
}
providers := rawConfig.ProxyProvider
if len(rawConfig.ProxyProvider) == 0 {
providers = rawConfig.ProxyProviderOld
}
for _, provider := range providers {
path, ok := provider["path"].(string)
if !ok {
continue
}
provider["path"] = url.QueryEscape(path)
}
}
func patchConfig(config *config.Config) {
if config.DNS.FakeIPRange != nil {
if c := cachedPool; c != nil {
if config.DNS.FakeIPRange.Gateway().String() == c.Gateway().String() {
c.OverrideHostFrom(config.DNS.FakeIPRange)
config.DNS.FakeIPRange = c
}
} else {
cachedPool = config.DNS.FakeIPRange
}
}
}

View File

@@ -4,12 +4,8 @@ go 1.13
require (
github.com/Dreamacro/clash v0.0.0 // local
github.com/go-chi/render v1.0.1
github.com/google/go-cmp v0.3.1 // indirect
golang.org/x/mobile v0.0.0-20191210151939-1a1fef82734d // indirect
golang.org/x/sys v0.0.0-20191224085550-c709ea063b76
github.com/kr328/tun2socket v0.0.0-20200415021819-256b721ac9a4
github.com/miekg/dns v1.1.29
)
replace github.com/Dreamacro/clash => ./clash
replace github.com/google/netstack => github.com/comzyh/netstack v0.0.0-20191217044024-67c27819ada4

View File

@@ -1,158 +1,44 @@
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802 h1:1BDTz0u9nC3//pOCMdNH+CiXJVYJh5UQNCOBG7jbELc=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/Dreamacro/clash v0.16.0 h1:ZvV9apOrDHC0s5ff4YCHTRANd/XyOuLWvpyLWOkwS6U=
github.com/Dreamacro/clash v0.16.0/go.mod h1:4ZBtABBmIGqISniBtu9fpYORrE5mKqIP8SMCTrNNVNY=
github.com/Dreamacro/go-shadowsocks2 v0.1.3 h1:1ffY/q4e3o+MnztYgIq1iZiX1BWoWQ6D3AIO1kkb8bc=
github.com/Dreamacro/go-shadowsocks2 v0.1.3/go.mod h1:0x17IhQ+mlY6q/ffKRpzaE7u4aHMxxnitTRSrV5G6TU=
github.com/Dreamacro/go-shadowsocks2 v0.1.5-0.20191012162057-46254afc8b68 h1:UBDLpj1IGVkUcUBuZWE6DmZApPTZcnmcV6AfyDN/yhg=
github.com/Dreamacro/go-shadowsocks2 v0.1.5-0.20191012162057-46254afc8b68/go.mod h1:Y8obOtHDOqxMGHjPglfCiXZBKExOA9VL6I6sJagOwYM=
github.com/Dreamacro/go-shadowsocks2 v0.1.5 h1:BizWSjmwzAyQoslz6YhJYMiAGT99j9cnm9zlxVr+kyI=
github.com/Dreamacro/go-shadowsocks2 v0.1.5/go.mod h1:LSXCjyHesPY3pLjhwff1mQX72ItcBT/N2xNC685cYeU=
github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da h1:KjTM2ks9d14ZYCvmHS9iAKVt9AyzRSqNU1qabPih5BY=
github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da/go.mod h1:eHEWzANqSiWQsof+nXEI9bUVUyV6F53Fp89EuCh2EAA=
github.com/comzyh/netstack v0.0.0-20191217044024-67c27819ada4 h1:30ykXB9NWubvyVWE5pe/YakDgEdu6wJkBZlZYDtV464=
github.com/comzyh/netstack v0.0.0-20191217044024-67c27819ada4/go.mod h1:jMMWEkl1smElz5KtnrIWDHxc8gtMIO/Pd8+pLyGRzT8=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/eapache/queue v1.1.0 h1:YOEu7KNc61ntiQlcEeUIoDTJ2o8mQznoNvUhiigpIqc=
github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I=
github.com/go-chi/chi v4.0.2+incompatible h1:maB6vn6FqCxrpz4FqWdh4+lwpyZIQS7YEAUcHlgXVRs=
github.com/go-chi/chi v4.0.2+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ=
github.com/go-chi/chi v4.0.3+incompatible h1:gakN3pDJnzZN5jqFV2TEdF66rTfKeITyR8qu6ekICEY=
github.com/go-chi/chi v4.0.3+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ=
github.com/go-chi/cors v1.0.0 h1:e6x8k7uWbUwYs+aXDoiUzeQFT6l0cygBYyNhD7/1Tg0=
github.com/go-chi/cors v1.0.0/go.mod h1:K2Yje0VW/SJzxiyMYu6iPQYa7hMjQX2i/F491VChg1I=
github.com/go-chi/render v1.0.1 h1:4/5tis2cKaNdnv9zFLfXzcquC9HbeZgCnxGnKrltBS8=
github.com/go-chi/cors v1.0.1/go.mod h1:K2Yje0VW/SJzxiyMYu6iPQYa7hMjQX2i/F491VChg1I=
github.com/go-chi/render v1.0.1/go.mod h1:pq4Rr7HbnsdaeHagklXub+p6Wd16Af5l9koip1OvJns=
github.com/gofrs/uuid v3.2.0+incompatible h1:y12jRkkFxsd7GpqdSZ+/KCs/fJbqpEXSGd4+jfEaewE=
github.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
github.com/google/btree v1.0.0 h1:0udJVsspx3VBr5FwtLhQQtuAsVc79tTq0ocGIPAU6qo=
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/go-cmp v0.3.1 h1:Xye71clBPdm5HgqGwUkwhbynsUJZhDbS20FvLhQ2izg=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/netstack v0.0.0-20191031000057-4787376a6744 h1:wKeh74w+ydKcE1Eo44WDzIOcPHWmxxmtAzkAL0Mlspc=
github.com/google/netstack v0.0.0-20191031000057-4787376a6744/go.mod h1:r/rILWg3r1Qy9G1IFMhsqWLq2GjwuYoTuPgG7ckMAjk=
github.com/google/netstack v0.0.0-20191116005144-95bf25ab4723 h1:z92yYpQg57ql9n96PBLFhT+N5X44uQ2f3+CNp9Osmu8=
github.com/google/netstack v0.0.0-20191116005144-95bf25ab4723/go.mod h1:r/rILWg3r1Qy9G1IFMhsqWLq2GjwuYoTuPgG7ckMAjk=
github.com/google/netstack v0.0.0-20191123085552-55fcc16cd0eb h1:/YcrD0GSdU5gtckXHVjSEd0Y6VgboNW7VYyImZS3y6g=
github.com/google/netstack v0.0.0-20191123085552-55fcc16cd0eb/go.mod h1:r/rILWg3r1Qy9G1IFMhsqWLq2GjwuYoTuPgG7ckMAjk=
github.com/gorilla/websocket v1.4.0 h1:WDFjx/TMzVgy9VdMMQi2K2Emtwi2QcUQsztZ/zLaH/Q=
github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
github.com/gorilla/websocket v1.4.1 h1:q7AeDBpnBk8AogcD4DSag/Ukw/KV+YhzLj2bP5HvKCM=
github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.2 h1:DB17ag19krx9CFsz4o3enTrPXyIXCl+2iCXH/aMAp9s=
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/miekg/dns v1.1.9 h1:OIdC9wT96RzuZMf2PfKRhFgsStHUUBZLM/lo1LqiM9E=
github.com/miekg/dns v1.1.9/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
github.com/miekg/dns v1.1.22 h1:Jm64b3bO9kP43ddLjL2EY3Io6bmy1qGb9Xxz6TqS6rc=
github.com/miekg/dns v1.1.22/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso=
github.com/miekg/dns v1.1.24 h1:6G8Eop/HM8hpagajbn0rFQvAKZWiiCa8P6N2I07+wwI=
github.com/miekg/dns v1.1.24/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso=
github.com/miekg/dns v1.1.26 h1:gPxPSwALAeHJSjarOs00QjVdV9QoBvc1D2ujQUr5BzU=
github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso=
github.com/miekg/dns v1.1.27 h1:aEH/kqUzUxGJ/UHcEKdJY+ugH6WEzsEBBSPa8zuy1aM=
github.com/miekg/dns v1.1.27/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7xM=
github.com/oschwald/geoip2-golang v1.2.1 h1:3iz+jmeJc6fuCyWeKgtXSXu7+zvkxJbHFXkMT5FVebU=
github.com/oschwald/geoip2-golang v1.2.1/go.mod h1:0LTTzix/Ao1uMvOhAV4iLU0Lz7eCrP94qZWBTDKf0iE=
github.com/oschwald/geoip2-golang v1.3.0 h1:D+Hsdos1NARPbzZ2aInUHZL+dApIzo8E0ErJVsWcku8=
github.com/oschwald/geoip2-golang v1.3.0/go.mod h1:0LTTzix/Ao1uMvOhAV4iLU0Lz7eCrP94qZWBTDKf0iE=
github.com/oschwald/geoip2-golang v1.4.0 h1:5RlrjCgRyIGDz/mBmPfnAF4h8k0IAcRv9PvrpOfz+Ug=
github.com/kr328/tun2socket v0.0.0-20200415021819-256b721ac9a4/go.mod h1:FWfSixjrLgtK+dHkDoN6lHMNhvER24gnjUZd/wt8Z9o=
github.com/miekg/dns v1.1.29/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7xM=
github.com/oschwald/geoip2-golang v1.4.0/go.mod h1:8QwxJvRImBH+Zl6Aa6MaIcs5YdlZSTKtzmPGzQqi9ng=
github.com/oschwald/maxminddb-golang v1.3.0 h1:oTh8IBSj10S5JNlUDg5WjJ1QdBMdeaZIkPEVfESSWgE=
github.com/oschwald/maxminddb-golang v1.3.0/go.mod h1:3jhIUymTJ5VREKyIhWm66LJiQt04F0UCDdodShpjWsY=
github.com/oschwald/maxminddb-golang v1.5.0 h1:rmyoIV6z2/s9TCJedUuDiKht2RN12LWJ1L7iRGtWY64=
github.com/oschwald/maxminddb-golang v1.5.0/go.mod h1:3jhIUymTJ5VREKyIhWm66LJiQt04F0UCDdodShpjWsY=
github.com/oschwald/maxminddb-golang v1.6.0 h1:KAJSjdHQ8Kv45nFIbtoLGrGWqHFajOIm7skTyz/+Dls=
github.com/oschwald/maxminddb-golang v1.6.0/go.mod h1:DUJFucBg2cvqx42YmDa/+xHvb0elJtOm3o4aFQ/nb/w=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/sirupsen/logrus v1.4.1 h1:GL2rEmy6nsikmW0r8opw9JIRScdMF5hA8cOYLH7In1k=
github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1 h1:2vfRuCMp5sSVIDSqO8oNnWJq7mPa6KVP3iPIwFBuy8A=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190426145343-a29dc8fdc734 h1:p/H982KKEjUnLJkM3tt/LemDnOc1GiZL5FCVlORJ5zo=
golang.org/x/crypto v0.0.0-20190426145343-a29dc8fdc734/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550 h1:ObdrDkeb4kJdCP557AjRjq69pTHfNouLtWZG7j9rPN8=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191128160524-b544559bb6d1 h1:anGSYQpPhQwXlwsu5wmfq0nWkCNaMEMUwAv13Y92hd8=
golang.org/x/crypto v0.0.0-20191128160524-b544559bb6d1/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20191206172530-e9b2fee46413 h1:ULYEB3JvPRE/IfO+9uO7vKV/xzVTO7XPAwm8xbf4w2g=
golang.org/x/crypto v0.0.0-20191206172530-e9b2fee46413/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200214034016-1d94cc7ab1c6 h1:Sy5bstxEqwwbYs6n0/pBuxKENqOeZUgD45Gp3Q3pqLg=
golang.org/x/crypto v0.0.0-20200214034016-1d94cc7ab1c6/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56 h1:estk1glOnSVeJ9tdEZZc5mAMDZk5lNJNyJ6DvrBkTEU=
golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56/go.mod h1:JhuoJpWY28nO4Vef9tZUw9qufEGTyX1+7lmHxV5q5G4=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b h1:+qEpEAPhDZ1o0x3tHzZTQDArnOixOzGD9HUJfcg0mb4=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
golang.org/x/mobile v0.0.0-20191210151939-1a1fef82734d h1:LlA9R5JFi974qK4gm9FRK1+qSkduxnQKcrimdzcidyc=
golang.org/x/mobile v0.0.0-20191210151939-1a1fef82734d/go.mod h1:p895TfNkDgPEmEQrNiOtIl3j98d/tGU95djDj7NfyjQ=
golang.org/x/mod v0.1.0 h1:sfUMP1Gu8qASkorDVjnMuvgJzwFbTZSeXFiGBYAVdl4=
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
golang.org/x/crypto v0.0.0-20200320181102-891825fb96df/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3 h1:0GoQqolDA55aaLxZyTzK/Y2ePZzZTUrRacwib7cNsYQ=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20191011234655-491137f69257 h1:ry8e2D+cwaV6hk7lb3aRTjjZo24shrbK0e11QEOkTIg=
golang.org/x/net v0.0.0-20191011234655-491137f69257/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20191126235420-ef20fe5d7933 h1:e6HwijUxhDe+hPNjZQQn9bA5PW3vNmnN64U2ZW759Lk=
golang.org/x/net v0.0.0-20191126235420-ef20fe5d7933/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20191207000613-e7e4b65ae663 h1:Dd5RoEW+yQi+9DMybroBctIdyiwuNT7sJFMC27/6KxI=
golang.org/x/net v0.0.0-20191207000613-e7e4b65ae663/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553 h1:efeOvDhwQ29Dj3SdAV/MJf8oukgn+8D8WgaCaRMchF8=
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200202094626-16171245cfb2 h1:CCH4IOTTfewWjGOlSp+zGcjutRKlBEZQ6wTn8ozI/nI=
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/net v0.0.0-20200320220750-118fecf932d8/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e h1:vcxGaoTs7kV8m5Np9uUNQin4BrLOthgV7252N8V+FwY=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190922100055-0a153f010e69/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe h1:6fAMxZRR6sl1Uq8U61gxU+kPTs2tR8uOySCbBP7BN/M=
golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191128015809-6d18c012aee9 h1:ZBzSG/7F4eNKz2L3GE9o300RX0Az1Bw5HF7PDraD+qU=
golang.org/x/sys v0.0.0-20191128015809-6d18c012aee9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191224085550-c709ea063b76 h1:Dho5nD6R3PcW2SH1or8vS0dszDaXRxIw55lBX7XiE5g=
golang.org/x/sys v0.0.0-20191224085550-c709ea063b76/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0 h1:/5xXl8Y5W96D+TtHSlonuFqGHIWVuyCkGJLwGh9JJFs=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190907020128-2ca718005c18/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20190909214602-067311248421 h1:NmmWqJbt02YJHmp4A4gBXvsXXIzzixjzE1y6PKUyIjk=
golang.org/x/tools v0.0.0-20190909214602-067311248421/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191216052735-49a3e744a425/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7 h1:9zdDQZ7Thm29KFXgAX/+yaf3eVbP7djjWp/dXAppNCc=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/eapache/channels.v1 v1.1.0 h1:5bGAyKKvyCTWjSj7mhefG6Lc68VyN4MH1v8/7OoeeB4=
gopkg.in/eapache/channels.v1 v1.1.0/go.mod h1:BHIBujSvu9yMTrTYbTCjDD43gUhtmaOtTWDe7sTv1js=
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.7 h1:VUgggvou5XRW9mHwD/yXxIYSMtY0zoKQf/v226p2nyo=
gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=

View File

@@ -1,122 +0,0 @@
package profile
import (
"fmt"
"io/ioutil"
"github.com/Dreamacro/clash/config"
"github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/dns"
"github.com/Dreamacro/clash/hub/executor"
"github.com/Dreamacro/clash/log"
"github.com/kr328/cfa/tun"
)
const tunAddress = "172.31.255.253/30"
const defaultConfig = `
log: debug
mode: Direct
Proxy:
- name: "broadcast"
type: socks5
server: 255.255.255.255
port: 1080
Proxy Group:
- name: "select"
type: select
proxies: [DIRECT]
Rule:
- 'MATCH,DIRECT'
`
func init() {
defaultNameServers := []string{
"223.5.5.5",
"119.29.29.29",
"1.1.1.1",
"208.67.222.222",
}
OptionalDnsPatch = &config.RawDNS{
Enable: true,
IPv6: true,
NameServer: defaultNameServers,
Fallback: []string{},
FallbackFilter: config.RawFallbackFilter{
GeoIP: false,
IPCIDR: []string{},
},
Listen: ":0",
EnhancedMode: dns.FAKEIP,
FakeIPRange: "198.18.0.0/16",
FakeIPFilter: []string{},
DefaultNameserver: defaultNameServers,
}
}
// LoadDefault - load default configure
func LoadDefault() {
defaultC, err := parseConfig([]byte(defaultConfig), constant.Path.HomeDir())
if err != nil {
log.Warnln("Load Default Failure " + err.Error())
return
}
DnsPatch = nil
NameServersAppend = make([]string, 0)
executor.ApplyConfig(defaultC, true)
tun.ResetDnsRedirect()
}
// LoadFromFile - load file
func LoadFromFile(path, baseDir string) error {
data, err := ioutil.ReadFile(path)
if err != nil {
return err
}
cfg, err := parseConfig(data, baseDir)
if err != nil {
return err
}
for _, ns := range cfg.DNS.NameServer {
log.Infoln("DNS: %s", ns.Addr)
}
executor.ApplyConfig(cfg, true)
tun.ResetDnsRedirect()
log.Infoln("Profile " + path + " loaded")
return nil
}
func parseConfig(data []byte, baseDir string) (*config.Config, error) {
raw, err := config.UnmarshalRawConfig(data)
if err != nil {
return nil, err
}
raw.Experimental.Interface = ""
raw.ExternalUI = ""
raw.ExternalController = ""
raw.Rule = append([]string{fmt.Sprintf("IP-CIDR,%s,REJECT,no-resolve", tunAddress)}, raw.Rule...)
patchRawConfig(raw)
cfg, err := config.ParseRawConfig(raw, baseDir)
if err != nil {
return nil, err
}
patchConfig(cfg)
return cfg, nil
}

View File

@@ -1,63 +0,0 @@
package profile
import (
"strings"
"github.com/Dreamacro/clash/component/fakeip"
"github.com/Dreamacro/clash/config"
)
var (
OptionalDnsPatch *config.RawDNS
DnsPatch *config.RawDNS
NameServersAppend []string
cachedPool *fakeip.Pool
)
func patchRawConfig(rawConfig *config.RawConfig) {
if d := DnsPatch; d != nil {
rawConfig.DNS = *d
} else if d := OptionalDnsPatch; d != nil {
if !rawConfig.DNS.Enable {
rawConfig.DNS = *d
}
}
if append := NameServersAppend; len(append) > 0 {
d := &rawConfig.DNS
nameservers := make([]string, len(append)+len(d.NameServer))
copy(nameservers, append)
copy(nameservers[len(append):], d.NameServer)
d.NameServer = nameservers
}
providers := rawConfig.ProxyProvider
for _, provider := range providers {
path, ok := provider["path"].(string)
if !ok {
continue
}
path = strings.TrimSuffix(path, ".yaml")
path = strings.Replace(path, "/", "", -1)
path = strings.Replace(path, ".", "", -1)
path = "./" + path + ".yaml"
provider["path"] = path
}
}
func patchConfig(config *config.Config) {
if config.DNS.FakeIPRange != nil {
if c := cachedPool; c != nil {
if config.DNS.FakeIPRange.Gateway().String() == c.Gateway().String() {
config.DNS.FakeIPRange = c
}
} else {
cachedPool = config.DNS.FakeIPRange
}
}
}

View File

@@ -0,0 +1,252 @@
package tun
import (
"encoding/binary"
"github.com/Dreamacro/clash/component/resolver"
"github.com/Dreamacro/clash/dns"
"github.com/kr328/cfa/utils"
"github.com/kr328/tun2socket/binding"
"github.com/kr328/tun2socket/redirect"
D "github.com/miekg/dns"
"io"
"net"
"sync"
"time"
)
const (
defaultDNSReadTimeout = time.Second * 10
)
var lock sync.Mutex
var hijackAddress net.IP
var dnsHandler dns.Handler
func setHijackAddress(hijackAddr net.IP) {
lock.Lock()
defer lock.Unlock()
hijackAddress = hijackAddr
}
func InitialResolver() {
lock.Lock()
defer lock.Unlock()
rawResolver := resolver.DefaultResolver
if rawResolver == nil {
dnsHandler = nil
return
}
r, ok := rawResolver.(*dns.Resolver)
if !ok || r == nil {
dnsHandler = nil
return
}
dnsHandler = dns.NewHandler(r)
}
func hijackTCPDNS(conn net.Conn, endpoint *binding.Endpoint) bool {
if endpoint.Target.Port != 53 {
return false
}
if dnsHandler == nil {
return false
}
if !hijackAddress.Equal(net.IPv4zero) && !hijackAddress.Equal(net.IPv6zero) && !hijackAddress.Equal(endpoint.Target.IP) {
return false
}
go func() {
defer utils.CloseSilent(conn)
for {
if err := conn.SetReadDeadline(time.Now().Add(defaultDNSReadTimeout)); err != nil {
return
}
var length uint16
if err := binary.Read(conn, binary.BigEndian, &length); err != nil {
return
}
data := make([]byte, length)
msg := &D.Msg{}
_, err := io.ReadFull(conn, data)
if err != nil {
return
}
if err := msg.Unpack(data); err != nil || len(msg.Question) == 0 {
return
}
handler := dnsHandler
if handler == nil {
return
}
handler(&tcpWriter{
conn: conn,
endpoint: endpoint,
}, msg)
}
}()
return true
}
func hijackDNS(payload []byte, endpoint *binding.Endpoint, sender redirect.UDPSender, recycle func([]byte)) bool {
if endpoint.Target.Port != 53 {
return false
}
if dnsHandler == nil {
return false
}
if !hijackAddress.Equal(net.IPv4zero) && !hijackAddress.Equal(net.IPv6zero) && !hijackAddress.Equal(endpoint.Target.IP) {
return false
}
go func() {
msg := &D.Msg{}
if err := msg.Unpack(payload); err != nil {
return
}
handler := dnsHandler
handler(&udpWriter{
endpoint: endpoint,
sender: sender,
}, msg)
recycle(payload)
}()
return true
}
type tcpWriter struct {
conn net.Conn
endpoint *binding.Endpoint
}
func (r *tcpWriter) LocalAddr() net.Addr {
return &net.TCPAddr{
IP: r.endpoint.Target.IP,
Port: int(r.endpoint.Target.Port),
Zone: "",
}
}
func (r *tcpWriter) RemoteAddr() net.Addr {
return &net.TCPAddr{
IP: r.endpoint.Source.IP,
Port: int(r.endpoint.Source.Port),
Zone: "",
}
}
func (r *tcpWriter) Write(b []byte) (int, error) {
if len(b) > 65535 {
return 0, io.ErrShortBuffer
}
var length [2]byte
binary.BigEndian.PutUint16(length[:], uint16(len(b)))
n, err := (&net.Buffers{length[:], b}).WriteTo(r.conn)
return int(n), err
}
func (r *tcpWriter) Close() error {
return nil
}
func (r *tcpWriter) WriteMsg(d *D.Msg) error {
msg, err := d.Pack()
if err != nil {
return err
}
_, err = r.Write(msg)
return err
}
func (r *tcpWriter) TsigStatus() error {
// Unsupported
return nil
}
func (r *tcpWriter) TsigTimersOnly(bool) {
// Unsupported
}
func (r *tcpWriter) Hijack() {
// Unsupported
}
type udpWriter struct {
endpoint *binding.Endpoint
sender redirect.UDPSender
}
func (r *udpWriter) LocalAddr() net.Addr {
return &net.UDPAddr{
IP: r.endpoint.Target.IP,
Port: int(r.endpoint.Target.Port),
Zone: "",
}
}
func (r *udpWriter) RemoteAddr() net.Addr {
return &net.UDPAddr{
IP: r.endpoint.Source.IP,
Port: int(r.endpoint.Source.Port),
Zone: "",
}
}
func (r *udpWriter) WriteMsg(d *D.Msg) error {
msg, err := d.Pack()
if err != nil {
return err
}
_, err = r.Write(msg)
return err
}
func (r *udpWriter) Write(msg []byte) (int, error) {
ep := &binding.Endpoint{
Source: r.endpoint.Target,
Target: r.endpoint.Source,
}
return len(msg), r.sender(msg, ep)
}
func (r *udpWriter) Close() error {
return nil
}
func (r *udpWriter) TsigStatus() error {
// Unsupported
return nil
}
func (r *udpWriter) TsigTimersOnly(bool) {
// Unsupported
}
func (r *udpWriter) Hijack() {
// Unsupported
}

View File

@@ -0,0 +1,22 @@
package tun
import "github.com/Dreamacro/clash/log"
type ClashLogger struct {}
func (c *ClashLogger) D(format string, args ...interface{}) {
log.Debugln(format, args)
}
func (c *ClashLogger) I(format string, args ...interface{}) {
log.Infoln(format, args)
}
func (c *ClashLogger) W(format string, args ...interface{}) {
log.Warnln(format, args)
}
func (c *ClashLogger) E(format string, args ...interface{}) {
log.Errorln(format, args)
}

View File

@@ -1,36 +1,116 @@
package tun
import (
"strconv"
"errors"
adapters "github.com/Dreamacro/clash/adapters/inbound"
"github.com/Dreamacro/clash/component/socks5"
C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/tunnel"
"github.com/kr328/tun2socket"
"github.com/kr328/tun2socket/binding"
"github.com/kr328/tun2socket/redirect"
"net"
"os"
"sync"
"syscall"
"github.com/Dreamacro/clash/component/resolver"
"github.com/Dreamacro/clash/dns"
"github.com/Dreamacro/clash/log"
"github.com/Dreamacro/clash/proxy/tun"
)
var tunInstance *tun.TunAdapter
var dnsAddress string
const (
maxUdpPacketSize = 65535
)
var adapter *tun2socket.Tun2Socket
var mutex sync.Mutex
func StartTunDevice(fd, mtu int, dns string) error {
func StartTunDevice(fd, mtu int, gateway, mirror, dnsAddress string) error {
mutex.Lock()
defer mutex.Unlock()
if tunInstance != nil {
return nil
if adapter != nil {
adapter.Close()
adapter = nil
log.Infoln("Android tun stopped")
}
t, err := tun.NewTunProxy("fd://" + strconv.Itoa(fd) + "?mtu=" + strconv.Itoa(mtu))
if err != nil {
return err
gatewayIP, gatewayNet, err := net.ParseCIDR(gateway)
mirrorIP := net.ParseIP(mirror)
if err != nil || mirrorIP == nil || !gatewayNet.Contains(mirrorIP) {
return errors.New("invalid gateway or mirror")
}
tunInstance = &t
dnsAddress = dns
udpPool := sync.Pool{New: func() interface{} {
return make([]byte, maxUdpPacketSize)
}}
udpRecycle := func(bytes []byte) {
if cap(bytes) == maxUdpPacketSize {
udpPool.Put(bytes[:maxUdpPacketSize])
}
}
ResetDnsRedirect()
file := os.NewFile(uintptr(fd), "/dev/tun")
_ = syscall.SetNonblock(fd, true)
adapter = tun2socket.NewTun2Socket(file, mtu, gatewayIP, mirrorIP.To4())
adapter.SetLogger(&ClashLogger{})
adapter.SetClosedHandler(func() {
StopTunDevice()
})
adapter.SetAllocator(func(length int) []byte {
if length <= maxUdpPacketSize {
return udpPool.Get().([]byte)[:length]
}
return make([]byte, length)
})
adapter.SetTCPHandler(func(conn net.Conn, endpoint *binding.Endpoint) {
if gatewayNet.Contains(endpoint.Target.IP) {
return
}
if hijackTCPDNS(conn, endpoint) {
return
}
addr := socks5.ParseAddrToSocksAddr(&net.TCPAddr{
IP: endpoint.Target.IP,
Port: int(endpoint.Target.Port),
Zone: "",
})
tunnel.Add(adapters.NewSocket(addr, conn, C.SOCKS, C.TCP))
})
adapter.SetUDPHandler(func(payload []byte, endpoint *binding.Endpoint, sender redirect.UDPSender) {
if gatewayNet.Contains(endpoint.Target.IP) {
udpRecycle(payload)
return
}
if hijackDNS(payload, endpoint, sender, udpRecycle) {
return
}
addr := socks5.ParseAddrToSocksAddr(&net.TCPAddr{
IP: endpoint.Target.IP,
Port: int(endpoint.Target.Port),
Zone: "",
})
pkt := &udpPacket{
payload: payload,
endpoint: endpoint,
sender: sender,
}
tunnel.AddPacket(adapters.NewPacket(addr, pkt, C.SOCKS))
})
setHijackAddress(net.ParseIP(dnsAddress))
InitialResolver()
adapter.Start()
log.Infoln("Android tun started")
@@ -41,21 +121,10 @@ func StopTunDevice() {
mutex.Lock()
defer mutex.Unlock()
t := tunInstance
if t == nil {
return
if adapter != nil {
adapter.Close()
adapter = nil
log.Infoln("Android tun stopped")
}
(*t).Close()
tunInstance = nil
log.Infoln("Android tun stopped")
}
func ResetDnsRedirect() {
if tunInstance == nil {
return
}
(*tunInstance).ReCreateDNSServer(resolver.DefaultResolver.(*dns.Resolver), dnsAddress)
}

View File

@@ -0,0 +1,55 @@
package tun
import (
"errors"
"github.com/kr328/tun2socket/binding"
"github.com/kr328/tun2socket/redirect"
"net"
)
type udpPacket struct {
payload []byte
endpoint *binding.Endpoint
sender redirect.UDPSender
}
func (conn *udpPacket) Data() []byte {
return conn.payload
}
func (conn *udpPacket) WriteBack(b []byte, addr net.Addr) (n int, err error) {
if addr == nil {
addr = &net.UDPAddr{
IP: conn.endpoint.Target.IP,
Port: int(conn.endpoint.Target.Port),
Zone: "",
}
}
udpAddr, ok := addr.(*net.UDPAddr)
if !ok {
return 0, errors.New("Invalid udp address")
}
ep := &binding.Endpoint{
Source: binding.Address{
IP: udpAddr.IP,
Port: uint16(udpAddr.Port),
},
Target: conn.endpoint.Source,
}
return len(b), conn.sender(b, ep)
}
func (conn *udpPacket) Close() error {
return nil
}
func (conn *udpPacket) LocalAddr() net.Addr {
return &net.UDPAddr{
IP: conn.endpoint.Source.IP,
Port: int(conn.endpoint.Source.Port),
Zone: "",
}
}

View File

@@ -0,0 +1,7 @@
package utils
import "io"
func CloseSilent(closer io.Closer) {
_ = closer.Close()
}

View File

@@ -1,8 +1,8 @@
package com.github.kr328.clash.core
import android.content.Context
import bridge.Bridge
import bridge.TunCallback
import com.github.kr328.clash.common.Global
import com.github.kr328.clash.core.event.LogEvent
import com.github.kr328.clash.core.model.General
import com.github.kr328.clash.core.model.Proxy
@@ -18,20 +18,13 @@ import java.io.InputStream
object Clash {
private val logReceivers = mutableMapOf<String, (LogEvent) -> Unit>()
private var initialized = false
@Synchronized
fun initialize(context: Context) {
if (initialized)
return
initialized = true
init {
val context = Global.application
val bytes = context.assets.open("Country.mmdb")
.use(InputStream::readBytes)
Bridge.loadMMDB(bytes)
Bridge.setHome(context.cacheDir.absolutePath)
Bridge.setApplicationVersion(BuildConfig.VERSION_NAME)
Bridge.initCore(bytes, context.cacheDir.absolutePath, BuildConfig.VERSION_NAME)
Bridge.reset()
}
@@ -46,11 +39,13 @@ object Clash {
fun startTunDevice(
fd: Int,
mtu: Int,
gateway: String,
mirror: String,
dns: String,
onNewSocket: (Int) -> Boolean,
onTunStop: () -> Unit
) {
Bridge.startTunDevice(fd.toLong(), mtu.toLong(), dns, object : TunCallback {
Bridge.startTunDevice(fd.toLong(), mtu.toLong(), gateway, mirror, dns, object: TunCallback {
override fun onCreateSocket(fd: Long) {
onNewSocket(fd.toInt())
}

View File

@@ -1,11 +0,0 @@
package com.github.kr328.clash.core
import android.app.Application
object Global {
lateinit var application: Application
fun init(application: Application) {
this.application = application
}
}

Some files were not shown because too many files have changed in this diff Show More