mirror of
https://github.com/MetaCubeX/ClashMetaForAndroid.git
synced 2026-05-09 18:11:26 +08:00
Compare commits
126 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
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 | ||
|
|
a3cfd10d39 | ||
|
|
0e61f02046 | ||
|
|
d8b1c0b7b8 | ||
|
|
8bf908b731 | ||
|
|
568f1f12e7 | ||
|
|
ca739a5e2f | ||
|
|
7833f747d5 | ||
|
|
977519b383 | ||
|
|
b39b3812d0 | ||
|
|
f7dddb41fa | ||
|
|
053300919c | ||
|
|
93dff86383 | ||
|
|
1b7e9c6b12 | ||
|
|
f072cd0839 | ||
|
|
f626a0fc5c | ||
|
|
63da12f046 | ||
|
|
ce1beafb08 |
43
.github/ISSUE_TEMPLATE/01-bug-report-en.md
vendored
Normal file
43
.github/ISSUE_TEMPLATE/01-bug-report-en.md
vendored
Normal file
@@ -0,0 +1,43 @@
|
||||
---
|
||||
name: "[English] Bug report"
|
||||
about: Create a report to help us improve
|
||||
title: "[BUG] "
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
**To Reproduce**
|
||||
Steps to reproduce the behavior:
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
|
||||
**Expected behavior**
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
**Screenshots**
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
|
||||
**Logs**
|
||||
if applicable, add logs to help detect problem
|
||||
|
||||
**Device Info (please complete the following information):**
|
||||
|
||||
- Device: [e.g. Pixel 4]
|
||||
- ROM: [e.g: AOSP]
|
||||
- ROM Version:
|
||||
- Android Version [e.g. Oreo]
|
||||
|
||||
**Application Info (please complete the following information):**
|
||||
|
||||
- Version: [e.g. 1.1.10]
|
||||
- Apk File Name: [e.g. app-release-arm64-v8a.apk]
|
||||
- Distribution Channel: [e.g. Google Play]
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the problem here.
|
||||
20
.github/ISSUE_TEMPLATE/02-feature-request-en.md
vendored
Normal file
20
.github/ISSUE_TEMPLATE/02-feature-request-en.md
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
---
|
||||
name: "[English] Feature request"
|
||||
about: Suggest an idea for this app
|
||||
title: "[Feature Request] "
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
|
||||
**Describe the solution you'd like**
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
|
||||
**Additional context**
|
||||
Add any other context or screenshots about the feature request here.
|
||||
42
.github/ISSUE_TEMPLATE/03-bug-report-zh-cn.md
vendored
Normal file
42
.github/ISSUE_TEMPLATE/03-bug-report-zh-cn.md
vendored
Normal file
@@ -0,0 +1,42 @@
|
||||
---
|
||||
name: "[简体中文] 创建错误报告"
|
||||
about: 创建错误报告以帮助我们改进应用
|
||||
title: "[BUG] "
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**描述出现的错误**
|
||||
请简洁的描述你遇到的错误
|
||||
|
||||
**如何复现该错误**
|
||||
复现步骤:
|
||||
1. ...
|
||||
2. ...
|
||||
3. ...
|
||||
4. ...
|
||||
|
||||
**预期行为**
|
||||
清晰简单的描述你预期的应用应该表现的行为
|
||||
|
||||
**屏幕截图**
|
||||
如果适用, 上传屏幕截图以帮助描述错误
|
||||
|
||||
**日志**
|
||||
|
||||
如果适用, 上传日志以帮助侦测错误
|
||||
|
||||
**设备信息 (请完成一下信息):**
|
||||
- 机型: [例如: Pixel 4]
|
||||
- 系统/ROM: [例如: MIUI 11]
|
||||
- Android 版本 [例如: Oreo]
|
||||
- ROM版本 [例如: 20.3.19]
|
||||
|
||||
**应用信息**
|
||||
- 版本: [例如: 1.1.10]
|
||||
- 安装包文件名: [例如: app-release-arm64-v8a.apk]
|
||||
- 应用来源: [例如: Google Play]
|
||||
|
||||
**附加信息**
|
||||
其他的可能与改错误相关的信息
|
||||
17
.github/ISSUE_TEMPLATE/04-feature-request-zh-cn.md
vendored
Normal file
17
.github/ISSUE_TEMPLATE/04-feature-request-zh-cn.md
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
---
|
||||
name: "[简体中文] 功能请求"
|
||||
about: 你希望的能够在应用中增加的功能
|
||||
title: "[Feature Request] "
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**功能描述**
|
||||
请清晰的描述你想要的功能
|
||||
|
||||
**描述你希望的实现方式**
|
||||
清晰的描述应用应该如何实现该功能
|
||||
|
||||
**附加信息**
|
||||
其他的与改功能相关的附加信息
|
||||
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,69 +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
|
||||
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
||||
}
|
||||
}
|
||||
compileOptions {
|
||||
sourceCompatibility = 1.8
|
||||
targetCompatibility = 1.8
|
||||
}
|
||||
kotlinOptions {
|
||||
jvmTarget = "1.8"
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
kapt "androidx.room:room-compiler:$gRoomVersion"
|
||||
|
||||
implementation project(":core")
|
||||
implementation project(":service")
|
||||
implementation project(":design")
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$gKotlinVersion"
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$gKotlinCoroutineVersion"
|
||||
implementation "androidx.lifecycle:lifecycle-extensions:$gLifecycleVersion"
|
||||
implementation "androidx.lifecycle:lifecycle-common-java8:$gLifecycleVersion"
|
||||
implementation "androidx.recyclerview:recyclerview:$gRecyclerviewVersion"
|
||||
implementation "androidx.core:core-ktx:$gAndroidKtxVersion"
|
||||
implementation "androidx.appcompat:appcompat:$gAppCompatVersion"
|
||||
implementation "androidx.room:room-runtime:$gRoomVersion"
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-serialization-runtime:$gKotlinSerializationVersion"
|
||||
implementation "com.google.android.material:material:$gMaterialDesignVersion"
|
||||
implementation "moe.shizuku.preference:preference-appcompat:$gShizukuPreferenceVersion"
|
||||
implementation "moe.shizuku.preference:preference-simplemenu-appcompat:$gShizukuPreferenceVersion"
|
||||
implementation "com.microsoft.appcenter:appcenter-analytics:$gAppCenterVersion"
|
||||
implementation "com.microsoft.appcenter:appcenter-crashes:$gAppCenterVersion"
|
||||
}
|
||||
|
||||
task injectAppCenterKey() {
|
||||
doFirst {
|
||||
Properties properties = new Properties()
|
||||
properties.load(rootProject.file('local.properties').newDataInputStream())
|
||||
|
||||
def key = properties.getProperty("appcenter.key", "")
|
||||
|
||||
android.buildTypes.each {
|
||||
it.buildConfigField 'String', 'APP_CENTER_KEY', "\"$key\""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
afterEvaluate {
|
||||
preBuild.dependsOn(injectAppCenterKey)
|
||||
}
|
||||
126
app/build.gradle.kts
Normal file
126
app/build.gradle.kts
Normal file
@@ -0,0 +1,126 @@
|
||||
import java.util.*
|
||||
|
||||
plugins {
|
||||
id("com.android.application")
|
||||
id("kotlin-android")
|
||||
id("kotlin-android-extensions")
|
||||
}
|
||||
|
||||
val rootExtra = rootProject.extra
|
||||
|
||||
val gCompileSdkVersion: Int by rootExtra
|
||||
val gBuildToolsVersion: String by rootExtra
|
||||
|
||||
val gMinSdkVersion: Int by rootExtra
|
||||
val gTargetSdkVersion: Int by rootExtra
|
||||
|
||||
val gVersionCode: Int by rootExtra
|
||||
val gVersionName: String by rootExtra
|
||||
|
||||
val gKotlinVersion: String by rootExtra
|
||||
val gKotlinCoroutineVersion: String by rootExtra
|
||||
val gAppCenterVersion: String by rootExtra
|
||||
val gAndroidKtxVersion: String by rootExtra
|
||||
val gRecyclerviewVersion: String by rootExtra
|
||||
val gAppCompatVersion: String by rootExtra
|
||||
val gMaterialDesignVersion: String by rootExtra
|
||||
val gShizukuPreferenceVersion: String by rootExtra
|
||||
val gMultiprocessPreferenceVersion: String by rootExtra
|
||||
|
||||
android {
|
||||
compileSdkVersion(gCompileSdkVersion)
|
||||
buildToolsVersion(gBuildToolsVersion)
|
||||
|
||||
defaultConfig {
|
||||
applicationId = "com.github.kr328.clash"
|
||||
|
||||
minSdkVersion(gMinSdkVersion)
|
||||
targetSdkVersion(gTargetSdkVersion)
|
||||
|
||||
versionCode = gVersionCode
|
||||
versionName = gVersionName
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
maybeCreate("release").apply {
|
||||
isMinifyEnabled = true
|
||||
isShrinkResources = true
|
||||
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
|
||||
}
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_1_8
|
||||
targetCompatibility = JavaVersion.VERSION_1_8
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = "1.8"
|
||||
}
|
||||
|
||||
splits {
|
||||
abi {
|
||||
isEnable = true
|
||||
isUniversalApk = true
|
||||
}
|
||||
}
|
||||
|
||||
val signingFile = rootProject.file("keystore.properties")
|
||||
if ( signingFile.exists() ) {
|
||||
val properties = Properties().apply {
|
||||
signingFile.inputStream().use {
|
||||
load(it)
|
||||
}
|
||||
}
|
||||
signingConfigs {
|
||||
maybeCreate("release").apply {
|
||||
storeFile = rootProject.file(Objects.requireNonNull(properties.getProperty("storeFile")))
|
||||
storePassword = Objects.requireNonNull(properties.getProperty("storePassword"))
|
||||
keyAlias = Objects.requireNonNull(properties.getProperty("keyAlias"))
|
||||
keyPassword = Objects.requireNonNull(properties.getProperty("keyPassword"))
|
||||
}
|
||||
}
|
||||
buildTypes {
|
||||
maybeCreate("release").apply {
|
||||
this.signingConfig = signingConfigs.findByName("release")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(project(":core"))
|
||||
implementation(project(":service"))
|
||||
implementation(project(":design"))
|
||||
implementation(project(":common"))
|
||||
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8:$gKotlinVersion")
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$gKotlinCoroutineVersion")
|
||||
implementation("androidx.recyclerview:recyclerview:$gRecyclerviewVersion")
|
||||
implementation("androidx.core:core-ktx:$gAndroidKtxVersion")
|
||||
implementation("androidx.appcompat:appcompat:$gAppCompatVersion")
|
||||
implementation("com.google.android.material:material:$gMaterialDesignVersion")
|
||||
implementation("moe.shizuku.preference:preference-appcompat:$gShizukuPreferenceVersion")
|
||||
implementation("moe.shizuku.preference:preference-simplemenu-appcompat:$gShizukuPreferenceVersion")
|
||||
implementation("com.microsoft.appcenter:appcenter-analytics:$gAppCenterVersion")
|
||||
implementation("com.microsoft.appcenter:appcenter-crashes:$gAppCenterVersion")
|
||||
}
|
||||
|
||||
task("injectAppCenterKey") {
|
||||
doFirst {
|
||||
val properties = Properties().apply {
|
||||
rootProject.file("local.properties").inputStream().use {
|
||||
load(it)
|
||||
}
|
||||
}
|
||||
|
||||
val key = properties.getProperty("appcenter.key", "")
|
||||
|
||||
android.buildTypes.forEach {
|
||||
it.buildConfigField("String", "APP_CENTER_KEY", "\"$key\"")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
afterEvaluate {
|
||||
tasks["preBuild"].dependsOn(tasks["injectAppCenterKey"])
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -13,8 +13,10 @@ class ApkBrokenActivity : BaseActivity() {
|
||||
setContentView(R.layout.activity_application_broken)
|
||||
setSupportActionBar(toolbar)
|
||||
|
||||
text.text = Html.fromHtml(getString(R.string.application_broken_description),
|
||||
Html.FROM_HTML_MODE_COMPACT)
|
||||
text.text = Html.fromHtml(
|
||||
getString(R.string.application_broken_description),
|
||||
Html.FROM_HTML_MODE_COMPACT
|
||||
)
|
||||
|
||||
commonUi.build {
|
||||
option(
|
||||
@@ -22,8 +24,10 @@ class ApkBrokenActivity : BaseActivity() {
|
||||
title = getString(R.string.learn_more_about_split_apks)
|
||||
) {
|
||||
onClick {
|
||||
startActivity(Intent(Intent.ACTION_VIEW)
|
||||
.setData(Uri.parse(getString(R.string.about_split_apks_url))))
|
||||
startActivity(
|
||||
Intent(Intent.ACTION_VIEW)
|
||||
.setData(Uri.parse(getString(R.string.about_split_apks_url)))
|
||||
)
|
||||
}
|
||||
}
|
||||
option(
|
||||
@@ -31,8 +35,10 @@ class ApkBrokenActivity : BaseActivity() {
|
||||
title = getString(R.string.reinstall_from_google_play)
|
||||
) {
|
||||
onClick {
|
||||
startActivity(Intent(Intent.ACTION_VIEW)
|
||||
.setData(Uri.parse(getString(R.string.google_play_url))))
|
||||
startActivity(
|
||||
Intent(Intent.ACTION_VIEW)
|
||||
.setData(Uri.parse(getString(R.string.google_play_url)))
|
||||
)
|
||||
}
|
||||
}
|
||||
option(
|
||||
@@ -40,8 +46,10 @@ class ApkBrokenActivity : BaseActivity() {
|
||||
title = getString(R.string.download_from_github_releases)
|
||||
) {
|
||||
onClick {
|
||||
startActivity(Intent(Intent.ACTION_VIEW)
|
||||
.setData(Uri.parse(getString(R.string.github_releases_url))))
|
||||
startActivity(
|
||||
Intent(Intent.ACTION_VIEW)
|
||||
.setData(Uri.parse(getString(R.string.github_releases_url)))
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -57,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()
|
||||
@@ -187,7 +183,7 @@ abstract class BaseActivity : AppCompatActivity(), CoroutineScope by MainScope()
|
||||
}
|
||||
|
||||
private fun resetDarkMode() {
|
||||
when ( uiSettings.get(UiSettings.DARK_MODE).also { darkMode = it } ) {
|
||||
when (uiSettings.get(UiSettings.DARK_MODE).also { darkMode = it }) {
|
||||
UiSettings.DARK_MODE_AUTO ->
|
||||
delegate.localNightMode = AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM
|
||||
UiSettings.DARK_MODE_DARK ->
|
||||
@@ -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,8 +10,8 @@ import androidx.core.net.toFile
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import com.github.kr328.clash.adapter.LiveLogAdapter
|
||||
import com.github.kr328.clash.adapter.LogAdapter
|
||||
import com.github.kr328.clash.common.utils.intent
|
||||
import com.github.kr328.clash.core.event.LogEvent
|
||||
import com.github.kr328.clash.service.util.intent
|
||||
import kotlinx.android.synthetic.main.activity_log_viewer.*
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
@@ -63,14 +63,11 @@ class LogViewerActivity : BaseActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
override val activityLabel: CharSequence
|
||||
get() = getText(R.string.log_viewer)
|
||||
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
|
||||
launch {
|
||||
if ( pauseMutex.isLocked )
|
||||
if (pauseMutex.isLocked)
|
||||
pauseMutex.unlock()
|
||||
}
|
||||
}
|
||||
@@ -99,14 +96,13 @@ class LogViewerActivity : BaseActivity() {
|
||||
.split("\n")
|
||||
.parallelStream()
|
||||
.map { it.trim() }
|
||||
.filter { it.isNotEmpty() && !it.startsWith("#")}
|
||||
.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)
|
||||
} catch (e: Exception) {
|
||||
showSnackbarException(getString(R.string.open_log_failure), e.message)
|
||||
|
||||
throw CancellationException()
|
||||
}
|
||||
@@ -130,7 +126,7 @@ class LogViewerActivity : BaseActivity() {
|
||||
(mainList.adapter as LiveLogAdapter).insertItems(response.logs)
|
||||
|
||||
mainList.apply {
|
||||
if ( computeVerticalScrollOffset() < 30 )
|
||||
if (computeVerticalScrollOffset() < 30)
|
||||
scrollToPosition(0)
|
||||
}
|
||||
|
||||
|
||||
@@ -8,24 +8,25 @@ import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.ServiceConnection
|
||||
import android.os.*
|
||||
import android.os.Binder
|
||||
import android.os.Build
|
||||
import android.os.IBinder
|
||||
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 com.microsoft.appcenter.analytics.Analytics
|
||||
import com.microsoft.appcenter.crashes.Crashes
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.selects.select
|
||||
@@ -39,6 +40,7 @@ class LogcatService : Service(), CoroutineScope by MainScope(), IInterface {
|
||||
private const val NOTIFICATION_CHANNEL_ID = "clash_logcat_channel"
|
||||
private const val NOTIFICATION_ID = 256
|
||||
private const val MAX_CACHE_COUNT = 200
|
||||
private const val LOG_LISTENER_KEY = "logcat_service"
|
||||
|
||||
private const val LOG_CONTENT_FORMAT = "%d %s %s"
|
||||
|
||||
@@ -55,29 +57,23 @@ class LogcatService : Service(), CoroutineScope by MainScope(), IInterface {
|
||||
private val entity = LogFile.generate()
|
||||
|
||||
private val connection = object : ServiceConnection {
|
||||
override fun onServiceDisconnected(name: ComponentName?) {
|
||||
logChannel.offer(LogEvent(LogEvent.Level.ERROR, "Clash Service Crashed"))
|
||||
private var manager: IClashManager? = null
|
||||
|
||||
Crashes.trackError(RemoteException("Clash Service Crashed"))
|
||||
override fun onServiceDisconnected(name: ComponentName?) {
|
||||
manager?.unregisterLogListener(LOG_LISTENER_KEY)
|
||||
}
|
||||
|
||||
override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
|
||||
val manager = IClashManager.Stub.asInterface(service) ?: return stopSelf()
|
||||
|
||||
manager.openLogEvent(object : IStreamCallback.Stub() {
|
||||
override fun complete() {
|
||||
stopSelf()
|
||||
}
|
||||
|
||||
override fun completeExceptionally(reason: String?) {
|
||||
stopSelf()
|
||||
}
|
||||
manager = IClashManager.Stub.asInterface(service) ?: return stopSelf()
|
||||
|
||||
manager?.registerLogListener(LOG_LISTENER_KEY, object : IStreamCallback.Stub() {
|
||||
override fun complete() {}
|
||||
override fun completeExceptionally(reason: String?) {}
|
||||
override fun send(data: ParcelableContainer?) {
|
||||
val logEvent = (data?.data as LogEvent?) ?: return
|
||||
data ?: return
|
||||
data.data ?: return
|
||||
|
||||
if (!logChannel.offer(logEvent))
|
||||
Log.w("Drop log $logEvent")
|
||||
logChannel.offer(data.data as LogEvent)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -103,11 +99,15 @@ class LogcatService : Service(), CoroutineScope by MainScope(), IInterface {
|
||||
|
||||
cancel()
|
||||
|
||||
connection.onServiceDisconnected(null)
|
||||
|
||||
unbindService(connection)
|
||||
|
||||
stopForeground(true)
|
||||
|
||||
super.onDestroy()
|
||||
|
||||
isServiceRunning = false
|
||||
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
override fun onBind(intent: Intent?): IBinder? {
|
||||
@@ -182,8 +182,7 @@ class LogcatService : Service(), CoroutineScope by MainScope(), IInterface {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (e: Exception) {
|
||||
} catch (e: Exception) {
|
||||
return@launch
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,11 +11,11 @@ import androidx.appcompat.app.AlertDialog
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import com.github.kr328.clash.adapter.LogFileAdapter
|
||||
import com.github.kr328.clash.common.utils.intent
|
||||
import com.github.kr328.clash.common.utils.startForegroundServiceCompat
|
||||
import com.github.kr328.clash.design.common.Category
|
||||
import com.github.kr328.clash.design.view.CommonUiLayout
|
||||
import com.github.kr328.clash.model.LogFile
|
||||
import com.github.kr328.clash.service.util.intent
|
||||
import com.github.kr328.clash.service.util.startForegroundServiceCompat
|
||||
import com.github.kr328.clash.utils.format
|
||||
import com.github.kr328.clash.utils.logsDir
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialog
|
||||
@@ -40,7 +40,7 @@ class LogsActivity : BaseActivity() {
|
||||
|
||||
setSupportActionBar(toolbar)
|
||||
|
||||
if ( LogcatService.isServiceRunning ) {
|
||||
if (LogcatService.isServiceRunning) {
|
||||
startActivity(LogViewerActivity::class.intent)
|
||||
finish()
|
||||
return
|
||||
@@ -67,12 +67,16 @@ class LogsActivity : BaseActivity() {
|
||||
showClearAllDialog()
|
||||
}
|
||||
|
||||
val adapter = LogFileAdapter(this@LogsActivity,
|
||||
val adapter = LogFileAdapter(
|
||||
this@LogsActivity,
|
||||
onItemClicked = {
|
||||
startActivity(LogViewerActivity::class.intent
|
||||
.setData(Uri.fromFile(logsDir.resolve(it.fileName))))
|
||||
startActivity(
|
||||
LogViewerActivity::class.intent
|
||||
.setData(Uri.fromFile(logsDir.resolve(it.fileName)))
|
||||
)
|
||||
},
|
||||
onMenuClicked = this::showMenu)
|
||||
onMenuClicked = this::showMenu
|
||||
)
|
||||
val layoutManager = LinearLayoutManager(this@LogsActivity)
|
||||
|
||||
mainList.layoutManager = layoutManager
|
||||
@@ -82,18 +86,15 @@ class LogsActivity : BaseActivity() {
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
|
||||
if ( LogcatService.isServiceRunning )
|
||||
if (LogcatService.isServiceRunning)
|
||||
return
|
||||
|
||||
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 ) {
|
||||
if (requestCode == REQUEST_CODE) {
|
||||
if (resultCode == Activity.RESULT_OK) {
|
||||
val url = data?.data ?: return
|
||||
val file = lastWriteFile ?: return
|
||||
|
||||
@@ -135,8 +136,11 @@ class LogsActivity : BaseActivity() {
|
||||
val old = adapter.fileList
|
||||
|
||||
val result = withContext(Dispatchers.Default) {
|
||||
DiffUtil.calculateDiff(object: DiffUtil.Callback() {
|
||||
override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
|
||||
DiffUtil.calculateDiff(object : DiffUtil.Callback() {
|
||||
override fun areItemsTheSame(
|
||||
oldItemPosition: Int,
|
||||
newItemPosition: Int
|
||||
): Boolean {
|
||||
return old[oldItemPosition].fileName == files[newItemPosition].fileName
|
||||
}
|
||||
|
||||
@@ -179,6 +183,7 @@ class LogsActivity : BaseActivity() {
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT
|
||||
)
|
||||
}
|
||||
|
||||
@ColorInt
|
||||
val errorColor = TypedValue().run {
|
||||
theme.resolveAttribute(R.attr.colorError, this, true)
|
||||
@@ -188,7 +193,8 @@ class LogsActivity : BaseActivity() {
|
||||
menu.build {
|
||||
option(
|
||||
icon = getDrawable(R.drawable.ic_save),
|
||||
title = getString(R.string.export)) {
|
||||
title = getString(R.string.export)
|
||||
) {
|
||||
onClick {
|
||||
export(logFile)
|
||||
|
||||
@@ -197,7 +203,8 @@ class LogsActivity : BaseActivity() {
|
||||
}
|
||||
option(
|
||||
icon = getDrawable(R.drawable.ic_delete_colorful),
|
||||
title = getString(R.string.delete)) {
|
||||
title = getString(R.string.delete)
|
||||
) {
|
||||
textColor = errorColor
|
||||
|
||||
onClick {
|
||||
@@ -224,7 +231,7 @@ class LogsActivity : BaseActivity() {
|
||||
}
|
||||
|
||||
private fun export(file: LogFile) {
|
||||
if ( lastWriteFile != null )
|
||||
if (lastWriteFile != null)
|
||||
return
|
||||
|
||||
val d = Date(file.date)
|
||||
|
||||
@@ -8,15 +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.ClashService
|
||||
import com.github.kr328.clash.service.util.intent
|
||||
import com.github.kr328.clash.service.util.startForegroundServiceCompat
|
||||
import com.github.kr328.clash.utils.startClashService
|
||||
import com.github.kr328.clash.utils.stopClashService
|
||||
import com.github.kr328.clash.service.util.startClashService
|
||||
import com.github.kr328.clash.service.util.stopClashService
|
||||
import kotlinx.android.synthetic.main.activity_main.*
|
||||
import kotlinx.coroutines.*
|
||||
|
||||
@@ -37,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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -79,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)
|
||||
@@ -99,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() {
|
||||
@@ -157,7 +159,7 @@ class MainActivity : BaseActivity() {
|
||||
queryGeneral()
|
||||
}
|
||||
val active = withProfile {
|
||||
queryActiveProfile()
|
||||
queryActive()
|
||||
}
|
||||
|
||||
val modeResId = when (general.mode) {
|
||||
|
||||
@@ -2,12 +2,19 @@ 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
|
||||
import com.microsoft.appcenter.AppCenter
|
||||
import com.microsoft.appcenter.analytics.Analytics
|
||||
import com.microsoft.appcenter.crashes.AbstractCrashesListener
|
||||
import com.microsoft.appcenter.crashes.Crashes
|
||||
import com.microsoft.appcenter.crashes.ingestion.models.ErrorAttachmentLog
|
||||
import com.microsoft.appcenter.crashes.model.ErrorReport
|
||||
|
||||
@Suppress("unused")
|
||||
class MainApplication : Application() {
|
||||
@@ -27,6 +34,35 @@ class MainApplication : Application() {
|
||||
BuildConfig.APP_CENTER_KEY,
|
||||
Analytics::class.java, Crashes::class.java
|
||||
)
|
||||
|
||||
Crashes.setListener(object : AbstractCrashesListener() {
|
||||
override fun getErrorAttachments(report: ErrorReport?): MutableIterable<ErrorAttachmentLog> {
|
||||
report ?: return mutableListOf()
|
||||
|
||||
if (!report.stackTrace.contains("DeadObjectException"))
|
||||
return mutableListOf()
|
||||
|
||||
val logcat = LogcatDumper.dump().joinToString(separator = "\n")
|
||||
|
||||
return mutableListOf(
|
||||
ErrorAttachmentLog.attachmentWithText(logcat, "logcat.txt")
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
@@ -1,21 +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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -59,8 +59,10 @@ class ProxiesActivity : BaseActivity(), ScrollBinding.Callback {
|
||||
|
||||
override fun onStop() {
|
||||
uiSettings.commit {
|
||||
put(UiSettings.PROXY_LAST_SELECT_GROUP,
|
||||
(chipList.adapter!! as ProxyChipAdapter).selected)
|
||||
put(
|
||||
UiSettings.PROXY_LAST_SELECT_GROUP,
|
||||
(chipList.adapter!! as ProxyChipAdapter).selected
|
||||
)
|
||||
}
|
||||
|
||||
super.onStop()
|
||||
@@ -159,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()
|
||||
}
|
||||
@@ -234,7 +233,7 @@ class ProxiesActivity : BaseActivity(), ScrollBinding.Callback {
|
||||
|
||||
private fun refreshList(scrollTop: Boolean = false) {
|
||||
launch {
|
||||
if ( !refreshMutex.tryLock() )
|
||||
if (!refreshMutex.tryLock())
|
||||
return@launch
|
||||
|
||||
val general = withClash {
|
||||
@@ -256,9 +255,9 @@ class ProxiesActivity : BaseActivity(), ScrollBinding.Callback {
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
|
||||
if ( scrollTop )
|
||||
if (scrollTop)
|
||||
mainList.smoothScrollToPosition(0)
|
||||
else if ( scrollToLast ) {
|
||||
else if (scrollToLast) {
|
||||
scrollToLast = false
|
||||
|
||||
val selected = uiSettings.get(UiSettings.PROXY_LAST_SELECT_GROUP)
|
||||
@@ -266,7 +265,7 @@ class ProxiesActivity : BaseActivity(), ScrollBinding.Callback {
|
||||
scrollBinding.scrollMaster(selected)
|
||||
}
|
||||
|
||||
delay(200)
|
||||
delay(500)
|
||||
|
||||
refreshMutex.unlock()
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
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() {
|
||||
class SettingsActivity : BaseActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.activity_settings)
|
||||
@@ -13,7 +13,8 @@ class SettingsActivity: BaseActivity() {
|
||||
commonUi.build {
|
||||
option(
|
||||
icon = getDrawable(R.drawable.ic_settings_applications),
|
||||
title = getString(R.string.behavior)) {
|
||||
title = getString(R.string.behavior)
|
||||
) {
|
||||
paddingHeight = true
|
||||
|
||||
onClick {
|
||||
@@ -22,7 +23,8 @@ class SettingsActivity: BaseActivity() {
|
||||
}
|
||||
option(
|
||||
icon = getDrawable(R.drawable.ic_network),
|
||||
title = getString(R.string.network)) {
|
||||
title = getString(R.string.network)
|
||||
) {
|
||||
paddingHeight = true
|
||||
|
||||
onClick {
|
||||
@@ -31,7 +33,8 @@ class SettingsActivity: BaseActivity() {
|
||||
}
|
||||
option(
|
||||
icon = getDrawable(R.drawable.ic_interface),
|
||||
title = getString(R.string.interface_)) {
|
||||
title = getString(R.string.interface_)
|
||||
) {
|
||||
paddingHeight = true
|
||||
|
||||
onClick {
|
||||
@@ -40,7 +43,4 @@ class SettingsActivity: BaseActivity() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override val activityLabel: CharSequence?
|
||||
get() = getText(R.string.settings)
|
||||
}
|
||||
@@ -3,7 +3,7 @@ package com.github.kr328.clash
|
||||
import android.os.Bundle
|
||||
import com.github.kr328.clash.settings.BehaviorFragment
|
||||
|
||||
class SettingsBehaviorActivity: BaseActivity() {
|
||||
class SettingsBehaviorActivity : BaseActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
@@ -14,7 +14,4 @@ class SettingsBehaviorActivity: BaseActivity() {
|
||||
.replace(R.id.fragment, BehaviorFragment())
|
||||
.commit()
|
||||
}
|
||||
|
||||
override val activityLabel: CharSequence?
|
||||
get() = getText(R.string.behavior)
|
||||
}
|
||||
@@ -3,7 +3,7 @@ package com.github.kr328.clash
|
||||
import android.os.Bundle
|
||||
import com.github.kr328.clash.settings.InterfaceFragment
|
||||
|
||||
class SettingsInterfaceActivity: BaseActivity() {
|
||||
class SettingsInterfaceActivity : BaseActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
@@ -14,7 +14,4 @@ class SettingsInterfaceActivity: BaseActivity() {
|
||||
.replace(R.id.fragment, InterfaceFragment())
|
||||
.commit()
|
||||
}
|
||||
|
||||
override val activityLabel: CharSequence?
|
||||
get() = getText(R.string.interface_)
|
||||
}
|
||||
@@ -15,10 +15,15 @@ class SettingsNetworkActivity : BaseActivity() {
|
||||
.replace(R.id.fragment, NetworkFragment())
|
||||
.commit()
|
||||
|
||||
if ( clashRunning )
|
||||
if (clashRunning)
|
||||
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,18 +1,12 @@
|
||||
package com.github.kr328.clash
|
||||
|
||||
import android.content.ClipData
|
||||
import android.content.ClipboardManager
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import androidx.core.content.getSystemService
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import android.text.Html
|
||||
import kotlinx.android.synthetic.main.activity_support.*
|
||||
|
||||
class SupportActivity: BaseActivity() {
|
||||
override val activityLabel: CharSequence?
|
||||
get() = getText(R.string.support)
|
||||
|
||||
class SupportActivity : BaseActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
@@ -20,14 +14,22 @@ 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(
|
||||
title = getString(R.string.clash),
|
||||
summary = getString(R.string.clash_url)) {
|
||||
summary = getString(R.string.clash_url)
|
||||
) {
|
||||
onClick {
|
||||
startActivity(Intent(Intent.ACTION_VIEW)
|
||||
.setData(Uri.parse(getString(R.string.clash_url))))
|
||||
startActivity(
|
||||
Intent(Intent.ACTION_VIEW)
|
||||
.setData(Uri.parse(getString(R.string.clash_url)))
|
||||
)
|
||||
}
|
||||
}
|
||||
option(
|
||||
@@ -35,44 +37,41 @@ class SupportActivity: BaseActivity() {
|
||||
summary = getString(R.string.clash_for_android_url)
|
||||
) {
|
||||
onClick {
|
||||
startActivity(Intent(Intent.ACTION_VIEW)
|
||||
.setData(Uri.parse(getString(R.string.clash_for_android_url))))
|
||||
startActivity(
|
||||
Intent(Intent.ACTION_VIEW)
|
||||
.setData(Uri.parse(getString(R.string.clash_for_android_url)))
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
category(text = getString(R.string.contacts))
|
||||
category(text = getString(R.string.feedback))
|
||||
|
||||
option(
|
||||
title = getString(R.string.email),
|
||||
summary = getString(R.string.email_url)
|
||||
) {
|
||||
onClick {
|
||||
val data =
|
||||
ClipData.newPlainText("email", getText(R.string.email_url))
|
||||
getSystemService<ClipboardManager>()?.setPrimaryClip(data)
|
||||
|
||||
Snackbar.make(rootView, getText(R.string.copied), Snackbar.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
option(
|
||||
title = getString(R.string.github_issues),
|
||||
summary = getString(R.string.github_issues_url)
|
||||
) {
|
||||
onClick {
|
||||
startActivity(Intent(Intent.ACTION_VIEW)
|
||||
.setData(Uri.parse(getString(R.string.github_issues_url))))
|
||||
startActivity(
|
||||
Intent(Intent.ACTION_VIEW)
|
||||
.setData(Uri.parse(getString(R.string.github_issues_url)))
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
) {
|
||||
onClick {
|
||||
startActivity(Intent(Intent.ACTION_VIEW)
|
||||
.setData(Uri.parse(getString(R.string.telegram_channel_url))))
|
||||
startActivity(
|
||||
Intent(Intent.ACTION_VIEW)
|
||||
.setData(Uri.parse(getString(R.string.telegram_channel_url)))
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 = ""
|
||||
@@ -37,12 +38,12 @@ class TileService : TileService() {
|
||||
if (qsTile == null)
|
||||
return
|
||||
|
||||
qsTile.state = if ( clashRunning )
|
||||
qsTile.state = if (clashRunning)
|
||||
Tile.STATE_ACTIVE
|
||||
else
|
||||
Tile.STATE_INACTIVE
|
||||
|
||||
qsTile.label = if ( currentProfile.isEmpty() )
|
||||
qsTile.label = if (currentProfile.isEmpty())
|
||||
getText(R.string.launch_name)
|
||||
else
|
||||
currentProfile
|
||||
@@ -54,7 +55,7 @@ class TileService : TileService() {
|
||||
|
||||
private val clashStatusReceiver = object : BroadcastReceiver() {
|
||||
override fun onReceive(context: Context?, intent: Intent?) {
|
||||
when ( intent?.action ) {
|
||||
when (intent?.action) {
|
||||
Intents.INTENT_ACTION_CLASH_STARTED -> {
|
||||
clashRunning = true
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -34,14 +34,13 @@ class LiveLogAdapter(private val context: Context) : RecyclerView.Adapter<LogAda
|
||||
}
|
||||
|
||||
fun insertItems(i: List<LogEvent>) {
|
||||
val items = if ( i.size > MAX_LOG_ITEMS ) {
|
||||
val items = if (i.size > MAX_LOG_ITEMS) {
|
||||
i.subList(i.size - MAX_LOG_ITEMS, i.size)
|
||||
}
|
||||
else i
|
||||
} else i
|
||||
|
||||
val predictSize = items.size + circularArray.size()
|
||||
|
||||
if ( predictSize > MAX_LOG_ITEMS ) {
|
||||
if (predictSize > MAX_LOG_ITEMS) {
|
||||
val removeSize = predictSize - MAX_LOG_ITEMS
|
||||
notifyItemRangeRemoved(MAX_LOG_ITEMS - removeSize, removeSize)
|
||||
circularArray.removeFromEnd(removeSize)
|
||||
|
||||
@@ -15,11 +15,15 @@ import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlin.streams.toList
|
||||
|
||||
class PackagesAdapter(private val context: Context,
|
||||
private val apps: List<AppInfo>) :
|
||||
class PackagesAdapter(
|
||||
private val context: Context,
|
||||
private val apps: List<AppInfo>
|
||||
) :
|
||||
RecyclerView.Adapter<PackagesAdapter.Holder>() {
|
||||
data class AppInfo(val packageName: String, val label: String, val icon: Drawable,
|
||||
val installTime: Long, val updateTime: Long, val isSystem: Boolean)
|
||||
data class AppInfo(
|
||||
val packageName: String, val label: String, val icon: Drawable,
|
||||
val installTime: Long, val updateTime: Long, val isSystem: Boolean
|
||||
)
|
||||
|
||||
enum class Sort {
|
||||
NAME, PACKAGE, INSTALL_TIME, UPDATE_TIME
|
||||
@@ -49,7 +53,7 @@ class PackagesAdapter(private val context: Context,
|
||||
val sA = selectedPackages.contains(a.packageName)
|
||||
val sB = selectedPackages.contains(b.packageName)
|
||||
|
||||
if ( sA != sB ) {
|
||||
if (sA != sB) {
|
||||
when {
|
||||
sA -> return@sorted -1
|
||||
sB -> return@sorted 1
|
||||
@@ -115,10 +119,9 @@ class PackagesAdapter(private val context: Context,
|
||||
holder.packageName.text = current.packageName
|
||||
holder.checkbox.isChecked = selectedPackages.contains(current.packageName)
|
||||
holder.root.setOnClickListener {
|
||||
if ( selectedPackages.contains(current.packageName) ) {
|
||||
if (selectedPackages.contains(current.packageName)) {
|
||||
selectedPackages.remove(current.packageName)
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
selectedPackages.add(current.packageName)
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
@@ -132,6 +132,6 @@ class ProfileAdapter(private val context: Context, private val callback: Callbac
|
||||
}
|
||||
|
||||
private fun offsetDate(date: Long): CharSequence {
|
||||
return IntervalUtils.intervalString(context,System.currentTimeMillis() - date)
|
||||
return IntervalUtils.intervalString(context, System.currentTimeMillis() - date)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -141,7 +143,7 @@ class ProxyAdapter(
|
||||
groupCache[it.name] = index
|
||||
is ProxyRenderInfo -> {
|
||||
if (it.info.active)
|
||||
activeCache[it.name] = index
|
||||
activeCache[it.group] = index
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -194,8 +196,10 @@ class ProxyAdapter(
|
||||
is ProxyGroupHeader -> {
|
||||
val current = renderList[position] as ProxyGroupRenderInfo
|
||||
|
||||
holder.title.text = context.getString(R.string.format_proxy_group_title,
|
||||
current.info.name, current.info.current)
|
||||
holder.title.text = context.getString(
|
||||
R.string.format_proxy_group_title,
|
||||
current.info.name, current.info.current
|
||||
)
|
||||
holder.urlTest.setOnClickListener {
|
||||
holder.urlTest.visibility = View.GONE
|
||||
holder.urlTestProgress.visibility = View.VISIBLE
|
||||
@@ -237,14 +241,16 @@ class ProxyAdapter(
|
||||
if (current.info.selectable) {
|
||||
holder.root.setOnClickListener {
|
||||
val oldPosition = activeList[current.group] ?: return@setOnClickListener
|
||||
val groupPosition = groupPosition[current.group] ?: return@setOnClickListener
|
||||
val groupPosition =
|
||||
groupPosition[current.group] ?: return@setOnClickListener
|
||||
val old = renderList[oldPosition] as ProxyRenderInfo
|
||||
val new = renderList[position] as ProxyRenderInfo
|
||||
val group = renderList[groupPosition] as ProxyGroupRenderInfo
|
||||
|
||||
renderList[oldPosition] = old.copy(info = old.info.copy(active = false))
|
||||
renderList[position] = new.copy(info = new.info.copy(active = true))
|
||||
renderList[groupPosition] = group.copy(info = group.info.copy(current = current.name))
|
||||
renderList[groupPosition] =
|
||||
group.copy(info = group.info.copy(current = current.name))
|
||||
|
||||
activeList[current.group] = position
|
||||
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
package com.github.kr328.clash.dump
|
||||
|
||||
object LogcatDumper {
|
||||
fun dump(): List<String> {
|
||||
return try {
|
||||
val process =
|
||||
Runtime.getRuntime().exec(arrayOf("logcat", "-d", "-s", "-v", "raw", "Go"))
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
process.waitFor()
|
||||
|
||||
result
|
||||
} catch (e: Exception) {
|
||||
emptyList()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,206 @@
|
||||
package com.github.kr328.clash.fragment
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.text.Html
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.ViewGroup.LayoutParams
|
||||
import android.webkit.MimeTypeMap
|
||||
import android.webkit.URLUtil
|
||||
import androidx.fragment.app.Fragment
|
||||
import com.github.kr328.clash.Constants
|
||||
import com.github.kr328.clash.R
|
||||
import com.github.kr328.clash.design.common.TextInput
|
||||
import com.github.kr328.clash.design.view.CommonUiLayout
|
||||
import com.github.kr328.clash.service.model.Profile.Type
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
|
||||
class ProfileEditFragment(
|
||||
val id: Long,
|
||||
var name: String,
|
||||
var uri: Uri,
|
||||
var interval: Long,
|
||||
private val type: Type,
|
||||
private val source: String?
|
||||
) : Fragment() {
|
||||
var isModified = false
|
||||
|
||||
companion object {
|
||||
private const val REQUEST_CODE = 10000
|
||||
|
||||
private const val KEY_NAME = "name"
|
||||
private const val KEY_URL = "url"
|
||||
private const val KEY_AUTO_UPDATE = "auto_update"
|
||||
|
||||
private val TYPE_YAML = MimeTypeMap.getSingleton()
|
||||
.getMimeTypeFromExtension("yaml") ?: "*/*"
|
||||
}
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? {
|
||||
return CommonUiLayout(requireContext()).apply {
|
||||
layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)
|
||||
|
||||
build {
|
||||
tips(icon = requireContext().getDrawable(R.drawable.ic_info)) {
|
||||
title =
|
||||
Html.fromHtml(getString(R.string.tips_profile), Html.FROM_HTML_MODE_LEGACY)
|
||||
}
|
||||
|
||||
textInput(
|
||||
title = getString(R.string.name),
|
||||
icon = requireContext().getDrawable(R.drawable.ic_label_outline),
|
||||
hint = getString(R.string.profile_name),
|
||||
id = KEY_NAME,
|
||||
content = name
|
||||
) {
|
||||
onTextChanged {
|
||||
name = content.toString()
|
||||
isModified = true
|
||||
}
|
||||
}
|
||||
textInput(
|
||||
title = getString(R.string.url),
|
||||
icon = requireContext().getDrawable(R.drawable.ic_content),
|
||||
hint = getString(R.string.profile_url),
|
||||
id = KEY_URL,
|
||||
content = uri.toString()
|
||||
) {
|
||||
onOpenInput {
|
||||
if (!openUrlProvider())
|
||||
openDialogInput()
|
||||
}
|
||||
onDisplayContent {
|
||||
it.split("/").last()
|
||||
}
|
||||
onTextChanged {
|
||||
if (!URLUtil.isValidUrl(content.toString())) {
|
||||
content = ""
|
||||
Snackbar.make(view, R.string.invalid_url, Snackbar.LENGTH_LONG).show()
|
||||
return@onTextChanged
|
||||
}
|
||||
|
||||
uri = Uri.parse(content.toString())
|
||||
isModified = true
|
||||
}
|
||||
}
|
||||
textInput(
|
||||
title = getString(R.string.auto_update),
|
||||
icon = requireContext().getDrawable(R.drawable.ic_update),
|
||||
hint = getString(R.string.more_than_15_minutes),
|
||||
id = KEY_AUTO_UPDATE,
|
||||
content = (interval / 1000 / 60).toStringIfNonZero()
|
||||
) {
|
||||
onDisplayContent {
|
||||
val interval = it.toString().toIntOrNull() ?: 0
|
||||
|
||||
if (interval <= 0)
|
||||
getString(R.string.disabled)
|
||||
else
|
||||
getString(R.string.format_minutes, interval)
|
||||
}
|
||||
onTextChanged {
|
||||
val s = it.toString()
|
||||
|
||||
if (s.isBlank()) {
|
||||
content = ""
|
||||
return@onTextChanged
|
||||
}
|
||||
|
||||
val value = s.toIntOrNull()
|
||||
if (value == null || value < 15) {
|
||||
content = ""
|
||||
Snackbar.make(view, R.string.invalid_interval, Snackbar.LENGTH_LONG)
|
||||
.show()
|
||||
return@onTextChanged
|
||||
}
|
||||
|
||||
interval = content.toString().toLong() * 1000 * 60
|
||||
isModified = true
|
||||
}
|
||||
|
||||
if ( type == Type.FILE )
|
||||
isHidden = true
|
||||
}
|
||||
|
||||
screen.restoreState(savedInstanceState)
|
||||
|
||||
if (type == Type.EXTERNAL)
|
||||
openUrlProvider()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
if (requestCode == REQUEST_CODE) {
|
||||
if (resultCode != Activity.RESULT_OK || data == null)
|
||||
return
|
||||
|
||||
val layout = view as CommonUiLayout
|
||||
|
||||
data.data?.apply {
|
||||
layout.screen.requireElement<TextInput>(KEY_URL).content = this.toString()
|
||||
}
|
||||
|
||||
data.getStringExtra(Constants.URL_PROVIDER_INTENT_EXTRA_NAME)?.also {
|
||||
layout.screen.requireElement<TextInput>(KEY_NAME).apply {
|
||||
if (content.isBlank())
|
||||
content = it
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
super.onActivityResult(requestCode, resultCode, data)
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
super.onSaveInstanceState(outState)
|
||||
|
||||
(view as CommonUiLayout?)?.screen?.saveState(outState)
|
||||
}
|
||||
|
||||
private fun openUrlProvider(): Boolean {
|
||||
try {
|
||||
when (type) {
|
||||
Type.FILE ->
|
||||
startActivityForResult(
|
||||
Intent(Intent.ACTION_GET_CONTENT).setType(TYPE_YAML),
|
||||
REQUEST_CODE
|
||||
)
|
||||
Type.EXTERNAL ->
|
||||
startActivityForResult(
|
||||
source?.toIntent() ?: return false,
|
||||
REQUEST_CODE
|
||||
)
|
||||
else -> return false
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Snackbar.make(
|
||||
view as ViewGroup,
|
||||
R.string.start_url_provider_failure,
|
||||
Snackbar.LENGTH_LONG
|
||||
).show()
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
private fun Long.toStringIfNonZero(): String {
|
||||
return if ( this == 0L ) "" else this.toString()
|
||||
}
|
||||
|
||||
private fun String.toIntent(): Intent? {
|
||||
return try {
|
||||
Intent.parseUri(this, 0)
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
@@ -16,7 +16,7 @@ data class ProxyEntry(val group: String, val name: String)
|
||||
data class ProxyMerged(val prefix: String, val content: String)
|
||||
|
||||
suspend fun Pipeline<List<ProxyGroup>>.mergePrefix(): Pipeline<Map<ProxyEntry, ProxyMerged>> {
|
||||
if ( !settings.get(UiSettings.PROXY_MERGE_PREFIX) )
|
||||
if (!settings.get(UiSettings.PROXY_MERGE_PREFIX))
|
||||
return Pipeline(emptyMap(), settings)
|
||||
|
||||
val result = coroutineScope {
|
||||
@@ -31,7 +31,10 @@ suspend fun Pipeline<List<ProxyGroup>>.mergePrefix(): Pipeline<Map<ProxyEntry, P
|
||||
}
|
||||
.flatMap {
|
||||
it.second.map { merged ->
|
||||
ProxyEntry(it.first, merged.value.name) to ProxyMerged(merged.prefix, merged.content)
|
||||
ProxyEntry(it.first, merged.value.name) to ProxyMerged(
|
||||
merged.prefix,
|
||||
merged.content
|
||||
)
|
||||
}
|
||||
}
|
||||
.toMap()
|
||||
@@ -66,27 +69,31 @@ suspend fun Pipeline<List<ProxyGroup>>.sort(): Pipeline<List<ProxyGroup>> {
|
||||
return copy(input = sorter.sort(input))
|
||||
}
|
||||
|
||||
suspend fun Pipeline<List<ProxyGroup>>.toAdapterElement(prefixMerged: Map<ProxyEntry, ProxyMerged>, general: General):
|
||||
List<ProxyAdapter.ProxyGroupInfo> {
|
||||
suspend fun Pipeline<List<ProxyGroup>>.toAdapterElement(
|
||||
prefixMerged: Map<ProxyEntry, ProxyMerged>,
|
||||
general: General
|
||||
): List<ProxyAdapter.ProxyGroupInfo> {
|
||||
return input.map { group ->
|
||||
val proxies = group.proxies.map { proxy ->
|
||||
val merged = prefixMerged[ProxyEntry(group.name, proxy.name)]?.takeIf {
|
||||
it.prefix.isNotBlank() && it.content.isNotBlank()
|
||||
} ?: ProxyMerged(proxy.type.toString(), proxy.name)
|
||||
|
||||
ProxyAdapter.ProxyInfo(proxy.name, group.name, merged.content, merged.prefix,
|
||||
ProxyAdapter.ProxyInfo(
|
||||
proxy.name, group.name, merged.content, merged.prefix,
|
||||
proxy.delay.toShort(), group.type == Proxy.Type.SELECT,
|
||||
group.current == proxy.name)
|
||||
group.current == proxy.name
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
ProxyAdapter.ProxyGroupInfo(group.name, group.current, proxies)
|
||||
}.let {
|
||||
withContext(Dispatchers.Default) {
|
||||
when ( general.mode ) {
|
||||
when (general.mode) {
|
||||
General.Mode.DIRECT -> emptyList()
|
||||
General.Mode.GLOBAL -> it
|
||||
General.Mode.RULE -> it.filter { it.name != "GLOBAL"}
|
||||
General.Mode.RULE -> it.filter { it.name != "GLOBAL" }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
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):
|
||||
class UiSettings(context: Context) :
|
||||
BaseSettings(context.getSharedPreferences(FILE_NAME, Context.MODE_PRIVATE)) {
|
||||
companion object {
|
||||
private const val FILE_NAME = "ui"
|
||||
@@ -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,15 +4,15 @@ 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
|
||||
|
||||
class ClashClient(val service: IClashManager) {
|
||||
suspend fun setSelectProxy(name: String, proxy: String): Boolean = withContext(Dispatchers.IO) {
|
||||
return@withContext service.setSelectProxy(name, proxy)
|
||||
service.setSelectProxy(name, proxy)
|
||||
}
|
||||
|
||||
suspend fun startHealthCheck(group: String) = withContext(Dispatchers.IO) {
|
||||
@@ -39,10 +39,9 @@ class ClashClient(val service: IClashManager) {
|
||||
service.queryGeneral()
|
||||
}
|
||||
|
||||
suspend fun queryBandwidth(): Long =
|
||||
withContext(Dispatchers.IO) {
|
||||
service.queryBandwidth()
|
||||
}
|
||||
suspend fun queryBandwidth(): Long = withContext(Dispatchers.IO) {
|
||||
service.queryBandwidth()
|
||||
}
|
||||
|
||||
suspend fun setProxyMode(mode: General.Mode) = withContext(Dispatchers.IO) {
|
||||
service.setProxyMode(mode.toString())
|
||||
|
||||
@@ -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,21 +5,24 @@ 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 androidx.core.content.edit
|
||||
import androidx.lifecycle.DefaultLifecycleObserver
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.lifecycle.ProcessLifecycleOwner
|
||||
import com.github.kr328.clash.ApkBrokenActivity
|
||||
import com.github.kr328.clash.Constants
|
||||
import com.github.kr328.clash.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 kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import java.io.File
|
||||
import java.util.zip.ZipFile
|
||||
|
||||
object Remote {
|
||||
@@ -43,9 +46,7 @@ object Remote {
|
||||
if (service != null)
|
||||
instance = ClashClient(IClashManager.Stub.asInterface(service))
|
||||
|
||||
service?.linkToDeath({
|
||||
onServiceDisconnected(null)
|
||||
}, 0)
|
||||
service?.linkToDeath({ onServiceDisconnected(null) }, 0)
|
||||
|
||||
sender = GlobalScope.launch {
|
||||
while (isActive) {
|
||||
@@ -70,9 +71,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) {
|
||||
@@ -83,58 +82,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
|
||||
@@ -142,21 +150,36 @@ 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 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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,40 +1,55 @@
|
||||
package com.github.kr328.clash.remote
|
||||
|
||||
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
|
||||
|
||||
object RemoteUtils {
|
||||
fun detectClashRunning(context: Context): Boolean {
|
||||
val authority = Uri.Builder()
|
||||
.scheme("content")
|
||||
.authority("${context.packageName}${Constants.STATUS_PROVIDER_SUFFIX}")
|
||||
.build()
|
||||
try {
|
||||
val authority = Uri.Builder()
|
||||
.scheme("content")
|
||||
.authority("${context.packageName}${Constants.STATUS_PROVIDER_SUFFIX}")
|
||||
.build()
|
||||
|
||||
val pong = context.contentResolver.call(
|
||||
authority,
|
||||
ServiceStatusProvider.METHOD_PING_CLASH_SERVICE,
|
||||
null,
|
||||
null
|
||||
)
|
||||
val pong = context.contentResolver.call(
|
||||
authority,
|
||||
ServiceStatusProvider.METHOD_PING_CLASH_SERVICE,
|
||||
null,
|
||||
null
|
||||
)
|
||||
|
||||
return pong != null
|
||||
return pong != null
|
||||
} catch (e: IllegalArgumentException) {
|
||||
context.startActivity(ApkBrokenActivity::class.intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK))
|
||||
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
fun getCurrentClashProfileName(context: Context): String? {
|
||||
val authority = Uri.Builder()
|
||||
.scheme("content")
|
||||
.authority("${context.packageName}${Constants.STATUS_PROVIDER_SUFFIX}")
|
||||
.build()
|
||||
try {
|
||||
val authority = Uri.Builder()
|
||||
.scheme("content")
|
||||
.authority("${context.packageName}${Constants.STATUS_PROVIDER_SUFFIX}")
|
||||
.build()
|
||||
|
||||
val pong = context.contentResolver.call(
|
||||
authority,
|
||||
ServiceStatusProvider.METHOD_PING_CLASH_SERVICE,
|
||||
null,
|
||||
null
|
||||
)
|
||||
val pong = context.contentResolver.call(
|
||||
authority,
|
||||
ServiceStatusProvider.METHOD_PING_CLASH_SERVICE,
|
||||
null,
|
||||
null
|
||||
)
|
||||
|
||||
return pong?.getString("name")
|
||||
return pong?.getString("name")
|
||||
} catch (e: IllegalArgumentException) {
|
||||
context.startActivity(ApkBrokenActivity::class.intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK))
|
||||
|
||||
return null
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,7 +8,7 @@ import com.github.kr328.clash.preference.UiSettings
|
||||
import com.github.kr328.clash.service.settings.ServiceSettings
|
||||
import moe.shizuku.preference.PreferenceFragment
|
||||
|
||||
abstract class BaseSettingFragment: PreferenceFragment() {
|
||||
abstract class BaseSettingFragment : PreferenceFragment() {
|
||||
abstract fun onCreateDataStore(): SettingsDataStore
|
||||
abstract val xmlResourceId: Int
|
||||
|
||||
|
||||
@@ -2,13 +2,13 @@ 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() {
|
||||
class BehaviorFragment : BaseSettingFragment() {
|
||||
companion object {
|
||||
private const val KEY_START_ON_BOOT = "start_on_boot"
|
||||
private const val KEY_SHOW_TRAFFIC = "show_traffic"
|
||||
@@ -30,24 +30,25 @@ class BehaviorFragment: BaseSettingFragment() {
|
||||
override val xmlResourceId: Int
|
||||
get() = R.xml.settings_behavior
|
||||
|
||||
private inner class StartOnBootSource: SettingsDataStore.Source {
|
||||
private inner class StartOnBootSource : SettingsDataStore.Source {
|
||||
override fun set(value: Any?) {
|
||||
val v = value as Boolean? ?: return
|
||||
|
||||
val status = if ( v )
|
||||
val status = if (v)
|
||||
PackageManager.COMPONENT_ENABLED_STATE_ENABLED
|
||||
else
|
||||
PackageManager.COMPONENT_ENABLED_STATE_DISABLED
|
||||
|
||||
requireActivity().packageManager.setComponentEnabledSetting(
|
||||
OnBootReceiver::class.componentName,
|
||||
RestartReceiver::class.componentName,
|
||||
status,
|
||||
PackageManager.DONT_KILL_APP)
|
||||
PackageManager.DONT_KILL_APP
|
||||
)
|
||||
}
|
||||
|
||||
override fun get(): Any? {
|
||||
val status = requireActivity().packageManager
|
||||
.getComponentEnabledSetting(OnBootReceiver::class.componentName)
|
||||
.getComponentEnabledSetting(RestartReceiver::class.componentName)
|
||||
|
||||
return status == PackageManager.COMPONENT_ENABLED_STATE_ENABLED
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import com.github.kr328.clash.R
|
||||
import com.github.kr328.clash.preference.UiSettings
|
||||
import com.github.kr328.clash.service.settings.ServiceSettings
|
||||
|
||||
class InterfaceFragment: BaseSettingFragment() {
|
||||
class InterfaceFragment : BaseSettingFragment() {
|
||||
companion object {
|
||||
private const val KEY_DARK_MODE = "dark_mode"
|
||||
private const val KEY_LANGUAGE = "language"
|
||||
|
||||
@@ -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() {
|
||||
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,9 +1,9 @@
|
||||
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() {
|
||||
class SettingsDataStore : PreferenceDataStore() {
|
||||
interface Source {
|
||||
fun set(value: Any?)
|
||||
fun get(): Any?
|
||||
@@ -20,8 +20,8 @@ class SettingsDataStore: PreferenceDataStore() {
|
||||
this.applyListener = block
|
||||
}
|
||||
|
||||
inline fun <reified T>BaseSettings.Entry<T>.asSource(settings: BaseSettings): Source {
|
||||
return object: Source {
|
||||
inline fun <reified T> BaseSettings.Entry<T>.asSource(settings: BaseSettings): Source {
|
||||
return object : Source {
|
||||
override fun set(value: Any?) {
|
||||
val v = value ?: throw NullPointerException()
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -8,7 +8,12 @@ const val DATE_DATE_ONLY = "yyyy-MM-dd"
|
||||
const val DATE_TIME_ONLY = "HH:mm:ss"
|
||||
const val DATE_ALL = "$DATE_DATE_ONLY $DATE_TIME_ONLY"
|
||||
|
||||
fun Date.format(context: Context, includeDate: Boolean = true, includeTime: Boolean = true, custom: String = ""): String {
|
||||
fun Date.format(
|
||||
context: Context,
|
||||
includeDate: Boolean = true,
|
||||
includeTime: Boolean = true,
|
||||
custom: String = ""
|
||||
): String {
|
||||
val locale = context.resources.configuration.locales[0]
|
||||
|
||||
return when {
|
||||
|
||||
@@ -20,7 +20,7 @@ object PrefixMerger {
|
||||
val result = mutableListOf<Result<T>>()
|
||||
|
||||
for (pair in pairs) {
|
||||
if ( pair.first.isEmpty() )
|
||||
if (pair.first.isEmpty())
|
||||
continue
|
||||
|
||||
if (pair.first[0] == currentCodePoint) {
|
||||
@@ -56,8 +56,12 @@ object PrefixMerger {
|
||||
val prefix = it.first.subList(0, diffIndex)
|
||||
val content = it.first.subList(diffIndex, it.first.size)
|
||||
|
||||
result.add(Result(prefix.asCodePointString().replace(REGEX_PREFIX_TRIM, ""),
|
||||
content.asCodePointString(), it.second))
|
||||
result.add(
|
||||
Result(
|
||||
prefix.asCodePointString().replace(REGEX_PREFIX_TRIM, ""),
|
||||
content.asCodePointString(), it.second
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ class ProxySorter(private val groupOrder: Order, private val proxyOrder: Order)
|
||||
suspend fun sort(proxyGroup: List<ProxyGroup>): List<ProxyGroup> =
|
||||
withContext(Dispatchers.Default) {
|
||||
val groups = proxyGroup.groupBy {
|
||||
if ( it.name == "GLOBAL" )
|
||||
if (it.name == "GLOBAL")
|
||||
"GLOBAL"
|
||||
else
|
||||
"OTHER"
|
||||
@@ -30,7 +30,7 @@ class ProxySorter(private val groupOrder: Order, private val proxyOrder: Order)
|
||||
Order.NAME_DECREASE -> groupSortWithName(false, other)
|
||||
}
|
||||
|
||||
val sorted = if ( global == null )
|
||||
val sorted = if (global == null)
|
||||
sortedGroup
|
||||
else
|
||||
listOf(global) + sortedGroup
|
||||
|
||||
@@ -6,7 +6,7 @@ import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.LinearSmoothScroller
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
|
||||
class QuickSmoothScroller(context: Context, target: Int):
|
||||
class QuickSmoothScroller(context: Context, target: Int) :
|
||||
LinearSmoothScroller(context) {
|
||||
companion object {
|
||||
const val MAX_OFFSET = 2
|
||||
|
||||
@@ -1,33 +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))
|
||||
}
|
||||
@@ -4,7 +4,7 @@ fun String.toCodePointList(): List<Int> {
|
||||
var offset = 0
|
||||
val result = mutableListOf<Int>()
|
||||
|
||||
while ( offset < length ) {
|
||||
while (offset < length) {
|
||||
val codePoint = codePointAt(offset)
|
||||
result.add(codePoint)
|
||||
|
||||
|
||||
@@ -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,20 @@
|
||||
<string name="reverse">反转</string>
|
||||
<string name="clash">Clash</string>
|
||||
<string name="clash_for_android">Clash for Android</string>
|
||||
<string name="contacts">联系我们</string>
|
||||
<string name="email">E-Mail</string>
|
||||
<string name="feedback">反馈</string>
|
||||
<string name="sources">源代码</string>
|
||||
<string name="telegram_channel">Telegram 频道</string>
|
||||
<string name="github_issues">Github Issues</string>
|
||||
<string name="format_seconds">%d 秒</string>
|
||||
<string name="copied">已复制</string>
|
||||
<string name="tips_profile"><![CDATA[仅接受 <strong>Clash 配置文件</strong> <br />其中包含了 <strong>代理, 代理组 和 规则</strong>]]></string>
|
||||
<string name="application_broken_description"><![CDATA[这通常意味着您使用来自 <strong>Google Play</strong> 的副本, 但是生成该副本的应用未能正确处理 <strong>分包机制</strong><br />这意味着您获取的是 <strong>应用的一部分</strong>]]></string>
|
||||
<string name="learn_more_about_split_apks">了解更多关于分包机制</string>
|
||||
<string name="reinstall_from_google_play">重新从 Google Play 安装</string>
|
||||
<string name="download_from_github_releases">从 Github Release 下载</string>
|
||||
<string name="missing_vpn_component">系统 VPN 组件缺失</string>
|
||||
<string name="profile_not_found">配置文件丢失</string>
|
||||
<string name="more_than_15_minutes">大于15分钟</string>
|
||||
<string name="loading">载入中</string>
|
||||
<string name="tips_support"><![CDATA[Clash for Android 是一个<strong>免费开源</strong>的项目<br /> 我们<strong>不提供</strong>任何代理服务<br />请务必<strong>不要</strong>反馈非应用自身引起的问题]]></string>
|
||||
<string name="donate">捐赠</string>
|
||||
<string name="clone_profile">复制配置</string>
|
||||
</resources>
|
||||
@@ -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 slash 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,7 @@
|
||||
<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>
|
||||
</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 = 10103
|
||||
gVersionName = "1.1.3"
|
||||
|
||||
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-alpha04"
|
||||
gShizukuPreferenceVersion = "4.2.0"
|
||||
gMultiprocessPreferenceVersion = "1.0.0"
|
||||
}
|
||||
repositories {
|
||||
google()
|
||||
jcenter()
|
||||
}
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:3.5.3'
|
||||
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.71"
|
||||
|
||||
rootProject.extra.apply {
|
||||
this["gBuildToolsVersion"] = "29.0.3"
|
||||
|
||||
this["gCompileSdkVersion"] = 29
|
||||
this["gMinSdkVersion"] = 24
|
||||
this["gTargetSdkVersion"] = 29
|
||||
|
||||
this["gVersionCode"] = 10202
|
||||
this["gVersionName"] = "1.2.2"
|
||||
|
||||
this["gKotlinVersion"] = kotlinVersion
|
||||
this["gKotlinCoroutineVersion"] = "1.3.5"
|
||||
this["gKotlinSerializationVersion"] = "0.20.0"
|
||||
this["gRoomVersion"] = "2.2.5"
|
||||
this["gAppCenterVersion"] = "2.5.1"
|
||||
this["gAndroidKtxVersion"] = "1.2.0"
|
||||
this["gRecyclerviewVersion"] = "1.1.0"
|
||||
this["gAppCompatVersion"] = "1.1.0"
|
||||
this["gMaterialDesignVersion"] = "1.1.0"
|
||||
this["gShizukuPreferenceVersion"] = "4.2.0"
|
||||
this["gMultiprocessPreferenceVersion"] = "1.0.0"
|
||||
}
|
||||
repositories {
|
||||
google()
|
||||
jcenter()
|
||||
}
|
||||
dependencies {
|
||||
classpath("com.android.tools.build:gradle:4.0.0-beta04")
|
||||
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion")
|
||||
classpath("org.jetbrains.kotlin:kotlin-serialization:$kotlinVersion")
|
||||
}
|
||||
}
|
||||
|
||||
allprojects {
|
||||
repositories {
|
||||
google()
|
||||
jcenter()
|
||||
|
||||
maven {
|
||||
url = java.net.URI("https://dl.bintray.com/rikkaw/Libraries")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
task("clean", type = Delete::class) {
|
||||
delete(rootProject.buildDir)
|
||||
}
|
||||
@@ -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\" 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)!!
|
||||
}
|
||||
@@ -18,7 +19,8 @@ abstract class BaseSettings(private val preferences: SharedPreferences) {
|
||||
}
|
||||
}
|
||||
|
||||
class BooleanEntry(private val key: String, private val defaultValue: Boolean): Entry<Boolean> {
|
||||
class BooleanEntry(private val key: String, private val defaultValue: Boolean) :
|
||||
Entry<Boolean> {
|
||||
override fun get(preferences: SharedPreferences): Boolean {
|
||||
return preferences.getBoolean(key, defaultValue)
|
||||
}
|
||||
@@ -28,7 +30,8 @@ abstract class BaseSettings(private val preferences: SharedPreferences) {
|
||||
}
|
||||
}
|
||||
|
||||
class StringSetEntry(private val key: String, private val defaultValue: Set<String>): Entry<Set<String>> {
|
||||
class StringSetEntry(private val key: String, private val defaultValue: Set<String>) :
|
||||
Entry<Set<String>> {
|
||||
override fun get(preferences: SharedPreferences): Set<String> {
|
||||
return preferences.getStringSet(key, defaultValue)!!
|
||||
}
|
||||
@@ -39,12 +42,12 @@ abstract class BaseSettings(private val preferences: SharedPreferences) {
|
||||
}
|
||||
|
||||
class Editor(private val editor: SharedPreferences.Editor) {
|
||||
fun <T>put(entry: Entry<T>, value: T) {
|
||||
fun <T> put(entry: Entry<T>, value: T) {
|
||||
entry.put(editor, value)
|
||||
}
|
||||
}
|
||||
|
||||
fun <T>get(entry: Entry<T>): T {
|
||||
fun <T> get(entry: Entry<T>): T {
|
||||
return entry.get(preferences)
|
||||
}
|
||||
|
||||
@@ -53,7 +56,7 @@ abstract class BaseSettings(private val preferences: SharedPreferences) {
|
||||
|
||||
Editor(editor).apply(block)
|
||||
|
||||
if ( async )
|
||||
if (async)
|
||||
editor.apply()
|
||||
else
|
||||
editor.commit()
|
||||
@@ -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,16 +1,16 @@
|
||||
package com.github.kr328.clash.service.util
|
||||
package com.github.kr328.clash.common.utils
|
||||
|
||||
import android.content.Context
|
||||
import android.content.res.Configuration
|
||||
import java.util.*
|
||||
|
||||
fun Context.createLanguageConfigurationContext(language: String): Context {
|
||||
if ( language.isBlank() ) {
|
||||
if (language.isBlank()) {
|
||||
return this
|
||||
}
|
||||
|
||||
val split = language.split("-")
|
||||
val locale = if ( split.size == 1 )
|
||||
val locale = if (split.size == 1)
|
||||
Locale(split[0])
|
||||
else
|
||||
Locale(split[0], split[1])
|
||||
@@ -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["resetGolangMode"])
|
||||
tasks["preBuild"].dependsOn(tasks["extractSources"], tasks["downloadGeoipDatabase"])
|
||||
}
|
||||
222
core/clash.gradle.kts
Normal file
222
core/clash.gradle.kts
Normal file
@@ -0,0 +1,222 @@
|
||||
import org.apache.tools.ant.taskdefs.condition.Os
|
||||
import java.io.*
|
||||
import java.util.*
|
||||
import java.net.*
|
||||
|
||||
object Constants {
|
||||
const val GEOIP_DATABASE_URL = "https://github.com/Dreamacro/maxmind-geoip/releases/latest/download/Country.mmdb"
|
||||
const val GEOIP_INVALID_INTERVAL = 1000L * 60 * 60 * 24 * 7
|
||||
|
||||
const val SOURCE_PATH = "src/main/golang"
|
||||
const val OUTPUT_PATH = "extraSources"
|
||||
|
||||
const val GOLANG_BASE = "intermediates/golang"
|
||||
const val GOLANG_PATH = "$GOLANG_BASE/path"
|
||||
const val GOLANG_BIND = "$GOLANG_BASE/bind"
|
||||
const val GOLANG_BINARY = "$GOLANG_PATH/bin"
|
||||
const val GOLANG_OUTPUT = "$GOLANG_BASE/bridge.aar"
|
||||
const val GOLANG_OUTPUT_SOURCES = "$GOLANG_BASE/bridge-sources.jar"
|
||||
|
||||
val STUB_GO_FILE_CONTENT = """
|
||||
package main
|
||||
|
||||
import "github.com/kr328/cfa/bridge"
|
||||
|
||||
func main() {}
|
||||
""".trimIndent()
|
||||
val STUB_GO_MOD_CONTENT = """
|
||||
module github.com/kr328/cfa-bind
|
||||
|
||||
require github.com/kr328/cfa v0.0.0 // redirect
|
||||
|
||||
replace github.com/kr328/cfa => {SOURCE_PATH}
|
||||
|
||||
""".trimIndent()
|
||||
|
||||
val REGEX_REPLACE = Regex("replace\\s+(\\S+)\\s+(\\S*)\\s*=>\\s*(\\S+)\\s*(\\S*)\\s*")
|
||||
val REGEX_JNI = Regex("^jni/")
|
||||
}
|
||||
|
||||
fun generateGolangBuildEnvironment(vararg pathAppend: String): Map<String, String> {
|
||||
val environment = TreeMap<String, String>(String.CASE_INSENSITIVE_ORDER).apply { putAll(System.getenv()) }
|
||||
val properties = Properties().apply { load(rootProject.file("local.properties").inputStream()) }
|
||||
|
||||
val sdkPath = properties.getProperty("sdk.dir")
|
||||
?: throw GradleScriptException("sdk.dir not found", FileNotFoundException())
|
||||
val ndkPath = properties.getProperty("ndk.dir")
|
||||
?: throw GradleScriptException("ndk.dir not found", FileNotFoundException())
|
||||
|
||||
val pathSeparator = if ( Os.isFamily(Os.FAMILY_WINDOWS) ) ";" else ":"
|
||||
|
||||
environment["GOPATH"] = listOf(buildDir.resolve(Constants.GOLANG_PATH).absolutePath, *pathAppend)
|
||||
.joinToString(separator = pathSeparator)
|
||||
environment["ANDROID_HOME"] = sdkPath
|
||||
environment["ANDROID_NDK_HOME"] = ndkPath
|
||||
environment["PATH"] += "$pathSeparator${buildDir.resolve(Constants.GOLANG_BINARY)}"
|
||||
|
||||
return environment
|
||||
}
|
||||
|
||||
fun generateGolangModule(): String {
|
||||
val moduleFile = file(Constants.SOURCE_PATH).resolve("go.mod")
|
||||
|
||||
val replaces = moduleFile
|
||||
.readLines()
|
||||
.asSequence()
|
||||
.map { line -> Constants.REGEX_REPLACE.matchEntire(line) }
|
||||
.filterNotNull()
|
||||
.map { match ->
|
||||
val source = match.groupValues[1].trim()
|
||||
val sVersion = match.groupValues[2].trim()
|
||||
val target = match.groupValues[3].trim()
|
||||
val tVersion = match.groupValues[4].trim()
|
||||
|
||||
val resolvedTarget = if ( target.startsWith("./") )
|
||||
moduleFile.parentFile!!.resolve(target).canonicalPath
|
||||
else
|
||||
target
|
||||
|
||||
"replace $source $sVersion => $resolvedTarget $tVersion"
|
||||
}.joinToString(separator = "\n")
|
||||
|
||||
return Constants.STUB_GO_MOD_CONTENT
|
||||
.replace("{SOURCE_PATH}", file(Constants.SOURCE_PATH).absolutePath) + replaces
|
||||
}
|
||||
|
||||
fun String.exec(pwd: File = buildDir, env: Map<String, String> = System.getenv()): String {
|
||||
val process = ProcessBuilder().run {
|
||||
if ( Os.isFamily(Os.FAMILY_WINDOWS) )
|
||||
command("cmd.exe", "/c", this@exec)
|
||||
else
|
||||
command("bash", "-c", this@exec)
|
||||
|
||||
environment().putAll(env)
|
||||
directory(pwd)
|
||||
|
||||
redirectErrorStream(true)
|
||||
|
||||
start()
|
||||
}
|
||||
|
||||
val outputStream = ByteArrayOutputStream()
|
||||
process.inputStream.copyTo(outputStream)
|
||||
|
||||
if ( process.waitFor() != 0 ) {
|
||||
println(outputStream.toString("utf-8"))
|
||||
throw GradleScriptException("Exec $this failure", IOException())
|
||||
}
|
||||
|
||||
return outputStream.toString("utf-8")
|
||||
}
|
||||
|
||||
task("generateClashBindSources") {
|
||||
onlyIf {
|
||||
val lastModified = file(Constants.SOURCE_PATH).walk()
|
||||
.filter { it.extension == "go" || it.extension == "mod" }
|
||||
.map { it.lastModified() }
|
||||
.max() ?: 0L
|
||||
|
||||
return@onlyIf lastModified > buildDir.resolve(Constants.GOLANG_OUTPUT).lastModified()
|
||||
}
|
||||
|
||||
doFirst {
|
||||
buildDir.resolve(Constants.GOLANG_BIND).apply {
|
||||
deleteRecursively()
|
||||
mkdirs()
|
||||
}
|
||||
}
|
||||
|
||||
doLast {
|
||||
val environment = generateGolangBuildEnvironment()
|
||||
|
||||
val bind = buildDir.resolve(Constants.GOLANG_BIND).apply {
|
||||
resolve("main.go").writeText(Constants.STUB_GO_FILE_CONTENT)
|
||||
resolve("go.mod").writeText(generateGolangModule())
|
||||
}
|
||||
|
||||
"go mod vendor".exec(pwd = bind, env = environment)
|
||||
|
||||
buildDir.resolve(Constants.GOLANG_BIND).apply {
|
||||
resolve("vendor").renameTo(resolve("src"))
|
||||
resolve("go.mod").delete()
|
||||
resolve("main.go").delete()
|
||||
resolve("go.sum").delete()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
task("bindClashCore") {
|
||||
dependsOn(tasks["generateClashBindSources"])
|
||||
|
||||
onlyIf {
|
||||
!tasks["generateClashBindSources"].state.skipped
|
||||
}
|
||||
|
||||
doFirst {
|
||||
val environment = generateGolangBuildEnvironment()
|
||||
|
||||
"go get golang.org/x/mobile/cmd/gomobile".exec(env = environment)
|
||||
}
|
||||
|
||||
doLast {
|
||||
val bind = buildDir.resolve(Constants.GOLANG_BIND)
|
||||
val environment = generateGolangBuildEnvironment(bind.absolutePath)
|
||||
|
||||
"gomobile init".exec(pwd = bind, env = environment)
|
||||
"gomobile bind -target=android -trimpath github.com/kr328/cfa/bridge"
|
||||
.exec(pwd = buildDir.resolve(Constants.GOLANG_BASE), env = environment)
|
||||
}
|
||||
}
|
||||
|
||||
task("extractSources", type = Copy::class) {
|
||||
dependsOn(tasks["bindClashCore"])
|
||||
|
||||
doFirst {
|
||||
buildDir.resolve(Constants.OUTPUT_PATH).apply {
|
||||
resolve("jniLibs").deleteRecursively()
|
||||
resolve("classes").deleteRecursively()
|
||||
}
|
||||
}
|
||||
from(zipTree(buildDir.resolve(Constants.GOLANG_OUTPUT))) {
|
||||
include("**/*.so")
|
||||
eachFile {
|
||||
path = path.replace(Constants.REGEX_JNI, "jniLibs/")
|
||||
}
|
||||
}
|
||||
from(zipTree(buildDir.resolve(Constants.GOLANG_OUTPUT_SOURCES))) {
|
||||
include("**/*.java")
|
||||
into("classes")
|
||||
}
|
||||
|
||||
destinationDir = buildDir.resolve(Constants.OUTPUT_PATH)
|
||||
}
|
||||
|
||||
task("downloadGeoipDatabase") {
|
||||
onlyIf {
|
||||
val file = buildDir.resolve(Constants.OUTPUT_PATH).resolve("assets/Country.mmdb")
|
||||
|
||||
System.currentTimeMillis() - file.lastModified() > Constants.GEOIP_INVALID_INTERVAL
|
||||
}
|
||||
|
||||
doLast {
|
||||
val assets = buildDir.resolve(Constants.OUTPUT_PATH).resolve("assets")
|
||||
|
||||
assets.mkdirs()
|
||||
|
||||
URL(Constants.GEOIP_DATABASE_URL).openConnection().getInputStream().use { input ->
|
||||
FileOutputStream(assets.resolve("Country.mmdb")).use { output ->
|
||||
input.copyTo(output)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
task("resetGolangMode", type = Exec::class) {
|
||||
onlyIf {
|
||||
!Os.isFamily(Os.FAMILY_WINDOWS)
|
||||
}
|
||||
|
||||
commandLine("chmod", "-R", "777", buildDir.resolve(Constants.GOLANG_PATH))
|
||||
|
||||
isIgnoreExitValue = true
|
||||
}
|
||||
@@ -3,26 +3,59 @@ package bridge
|
||||
import (
|
||||
"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"
|
||||
)
|
||||
|
||||
func LoadMMDB(data []byte) {
|
||||
dataClone := make([]byte, len(data))
|
||||
copy(dataClone, data)
|
||||
var (
|
||||
logCallback LogCallback
|
||||
logSubscribe sync.Once
|
||||
)
|
||||
|
||||
mmdb.LoadFromBytes(dataClone)
|
||||
type LogCallback interface {
|
||||
OnLogEvent(level, payload string)
|
||||
}
|
||||
|
||||
func SetHome(homeDir string) {
|
||||
func InitCore(geoipDatabase[] byte, homeDir string, version string) {
|
||||
dataClone := make([]byte, len(geoipDatabase))
|
||||
copy(dataClone, geoipDatabase)
|
||||
|
||||
mmdb.LoadFromBytes(dataClone)
|
||||
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() {
|
||||
sub := log.Subscribe()
|
||||
defer log.UnSubscribe(sub)
|
||||
|
||||
for {
|
||||
elm := <-sub
|
||||
l := elm.(*log.Event)
|
||||
|
||||
if l.LogLevel < log.Level() {
|
||||
continue
|
||||
}
|
||||
|
||||
if cb := logCallback; cb != nil {
|
||||
cb.OnLogEvent(l.LogLevel.String(), l.Payload)
|
||||
}
|
||||
}
|
||||
}()
|
||||
})
|
||||
|
||||
logCallback = callback
|
||||
}
|
||||
|
||||
@@ -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,55 +83,23 @@ func QueryAllProxyGroups(collection ProxyGroupCollection) {
|
||||
ps := tunnel.Proxies()
|
||||
|
||||
for _, p := range ps {
|
||||
pa, ok := p.(*outbound.Proxy)
|
||||
pi, ok := p.(*outbound.Proxy)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
switch group := pa.ProxyAdapter.(type) {
|
||||
case *outboundgroup.Fallback:
|
||||
collection.Add(
|
||||
&ProxyGroupItem{
|
||||
Name: group.Name(),
|
||||
Type: group.Type().String(),
|
||||
Current: group.Now(),
|
||||
Delay: int(p.LastDelay()),
|
||||
providers: group.GetProviders(),
|
||||
},
|
||||
)
|
||||
case *outboundgroup.URLTest:
|
||||
collection.Add(
|
||||
&ProxyGroupItem{
|
||||
Name: group.Name(),
|
||||
Type: group.Type().String(),
|
||||
Current: group.Now(),
|
||||
Delay: int(p.LastDelay()),
|
||||
providers: group.GetProviders(),
|
||||
},
|
||||
)
|
||||
case *outboundgroup.LoadBalance:
|
||||
collection.Add(
|
||||
&ProxyGroupItem{
|
||||
Name: group.Name(),
|
||||
Type: group.Type().String(),
|
||||
Current: "",
|
||||
Delay: int(p.LastDelay()),
|
||||
providers: group.GetProviders(),
|
||||
},
|
||||
)
|
||||
case *outboundgroup.Selector:
|
||||
collection.Add(
|
||||
&ProxyGroupItem{
|
||||
Name: group.Name(),
|
||||
Type: group.Type().String(),
|
||||
Current: group.Now(),
|
||||
Delay: int(p.LastDelay()),
|
||||
providers: group.GetProviders(),
|
||||
},
|
||||
)
|
||||
default:
|
||||
group, ok := pi.ProxyAdapter.(outboundgroup.ProxyGroup)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
collection.Add(&ProxyGroupItem{
|
||||
Name: group.Name(),
|
||||
Type: group.Type().String(),
|
||||
Current: group.Now(),
|
||||
Delay: 0,
|
||||
providers: group.GetProxyProviders(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,16 +1,21 @@
|
||||
package bridge
|
||||
|
||||
import (
|
||||
"github.com/Dreamacro/clash/log"
|
||||
"sync"
|
||||
|
||||
"github.com/Dreamacro/clash/tunnel"
|
||||
)
|
||||
|
||||
type EventPoll struct {
|
||||
stop sync.Once
|
||||
|
||||
onStop func()
|
||||
}
|
||||
|
||||
func (e *EventPoll) Stop() {
|
||||
e.onStop()
|
||||
e.stop.Do(func() {
|
||||
e.onStop()
|
||||
})
|
||||
}
|
||||
|
||||
type Traffic struct {
|
||||
@@ -40,35 +45,3 @@ func QueryTraffic() *Traffic {
|
||||
Download: down,
|
||||
}
|
||||
}
|
||||
|
||||
func PollLogs(logs Logs) *EventPoll {
|
||||
stopChannel := make(chan int, 1)
|
||||
sub := log.Subscribe()
|
||||
|
||||
go func() {
|
||||
defer log.UnSubscribe(sub)
|
||||
defer close(stopChannel)
|
||||
defer log.Infoln("Logs Poll Stopped")
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-stopChannel:
|
||||
return
|
||||
case elm := <-sub:
|
||||
l := elm.(*log.Event)
|
||||
|
||||
if l.LogLevel < log.Level() {
|
||||
break
|
||||
}
|
||||
|
||||
logs.OnEvent(l.Type(), l.Payload)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
return &EventPoll{
|
||||
onStop: func() {
|
||||
stopChannel <- 0
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,9 +28,9 @@ func onNewListenConfig(listen *net.ListenConfig) {
|
||||
listen.Control = onNewSocket
|
||||
}
|
||||
|
||||
func onNewSocket(network, address string, c syscall.RawConn) error {
|
||||
func onNewSocket(_, _ string, c syscall.RawConn) error {
|
||||
if cb := callback; cb != nil {
|
||||
c.Control(func(fd uintptr) {
|
||||
_ = c.Control(func(fd uintptr) {
|
||||
cb.OnCreateSocket(int(fd))
|
||||
})
|
||||
}
|
||||
@@ -38,10 +38,10 @@ func onNewSocket(network, address string, c syscall.RawConn) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func StartTunDevice(fd, mtu int, dns string, cb TunCallback) error {
|
||||
func StartTunDevice(fd, mtu int, gateway, mirror, dns string, cb TunCallback) error {
|
||||
callback = cb
|
||||
|
||||
return tun.StartTunDevice(fd, mtu, dns)
|
||||
return tun.StartTunDevice(fd, mtu, gateway, mirror, dns)
|
||||
}
|
||||
|
||||
func StopTunDevice() {
|
||||
|
||||
Submodule core/src/main/golang/clash updated: c6eb441961...9c5b93f8c0
@@ -1,8 +1,9 @@
|
||||
package profile
|
||||
package config
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"github.com/kr328/cfa/utils"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"net/http"
|
||||
@@ -30,63 +31,70 @@ var client = &http.Client{
|
||||
|
||||
tunnel.Add(inbound.NewSocket(socks5.ParseAddr(address), server, constant.HTTP, constant.TCP))
|
||||
|
||||
go func() {
|
||||
if ctx == nil || ctx.Done() == nil {
|
||||
return
|
||||
}
|
||||
|
||||
<-ctx.Done()
|
||||
|
||||
client.Close()
|
||||
server.Close()
|
||||
}()
|
||||
|
||||
return client, nil
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
func DownloadAndCheck(url, output, baseDir string) error {
|
||||
func fetchRemote(url string) ([]byte, error) {
|
||||
request, err := http.NewRequest("GET", url, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
request.Header.Set("User-Agent", "ClashForAndroid/"+ApplicationVersion)
|
||||
|
||||
response, err := client.Do(request)
|
||||
if err != nil {
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
defer response.Body.Close()
|
||||
defer utils.CloseSilent(response.Body)
|
||||
|
||||
data, err := ioutil.ReadAll(response.Body)
|
||||
if err != nil {
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return SaveAndCheck(data, output, baseDir)
|
||||
return data, nil
|
||||
}
|
||||
|
||||
func ReadAndCheck(fd int, output, baseDir string) error {
|
||||
syscall.SetNonblock(fd, true)
|
||||
func fetchLocal(fd int) ([]byte, error) {
|
||||
_ = syscall.SetNonblock(fd, true)
|
||||
|
||||
file := os.NewFile(uintptr(fd), "/dev/null")
|
||||
defer file.Close()
|
||||
defer utils.CloseSilent(file)
|
||||
|
||||
data, err := ioutil.ReadAll(file)
|
||||
return ioutil.ReadAll(file)
|
||||
}
|
||||
|
||||
func PullRemote(url, output, baseDir string) error {
|
||||
data, err := fetchRemote(url)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return SaveAndCheck(data, output, baseDir)
|
||||
return save(data, output, baseDir)
|
||||
}
|
||||
|
||||
func SaveAndCheck(data []byte, output, baseDir string) error {
|
||||
_, err := parseConfig(data, 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 {
|
||||
cfg, err := parseConfig(data, baseDir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, v := range cfg.Providers {
|
||||
_ = v.Destroy()
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user