Compare commits

...
183 Commits
Author SHA1 Message Date
Kr328 cdd0e3dd30 fix break connection 2020-05-03 00:51:51 +08:00
Kr328 5823955c71 update clash core 2020-05-03 00:50:34 +08:00
Kr328 714c9a554f break connection if set proxy for selector 2020-05-03 00:50:14 +08:00
Kr328 e621324b48 update clash core 2020-05-03 00:29:45 +08:00
Kr328 dfe1e7ecd9 exclude 127.0.0.0/8 from route table 2020-05-03 00:17:55 +08:00
Kr328 4c3380d822 update clash core & tun2socket 2020-04-29 13:22:36 +08:00
Kr328 1d68516cdb fix receivers & service connection leak 2020-04-24 00:49:03 +08:00
Kr328 821bb49914 update issue template 2020-04-23 19:33:28 +08:00
Kr328 c95993a629 fix crash on uploaded 2020-04-23 19:15:46 +08:00
Kr328 e6495f5e1f add uploaded feedback 2020-04-23 19:15:16 +08:00
Kr328 c104942b5f update version 2020-04-23 19:13:34 +08:00
Kr328 59835667aa add report logcat button 2020-04-23 19:04:51 +08:00
Kr328 8a98eea8fa improve core log 2020-04-23 18:52:23 +08:00
Kr328 3dc20b9e70 update issue template 2020-04-23 16:29:17 +08:00
Kr328 d8ab6fd755 update tun2socket version 2020-04-23 13:10:22 +08:00
Kr328 cc499a7897 improve package name modify 2020-04-23 12:14:05 +08:00
Kr328 76f5261b20 Revert "Fix tun start at second user"
This reverts commit 94a0282c
2020-04-23 11:08:36 +08:00
Kr328 6a506b761f update tun2socket 2020-04-23 11:06:50 +08:00
Kr328 fe966f9c5d Revert "Fix tun start at second user"
This reverts commit 94a0282c
2020-04-23 10:52:59 +08:00
Kr328 bcf7f793ec add stop tun on destroy & update version 2020-04-23 10:51:20 +08:00
Kr328 75de6ce041 add full crash logcat report 2020-04-23 00:12:07 +08:00
Kr328 4417a85ef7 update version 2020-04-23 00:09:24 +08:00
Kr328 9e0b67c04a add dump AndroidRuntime tag 2020-04-23 00:06:53 +08:00
Kr328 8cd203e7b0 detect vpn establish failure 2020-04-23 00:03:05 +08:00
Kr328 211160f7f8 Rename fields to let lint happy :) 2020-04-22 13:25:50 +08:00
Kr328 2afc7262eb Update version 2020-04-22 11:25:01 +08:00
Kr328 94a0282c70 Fix tun start at second user 2020-04-22 11:05:10 +08:00
Kr328 96b78829ab Fix cancel profile auto update 2020-04-22 00:25:22 +08:00
Kr328 35d5a90b2a Improve database migrations 2020-04-21 23:50:01 +08:00
Kr328 50d73ad498 update version & dependencies 2020-04-21 22:38:43 +08:00
Kr328 d1f8bc3062 fix delete profile not cancel auto update 2020-04-21 22:32:16 +08:00
Kr328 d3353b1aa5 fix update interval reset 2020-04-21 22:29:21 +08:00
Kr328 cd48fb4e08 fix dialer hook 2020-04-21 22:27:09 +08:00
Kr328 f70738ecf5 update clash core 2020-04-21 22:18:40 +08:00
Kr328 2d63466597 update tun2socket 2020-04-21 21:43:35 +08:00
Kr328 ecf67443fb remove reduce zero ip detect 2020-04-20 12:35:07 +08:00
Kr328 43987ad111 remove reduce zero ip detect 2020-04-20 12:34:27 +08:00
Kr328 5fbd6c6196 Update bug report issue 2020-04-19 02:06:12 +08:00
Kr328 1ec7f1f7e9 Merge branch 'master' into dev 2020-04-19 02:01:39 +08:00
Kr328 c5104c241a add basic url auth support 2020-04-18 20:41:20 +08:00
Kr328 021ba3519d format export log file entry 2020-04-17 13:50:10 +08:00
Kr328 119f74cd05 fix profile edit fragment cast crash 2020-04-17 00:19:39 +08:00
Kr328 f0b61c9e1f cleanup code 2020-04-16 22:40:27 +08:00
Kr328 a573631068 use set directly instead of toList 2020-04-16 22:36:45 +08:00
Kr328 a12444b258 add tun start failure detect 2020-04-16 21:52:11 +08:00
Kr328 17257228a9 Update clash core & add udp packet recycle 2020-04-16 21:26:33 +08:00
Kr328 53b6a9dfc1 Fix pack mmdb failure if downloadGepip before assembleClashCore 2020-04-16 12:13:43 +08:00
Kr328 c45f8ddfc3 Add simple package name modify detect 2020-04-16 11:05:49 +08:00
Kr328 e04c9c15ff Fix Database migrations 2020-04-16 11:03:43 +08:00
Kr328 d5cb363579 Update core 2020-04-15 21:17:26 +08:00
Kr328 0748021ffa Fix cloned profile type 2020-04-15 20:44:49 +08:00
Kr328 1d21368385 Fix database migrations 2020-04-15 13:16:50 +08:00
Kr328 2a5865397a fix DatabaseMigrations.kt 2020-04-15 12:25:28 +08:00
Kr328 f5f378f3b0 update clash core 2020-04-15 11:14:37 +08:00
Kr328 861d5fa871 Update tun2socket 2020-04-15 11:07:02 +08:00
Kr328 4518825aca update tun2socket & update build tools 2020-04-15 01:29:50 +08:00
Kr328 260ebc83ff fix access control & remove context support in fetch.go 2020-04-14 23:44:41 +08:00
Kr328 640cf3e75c add scroll view to support activity 2020-04-14 20:25:13 +08:00
Kr328 7466990e0c update version 1.1.11 -> 1.2.0 2020-04-14 20:07:07 +08:00
Kr328 b41715686c cleanup code & rename util -> utils 2020-04-14 19:50:56 +08:00
Kr328 bca2a7a520 decrease waiting time 2020-04-14 19:47:32 +08:00
Kr328 545c43fe2c fix external profile 2020-04-14 19:47:16 +08:00
Kr328 eaf16318de fix external intent lost & replace application observer 2020-04-14 19:17:24 +08:00
Kr328 a809f94a02 add profile notification intent & detect is modified 2020-04-14 18:47:43 +08:00
Kr328 541862cda8 change routing system traffic string 2020-04-14 14:28:54 +08:00
Kr328 7ab2df9afe fix profile reload detect 2020-04-14 14:25:18 +08:00
Kr328 c45c4a1ad8 fix profile edit 2020-04-14 14:18:41 +08:00
Kr328 2b71f9a9f7 remove channels on migration 2020-04-14 13:57:28 +08:00
Kr328 3db2e1c618 fix profile edit & cleanup code 2020-04-14 12:25:20 +08:00
Kr328 d24617d842 add clone profile detect 2020-04-14 11:03:30 +08:00
Kr328 29dc8dc492 fix as Entity.Profile 2020-04-14 10:48:29 +08:00
Kr328 57b8ce3d7f rename ProfileMetadata.kt -> Profile.kt 2020-04-14 10:47:14 +08:00
Kr328 1536c9f056 fix auto update 2020-04-14 10:45:55 +08:00
Kr328 c2280555d3 fix dns hijacking 2020-04-14 02:17:07 +08:00
Kr328 532760e71a add loading notification 2020-04-14 02:16:58 +08:00
Kr328 6da7eff62c fix reload logic 2020-04-14 02:07:19 +08:00
Kr328 a75192b6bf improve code by Inspect Code 2020-04-14 01:23:15 +08:00
Kr328 f4ceebb12c rename ipc -> transact 2020-04-14 01:03:29 +08:00
Kr328 151327e9ba add waiting status for profile service 2020-04-14 00:58:07 +08:00
Kr328 a343678bad fix auto restart 2020-04-14 00:55:24 +08:00
Kr328 1b2c97d381 bug fix 2020-04-13 23:48:40 +08:00
Kr328 d034eb6f4b remove unused resources 2020-04-13 16:35:09 +08:00
Kr328 08dc165450 rename makeSnackbar & remove modified detect 2020-04-13 16:28:59 +08:00
Kr328 50eef54ab6 update support activity 2020-04-13 16:28:13 +08:00
Kr328 a75bdd457f profile ui refactored 2020-04-13 14:59:24 +08:00
Kr328 d5d219789f add clone support 2020-04-13 12:15:37 +08:00
Kr328 04eed1a768 broadcast with permission 2020-04-13 12:00:48 +08:00
Kr328 87d17bcb9a remove providers only on new profile 2020-04-13 11:58:01 +08:00
Kr328 2d3bd5c5cf add temp acquire 2020-04-13 11:45:05 +08:00
Kr328 12d76a2ad1 cleanup code 2020-04-13 11:13:04 +08:00
Kr328 a0026b1bb3 add timeout to profile processor 2020-04-13 11:12:45 +08:00
Kr328 761f11fa5f move utils to common 2020-04-13 11:12:28 +08:00
Kr328 a7b28692ea remove sync map 2020-04-13 10:48:05 +08:00
Kr328 daac6e1e5c service refactored 2020-04-13 10:46:02 +08:00
Kr328 8c9b7e4a01 decrease mtu 2020-04-11 19:49:20 +08:00
Kr328 1b0778141c update README.md 2020-04-10 14:35:27 +08:00
Kr328 96ed63b704 update README.md 2020-04-10 14:34:32 +08:00
Kr328 bb07bc639d update README.md 2020-04-10 13:49:25 +08:00
Kr328 c3eb4b2f51 update clash core 2020-04-10 13:45:51 +08:00
Kr328 9deeb37f21 add auto signing with properties 2020-04-10 13:45:16 +08:00
Kr328 0befdc0d34 clean up code 2020-04-09 23:19:59 +08:00
Kr328 606a21dff0 add documents provider 2020-04-09 14:39:57 +08:00
Kr328 ef6a37fb7c remove default network detect logs 2020-04-08 16:23:37 +08:00
Kr328 2f7b8c4b59 fix default network detect 2020-04-08 16:21:22 +08:00
Kr328 968e82b072 move load.go init to patch.go 2020-04-08 16:14:09 +08:00
Kr328 0240eea776 android: add fake pool host override 2020-04-08 11:13:40 +08:00
Kr328 dc8d94fa31 remove ipv6 for VpnService 2020-04-08 11:11:38 +08:00
Kr328 f6ec8cc882 update tun2socket & remove ipv6 for VpnService 2020-04-08 11:10:13 +08:00
Kr328 b648d21068 use ServiceStatusProvider instead of requery database 2020-04-08 02:05:05 +08:00
Kr328 2b63997b22 cleanup code 2020-04-08 01:58:14 +08:00
Kr328 198daaf720 add restore selected proxy 2020-04-08 01:57:50 +08:00
Kr328 a1847dc6f2 update core & broadcast on loaded & stop on load failure 2020-04-08 01:42:21 +08:00
Kr328 8ebb3a5f31 fix dns hijack not working 2020-04-08 01:00:06 +08:00
Kr328 7cf3dc2bf2 clean up code 2020-04-08 00:44:28 +08:00
Kr328 2346095323 increase mtu 2020-04-08 00:43:47 +08:00
Kr328 40ae2d456c rename module component -> common 2020-04-08 00:42:41 +08:00
Kr328 3152bcaaa6 refactor clash core context 2020-04-08 00:42:22 +08:00
Kr328 c69c41c57f refactor clash core context 2020-04-03 01:08:04 +08:00
Kr328 ea1f424b33 refactor clash core context 2020-04-02 00:44:26 +08:00
Kr328 1ef828497c remove gc period 2020-04-01 14:45:53 +08:00
Kr328 88b28faa48 add component module & clean up code 2020-04-01 14:45:06 +08:00
Kr328 7bfaacf936 call gc period to decrease ram usage 2020-04-01 01:12:50 +08:00
Kr328 05d8dae4e0 fix notification not show profile 2020-04-01 00:52:36 +08:00
Kr328 27cd92b4b1 recycle dns udp packet 2020-03-31 23:21:02 +08:00
Kr328 4b6b5b5d95 update core 2020-03-30 16:51:39 +08:00
Kr328 6468575a42 fix fake ip filter not working 2020-03-30 16:38:33 +08:00
Kr328 5ea22cecad update tun2socket & fix crash on tcp dns query 2020-03-30 13:09:36 +08:00
Kr328 c8205d3f95 update tun2socket 2020-03-29 20:14:45 +08:00
Kr328 639377760c update tun2socket 2020-03-29 15:25:39 +08:00
Kr328 a9323f0528 update tun2socket & core & mtu 2020-03-28 23:41:52 +08:00
Kr328 2e31e90225 update core 2020-03-28 14:49:11 +08:00
Kr328 9d5b8188eb fix patch bug 2020-03-28 14:39:28 +08:00
Kr328 1f9e330f6a fix patch bug 2020-03-28 13:56:54 +08:00
Kr328 a69776fe33 update clash core 2020-03-28 12:31:48 +08:00
Kr328 d42dc46b8c use tun2socket instead of netstack 2020-03-28 12:30:26 +08:00
Kr328 c37dc2f874 fix delete geoip database on rebuild 2020-03-28 12:29:28 +08:00
Kr328 9c20f13f95 update dependencies 2020-03-27 23:59:33 +08:00
Kr328 e958e7f675 Update 03-bug-report-zh-cn.md 2020-03-22 20:45:20 +08:00
Kr328 68113e694e update issue template 2020-03-22 13:04:50 +08:00
Kr328 88aec66ef8 Merge branch 'master' into dev 2020-03-21 16:13:55 +08:00
Kr328 f500596621 update README.md 2020-03-21 16:11:03 +08:00
Kr328 df5bafd0bb update README.md 2020-03-21 16:01:35 +08:00
Kr328 01fe9deb20 update issue template 2020-03-20 13:16:43 +08:00
Kr328 4e44298e98 sorted github issue 2020-03-19 19:46:12 +08:00
Kr328 a7c3a05c23 add application info request 2020-03-19 19:09:20 +08:00
Kr328 05fa36497b rename issues template file 2020-03-19 18:56:41 +08:00
Kr328 e6f8ae265e Add issue templates 2020-03-19 18:53:05 +08:00
Kr328 d107847747 remove unused General.Mode serializer 2020-03-19 15:21:20 +08:00
Kr328 ea8e270df6 add auto activity name detect 2020-03-19 15:16:50 +08:00
Kr328 4bc40e0663 update dependencies 2020-03-19 15:16:39 +08:00
Kr328 5e6ccd990a update dependencies 2020-03-19 12:29:34 +08:00
Kr328 1a8d673742 update clash core 2020-03-03 22:48:54 +08:00
Kr328 a4069aa6f6 remove unused var 2020-03-03 22:01:07 +08:00
Kr328 3cfb90c078 use kotlin build script 2020-03-03 21:50:30 +08:00
Kr328 594949d3f0 format code & update core 2020-02-28 23:52:58 +08:00
Kr328 8fd34b5258 improve apk broken detect & fix udp crash 2020-02-28 14:00:38 +08:00
Kr328 1161482a5b fix udp crash 2020-02-28 11:19:06 +08:00
Kr328 8a123e7a0d improve network settings 2020-02-28 01:13:11 +08:00
Kr328 4ef3a20929 add vpn component detect 2020-02-28 00:32:49 +08:00
Kr328 16c2e9b694 increase refresh interval 2020-02-28 00:24:24 +08:00
Kr328 c9a9d310ef update version 2020-02-28 00:22:21 +08:00
Kr328 84996a5652 update apk broken detect 2020-02-27 16:30:57 +08:00
Kr328 115afc5735 update apk broken detect 2020-02-27 16:30:34 +08:00
Kr328 2ae75e876d fix uri case 2020-02-27 16:03:28 +08:00
Kr328 126291d544 improve duplicate profile 2020-02-27 14:26:47 +08:00
Kr328 37db81f6b1 improve profile update & update clash core 2020-02-27 14:05:00 +08:00
Kr328 a3cfd10d39 fix udp crash 2020-02-26 16:31:59 +08:00
Kr328 0e61f02046 fix typo 2020-02-26 15:20:22 +08:00
Kr328 d8b1c0b7b8 remove debug symbol of clash core 2020-02-26 14:58:29 +08:00
Kr328 8bf908b731 remove default disallow package 2020-02-26 14:53:30 +08:00
Kr328 568f1f12e7 fix crash on ipv6 enable & reduce provider call 2020-02-26 14:06:46 +08:00
Kr328 ca739a5e2f add abi split 2020-02-26 13:53:42 +08:00
Kr328 7833f747d5 format & cleanup 2020-02-26 13:33:44 +08:00
Kr328 977519b383 change crash reporter
fix crash on log polling
fix crash on udp write back
fix goroutine leak
fix default rule inject
2020-02-26 13:32:29 +08:00
Kr328 b39b3812d0 add crash reporter 2020-02-25 20:53:39 +08:00
Kr328 f7dddb41fa add crash reporter 2020-02-25 20:53:26 +08:00
Kr328 053300919c add crash reporter 2020-02-25 20:38:55 +08:00
Kr328 93dff86383 clear dumper 2020-02-25 14:42:06 +08:00
Kr328 1b7e9c6b12 add golang crash dumper 2020-02-25 14:41:53 +08:00
Kr328 f072cd0839 update dependencies 2020-02-25 12:43:58 +08:00
Kr328 f626a0fc5c fix ipv6 support
https://github.com/shadowsocks/shadowsocks-android/commit/cc840c9fddb3f4f6677005de18f1fcb387b84064#diff-e089fe63dcb3674c0a1e459a95508e3e
2020-02-24 20:33:29 +08:00
Kr328 63da12f046 1.1.4 2020-02-23 23:50:41 +08:00
Kr328 ce1beafb08 fix proxies active cache 2020-02-23 23:22:32 +08:00
207 changed files with 5578 additions and 3687 deletions

No files matched your search

@@ -0,0 +1,51 @@
---
name: "[English] Bug report"
about: Create a report to help us improve
title: "[BUG] "
labels: ''
assignees: ''
---
<!-- Be sure to put a clear title after [BUG] in the text box above -->
<!-- Be sure to put a clear title after [BUG] in the text box above -->
<!-- Be sure to put a clear title after [BUG] in the text box above -->
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Device Info (please complete the following information):**
- Device: [e.g. Pixel 4]
- ROM: [e.g: AOSP]
- ROM Version:
- Android Version [e.g. 10]
**Application Info (please complete the following information):**
- Version: [e.g. 1.1.10]
- Apk File Name: [e.g. app-release-arm64-v8a.apk]
- Distribution Channel: [e.g. Google Play]
**Additional context**
Add any other context about the problem here.
<!--
*Logs*
if applicable, upload logs to help detect problem
`Open App` -> `Support` -> `Feedback` -> `Upload Logcat`
-->
@@ -0,0 +1,24 @@
---
name: "[English] Feature request"
about: Suggest an idea for this app
title: "[Feature Request] "
labels: ''
assignees: ''
---
<!-- Be sure to put a clear title after [Feature Request] in the text box above -->
<!-- Be sure to put a clear title after [Feature Request] in the text box above -->
<!-- Be sure to put a clear title after [Feature Request] in the text box above -->
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.
@@ -0,0 +1,49 @@
---
name: "[简体中文] 创建错误报告"
about: 创建错误报告以帮助我们改进应用
title: "[BUG] "
labels: ''
assignees: ''
---
<!-- 请务必在上方文本框处 [BUG] 后填入清晰明了的标题 -->
<!-- 请务必在上方文本框处 [BUG] 后填入清晰明了的标题 -->
<!-- 请务必在上方文本框处 [BUG] 后填入清晰明了的标题 -->
**描述出现的错误**
请简洁的描述你遇到的错误
**如何复现该错误**
复现步骤:
1. ...
2. ...
3. ...
4. ...
**预期行为**
清晰简单的描述你预期的应用应该表现的行为
**屏幕截图**
如果适用, 上传屏幕截图以帮助描述错误
**设备信息 (请完成以下信息):**
- 机型: [例如: Pixel 4]
- 系统/ROM: [例如: MIUI 11]
- Android 版本 [例如: 10]
- ROM版本 [例如: 20.3.19]
**应用信息**
- 版本: [例如: 1.1.10]
- 安装包文件名: [例如: app-release-arm64-v8a.apk]
- 应用来源: [例如: Google Play]
**附加信息**
其他的可能与改错误相关的信息
<!--
*日志*
如果适用, 上传日志以帮助侦测错误
`打开应用` -> `支持` -> `反馈` -> `上传日志`
-->
@@ -0,0 +1,21 @@
---
name: "[简体中文] 功能请求"
about: 你希望的能够在应用中增加的功能
title: "[Feature Request] "
labels: ''
assignees: ''
---
<!-- 请务必在上方文本框处 [Feature Request] 后填入清晰明了的标题 -->
<!-- 请务必在上方文本框处 [Feature Request] 后填入清晰明了的标题 -->
<!-- 请务必在上方文本框处 [Feature Request] 后填入清晰明了的标题 -->
**功能描述**
请清晰的描述你想要的功能
**描述你希望的实现方式**
清晰的描述应用应该如何实现该功能
**附加信息**
其他的与改功能相关的附加信息
+1
View File
@@ -0,0 +1 @@
blank_issues_enabled: false
-3
View File
@@ -23,9 +23,6 @@ gradle-app.setting
*.keystore
*.jks
# gradle
.gradle
# clion cmake build
cmake-build-*
+16 -19
View File
@@ -1,10 +1,8 @@
## Clash for Android
A GUI for [clash](https://github.com/Dreamacro/clash) on Android
> NOTICE: Early testing currently
A Graphical user interface of [clash](https://github.com/Dreamacro/clash) for Android
<a href="https://play.google.com/store/apps/details?id=com.github.kr328.clash"><img width="200px" alt="Get it on Google Play" src="https://play.google.com/intl/en_us/badges/static/images/badges/en_badge_web_generic.png"/></a> or [Releases](https://github.com/Kr328/ClashForAndroid/releases)
### Feature
@@ -15,9 +13,7 @@ Fully feature of [clash](https://github.com/Dreamacro/clash) ~~(Exclude `externa
### Requirement
* Android 7.0+
* `arm64` or `x86_64` architecture
* `armeabi-v7a` , `arm64-v8a`, `x86` or `x86_64` Architecture
### License
@@ -39,9 +35,9 @@ See also [PRIVACY_POLICY.md](./PRIVACY_POLICY.md)
git submodule update --init --recursive
```
2. Install `Android SDK (include JDK)` ,`Android NDK` and `Golang`
2. Install `JDK 1.8`, `Android SDK` ,`Android NDK` and `Golang`
3. Configure `local.properties`
3. Create `local.properties` in project root with
```properties
sdk.dir=/path/to/android-sdk
@@ -49,18 +45,19 @@ See also [PRIVACY_POLICY.md](./PRIVACY_POLICY.md)
appcenter.key=<AppCenter Key> # Optional, from "appcenter.ms"
```
4. Build
4. Create `keystore.properties` in project root with
on Linux
```properties
storeFile=/path/to/keystore/file
storePassword=<key store password>
keyAlias=<key alias>
keyPassword=<key password>
```
5. Build
```bash
./gradlew build
./gradlew app:assembleRelease
```
on Windows
```bash
.\gradlew.bat build
```
6. Pick `app-release-<arch>.apk` in `app/build/outputs/apks`
-69
View File
@@ -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)
}
+139
View File
@@ -0,0 +1,139 @@
import java.util.*
import java.security.*
plugins {
id("com.android.application")
id("kotlin-android")
id("kotlin-android-extensions")
}
val rootExtra = rootProject.extra
val gCompileSdkVersion: Int by rootExtra
val gBuildToolsVersion: String by rootExtra
val gMinSdkVersion: Int by rootExtra
val gTargetSdkVersion: Int by rootExtra
val gVersionCode: Int by rootExtra
val gVersionName: String by rootExtra
val gKotlinVersion: String by rootExtra
val gKotlinCoroutineVersion: String by rootExtra
val gAppCenterVersion: String by rootExtra
val gAndroidKtxVersion: String by rootExtra
val gRecyclerviewVersion: String by rootExtra
val gAppCompatVersion: String by rootExtra
val gMaterialDesignVersion: String by rootExtra
val gShizukuPreferenceVersion: String by rootExtra
val gMultiprocessPreferenceVersion: String by rootExtra
android {
compileSdkVersion(gCompileSdkVersion)
buildToolsVersion(gBuildToolsVersion)
defaultConfig {
applicationId = "com.github.kr328.clash"
minSdkVersion(gMinSdkVersion)
targetSdkVersion(gTargetSdkVersion)
versionCode = gVersionCode
versionName = gVersionName
}
buildTypes {
maybeCreate("release").apply {
isMinifyEnabled = true
isShrinkResources = true
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = "1.8"
}
splits {
abi {
isEnable = true
isUniversalApk = true
}
}
val signingFile = rootProject.file("keystore.properties")
if ( signingFile.exists() ) {
val properties = Properties().apply {
signingFile.inputStream().use {
load(it)
}
}
signingConfigs {
maybeCreate("release").apply {
storeFile = rootProject.file(Objects.requireNonNull(properties.getProperty("storeFile")))
storePassword = Objects.requireNonNull(properties.getProperty("storePassword"))
keyAlias = Objects.requireNonNull(properties.getProperty("keyAlias"))
keyPassword = Objects.requireNonNull(properties.getProperty("keyPassword"))
}
}
buildTypes {
maybeCreate("release").apply {
this.signingConfig = signingConfigs.findByName("release")
}
}
}
}
dependencies {
implementation(project(":core"))
implementation(project(":service"))
implementation(project(":design"))
implementation(project(":common"))
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8:$gKotlinVersion")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$gKotlinCoroutineVersion")
implementation("androidx.recyclerview:recyclerview:$gRecyclerviewVersion")
implementation("androidx.core:core-ktx:$gAndroidKtxVersion")
implementation("androidx.appcompat:appcompat:$gAppCompatVersion")
implementation("com.google.android.material:material:$gMaterialDesignVersion")
implementation("moe.shizuku.preference:preference-appcompat:$gShizukuPreferenceVersion")
implementation("moe.shizuku.preference:preference-simplemenu-appcompat:$gShizukuPreferenceVersion")
implementation("com.microsoft.appcenter:appcenter-analytics:$gAppCenterVersion")
implementation("com.microsoft.appcenter:appcenter-crashes:$gAppCenterVersion")
}
task("injectAppCenterKey") {
doFirst {
val properties = Properties().apply {
rootProject.file("local.properties").inputStream().use {
load(it)
}
}
val key = properties.getProperty("appcenter.key", "")
android.buildTypes.forEach {
it.buildConfigField("String", "APP_CENTER_KEY", "\"$key\"")
}
}
}
task("injectPackageNameBase64") {
doFirst {
val packageName = android.defaultConfig.applicationId ?: return@doFirst
val base64 = Base64.getEncoder().encodeToString(packageName.toByteArray(Charsets.UTF_8))
android.buildTypes.forEach {
it.buildConfigField("String", "PACKAGE_NAME_BASE64", "\"$base64\"")
}
}
}
afterEvaluate {
tasks["preBuild"].dependsOn(tasks["injectAppCenterKey"], tasks["injectPackageNameBase64"])
}
-7
View File
@@ -109,12 +109,5 @@
<action android:name="android.service.quicksettings.action.QS_TILE" />
</intent-filter>
</service>
<receiver
android:name=".OnBootReceiver"
android:enabled="false">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
</intent-filter>
</receiver>
</application>
</manifest>
@@ -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,17 +10,15 @@ import androidx.core.net.toFile
import androidx.recyclerview.widget.LinearLayoutManager
import com.github.kr328.clash.adapter.LiveLogAdapter
import com.github.kr328.clash.adapter.LogAdapter
import com.github.kr328.clash.common.utils.intent
import com.github.kr328.clash.core.event.LogEvent
import com.github.kr328.clash.service.util.intent
import kotlinx.android.synthetic.main.activity_log_viewer.*
import kotlinx.coroutines.*
import kotlinx.coroutines.sync.Mutex
import java.io.File
import kotlin.streams.toList
class LogViewerActivity : BaseActivity() {
private val pauseMutex = Mutex()
private var pollingThread: Thread? = null
private val connection = object : ServiceConnection {
override fun onServiceDisconnected(name: ComponentName?) {
finish()
@@ -49,12 +47,6 @@ class LogViewerActivity : BaseActivity() {
startFileMode(file.toFile())
}
override fun onDestroy() {
super.onDestroy()
pollingThread?.interrupt()
}
override fun onStop() {
super.onStop()
@@ -63,14 +55,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()
}
}
@@ -82,6 +71,7 @@ class LogViewerActivity : BaseActivity() {
mainList.itemAnimator?.removeDuration = 100
stop.setOnClickListener {
unbindService(connection)
stopService(LogcatService::class.intent)
finish()
}
@@ -95,18 +85,17 @@ class LogViewerActivity : BaseActivity() {
launch {
val items = withContext(Dispatchers.IO) {
try {
file.readText()
.split("\n")
.parallelStream()
.map { it.trim() }
.filter { it.isNotEmpty() && !it.startsWith("#")}
.map { it.split(" ", limit = 3) }
.filter { it.size == 3 }
.map { LogEvent(LogEvent.Level.valueOf(it[1]), it[2], it[0].toLong()) }
.toList()
}
catch (e: Exception) {
makeSnackbarException(getString(R.string.open_log_failure), e.message)
file.bufferedReader().useLines { lines ->
lines
.map { it.trim() }
.filter { it.isNotEmpty() && !it.startsWith("#") }
.map { it.split(" ", limit = 3) }
.filter { it.size == 3 }
.map { LogEvent(LogEvent.Level.valueOf(it[1]), it[2], it[0].toLong()) }
.toList()
}
} catch (e: Exception) {
showSnackbarException(getString(R.string.open_log_failure), e.message)
throw CancellationException()
}
@@ -130,7 +119,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,12 @@ import androidx.appcompat.app.AlertDialog
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.LinearLayoutManager
import com.github.kr328.clash.adapter.LogFileAdapter
import com.github.kr328.clash.common.utils.intent
import com.github.kr328.clash.common.utils.startForegroundServiceCompat
import com.github.kr328.clash.core.event.LogEvent
import com.github.kr328.clash.design.common.Category
import com.github.kr328.clash.design.view.CommonUiLayout
import com.github.kr328.clash.model.LogFile
import com.github.kr328.clash.service.util.intent
import com.github.kr328.clash.service.util.startForegroundServiceCompat
import com.github.kr328.clash.utils.format
import com.github.kr328.clash.utils.logsDir
import com.google.android.material.bottomsheet.BottomSheetDialog
@@ -24,12 +25,15 @@ import kotlinx.android.synthetic.main.activity_logs.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.io.FileInputStream
import java.text.SimpleDateFormat
import java.util.*
class LogsActivity : BaseActivity() {
companion object {
const val REQUEST_CODE = 50000
private val LOG_EXPORT_DATE_FORMAT = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.ENGLISH)
private val LOG_EXPORT_TIME_FORMAT = SimpleDateFormat("HH:mm:ss", Locale.ENGLISH)
}
private var lastWriteFile: LogFile? = null
@@ -40,7 +44,7 @@ class LogsActivity : BaseActivity() {
setSupportActionBar(toolbar)
if ( LogcatService.isServiceRunning ) {
if (LogcatService.isServiceRunning) {
startActivity(LogViewerActivity::class.intent)
finish()
return
@@ -67,12 +71,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 +90,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
@@ -101,9 +106,31 @@ class LogsActivity : BaseActivity() {
launch {
withContext(Dispatchers.IO) {
contentResolver.openOutputStream(url)?.use { output ->
FileInputStream(logsDir.resolve(file.fileName)).use { input ->
input.copyTo(output)
contentResolver.openOutputStream(url)?.bufferedWriter()?.use { output ->
output.write("# Logcat on " + LOG_EXPORT_DATE_FORMAT.format(Date(file.date)) + "\n")
logsDir.resolve(file.fileName).bufferedReader().useLines { lines ->
lines.map { it.trim() }
.filter { it.isNotEmpty() && !it.startsWith("#") }
.map { it.split(" ", limit = 3) }
.filter { it.size == 3 }
.map {
LogEvent(
LogEvent.Level.valueOf(it[1]),
it[2],
it[0].toLong()
)
}
.forEach {
output.write(
String.format(
"%s |%s| %s\n",
LOG_EXPORT_TIME_FORMAT.format(Date(it.time)),
it.level.toString(),
it.message
)
)
}
}
}
}
@@ -135,8 +162,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 +209,7 @@ class LogsActivity : BaseActivity() {
ViewGroup.LayoutParams.WRAP_CONTENT
)
}
@ColorInt
val errorColor = TypedValue().run {
theme.resolveAttribute(R.attr.colorError, this, true)
@@ -188,7 +219,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 +229,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 +257,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.dumpCrash()
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,17 +1,21 @@
package com.github.kr328.clash
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import androidx.core.content.getSystemService
import android.text.Html
import androidx.appcompat.app.AlertDialog
import com.github.kr328.clash.dump.LogcatDumper
import com.google.android.material.snackbar.Snackbar
import com.microsoft.appcenter.crashes.Crashes
import com.microsoft.appcenter.crashes.ingestion.models.ErrorAttachmentLog
import kotlinx.android.synthetic.main.activity_support.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
class SupportActivity: BaseActivity() {
override val activityLabel: CharSequence?
get() = getText(R.string.support)
class SupportActivity : BaseActivity() {
class UserRequestTrackException: Exception()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@@ -20,14 +24,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,47 +47,73 @@ 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)
title = getString(R.string.upload_logcat),
summary = getString(R.string.upload_logcat_summary)
) {
onClick {
val data =
ClipData.newPlainText("email", getText(R.string.email_url))
getSystemService<ClipboardManager>()?.setPrimaryClip(data)
Snackbar.make(rootView, getText(R.string.copied), Snackbar.LENGTH_SHORT).show()
AlertDialog.Builder(this@SupportActivity)
.setTitle(R.string.upload_logcat)
.setMessage(R.string.upload_logcat_warn)
.setNegativeButton(R.string.cancel) {_, _ -> }
.setPositiveButton(R.string.ok) {_, _ -> upload() }
.show()
}
}
option(
title = getString(R.string.github_issues),
summary = getString(R.string.github_issues_url)
) {
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)))
)
}
}
}
}
}
private fun upload() {
launch {
withContext(Dispatchers.IO) {
val attachment = ErrorAttachmentLog
.attachmentWithText(LogcatDumper.dumpAll(), "logcat.txt")
Crashes.trackError(UserRequestTrackException(), null, listOf(attachment))
}
withContext(Dispatchers.Main) {
Snackbar.make(rootView, R.string.uploaded, Snackbar.LENGTH_LONG).show()
}
}
}
}
@@ -7,10 +7,11 @@ import android.content.IntentFilter
import android.graphics.drawable.Icon
import android.service.quicksettings.Tile
import android.service.quicksettings.TileService
import com.github.kr328.clash.common.Permissions
import com.github.kr328.clash.common.ids.Intents
import com.github.kr328.clash.remote.RemoteUtils
import com.github.kr328.clash.service.Intents
import com.github.kr328.clash.utils.startClashService
import com.github.kr328.clash.utils.stopClashService
import com.github.kr328.clash.service.util.startClashService
import com.github.kr328.clash.service.util.stopClashService
class TileService : TileService() {
private var currentProfile = ""
@@ -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,37 @@
package com.github.kr328.clash.dump
object LogcatDumper {
fun dumpCrash(): String {
return try {
val process =
Runtime.getRuntime().exec(arrayOf("logcat", "-d", "-s", "Go", "AndroidRuntime", "DEBUG"))
val result = process.inputStream.use {
it.reader().readText()
}
process.waitFor()
result
} catch (e: Exception) {
""
}
}
fun dumpAll(): String {
return try {
val process =
Runtime.getRuntime().exec(arrayOf("logcat", "-d"))
val result = process.inputStream.use {
it.reader().readText()
}
process.waitFor()
result
} catch (e: Exception) {
""
}
}
}
@@ -0,0 +1,215 @@
package com.github.kr328.clash.fragment
import android.app.Activity
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.text.Html
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.ViewGroup.LayoutParams
import android.webkit.MimeTypeMap
import android.webkit.URLUtil
import androidx.fragment.app.Fragment
import com.github.kr328.clash.Constants
import com.github.kr328.clash.R
import com.github.kr328.clash.design.common.TextInput
import com.github.kr328.clash.design.view.CommonUiLayout
import com.github.kr328.clash.service.model.Profile.Type
import com.google.android.material.snackbar.Snackbar
class ProfileEditFragment(
val id: Long,
var name: String,
var uri: Uri,
var interval: Long,
private val type: Type,
private val source: String?
) : Fragment() {
private var root: CommonUiLayout? = null
var isModified = false
companion object {
private const val REQUEST_CODE = 10000
private const val KEY_NAME = "name"
private const val KEY_URL = "url"
private const val KEY_AUTO_UPDATE = "auto_update"
private val TYPE_YAML = MimeTypeMap.getSingleton()
.getMimeTypeFromExtension("yaml") ?: "*/*"
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return CommonUiLayout(requireContext()).apply {
root = this
layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)
build {
tips(icon = requireContext().getDrawable(R.drawable.ic_info)) {
title =
Html.fromHtml(getString(R.string.tips_profile), Html.FROM_HTML_MODE_LEGACY)
}
textInput(
title = getString(R.string.name),
icon = requireContext().getDrawable(R.drawable.ic_label_outline),
hint = getString(R.string.profile_name),
id = KEY_NAME,
content = name
) {
onTextChanged {
name = content.toString()
isModified = true
}
}
textInput(
title = getString(R.string.url),
icon = requireContext().getDrawable(R.drawable.ic_content),
hint = getString(R.string.profile_url),
id = KEY_URL,
content = uri.toString()
) {
onOpenInput {
if (!openUrlProvider())
openDialogInput()
}
onDisplayContent {
it.split("/").last()
}
onTextChanged {
if (!URLUtil.isValidUrl(content.toString())) {
content = ""
Snackbar.make(view, R.string.invalid_url, Snackbar.LENGTH_LONG).show()
return@onTextChanged
}
uri = Uri.parse(content.toString())
isModified = true
}
}
textInput(
title = getString(R.string.auto_update),
icon = requireContext().getDrawable(R.drawable.ic_update),
hint = getString(R.string.more_than_15_minutes),
id = KEY_AUTO_UPDATE,
content = (interval / 1000 / 60).toStringIfNonZero()
) {
onDisplayContent {
val interval = it.toString().toIntOrNull() ?: 0
if (interval <= 0)
getString(R.string.disabled)
else
getString(R.string.format_minutes, interval)
}
onTextChanged {
val s = it.toString()
if (s.isBlank()) {
content = ""
interval = 0
return@onTextChanged
}
val value = s.toIntOrNull()
if (value == null || value < 15) {
content = ""
interval = 0
Snackbar.make(view, R.string.invalid_interval, Snackbar.LENGTH_LONG)
.show()
return@onTextChanged
}
interval = content.toString().toLong() * 1000 * 60
isModified = true
}
if ( type == Type.FILE )
isHidden = true
}
screen.restoreState(savedInstanceState)
if (type == Type.EXTERNAL)
openUrlProvider()
}
}
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
if (requestCode == REQUEST_CODE) {
if (resultCode != Activity.RESULT_OK || data == null)
return
root?.apply {
data.data?.apply {
screen.requireElement<TextInput>(KEY_URL).content = this.toString()
}
data.getStringExtra(Constants.URL_PROVIDER_INTENT_EXTRA_NAME)?.also {
screen.requireElement<TextInput>(KEY_NAME).apply {
if (content.isBlank())
content = it
}
}
}
}
super.onActivityResult(requestCode, resultCode, data)
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
root?.apply {
screen.saveState(outState)
}
}
private fun openUrlProvider(): Boolean {
try {
when (type) {
Type.FILE ->
startActivityForResult(
Intent(Intent.ACTION_GET_CONTENT).setType(TYPE_YAML),
REQUEST_CODE
)
Type.EXTERNAL ->
startActivityForResult(
source?.toIntent() ?: return false,
REQUEST_CODE
)
else -> return false
}
} catch (e: Exception) {
root?.apply {
Snackbar.make(
this,
R.string.start_url_provider_failure,
Snackbar.LENGTH_LONG
).show()
}
}
return true
}
private fun Long.toStringIfNonZero(): String {
return if ( this == 0L ) "" else this.toString()
}
private fun String.toIntent(): Intent? {
return try {
Intent.parseUri(this, 0)
} catch (e: Exception) {
null
}
}
}
@@ -1,5 +1,5 @@
package com.github.kr328.clash.pipeline
import com.github.kr328.clash.service.settings.BaseSettings
import com.github.kr328.clash.common.settings.BaseSettings
data class Pipeline<T>(val input: T, val settings: BaseSettings)
@@ -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,26 @@ import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.ServiceConnection
import android.os.Build
import android.os.Handler
import android.os.IBinder
import android.util.Base64
import androidx.core.content.edit
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.ProcessLifecycleOwner
import com.github.kr328.clash.ApkBrokenActivity
import com.github.kr328.clash.BuildConfig
import com.github.kr328.clash.Constants
import com.github.kr328.clash.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 +48,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 +73,7 @@ object Remote {
if (service != null)
instance = ProfileClient(IProfileService.Stub.asInterface(service))
service?.linkToDeath({
onServiceDisconnected(null)
}, 0)
service?.linkToDeath({ onServiceDisconnected(null) }, 0)
sender = GlobalScope.launch {
while (isActive) {
@@ -83,58 +84,67 @@ object Remote {
}
}
fun init(application: Application) {
val handler = Handler()
private val handler = Handler()
private val observer = ApplicationObserver {
Log.d("Remote Connection State = $it")
ProcessLifecycleOwner.get().lifecycle.addObserver(object : DefaultLifecycleObserver {
override fun onStart(owner: LifecycleOwner) {
handler.removeMessages(0)
val application = Global.application
GlobalScope.launch {
if (!verifyApk(application)) {
application.startActivity(ApkBrokenActivity::class.intent
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK))
return@launch
}
if (it) {
handler.removeMessages(0)
clashConnection = ClashConnection().apply {
application.bindService(
ClashManagerService::class.intent,
this,
Context.BIND_AUTO_CREATE
)
}
GlobalScope.launch {
val valid = withContext(Dispatchers.IO) {
verifyApk(application)
}
profileConnection = ProfileConnection().apply {
application.bindService(
ProfileService::class.intent,
this,
Context.BIND_AUTO_CREATE
)
}
if (!valid) {
application.startActivity(
ApkBrokenActivity::class.intent
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
)
return@launch
}
clashConnection = ClashConnection().apply {
application.bindService(
ClashManagerService::class.intent,
this,
Context.BIND_AUTO_CREATE
)
}
profileConnection = ProfileConnection().apply {
application.bindService(
ProfileService::class.intent,
this,
Context.BIND_AUTO_CREATE
)
}
}
} else {
handler.postDelayed({
clashConnection?.also {
application.unbindService(it)
it.onServiceDisconnected(null)
}
profileConnection?.also {
application.unbindService(it)
it.onServiceDisconnected(null)
}
override fun onStop(owner: LifecycleOwner) {
handler.postDelayed({
clashConnection?.also {
application.unbindService(it)
it.onServiceDisconnected(null)
}
profileConnection?.also {
application.unbindService(it)
it.onServiceDisconnected(null)
}
clashConnection = null
profileConnection = null
}, 5000)
}
})
clashConnection = null
profileConnection = null
}, 5000)
}
}
private suspend fun verifyApk(application: Application): Boolean {
return withContext(Dispatchers.IO) {
fun init(application: Application) {
observer.register(application)
}
private fun verifyApk(application: Application): Boolean {
return try {
val sp = application.getSharedPreferences(
Constants.PREFERENCE_NAME_APP,
Context.MODE_PRIVATE
@@ -142,21 +152,68 @@ object Remote {
val pkg = application.packageManager.getPackageInfo(application.packageName, 0)
if (sp.getLong(Constants.PREFERENCE_KEY_LAST_INSTALL, 0) == pkg.lastUpdateTime)
return@withContext true
return true
val pkgName: String = try {
application::class.java.getMethod(
String(
charArrayOf(
'g',
'e',
't',
'P',
'a',
'c',
'k',
'a',
'g',
'e',
'N',
'a',
'm',
'e'
)
)
).invoke(application)?.toString()
} catch (e: Exception) {
Log.w("getPackageName failure", e)
null
} ?: application.packageName
val packageNameBase64 = Base64
.encodeToString(pkgName.toByteArray(Charsets.UTF_8), Base64.NO_WRAP)
if (packageNameBase64 != BuildConfig.PACKAGE_NAME_BASE64)
return false
val info = application.applicationInfo
val sources =
info.splitSourceDirs ?: arrayOf(info.sourceDir) ?: return@withContext false
info.splitSourceDirs ?: arrayOf(info.sourceDir) ?: return false
for (apk in sources) {
if (ZipFile(apk).entries().asSequence().any { it.name.endsWith("libgojni.so") }) {
sp.edit {
putLong(Constants.PREFERENCE_KEY_LAST_INSTALL, pkg.lastUpdateTime)
}
return@withContext true
val regexNativeLibrary = Regex("lib/(\\S+)/libgojni.so")
val availableAbi = Build.SUPPORTED_ABIS.toSet()
val apkAbi =
sources
.asSequence()
.filter { File(it).exists() }
.flatMap { ZipFile(it).entries().asSequence() }
.mapNotNull { regexNativeLibrary.matchEntire(it.name) }
.mapNotNull { it.groups[1]?.value }
.toSet()
if (availableAbi.intersect(apkAbi).isNotEmpty()) {
sp.edit {
putLong(Constants.PREFERENCE_KEY_LAST_INSTALL, pkg.lastUpdateTime)
}
true
} else {
false
}
return@withContext false
} catch (e: Exception) {
Crashes.trackError(e)
false
}
}
}
@@ -1,40 +1,56 @@
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
import java.lang.Exception
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: Exception) {
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: Exception) {
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>
+7 -3
View File
@@ -16,8 +16,12 @@
android:background="@color/toolbarColor" />
</com.google.android.material.appbar.AppBarLayout>
<com.github.kr328.clash.design.view.CommonUiLayout
android:id="@+id/commonUi"
<ScrollView
android:layout_width="match_parent"
android:layout_height="match_parent" />
android:layout_height="match_parent">
<com.github.kr328.clash.design.view.CommonUiLayout
android:id="@+id/commonUi"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</ScrollView>
</LinearLayout>
+19 -14
View File
@@ -8,9 +8,9 @@
<string name="append_system_dns_summary">自动追加系统 DNS 到 Clash</string>
<string name="application_broken">应用损坏</string>
<string name="application_name">Clash for Android</string>
<string name="auto_update">自动更新 (秒)</string>
<string name="auto_update">自动更新 (分钟)</string>
<string name="behavior">行为</string>
<string name="boot">启动</string>
<string name="restart">重启</string>
<string name="bypass_private_network">绕过私有网络</string>
<string name="bypass_private_network_summary">绕过私有网络地址</string>
<string name="cancel">取消</string>
@@ -39,7 +39,6 @@
<string name="edit">编辑</string>
<string name="edit_profile">编辑配置</string>
<string name="empty_name">空名称</string>
<string name="enable_ipv6">启用 IPv6 支持 (不建议)</string>
<string name="exit_without_save">退出而不保存</string>
<string name="exit_without_save_warning">所有变更将会丢失</string>
<string name="export">导出</string>
@@ -59,11 +58,9 @@
<string name="history">历史</string>
<string name="import_from_file">从文件导入</string>
<string name="import_from_url">从 URL 导入</string>
<string name="seconds">秒</string>
<string name="interface_">界面</string>
<string name="invalid_interval">无效的间隔</string>
<string name="invalid_url">无效的 URL</string>
<string name="ipv6">IPv6</string>
<string name="language">语言</string>
<string name="launch_name">Clash</string>
<string name="log_viewer">日志查看器</string>
@@ -87,8 +84,8 @@
<string name="recently">近期</string>
<string name="refresh">刷新</string>
<string name="reset_provider">重置外部引用</string>
<string name="route_system_traffic">路由系统流量</string>
<string name="routing_via_vpn_service">通过 VpnService 路由所有系统流量</string>
<string name="route_system_traffic">自动路由系统流量</string>
<string name="routing_via_vpn_service">通过 VpnService 自动路由所有系统流量</string>
<string name="rule">规则</string>
<string name="rule_mode">规则模式</string>
<string name="running">运行中</string>
@@ -97,9 +94,9 @@
<string name="show_traffic_summary">在通知中自动刷新流量</string>
<string name="sort_group">代理组排序</string>
<string name="sort_proxy">代理排序</string>
<string name="start_clash_on_system_boot">在系统启动时启动 Clash</string>
<string name="start_on_boot">开机时启动</string>
<string name="start_url_provider_failure">打开 URL 提供商 失败</string>
<string name="allow_clash_auto_restart">允许 Clash 自动重启</string>
<string name="auto_restart">自动重启</string>
<string name="start_url_provider_failure">打开 URL 提供程序失败</string>
<string name="stopped">已停止</string>
<string name="support">支持</string>
<string name="tap_to_start">点此启动</string>
@@ -118,16 +115,24 @@
<string name="reverse">反转</string>
<string name="clash">Clash</string>
<string name="clash_for_android">Clash for Android</string>
<string name="contacts">联系我们</string>
<string name="email">E-Mail</string>
<string name="feedback">反馈</string>
<string name="sources">源代码</string>
<string name="telegram_channel">Telegram 频道</string>
<string name="github_issues">Github Issues</string>
<string name="format_seconds">%d 秒</string>
<string name="copied">已复制</string>
<string name="tips_profile"><![CDATA[仅接受 <strong>Clash 配置文件</strong> <br />其中包含了 <strong>代理, 代理组 和 规则</strong>]]></string>
<string name="application_broken_description"><![CDATA[这通常意味着您使用来自 <strong>Google Play</strong> 的副本, 但是生成该副本的应用未能正确处理 <strong>分包机制</strong><br />这意味着您获取的是 <strong>应用的一部分</strong>]]></string>
<string name="learn_more_about_split_apks">了解更多关于分包机制</string>
<string name="reinstall_from_google_play">重新从 Google Play 安装</string>
<string name="download_from_github_releases">从 Github Release 下载</string>
<string name="missing_vpn_component">系统 VPN 组件缺失</string>
<string name="profile_not_found">配置文件丢失</string>
<string name="more_than_15_minutes">大于15分钟</string>
<string name="loading">载入中</string>
<string name="tips_support"><![CDATA[Clash for Android 是一个<strong>免费开源</strong>的项目<br /> 我们<strong>不提供</strong>任何代理服务<br />请务必<strong>不要</strong>反馈非应用自身引起的问题]]></string>
<string name="donate">捐赠</string>
<string name="clone_profile">复制配置</string>
<string name="upload_logcat">上传日志</string>
<string name="upload_logcat_summary">上传日志以帮助我们侦测问题</string>
<string name="upload_logcat_warn">请注意, 上传的日志可能包含个人敏感信息, 仍要继续吗?</string>
<string name="uploaded">已上传</string>
</resources>
+20 -14
View File
@@ -25,7 +25,6 @@
<string name="recently">Recently</string>
<string name="format_seconds">%d seconds</string>
<string name="format_minutes">%d minutes</string>
<string name="format_hours">%d hours</string>
<string name="format_days">%d days</string>
@@ -46,8 +45,8 @@
<string name="profile_name">Profile Name</string>
<string name="profile_url">Profile URL</string>
<string name="auto_update">Auto Update (Seconds)</string>
<string name="seconds">Seconds</string>
<string name="auto_update">Auto Update (Minutes)</string>
<string name="more_than_15_minutes">More than 15 minutes</string>
<string name="invalid_interval">Invalid Interval</string>
<string name="download_failure">Download Failure</string>
<string name="detail">Detail</string>
@@ -66,12 +65,13 @@
<string name="empty_name">Empty Name</string>
<string name="invalid_url">Invalid URL</string>
<string name="processing">Processing</string>
<string name="loading">Loading</string>
<string name="new_profile">New Profile</string>
<string name="clone_profile">Clone Profile</string>
<string name="edit_profile">Edit Profile</string>
<string name="clash_start_failure">Clash Start Failure</string>
<string name="refresh">Refresh</string>
<string name="copied">Copied</string>
<string name="delay">Delay</string>
<string name="direct">Direct</string>
@@ -107,20 +107,18 @@
<string name="network">Network</string>
<string name="interface_">Interface</string>
<string name="boot">Boot</string>
<string name="start_on_boot">Start on Boot</string>
<string name="start_clash_on_system_boot">Start 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">[email protected]</string>
<string name="github_issues_url" translatable="false">https://github.com/Kr328/ClashForAndroid/issues</string>
<string name="telegram_channel_url" translatable="false">https://t.me/clash_for_android_channel</string>
<string name="tips_profile"><![CDATA[Accept Only <strong>Clash Config</strong> which contains <br /><strong>Proxies, Proxy Groups and Rules</strong>]]></string>
<string name="tips_support"><![CDATA[Clash for Android is <strong>free</strong> and <strong>open source</strong> <br /> It does <strong>NOT</strong> provide any proxy services]]></string>
<string name="application_broken_description"><![CDATA[This is usually because you used a copy from <strong>Google Play</strong>, but the app that generated the copy did not handle <strong>Split Apks</strong> correctly <br />This means you get <strong>part of the app</strong>, not all of it]]></string>
<string name="learn_more_about_split_apks">Learn more about Split Apks</string>
@@ -172,4 +170,12 @@
<string name="github_releases_url" translatable="false">https://github.com/Kr328/ClashForAndroid/releases</string>
<string name="format_proxy_group_title" translatable="false">%s - %s</string>
<string name="missing_vpn_component">Missing VPN Components</string>
<string name="profile_not_found">Profile not found</string>
<string name="upload_logcat">Upload Logcat</string>
<string name="upload_logcat_summary">Upload logcat to help us detect issues</string>
<string name="upload_logcat_warn">Please note that the uploaded logs may contain personally sensitive information, do you still want to continue?</string>
<string name="uploaded">Uploaded</string>
</resources>
+3 -3
View File
@@ -1,11 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<!--suppress DeprecatedClassUsageInspection -->
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
<PreferenceCategory android:title="@string/boot">
<PreferenceCategory android:title="@string/restart">
<SwitchPreference
android:key="start_on_boot"
android:title="@string/start_on_boot"
android:summary="@string/start_clash_on_system_boot" />
android:title="@string/auto_restart"
android:summary="@string/allow_clash_auto_restart" />
</PreferenceCategory>
<PreferenceCategory android:title="@string/notification">
<SwitchPreference
@@ -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
View File
@@ -1,53 +0,0 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
ext {
gBuildToolsVersion = "29.0.3"
gCompileSdkVersion = 29
gMinSdkVersion = 24
gTargetSdkVersion = 29
gVersionCode = 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
View File
@@ -0,0 +1,52 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
val kotlinVersion = "1.3.72"
rootProject.extra.apply {
this["gBuildToolsVersion"] = "29.0.3"
this["gCompileSdkVersion"] = 29
this["gMinSdkVersion"] = 24
this["gTargetSdkVersion"] = 29
this["gVersionCode"] = 10213
this["gVersionName"] = "1.2.13"
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-beta05")
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)
}
-32
View File
@@ -1,32 +0,0 @@
apply plugin: 'java'
apply plugin: 'kotlin'
buildscript {
ext.kotlin_version = '1.3.61'
repositories {
mavenCentral()
}
dependencies {
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
}
}
repositories {
mavenCentral()
}
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
}
compileKotlin {
kotlinOptions {
jvmTarget = "1.8"
}
}
compileTestKotlin {
kotlinOptions {
jvmTarget = "1.8"
}
}
-197
View File
@@ -1,197 +0,0 @@
import org.apache.tools.ant.taskdefs.condition.Os
import org.gradle.api.DefaultTask
import org.gradle.api.GradleException
import org.gradle.api.tasks.TaskAction
import java.io.File
import java.io.FileOutputStream
import java.io.FileReader
import java.io.FileWriter
import java.util.*
import java.util.zip.ZipFile
open class GolangBindTask : DefaultTask() {
companion object {
private val STUB_GO_MOD_CONTENT = """
module github.com/kr328/cfa-bind
require (
github.com/kr328/cfa v0.0.0 // redirect
)
replace github.com/kr328/cfa v0.0.0 => {SOURCE_PATH}
""".trimIndent()
private val STUB_GO_FILE_CONTENT = """
package main
import "github.com/kr328/cfa/bridge"
func main() {}
""".trimIndent()
private val REGEX_REPLACE_TARGET_LOCAL = Regex("=>\\s+\\./")
private val REGEX_REPLACE_SOURCE_VERSION = Regex("v.+\\s+=>")
}
private val javaOutput: File
get() {
return project.buildDir.resolve("intermediates/go_output/generate_java")
}
private val nativeOutput: File
get() {
return project.buildDir.resolve("intermediates/go_output/native_library")
}
private val goBuildPath: File
get() {
return project.buildDir.resolve("intermediates/go_build")
}
private val goPath: File
get() {
return goBuildPath.resolve("go_path")
}
private val goBindPath: File
get() {
return goBuildPath.resolve("go_bind_path")
}
private val sourcePath: File
get() {
return project.file("src/main/golang")
}
private val properties by lazy {
FileReader(project.rootProject.file("local.properties")).use {
Properties().apply { load(it) }
}
}
private val environment = mutableMapOf<String, String>()
init {
onlyIf {
val lastModify = sourcePath.walk()
.filter { it.extension == "go" || it.extension == "mod" }
.map { it.lastModified() }
.max() ?: 0L
return@onlyIf goBuildPath.resolve("bridge.aar").lastModified() < lastModify
}
}
@TaskAction
fun process() {
environment["GOPATH"] = goPath.absolutePath
environment["ANDROID_HOME"] = findAndroidSdkPath().absolutePath
environment["ANDROID_NDK_HOME"] = findAndroidNdkPath().absolutePath
if (Os.isFamily(Os.FAMILY_WINDOWS))
environment["Path"] = System.getenv("Path") + ";" + goPath.resolve("bin")
else
environment["PATH"] = System.getenv("PATH") + ":" + goPath.resolve("bin")
goPath.resolve("src/github.com/kr328").deleteRecursively()
goBindPath.deleteRecursively()
goBindPath.mkdirs()
"go get golang.org/x/mobile/cmd/gomobile".exec()
FileWriter(goBindPath.resolve("go.mod")).use {
it.write(buildStubGoModule(sourcePath))
}
FileWriter(goBindPath.resolve("main.go")).use {
it.write(STUB_GO_FILE_CONTENT)
}
"go mod vendor".exec(goBindPath)
goBindPath.resolve("vendor")
.copyRecursively(goPath.resolve("src"), overwrite = true)
"gomobile init".exec(goBuildPath)
"gomobile bind -target=android \"-gcflags=all=-trimpath=$goPath\" 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")
}
}
-41
View File
@@ -1,41 +0,0 @@
import org.gradle.api.DefaultTask
import org.gradle.api.GradleException
import org.gradle.api.tasks.TaskAction
import java.io.File
import java.io.FileOutputStream
import java.net.HttpURLConnection
import java.net.URL
open class MMDBDowloadTask : DefaultTask() {
companion object {
const val URL = "https://github.com/Dreamacro/maxmind-geoip/releases/latest/download/Country.mmdb"
}
var output: String = ""
@TaskAction
fun exec() {
val file = File(output).apply {
parentFile?.mkdirs()
}
try {
(URL(URL).openConnection() as HttpURLConnection).apply {
instanceFollowRedirects = true
connect()
require(responseCode / 100 == 2)
FileOutputStream(file).use {
inputStream.copyTo(it)
}
disconnect()
}
}
catch (e: Throwable) {
e.printStackTrace()
throw GradleException("Download failure", e)
}
}
}
+63
View File
@@ -0,0 +1,63 @@
plugins {
id("com.android.library")
id("kotlin-android")
id("kotlin-android-extensions")
}
val rootExtra = rootProject.extra
val gCompileSdkVersion: Int by rootExtra
val gBuildToolsVersion: String by rootExtra
val gMinSdkVersion: Int by rootExtra
val gTargetSdkVersion: Int by rootExtra
val gVersionCode: Int by rootExtra
val gVersionName: String by rootExtra
val gKotlinVersion: String by rootExtra
val gKotlinCoroutineVersion: String by rootExtra
val gAndroidKtxVersion: String by rootExtra
val gKotlinSerializationVersion: String by rootExtra
android {
compileSdkVersion(gCompileSdkVersion)
buildToolsVersion(gBuildToolsVersion)
defaultConfig {
minSdkVersion(gMinSdkVersion)
targetSdkVersion(gTargetSdkVersion)
versionCode = gVersionCode
versionName = gVersionName
consumerProguardFiles("consumer-rules.pro")
}
buildTypes {
maybeCreate("release").apply {
isMinifyEnabled = false
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = "1.8"
}
}
dependencies {
implementation("androidx.core:core-ktx:$gAndroidKtxVersion")
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8:$gKotlinVersion")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$gKotlinCoroutineVersion")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-runtime:$gKotlinSerializationVersion")
}
repositories {
mavenCentral()
}
View File
Whitespace-only changes.
+21
View File
@@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
+9
View File
@@ -0,0 +1,9 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.github.kr328.clash.common">
<permission
android:name="${applicationId}.permission.RECEIVE_BROADCASTS"
android:label="@string/receive_clash_broadcasts"
android:description="@string/receive_broadcasts_of_clash"
android:protectionLevel="privileged|signature" />
<uses-permission android:name="${applicationId}.permission.RECEIVE_BROADCASTS" />
</manifest>
@@ -1,4 +1,4 @@
package com.github.kr328.clash.core
package com.github.kr328.clash.common
object Constants {
const val TAG = "ClashForAndroid"
@@ -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)
}
}
@@ -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
View File
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="receive_clash_broadcasts">Receive Clash Broadcasts</string>
<string name="receive_broadcasts_of_clash">Receive broadcasts of clash services</string>
</resources>
-66
View File
@@ -1,66 +0,0 @@
apply plugin: 'com.android.library'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
apply plugin: 'kotlinx-serialization'
android {
compileSdkVersion gCompileSdkVersion
buildToolsVersion gBuildToolsVersion
defaultConfig {
minSdkVersion gMinSdkVersion
targetSdkVersion gTargetSdkVersion
versionCode gVersionCode
versionName gVersionName
consumerProguardFiles 'consumer-rules.pro'
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
sourceSets {
main {
assets.srcDirs += ["$buildDir/intermediates/dynamic_assets"]
jniLibs.srcDirs += ["$buildDir/intermediates/go_output/native_library"]
java.srcDirs += ["$buildDir/intermediates/go_output/generate_java"]
}
}
compileOptions {
sourceCompatibility = 1.8
targetCompatibility = 1.8
}
kotlinOptions {
jvmTarget = "1.8"
}
}
dependencies {
implementation "androidx.core:core-ktx:$gAndroidKtxVersion"
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$gKotlinVersion"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$gKotlinCoroutineVersion"
implementation "org.jetbrains.kotlinx:kotlinx-serialization-runtime:$gKotlinSerializationVersion"
}
afterEvaluate {
def ds = tasks.register("downloadMMDB", MMDBDowloadTask.class) {
onlyIf {
System.currentTimeMillis() - file("$buildDir/intermediates/dynamic_assets/Country.mmdb").lastModified() > 7 * 24 * 3600 * 1000L
}
output = "$buildDir/intermediates/dynamic_assets/Country.mmdb"
}
def gs = tasks.register("golangBind", GolangBindTask.class)
preBuild.dependsOn(ds, gs)
}
repositories {
mavenCentral()
}
+82
View File
@@ -0,0 +1,82 @@
apply(from = "clash.gradle.kts")
plugins {
id("com.android.library")
id("kotlin-android")
id("kotlin-android-extensions")
id("kotlinx-serialization")
}
val rootExtra = rootProject.extra
val gCompileSdkVersion: Int by rootExtra
val gBuildToolsVersion: String by rootExtra
val gMinSdkVersion: Int by rootExtra
val gTargetSdkVersion: Int by rootExtra
val gVersionCode: Int by rootExtra
val gVersionName: String by rootExtra
val gKotlinVersion: String by rootExtra
val gKotlinCoroutineVersion: String by rootExtra
val gKotlinSerializationVersion: String by rootExtra
val gAndroidKtxVersion: String by rootExtra
val clashCoreOutput = buildDir.resolve("extraSources")
android {
compileSdkVersion(gCompileSdkVersion)
buildToolsVersion(gBuildToolsVersion)
defaultConfig {
minSdkVersion(gMinSdkVersion)
targetSdkVersion(gTargetSdkVersion)
versionCode = gVersionCode
versionName = gVersionName
consumerProguardFiles("consumer-rules.pro")
}
buildTypes {
maybeCreate("release").apply {
isMinifyEnabled = false
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
}
}
sourceSets {
maybeCreate("main").apply {
assets.srcDir(clashCoreOutput.resolve("assets"))
jniLibs.srcDir(clashCoreOutput.resolve("jniLibs"))
java.srcDir(clashCoreOutput.resolve("classes"))
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = "1.8"
}
}
dependencies {
implementation(project(":common"))
implementation("androidx.core:core-ktx:$gAndroidKtxVersion")
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8:$gKotlinVersion")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$gKotlinCoroutineVersion")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-runtime:$gKotlinSerializationVersion")
}
repositories {
mavenCentral()
}
afterEvaluate {
tasks["clean"].dependsOn(tasks["resetGolangPathMode"])
tasks["preBuild"].dependsOn(tasks["extractSources"], tasks["downloadGeoipDatabase"])
}
+218
View File
@@ -0,0 +1,218 @@
import org.apache.tools.ant.taskdefs.condition.Os
import java.io.*
import java.util.*
import java.net.*
object Constants {
const val GEOIP_DATABASE_URL = "https://github.com/Dreamacro/maxmind-geoip/releases/latest/download/Country.mmdb"
const val GEOIP_INVALID_INTERVAL = 1000L * 60 * 60 * 24 * 7
const val SOURCE_PATH = "src/main/golang"
const val OUTPUT_PATH = "extraSources"
const val GOLANG_BASE = "intermediates/golang"
const val GOLANG_PATH = "$GOLANG_BASE/path"
const val GOLANG_BIND = "$GOLANG_BASE/bind"
const val GOLANG_BINARY = "$GOLANG_PATH/bin"
const val GOLANG_OUTPUT = "$GOLANG_BASE/bridge.aar"
const val GOLANG_OUTPUT_SOURCES = "$GOLANG_BASE/bridge-sources.jar"
val STUB_GO_FILE_CONTENT = """
package main
import "github.com/kr328/cfa/bridge"
func main() {}
""".trimIndent()
val STUB_GO_MOD_CONTENT = """
module github.com/kr328/cfa-bind
require github.com/kr328/cfa v0.0.0 // redirect
replace github.com/kr328/cfa => {SOURCE_PATH}
""".trimIndent()
val REGEX_REPLACE = Regex("replace\\s+(\\S+)\\s+(\\S*)\\s*=>\\s*(\\S+)\\s*(\\S*)\\s*")
val REGEX_JNI = Regex("^jni/")
}
fun generateGolangBuildEnvironment(vararg pathAppend: String): Map<String, String> {
val environment = TreeMap<String, String>(String.CASE_INSENSITIVE_ORDER).apply { putAll(System.getenv()) }
val properties = Properties().apply { load(rootProject.file("local.properties").inputStream()) }
val sdkPath = properties.getProperty("sdk.dir")
?: throw GradleScriptException("sdk.dir not found", FileNotFoundException())
val ndkPath = properties.getProperty("ndk.dir")
?: throw GradleScriptException("ndk.dir not found", FileNotFoundException())
val pathSeparator = if ( Os.isFamily(Os.FAMILY_WINDOWS) ) ";" else ":"
environment["GOPATH"] = listOf(buildDir.resolve(Constants.GOLANG_PATH).absolutePath, *pathAppend)
.joinToString(separator = pathSeparator)
environment["ANDROID_HOME"] = sdkPath
environment["ANDROID_NDK_HOME"] = ndkPath
environment["PATH"] += "$pathSeparator${buildDir.resolve(Constants.GOLANG_BINARY)}"
return environment
}
fun generateGolangModule(): String {
val moduleFile = file(Constants.SOURCE_PATH).resolve("go.mod")
val replaces = moduleFile
.readLines()
.asSequence()
.map { line -> Constants.REGEX_REPLACE.matchEntire(line) }
.filterNotNull()
.map { match ->
val source = match.groupValues[1].trim()
val sVersion = match.groupValues[2].trim()
val target = match.groupValues[3].trim()
val tVersion = match.groupValues[4].trim()
val resolvedTarget = if ( target.startsWith("./") )
moduleFile.parentFile!!.resolve(target).canonicalPath
else
target
"replace $source $sVersion => $resolvedTarget $tVersion"
}.joinToString(separator = "\n")
return Constants.STUB_GO_MOD_CONTENT
.replace("{SOURCE_PATH}", file(Constants.SOURCE_PATH).absolutePath) + replaces
}
fun String.exec(pwd: File = buildDir, env: Map<String, String> = System.getenv()): String {
val process = ProcessBuilder().run {
if ( Os.isFamily(Os.FAMILY_WINDOWS) )
command("cmd.exe", "/c", this@exec)
else
command("bash", "-c", this@exec)
environment().putAll(env)
directory(pwd)
redirectErrorStream(true)
start()
}
val outputStream = ByteArrayOutputStream()
process.inputStream.copyTo(outputStream)
if ( process.waitFor() != 0 ) {
println(outputStream.toString("utf-8"))
throw GradleScriptException("Exec $this failure", IOException())
}
return outputStream.toString("utf-8")
}
task("generateClashBindSources") {
onlyIf {
val lastModified = file(Constants.SOURCE_PATH).walk()
.filter { it.extension == "go" || it.extension == "mod" }
.map { it.lastModified() }
.max() ?: 0L
return@onlyIf lastModified > buildDir.resolve(Constants.GOLANG_OUTPUT).lastModified()
}
doFirst {
buildDir.resolve(Constants.GOLANG_BIND).apply {
deleteRecursively()
mkdirs()
}
}
doLast {
val environment = generateGolangBuildEnvironment()
val bind = buildDir.resolve(Constants.GOLANG_BIND).apply {
resolve("main.go").writeText(Constants.STUB_GO_FILE_CONTENT)
resolve("go.mod").writeText(generateGolangModule())
}
"go mod vendor".exec(pwd = bind, env = environment)
buildDir.resolve(Constants.GOLANG_BIND).apply {
resolve("vendor").renameTo(resolve("src"))
resolve("go.mod").delete()
resolve("main.go").delete()
resolve("go.sum").delete()
}
}
}
task("assembleClashCore") {
dependsOn(tasks["generateClashBindSources"])
onlyIf {
!tasks["generateClashBindSources"].state.skipped
}
doFirst {
val environment = generateGolangBuildEnvironment()
"go get golang.org/x/mobile/cmd/gomobile".exec(env = environment)
}
doLast {
val bind = buildDir.resolve(Constants.GOLANG_BIND)
val environment = generateGolangBuildEnvironment(bind.absolutePath)
"gomobile init".exec(pwd = bind, env = environment)
"gomobile bind -target=android -trimpath github.com/kr328/cfa/bridge"
.exec(pwd = buildDir.resolve(Constants.GOLANG_BASE), env = environment)
}
}
task("extractSources", type = Copy::class) {
dependsOn(tasks["assembleClashCore"])
from(zipTree(buildDir.resolve(Constants.GOLANG_OUTPUT))) {
include("**/*.so")
eachFile {
path = path.replace(Constants.REGEX_JNI, "jniLibs/")
}
}
from(zipTree(buildDir.resolve(Constants.GOLANG_OUTPUT_SOURCES))) {
include("**/*.java")
into("classes")
}
destinationDir = buildDir.resolve(Constants.OUTPUT_PATH)
}
task("downloadGeoipDatabase") {
dependsOn(tasks["extractSources"])
onlyIf {
val file = buildDir.resolve(Constants.OUTPUT_PATH).resolve("assets/Country.mmdb")
System.currentTimeMillis() - file.lastModified() > Constants.GEOIP_INVALID_INTERVAL
}
doLast {
val assets = buildDir.resolve(Constants.OUTPUT_PATH).resolve("assets")
assets.mkdirs()
URL(Constants.GEOIP_DATABASE_URL).openConnection().getInputStream().use { input ->
FileOutputStream(assets.resolve("Country.mmdb")).use { output ->
input.copyTo(output)
}
}
}
}
task("resetGolangPathMode", type = Exec::class) {
onlyIf {
!Os.isFamily(Os.FAMILY_WINDOWS)
}
commandLine("chmod", "-R", "777", buildDir.resolve(Constants.GOLANG_PATH))
isIgnoreExitValue = true
}
+42 -9
View File
@@ -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
}
+8 -8
View File
@@ -3,40 +3,40 @@ package bridge
import (
"strings"
"github.com/kr328/cfa/profile"
"github.com/kr328/cfa/config"
)
func ResetDnsAppend(dns string) {
if len(dns) == 0 {
profile.NameServersAppend = make([]string, 0)
config.NameServersAppend = make([]string, 0)
} else {
profile.NameServersAppend = strings.Split(dns, ",")
config.NameServersAppend = strings.Split(dns, ",")
}
}
func SetDnsOverrideEnabled(enabled bool) {
if enabled {
profile.DnsPatch = profile.OptionalDnsPatch
config.DnsPatch = config.OptionalDnsPatch
} else {
profile.DnsPatch = nil
config.DnsPatch = nil
}
}
func LoadProfileFile(path, baseDir string, callback DoneCallback) {
go func() {
call(profile.LoadFromFile(path, baseDir), callback)
call(config.LoadFromFile(path, baseDir), callback)
}()
}
func DownloadProfileAndCheck(url, output, baseDir string, callback DoneCallback) {
go func() {
call(profile.DownloadAndCheck(url, output, baseDir), callback)
call(config.PullRemote(url, output, baseDir), callback)
}()
}
func ReadProfileAndCheck(fd int, output, baseDir string, callback DoneCallback) {
go func() {
call(profile.ReadAndCheck(fd, output, baseDir), callback)
call(config.PullLocal(fd, output, baseDir), callback)
}()
}
+35 -56
View File
@@ -53,26 +53,18 @@ func StartUrlTest(group string, callback DoneCallback) {
p := tunnel.Proxies()[group]
pa, ok := p.(*outbound.Proxy)
pi, ok := p.(*outbound.Proxy)
if !ok {
return
}
var providers []provider.ProxyProvider
switch group := pa.ProxyAdapter.(type) {
case *outboundgroup.Fallback:
providers = group.GetProviders()
case *outboundgroup.URLTest:
providers = group.GetProviders()
case *outboundgroup.LoadBalance:
providers = group.GetProviders()
case *outboundgroup.Selector:
providers = group.GetProviders()
default:
group, ok := pi.ProxyAdapter.(outboundgroup.ProxyGroup)
if !ok {
return
}
providers := group.GetProxyProviders()
wg := &sync.WaitGroup{}
wg.Add(len(providers))
@@ -91,78 +83,65 @@ func QueryAllProxyGroups(collection ProxyGroupCollection) {
ps := tunnel.Proxies()
for _, p := range ps {
pa, ok := p.(*outbound.Proxy)
pi, ok := p.(*outbound.Proxy)
if !ok {
continue
}
switch group := pa.ProxyAdapter.(type) {
case *outboundgroup.Fallback:
collection.Add(
&ProxyGroupItem{
Name: group.Name(),
Type: group.Type().String(),
Current: group.Now(),
Delay: int(p.LastDelay()),
providers: group.GetProviders(),
},
)
case *outboundgroup.URLTest:
collection.Add(
&ProxyGroupItem{
Name: group.Name(),
Type: group.Type().String(),
Current: group.Now(),
Delay: int(p.LastDelay()),
providers: group.GetProviders(),
},
)
case *outboundgroup.LoadBalance:
collection.Add(
&ProxyGroupItem{
Name: group.Name(),
Type: group.Type().String(),
Current: "",
Delay: int(p.LastDelay()),
providers: group.GetProviders(),
},
)
case *outboundgroup.Selector:
collection.Add(
&ProxyGroupItem{
Name: group.Name(),
Type: group.Type().String(),
Current: group.Now(),
Delay: int(p.LastDelay()),
providers: group.GetProviders(),
},
)
default:
group, ok := pi.ProxyAdapter.(outboundgroup.ProxyGroup)
if !ok {
continue
}
collection.Add(&ProxyGroupItem{
Name: group.Name(),
Type: group.Type().String(),
Current: group.Now(),
Delay: 0,
providers: group.GetProxyProviders(),
})
}
}
func SetSelectedProxy(name, proxy string) bool {
p := tunnel.Proxies()[name]
if p == nil {
log.Infoln("Set %s: Not such proxy group", name)
return false
}
pb, ok := p.(*outbound.Proxy)
if !ok {
log.Infoln("Set %s: Not a proxy object", name)
return false
}
selector, ok := pb.ProxyAdapter.(*outboundgroup.Selector)
if !ok {
log.Infoln("Set %s: Not a selector group", name)
return false
}
selected := selector.Now()
if selected == proxy {
log.Infoln("Set " + name + " -> " + proxy)
return true
}
if err := selector.Set(proxy); err != nil {
log.Infoln("Set %s: %s", name, err.Error())
return false
}
for _, conn := range tunnel.DefaultManager.Snapshot().Connections {
for _, p := range conn.Chain() {
if p == name {
_ = conn.Close()
break
}
}
}
log.Infoln("Set " + name + " -> " + proxy)
return true
+7 -34
View File
@@ -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
},
}
}
+8 -6
View File
@@ -20,17 +20,19 @@ func init() {
dialer.ListenConfigHook = onNewListenConfig
}
func onNewDialer(dialer *net.Dialer) {
func onNewDialer(dialer *net.Dialer) error {
dialer.Control = onNewSocket
return nil
}
func onNewListenConfig(listen *net.ListenConfig) {
func onNewListenConfig(listen *net.ListenConfig) error {
listen.Control = onNewSocket
return nil
}
func onNewSocket(network, address string, c syscall.RawConn) error {
func onNewSocket(_, _ string, c syscall.RawConn) error {
if cb := callback; cb != nil {
c.Control(func(fd uintptr) {
_ = c.Control(func(fd uintptr) {
cb.OnCreateSocket(int(fd))
})
}
@@ -38,10 +40,10 @@ func onNewSocket(network, address string, c syscall.RawConn) error {
return nil
}
func StartTunDevice(fd, mtu int, dns string, cb TunCallback) error {
func StartTunDevice(fd, mtu int, gateway, mirror, dns string, cb TunCallback) error {
callback = cb
return tun.StartTunDevice(fd, mtu, dns)
return tun.StartTunDevice(fd, mtu, gateway, mirror, dns)
}
func StopTunDevice() {
@@ -1,11 +1,13 @@
package profile
package config
import (
"context"
"errors"
"github.com/kr328/cfa/utils"
"io/ioutil"
"net"
"net/http"
"net/url"
"os"
"syscall"
@@ -28,61 +30,73 @@ var client = &http.Client{
client, server := net.Pipe()
tunnel.Add(inbound.NewSocket(socks5.ParseAddr(address), server, constant.HTTP, constant.TCP))
go func() {
if ctx == nil || ctx.Done() == nil {
return
}
<-ctx.Done()
client.Close()
server.Close()
}()
tunnel.Add(inbound.NewSocket(socks5.ParseAddr(address), server, constant.HTTP))
return client, nil
},
},
}
func DownloadAndCheck(url, output, baseDir string) error {
request, err := http.NewRequest("GET", url, nil)
func fetchRemote(sUrl string) ([]byte, error) {
uri, err := url.Parse(sUrl)
if err != nil {
return err
return nil, err
}
request, err := http.NewRequest("GET", uri.String(), nil)
if err != nil {
return nil, err
}
request.Header.Set("User-Agent", "ClashForAndroid/"+ApplicationVersion)
if user := uri.User; user != nil {
password, _ := user.Password()
request.SetBasicAuth(user.Username(), password)
}
response, err := client.Do(request)
if err != nil {
return err
return nil, err
}
defer response.Body.Close()
defer utils.CloseSilent(response.Body)
data, err := ioutil.ReadAll(response.Body)
if err != nil {
return err
return nil, err
}
return SaveAndCheck(data, output, baseDir)
return data, nil
}
func ReadAndCheck(fd int, output, baseDir string) error {
syscall.SetNonblock(fd, true)
func fetchLocal(fd int) ([]byte, error) {
_ = syscall.SetNonblock(fd, true)
file := os.NewFile(uintptr(fd), "/dev/null")
defer file.Close()
defer utils.CloseSilent(file)
data, err := ioutil.ReadAll(file)
return ioutil.ReadAll(file)
}
func PullRemote(url, output, baseDir string) error {
data, err := fetchRemote(url)
if err != nil {
return err
}
return SaveAndCheck(data, output, baseDir)
return save(data, output, baseDir)
}
func SaveAndCheck(data []byte, output, baseDir string) error {
func PullLocal(fd int, output, baseDir string) error {
data, err := fetchLocal(fd)
if err != nil {
return err
}
return save(data, output, baseDir)
}
func save(data []byte, output, baseDir string) error {
_, err := parseConfig(data, baseDir)
if err != nil {
return err
+75
View File
@@ -0,0 +1,75 @@
package config
import (
"errors"
"io/ioutil"
"github.com/Dreamacro/clash/config"
"github.com/Dreamacro/clash/hub/executor"
"github.com/Dreamacro/clash/log"
"github.com/kr328/cfa/tun"
)
// LoadDefault - load default configure
func LoadDefault() {
DnsPatch = nil
NameServersAppend = make([]string, 0)
defaultC, err := config.Parse([]byte{})
if err != nil {
log.Warnln("Load Default Failure " + err.Error())
return
}
executor.ApplyConfig(defaultC, true)
tun.InitialResolver()
}
// LoadFromFile - load file
func LoadFromFile(path, baseDir string) error {
data, err := ioutil.ReadFile(path)
if err != nil {
return err
}
cfg, err := parseConfig(data, baseDir)
if err != nil {
return err
}
for _, ns := range cfg.DNS.NameServer {
log.Infoln("DNS: %s", ns.Addr)
}
executor.ApplyConfig(cfg, true)
tun.InitialResolver()
log.Infoln("Profile " + path + " loaded")
return nil
}
func parseConfig(data []byte, baseDir string) (*config.Config, error) {
raw, err := config.UnmarshalRawConfig(data)
if err != nil {
return nil, err
}
patchRawConfig(raw)
if len(raw.Proxy) == 0 && len(raw.ProxyProvider) == 0 &&
len(raw.ProxyOld) == 0 && len(raw.ProxyProviderOld) == 0 {
return nil, errors.New("Empty Profile")
}
cfg, err := config.ParseRawConfig(raw, baseDir)
if err != nil {
return nil, err
}
patchConfig(cfg)
return cfg, nil
}
Loaded 100 of 207 files, more files were not shown because too many files have changed in this diff. Show more