mirror of
https://github.com/MetaCubeX/ClashMetaForAndroid.git
synced 2026-05-09 18:11:26 +08:00
Compare commits
170 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b42d762fa0 | ||
|
|
f23e6e82b0 | ||
|
|
2af11c29a5 | ||
|
|
b0bf57e715 | ||
|
|
cdd0e3dd30 | ||
|
|
5823955c71 | ||
|
|
714c9a554f | ||
|
|
e621324b48 | ||
|
|
dfe1e7ecd9 | ||
|
|
4c3380d822 | ||
|
|
1d68516cdb | ||
|
|
821bb49914 | ||
|
|
c95993a629 | ||
|
|
e6495f5e1f | ||
|
|
c104942b5f | ||
|
|
59835667aa | ||
|
|
8a98eea8fa | ||
|
|
3dc20b9e70 | ||
|
|
d8ab6fd755 | ||
|
|
cc499a7897 | ||
|
|
76f5261b20 | ||
|
|
6a506b761f | ||
|
|
fe966f9c5d | ||
|
|
bcf7f793ec | ||
|
|
75de6ce041 | ||
|
|
4417a85ef7 | ||
|
|
9e0b67c04a | ||
|
|
8cd203e7b0 | ||
|
|
211160f7f8 | ||
|
|
2afc7262eb | ||
|
|
94a0282c70 | ||
|
|
96b78829ab | ||
|
|
35d5a90b2a | ||
|
|
50d73ad498 | ||
|
|
d1f8bc3062 | ||
|
|
d3353b1aa5 | ||
|
|
cd48fb4e08 | ||
|
|
f70738ecf5 | ||
|
|
2d63466597 | ||
|
|
ecf67443fb | ||
|
|
43987ad111 | ||
|
|
5fbd6c6196 | ||
|
|
1ec7f1f7e9 | ||
|
|
c5104c241a | ||
|
|
021ba3519d | ||
|
|
119f74cd05 | ||
|
|
f0b61c9e1f | ||
|
|
a573631068 | ||
|
|
a12444b258 | ||
|
|
17257228a9 | ||
|
|
53b6a9dfc1 | ||
|
|
c45f8ddfc3 | ||
|
|
e04c9c15ff | ||
|
|
d5cb363579 | ||
|
|
0748021ffa | ||
|
|
1d21368385 | ||
|
|
2a5865397a | ||
|
|
f5f378f3b0 | ||
|
|
861d5fa871 | ||
|
|
4518825aca | ||
|
|
260ebc83ff | ||
|
|
640cf3e75c | ||
|
|
7466990e0c | ||
|
|
b41715686c | ||
|
|
bca2a7a520 | ||
|
|
545c43fe2c | ||
|
|
eaf16318de | ||
|
|
a809f94a02 | ||
|
|
541862cda8 | ||
|
|
7ab2df9afe | ||
|
|
c45c4a1ad8 | ||
|
|
2b71f9a9f7 | ||
|
|
3db2e1c618 | ||
|
|
d24617d842 | ||
|
|
29dc8dc492 | ||
|
|
57b8ce3d7f | ||
|
|
1536c9f056 | ||
|
|
c2280555d3 | ||
|
|
532760e71a | ||
|
|
6da7eff62c | ||
|
|
a75192b6bf | ||
|
|
f4ceebb12c | ||
|
|
151327e9ba | ||
|
|
a343678bad | ||
|
|
1b2c97d381 | ||
|
|
d034eb6f4b | ||
|
|
08dc165450 | ||
|
|
50eef54ab6 | ||
|
|
a75bdd457f | ||
|
|
d5d219789f | ||
|
|
04eed1a768 | ||
|
|
87d17bcb9a | ||
|
|
2d3bd5c5cf | ||
|
|
12d76a2ad1 | ||
|
|
a0026b1bb3 | ||
|
|
761f11fa5f | ||
|
|
a7b28692ea | ||
|
|
daac6e1e5c | ||
|
|
8c9b7e4a01 | ||
|
|
1b0778141c | ||
|
|
96ed63b704 | ||
|
|
bb07bc639d | ||
|
|
c3eb4b2f51 | ||
|
|
9deeb37f21 | ||
|
|
0befdc0d34 | ||
|
|
606a21dff0 | ||
|
|
ef6a37fb7c | ||
|
|
2f7b8c4b59 | ||
|
|
968e82b072 | ||
|
|
0240eea776 | ||
|
|
dc8d94fa31 | ||
|
|
f6ec8cc882 | ||
|
|
b648d21068 | ||
|
|
2b63997b22 | ||
|
|
198daaf720 | ||
|
|
a1847dc6f2 | ||
|
|
8ebb3a5f31 | ||
|
|
7cf3dc2bf2 | ||
|
|
2346095323 | ||
|
|
40ae2d456c | ||
|
|
3152bcaaa6 | ||
|
|
c69c41c57f | ||
|
|
ea1f424b33 | ||
|
|
1ef828497c | ||
|
|
88b28faa48 | ||
|
|
7bfaacf936 | ||
|
|
05d8dae4e0 | ||
|
|
27cd92b4b1 | ||
|
|
4b6b5b5d95 | ||
|
|
6468575a42 | ||
|
|
5ea22cecad | ||
|
|
c8205d3f95 | ||
|
|
639377760c | ||
|
|
a9323f0528 | ||
|
|
2e31e90225 | ||
|
|
9d5b8188eb | ||
|
|
1f9e330f6a | ||
|
|
a69776fe33 | ||
|
|
d42dc46b8c | ||
|
|
c37dc2f874 | ||
|
|
9c20f13f95 | ||
|
|
e958e7f675 | ||
|
|
68113e694e | ||
|
|
88aec66ef8 | ||
|
|
f500596621 | ||
|
|
df5bafd0bb | ||
|
|
01fe9deb20 | ||
|
|
4e44298e98 | ||
|
|
a7c3a05c23 | ||
|
|
05fa36497b | ||
|
|
e6f8ae265e | ||
|
|
d107847747 | ||
|
|
ea8e270df6 | ||
|
|
4bc40e0663 | ||
|
|
5e6ccd990a | ||
|
|
1a8d673742 | ||
|
|
a4069aa6f6 | ||
|
|
3cfb90c078 | ||
|
|
594949d3f0 | ||
|
|
8fd34b5258 | ||
|
|
1161482a5b | ||
|
|
8a123e7a0d | ||
|
|
4ef3a20929 | ||
|
|
16c2e9b694 | ||
|
|
c9a9d310ef | ||
|
|
84996a5652 | ||
|
|
115afc5735 | ||
|
|
2ae75e876d | ||
|
|
126291d544 | ||
|
|
37db81f6b1 |
51
.github/ISSUE_TEMPLATE/01-bug-report-en.md
vendored
Normal file
51
.github/ISSUE_TEMPLATE/01-bug-report-en.md
vendored
Normal file
@@ -0,0 +1,51 @@
|
||||
---
|
||||
name: "[English] Bug report"
|
||||
about: Create a report to help us improve
|
||||
title: "[BUG] "
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
<!-- Be sure to put a clear title after [BUG] in the text box above -->
|
||||
<!-- Be sure to put a clear title after [BUG] in the text box above -->
|
||||
<!-- Be sure to put a clear title after [BUG] in the text box above -->
|
||||
|
||||
**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.
|
||||
|
||||
**Device Info (please complete the following information):**
|
||||
|
||||
- Device: [e.g. Pixel 4]
|
||||
- ROM: [e.g: AOSP]
|
||||
- ROM Version:
|
||||
- Android Version [e.g. 10]
|
||||
|
||||
**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.
|
||||
|
||||
<!--
|
||||
*Logs*
|
||||
if applicable, upload logs to help detect problem
|
||||
|
||||
`Open App` -> `Support` -> `Feedback` -> `Upload Logcat`
|
||||
-->
|
||||
24
.github/ISSUE_TEMPLATE/02-feature-request-en.md
vendored
Normal file
24
.github/ISSUE_TEMPLATE/02-feature-request-en.md
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
---
|
||||
name: "[English] Feature request"
|
||||
about: Suggest an idea for this app
|
||||
title: "[Feature Request] "
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
<!-- Be sure to put a clear title after [Feature Request] in the text box above -->
|
||||
<!-- Be sure to put a clear title after [Feature Request] in the text box above -->
|
||||
<!-- Be sure to put a clear title after [Feature Request] in the text box above -->
|
||||
|
||||
**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.
|
||||
49
.github/ISSUE_TEMPLATE/03-bug-report-zh-cn.md
vendored
Normal file
49
.github/ISSUE_TEMPLATE/03-bug-report-zh-cn.md
vendored
Normal file
@@ -0,0 +1,49 @@
|
||||
---
|
||||
name: "[简体中文] 创建错误报告"
|
||||
about: 创建错误报告以帮助我们改进应用
|
||||
title: "[BUG] "
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
<!-- 请务必在上方文本框处 [BUG] 后填入清晰明了的标题 -->
|
||||
<!-- 请务必在上方文本框处 [BUG] 后填入清晰明了的标题 -->
|
||||
<!-- 请务必在上方文本框处 [BUG] 后填入清晰明了的标题 -->
|
||||
|
||||
**描述出现的错误**
|
||||
请简洁的描述你遇到的错误
|
||||
|
||||
**如何复现该错误**
|
||||
复现步骤:
|
||||
1. ...
|
||||
2. ...
|
||||
3. ...
|
||||
4. ...
|
||||
|
||||
**预期行为**
|
||||
清晰简单的描述你预期的应用应该表现的行为
|
||||
|
||||
**屏幕截图**
|
||||
如果适用, 上传屏幕截图以帮助描述错误
|
||||
|
||||
**设备信息 (请完成以下信息):**
|
||||
- 机型: [例如: Pixel 4]
|
||||
- 系统/ROM: [例如: MIUI 11]
|
||||
- Android 版本 [例如: 10]
|
||||
- ROM版本 [例如: 20.3.19]
|
||||
|
||||
**应用信息**
|
||||
- 版本: [例如: 1.1.10]
|
||||
- 安装包文件名: [例如: app-release-arm64-v8a.apk]
|
||||
- 应用来源: [例如: Google Play]
|
||||
|
||||
**附加信息**
|
||||
其他的可能与改错误相关的信息
|
||||
|
||||
<!--
|
||||
*日志*
|
||||
如果适用, 上传日志以帮助侦测错误
|
||||
|
||||
`打开应用` -> `支持` -> `反馈` -> `上传日志`
|
||||
-->
|
||||
21
.github/ISSUE_TEMPLATE/04-feature-request-zh-cn.md
vendored
Normal file
21
.github/ISSUE_TEMPLATE/04-feature-request-zh-cn.md
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
---
|
||||
name: "[简体中文] 功能请求"
|
||||
about: 你希望的能够在应用中增加的功能
|
||||
title: "[Feature Request] "
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
<!-- 请务必在上方文本框处 [Feature Request] 后填入清晰明了的标题 -->
|
||||
<!-- 请务必在上方文本框处 [Feature Request] 后填入清晰明了的标题 -->
|
||||
<!-- 请务必在上方文本框处 [Feature Request] 后填入清晰明了的标题 -->
|
||||
|
||||
**功能描述**
|
||||
请清晰的描述你想要的功能
|
||||
|
||||
**描述你希望的实现方式**
|
||||
清晰的描述应用应该如何实现该功能
|
||||
|
||||
**附加信息**
|
||||
其他的与改功能相关的附加信息
|
||||
1
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
1
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
@@ -0,0 +1 @@
|
||||
blank_issues_enabled: false
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -23,9 +23,6 @@ gradle-app.setting
|
||||
*.keystore
|
||||
*.jks
|
||||
|
||||
# gradle
|
||||
.gradle
|
||||
|
||||
# clion cmake build
|
||||
cmake-build-*
|
||||
|
||||
|
||||
35
README.md
35
README.md
@@ -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`
|
||||
@@ -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)
|
||||
}
|
||||
139
app/build.gradle.kts
Normal file
139
app/build.gradle.kts
Normal file
@@ -0,0 +1,139 @@
|
||||
import java.util.*
|
||||
import java.security.*
|
||||
|
||||
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\"")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
task("injectPackageNameBase64") {
|
||||
doFirst {
|
||||
val packageName = android.defaultConfig.applicationId ?: return@doFirst
|
||||
|
||||
val base64 = Base64.getEncoder().encodeToString(packageName.toByteArray(Charsets.UTF_8))
|
||||
|
||||
android.buildTypes.forEach {
|
||||
it.buildConfigField("String", "PACKAGE_NAME_BASE64", "\"$base64\"")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
afterEvaluate {
|
||||
tasks["preBuild"].dependsOn(tasks["injectAppCenterKey"], tasks["injectPackageNameBase64"])
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -65,7 +65,4 @@ class ApkBrokenActivity : BaseActivity() {
|
||||
override fun shouldDisplayHomeAsUpEnabled(): Boolean {
|
||||
return false
|
||||
}
|
||||
|
||||
override val activityLabel: CharSequence
|
||||
get() = getText(R.string.application_broken)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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?
|
||||
)
|
||||
|
||||
|
||||
@@ -10,17 +10,15 @@ 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
|
||||
import java.io.File
|
||||
import kotlin.streams.toList
|
||||
|
||||
class LogViewerActivity : BaseActivity() {
|
||||
private val pauseMutex = Mutex()
|
||||
private var pollingThread: Thread? = null
|
||||
private val connection = object : ServiceConnection {
|
||||
override fun onServiceDisconnected(name: ComponentName?) {
|
||||
finish()
|
||||
@@ -49,12 +47,6 @@ class LogViewerActivity : BaseActivity() {
|
||||
startFileMode(file.toFile())
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
|
||||
pollingThread?.interrupt()
|
||||
}
|
||||
|
||||
override fun onStop() {
|
||||
super.onStop()
|
||||
|
||||
@@ -63,9 +55,6 @@ class LogViewerActivity : BaseActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
override val activityLabel: CharSequence
|
||||
get() = getText(R.string.log_viewer)
|
||||
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
|
||||
@@ -82,6 +71,7 @@ class LogViewerActivity : BaseActivity() {
|
||||
mainList.itemAnimator?.removeDuration = 100
|
||||
|
||||
stop.setOnClickListener {
|
||||
unbindService(connection)
|
||||
stopService(LogcatService::class.intent)
|
||||
finish()
|
||||
}
|
||||
@@ -95,17 +85,17 @@ class LogViewerActivity : BaseActivity() {
|
||||
launch {
|
||||
val items = withContext(Dispatchers.IO) {
|
||||
try {
|
||||
file.readText()
|
||||
.split("\n")
|
||||
.parallelStream()
|
||||
.map { it.trim() }
|
||||
.filter { it.isNotEmpty() && !it.startsWith("#") }
|
||||
.map { it.split(" ", limit = 3) }
|
||||
.filter { it.size == 3 }
|
||||
.map { LogEvent(LogEvent.Level.valueOf(it[1]), it[2], it[0].toLong()) }
|
||||
.toList()
|
||||
file.bufferedReader().useLines { lines ->
|
||||
lines
|
||||
.map { it.trim() }
|
||||
.filter { it.isNotEmpty() && !it.startsWith("#") }
|
||||
.map { it.split(" ", limit = 3) }
|
||||
.filter { it.size == 3 }
|
||||
.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()
|
||||
}
|
||||
|
||||
@@ -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.*
|
||||
@@ -101,11 +101,13 @@ class LogcatService : Service(), CoroutineScope by MainScope(), IInterface {
|
||||
|
||||
connection.onServiceDisconnected(null)
|
||||
|
||||
unbindService(connection)
|
||||
|
||||
stopForeground(true)
|
||||
|
||||
super.onDestroy()
|
||||
|
||||
isServiceRunning = false
|
||||
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
override fun onBind(intent: Intent?): IBinder? {
|
||||
|
||||
@@ -11,11 +11,12 @@ 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.core.event.LogEvent
|
||||
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
|
||||
@@ -24,12 +25,15 @@ import kotlinx.android.synthetic.main.activity_logs.*
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.io.FileInputStream
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
|
||||
class LogsActivity : BaseActivity() {
|
||||
companion object {
|
||||
const val REQUEST_CODE = 50000
|
||||
|
||||
private val LOG_EXPORT_DATE_FORMAT = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.ENGLISH)
|
||||
private val LOG_EXPORT_TIME_FORMAT = SimpleDateFormat("HH:mm:ss", Locale.ENGLISH)
|
||||
}
|
||||
|
||||
private var lastWriteFile: LogFile? = null
|
||||
@@ -92,9 +96,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) {
|
||||
@@ -105,9 +106,31 @@ class LogsActivity : BaseActivity() {
|
||||
|
||||
launch {
|
||||
withContext(Dispatchers.IO) {
|
||||
contentResolver.openOutputStream(url)?.use { output ->
|
||||
FileInputStream(logsDir.resolve(file.fileName)).use { input ->
|
||||
input.copyTo(output)
|
||||
contentResolver.openOutputStream(url)?.bufferedWriter()?.use { output ->
|
||||
output.write("# Logcat on " + LOG_EXPORT_DATE_FORMAT.format(Date(file.date)) + "\n")
|
||||
|
||||
logsDir.resolve(file.fileName).bufferedReader().useLines { lines ->
|
||||
lines.map { it.trim() }
|
||||
.filter { it.isNotEmpty() && !it.startsWith("#") }
|
||||
.map { it.split(" ", limit = 3) }
|
||||
.filter { it.size == 3 }
|
||||
.map {
|
||||
LogEvent(
|
||||
LogEvent.Level.valueOf(it[1]),
|
||||
it[2],
|
||||
it[0].toLong()
|
||||
)
|
||||
}
|
||||
.forEach {
|
||||
output.write(
|
||||
String.format(
|
||||
"%s |%s| %s\n",
|
||||
LOG_EXPORT_TIME_FORMAT.format(Date(it.time)),
|
||||
it.level.toString(),
|
||||
it.message
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -186,6 +209,7 @@ class LogsActivity : BaseActivity() {
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT
|
||||
)
|
||||
}
|
||||
|
||||
@ColorInt
|
||||
val errorColor = TypedValue().run {
|
||||
theme.resolveAttribute(R.attr.colorError, this, true)
|
||||
|
||||
@@ -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.*
|
||||
|
||||
@@ -35,8 +35,14 @@ class MainActivity : BaseActivity() {
|
||||
stopClashService()
|
||||
} else {
|
||||
val vpnRequest = startClashService()
|
||||
if (vpnRequest != null)
|
||||
startActivityForResult(vpnRequest, REQUEST_CODE)
|
||||
if (vpnRequest != null) {
|
||||
val resolved = packageManager.resolveActivity(vpnRequest, 0)
|
||||
if (resolved != null) {
|
||||
startActivityForResult(vpnRequest, REQUEST_CODE)
|
||||
} else {
|
||||
showSnackbarException(getString(R.string.missing_vpn_component), null)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -77,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)
|
||||
@@ -97,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() {
|
||||
@@ -155,7 +159,7 @@ class MainActivity : BaseActivity() {
|
||||
queryGeneral()
|
||||
}
|
||||
val active = withProfile {
|
||||
queryActiveProfile()
|
||||
queryActive()
|
||||
}
|
||||
|
||||
val modeResId = when (general.mode) {
|
||||
|
||||
@@ -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
|
||||
@@ -39,7 +42,7 @@ class MainApplication : Application() {
|
||||
if (!report.stackTrace.contains("DeadObjectException"))
|
||||
return mutableListOf()
|
||||
|
||||
val logcat = LogcatDumper.dump().joinToString(separator = "\n")
|
||||
val logcat = LogcatDumper.dumpCrash()
|
||||
|
||||
return mutableListOf(
|
||||
ErrorAttachmentLog.attachmentWithText(logcat, "logcat.txt")
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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?
|
||||
|
||||
@@ -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,167 +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 != "http" && url.scheme != "https" && url.scheme != "content")
|
||||
) {
|
||||
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)
|
||||
@@ -214,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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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", type)
|
||||
.putExtra("intent", intent)
|
||||
.putExtra("name", name)
|
||||
.putExtra("url", uri)
|
||||
.putExtra("interval", 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -268,7 +265,7 @@ class ProxiesActivity : BaseActivity(), ScrollBinding.Callback {
|
||||
scrollBinding.scrollMaster(selected)
|
||||
}
|
||||
|
||||
delay(200)
|
||||
delay(500)
|
||||
|
||||
refreshMutex.unlock()
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -14,7 +14,4 @@ class SettingsBehaviorActivity : BaseActivity() {
|
||||
.replace(R.id.fragment, BehaviorFragment())
|
||||
.commit()
|
||||
}
|
||||
|
||||
override val activityLabel: CharSequence?
|
||||
get() = getText(R.string.behavior)
|
||||
}
|
||||
@@ -14,7 +14,4 @@ class SettingsInterfaceActivity : BaseActivity() {
|
||||
.replace(R.id.fragment, InterfaceFragment())
|
||||
.commit()
|
||||
}
|
||||
|
||||
override val activityLabel: CharSequence?
|
||||
get() = getText(R.string.interface_)
|
||||
}
|
||||
@@ -19,6 +19,11 @@ class SettingsNetworkActivity : BaseActivity() {
|
||||
Snackbar.make(rootView, R.string.options_unavailable, Snackbar.LENGTH_INDEFINITE).show()
|
||||
}
|
||||
|
||||
override val activityLabel: CharSequence?
|
||||
get() = getText(R.string.network)
|
||||
}
|
||||
override suspend fun onClashStopped(reason: String?) {
|
||||
recreate()
|
||||
}
|
||||
|
||||
override suspend fun onClashStarted() {
|
||||
recreate()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,17 +1,21 @@
|
||||
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 android.text.Html
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import com.github.kr328.clash.dump.LogcatDumper
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import com.microsoft.appcenter.crashes.Crashes
|
||||
import com.microsoft.appcenter.crashes.ingestion.models.ErrorAttachmentLog
|
||||
import kotlinx.android.synthetic.main.activity_support.*
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
class SupportActivity : BaseActivity() {
|
||||
override val activityLabel: CharSequence?
|
||||
get() = getText(R.string.support)
|
||||
class UserRequestTrackException: Exception()
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
@@ -20,6 +24,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 +54,22 @@ 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)
|
||||
title = getString(R.string.upload_logcat),
|
||||
summary = getString(R.string.upload_logcat_summary)
|
||||
) {
|
||||
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()
|
||||
AlertDialog.Builder(this@SupportActivity)
|
||||
.setTitle(R.string.upload_logcat)
|
||||
.setMessage(R.string.upload_logcat_warn)
|
||||
.setNegativeButton(R.string.cancel) {_, _ -> }
|
||||
.setPositiveButton(R.string.ok) {_, _ -> upload() }
|
||||
.show()
|
||||
}
|
||||
}
|
||||
|
||||
option(
|
||||
title = getString(R.string.github_issues),
|
||||
summary = getString(R.string.github_issues_url)
|
||||
@@ -71,9 +82,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)
|
||||
@@ -88,4 +101,19 @@ class SupportActivity : BaseActivity() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun upload() {
|
||||
launch {
|
||||
withContext(Dispatchers.IO) {
|
||||
val attachment = ErrorAttachmentLog
|
||||
.attachmentWithText(LogcatDumper.dumpAll(), "logcat.txt")
|
||||
|
||||
Crashes.trackError(UserRequestTrackException(), null, listOf(attachment))
|
||||
}
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
Snackbar.make(rootView, R.string.uploaded, Snackbar.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -1,34 +1,37 @@
|
||||
package com.github.kr328.clash.dump
|
||||
|
||||
object LogcatDumper {
|
||||
fun dump(): List<String> {
|
||||
fun dumpCrash(): String {
|
||||
return try {
|
||||
val process =
|
||||
Runtime.getRuntime().exec(arrayOf("logcat", "-d", "-s", "-v", "raw", "Go"))
|
||||
Runtime.getRuntime().exec(arrayOf("logcat", "-d", "-s", "Go", "AndroidRuntime", "DEBUG"))
|
||||
|
||||
val result = process.inputStream.bufferedReader().useLines {
|
||||
var list = mutableListOf<String>()
|
||||
var capture = false
|
||||
|
||||
it.forEach { line ->
|
||||
if (line.startsWith("panic")) {
|
||||
capture = true
|
||||
|
||||
list = mutableListOf()
|
||||
}
|
||||
|
||||
if (capture)
|
||||
list.add(line)
|
||||
}
|
||||
|
||||
list
|
||||
val result = process.inputStream.use {
|
||||
it.reader().readText()
|
||||
}
|
||||
|
||||
process.waitFor()
|
||||
|
||||
result
|
||||
} catch (e: Exception) {
|
||||
emptyList()
|
||||
""
|
||||
}
|
||||
}
|
||||
|
||||
fun dumpAll(): String {
|
||||
return try {
|
||||
val process =
|
||||
Runtime.getRuntime().exec(arrayOf("logcat", "-d"))
|
||||
|
||||
val result = process.inputStream.use {
|
||||
it.reader().readText()
|
||||
}
|
||||
|
||||
process.waitFor()
|
||||
|
||||
result
|
||||
} catch (e: Exception) {
|
||||
""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,215 @@
|
||||
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() {
|
||||
private var root: CommonUiLayout? = null
|
||||
|
||||
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 {
|
||||
root = this
|
||||
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 = ""
|
||||
interval = 0
|
||||
return@onTextChanged
|
||||
}
|
||||
|
||||
val value = s.toIntOrNull()
|
||||
if (value == null || value < 15) {
|
||||
content = ""
|
||||
interval = 0
|
||||
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
|
||||
|
||||
root?.apply {
|
||||
data.data?.apply {
|
||||
screen.requireElement<TextInput>(KEY_URL).content = this.toString()
|
||||
}
|
||||
|
||||
data.getStringExtra(Constants.URL_PROVIDER_INTENT_EXTRA_NAME)?.also {
|
||||
screen.requireElement<TextInput>(KEY_NAME).apply {
|
||||
if (content.isBlank())
|
||||
content = it
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
super.onActivityResult(requestCode, resultCode, data)
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
super.onSaveInstanceState(outState)
|
||||
|
||||
root?.apply {
|
||||
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) {
|
||||
root?.apply {
|
||||
Snackbar.make(
|
||||
this,
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
@@ -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 {
|
||||
|
||||
@@ -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", "")
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -5,25 +5,26 @@ import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.ServiceConnection
|
||||
import android.os.Build
|
||||
import android.os.Handler
|
||||
import android.os.IBinder
|
||||
import android.os.RemoteException
|
||||
import android.util.Base64
|
||||
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.BuildConfig
|
||||
import com.github.kr328.clash.Constants
|
||||
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.util.zip.ZipFile
|
||||
|
||||
object Remote {
|
||||
@@ -47,19 +48,7 @@ object Remote {
|
||||
if (service != null)
|
||||
instance = ClashClient(IClashManager.Stub.asInterface(service))
|
||||
|
||||
service?.linkToDeath({
|
||||
val log = LogcatDumper.dump().joinToString(separator = "\n")
|
||||
|
||||
val attachmentLog = ErrorAttachmentLog
|
||||
.attachmentWithText(log, "logcat.txt")
|
||||
|
||||
Crashes.trackError(
|
||||
RemoteException("Clash Service Crashed"),
|
||||
null, listOf(attachmentLog)
|
||||
)
|
||||
|
||||
onServiceDisconnected(null)
|
||||
}, 0)
|
||||
service?.linkToDeath({ onServiceDisconnected(null) }, 0)
|
||||
|
||||
sender = GlobalScope.launch {
|
||||
while (isActive) {
|
||||
@@ -84,9 +73,7 @@ object Remote {
|
||||
if (service != null)
|
||||
instance = ProfileClient(IProfileService.Stub.asInterface(service))
|
||||
|
||||
service?.linkToDeath({
|
||||
onServiceDisconnected(null)
|
||||
}, 0)
|
||||
service?.linkToDeath({ onServiceDisconnected(null) }, 0)
|
||||
|
||||
sender = GlobalScope.launch {
|
||||
while (isActive) {
|
||||
@@ -97,60 +84,67 @@ 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): Boolean {
|
||||
return withContext(Dispatchers.IO) {
|
||||
fun init(application: Application) {
|
||||
observer.register(application)
|
||||
}
|
||||
|
||||
private fun verifyApk(application: Application): Boolean {
|
||||
return try {
|
||||
val sp = application.getSharedPreferences(
|
||||
Constants.PREFERENCE_NAME_APP,
|
||||
Context.MODE_PRIVATE
|
||||
@@ -158,21 +152,68 @@ object Remote {
|
||||
val pkg = application.packageManager.getPackageInfo(application.packageName, 0)
|
||||
|
||||
if (sp.getLong(Constants.PREFERENCE_KEY_LAST_INSTALL, 0) == pkg.lastUpdateTime)
|
||||
return@withContext true
|
||||
return true
|
||||
|
||||
val pkgName: String = try {
|
||||
application::class.java.getMethod(
|
||||
String(
|
||||
charArrayOf(
|
||||
'g',
|
||||
'e',
|
||||
't',
|
||||
'P',
|
||||
'a',
|
||||
'c',
|
||||
'k',
|
||||
'a',
|
||||
'g',
|
||||
'e',
|
||||
'N',
|
||||
'a',
|
||||
'm',
|
||||
'e'
|
||||
)
|
||||
)
|
||||
).invoke(application)?.toString()
|
||||
} catch (e: Exception) {
|
||||
Log.w("getPackageName failure", e)
|
||||
null
|
||||
} ?: application.packageName
|
||||
|
||||
val packageNameBase64 = Base64
|
||||
.encodeToString(pkgName.toByteArray(Charsets.UTF_8), Base64.NO_WRAP)
|
||||
|
||||
if (packageNameBase64 != BuildConfig.PACKAGE_NAME_BASE64)
|
||||
return false
|
||||
|
||||
val info = application.applicationInfo
|
||||
val sources =
|
||||
info.splitSourceDirs ?: arrayOf(info.sourceDir) ?: return@withContext false
|
||||
info.splitSourceDirs ?: arrayOf(info.sourceDir) ?: return false
|
||||
|
||||
for (apk in sources) {
|
||||
if (ZipFile(apk).entries().asSequence().any { it.name.endsWith("libgojni.so") }) {
|
||||
sp.edit {
|
||||
putLong(Constants.PREFERENCE_KEY_LAST_INSTALL, pkg.lastUpdateTime)
|
||||
}
|
||||
return@withContext true
|
||||
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
|
||||
}
|
||||
return@withContext false
|
||||
} catch (e: Exception) {
|
||||
Crashes.trackError(e)
|
||||
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,9 +4,10 @@ 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
|
||||
import java.lang.Exception
|
||||
|
||||
object RemoteUtils {
|
||||
fun detectClashRunning(context: Context): Boolean {
|
||||
@@ -24,7 +25,7 @@ object RemoteUtils {
|
||||
)
|
||||
|
||||
return pong != null
|
||||
} catch (e: IllegalArgumentException) {
|
||||
} catch (e: Exception) {
|
||||
context.startActivity(ApkBrokenActivity::class.intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK))
|
||||
|
||||
return false
|
||||
@@ -46,7 +47,7 @@ object RemoteUtils {
|
||||
)
|
||||
|
||||
return pong?.getString("name")
|
||||
} catch (e: IllegalArgumentException) {
|
||||
} catch (e: Exception) {
|
||||
context.startActivity(ApkBrokenActivity::class.intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK))
|
||||
|
||||
return null
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
@@ -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,16 +115,24 @@
|
||||
<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>
|
||||
<string name="upload_logcat">上传日志</string>
|
||||
<string name="upload_logcat_summary">上传日志以帮助我们侦测问题</string>
|
||||
<string name="upload_logcat_warn">请注意, 上传的日志可能包含个人敏感信息, 仍要继续吗?</string>
|
||||
<string name="uploaded">已上传</string>
|
||||
</resources>
|
||||
@@ -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>
|
||||
@@ -172,4 +170,12 @@
|
||||
<string name="github_releases_url" translatable="false">https://github.com/Kr328/ClashForAndroid/releases</string>
|
||||
|
||||
<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>
|
||||
|
||||
<string name="upload_logcat">Upload Logcat</string>
|
||||
<string name="upload_logcat_summary">Upload logcat to help us detect issues</string>
|
||||
<string name="upload_logcat_warn">Please note that the uploaded logs may contain personally sensitive information, do you still want to continue?</string>
|
||||
<string name="uploaded">Uploaded</string>
|
||||
</resources>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
53
build.gradle
53
build.gradle
@@ -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 = 10106
|
||||
gVersionName = "1.1.6"
|
||||
|
||||
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
52
build.gradle.kts
Normal 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.72"
|
||||
|
||||
rootProject.extra.apply {
|
||||
this["gBuildToolsVersion"] = "29.0.3"
|
||||
|
||||
this["gCompileSdkVersion"] = 29
|
||||
this["gMinSdkVersion"] = 24
|
||||
this["gTargetSdkVersion"] = 29
|
||||
|
||||
this["gVersionCode"] = 10214
|
||||
this["gVersionName"] = "1.2.14"
|
||||
|
||||
this["gKotlinVersion"] = kotlinVersion
|
||||
this["gKotlinCoroutineVersion"] = "1.3.6"
|
||||
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-rc01")
|
||||
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)
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
@@ -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
63
common/build.gradle.kts
Normal 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()
|
||||
}
|
||||
0
common/consumer-rules.pro
Normal file
0
common/consumer-rules.pro
Normal file
21
common/proguard-rules.pro
vendored
Normal file
21
common/proguard-rules.pro
vendored
Normal 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
|
||||
9
common/src/main/AndroidManifest.xml
Normal file
9
common/src/main/AndroidManifest.xml
Normal 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>
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.github.kr328.clash.core
|
||||
package com.github.kr328.clash.common
|
||||
|
||||
object Constants {
|
||||
const val TAG = "ClashForAndroid"
|
||||
16
common/src/main/java/com/github/kr328/clash/common/Global.kt
Normal file
16
common/src/main/java/com/github/kr328/clash/common/Global.kt
Normal 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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package com.github.kr328.clash.common
|
||||
|
||||
object Permissions {
|
||||
val PERMISSION_RECEIVE_BROADCASTS: String
|
||||
get() = Global.application.packageName + ".permission.RECEIVE_BROADCASTS"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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)!!
|
||||
}
|
||||
@@ -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 {
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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) =
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
5
common/src/main/res/values-zh/strings.xml
Normal file
5
common/src/main/res/values-zh/strings.xml
Normal 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>
|
||||
5
common/src/main/res/values/strings.xml
Normal file
5
common/src/main/res/values/strings.xml
Normal 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>
|
||||
@@ -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
82
core/build.gradle.kts
Normal 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["resetGolangPathMode"])
|
||||
tasks["preBuild"].dependsOn(tasks["extractSources"], tasks["downloadGeoipDatabase"])
|
||||
}
|
||||
218
core/clash.gradle.kts
Normal file
218
core/clash.gradle.kts
Normal file
@@ -0,0 +1,218 @@
|
||||
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("assembleClashCore") {
|
||||
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["assembleClashCore"])
|
||||
|
||||
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") {
|
||||
dependsOn(tasks["extractSources"])
|
||||
|
||||
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("resetGolangPathMode", type = Exec::class) {
|
||||
onlyIf {
|
||||
!Os.isFamily(Os.FAMILY_WINDOWS)
|
||||
}
|
||||
|
||||
commandLine("chmod", "-R", "777", buildDir.resolve(Constants.GOLANG_PATH))
|
||||
|
||||
isIgnoreExitValue = true
|
||||
}
|
||||
@@ -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() {
|
||||
|
||||
@@ -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)
|
||||
}()
|
||||
}
|
||||
|
||||
|
||||
@@ -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,78 +83,65 @@ 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(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func SetSelectedProxy(name, proxy string) bool {
|
||||
p := tunnel.Proxies()[name]
|
||||
if p == nil {
|
||||
log.Infoln("Set %s: Not such proxy group", name)
|
||||
return false
|
||||
}
|
||||
|
||||
pb, ok := p.(*outbound.Proxy)
|
||||
if !ok {
|
||||
log.Infoln("Set %s: Not a proxy object", name)
|
||||
return false
|
||||
}
|
||||
|
||||
selector, ok := pb.ProxyAdapter.(*outboundgroup.Selector)
|
||||
if !ok {
|
||||
log.Infoln("Set %s: Not a selector group", name)
|
||||
return false
|
||||
}
|
||||
|
||||
selected := selector.Now()
|
||||
if selected == proxy {
|
||||
log.Infoln("Set " + name + " -> " + proxy)
|
||||
return true
|
||||
}
|
||||
|
||||
if err := selector.Set(proxy); err != nil {
|
||||
log.Infoln("Set %s: %s", name, err.Error())
|
||||
return false
|
||||
}
|
||||
|
||||
for _, conn := range tunnel.DefaultManager.Snapshot().Connections {
|
||||
for _, p := range conn.Chain() {
|
||||
if p == name {
|
||||
_ = conn.Close()
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
log.Infoln("Set " + name + " -> " + proxy)
|
||||
|
||||
return true
|
||||
|
||||
@@ -20,17 +20,19 @@ func init() {
|
||||
dialer.ListenConfigHook = onNewListenConfig
|
||||
}
|
||||
|
||||
func onNewDialer(dialer *net.Dialer) {
|
||||
func onNewDialer(dialer *net.Dialer) error {
|
||||
dialer.Control = onNewSocket
|
||||
return nil
|
||||
}
|
||||
|
||||
func onNewListenConfig(listen *net.ListenConfig) {
|
||||
func onNewListenConfig(listen *net.ListenConfig) error {
|
||||
listen.Control = onNewSocket
|
||||
return nil
|
||||
}
|
||||
|
||||
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 +40,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() {
|
||||
|
||||
Submodule core/src/main/golang/clash updated: 380c0c09cd...ff4ad6d96a
@@ -1,11 +1,13 @@
|
||||
package profile
|
||||
package config
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"github.com/kr328/cfa/utils"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"syscall"
|
||||
|
||||
@@ -28,68 +30,76 @@ var client = &http.Client{
|
||||
|
||||
client, server := net.Pipe()
|
||||
|
||||
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()
|
||||
}()
|
||||
tunnel.Add(inbound.NewSocket(socks5.ParseAddr(address), server, constant.HTTP))
|
||||
|
||||
return client, nil
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
func DownloadAndCheck(url, output, baseDir string) error {
|
||||
request, err := http.NewRequest("GET", url, nil)
|
||||
func fetchRemote(sUrl string) ([]byte, error) {
|
||||
uri, err := url.Parse(sUrl)
|
||||
if err != nil {
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
request, err := http.NewRequest("GET", uri.String(), nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
request.Header.Set("User-Agent", "ClashForAndroid/"+ApplicationVersion)
|
||||
if user := uri.User; user != nil {
|
||||
password, _ := user.Password()
|
||||
request.SetBasicAuth(user.Username(), password)
|
||||
}
|
||||
|
||||
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)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return SaveAndCheck(data, output, baseDir)
|
||||
return ioutil.ReadAll(file)
|
||||
}
|
||||
|
||||
func SaveAndCheck(data []byte, output, baseDir string) error {
|
||||
cfg, err := parseConfig(data, baseDir)
|
||||
func PullRemote(url, output, baseDir string) error {
|
||||
data, err := fetchRemote(url)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, v := range cfg.Providers {
|
||||
v.Destroy()
|
||||
return save(data, output, baseDir)
|
||||
}
|
||||
|
||||
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 {
|
||||
_, err := parseConfig(data, baseDir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return ioutil.WriteFile(output, data, defaultFileMode)
|
||||
75
core/src/main/golang/config/load.go
Normal file
75
core/src/main/golang/config/load.go
Normal 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
|
||||
}
|
||||
93
core/src/main/golang/config/patch.go
Normal file
93
core/src/main/golang/config/patch.go
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,15 +1,11 @@
|
||||
module github.com/kr328/cfa
|
||||
|
||||
go 1.13
|
||||
go 1.14
|
||||
|
||||
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-20200511061008-edd2b9608763
|
||||
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
|
||||
|
||||
@@ -1,158 +1,84 @@
|
||||
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/cenkalti/backoff v0.0.0-20190506075156-2146c9339422/go.mod h1:b6Nc7NRH5C4aCISLry0tLnTjcuTEvoiqcWDdsU0sOGM=
|
||||
github.com/comzyh/gvisor v0.0.0-20200510171600-c4d4be34b573/go.mod h1:h+6z+LgXLDMOwidDk/XF0VPA+V6HLTWBWkwmRjgWwF8=
|
||||
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/chi v4.1.1+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ=
|
||||
github.com/go-chi/cors v1.1.1/go.mod h1:K2Yje0VW/SJzxiyMYu6iPQYa7hMjQX2i/F491VChg1I=
|
||||
github.com/go-chi/render v1.0.1/go.mod h1:pq4Rr7HbnsdaeHagklXub+p6Wd16Af5l9koip1OvJns=
|
||||
github.com/gofrs/flock v0.6.1-0.20180915234121-886344bea079/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU=
|
||||
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/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
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/google/subcommands v0.0.0-20190508160503-636abe8753b8/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk=
|
||||
github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
|
||||
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk=
|
||||
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/kr/pretty v0.2.0 h1:s5hAObm+yFO5uHYt5dYjxi2rXrsnmRpJx4OYvIWUaQs=
|
||||
github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr328/tun2socket v0.0.0-20200511040303-5c6e74fe4a3c h1:o0m4oU/loVTbzdt9SopN0d4WRn19LP6VBdEjy0vRBQo=
|
||||
github.com/kr328/tun2socket v0.0.0-20200511040303-5c6e74fe4a3c/go.mod h1:FWfSixjrLgtK+dHkDoN6lHMNhvER24gnjUZd/wt8Z9o=
|
||||
github.com/kr328/tun2socket v0.0.0-20200511061008-edd2b9608763 h1:VniQVXI2Nfa9RrqkoGpjUHnVh1wIvJWVXGDhzs/kvcA=
|
||||
github.com/kr328/tun2socket v0.0.0-20200511061008-edd2b9608763/go.mod h1:FWfSixjrLgtK+dHkDoN6lHMNhvER24gnjUZd/wt8Z9o=
|
||||
github.com/miekg/dns v1.1.29 h1:xHBEhR+t5RzcFJjBLJlax2daXOrTYtr9z4WdKEfWFzg=
|
||||
github.com/miekg/dns v1.1.29/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7xM=
|
||||
github.com/opencontainers/runtime-spec v0.1.2-0.20171211145439-b2d941ef6a78/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0=
|
||||
github.com/oschwald/geoip2-golang v1.4.0 h1:5RlrjCgRyIGDz/mBmPfnAF4h8k0IAcRv9PvrpOfz+Ug=
|
||||
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/sirupsen/logrus v1.5.0 h1:1N5EYkVAPEywqZRJd7cwnRtCb6xJx7NH3T3WUTF980Q=
|
||||
github.com/sirupsen/logrus v1.5.0/go.mod h1:+F7Ogzej0PZc/94MaYx/nvG9jOFMD2osvC3s+Squfpo=
|
||||
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 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4=
|
||||
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||
github.com/syndtr/gocapability v0.0.0-20180916011248-d98352740cb2/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww=
|
||||
github.com/vishvananda/netlink v1.0.1-0.20190318003149-adb577d4a45e/go.mod h1:+SR5DhBJrl6ZM7CoCKvpw5BKroDKQ+PJqOg65H/2ktk=
|
||||
github.com/vishvananda/netns v0.0.0-20171111001504-be1fbeda1936/go.mod h1:ZjcWmFBXmLKZu9Nxj3WKYEafiSqer2rnvPr0en9UNpI=
|
||||
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-20200423211502-4bdfaf469ed5 h1:Q7tZBpemrlsc2I7IyODzhtallWRSm4Q0d09pL6XbQtU=
|
||||
golang.org/x/crypto v0.0.0-20200423211502-4bdfaf469ed5/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-20200421231249-e086a090c8fd h1:QPwSajcTUrFriMF1nJ3XzgoqakqQEsnZf9LdXdi2nkI=
|
||||
golang.org/x/net v0.0.0-20200421231249-e086a090c8fd/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
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 h1:WXEvlFVvvGxCJLG6REjsT03iWnKLEWinaScsxF2Vm2o=
|
||||
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/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd h1:xhmwyvizuTgC2qz7ZlMluP20uW+C3Rm0FD/WLDX8884=
|
||||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/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/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/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=
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
252
core/src/main/golang/tun/dns.go
Normal file
252
core/src/main/golang/tun/dns.go
Normal 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(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(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
|
||||
}
|
||||
21
core/src/main/golang/tun/log.go
Normal file
21
core/src/main/golang/tun/log.go
Normal file
@@ -0,0 +1,21 @@
|
||||
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...)
|
||||
}
|
||||
@@ -1,36 +1,119 @@
|
||||
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)
|
||||
_, ipv4Loopback, _ := net.ParseCIDR("127.0.0.0/8")
|
||||
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) || ipv4Loopback.Contains(endpoint.Target.IP) {
|
||||
_ = conn.Close()
|
||||
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))
|
||||
})
|
||||
adapter.SetUDPHandler(func(payload []byte, endpoint *binding.Endpoint, sender redirect.UDPSender) {
|
||||
if gatewayNet.Contains(endpoint.Target.IP) || ipv4Loopback.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,
|
||||
send: sender,
|
||||
recycle: udpRecycle,
|
||||
}
|
||||
|
||||
tunnel.AddPacket(adapters.NewPacket(addr, pkt, C.SOCKS))
|
||||
})
|
||||
|
||||
setHijackAddress(net.ParseIP(dnsAddress))
|
||||
InitialResolver()
|
||||
|
||||
adapter.Start()
|
||||
|
||||
log.Infoln("Android tun started")
|
||||
|
||||
@@ -41,21 +124,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)
|
||||
}
|
||||
|
||||
56
core/src/main/golang/tun/udp.go
Normal file
56
core/src/main/golang/tun/udp.go
Normal file
@@ -0,0 +1,56 @@
|
||||
package tun
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"github.com/kr328/tun2socket/binding"
|
||||
"github.com/kr328/tun2socket/redirect"
|
||||
"net"
|
||||
)
|
||||
|
||||
type udpPacket struct {
|
||||
payload []byte
|
||||
endpoint *binding.Endpoint
|
||||
send redirect.UDPSender
|
||||
recycle func([]byte)
|
||||
}
|
||||
|
||||
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.send(b, ep)
|
||||
}
|
||||
|
||||
func (conn *udpPacket) LocalAddr() net.Addr {
|
||||
return &net.UDPAddr{
|
||||
IP: conn.endpoint.Source.IP,
|
||||
Port: int(conn.endpoint.Source.Port),
|
||||
Zone: "",
|
||||
}
|
||||
}
|
||||
|
||||
func (conn *udpPacket) Drop() {
|
||||
conn.recycle(conn.payload)
|
||||
}
|
||||
7
core/src/main/golang/utils/close.go
Normal file
7
core/src/main/golang/utils/close.go
Normal file
@@ -0,0 +1,7 @@
|
||||
package utils
|
||||
|
||||
import "io"
|
||||
|
||||
func CloseSilent(closer io.Closer) {
|
||||
_ = closer.Close()
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user