Compare commits

..
216 Commits
Author SHA1 Message Date
Kr328 8506303693 Chore: bump version 2022-11-17 00:44:11 +08:00
Kr328 8758c561d2 Chore: update PRIVACY_POLICY.md 2022-11-17 00:43:06 +08:00
Kr328 04efed17af Fix: decrease mtu 2022-11-17 00:31:23 +08:00
EdwardWu 769b80b079 Chore: Translation: Update zh-rTW strings (#1537)
* Chore: Translation: Update zh-rTW strings[1/2]

The words usage of the translation strings in the past are in the style of China rather than Taiwan.
I'm Taiwanese, who works on Android and Translation projects.

Signed-off-by: Edward Wu <[email protected]>

* Chore: Translation: Update zh-rTW strings[2/2]

* With latest strings and merge @pan93412 suggest.

Signed-off-by: Edward Wu <[email protected]>
2022-07-04 10:30:43 +08:00
Kr328 628eb2809e Chore: bump version 2022-07-02 11:07:30 +08:00
Kr328 9f76a377ab Improve: increase tun mtu 2022-07-02 11:02:07 +08:00
Kr328 26d3decd06 Chore: remove temp debug code 2022-07-02 11:01:00 +08:00
Kr328 7b7ee554e3 Feature: add option to enable sni sniff 2022-07-02 10:59:56 +08:00
Kr328 eb7cd8923f Chore: bump version 2022-07-01 13:51:13 +08:00
Kr328 c2c09f52cc Feature: enable sniff & disable tun for premium 2022-07-01 13:50:32 +08:00
Kr328 a56b372a39 Chore: update clash core 2022-07-01 13:49:29 +08:00
Kr328 c3bab32b83 Chore: update dependencies & remove appcenter tracker 2022-07-01 12:07:45 +08:00
GrilledPear c18d78db85 Chore: Add <version> to match output apks (#1509) 2022-07-01 11:15:28 +08:00
apevogoci 178e6dcb07 Improve: Try tcp6&udp6 files first when search UID for connection (#1514) 2022-07-01 11:14:50 +08:00
kr328 a405372f6a Fix: operate clipboard on main thread 2022-05-16 20:00:48 +08:00
kr328 b472580ca6 Improve: add label to tun service 2022-05-16 19:53:32 +08:00
kr328 5a587167ae Improve: add workgroup to default fake-ip-filter 2022-05-16 19:47:55 +08:00
kr328 5149caf2b6 Improve: add ifly ime api to http proxy black list 2022-05-16 19:45:13 +08:00
kr328 ffa559a571 Chore: bump version 2022-05-13 13:50:39 +08:00
kr328 5c3ab5470a Chore: update dependencies 2022-05-12 20:15:21 +08:00
kr328 e6859ff56d Fix: use package if only single app in sharedUid group 2022-05-12 20:15:00 +08:00
kr328 f16b0048f3 Chore: update dependencies 2022-04-25 23:13:05 +08:00
kr328 17b3a0d0ed Chore: update clash core 2022-04-25 23:12:54 +08:00
kr328 1e9f38f2bb Chore: bump version 2022-04-25 12:25:50 +08:00
Kr328 ef8f9e5738 Chore: create FUNDING.yml 2022-04-25 12:12:59 +08:00
kr328 bf59ead3b2 Feature: close all connections on reset 2022-04-20 00:39:46 +08:00
kr328 5dbd94e42c Chore: update issue template 2022-04-15 14:31:10 +08:00
kr328 b92fc860eb Chore: bump version 2022-04-14 14:13:56 +08:00
kr328 20bdf171ee Feature: add zero-copy support 2022-04-14 14:13:38 +08:00
kr328 5a6ca2ce12 Chore: bump version 2022-04-11 20:59:27 +08:00
kr328 636941da70 Fix: add *.jd.com to system proxy blacklist 2022-04-11 20:49:00 +08:00
kr328 5bc3975591 Fix: fix traffic format 2022-04-11 20:49:00 +08:00
kr328 5e54e4561c Chore: minor default dns config changes 2022-04-11 20:49:00 +08:00
kr328 a7b560b576 Fix: LoadDefault should not apply dns patch 2022-04-11 20:49:00 +08:00
kr328 5f9d4c1572 Chore: minor default dns changes 2022-04-11 20:49:00 +08:00
kr328 a83ecb8b22 Chore: update clash core 2022-04-11 20:49:00 +08:00
Goooler 1cc88cc04f Chore: cache go on CI (#1440) 2022-03-22 20:36:31 +08:00
Goooler 92bce6c2c6 Chore: optimize CI (#1414)
* Bump checkout action to v3

* Bump setup-java action to v3 & Java 17

* Bump setup-go action to v3

* Use gradle-build-action

* Go 1.18

* Insert a new line
2022-03-22 18:41:06 +08:00
kr328 ab96790c73 Chore: update dependencies 2022-03-22 13:48:02 +08:00
kr328 d5618321e9 Chore: update clash core 2022-03-22 13:47:11 +08:00
Kr328 4b8fc6ae9a Chore: update dependencies 2022-02-21 19:48:35 +08:00
人工知能 152e24748b Chore: update zh-rTW translate (#1386)
* Update strings.xml

* Update strings.xml

* Update strings.xml

* Update strings.xml

* Update strings.xml
2022-02-21 19:40:06 +08:00
Kr328 2efdf21130 Chore: update clash core 2022-02-21 19:24:48 +08:00
Kr328 e99c2b64b0 Fix: add mark for issues template 2022-02-21 11:22:16 +08:00
Kr328 3915d9549e Chore: ignore .github on build 2022-02-21 11:21:00 +08:00
Kr328 23077537f9 Fix: fix issue template 2022-02-21 11:19:43 +08:00
Kr328 de0cbd6b29 Chore: refactor issue template 2022-02-21 11:15:41 +08:00
Kr328 d0efa0690a Chore: fix bug report form 2022-02-21 01:50:30 +08:00
Kr328 5b6d04d558 Chore: replace bug report with issues forms 2022-02-21 01:46:07 +08:00
Kr328 791794f59d Chore: bump version 2022-01-15 00:15:02 +08:00
Kr328 6a6465ae5b Chore: update dependencies 2022-01-15 00:13:05 +08:00
Kr328 056a78c7c4 Improve: update default fake filter list 2022-01-15 00:07:52 +08:00
YeyeBBC 11b0025faf Slightly adjust QuickSettings Tile icon and App icon, making the cat more visually centred rather than geometrically centred. (#1341) 2022-01-15 00:02:25 +08:00
Kr328 3009b29e0c Chore: update dependencies 2022-01-14 23:59:14 +08:00
Kr328 5d52d970a3 Chore: clean code 2022-01-14 23:59:00 +08:00
Kr328 aef0b84275 Fix: disable tproxy & redir by default 2022-01-14 23:58:46 +08:00
Kr328 12c704d89c Chore: reformat build.gradle.kts 2022-01-14 23:40:41 +08:00
Kr328 38e070a565 Chore: update clash core 2022-01-14 23:39:33 +08:00
Kr328 c7d33c684b Chore: clean build gradle 2022-01-14 23:20:01 +08:00
Kr328 b2661d31fd Chore: update dependencies 2022-01-14 23:10:16 +08:00
kr328 53aa718674 Improve: patch all dns to core 2021-11-21 19:56:06 +08:00
okhowang c173114368 Fix: crash with app icon (#1260) 2021-11-21 17:41:21 +08:00
kr328 7006cb516c Chore: update clash core 2021-11-21 17:17:33 +08:00
kr328 7809c30052 Refactor: refactor network observer 2021-11-21 16:55:55 +08:00
kr328 7e46d75159 Chore: update default fake-ip-filter 2021-11-21 13:54:31 +08:00
kr328 87f96b9ec7 Chore: bump version 2021-11-21 13:38:09 +08:00
kr328 3920f338f6 Chore: filter out premium build if unavailable 2021-11-21 13:38:01 +08:00
kr328 2565231867 Chore: remove unused ErrInvalidType 2021-11-21 13:31:34 +08:00
kr328 b7af3f7341 Fix: fix update profile dial 2021-11-21 13:31:25 +08:00
kr328 d93a7af534 Fix: fix network observe 2021-11-21 13:01:56 +08:00
kr328 1dcc8307ee Chore: remove unused patch proxy group 2021-11-21 13:01:44 +08:00
kr328 9936a9469d Fix: fix tun local addr 2021-11-21 13:01:27 +08:00
kr328 0e245b8936 Fix: downgrade kotlin version 2021-11-16 21:04:18 +08:00
kr328 226c11dda3 Chore: bump version 2021-11-16 20:29:46 +08:00
kr328 8330054a81 Chore: update dependencies 2021-11-16 20:29:36 +08:00
kr328 c7cb1dab38 Improve: disable network observer on screen off 2021-11-16 20:23:54 +08:00
kr328 a3686c16d4 Chore: update dependencies 2021-11-16 20:03:54 +08:00
kr328 b2a6a6a1ac Chore: update tun2socket 2021-11-16 19:59:34 +08:00
kr328 e3e9a8ccc3 Fix: fix tunnel dialer 2021-11-16 19:59:22 +08:00
kr328 7058c7a4cd Chore: update dependencies 2021-11-16 19:58:50 +08:00
kr328 2591de1c1d Chore: bump version 2021-11-14 21:09:02 +08:00
kr328 d20f19c08f Fix: fix network observer 2021-11-14 20:56:57 +08:00
kr328 a5ba4b4d23 Chore: update clash core 2021-11-14 20:55:35 +08:00
kr328 094815a098 Improve: remove loopback block & add allow bypass 2021-11-14 20:42:52 +08:00
kr328 5224fa656c Chore: clean compat code 2021-11-14 20:33:37 +08:00
kr328 73550164a8 Refactor: refactor network observer 2021-11-14 20:33:25 +08:00
kr328 48a9ab313e Chore: mark foss is default 2021-11-14 19:45:33 +08:00
kr328 25936c8361 Fix: fix dns fake-ip patch 2021-11-14 19:40:02 +08:00
kr328 3397cdd95f Chore: update dependencies 2021-11-14 19:17:06 +08:00
kr328 69f8ff2688 Chore: update clash core 2021-11-14 15:45:31 +08:00
kr328 41ab062103 Refactor: migrate to tun2socket 2021-11-14 14:50:11 +08:00
Kr328 c78431a0fb Chore: bump version 2021-10-15 11:07:10 +08:00
Kr328 3b33216961 Chore: update dependencies 2021-10-15 11:07:00 +08:00
Kr328 6373636d57 Chore: update clash core 2021-10-15 10:42:00 +08:00
Kr328 04a1f9987e Chore: update tun2socket 2021-10-15 10:41:54 +08:00
Kr328 90d286cd5a Chore: update gitignore 2021-09-26 10:04:35 +08:00
Kr328 1826d108a5 Chore: bump version 2021-09-24 11:24:05 +08:00
Goooler a5619bb0ee Chore: add cache action in CI (#1201) 2021-09-24 11:15:07 +08:00
Goooler 5c1fadaba2 Chore: update dependencies (#1198) 2021-09-24 11:13:10 +08:00
Kr328 7928f4321a Chore: update clash core 2021-09-24 11:11:47 +08:00
Kr328 3b62367003 Chore: bump version 2021-09-17 11:21:21 +08:00
Kr328 c8994d8488 Chore: add version to output apks name 2021-09-17 11:19:03 +08:00
kr328 4033994111 Chore: bump version 2021-09-14 01:50:06 +08:00
kr328 2050fb56d0 Chore: update clash core 2021-09-14 01:49:45 +08:00
kr328 eeea9d0fce Fix: add zhihu to http proxy black list 2021-09-14 01:07:55 +08:00
kr328 4b1f7a76c7 Chore: update dependencies 2021-09-13 22:44:01 +08:00
kr328 db2a7feb34 Chore: remove dependencies update detect plugin 2021-09-13 22:41:19 +08:00
Goooler 8b91b2eeb1 Chore: add .gitattributes (#1191)
* Add .editorconfig

* Add .gitattributes

* Remove indent_style

* Remove .editorconfig
2021-09-13 18:34:34 +08:00
Goooler 899803739b Chore: remove unused dependencies & improve gradle properties & code style (#1189)
* Inline version strings

* Optimize gradle

* Remove kotlin-stdlib-jdk7

* Add jvmargs in gradle.properties

* Revert "Inline version strings"

This reverts commit ad2ab2bb

* Revert asserts
2021-09-13 17:49:23 +08:00
Goooler 1ea3a5b682 Chore: bump setup-java action & Remove setup-android action (#1188)
* Bump setup-java action to v2

* Remove setup-android action

* Remove ignore rules under github folder
2021-09-13 17:45:46 +08:00
kr328 f21cf86cc4 Chore: allow run workflow only main branch 2021-09-12 18:31:16 +08:00
kr328 bb1052bbc7 Chore: update README.md 2021-09-12 18:25:09 +08:00
kr328 7dad08346c Chore: update dependencies 2021-09-12 18:02:30 +08:00
kr328 b7c56e58ae Chore: add hideapi to app module to avoid r8 class missing 2021-09-12 17:50:48 +08:00
kr328 a6f6da5bc5 Chore: remove unused check 2021-09-12 17:39:04 +08:00
kr328 f271ba3d97 Feature: add dns.geoip-code support 2021-09-12 17:38:52 +08:00
kr328 66d02c7e2d Chore: bump version 2021-09-12 13:19:55 +08:00
kr328 0406573317 Refactor: refactor properties 2021-09-12 13:19:40 +08:00
kr328 4a8b6b2704 Refactor: refactor design structure 2021-09-12 13:01:08 +08:00
kr328 603cec113c Chore: update clash patch 2021-09-12 12:40:15 +08:00
kr328 a579c7bb86 Fix: should not delay before patch system dns 2021-09-12 12:24:09 +08:00
kr328 654c488ed8 Fix: fix time zone 2021-09-12 11:46:20 +08:00
kr328 ecfb339680 Chore: fix code style 2021-09-12 11:26:27 +08:00
kr328 ecbe222a50 Chore: update dependencies 2021-09-12 11:14:45 +08:00
kr328 aacf62c259 Chore: update dependencies 2021-09-12 11:04:26 +08:00
kr328 3973fb2706 Fix: fix premium build 2021-09-12 01:33:52 +08:00
kr328 07ff81f4db Fix: should add main golang source as input 2021-09-12 00:43:32 +08:00
kr328 c0e1e45b72 Refactor: refactor clash core building 2021-09-12 00:39:59 +08:00
Kr328 b07f70ab0b Improve: should not reload profile on network changed 2021-09-11 23:37:54 +08:00
Kr328 e1a35f8b2d Chore: bump go version to 1.17 2021-09-11 19:49:26 +08:00
kr328 e364af393a Refactor: refactor golang flavor 2021-09-11 18:29:05 +08:00
Kr328 11c4e19e0c Refactor: refactor project structure & update clash core (#1174)
* Refactor: refactor project structure

* Chore: remove apply

* Chore: update clash core

* Fix: fix versionName & versionCode patch

* Chore: update golang plugin

* Fix: fix workflow build
2021-09-11 14:53:09 +08:00
Kr328 fa23be76ed Chore: update tun2socket 2021-06-20 13:22:48 +08:00
kr328 8e50d35eba Fix: fix GLOBAL url testing 2021-06-19 19:37:19 +08:00
kr328 1112534be6 Chore: bump version 2021-06-19 14:00:26 +08:00
kr328 784f249d54 Chore: migrate to new clash core 2021-06-19 13:59:58 +08:00
kr328 d6ad21b402 Chore: update clash core 2021-06-19 13:59:10 +08:00
kr328 bd03cac80d Chore: remove system proxy timeout 2021-06-19 13:39:29 +08:00
yi_Xu da3ee71dfd Docs: update readme (#1044)
* docs: update README.md

- Fix the path and name of the release.
- Format the style with markdownlint.

* docs: fix apk name

* ci: add paths-ignore for workflow
2021-06-17 22:10:03 +08:00
kr328 62fb089e27 Fix: system proxy should handle connection in parallel 2021-06-13 18:21:51 +08:00
kr328 51dab21f6f Chore: bump version 2021-06-13 14:19:00 +08:00
kr328 9e89b3a201 Chore: update clash core 2021-06-13 14:19:00 +08:00
kr328 1c1e2b9f3d Chore: update dependencies 2021-06-13 14:19:00 +08:00
kr328 e2e0238dcd Chore: update tun2socket 2021-06-13 14:19:00 +08:00
Tragic Life 9745e11da4 Chore: update zh-TW translate (#1063) 2021-06-13 12:38:33 +08:00
kr328 8ae7ccbfc9 Chore: add coroutine-android proguard rules 2021-06-13 12:32:34 +08:00
kr328 5a2229596a Chore: use coroutine-android instead of core 2021-06-13 12:32:10 +08:00
kr328 a463d94480 Chore: bump version 2021-06-13 04:22:53 +08:00
kr328 750abc8c71 Improve: enable system proxy by default 2021-06-13 04:22:15 +08:00
kr328 8375fbd8b3 Fix: add http timeout & disable keep-alive 2021-06-13 02:45:20 +08:00
kr328 394e406a36 Chore: update tun2socket 2021-06-13 02:35:35 +08:00
kr328 2645af0d4c Chore: update tun2socket 2021-05-29 19:56:21 +08:00
Kr328 48222c22c8 Fix: should close connection if match blocking list 2021-05-29 00:35:04 +08:00
kr328 d6a71267c6 Chore: bump version 2021-05-28 23:05:09 +08:00
Kr328 0f4a46188c Chore: remove verbose logs 2021-05-28 11:36:29 +08:00
Kr328 5917b90837 Feature: add block loopback connections 2021-05-28 11:32:21 +08:00
Kr328 a222e90d1f Chore: update kaidl 2021-05-28 10:52:41 +08:00
Kr328 3f60d713f8 Chore: update dependencies 2021-05-28 10:52:35 +08:00
Kr328 9cb8433f3b Chore: update tun2socket 2021-05-28 10:43:00 +08:00
Goooler 428ca53532 Chore: use "implementation" & remove "exclude" for buildSrc dependencies (#1034) 2021-05-28 10:37:23 +08:00
kr328 6c4d7e537b Chore: bump version 2021-05-26 19:48:11 +08:00
kr328 58ab89736a Chore: change tun interface address/route 2021-05-26 16:03:43 +08:00
Kr328 5e34221a09 Fix: refactor tun implement 2021-05-26 15:33:59 +08:00
Kr328 ac35f2a5f4 Chore: update tun2socket 2021-05-26 03:17:30 +08:00
kr328 73992dca54 Chore: update tun2socket 2021-05-25 22:50:35 +08:00
kr328 53dc20109d Improve: migrate to latest dependencies 2021-05-25 22:49:03 +08:00
kr328 e7fef0a767 Chore: bump version 2021-05-25 22:30:38 +08:00
kr328 c73beabf7e Fix: launch mode of MainActivity should be singleTop 2021-05-25 20:30:07 +08:00
kr328 c7409d7ac6 Chore: cleanup code 2021-05-25 20:28:57 +08:00
kr328 5e238ab5d3 Chore: update tun2socket 2021-05-25 20:28:51 +08:00
kr328 a8f502ef4f Improve: merge ClashManager and ProfileService 2021-05-25 20:28:40 +08:00
kr328 c48ce82640 Fix: fix generated ksp search path 2021-05-25 19:40:44 +08:00
kr328 5594485bec Improve: enable -O3 for libbridge.so 2021-05-25 19:23:19 +08:00
kr328 f967bd299a Improve: enable CMAKE_POSITION_INDEPENDENT_CODE 2021-05-25 19:14:57 +08:00
kr328 a75c8f5dfc Chore: update tun2socket 2021-05-25 19:10:47 +08:00
kr328 271d56c01c Chore: update tun2socket 2021-05-25 14:11:17 +08:00
Kr328 12220789a3 Improve: clean tun2socket build on tasks["clean"] 2021-05-25 02:25:30 +08:00
kr328 ca85a688cc Chore: bump version 2021-05-24 19:45:09 +08:00
kr328 0e3ed548c9 Chore: update tun2socket 2021-05-24 19:44:41 +08:00
Kr328 27dc5395e0 Improve: migrate to latest dependencies 2021-05-24 16:15:06 +08:00
Kr328 4f05ba1ac6 Feature: Add workflow for push/PR checking (#1030)
* Chore: downgrade ndk version

* Fix: fix local.properties load

* Feature: add workflow for push/PR checking
2021-05-24 16:15:06 +08:00
kr328 1a36218c80 Feature: add nameserver-policy override 2021-05-24 16:15:06 +08:00
Goooler 3125b90efe Chore: update dependencies 2021-05-24 16:14:59 +08:00
Kr328 2a7425700a Chore: update tun2socket 2021-05-24 03:15:58 +08:00
kr328 f8a3308b46 Chore: bump version 2021-05-23 22:42:20 +08:00
kr328 93aed001af Chore: update clash core 2021-05-23 22:20:21 +08:00
kr328 44461b67ee Chore: update tun2socket 2021-05-23 18:45:15 +08:00
kr328 8a55dc1945 Fix: remove tun address from bypass_private_route 2021-05-22 19:59:12 +08:00
kr328 3af16bcf34 Chore: update tun2socket 2021-05-22 19:55:51 +08:00
kr328 9adf0f00ae Chore: update README.md 2021-05-22 19:54:34 +08:00
kr328 a2f3330455 Fix: force use cmake installed in system 2021-05-22 19:53:28 +08:00
kr328 7cfefbca19 Improve: use 'redir-host' instead of 'fake-ip' 2021-05-22 01:38:15 +08:00
kr328 e8eddb3917 Chore: remove unused logger 2021-05-22 01:30:53 +08:00
kr328 634c25894c Fix: should close all endpoint on exit 2021-05-22 01:29:25 +08:00
kr328 42f5331a0c Fix: should close TCP/UDP endpoint on exit 2021-05-22 01:26:02 +08:00
kr328 e2383df06b Improve: improve clash core build 2021-05-22 01:22:09 +08:00
Kr328 7381b787f3 Improve: use lwip stack instead of kernel 2021-05-21 21:02:22 +08:00
Kr328 eff310ac40 Chore: should ignore appcenter.key in foss build 2021-05-20 10:59:54 +08:00
kr328 c245fc6dfa Fix: use hardcoded components package name 2021-05-16 22:56:57 +08:00
kr328 b8f0c32ad5 Chore: remove migration error tracking 2021-05-16 20:00:47 +08:00
kr328 4d580eda0e Fix: .gitignore ignore foss/release 2021-05-16 18:52:22 +08:00
kr328 2db91740a4 Chore: bump version 2021-05-16 18:38:11 +08:00
kr328 cc304de7a7 Improve: add sources code urls for flavor 'foss' 2021-05-16 18:36:28 +08:00
kr328 d48b15afee Improve: "inline" kaidl 2021-05-16 18:26:01 +08:00
kr328 0338058495 Improve: add package suffix for flavor 'foss' 2021-05-16 18:18:28 +08:00
kr328 0b69bf735e Improve: remove tracking for flavor 'foss' 2021-05-16 18:15:36 +08:00
kr328 45ab647c67 Chore: rename flavor 'open' to 'foss' 2021-05-16 17:55:47 +08:00
kr328 b470dcc252 Fix: fix signing config 2021-05-16 17:47:49 +08:00
kr328 2f69985fe1 Chore: update gradle wrapper & append sha256sum 2021-05-15 19:15:43 +08:00
kr328 44424c5c9e Improve: disable proxy group lazy url-test 2021-05-15 10:58:07 +08:00
kr328 e4c27ac233 Improve: auto re-generate go.sum on build 2021-05-15 10:20:58 +08:00
kr328 08db7c56fa Chore: update core 2021-05-15 10:13:35 +08:00
Kr328 75745d89d7 Chore: update CONTRIBUTING.md 2021-05-15 02:01:38 +08:00
kr328 5dbc11a450 Chore: add open-source build outputs to .gitignore 2021-05-15 01:08:46 +08:00
kr328 590183ebc0 Chore: add build outputs to .gitignore 2021-05-15 01:07:09 +08:00
kr328 07e8afa69a Initial: initial commit 2021-05-15 00:51:08 +08:00
664 changed files with 23827 additions and 11563 deletions

No files matched your search

+4
View File
@@ -0,0 +1,4 @@
* text=auto eol=lf
*.bat text eol=crlf
*.jar binary
+106
View File
@@ -0,0 +1,106 @@
name: "[English] Bug Report"
description: "Create a report to help us debug bugs"
title: "[BUG] "
body:
- type: markdown
attributes:
value: |
Thanks for taking the time to fill out this bug report!
NOTE: Be sure to put a clear and concise title **AFTER** `[BUG]` in the text box above.
NOTE: We do not provide any services such as proxies, DO NOT feedback any problems not caused by this application here.
<!-- template -->
- type: textarea
id: description
attributes:
label: "Describe the bug"
description: "A clear and concise description of what the bug is."
validations:
required: true
- type: textarea
id: reproduce
attributes:
label: "To Reproduce"
description: "Steps to reproduce the behavior:"
value: |
Step 1: ...
Step 2: ...
Step 3: ...
...
validations:
required: true
- type: textarea
id: device-info
attributes:
label: "Device Info"
description: |
Input your device information.
Example:
- Device: Pixel 4
- ROM: AOSP
- Android Version: 10
value: |
- Device:
- ROM:
- Android Version:
validations:
required: true
- type: textarea
id: app-info
attributes:
label: "Application Info"
description: |
Input application you are using information.
Example:
```
- Version: 2.5.4-premium
- APK filename: cfa-2.5.4-premium-arm64-v8a-release.apk
- Distribution Channel: Google Play
```
value: |
- Version:
- APK filename:
- Distribution Channel:
validations:
required: true
- type: textarea
id: configure
attributes:
render: yml
label: "Configure File"
description: |
Please paste or upload the configuration file here.
TIPS: If you only have a subscription link, please use your browser to download it.
**NOTE: Please remove proxies from the configuration file before uploading it.**
**NOTE: Please remove proxies from the configuration file before uploading it.**
**NOTE: Please remove proxies from the configuration file before uploading it.**
validations:
required: true
- type: textarea
id: logs
attributes:
render: raw
label: "Logs"
description: |
Please paste or upload the log file here.
TIPS: Please use the `Logcat` in application or `adb logcat`. `adb logcat` would be better.
validations:
required: true
- type: textarea
id: screenshot
attributes:
label: "Screenshot"
description: "If applicable, add screenshots to help explain your problem."
placeholder: "Optional"
- type: textarea
id: additional
attributes:
label: "Additional"
description: "Add any other context about the problem here."
@@ -0,0 +1,26 @@
name: "[English] Feature Request"
description: "Create a report to help us improve"
title: "[Feature Request] "
body:
- type: markdown
attributes:
value: |
Thanks for taking the time to fill out this feature request!
NOTE: Be sure to put a clear and concise title **AFTER** `[Feature Request]` in the text box above.
<!-- template -->
- type: textarea
id: "description"
attributes:
label: "Feature Description"
description: |
A clear and concise description of the feature.
validations:
required: true
- type: textarea
id: "additional"
attributes:
label: "Additional"
description: |
Add any other context or screenshots about the feature request here.
@@ -0,0 +1,107 @@
name: "[简体中文] 错误报告"
description: "创建错误报告以帮助我们修正应用"
title: "[BUG] "
body:
- type: markdown
attributes:
value: |
感谢您在百忙之中填写此错误报告。
注意: 请务必在上方文本框的 `[BUG]` **之后**填写清晰明了的标题。
注意:这里不提供像是代理服务器之类的服务,请不要反馈非应用自身引起的问题。
<!-- template -->
- type: textarea
id: description
attributes:
label: "描述此错误"
description: "请清晰简洁的描述你遇到的错误。"
validations:
required: true
- type: textarea
id: reproduce
attributes:
label: "如何复现该错误"
description: "复现步骤:"
value: |
步骤 1: ...
步骤 2: ...
步骤 3: ...
...
validations:
required: true
- type: textarea
id: device-info
attributes:
label: "设备信息"
description: |
输入您正在使用的设备信息。
例子:
- 机型: Pixel 4
- 系统类型: MIUI/AOSP
- Android 版本: 10
value: |
- 机型:
- 系统类型:
- Android 版本:
validations:
required: true
- type: textarea
id: app-info
attributes:
label: "应用信息"
description: |
输入您正在使用的应用信息。
例子:
```
- 版本: 2.5.4-premium
- 安装包文件名: cfa-2.5.4-premium-arm64-v8a-release.apk
- 应用来源: Google Play
```
value: |
- 版本:
- 安装包文件名:
- 应用来源:
validations:
required: true
- type: textarea
id: configure
attributes:
render: yml
label: "配置文件"
description: |
请在此粘贴和上传配置文件。
提示:如果您仅有一个订阅链接,请使用浏览器打开此链接以下载配置文件。
**注意: 请在上传配置文件前,移除其中的代理服务器信息。**
**注意: 请在上传配置文件前,移除其中的代理服务器信息。**
**注意: 请在上传配置文件前,移除其中的代理服务器信息。**
validations:
required: true
- type: textarea
id: logs
attributes:
render: raw
label: "日志"
description: |
请在此粘贴或上传日志。
提示: 请使用应用内的 `Logcat` 或 `adb logcat` 捕获日志. `adb logcat` 能更好地帮助侦测问题.
validations:
required: true
- type: textarea
id: screenshot
attributes:
label: "屏幕截图"
description: "如果适用,请在此粘贴或上传屏幕截图。"
placeholder: "可选"
- type: textarea
id: additional
attributes:
label: "附加信息"
description: "其他的可能与改错误相关的信息。"
placeholder: "可选"
@@ -0,0 +1,26 @@
name: "[简体中文] 功能请求"
description: "您希望的能够在应用中增加功能"
title: "[Feature Request] "
body:
- type: markdown
attributes:
value: |
感谢您在百忙之中填写此功能请求报告。
注意: 请务必在上方文本框的 `[Feature Request]` **之后**填写清晰明了的标题。
<!-- template -->
- type: textarea
id: "description"
attributes:
label: "功能描述"
description: |
简介明了的描述此功能。
validations:
required: true
- type: textarea
id: "additional"
attributes:
label: "附加信息"
description: |
与此功能相关的其他附加信息。
+1
View File
@@ -0,0 +1 @@
blank_issues_enabled: false
+54
View File
@@ -0,0 +1,54 @@
name: Build Unsigned
on:
push:
branches:
- main
paths-ignore:
- '.github/**'
- '.idea/**'
- '.gitattributes'
- '.gitignore'
- '.gitmodules'
- '**.md'
- 'LICENSE'
- 'NOTICE'
pull_request:
paths-ignore:
- '.github/**'
- '.idea/**'
- '.gitattributes'
- '.gitignore'
- '.gitmodules'
- '**.md'
- 'LICENSE'
- 'NOTICE'
jobs:
BuildUnsigned:
runs-on: ubuntu-latest
steps:
- name: Checkout Repository
uses: actions/checkout@v3
with:
submodules: recursive
- name: Setup Java
uses: actions/setup-java@v3
with:
distribution: 'zulu'
java-version: 17
- name: Setup Go
uses: actions/setup-go@v3
with:
go-version: 1.18
- uses: actions/cache@v3
with:
path: |
~/.cache/go-build
~/go/pkg/mod
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
restore-keys: |
${{ runner.os }}-go-
- name: Build
uses: gradle/gradle-build-action@v2
with:
arguments: --no-daemon app:assembleFossRelease
+20 -7
View File
@@ -1,6 +1,7 @@
.gradle
build/
/app/release/
/app/foss/release
/app/premium/release
/captures
# Ignore Gradle GUI config
@@ -16,24 +17,30 @@ gradle-app.setting
# gradle/wrapper/gradle-wrapper.properties
# Ignore IDEA config
.idea
*.iml
/.idea/*
!/.idea/codeStyles
/core/src/main/golang/.idea/*
!/core/src/main/golang/.idea/codeStyles
/core/src/foss/golang/.idea/*
!/core/src/foss/golang/.idea/codeStyles
/core/src/premium/golang/.idea/*
!/core/src/premium/golang/.idea/codeStyles
# KeyStore
signing.properties
*.keystore
*.jks
# gradle
.gradle
# clion cmake build
cmake-build-*
# local.properties
local.properties
# keystore
keystore.properties
# tracker
tracker.properties
# vscode
.vscode
@@ -48,3 +55,9 @@ google-services.json
# Dolphin
.directory
# logs
*.log
# MacOS
.DS_Store
+6 -3
View File
@@ -1,3 +1,6 @@
[submodule "core/src/main/golang/clash"]
path = core/src/main/golang/clash
url = https://github.com/Kr328/Clash
[submodule "clash-foss"]
path = core/src/foss/golang/clash
url = https://github.com/Kr328/clash.git
[submodule "clash-premium"]
path = core/src/premium/golang/clash
url = https://github.com/Kr328/clash.git
+151
View File
@@ -0,0 +1,151 @@
<component name="ProjectCodeStyleConfiguration">
<code_scheme name="Project" version="173">
<AndroidXmlCodeStyleSettings>
<option name="LAYOUT_SETTINGS">
<value>
<option name="INSERT_BLANK_LINE_BEFORE_TAG" value="false" />
</value>
</option>
<option name="VALUE_RESOURCE_FILE_SETTINGS">
<value>
<option name="INSERT_LINE_BREAKS_AROUND_STYLE" value="false" />
</value>
</option>
</AndroidXmlCodeStyleSettings>
<JetCodeStyleSettings>
<option name="PACKAGES_TO_USE_STAR_IMPORTS">
<value>
<package name="java.util" alias="false" withSubpackages="false" />
<package name="kotlinx.android.synthetic" alias="false" withSubpackages="true" />
<package name="io.ktor" alias="false" withSubpackages="true" />
</value>
</option>
<option name="PACKAGES_IMPORT_LAYOUT">
<value>
<package name="" alias="false" withSubpackages="true" />
<package name="java" alias="false" withSubpackages="true" />
<package name="javax" alias="false" withSubpackages="true" />
<package name="kotlin" alias="false" withSubpackages="true" />
<package name="" alias="true" withSubpackages="true" />
</value>
</option>
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
</JetCodeStyleSettings>
<codeStyleSettings language="XML">
<option name="FORCE_REARRANGE_MODE" value="1" />
<indentOptions>
<option name="CONTINUATION_INDENT_SIZE" value="4" />
</indentOptions>
<arrangement>
<rules>
<section>
<rule>
<match>
<AND>
<NAME>xmlns:android</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>xmlns:.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:id</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:name</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>name</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>style</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
<order>ANDROID_ATTRIBUTE_ORDER</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>.*</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
</rules>
</arrangement>
</codeStyleSettings>
<codeStyleSettings language="kotlin">
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
</codeStyleSettings>
</code_scheme>
</component>
+5
View File
@@ -0,0 +1,5 @@
<component name="ProjectCodeStyleConfiguration">
<state>
<option name="USE_PER_PROJECT_SETTINGS" value="true" />
</state>
</component>
+14
View File
@@ -0,0 +1,14 @@
## Contributing to Clash for Android
#### Code Style
Please use `Android Studio` or `Intellij IDEA` to open the project and use the project code style profile.
`File` -> `Settings` -> `Editor` -> `Code Style` -> `C/C++ and Kotlin` -> `Scheme` -> `Project`
#### License
Contributing to Clash for Android that assumes you allow code to be merged into closed-source branch of Clash for Android. Other terms follow the [GPLv3](https://www.gnu.org/licenses/gpl-3.0.html)
+5 -14
View File
@@ -4,24 +4,19 @@ The Clash for Android is built as an Open Source software. This app is provided
This page is used to inform visitors regarding our policies with the collection, use, and disclosure of Personal Information if anyone decided to use our app.
If you choose to use our app, then you agree to the collection and use of information in relation to this policy. The Personal Information that we collect is used for providing and improving the app. We will not use or share your information with anyone except as described in this Privacy Policy.
The terms used in this Privacy Policy have the same meanings as in our Terms and Conditions, which is accessible at Clash for Android unless otherwise defined in this Privacy Policy.
**Information Collection and Use**
For a better experience, while using our app, we may require you to provide us with certain personally identifiable information. The information that we request will be retained by us and used as described in this privacy policy.
We will not upload any of your personally information and that will be stored in the internal storage or memory.
The app does use third party services that may collect information used to identify you.
We collect the following information and store it in memory, and such information will be destroyed when the application is fully exited.
Link to privacy policy of third party service providers used by the app
- Installed Applications
* [Google Play Services](https://www.google.com/policies/privacy/)
* [Firebase Analytics](https://firebase.google.com/policies/analytics)
This data is used for the PROCESS-NAME rule.
**Log Data**
We want to inform you that whenever you use our app, in a case of an error in the app we collect data and information (through third party products) on your phone called Log Data. This Log Data may include information such as your device Internet Protocol (“IP”) address, device name, operating system version, the configuration of the app when utilizing our App, the time and date of your use of the app, and other statistics.
We do not collect log data unless you use log collector.
**Cookies**
@@ -37,10 +32,6 @@ We value your trust in providing us your Personal Information, thus we are striv
This app may contain links to other sites. If you click on a third-party link, you will be directed to that site. Note that these external sites are not operated by us. Therefore, we strongly advise you to review the Privacy Policy of these websites. We have no control over and assume no responsibility for the content, privacy policies, or practices of any third-party sites or services.
**Children’s Privacy**
These Services do not address anyone under the age of 13\. We do not knowingly collect personally identifiable information from children under 13\. In the case we discover that a child under 13 has provided us with personal information, we immediately delete this from our servers. If you are a parent or guardian and you are aware that your child has provided us with personal information, please contact us so that we will be able to do necessary actions.
**Changes to This Privacy Policy**
We may update our Privacy Policy from time to time. Thus, you are advised to review this page periodically for any changes. We will notify you of any changes by posting the new Privacy Policy on this page. These changes are effective immediately after they are posted on this page.
+19 -29
View File
@@ -1,36 +1,27 @@
## 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
Fully feature of [clash](https://github.com/Dreamacro/clash) ~~(Exclude `external-controller`~~
### Requirement
* Android 7.0+
* `arm64` or `x86_64` architecture
- Android 5.0+ (minimum)
- Android 7.0+ (recommend)
- `armeabi-v7a` , `arm64-v8a`, `x86` or `x86_64` Architecture
### License
See also [LICENSE](./LICENSE) and [NOTICE](./NOTICE)
### Privacy Policy
### Privacy Policy
See also [PRIVACY_POLICY.md](./PRIVACY_POLICY.md)
### Build
1. Update submodules
@@ -39,28 +30,27 @@ See also [PRIVACY_POLICY.md](./PRIVACY_POLICY.md)
git submodule update --init --recursive
```
2. Install `Android SDK` ,`Android NDK` and `Golang`
2. Install **OpenJDK 11**, **Android SDK**, **CMake** and **Golang**
3. Configure `local.properties`
3. Create `local.properties` in project root with
```properties
sdk.dir=/path/to/android-sdk
ndk.dir=/path/to/android-ndk
appcenter.key=<AppCenter Key> # Optional, from "appcenter.ms"
```
4. Build
4. Create `signing.properties` in project root with
on Linux
```properties
keystore.path=/path/to/keystore/file
keystore.password=<key store password>
key.alias=<key alias>
key.password=<key password>
```
5. Build
```bash
./gradlew build
./gradlew app:assembleFossRelease
```
on Windows
```bash
.\gradlew.bat build
```
6. Pick `app-<version>-foss-<arch>-release.apk` in `app/build/outputs/apk/foss/release/`
-1
View File
@@ -1 +0,0 @@
/build
-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)
}
+27
View File
@@ -0,0 +1,27 @@
plugins {
kotlin("android")
kotlin("kapt")
id("com.android.application")
}
dependencies {
compileOnly(project(":hideapi"))
implementation(project(":core"))
implementation(project(":service"))
implementation(project(":design"))
implementation(project(":common"))
implementation(libs.kotlin.coroutine)
implementation(libs.androidx.core)
implementation(libs.androidx.activity)
implementation(libs.androidx.fragment)
implementation(libs.androidx.appcompat)
implementation(libs.androidx.coordinator)
implementation(libs.androidx.recyclerview)
implementation(libs.google.material)
}
tasks.getByName("clean", type = Delete::class) {
delete(file("release"))
}
+37 -1
View File
@@ -20,4 +20,40 @@
# hide the original source file name.
#-renamesourcefileattribute SourceFile
-dontobfuscate
-dontobfuscate
-assumenosideeffects class kotlin.jvm.internal.Intrinsics {
public static void checkNotNull(...);
public static void checkExpressionValueIsNotNull(...);
public static void checkNotNullExpressionValue(...);
public static void checkReturnedValueIsNotNull(...);
public static void checkFieldIsNotNull(...);
public static void checkParameterIsNotNull(...);
public static void checkNotNullParameter(...);
}
# Kotlin Coroutine
# Allow R8 to optimize away the FastServiceLoader.
# Together with ServiceLoader optimization in R8
# this results in direct instantiation when loading Dispatchers.Main
-assumenosideeffects class kotlinx.coroutines.internal.MainDispatcherLoader {
boolean FAST_SERVICE_LOADER_ENABLED return false;
}
-assumenosideeffects class kotlinx.coroutines.internal.FastServiceLoaderKt {
boolean ANDROID_DETECTED return true;
}
-keep class kotlinx.coroutines.android.AndroidDispatcherFactory {*;}
# Disable support for "Missing Main Dispatcher", since we always have Android main dispatcher
-assumenosideeffects class kotlinx.coroutines.internal.MainDispatchersKt {
boolean SUPPORT_MISSING return false;
}
# Statically turn off all debugging facilities and assertions
-assumenosideeffects class kotlinx.coroutines.DebugKt {
boolean getASSERTIONS_ENABLED() return false;
boolean getDEBUG() return false;
boolean getRECOVER_STACK_TRACES() return false;
}
+105 -45
View File
@@ -3,117 +3,177 @@
xmlns:tools="http://schemas.android.com/tools"
package="com.github.kr328.clash">
<uses-feature
android:name="android.software.leanback"
android:required="false" />
<uses-feature
android:name="android.hardware.touchscreen"
android:required="false" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission
android:name="android.permission.QUERY_ALL_PACKAGES"
tools:ignore="QueryAllPackagesPermission" />
<application
android:name=".MainApplication"
android:allowBackup="true"
android:banner="@mipmap/ic_banner"
android:fullBackupContent="@xml/full_backup_content"
android:icon="@mipmap/ic_launcher"
android:label="@string/application_name"
android:networkSecurityConfig="@xml/network_security_config"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme"
tools:ignore="GoogleAppIndexingWarning">
android:theme="@style/BootstrapTheme"
tools:ignore="GoogleAppIndexingWarning"
tools:targetApi="N">
<meta-data
android:name="releaseName"
android:value="@string/release_name" />
<meta-data
android:name="releaseCode"
android:value="@integer/release_code" />
<activity
android:name=".MainActivity"
android:label="@string/launch_name"
android:configChanges="uiMode"
android:launchMode="singleTask">
android:exported="true"
android:label="@string/launch_name"
android:launchMode="singleTop">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
<intent-filter>
<action android:name="android.service.quicksettings.action.QS_TILE_PREFERENCES" />
</intent-filter>
</activity>
<activity
android:name=".ExternalImportActivity"
android:exported="true"
android:label="@string/import_from_file"
android:theme="@android:style/Theme.Translucent.NoTitleBar.Fullscreen">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:host="install-config"
android:scheme="clash" />
</intent-filter>
</activity>
<activity
android:name=".ApkBrokenActivity"
android:label="@string/application_broken"
android:configChanges="uiMode"
android:exported="false"
android:configChanges="uiMode"/>
android:label="@string/application_broken" />
<activity
android:name=".AppCrashedActivity"
android:configChanges="uiMode"
android:exported="false"
android:label="@string/application_crashed"
android:launchMode="singleTask" />
<activity
android:name=".ProfilesActivity"
android:label="@string/profiles"
android:configChanges="uiMode"
android:exported="false"
android:configChanges="uiMode"/>
android:label="@string/profiles" />
<activity
android:name=".CreateProfileActivity"
android:label="@string/create_profile"
android:name=".NewProfileActivity"
android:configChanges="uiMode"
android:exported="false"
android:configChanges="uiMode" />
android:label="@string/create_profile" />
<activity
android:name=".ProfileEditActivity"
android:label="@string/profile"
android:name=".PropertiesActivity"
android:configChanges="uiMode"
android:exported="false"
android:configChanges="uiMode" />
android:label="@string/profile" />
<activity
android:name=".ProxiesActivity"
android:label="@string/proxy"
android:name=".ProxyActivity"
android:configChanges="uiMode"
android:exported="false"
android:configChanges="uiMode" />
android:label="@string/proxy" />
<activity
android:name=".ProvidersActivity"
android:configChanges="uiMode"
android:exported="false"
android:label="@string/providers" />
<activity
android:name=".LogsActivity"
android:label="@string/logs"
android:exported="false"
android:configChanges="uiMode" />
<activity
android:name=".LogViewerActivity"
android:label="@string/log_viewer"
android:exported="false"
android:configChanges="uiMode"
android:launchMode="singleTask"/>
android:exported="false"
android:label="@string/logs" />
<activity
android:name=".LogcatActivity"
android:configChanges="uiMode"
android:exported="false"
android:label="@string/logcat" />
<activity
android:name=".SettingsActivity"
android:label="@string/settings"
android:configChanges="uiMode"
android:exported="false"
android:configChanges="uiMode" />
android:label="@string/settings" />
<activity
android:name=".SettingsNetworkActivity"
android:label="@string/network"
android:name=".NetworkSettingsActivity"
android:configChanges="uiMode"
android:exported="false"
android:configChanges="uiMode" />
android:label="@string/network" />
<activity
android:name=".SettingsBehaviorActivity"
android:label="@string/behavior"
android:name=".AppSettingsActivity"
android:configChanges="uiMode"
android:exported="false"
android:configChanges="uiMode" />
android:label="@string/app" />
<activity
android:name=".SettingsInterfaceActivity"
android:label="@string/interface_"
android:name=".OverrideSettingsActivity"
android:configChanges="uiMode"
android:exported="false"
android:configChanges="uiMode" />
android:label="@string/override" />
<activity
android:name=".PackagesActivity"
android:label="@string/access_control_packages"
android:name=".AccessControlActivity"
android:configChanges="uiMode"
android:exported="false"
android:configChanges="uiMode" />
android:label="@string/access_control_packages" />
<activity
android:name=".SupportActivity"
android:label="@string/support"
android:name=".HelpActivity"
android:configChanges="uiMode"
android:exported="false"
android:configChanges="uiMode" />
android:label="@string/help" />
<activity
android:name=".FilesActivity"
android:configChanges="uiMode"
android:exported="false"
android:label="@string/files" />
<service
android:name=".LogcatService"
android:exported="false"
android:label="@string/clash_logcat" />
<service
android:name=".TileService"
android:icon="@drawable/ic_notification"
android:exported="true"
android:icon="@drawable/ic_logo_service"
android:label="@string/launch_name"
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE">
<intent-filter>
<action android:name="android.service.quicksettings.action.QS_TILE" />
</intent-filter>
</service>
<receiver
android:name=".OnBootReceiver"
android:enabled="false">
android:name=".RestartReceiver"
android:enabled="false"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
<action android:name="android.intent.action.MY_PACKAGE_REPLACED" />
</intent-filter>
</receiver>
</application>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 16 KiB

@@ -0,0 +1,137 @@
package com.github.kr328.clash
import android.Manifest.permission.INTERNET
import android.content.ClipData
import android.content.ClipboardManager
import android.content.pm.ApplicationInfo
import android.content.pm.PackageInfo
import android.content.pm.PackageManager
import androidx.core.content.getSystemService
import com.github.kr328.clash.design.AccessControlDesign
import com.github.kr328.clash.design.model.AppInfo
import com.github.kr328.clash.design.util.toAppInfo
import com.github.kr328.clash.service.store.ServiceStore
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.isActive
import kotlinx.coroutines.selects.select
import kotlinx.coroutines.withContext
class AccessControlActivity : BaseActivity<AccessControlDesign>() {
override suspend fun main() {
val service = ServiceStore(this)
val selected = withContext(Dispatchers.IO) {
service.accessControlPackages.toMutableSet()
}
defer {
withContext(Dispatchers.IO) {
service.accessControlPackages = selected
}
}
val design = AccessControlDesign(this, uiStore, selected)
setContentDesign(design)
design.requests.send(AccessControlDesign.Request.ReloadApps)
while (isActive) {
select<Unit> {
events.onReceive {
}
design.requests.onReceive {
when (it) {
AccessControlDesign.Request.ReloadApps -> {
design.patchApps(loadApps(selected))
}
AccessControlDesign.Request.SelectAll -> {
val all = withContext(Dispatchers.Default) {
design.apps.map(AppInfo::packageName)
}
selected.clear()
selected.addAll(all)
design.rebindAll()
}
AccessControlDesign.Request.SelectNone -> {
selected.clear()
design.rebindAll()
}
AccessControlDesign.Request.SelectInvert -> {
val all = withContext(Dispatchers.Default) {
design.apps.map(AppInfo::packageName).toSet() - selected
}
selected.clear()
selected.addAll(all)
design.rebindAll()
}
AccessControlDesign.Request.Import -> {
val clipboard = getSystemService<ClipboardManager>()
val data = clipboard?.primaryClip
if (data != null && data.itemCount > 0) {
val packages = data.getItemAt(0).text.split("\n").toSet()
val all = design.apps.map(AppInfo::packageName).intersect(packages)
selected.clear()
selected.addAll(all)
}
design.rebindAll()
}
AccessControlDesign.Request.Export -> {
val clipboard = getSystemService<ClipboardManager>()
val data = ClipData.newPlainText(
"packages",
selected.joinToString("\n")
)
clipboard?.setPrimaryClip(data)
}
}
}
}
}
}
private suspend fun loadApps(selected: Set<String>): List<AppInfo> =
withContext(Dispatchers.IO) {
val reverse = uiStore.accessControlReverse
val sort = uiStore.accessControlSort
val systemApp = uiStore.accessControlSystemApp
val base = compareByDescending<AppInfo> { it.packageName in selected }
val comparator = if (reverse) base.thenDescending(sort) else base.then(sort)
val pm = packageManager
val packages = pm.getInstalledPackages(PackageManager.GET_PERMISSIONS)
packages.asSequence()
.filter {
it.packageName != packageName
}
.filter {
it.packageName == "android" || it.requestedPermissions?.contains(INTERNET) == true
}
.filter {
systemApp || !it.isSystemApp
}
.map {
it.toAppInfo(pm)
}
.sortedWith(comparator)
.toList()
}
private val PackageInfo.isSystemApp: Boolean
get() {
return applicationInfo.flags and ApplicationInfo.FLAG_SYSTEM != 0
}
}
@@ -2,62 +2,19 @@ package com.github.kr328.clash
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.text.Html
import kotlinx.android.synthetic.main.activity_application_broken.*
import com.github.kr328.clash.design.ApkBrokenDesign
import kotlinx.coroutines.isActive
class ApkBrokenActivity : BaseActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
class ApkBrokenActivity : BaseActivity<ApkBrokenDesign>() {
override suspend fun main() {
val design = ApkBrokenDesign(this)
setContentView(R.layout.activity_application_broken)
setSupportActionBar(toolbar)
setContentDesign(design)
text.text = Html.fromHtml(getString(R.string.application_broken_description),
Html.FROM_HTML_MODE_COMPACT)
while (isActive) {
val req = design.requests.receive()
commonUi.build {
option(
icon = getDrawable(R.drawable.ic_info),
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))))
}
}
option(
icon = getDrawable(R.drawable.ic_input),
title = getString(R.string.reinstall_from_google_play)
) {
onClick {
startActivity(Intent(Intent.ACTION_VIEW)
.setData(Uri.parse(getString(R.string.google_play_url))))
}
}
option(
icon = getDrawable(R.drawable.ic_play_for_work),
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(req.url)))
}
}
override fun onBackPressed() {
super.onBackPressed()
finishAffinity()
finish()
}
override fun shouldDisplayHomeAsUpEnabled(): Boolean {
return false
}
override val activityLabel: CharSequence
get() = getText(R.string.application_broken)
}
@@ -0,0 +1,33 @@
package com.github.kr328.clash
import com.github.kr328.clash.common.compat.versionCodeCompat
import com.github.kr328.clash.common.log.Log
import com.github.kr328.clash.design.AppCrashedDesign
import com.github.kr328.clash.log.SystemLogcat
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.isActive
import kotlinx.coroutines.withContext
class AppCrashedActivity : BaseActivity<AppCrashedDesign>() {
override suspend fun main() {
val design = AppCrashedDesign(this)
setContentDesign(design)
val packageInfo = withContext(Dispatchers.IO) {
packageManager.getPackageInfo(packageName, 0)
}
Log.i("App version: versionName = ${packageInfo.versionName} versionCode = ${packageInfo.versionCodeCompat}")
val logs = withContext(Dispatchers.IO) {
SystemLogcat.dumpCrash()
}
design.setAppLogs(logs)
while (isActive) {
events.receive()
}
}
}
@@ -0,0 +1,62 @@
package com.github.kr328.clash
import android.content.pm.PackageManager
import com.github.kr328.clash.common.util.componentName
import com.github.kr328.clash.design.AppSettingsDesign
import com.github.kr328.clash.design.model.Behavior
import com.github.kr328.clash.service.store.ServiceStore
import com.github.kr328.clash.util.ApplicationObserver
import kotlinx.coroutines.isActive
import kotlinx.coroutines.selects.select
class AppSettingsActivity : BaseActivity<AppSettingsDesign>(), Behavior {
override suspend fun main() {
val design = AppSettingsDesign(
this,
uiStore,
ServiceStore(this),
this,
clashRunning,
)
setContentDesign(design)
while (isActive) {
select<Unit> {
events.onReceive {
when (it) {
Event.ClashStart, Event.ClashStop, Event.ServiceRecreated ->
recreate()
else -> Unit
}
}
design.requests.onReceive {
ApplicationObserver.createdActivities.forEach {
it.recreate()
}
}
}
}
}
override var autoRestart: Boolean
get() {
val status = packageManager.getComponentEnabledSetting(
RestartReceiver::class.componentName
)
return status == PackageManager.COMPONENT_ENABLED_STATE_ENABLED
}
set(value) {
val status = if (value)
PackageManager.COMPONENT_ENABLED_STATE_ENABLED
else
PackageManager.COMPONENT_ENABLED_STATE_DISABLED
packageManager.setComponentEnabledSetting(
RestartReceiver::class.componentName,
status,
PackageManager.DONT_KILL_APP,
)
}
}
@@ -1,173 +1,164 @@
package com.github.kr328.clash
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.res.Configuration
import android.os.Build
import android.os.Bundle
import android.view.*
import android.view.View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR
import androidx.appcompat.app.AlertDialog
import android.view.View
import androidx.activity.result.contract.ActivityResultContract
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.preference.UiSettings
import com.github.kr328.clash.common.compat.isAllowForceDarkCompat
import com.github.kr328.clash.common.compat.isLightNavigationBarCompat
import com.github.kr328.clash.common.compat.isLightStatusBarsCompat
import com.github.kr328.clash.common.compat.isSystemBarsTranslucentCompat
import com.github.kr328.clash.core.bridge.ClashException
import com.github.kr328.clash.design.Design
import com.github.kr328.clash.design.model.DarkMode
import com.github.kr328.clash.design.store.UiStore
import com.github.kr328.clash.design.ui.DayNight
import com.github.kr328.clash.design.util.resolveThemedBoolean
import com.github.kr328.clash.design.util.resolveThemedColor
import com.github.kr328.clash.design.util.showExceptionToast
import com.github.kr328.clash.remote.Broadcasts
import com.github.kr328.clash.service.data.ClashProfileEntity
import com.google.android.material.snackbar.Snackbar
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch
import java.util.*
import com.github.kr328.clash.remote.Remote
import com.github.kr328.clash.util.ActivityResultLifecycle
import com.github.kr328.clash.util.ApplicationObserver
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.Channel
import java.util.concurrent.atomic.AtomicInteger
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
abstract class BaseActivity : AppCompatActivity(), CoroutineScope by MainScope() {
class EmptyBroadcastReceiver : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {}
abstract class BaseActivity<D : Design<*>> :
AppCompatActivity(),
CoroutineScope by MainScope(),
Broadcasts.Observer {
enum class Event {
ServiceRecreated,
ActivityStart,
ActivityStop,
ClashStop,
ClashStart,
ProfileLoaded,
ProfileChanged
}
private val receiver = object : Broadcasts.Receiver {
override fun onStarted() {
launch {
onClashStarted()
protected val uiStore by lazy { UiStore(this) }
protected val events = Channel<Event>(Channel.UNLIMITED)
protected var activityStarted: Boolean = false
protected val clashRunning: Boolean
get() = Remote.broadcasts.clashRunning
protected var design: D? = null
private set(value) {
field = value
if (value != null) {
setContentView(value.root)
} else {
setContentView(View(this))
}
}
override fun onStopped(cause: String?) {
launch {
onClashStopped(cause)
}
}
private var defer: suspend () -> Unit = {}
private var deferRunning = false
private val nextRequestKey = AtomicInteger(0)
private var dayNight: DayNight = DayNight.Day
override fun onProfileChanged() {
launch {
onClashProfileChanged()
}
}
protected abstract suspend fun main()
override fun onProfileLoaded(profileEntity: ClashProfileEntity) {
launch {
onClashProfileLoaded(profileEntity)
fun defer(operation: suspend () -> Unit) {
this.defer = operation
}
suspend fun <I, O> startActivityForResult(
contracts: ActivityResultContract<I, O>,
input: I
): O = withContext(Dispatchers.Main) {
val requestKey = nextRequestKey.getAndIncrement().toString()
ActivityResultLifecycle().use { lifecycle, start ->
suspendCoroutine { c ->
activityResultRegistry.register(requestKey, lifecycle, contracts) {
c.resumeWith(Result.success(it))
}.apply { start() }.launch(input)
}
}
}
private var overrideRootView: View? = null
suspend fun setContentDesign(design: D) {
suspendCoroutine<Unit> {
window.decorView.post {
this.design = design
val clashRunning: Boolean
get() = Broadcasts.clashRunning
val rootView: View
get() = overrideRootView ?: window.decorView
var menu: Menu? = null
lateinit var uiSettings: UiSettings
private set
private lateinit var language: String
private lateinit var darkMode: String
open suspend fun onClashStarted() {}
open suspend fun onClashStopped(reason: String?) {}
open suspend fun onClashProfileChanged() {}
open suspend fun onClashProfileLoaded(profile: ClashProfileEntity) {}
override fun setContentView(layoutResID: Int) {
val base = CoordinatorLayout(this).apply {
layoutParams = ViewGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT
)
val displayMetrics = resources.displayMetrics
if (displayMetrics.widthPixels > displayMetrics.heightPixels) {
val padding = (displayMetrics.widthPixels - displayMetrics.heightPixels) / 2
setPadding(padding, 0, padding, 0)
it.resume(Unit)
}
}
overrideRootView = LayoutInflater.from(this).inflate(layoutResID, base, true)
super.setContentView(base)
}
override fun attachBaseContext(newBase: Context?) {
val base = newBase ?: return super.attachBaseContext(newBase)
uiSettings = UiSettings(base)
language = uiSettings.get(UiSettings.LANGUAGE)
val languageOverride = language.split("-")
if (language.isEmpty())
return super.attachBaseContext(base)
val configuration = Configuration()
val localeOverride = if (languageOverride.size == 2)
Locale(languageOverride[0], languageOverride[1])
else
Locale(languageOverride[0])
configuration.setLocale(localeOverride)
super.attachBaseContext(base.createConfigurationContext(configuration))
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
language = uiSettings.get(UiSettings.LANGUAGE)
applyDayNight()
resetDarkMode()
launch {
main()
resetLightNavigationBar()
finish()
}
}
override fun onStart() {
super.onStart()
if (language != uiSettings.get(UiSettings.LANGUAGE))
recreate()
if (darkMode != uiSettings.get(UiSettings.DARK_MODE)) {
resetDarkMode()
recreate()
}
activityStarted = true
Broadcasts.register(receiver)
Remote.broadcasts.addObserver(this)
events.trySend(Event.ActivityStart)
}
override fun onStop() {
super.onStop()
Broadcasts.unregister(receiver)
activityStarted = false
Remote.broadcasts.removeObserver(this)
events.trySend(Event.ActivityStop)
}
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
this.menu = menu
return super.onCreateOptionsMenu(menu)
override fun onDestroy() {
design?.cancel()
cancel()
super.onDestroy()
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
if (super.onOptionsItemSelected(item))
return true
if (item.itemId == android.R.id.home) {
onSupportNavigateUp()
return true
override fun finish() {
if (deferRunning) {
return
}
return false
deferRunning = true
launch {
try {
defer()
} finally {
withContext(NonCancellable) {
super.finish()
}
}
}
}
override fun setSupportActionBar(toolbar: Toolbar?) {
super.setSupportActionBar(toolbar)
override fun onConfigurationChanged(newConfig: Configuration) {
super.onConfigurationChanged(newConfig)
supportActionBar?.apply {
setDisplayHomeAsUpEnabled(shouldDisplayHomeAsUpEnabled())
activityLabel?.let {
title = it
if (queryDayNight(newConfig) != dayNight) {
ApplicationObserver.createdActivities.forEach {
it.recreate()
}
}
}
@@ -176,58 +167,83 @@ abstract class BaseActivity : AppCompatActivity(), CoroutineScope by MainScope()
return true
}
abstract val activityLabel: CharSequence?
override fun onSupportNavigateUp(): Boolean {
this.onBackPressed()
return true
}
override fun onDestroy() {
cancel()
super.onDestroy()
override fun onProfileChanged() {
events.trySend(Event.ProfileChanged)
}
override fun onConfigurationChanged(newConfig: Configuration) {
super.onConfigurationChanged(newConfig)
recreate()
override fun onProfileLoaded() {
events.trySend(Event.ProfileLoaded)
}
protected fun makeSnackbarException(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()
}.show()
override fun onServiceRecreated() {
events.trySend(Event.ServiceRecreated)
}
private fun resetDarkMode() {
when ( uiSettings.get(UiSettings.DARK_MODE).also { darkMode = it } ) {
UiSettings.DARK_MODE_AUTO ->
delegate.localNightMode = AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM
UiSettings.DARK_MODE_DARK ->
delegate.localNightMode = AppCompatDelegate.MODE_NIGHT_YES
UiSettings.DARK_MODE_LIGHT ->
delegate.localNightMode = AppCompatDelegate.MODE_NIGHT_NO
override fun onStarted() {
events.trySend(Event.ClashStart)
}
override fun onStopped(cause: String?) {
events.trySend(Event.ClashStop)
if (cause != null && activityStarted) {
launch {
design?.showExceptionToast(ClashException(cause))
}
}
}
private fun resetLightNavigationBar() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O)
return
private fun queryDayNight(config: Configuration = resources.configuration): DayNight {
return when (uiStore.darkMode) {
DarkMode.Auto -> {
if (config.uiMode and Configuration.UI_MODE_NIGHT_MASK == Configuration.UI_MODE_NIGHT_YES)
DayNight.Night
else
DayNight.Day
}
DarkMode.ForceLight -> {
DayNight.Day
}
DarkMode.ForceDark -> {
DayNight.Night
}
}
}
val light = resources.getBoolean(R.bool.lightStatusBar)
private fun applyDayNight(config: Configuration = resources.configuration) {
val dayNight = queryDayNight(config)
if (light) {
window.decorView.systemUiVisibility =
window.decorView.systemUiVisibility or SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR
} else {
window.decorView.systemUiVisibility =
window.decorView.systemUiVisibility and SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR.inv()
when (dayNight) {
DayNight.Night -> {
theme.applyStyle(R.style.AppThemeDark, true)
}
DayNight.Day -> {
theme.applyStyle(R.style.AppThemeLight, true)
}
}
window.navigationBarColor = getColor(R.color.backgroundColor)
window.isAllowForceDarkCompat = false
window.isSystemBarsTranslucentCompat = true
window.statusBarColor = resolveThemedColor(android.R.attr.statusBarColor)
window.navigationBarColor = resolveThemedColor(android.R.attr.navigationBarColor)
if (Build.VERSION.SDK_INT >= 23) {
window.isLightStatusBarsCompat =
resolveThemedBoolean(android.R.attr.windowLightStatusBar)
}
if (Build.VERSION.SDK_INT >= 27) {
window.isLightNavigationBarCompat =
resolveThemedBoolean(android.R.attr.windowLightNavigationBar)
}
this.dayNight = dayNight
}
}
}
@@ -1,16 +0,0 @@
package com.github.kr328.clash
object Constants {
const val PREFERENCE_NAME_APP = "app"
const val PREFERENCE_KEY_LAST_INSTALL = "last_install"
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"
}
@@ -1,151 +0,0 @@
package com.github.kr328.clash
import android.app.Activity
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.graphics.drawable.Drawable
import android.net.Uri
import android.os.Bundle
import android.provider.Settings
import android.view.LayoutInflater
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 kotlinx.android.synthetic.main.activity_create_profile.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
class CreateProfileActivity : BaseActivity() {
companion object {
const val REQUEST_CODE = 20000
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_create_profile)
setSupportActionBar(toolbar)
launch {
val providers = queryUrlProviders()
mainList.adapter = Adapter(this@CreateProfileActivity, providers)
mainList.divider = null
mainList.dividerHeight = 0
mainList.setOnItemClickListener { _, _, position, _ ->
val item = providers[position]
startActivityForResult(
ProfileEditActivity::class.intent
.putExtra("type", item.type)
.putExtra("intent", item.intent),
REQUEST_CODE
)
}
mainList.setOnItemLongClickListener { _, _, position, _ ->
val item = providers[position]
val packageName = item.intent?.component?.packageName
if (packageName != null) {
val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
.setData(Uri.fromParts("package", packageName, null))
startActivity(intent)
true
} else {
false
}
}
}
}
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()
super.onActivityResult(requestCode, resultCode, data)
}
private suspend fun queryUrlProviders(): List<UrlProvider> =
withContext(Dispatchers.IO) {
val common = listOf(
UrlProvider(
getText(R.string.file),
getText(R.string.import_from_file),
getDrawable(R.drawable.ic_file)!!,
Constants.URL_PROVIDER_TYPE_FILE,
null
),
UrlProvider(
getText(R.string.url),
getText(R.string.import_from_url),
getDrawable(R.drawable.ic_download)!!,
Constants.URL_PROVIDER_TYPE_URL,
null
)
)
val providers = packageManager.queryIntentActivities(
Intent(Constants.URL_PROVIDER_INTENT_ACTION),
0
).map {
val activity = it.activityInfo
val name = activity.applicationInfo.loadLabel(packageManager)
val summary = activity.loadLabel(packageManager)
val icon = activity.loadIcon(packageManager)
val type = Constants.URL_PROVIDER_TYPE_EXTERNAL
val intent = Intent(Constants.URL_PROVIDER_INTENT_ACTION)
.setComponent(
ComponentName.createRelative(
activity.packageName,
activity.name
)
)
UrlProvider(name, summary, icon, type, intent)
}
common + providers
}
private data class UrlProvider(
val name: CharSequence,
val summary: CharSequence,
val icon: Drawable,
val type: String,
val intent: Intent?
)
private class Adapter(private val context: Context, private val providers: List<UrlProvider>) :
BaseAdapter() {
override fun getView(position: Int, convertView: View?, parent: ViewGroup?): View {
val provider = providers[position]
val view = convertView ?: LayoutInflater.from(context).inflate(
R.layout.adapter_url_provider,
parent,
false
)
view.findViewById<TextView>(android.R.id.title).text = provider.name
view.findViewById<TextView>(android.R.id.summary).text = provider.summary
view.findViewById<View>(android.R.id.icon).background = provider.icon
return view
}
override fun getItem(position: Int): Any = providers[position]
override fun getItemId(position: Int): Long = providers[position].hashCode().toLong()
override fun getCount(): Int = providers.size
}
}
@@ -0,0 +1,44 @@
package com.github.kr328.clash
import android.app.Activity
import android.content.Intent
import android.os.Bundle
import com.github.kr328.clash.common.util.intent
import com.github.kr328.clash.common.util.setUUID
import com.github.kr328.clash.service.model.Profile
import com.github.kr328.clash.util.withProfile
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.launch
import java.util.*
class ExternalImportActivity : Activity(), CoroutineScope by MainScope() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if (intent.action != Intent.ACTION_VIEW)
return finish()
val uri = intent.data ?: return finish()
val url = uri.getQueryParameter("url") ?: return finish()
launch {
val uuid = withProfile {
val type = when (uri.getQueryParameter("type")?.lowercase(Locale.getDefault())) {
"url" -> Profile.Type.Url
"file" -> Profile.Type.File
else -> Profile.Type.Url
}
val name = uri.getQueryParameter("name") ?: getString(R.string.new_profile)
create(type, name).also {
patch(it, name, url, 0)
}
}
startActivity(PropertiesActivity::class.intent.setUUID(uuid))
finish()
}
}
}
@@ -0,0 +1,160 @@
@file:Suppress("BlockingMethodInNonBlockingContext")
package com.github.kr328.clash
import android.Manifest
import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri
import android.os.Build
import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.content.ContextCompat
import com.github.kr328.clash.common.util.grantPermissions
import com.github.kr328.clash.common.util.ticker
import com.github.kr328.clash.common.util.uuid
import com.github.kr328.clash.design.FilesDesign
import com.github.kr328.clash.design.util.showExceptionToast
import com.github.kr328.clash.remote.FilesClient
import com.github.kr328.clash.service.model.Profile
import com.github.kr328.clash.util.fileName
import com.github.kr328.clash.util.withProfile
import kotlinx.coroutines.isActive
import kotlinx.coroutines.selects.select
import java.util.*
import java.util.concurrent.TimeUnit
class FilesActivity : BaseActivity<FilesDesign>() {
override suspend fun main() {
val uuid = intent.uuid ?: return finish()
val profile = withProfile { queryByUUID(uuid) } ?: return finish()
val root = uuid.toString()
val design = FilesDesign(this)
val client = FilesClient(this)
val stack = Stack<String>()
design.configurationEditable = profile.type != Profile.Type.Url
design.fetch(client, stack, root)
setContentDesign(design)
val ticker = ticker(TimeUnit.MINUTES.toMillis(1))
while (isActive) {
select<Unit> {
events.onReceive {
when (it) {
Event.ActivityStart, Event.ActivityStop -> {
design.fetch(client, stack, root)
}
else -> Unit
}
}
design.requests.onReceive {
try {
when (it) {
FilesDesign.Request.PopStack -> {
if (stack.empty()) {
finish()
} else {
stack.pop()
}
}
is FilesDesign.Request.OpenDirectory -> {
stack.push(it.file.id)
}
is FilesDesign.Request.OpenFile -> {
startActivityForResult(
ActivityResultContracts.StartActivityForResult(),
Intent(Intent.ACTION_VIEW).setDataAndType(
client.buildDocumentUri(it.file.id),
"text/plain"
).grantPermissions()
)
}
is FilesDesign.Request.DeleteFile -> {
client.deleteDocument(it.file.id)
}
is FilesDesign.Request.RenameFile -> {
val newName = design.requestFileName(it.file.name)
client.renameDocument(it.file.id, newName)
}
is FilesDesign.Request.ImportFile -> {
if (Build.VERSION.SDK_INT >= 23) {
val hasPermission = ContextCompat.checkSelfPermission(
this@FilesActivity,
Manifest.permission.READ_EXTERNAL_STORAGE
) == PackageManager.PERMISSION_GRANTED
if (!hasPermission) {
val granted = startActivityForResult(
ActivityResultContracts.RequestPermission(),
Manifest.permission.READ_EXTERNAL_STORAGE,
)
if (!granted) {
return@onReceive
}
}
}
val uri: Uri? = startActivityForResult(
ActivityResultContracts.GetContent(),
"*/*"
)
if (uri != null) {
if (it.file == null) {
val name = design.requestFileName(uri.fileName ?: "File")
client.importDocument(stack.last(), uri, name)
} else {
client.copyDocument(it.file!!.id, uri)
}
}
}
is FilesDesign.Request.ExportFile -> {
val uri: Uri? = startActivityForResult(
ActivityResultContracts.CreateDocument("text/plain"),
it.file.name
)
if (uri != null) {
client.copyDocument(uri, it.file.id)
}
}
}
} catch (e: Exception) {
design.showExceptionToast(e)
}
design.fetch(client, stack, root)
}
if (activityStarted) {
ticker.onReceive {
design.updateElapsed()
}
}
}
}
}
override fun onBackPressed() {
design?.requests?.trySend(FilesDesign.Request.PopStack)
}
private suspend fun FilesDesign.fetch(client: FilesClient, stack: Stack<String>, root: String) {
val documentId = stack.lastOrNull() ?: root
val files = if (stack.empty()) {
val list = client.list(documentId)
val config = list.firstOrNull { it.id.endsWith("config.yaml") }
if (config == null || config.size > 0) list else listOf(config)
} else {
client.list(documentId)
}
swapFiles(files, stack.empty())
}
}
@@ -0,0 +1,19 @@
package com.github.kr328.clash
import android.content.Intent
import com.github.kr328.clash.design.HelpDesign
import kotlinx.coroutines.isActive
class HelpActivity : BaseActivity<HelpDesign>() {
override suspend fun main() {
val design = HelpDesign(this) {
startActivity(Intent(Intent.ACTION_VIEW).setData(it))
}
setContentDesign(design)
while (isActive) {
events.receive()
}
}
}
@@ -1,145 +0,0 @@
package com.github.kr328.clash
import android.content.ComponentName
import android.content.Context
import android.content.ServiceConnection
import android.os.Bundle
import android.os.IBinder
import android.view.View
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.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()
}
override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
val logcat =
requireNotNull(service?.queryLocalInterface(LogcatService::class.java.name)) as LogcatService
startLogcatPoll(logcat)
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_log_viewer)
setSupportActionBar(toolbar)
val file = intent?.data
if (file == null)
startLiveMode()
else
startFileMode(file.toFile())
}
override fun onDestroy() {
super.onDestroy()
pollingThread?.interrupt()
}
override fun onStop() {
super.onStop()
launch {
pauseMutex.lock()
}
}
override val activityLabel: CharSequence
get() = getText(R.string.log_viewer)
override fun onStart() {
super.onStart()
launch {
if ( pauseMutex.isLocked )
pauseMutex.unlock()
}
}
private fun startLiveMode() {
mainList.layoutManager = LinearLayoutManager(this)
mainList.adapter = LiveLogAdapter(this)
mainList.itemAnimator?.addDuration = 100
mainList.itemAnimator?.removeDuration = 100
stop.setOnClickListener {
stopService(LogcatService::class.intent)
finish()
}
bindService(LogcatService::class.intent, connection, Context.BIND_AUTO_CREATE)
}
private fun startFileMode(file: File) {
stop.visibility = View.GONE
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)
throw CancellationException()
}
}
mainList.layoutManager = LinearLayoutManager(this@LogViewerActivity)
mainList.adapter = LogAdapter(this@LogViewerActivity, items)
mainList.adapter!!.notifyItemRangeInserted(0, items.size)
}
}
private fun startLogcatPoll(service: LogcatService) {
launch {
var offset = 0L
while (isActive) {
pauseMutex.lock()
val response = service.pollLogEvent(offset).await()
(mainList.adapter as LiveLogAdapter).insertItems(response.logs)
mainList.apply {
if ( computeVerticalScrollOffset() < 30 )
scrollToPosition(0)
}
offset = response.offset + response.logs.size
pauseMutex.unlock()
delay(200)
}
}
}
}
@@ -0,0 +1,185 @@
package com.github.kr328.clash
import android.content.ComponentName
import android.content.Context
import android.content.ServiceConnection
import android.net.Uri
import android.os.IBinder
import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts
import com.github.kr328.clash.common.compat.startForegroundServiceCompat
import com.github.kr328.clash.common.util.fileName
import com.github.kr328.clash.common.util.intent
import com.github.kr328.clash.common.util.ticker
import com.github.kr328.clash.core.model.LogMessage
import com.github.kr328.clash.design.LogcatDesign
import com.github.kr328.clash.design.dialog.withModelProgressBar
import com.github.kr328.clash.design.model.LogFile
import com.github.kr328.clash.design.ui.ToastDuration
import com.github.kr328.clash.design.util.showExceptionToast
import com.github.kr328.clash.log.LogcatFilter
import com.github.kr328.clash.log.LogcatReader
import com.github.kr328.clash.util.logsDir
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.isActive
import kotlinx.coroutines.selects.select
import kotlinx.coroutines.withContext
import java.io.OutputStreamWriter
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
class LogcatActivity : BaseActivity<LogcatDesign>() {
private var conn: ServiceConnection? = null
override suspend fun main() {
val fileName = intent?.fileName
if (fileName != null) {
val file = LogFile.parseFromFileName(fileName) ?: return showInvalid()
return mainLocalFile(file)
}
return mainStreaming()
}
private suspend fun mainLocalFile(file: LogFile) {
val messages = try {
LogcatReader(this, file).readAll()
} catch (e: Exception) {
return showInvalid()
}
val design = LogcatDesign(this, false)
setContentDesign(design)
design.patchMessages(messages, 0, messages.size)
while (isActive) {
when (design.requests.receive()) {
LogcatDesign.Request.Delete -> {
withContext(Dispatchers.IO) {
logsDir.resolve(file.fileName).delete()
}
finish()
}
LogcatDesign.Request.Export -> {
val output = startActivityForResult(
ActivityResultContracts.CreateDocument("text/plain"),
file.fileName
)
if (output != null) {
try {
withContext(Dispatchers.IO) {
writeLogTo(messages, file, output)
}
design.showToast(R.string.file_exported, ToastDuration.Long)
} catch (e: Exception) {
design.showExceptionToast(e)
}
}
}
else -> Unit
}
}
}
private suspend fun mainStreaming() {
val design = LogcatDesign(this, true)
setContentDesign(design)
startForegroundServiceCompat(LogcatService::class.intent)
val logcat = bindLogcatService()
val ticker = ticker(500)
var initial = true
while (isActive) {
select<Unit> {
events.onReceive {
}
design.requests.onReceive {
when (it) {
LogcatDesign.Request.Close -> {
stopService(LogcatService::class.intent)
finish()
}
else -> Unit
}
}
if (activityStarted) {
ticker.onReceive {
val snapshot = logcat.snapshot(initial) ?: return@onReceive
design.patchMessages(snapshot.messages, snapshot.removed, snapshot.appended)
initial = false
}
}
}
}
}
override fun onDestroy() {
conn?.apply(this::unbindService)
super.onDestroy()
}
private suspend fun bindLogcatService(): LogcatService {
return suspendCoroutine { ctx ->
bindService(LogcatService::class.intent, object : ServiceConnection {
override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
val srv = service!!.queryLocalInterface("") as LogcatService
ctx.resume(srv)
conn = this
}
override fun onServiceDisconnected(name: ComponentName?) {
conn = null
}
}, Context.BIND_AUTO_CREATE)
}
}
@Suppress("BlockingMethodInNonBlockingContext")
private suspend fun writeLogTo(messages: List<LogMessage>, file: LogFile, uri: Uri) {
LogcatFilter(OutputStreamWriter(contentResolver.openOutputStream(uri)), this).use {
withContext(Dispatchers.Main) {
withModelProgressBar {
configure {
isIndeterminate = true
max = messages.size
}
withContext(Dispatchers.IO) {
it.writeHeader(file.date)
messages.forEachIndexed { idx, msg ->
configure {
isIndeterminate = false
progress = idx
}
it.writeMessage(msg)
}
}
}
}
}
}
private fun showInvalid() {
Toast.makeText(this, R.string.invalid_log_file, Toast.LENGTH_LONG).show()
}
}
@@ -1,7 +1,5 @@
package com.github.kr328.clash
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.app.Service
import android.content.ComponentName
@@ -9,49 +7,30 @@ import android.content.Context
import android.content.Intent
import android.content.ServiceConnection
import android.os.Binder
import android.os.Build
import android.os.IBinder
import android.os.IInterface
import androidx.collection.CircularArray
import androidx.core.app.NotificationChannelCompat
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
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.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.intent
import com.github.kr328.clash.utils.format
import com.github.kr328.clash.utils.logsDir
import com.github.kr328.clash.common.compat.getColorCompat
import com.github.kr328.clash.common.compat.pendingIntentFlags
import com.github.kr328.clash.common.log.Log
import com.github.kr328.clash.common.util.intent
import com.github.kr328.clash.core.model.LogMessage
import com.github.kr328.clash.log.LogcatCache
import com.github.kr328.clash.log.LogcatWriter
import com.github.kr328.clash.service.RemoteService
import com.github.kr328.clash.service.remote.ILogObserver
import com.github.kr328.clash.service.remote.IRemoteService
import com.github.kr328.clash.service.remote.unwrap
import com.github.kr328.clash.util.logsDir
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.selects.select
import java.io.FileWriter
import java.io.IOException
import java.util.*
import kotlin.math.max
class LogcatService : Service(), CoroutineScope by MainScope(), IInterface {
companion object {
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_CONTENT_FORMAT = "%d %s %s"
var isServiceRunning: Boolean = false
}
data class Request(val offset: Long, val response: CompletableDeferred<Response>)
data class Response(val offset: Long, val logs: List<LogEvent>)
private val logChannel = Channel<LogEvent>(MAX_CACHE_COUNT)
private val requestChannel = Channel<Request>()
private val cache: CircularArray<LogEvent> = CircularArray()
private var cacheOffset = 0L
private val entity = LogFile.generate()
class LogcatService : Service(), CoroutineScope by CoroutineScope(Dispatchers.Default), IInterface {
private val cache = LogcatCache()
private val connection = object : ServiceConnection {
override fun onServiceDisconnected(name: ComponentName?) {
@@ -59,197 +38,127 @@ class LogcatService : Service(), CoroutineScope by MainScope(), IInterface {
}
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()
}
override fun send(data: ParcelableContainer?) {
val logEvent = (data?.data as LogEvent?) ?: return
if (!logChannel.offer(logEvent))
Log.w("Drop log $logEvent")
}
})
startObserver(service ?: return stopSelf())
}
}
override fun onCreate() {
super.onCreate()
isServiceRunning = true
running = true
createNotificationChannel()
showNotification()
bindService(ClashManagerService::class.intent, connection, Context.BIND_AUTO_CREATE)
launchProcessor()
launchSaveThread()
bindService(RemoteService::class.intent, connection, Context.BIND_AUTO_CREATE)
}
override fun onDestroy() {
logChannel.close()
cancel()
unbindService(connection)
stopForeground(true)
super.onDestroy()
running = false
isServiceRunning = false
super.onDestroy()
}
override fun onBind(intent: Intent?): IBinder? {
override fun onBind(intent: Intent?): IBinder {
return this.asBinder()
}
override fun asBinder(): IBinder {
return object : Binder() {
override fun queryLocalInterface(descriptor: String): IInterface? {
override fun queryLocalInterface(descriptor: String): IInterface {
return this@LogcatService
}
}
}
// Export to UI
suspend fun pollLogEvent(offset: Long): CompletableDeferred<Response> {
val request = Request(offset, CompletableDeferred())
requestChannel.send(request)
return request.response
suspend fun snapshot(full: Boolean): LogcatCache.Snapshot? {
return cache.snapshot(full)
}
private fun launchProcessor() {
launch {
val pendingRequest: MutableList<Request> = mutableListOf()
private fun startObserver(binder: IBinder) {
if (!binder.isBinderAlive)
return stopSelf()
launch(Dispatchers.IO) {
val service = binder.unwrap(IRemoteService::class).clash()
val channel = Channel<LogMessage>(CACHE_CAPACITY)
try {
withContext(Dispatchers.Default) {
logsDir.mkdirs()
LogcatWriter(this@LogcatService).use {
val observer = object : ILogObserver {
override fun newItem(log: LogMessage) {
channel.trySend(log)
}
}
service.setLogObserver(observer)
while (isActive) {
select<Unit> {
logChannel.onReceive {
cache.addLast(it)
val msg = channel.receive()
if (cache.size() > MAX_CACHE_COUNT) {
cache.removeFromStart(1)
cacheOffset++
}
}
requestChannel.onReceive {
pendingRequest.add(it)
}
}
it.appendMessage(msg)
// Handle pending requests
val iterator = pendingRequest.iterator()
while (iterator.hasNext()) {
val request = iterator.next()
if (request.offset >= cacheOffset + cache.size())
continue
val logs = mutableListOf<LogEvent>()
val responseOffset = max(cacheOffset, request.offset)
val begin = (responseOffset - cacheOffset).toInt()
for (i in begin until cache.size())
logs.add(cache[i])
request.response.complete(Response(responseOffset, logs))
iterator.remove()
}
cache.append(msg)
}
}
}
catch (e: Exception) {
return@launch
}
}
}
private fun launchSaveThread() {
launch {
withContext(Dispatchers.IO) {
logsDir.mkdirs()
try {
FileWriter(logsDir.resolve(entity.fileName)).buffered().use { output ->
var offset = 0L
output.write("# Logcat on ${Date(entity.date).format(this@LogcatService)}")
output.newLine()
while (isActive) {
val response = pollLogEvent(offset).await()
if (response.offset != offset) {
output.write("# Lost ${response.offset - offset} items")
output.newLine()
}
response.logs.forEach {
output.write(
LOG_CONTENT_FORMAT.format(
it.time,
it.level,
it.message
)
)
output.newLine()
}
offset = response.offset + response.logs.size
}
} catch (e: IOException) {
Log.e("Write log file: $e", e)
} finally {
withContext(NonCancellable) {
if (binder.isBinderAlive) {
service.setLogObserver(null)
}
} catch (e: IOException) {
Log.w("Logcat file write failure", e)
stopSelf()
}
}
}
}
private fun createNotificationChannel() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O)
return
NotificationManagerCompat.from(this)
.createNotificationChannel(
NotificationChannel(
NOTIFICATION_CHANNEL_ID,
getString(R.string.clash_logcat),
NotificationManager.IMPORTANCE_DEFAULT
)
NotificationChannelCompat.Builder(
CHANNEL_ID,
NotificationManagerCompat.IMPORTANCE_DEFAULT
).setName(getString(R.string.clash_logcat)).build()
)
}
private fun showNotification() {
val notification = NotificationCompat
.Builder(this, NOTIFICATION_CHANNEL_ID)
.setSmallIcon(R.drawable.ic_notification)
.setColor(getColor(R.color.colorAccentService))
.Builder(this, CHANNEL_ID)
.setSmallIcon(R.drawable.ic_logo_service)
.setColor(getColorCompat(R.color.color_clash_light))
.setContentTitle(getString(R.string.clash_logcat))
.setContentText(getString(R.string.running))
.setContentIntent(
PendingIntent.getActivity(
this,
NOTIFICATION_ID,
LogsActivity::class.intent
.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK),
PendingIntent.FLAG_UPDATE_CURRENT
R.id.nf_logcat_status,
LogcatActivity::class.intent
.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_CLEAR_TOP),
pendingIntentFlags(PendingIntent.FLAG_UPDATE_CURRENT)
)
)
.build()
startForeground(NOTIFICATION_ID, notification)
startForeground(R.id.nf_logcat_status, notification)
}
companion object {
private const val CHANNEL_ID = "clash_logcat_channel"
private const val CACHE_CAPACITY = 128
var running: Boolean = false
}
}
@@ -1,261 +1,71 @@
package com.github.kr328.clash
import android.app.Activity
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.util.TypedValue
import android.view.ViewGroup
import androidx.annotation.ColorInt
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.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
import com.google.android.material.snackbar.Snackbar
import kotlinx.android.synthetic.main.activity_logs.*
import com.github.kr328.clash.common.util.intent
import com.github.kr328.clash.common.util.setFileName
import com.github.kr328.clash.design.LogsDesign
import com.github.kr328.clash.design.model.LogFile
import com.github.kr328.clash.util.logsDir
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.isActive
import kotlinx.coroutines.selects.select
import kotlinx.coroutines.withContext
import java.io.FileInputStream
import java.util.*
class LogsActivity : BaseActivity() {
companion object {
const val REQUEST_CODE = 50000
}
private var lastWriteFile: LogFile? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_logs)
setSupportActionBar(toolbar)
if ( LogcatService.isServiceRunning ) {
startActivity(LogViewerActivity::class.intent)
finish()
return
class LogsActivity : BaseActivity<LogsDesign>() {
override suspend fun main() {
if (LogcatService.running) {
return startActivity(LogcatActivity::class.intent)
}
commonUi.build {
option(
title = getString(R.string.clash_logcat),
summary = getString(R.string.tap_to_start),
icon = getDrawable(R.drawable.ic_adb)
) {
onClick {
startForegroundServiceCompat(LogcatService::class.intent)
val design = LogsDesign(this)
startActivity(LogViewerActivity::class.intent)
setContentDesign(design)
finish()
while (isActive) {
select<Unit> {
events.onReceive {
when (it) {
Event.ActivityStart -> {
val files = withContext(Dispatchers.IO) {
loadFiles()
}
design.patchLogs(files)
}
else -> Unit
}
}
}
category(text = getString(R.string.history), id = "history", showTopSeparator = true)
}
design.requests.onReceive {
when (it) {
LogsDesign.Request.StartLogcat -> {
startActivity(LogcatActivity::class.intent)
clearAll.setOnClickListener {
showClearAllDialog()
}
finish()
}
LogsDesign.Request.DeleteAll -> {
if (design.requestDeleteAll()) {
withContext(Dispatchers.IO) {
deleteAllLogs()
}
val adapter = LogFileAdapter(this@LogsActivity,
onItemClicked = {
startActivity(LogViewerActivity::class.intent
.setData(Uri.fromFile(logsDir.resolve(it.fileName))))
},
onMenuClicked = this::showMenu)
val layoutManager = LinearLayoutManager(this@LogsActivity)
mainList.layoutManager = layoutManager
mainList.adapter = adapter
}
override fun onStart() {
super.onStart()
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 ) {
val url = data?.data ?: return
val file = lastWriteFile ?: return
lastWriteFile = null
launch {
withContext(Dispatchers.IO) {
contentResolver.openOutputStream(url)?.use { output ->
FileInputStream(logsDir.resolve(file.fileName)).use { input ->
input.copyTo(output)
events.trySend(Event.ActivityStart)
}
}
is LogsDesign.Request.OpenFile -> {
startActivity(LogcatActivity::class.intent.setFileName(it.file.fileName))
}
}
Snackbar.make(rootView, R.string.file_exported, Snackbar.LENGTH_LONG).show()
}
}
return
}
super.onActivityResult(requestCode, resultCode, data)
}
private fun refreshList() {
launch {
val files = withContext(Dispatchers.IO) {
(logsDir.listFiles() ?: emptyArray())
.asSequence()
.filter { it.name.endsWith(".log") }
.map { LogFile.parseFromFileName(it.name) }
.filterNotNull()
.toList()
}
if (files.isEmpty())
commonUi.screen.requireElement<Category>("history").isHidden = true
val adapter = mainList.adapter as LogFileAdapter
val old = adapter.fileList
val result = withContext(Dispatchers.Default) {
DiffUtil.calculateDiff(object: DiffUtil.Callback() {
override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
return old[oldItemPosition].fileName == files[newItemPosition].fileName
}
override fun getOldListSize(): Int {
return old.size
}
override fun getNewListSize(): Int {
return files.size
}
override fun areContentsTheSame(
oldItemPosition: Int,
newItemPosition: Int
): Boolean {
return old[oldItemPosition] == files[newItemPosition]
}
})
}
adapter.fileList = files
result.dispatchUpdatesTo(adapter)
}
}
private fun showClearAllDialog() {
AlertDialog.Builder(this)
.setTitle(R.string.delete_all_logs)
.setMessage(R.string.delete_all_logs_warn)
.setPositiveButton(R.string.ok) { _, _ -> deleteAllLogs() }
.setNegativeButton(R.string.cancel) { _, _ -> }
.show()
}
private fun showMenu(logFile: LogFile) {
val dialog = BottomSheetDialog(this)
val menu = CommonUiLayout(this).apply {
layoutParams = ViewGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT
)
}
@ColorInt
val errorColor = TypedValue().run {
theme.resolveAttribute(R.attr.colorError, this, true)
data
}
menu.build {
option(
icon = getDrawable(R.drawable.ic_save),
title = getString(R.string.export)) {
onClick {
export(logFile)
dialog.dismiss()
}
}
option(
icon = getDrawable(R.drawable.ic_delete_colorful),
title = getString(R.string.delete)) {
textColor = errorColor
onClick {
delete(logFile)
dialog.dismiss()
}
}
}
}
dialog.dismissWithAnimation = true
dialog.setContentView(menu)
dialog.show()
private fun loadFiles(): List<LogFile> {
val list = cacheDir.resolve("logs").listFiles()?.toList() ?: emptyList()
return list.mapNotNull { LogFile.parseFromFileName(it.name) }
}
private fun deleteAllLogs() {
launch {
withContext(Dispatchers.IO) {
logsDir.deleteRecursively()
}
refreshList()
}
}
private fun export(file: LogFile) {
if ( lastWriteFile != null )
return
val d = Date(file.date)
val exportName = getString(R.string.format_export_log_name, d.format(this))
val intent = Intent(Intent.ACTION_CREATE_DOCUMENT)
.setType("text/plain")
.putExtra(Intent.EXTRA_TITLE, exportName)
lastWriteFile = file
startActivityForResult(intent, REQUEST_CODE)
}
private fun delete(file: LogFile) {
val d = {
launch {
withContext(Dispatchers.IO) {
logsDir.resolve(file.fileName).delete()
}
refreshList()
}
}
AlertDialog.Builder(this)
.setTitle(R.string.delete_log)
.setMessage(getString(R.string.delete_log_warn, file.fileName))
.setPositiveButton(R.string.ok) { _, _ -> d() }
.setNegativeButton(R.string.cancel) { _, _ -> }
.show()
logsDir.deleteRecursively()
}
}
@@ -1,200 +1,151 @@
package com.github.kr328.clash
import android.app.Activity
import android.content.Intent
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.appcompat.app.AlertDialog
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.data.ClashProfileEntity
import com.github.kr328.clash.service.util.intent
import com.github.kr328.clash.service.util.startForegroundServiceCompat
import com.github.kr328.clash.utils.startClashService
import kotlinx.android.synthetic.main.activity_main.*
import kotlinx.coroutines.*
import androidx.activity.result.contract.ActivityResultContracts
import com.github.kr328.clash.common.util.intent
import com.github.kr328.clash.common.util.ticker
import com.github.kr328.clash.design.MainDesign
import com.github.kr328.clash.design.ui.ToastDuration
import com.github.kr328.clash.store.TipsStore
import com.github.kr328.clash.util.startClashService
import com.github.kr328.clash.util.stopClashService
import com.github.kr328.clash.util.withClash
import com.github.kr328.clash.util.withProfile
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import kotlinx.coroutines.selects.select
import kotlinx.coroutines.withContext
import java.util.concurrent.TimeUnit
class MainActivity : BaseActivity() {
companion object {
private const val REQUEST_CODE = 40000
}
class MainActivity : BaseActivity<MainDesign>() {
override suspend fun main() {
val design = MainDesign(this)
private var bandwidthJob: Job? = null
setContentDesign(design)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
status.setOnClickListener {
if (clashRunning) {
stopService(ClashService::class.intent)
} else {
val vpnRequest = startClashService()
if (vpnRequest != null)
startActivityForResult(vpnRequest, REQUEST_CODE)
}
launch(Dispatchers.IO) {
showUpdatedTips(design)
}
proxies.setOnClickListener {
startActivity(ProxiesActivity::class.intent)
}
design.fetch()
profiles.setOnClickListener {
startActivity(ProfilesActivity::class.intent)
}
val ticker = ticker(TimeUnit.SECONDS.toMillis(1))
logs.setOnClickListener {
startActivity(LogsActivity::class.intent)
}
settings.setOnClickListener {
startActivity(SettingsActivity::class.intent)
}
support.setOnClickListener {
startActivity(SupportActivity::class.intent)
}
about.setOnClickListener {
showAboutDialog()
}
}
override fun onStart() {
super.onStart()
updateClashStatus()
}
override fun onStop() {
super.onStop()
stopBandwidthPolling()
}
override val activityLabel: CharSequence? = null
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
if (requestCode == REQUEST_CODE) {
if (resultCode == Activity.RESULT_OK)
startForegroundServiceCompat(ClashService::class.intent)
return
}
super.onActivityResult(requestCode, resultCode, data)
}
override suspend fun onClashStarted() {
updateClashStatus()
}
override suspend fun onClashStopped(reason: String?) {
updateClashStatus()
if (reason != null)
makeSnackbarException(getString(R.string.clash_start_failure), reason)
}
override suspend fun onClashProfileLoaded(profile: ClashProfileEntity) {
updateClashStatus()
}
private fun startBandwidthPolling() {
if (bandwidthJob != null)
return
bandwidthJob = launch {
withClash {
try {
while (clashRunning && isActive) {
val bandwidth = queryBandwidth()
status.summary = getString(
R.string.format_traffic_forwarded,
bandwidth.asBytesString()
)
delay(1000)
while (isActive) {
select<Unit> {
events.onReceive {
when (it) {
Event.ActivityStart,
Event.ServiceRecreated,
Event.ClashStop, Event.ClashStart,
Event.ProfileLoaded, Event.ProfileChanged -> design.fetch()
else -> Unit
}
}
design.requests.onReceive {
when (it) {
MainDesign.Request.ToggleStatus -> {
if (clashRunning)
stopClashService()
else
design.startClash()
}
MainDesign.Request.OpenProxy ->
startActivity(ProxyActivity::class.intent)
MainDesign.Request.OpenProfiles ->
startActivity(ProfilesActivity::class.intent)
MainDesign.Request.OpenProviders ->
startActivity(ProvidersActivity::class.intent)
MainDesign.Request.OpenLogs ->
startActivity(LogsActivity::class.intent)
MainDesign.Request.OpenSettings ->
startActivity(SettingsActivity::class.intent)
MainDesign.Request.OpenHelp ->
startActivity(HelpActivity::class.intent)
MainDesign.Request.OpenAbout ->
design.showAbout(queryAppVersionName())
}
}
if (clashRunning) {
ticker.onReceive {
design.fetchTraffic()
}
} finally {
bandwidthJob = null
}
}
}
}
private fun stopBandwidthPolling() {
bandwidthJob?.cancel()
}
private suspend fun showUpdatedTips(design: MainDesign) {
val tips = TipsStore(this)
private fun updateClashStatus() {
if (clashRunning) {
startBandwidthPolling()
if (tips.primaryVersion != TipsStore.CURRENT_PRIMARY_VERSION) {
tips.primaryVersion = TipsStore.CURRENT_PRIMARY_VERSION
status.setCardBackgroundColor(getColor(R.color.primaryCardColorStarted))
status.icon = getDrawable(R.drawable.ic_started)
status.title = getText(R.string.running)
val pkg = packageManager.getPackageInfo(packageName, 0)
proxies.visibility = View.VISIBLE
} else {
stopBandwidthPolling()
status.setCardBackgroundColor(getColor(R.color.primaryCardColorStopped))
status.icon = getDrawable(R.drawable.ic_stopped)
status.title = getText(R.string.stopped)
status.summary = getText(R.string.tap_to_start)
proxies.visibility = View.GONE
}
launch {
val general = withClash {
queryGeneral()
if (pkg.firstInstallTime != pkg.lastUpdateTime) {
design.showUpdatedTips()
}
val active = withProfile {
queryActiveProfile()
}
val modeResId = when (general.mode) {
General.Mode.DIRECT -> R.string.direct_mode
General.Mode.GLOBAL -> R.string.global_mode
General.Mode.RULE -> R.string.rule_mode
}
val profileString =
if (active == null)
getText(R.string.not_selected)
else
getString(R.string.format_profile_activated, active.name)
proxies.summary = getText(modeResId)
profiles.summary = profileString
}
}
private fun showAboutDialog() {
launch {
val content = withContext(Dispatchers.Default) {
val packageInfo = packageManager.getPackageInfo(packageName, 0)
private suspend fun MainDesign.fetch() {
setClashRunning(clashRunning)
LayoutInflater.from(this@MainActivity)
.inflate(R.layout.dialog_abort, rootView as ViewGroup?, false).apply {
findViewById<View>(android.R.id.icon).background =
getDrawable(R.drawable.ic_logo)
findViewById<TextView>(android.R.id.title).text =
getText(R.string.application_name)
findViewById<TextView>(android.R.id.summary).text = packageInfo.versionName
}
val state = withClash {
queryTunnelState()
}
val providers = withClash {
queryProviders()
}
setMode(state.mode)
setHasProviders(providers.isNotEmpty())
withProfile {
setProfileName(queryActive()?.name)
}
}
private suspend fun MainDesign.fetchTraffic() {
withClash {
setForwarded(queryTrafficTotal())
}
}
private suspend fun MainDesign.startClash() {
val active = withProfile { queryActive() }
if (active == null || !active.imported) {
showToast(R.string.no_profile_selected, ToastDuration.Long) {
setAction(R.string.profiles) {
startActivity(ProfilesActivity::class.intent)
}
}
AlertDialog.Builder(this@MainActivity)
.setView(content)
.show()
return
}
val vpnRequest = startClashService()
try {
if (vpnRequest != null) {
val result = startActivityForResult(
ActivityResultContracts.StartActivityForResult(),
vpnRequest
)
if (result.resultCode == RESULT_OK)
startClashService()
}
} catch (e: Exception) {
design?.showToast(R.string.unable_to_start_vpn, ToastDuration.Long)
}
}
private suspend fun queryAppVersionName(): String {
return withContext(Dispatchers.IO) {
packageManager.getPackageInfo(packageName, 0).versionName
}
}
}
@@ -2,12 +2,11 @@ package com.github.kr328.clash
import android.app.Application
import android.content.Context
import com.github.kr328.clash.core.Global
import com.github.kr328.clash.remote.Broadcasts
import com.github.kr328.clash.common.Global
import com.github.kr328.clash.common.compat.currentProcessName
import com.github.kr328.clash.common.log.Log
import com.github.kr328.clash.remote.Remote
import com.microsoft.appcenter.AppCenter
import com.microsoft.appcenter.analytics.Analytics
import com.microsoft.appcenter.crashes.Crashes
import com.github.kr328.clash.service.util.sendServiceRecreated
@Suppress("unused")
class MainApplication : Application() {
@@ -20,16 +19,18 @@ class MainApplication : Application() {
override fun onCreate() {
super.onCreate()
// Initialize AppCenter
if (BuildConfig.APP_CENTER_KEY.isNotEmpty() && !BuildConfig.DEBUG) {
AppCenter.start(
this,
BuildConfig.APP_CENTER_KEY,
Analytics::class.java, Crashes::class.java
)
}
val processName = currentProcessName
Remote.init(this)
Broadcasts.init(this)
Log.d("Process $processName started")
if (processName == packageName) {
Remote.launch()
} else {
sendServiceRecreated()
}
}
fun finalize() {
Global.destroy()
}
}
@@ -0,0 +1,39 @@
package com.github.kr328.clash
import com.github.kr328.clash.common.util.intent
import com.github.kr328.clash.design.NetworkSettingsDesign
import com.github.kr328.clash.service.store.ServiceStore
import kotlinx.coroutines.isActive
import kotlinx.coroutines.selects.select
class NetworkSettingsActivity : BaseActivity<NetworkSettingsDesign>() {
override suspend fun main() {
val design = NetworkSettingsDesign(
this,
uiStore,
ServiceStore(this),
clashRunning,
)
setContentDesign(design)
while (isActive) {
select<Unit> {
events.onReceive {
when (it) {
Event.ClashStart, Event.ClashStop, Event.ServiceRecreated ->
recreate()
else -> Unit
}
}
design.requests.onReceive {
when (it) {
NetworkSettingsDesign.Request.StartAccessControlList ->
startActivity(AccessControlActivity::class.intent)
}
}
}
}
}
}
@@ -0,0 +1,143 @@
package com.github.kr328.clash
import android.app.Activity
import android.content.ComponentName
import android.content.Intent
import android.net.Uri
import android.provider.Settings
import androidx.activity.result.contract.ActivityResultContracts
import com.github.kr328.clash.common.constants.Intents
import com.github.kr328.clash.common.util.intent
import com.github.kr328.clash.common.util.setUUID
import com.github.kr328.clash.design.NewProfileDesign
import com.github.kr328.clash.design.model.ProfileProvider
import com.github.kr328.clash.service.model.Profile
import com.github.kr328.clash.util.withProfile
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.isActive
import kotlinx.coroutines.selects.select
import kotlinx.coroutines.withContext
import java.util.*
class NewProfileActivity : BaseActivity<NewProfileDesign>() {
private val self: NewProfileActivity
get() = this
override suspend fun main() {
val design = NewProfileDesign(this)
design.patchProviders(queryProfileProviders())
setContentDesign(design)
while (isActive) {
select<Unit> {
events.onReceive {
}
design.requests.onReceive {
when (it) {
is NewProfileDesign.Request.Create -> {
withProfile {
val name = getString(R.string.new_profile)
val uuid: UUID? = when (val p = it.provider) {
is ProfileProvider.File ->
create(Profile.Type.File, name)
is ProfileProvider.Url ->
create(Profile.Type.Url, name)
is ProfileProvider.External -> {
val data = p.get()
if (data != null) {
val (uri, initialName) = data
create(
Profile.Type.External,
initialName ?: name,
uri.toString()
)
} else {
null
}
}
}
if (uuid != null)
launchProperties(uuid)
}
}
is NewProfileDesign.Request.OpenDetail -> {
launchAppDetailed(it.provider)
}
}
}
}
}
}
private fun launchAppDetailed(provider: ProfileProvider.External) {
val data = Uri.fromParts(
"package",
provider.intent.component?.packageName ?: return,
null
)
startActivity(Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).setData(data))
}
private suspend fun launchProperties(uuid: UUID) {
val r = startActivityForResult(
ActivityResultContracts.StartActivityForResult(),
PropertiesActivity::class.intent.setUUID(uuid)
)
if (r.resultCode == Activity.RESULT_OK)
finish()
}
private suspend fun ProfileProvider.External.get(): Pair<Uri, String?>? {
val result = startActivityForResult(
ActivityResultContracts.StartActivityForResult(),
intent
)
if (result.resultCode != RESULT_OK)
return null
val uri = result.data?.data
val name = result.data?.getStringExtra(Intents.EXTRA_NAME)
if (uri != null) {
return uri to name
}
return null
}
private suspend fun queryProfileProviders(): List<ProfileProvider> {
return withContext(Dispatchers.IO) {
val providers = packageManager.queryIntentActivities(
Intent(Intents.ACTION_PROVIDE_URL),
0
).map {
val activity = it.activityInfo
val name = activity.applicationInfo.loadLabel(packageManager)
val summary = activity.loadLabel(packageManager)
val icon = activity.loadIcon(packageManager)
val intent = Intent(Intents.ACTION_PROVIDE_URL)
.setComponent(
ComponentName(
activity.packageName,
activity.name
)
)
ProfileProvider.External(name.toString(), summary.toString(), icon, intent)
}
listOf(ProfileProvider.File(self), ProfileProvider.Url(self)) + providers
}
}
}
@@ -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))
}
}
@@ -0,0 +1,89 @@
package com.github.kr328.clash
import android.content.pm.PackageManager
import com.github.kr328.clash.common.compat.getDrawableCompat
import com.github.kr328.clash.common.constants.Metadata
import com.github.kr328.clash.core.Clash
import com.github.kr328.clash.design.OverrideSettingsDesign
import com.github.kr328.clash.design.model.AppInfo
import com.github.kr328.clash.design.util.toAppInfo
import com.github.kr328.clash.service.store.ServiceStore
import com.github.kr328.clash.util.withClash
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.isActive
import kotlinx.coroutines.selects.select
import kotlinx.coroutines.withContext
class OverrideSettingsActivity : BaseActivity<OverrideSettingsDesign>() {
override suspend fun main() {
val configuration = withClash { queryOverride(Clash.OverrideSlot.Persist) }
val service = ServiceStore(this)
defer {
withClash {
patchOverride(Clash.OverrideSlot.Persist, configuration)
}
}
val design = OverrideSettingsDesign(
this,
configuration
)
setContentDesign(design)
while (isActive) {
select<Unit> {
events.onReceive {
}
design.requests.onReceive {
when (it) {
OverrideSettingsDesign.Request.ResetOverride -> {
if (design.requestResetConfirm()) {
defer {
withClash {
clearOverride(Clash.OverrideSlot.Persist)
}
service.sideloadGeoip = ""
}
finish()
}
}
OverrideSettingsDesign.Request.EditSideloadGeoip -> {
withContext(Dispatchers.IO) {
val list = querySideloadProviders()
val initial = service.sideloadGeoip
val exist = list.any { info -> info.packageName == initial }
service.sideloadGeoip =
design.requestSelectSideload(if (exist) initial else "", list)
}
}
}
}
}
}
}
private fun querySideloadProviders(): List<AppInfo> {
val apps = packageManager.getInstalledPackages(PackageManager.GET_META_DATA)
.filter {
it.applicationInfo.metaData?.containsKey(Metadata.GEOIP_FILE_NAME)
?: false
}
.map { it.toAppInfo(packageManager) }
return listOf(
AppInfo(
packageName = "",
label = getString(R.string.use_built_in),
icon = getDrawableCompat(R.drawable.ic_baseline_work)!!,
installTime = 0,
updateDate = 0,
)
) + apps
}
}
@@ -1,169 +0,0 @@
package com.github.kr328.clash
import android.content.pm.ApplicationInfo
import android.os.Bundle
import android.view.Menu
import android.view.MenuItem
import android.view.View
import androidx.appcompat.widget.SearchView
import androidx.recyclerview.widget.LinearLayoutManager
import com.github.kr328.clash.adapter.PackagesAdapter
import com.github.kr328.clash.service.settings.ServiceSettings
import kotlinx.android.synthetic.main.activity_access_control_packages.*
import kotlinx.coroutines.*
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?
get() = mainList.adapter as PackagesAdapter?
private val refreshChannel = Channel<Unit>(Channel.CONFLATED)
private var keyword: String = ""
private var sort: PackagesAdapter.Sort = PackagesAdapter.Sort.NAME
private var decrease: Boolean = false
private var systemApp: Boolean = true
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_access_control_packages)
setSupportActionBar(toolbar)
launch {
val appsDeferred = async {
withContext(Dispatchers.IO) {
val pm = packageManager
val packages = pm.getInstalledPackages(0)
packages.parallelStream()
.map {
PackagesAdapter.AppInfo(
it.packageName,
it.applicationInfo.loadLabel(pm).toString(),
it.applicationInfo.loadIcon(pm),
it.firstInstallTime, it.lastUpdateTime,
it.applicationInfo.flags and ApplicationInfo.FLAG_SYSTEM != 0
)
}
.toList()
}
}
val selectedDeferred = async {
withContext(Dispatchers.IO) {
ServiceSettings(activity).get(ServiceSettings.ACCESS_CONTROL_PACKAGES)
}
}
val apps = appsDeferred.await()
val selected = selectedDeferred.await()
val adapter = PackagesAdapter(activity, apps)
mainList.adapter = adapter
mainList.layoutManager = LinearLayoutManager(activity)
adapter.selectedPackages.addAll(selected)
progress.visibility = View.GONE
refreshChannel.offer(Unit)
while (isActive) {
refreshChannel.receive()
adapter.applyFilter(keyword, sort, decrease, systemApp)
mainList.scrollToPosition(0)
delay(200)
}
}
}
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
if (!super.onCreateOptionsMenu(menu))
return false
menuInflater.inflate(R.menu.packages, menu)
menu?.apply {
(findItem(R.id.search).actionView as SearchView).apply {
setOnQueryTextListener(object : SearchView.OnQueryTextListener {
override fun onQueryTextSubmit(query: String?) = false
override fun onQueryTextChange(newText: String?): Boolean {
keyword = newText ?: ""
refreshChannel.offer(Unit)
return true
}
})
}
}
return true
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
if (super.onOptionsItemSelected(item))
return true
when (item.itemId) {
R.id.systemApps -> {
item.isChecked = !item.isChecked
systemApp = item.isChecked
}
R.id.sortReverse -> {
item.isChecked = !item.isChecked
decrease = item.isChecked
}
R.id.sortName -> {
item.isChecked = true
sort = PackagesAdapter.Sort.NAME
}
R.id.sortPackageName -> {
item.isChecked = true
sort = PackagesAdapter.Sort.PACKAGE
}
R.id.sortUpdateTime -> {
item.isChecked = true
sort = PackagesAdapter.Sort.UPDATE_TIME
}
R.id.sortInstallTime -> {
item.isChecked = true
sort = PackagesAdapter.Sort.INSTALL_TIME
}
else -> return false
}
refreshChannel.offer(Unit)
return true
}
override fun onStop() {
super.onStop()
val packageList = adapter?.selectedPackages ?: return
GlobalScope.launch {
withContext(Dispatchers.IO) {
ServiceSettings(activity).commit {
put(ServiceSettings.ACCESS_CONTROL_PACKAGES, packageList)
}
}
}
}
}
@@ -1,278 +0,0 @@
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.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.google.android.material.snackbar.Snackbar
import kotlinx.android.synthetic.main.activity_profile_edit.*
import kotlinx.coroutines.launch
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 processing = false
set(value) {
field = value
if (value) {
saving.visibility = View.VISIBLE
save.visibility = View.INVISIBLE
} else {
saving.visibility = View.INVISIBLE
save.visibility = View.VISIBLE
}
}
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)
}
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
if (interval <= 0)
getString(R.string.disabled)
else
getString(R.string.format_seconds, interval)
}
onTextChanged {
val s = it.toString()
if (s.isNotEmpty() && s.toIntOrNull() == null) {
content = ""
Snackbar.make(rootView, R.string.invalid_interval, Snackbar.LENGTH_LONG)
.show()
} else {
modified = true
}
}
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
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
}
processing = true
sendProfileRequest(name, url, interval)
}
}
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
}
AlertDialog.Builder(this)
.setTitle(R.string.exit_without_save)
.setMessage(R.string.exit_without_save_warning)
.setNegativeButton(R.string.cancel) { _, _ -> }
.setPositiveButton(R.string.ok) { _, _ -> finish() }
.show()
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
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
}
} catch (e: Exception) {
makeSnackbarException(getString(R.string.start_url_provider_failure), e.message)
}
return true
}
private fun sendProfileRequest(name: String, url: Uri, interval: Long) {
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()
}
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)
}
}
}
}
@@ -1,210 +1,77 @@
package com.github.kr328.clash
import android.content.Intent
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.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.weight.ProfilesMenu
import kotlinx.android.synthetic.main.activity_profiles.*
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import com.github.kr328.clash.common.util.intent
import com.github.kr328.clash.common.util.setUUID
import com.github.kr328.clash.common.util.ticker
import com.github.kr328.clash.design.ProfilesDesign
import com.github.kr328.clash.service.model.Profile
import com.github.kr328.clash.util.withProfile
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import java.util.*
import kotlinx.coroutines.selects.select
import java.util.concurrent.TimeUnit
class ProfilesActivity : BaseActivity(), ProfileAdapter.Callback, ProfilesMenu.Callback {
companion object {
private const val EDITOR_REQUEST_CODE = 30000
}
class ProfilesActivity : BaseActivity<ProfilesDesign>() {
override suspend fun main() {
val design = ProfilesDesign(this)
private var backgroundJob: Job? = null
private val reloadMutex = Mutex()
private val editorStack = Stack<String>()
setContentDesign(design)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_profiles)
setSupportActionBar(toolbar)
val ticker = ticker(TimeUnit.MINUTES.toMillis(1))
mainList.layoutManager = LinearLayoutManager(this)
mainList.adapter = ProfileAdapter(this, this)
}
while (isActive) {
select<Unit> {
events.onReceive {
when (it) {
Event.ActivityStart, Event.ProfileChanged -> {
design.fetch()
}
else -> Unit
}
}
design.requests.onReceive {
when (it) {
ProfilesDesign.Request.Create ->
startActivity(NewProfileActivity::class.intent)
ProfilesDesign.Request.UpdateAll ->
withProfile {
queryAll().forEach { p ->
if (p.imported && p.type != Profile.Type.File)
update(p.uuid)
}
}
is ProfilesDesign.Request.Update ->
withProfile { update(it.profile.uuid) }
is ProfilesDesign.Request.Delete ->
withProfile { delete(it.profile.uuid) }
is ProfilesDesign.Request.Edit ->
startActivity(PropertiesActivity::class.intent.setUUID(it.profile.uuid))
is ProfilesDesign.Request.Active -> {
withProfile {
if (it.profile.imported)
setActive(it.profile)
else
design.requestSave(it.profile)
}
}
is ProfilesDesign.Request.Duplicate -> {
val uuid = withProfile { clone(it.profile.uuid) }
override fun onStart() {
super.onStart()
backgroundJob = launch {
reloadProfiles()
while (isActive) {
delay(1000 * 60)
// Refresh without animation
(mainList.adapter as ProfileAdapter).notifyDataSetChanged()
}
}
}
override fun onStop() {
super.onStop()
backgroundJob?.cancel()
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()
withProfile {
commitProfileEditUri(uri)
startActivity(PropertiesActivity::class.intent.setUUID(uuid))
}
}
}
if (activityStarted) {
ticker.onReceive {
design.updateElapsed()
}
}
}
return
}
super.onActivityResult(requestCode, resultCode, data)
}
override suspend fun onClashProfileChanged() {
reloadProfiles()
}
private suspend fun reloadProfiles() {
if (!reloadMutex.tryLock())
return
val profiles = withProfile {
queryProfiles()
}
(mainList.adapter as ProfileAdapter)
.setEntitiesAsync(profiles.toList())
reloadMutex.unlock()
}
override fun onProfileClicked(entity: ClashProfileEntity) {
launch {
withProfile {
setActiveProfile(entity.id)
}
}
}
override fun onMenuClicked(entity: ClashProfileEntity) {
ProfilesMenu(this, entity, this).show()
}
override fun onNewProfile() {
startActivity(CreateProfileActivity::class.intent)
}
private fun deleteProfile(entity: ClashProfileEntity) = launch {
val request = ProfileRequest().action(ProfileRequest.Action.REMOVE).withId(entity.id)
private suspend fun ProfilesDesign.fetch() {
withProfile {
enqueueRequest(request)
patchProfiles(queryAll())
}
}
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 startUpdate(entity: ClashProfileEntity) {
val request = ProfileRequest()
.action(ProfileRequest.Action.UPDATE_OR_CREATE)
.withId(entity.id)
val intent = Intent(Intents.INTENT_ACTION_PROFILE_ENQUEUE_REQUEST)
.setComponent(ProfileBackgroundService::class.componentName)
.putExtra(Intents.INTENT_EXTRA_PROFILE_REQUEST, request)
startForegroundServiceCompat(intent)
}
override fun onOpenEditor(entity: ClashProfileEntity) {
openEditor(entity)
}
override fun onUpdate(entity: ClashProfileEntity) {
startUpdate(entity)
}
override fun onOpenProperties(entity: ClashProfileEntity) {
openPropertiesEditor(entity, false)
}
override fun onDuplicate(entity: ClashProfileEntity) {
openPropertiesEditor(entity, true)
}
override fun onResetProvider(entity: ClashProfileEntity) {
resetProviders(entity)
}
override fun onDelete(entity: ClashProfileEntity) {
deleteProfile(entity)
}
}
@@ -0,0 +1,114 @@
package com.github.kr328.clash
import com.github.kr328.clash.common.util.intent
import com.github.kr328.clash.common.util.setUUID
import com.github.kr328.clash.common.util.uuid
import com.github.kr328.clash.design.PropertiesDesign
import com.github.kr328.clash.design.ui.ToastDuration
import com.github.kr328.clash.design.util.showExceptionToast
import com.github.kr328.clash.service.model.Profile
import com.github.kr328.clash.util.withProfile
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import kotlinx.coroutines.selects.select
class PropertiesActivity : BaseActivity<PropertiesDesign>() {
private var canceled: Boolean = false
override suspend fun main() {
setResult(RESULT_CANCELED)
val uuid = intent.uuid ?: return finish()
val design = PropertiesDesign(this)
val original = withProfile { queryByUUID(uuid) } ?: return finish()
design.profile = original
setContentDesign(design)
defer {
canceled = true
withProfile { release(uuid) }
}
while (isActive) {
select<Unit> {
events.onReceive {
when (it) {
Event.ActivityStop -> {
val profile = design.profile
if (!canceled && profile != original) {
withProfile {
patch(profile.uuid, profile.name, profile.source, profile.interval)
}
}
}
Event.ServiceRecreated -> {
finish()
}
else -> Unit
}
}
design.requests.onReceive {
when (it) {
PropertiesDesign.Request.BrowseFiles -> {
startActivity(FilesActivity::class.intent.setUUID(uuid))
}
PropertiesDesign.Request.Commit -> {
design.verifyAndCommit()
}
}
}
}
}
}
override fun onBackPressed() {
design?.apply {
launch {
if (!progressing) {
if (requestExitWithoutSaving())
finish()
}
}
} ?: return super.onBackPressed()
}
private suspend fun PropertiesDesign.verifyAndCommit() {
when {
profile.name.isBlank() -> {
showToast(R.string.empty_name, ToastDuration.Long)
}
profile.type != Profile.Type.File && profile.source.isBlank() -> {
showToast(R.string.invalid_url, ToastDuration.Long)
}
else -> {
try {
withProcessing { updateStatus ->
withProfile {
patch(profile.uuid, profile.name, profile.source, profile.interval)
coroutineScope {
commit(profile.uuid) {
launch {
updateStatus(it)
}
}
}
}
}
setResult(RESULT_OK)
finish()
} catch (e: Exception) {
showExceptionToast(e)
}
}
}
}
}
@@ -0,0 +1,71 @@
package com.github.kr328.clash
import com.github.kr328.clash.common.util.intent
import com.github.kr328.clash.common.util.ticker
import com.github.kr328.clash.design.ProvidersDesign
import com.github.kr328.clash.design.util.showExceptionToast
import com.github.kr328.clash.util.withClash
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import kotlinx.coroutines.selects.select
import java.util.concurrent.TimeUnit
class ProvidersActivity : BaseActivity<ProvidersDesign>() {
override suspend fun main() {
val providers = withClash { queryProviders().sorted() }
val design = ProvidersDesign(this, providers)
setContentDesign(design)
val ticker = ticker(TimeUnit.MINUTES.toMillis(1))
while (isActive) {
select<Unit> {
events.onReceive {
when (it) {
Event.ProfileLoaded -> {
val newList = withClash { queryProviders().sorted() }
if (newList != providers) {
startActivity(ProvidersActivity::class.intent)
finish()
}
}
else -> Unit
}
}
design.requests.onReceive {
when (it) {
is ProvidersDesign.Request.Update -> {
launch {
try {
withClash {
updateProvider(it.provider.type, it.provider.name)
}
design.notifyChanged(it.index)
} catch (e: Exception) {
design.showExceptionToast(
getString(
R.string.format_update_provider_failure,
it.provider.name,
e.message
)
)
design.notifyUpdated(it.index)
}
}
}
}
}
if (activityStarted) {
ticker.onReceive {
design.updateElapsed()
}
}
}
}
}
}
@@ -1,350 +0,0 @@
package com.github.kr328.clash
import android.os.Bundle
import android.view.Menu
import android.view.MenuItem
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.LinearSmoothScroller
import androidx.recyclerview.widget.RecyclerView
import com.github.kr328.clash.adapter.ProxyAdapter
import com.github.kr328.clash.adapter.ProxyChipAdapter
import com.github.kr328.clash.core.model.General
import com.github.kr328.clash.core.model.Proxy
import com.github.kr328.clash.preference.UiSettings
import com.github.kr328.clash.remote.withClash
import com.github.kr328.clash.utils.PrefixMerger
import com.github.kr328.clash.utils.ProxySorter
import com.github.kr328.clash.utils.ScrollBinding
import kotlinx.android.synthetic.main.activity_proxies.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
class ProxiesActivity : BaseActivity(), ScrollBinding.Callback {
private val scrollBinding = ScrollBinding(this, this)
private val doScrollToLastProxy by lazy {
val selected = uiSettings.get(UiSettings.PROXY_LAST_SELECT_GROUP)
launch {
scrollBinding.scrollMaster(selected)
}
}
private val mainListAdapter: ProxyAdapter
get() = mainList.adapter as ProxyAdapter
private val chipListAdapter: ProxyChipAdapter
get() = chipList.adapter as ProxyChipAdapter
private val urlTesting: MutableSet<String> = mutableSetOf()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_proxies)
setSupportActionBar(toolbar)
mainList.adapter = ProxyAdapter(this, { group, proxy ->
launch {
withClash {
setSelectProxy(group, proxy)
}
}
}, {
launch {
urlTesting.add(it)
withClash {
urlTesting.add(it)
startHealthCheck(it)
urlTesting.remove(it)
refreshList()
}
}
})
mainList.layoutManager = mainListAdapter.layoutManager
chipList.adapter = ProxyChipAdapter(this) {
launch {
scrollBinding.scrollMaster(it)
}
}
chipList.layoutManager = LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, false)
chipList.itemAnimator?.changeDuration = 0
launch {
mainList.addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
scrollBinding.sendMasterScrolled()
}
})
scrollBinding.exec()
}
refreshList()
}
override fun onStop() {
uiSettings.commit {
put(UiSettings.PROXY_LAST_SELECT_GROUP,
(chipList.adapter!! as ProxyChipAdapter).selected)
}
super.onStop()
}
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
if (!super.onCreateOptionsMenu(menu))
return false
menuInflater.inflate(R.menu.proxies, menu)
setupMenu()
return true
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
if (super.onOptionsItemSelected(item))
return true
if (item.itemId == R.id.menuRefresh) {
refreshList()
return true
}
launch {
when (item.itemId) {
R.id.modeDirect -> {
withClash {
setProxyMode(General.Mode.DIRECT)
}
}
R.id.modeGlobal -> {
withClash {
setProxyMode(General.Mode.GLOBAL)
}
}
R.id.modeRule -> {
withClash {
setProxyMode(General.Mode.RULE)
}
}
R.id.groupDefault -> {
uiSettings.commit {
put(UiSettings.PROXY_GROUP_SORT, UiSettings.PROXY_SORT_DEFAULT)
}
}
R.id.groupName -> {
uiSettings.commit {
put(UiSettings.PROXY_GROUP_SORT, UiSettings.PROXY_SORT_NAME)
}
}
R.id.groupDelay -> {
uiSettings.commit {
put(UiSettings.PROXY_GROUP_SORT, UiSettings.PROXY_SORT_DELAY)
}
}
R.id.proxyDefault -> {
uiSettings.commit {
put(UiSettings.PROXY_PROXY_SORT, UiSettings.PROXY_SORT_DEFAULT)
}
}
R.id.proxyName -> {
uiSettings.commit {
put(UiSettings.PROXY_PROXY_SORT, UiSettings.PROXY_SORT_NAME)
}
}
R.id.proxyDelay -> {
uiSettings.commit {
put(UiSettings.PROXY_PROXY_SORT, UiSettings.PROXY_SORT_DELAY)
}
}
R.id.utilsMergePrefix -> {
item.isChecked = !item.isChecked
uiSettings.commit {
put(UiSettings.PROXY_MERGE_PREFIX, item.isChecked)
}
refreshList()
return@launch
}
else -> return@launch
}
item.isChecked = true
refreshList()
}
return true
}
override val activityLabel: CharSequence?
get() = getText(R.string.proxy)
override suspend fun onClashStarted() {
finish()
}
private fun setupMenu() {
launch {
val general = withClash {
queryGeneral()
}
menu?.apply {
when (general.mode) {
General.Mode.DIRECT ->
findItem(R.id.modeDirect).isChecked = true
General.Mode.GLOBAL ->
findItem(R.id.modeGlobal).isChecked = true
General.Mode.RULE ->
findItem(R.id.modeRule).isChecked = true
}
when (uiSettings.get(UiSettings.PROXY_GROUP_SORT)) {
UiSettings.PROXY_SORT_DEFAULT ->
findItem(R.id.groupDefault).isChecked = true
UiSettings.PROXY_SORT_NAME ->
findItem(R.id.groupName).isChecked = true
UiSettings.PROXY_SORT_DELAY ->
findItem(R.id.proxyDelay).isChecked = true
}
when (uiSettings.get(UiSettings.PROXY_PROXY_SORT)) {
UiSettings.PROXY_SORT_DEFAULT ->
findItem(R.id.proxyDefault).isChecked = true
UiSettings.PROXY_SORT_NAME ->
findItem(R.id.proxyName).isChecked = true
UiSettings.PROXY_SORT_DELAY ->
findItem(R.id.proxyDelay).isChecked = true
}
findItem(R.id.utilsMergePrefix).isChecked =
uiSettings.get(UiSettings.PROXY_MERGE_PREFIX)
}
}
}
private fun refreshList() {
launch {
val general = withClash {
queryGeneral()
}
val proxies = withClash {
queryAllProxyGroups()
}
val prefixDeferred = async {
if (uiSettings.get(UiSettings.PROXY_MERGE_PREFIX)) {
proxies.map {
async { PrefixMerger.merge(it.proxies.map { p -> it.name to p.name }) { it.second } }
}.flatMap {
it.await()
}.map {
it.value to it
}.toMap()
} else emptyMap()
}
val sortDeferred = async {
val groupSort = when (uiSettings.get(UiSettings.PROXY_GROUP_SORT)) {
UiSettings.PROXY_SORT_DEFAULT ->
ProxySorter.Order.DEFAULT
UiSettings.PROXY_SORT_NAME ->
ProxySorter.Order.NAME_INCREASE
UiSettings.PROXY_SORT_DELAY ->
ProxySorter.Order.DELAY_INCREASE
else -> throw IllegalArgumentException()
}
val proxySort = when (uiSettings.get(UiSettings.PROXY_PROXY_SORT)) {
UiSettings.PROXY_SORT_DEFAULT ->
ProxySorter.Order.DEFAULT
UiSettings.PROXY_SORT_NAME ->
ProxySorter.Order.NAME_INCREASE
UiSettings.PROXY_SORT_DELAY ->
ProxySorter.Order.DELAY_INCREASE
else -> throw IllegalArgumentException()
}
val sorter = ProxySorter(groupSort, proxySort)
sorter.sort(proxies).run {
when (general.mode) {
General.Mode.GLOBAL -> this
General.Mode.DIRECT -> emptyList()
General.Mode.RULE -> this.filter { it.name != "GLOBAL" }
}
}
}
val prefix = prefixDeferred.await()
val sorted = sortDeferred.await()
val newList = withContext(Dispatchers.Default) {
sorted.map {
ProxyAdapter.ProxyGroupInfo(it.name,
it.proxies.map { p ->
val r = prefix.getOrElse(it.name to p.name) {
PrefixMerger.Result(p.name, "", p)
}
val data = if ( r.content.isEmpty() ) {
r.prefix to p.type.toString()
}
else {
r.content to r.prefix
}
ProxyAdapter.ProxyInfo(
p.name,
it.name,
data.first,
data.second,
p.delay.toShort(),
it.type == Proxy.Type.SELECT,
p.name == it.current
)
}
)
}
}
mainListAdapter.applyChange(newList, urlTesting)
(chipList.adapter!! as ProxyChipAdapter).apply {
chips = sorted.map { it.name }
notifyDataSetChanged()
}
doScrollToLastProxy
}
}
override fun getCurrentMasterToken(): String {
return mainListAdapter.getCurrentGroup()
}
override fun onMasterTokenChanged(token: String) {
chipListAdapter.selected = token
val position = chipListAdapter.chips.indexOf(token)
if (position < 0)
return
chipList.smoothScrollToPosition(position)
}
override fun getMasterTokenPosition(token: String): Int {
return mainListAdapter.getGroupPosition(token)
}
override fun doMasterScroll(scroller: LinearSmoothScroller) {
mainListAdapter.layoutManager.startSmoothScroll(scroller)
}
}
@@ -0,0 +1,133 @@
package com.github.kr328.clash
import com.github.kr328.clash.common.util.intent
import com.github.kr328.clash.core.Clash
import com.github.kr328.clash.core.model.Proxy
import com.github.kr328.clash.design.ProxyDesign
import com.github.kr328.clash.design.model.ProxyState
import com.github.kr328.clash.store.TipsStore
import com.github.kr328.clash.util.withClash
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import kotlinx.coroutines.selects.select
import kotlinx.coroutines.sync.Semaphore
import kotlinx.coroutines.sync.withPermit
import java.util.concurrent.TimeUnit
class ProxyActivity : BaseActivity<ProxyDesign>() {
override suspend fun main() {
val mode = withClash { queryOverride(Clash.OverrideSlot.Session).mode }
val names = withClash { queryProxyGroupNames(uiStore.proxyExcludeNotSelectable) }
val states = List(names.size) { ProxyState("?") }
val unorderedStates = names.indices.map { names[it] to states[it] }.toMap()
val reloadLock = Semaphore(10)
val tips = TipsStore(this)
val design = ProxyDesign(
this,
mode,
names,
uiStore
)
setContentDesign(design)
launch(Dispatchers.IO) {
val pkg = packageManager.getPackageInfo(packageName, 0)
val validate = System.currentTimeMillis() - pkg.firstInstallTime > TimeUnit.DAYS.toMillis(5)
if (tips.requestDonate && validate) {
tips.requestDonate = false
design.requestDonate()
}
}
design.requests.send(ProxyDesign.Request.ReloadAll)
while (isActive) {
select<Unit> {
events.onReceive {
when (it) {
Event.ProfileLoaded -> {
val newNames = withClash {
queryProxyGroupNames(uiStore.proxyExcludeNotSelectable)
}
if (newNames != names) {
startActivity(ProxyActivity::class.intent)
finish()
}
}
else -> Unit
}
}
design.requests.onReceive {
when (it) {
ProxyDesign.Request.ReLaunch -> {
startActivity(ProxyActivity::class.intent)
finish()
}
ProxyDesign.Request.ReloadAll -> {
names.indices.forEach { idx ->
design.requests.trySend(ProxyDesign.Request.Reload(idx))
}
}
is ProxyDesign.Request.Reload -> {
launch {
val group = reloadLock.withPermit {
withClash {
queryProxyGroup(names[it.index], uiStore.proxySort)
}
}
val state = states[it.index]
state.now = group.now
design.updateGroup(
it.index,
group.proxies,
group.type == Proxy.Type.Selector,
state,
unorderedStates
)
}
}
is ProxyDesign.Request.Select -> {
withClash {
patchSelector(names[it.index], it.name)
states[it.index].now = it.name
}
design.requestRedrawVisible()
}
is ProxyDesign.Request.UrlTest -> {
launch {
withClash {
healthCheck(names[it.index])
}
design.requests.send(ProxyDesign.Request.Reload(it.index))
}
}
is ProxyDesign.Request.PatchMode -> {
design.showModeSwitchTips()
withClash {
val o = queryOverride(Clash.OverrideSlot.Session)
o.mode = it.mode
patchOverride(Clash.OverrideSlot.Session, o)
}
}
}
}
}
}
}
}
@@ -0,0 +1,18 @@
package com.github.kr328.clash
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import com.github.kr328.clash.service.StatusProvider
import com.github.kr328.clash.util.startClashService
class RestartReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
when (intent.action) {
Intent.ACTION_BOOT_COMPLETED, Intent.ACTION_MY_PACKAGE_REPLACED -> {
if (StatusProvider.shouldStartClashOnBoot)
context.startClashService()
}
}
}
}
@@ -1,46 +1,32 @@
package com.github.kr328.clash
import android.os.Bundle
import com.github.kr328.clash.service.util.intent
import kotlinx.android.synthetic.main.activity_settings.*
import com.github.kr328.clash.common.util.intent
import com.github.kr328.clash.design.SettingsDesign
import kotlinx.coroutines.isActive
import kotlinx.coroutines.selects.select
class SettingsActivity: BaseActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_settings)
setSupportActionBar(toolbar)
class SettingsActivity : BaseActivity<SettingsDesign>() {
override suspend fun main() {
val design = SettingsDesign(this)
commonUi.build {
option(
icon = getDrawable(R.drawable.ic_settings_applications),
title = getString(R.string.behavior)) {
paddingHeight = true
setContentDesign(design)
while (isActive) {
select<Unit> {
events.onReceive {
onClick {
startActivity(SettingsBehaviorActivity::class.intent)
}
}
option(
icon = getDrawable(R.drawable.ic_network),
title = getString(R.string.network)) {
paddingHeight = true
onClick {
startActivity(SettingsNetworkActivity::class.intent)
}
}
option(
icon = getDrawable(R.drawable.ic_interface),
title = getString(R.string.interface_)) {
paddingHeight = true
onClick {
startActivity(SettingsInterfaceActivity::class.intent)
design.requests.onReceive {
when (it) {
SettingsDesign.Request.StartApp ->
startActivity(AppSettingsActivity::class.intent)
SettingsDesign.Request.StartNetwork ->
startActivity(NetworkSettingsActivity::class.intent)
SettingsDesign.Request.StartOverride ->
startActivity(OverrideSettingsActivity::class.intent)
}
}
}
}
}
override val activityLabel: CharSequence?
get() = getText(R.string.settings)
}
@@ -1,20 +0,0 @@
package com.github.kr328.clash
import android.os.Bundle
import com.github.kr328.clash.settings.BehaviorFragment
class SettingsBehaviorActivity: BaseActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_fragment)
setSupportActionBar(findViewById(R.id.toolbar))
supportFragmentManager.beginTransaction()
.replace(R.id.fragment, BehaviorFragment())
.commit()
}
override val activityLabel: CharSequence?
get() = getText(R.string.behavior)
}
@@ -1,20 +0,0 @@
package com.github.kr328.clash
import android.os.Bundle
import com.github.kr328.clash.settings.InterfaceFragment
class SettingsInterfaceActivity: BaseActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_fragment)
setSupportActionBar(findViewById(R.id.toolbar))
supportFragmentManager.beginTransaction()
.replace(R.id.fragment, InterfaceFragment())
.commit()
}
override val activityLabel: CharSequence?
get() = getText(R.string.interface_)
}
@@ -1,24 +0,0 @@
package com.github.kr328.clash
import android.os.Bundle
import com.github.kr328.clash.settings.NetworkFragment
import com.google.android.material.snackbar.Snackbar
class SettingsNetworkActivity : BaseActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_fragment)
setSupportActionBar(findViewById(R.id.toolbar))
supportFragmentManager.beginTransaction()
.replace(R.id.fragment, NetworkFragment())
.commit()
if ( clashRunning )
Snackbar.make(rootView, R.string.options_unavailable, Snackbar.LENGTH_INDEFINITE).show()
}
override val activityLabel: CharSequence?
get() = getText(R.string.network)
}
@@ -1,81 +0,0 @@
package com.github.kr328.clash
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import androidx.core.content.getSystemService
import com.google.android.material.snackbar.Snackbar
import kotlinx.android.synthetic.main.activity_support.*
class SupportActivity: BaseActivity() {
override val activityLabel: CharSequence?
get() = getText(R.string.support)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_support)
setSupportActionBar(toolbar)
commonUi.build {
category(text = getString(R.string.sources))
option(
title = getString(R.string.clash),
summary = getString(R.string.clash_url)) {
onClick {
startActivity(Intent(Intent.ACTION_VIEW)
.setData(Uri.parse(getString(R.string.clash_url))))
}
}
option(
title = getString(R.string.clash_for_android),
summary = getString(R.string.clash_for_android_url)
) {
onClick {
startActivity(Intent(Intent.ACTION_VIEW)
.setData(Uri.parse(getString(R.string.clash_for_android_url))))
}
}
category(text = getString(R.string.contacts))
option(
title = getString(R.string.email),
summary = getString(R.string.email_url)
) {
onClick {
val data =
ClipData.newPlainText("email", getText(R.string.email_url))
getSystemService<ClipboardManager>()?.setPrimaryClip(data)
Snackbar.make(rootView, getText(R.string.copied), Snackbar.LENGTH_SHORT).show()
}
}
option(
title = getString(R.string.github_issues),
summary = getString(R.string.github_issues_url)
) {
onClick {
startActivity(Intent(Intent.ACTION_VIEW)
.setData(Uri.parse(getString(R.string.github_issues_url))))
}
}
if ( resources.configuration.locales.get(0)
.language.equals("zh", true) ) {
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))))
}
}
}
}
}
}
@@ -4,96 +4,101 @@ import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.graphics.drawable.Icon
import android.os.Build
import android.service.quicksettings.Tile
import android.service.quicksettings.TileService
import com.github.kr328.clash.remote.RemoteUtils
import com.github.kr328.clash.service.ClashService
import com.github.kr328.clash.service.Intents
import com.github.kr328.clash.service.data.ClashProfileEntity
import com.github.kr328.clash.service.util.intent
import com.github.kr328.clash.utils.startClashService
import androidx.annotation.RequiresApi
import com.github.kr328.clash.common.constants.Intents
import com.github.kr328.clash.common.constants.Permissions
import com.github.kr328.clash.remote.StatusClient
import com.github.kr328.clash.util.startClashService
import com.github.kr328.clash.util.stopClashService
@RequiresApi(Build.VERSION_CODES.N)
class TileService : TileService() {
private var currentProfile = ""
private var clashRunning = false
override fun onClick() {
val tile = qsTile
val tile = qsTile ?: return
when (tile.state) {
Tile.STATE_INACTIVE -> {
startClashService()
}
Tile.STATE_ACTIVE -> {
stopService(ClashService::class.intent)
stopClashService()
}
}
}
override fun onStartListening() {
refreshStatus()
super.onStartListening()
registerReceiver(
receiver,
IntentFilter().apply {
addAction(Intents.ACTION_CLASH_STARTED)
addAction(Intents.ACTION_CLASH_STOPPED)
addAction(Intents.ACTION_PROFILE_LOADED)
addAction(Intents.ACTION_SERVICE_RECREATED)
},
Permissions.RECEIVE_SELF_BROADCASTS,
null
)
val name = StatusClient(this).currentProfile()
clashRunning = name != null
currentProfile = name ?: ""
updateTile()
}
private fun refreshStatus() {
if (qsTile == null)
return
override fun onStopListening() {
super.onStopListening()
qsTile.state = if ( clashRunning )
unregisterReceiver(receiver)
}
private fun updateTile() {
val tile = qsTile ?: return
tile.state = if (clashRunning)
Tile.STATE_ACTIVE
else
Tile.STATE_INACTIVE
qsTile.label = if ( currentProfile.isEmpty() )
tile.label = if (currentProfile.isEmpty())
getText(R.string.launch_name)
else
currentProfile
qsTile.updateTile()
tile.icon = Icon.createWithResource(this, R.drawable.ic_logo_service)
tile.updateTile()
}
private val clashStatusReceiver = object : BroadcastReceiver() {
private val receiver = object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
when ( intent?.action ) {
Intents.INTENT_ACTION_CLASH_STARTED -> {
when (intent?.action) {
Intents.ACTION_CLASH_STARTED -> {
clashRunning = true
currentProfile = ""
}
Intents.INTENT_ACTION_CLASH_STOPPED -> {
Intents.ACTION_CLASH_STOPPED, Intents.ACTION_SERVICE_RECREATED -> {
clashRunning = false
currentProfile = ""
}
Intents.INTENT_ACTION_PROFILE_LOADED -> {
val entity = intent.
getParcelableExtra<ClashProfileEntity>(Intents.INTENT_EXTRA_PROFILE)
currentProfile = entity?.name ?: ""
Intents.ACTION_PROFILE_LOADED -> {
currentProfile = StatusClient(this@TileService).currentProfile() ?: ""
}
}
refreshStatus()
updateTile()
}
}
override fun onCreate() {
super.onCreate()
registerReceiver(
clashStatusReceiver,
IntentFilter().apply {
addAction(Intents.INTENT_ACTION_CLASH_STARTED)
addAction(Intents.INTENT_ACTION_CLASH_STOPPED)
addAction(Intents.INTENT_ACTION_PROFILE_LOADED)
}
)
clashRunning = RemoteUtils.detectClashRunning(this)
}
override fun onDestroy() {
super.onDestroy()
unregisterReceiver(clashStatusReceiver)
}
}
@@ -1,56 +0,0 @@
package com.github.kr328.clash.adapter
import android.content.Context
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.collection.CircularArray
import androidx.recyclerview.widget.RecyclerView
import com.github.kr328.clash.R
import com.github.kr328.clash.core.event.LogEvent
class LiveLogAdapter(private val context: Context) : RecyclerView.Adapter<LogAdapter.Holder>() {
companion object {
const val MAX_LOG_ITEMS = 100
}
private val circularArray = CircularArray<LogEvent>(MAX_LOG_ITEMS)
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): LogAdapter.Holder {
return LogAdapter.Holder(
LayoutInflater.from(context).inflate(
R.layout.adapter_log,
parent,
false
)
)
}
override fun getItemCount(): Int {
return circularArray.size()
}
override fun onBindViewHolder(holder: LogAdapter.Holder, position: Int) {
holder.bind(circularArray[position])
}
fun insertItems(i: List<LogEvent>) {
val items = if ( i.size > MAX_LOG_ITEMS ) {
i.subList(i.size - MAX_LOG_ITEMS, i.size)
}
else i
val predictSize = items.size + circularArray.size()
if ( predictSize > MAX_LOG_ITEMS ) {
val removeSize = predictSize - MAX_LOG_ITEMS
notifyItemRangeRemoved(MAX_LOG_ITEMS - removeSize, removeSize)
circularArray.removeFromEnd(removeSize)
}
items.forEach {
circularArray.addFirst(it)
}
notifyItemRangeInserted(0, items.size)
}
}
@@ -1,41 +0,0 @@
package com.github.kr328.clash.adapter
import android.content.Context
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
import com.github.kr328.clash.R
import com.github.kr328.clash.core.event.LogEvent
import com.github.kr328.clash.utils.format
import java.util.*
class LogAdapter(
private val context: Context,
private val logs: List<LogEvent>
) : RecyclerView.Adapter<LogAdapter.Holder>() {
class Holder(view: View) : RecyclerView.ViewHolder(view) {
private val level: TextView = view.findViewById(R.id.level)
private val time: TextView = view.findViewById(R.id.time)
private val payload: TextView = view.findViewById(R.id.payload)
fun bind(logEvent: LogEvent) {
level.text = logEvent.level.toString()
time.text = Date(logEvent.time).format(itemView.context, includeDate = false)
payload.text = logEvent.message
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): Holder {
return Holder(LayoutInflater.from(context).inflate(R.layout.adapter_log, parent, false))
}
override fun getItemCount(): Int {
return logs.size
}
override fun onBindViewHolder(holder: Holder, position: Int) {
holder.bind(logs[position])
}
}
@@ -1,55 +0,0 @@
package com.github.kr328.clash.adapter
import android.content.Context
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
import com.github.kr328.clash.R
import com.github.kr328.clash.model.LogFile
import com.github.kr328.clash.utils.format
import java.util.*
class LogFileAdapter(
private val context: Context,
private val onItemClicked: (LogFile) -> Unit,
private val onMenuClicked: (LogFile) -> Unit
) : RecyclerView.Adapter<LogFileAdapter.Holder>() {
var fileList: List<LogFile> = emptyList()
class Holder(view: View) : RecyclerView.ViewHolder(view) {
val root: View = view.findViewById(R.id.root)
val fileName: TextView = view.findViewById(R.id.fileName)
val date: TextView = view.findViewById(R.id.date)
val menu: View = view.findViewById(R.id.menu)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): Holder {
return Holder(
LayoutInflater.from(context).inflate(
R.layout.adapter_log_file,
parent,
false
)
)
}
override fun getItemCount(): Int {
return fileList.size
}
override fun onBindViewHolder(holder: Holder, position: Int) {
val current = fileList[position]
val date = Date(current.date)
holder.fileName.text = current.fileName
holder.date.text = date.format(context)
holder.menu.setOnClickListener {
onMenuClicked(current)
}
holder.root.setOnClickListener {
onItemClicked(current)
}
}
}
@@ -1,128 +0,0 @@
package com.github.kr328.clash.adapter
import android.content.Context
import android.graphics.drawable.Drawable
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView
import com.github.kr328.clash.R
import com.google.android.material.checkbox.MaterialCheckBox
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import kotlin.streams.toList
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)
enum class Sort {
NAME, PACKAGE, INSTALL_TIME, UPDATE_TIME
}
val selectedPackages: MutableSet<String> = mutableSetOf()
private var filteredCache: List<AppInfo> = apps
inner class Holder(view: View) : RecyclerView.ViewHolder(view) {
val root: View = view.findViewById(R.id.root)
val icon: ImageView = view.findViewById(R.id.icon)
val label: TextView = view.findViewById(R.id.label)
val packageName: TextView = view.findViewById(R.id.packageName)
val checkbox: MaterialCheckBox = view.findViewById(R.id.checkbox)
}
suspend fun applyFilter(keyword: String, sort: Sort, decrease: Boolean, systemApp: Boolean) {
withContext(Dispatchers.Default) {
val newList = apps.parallelStream()
.filter {
(it.label.contains(keyword, true)
|| it.packageName.contains(keyword, true))
&& (systemApp || !it.isSystem)
}
.sorted { a, b ->
val sA = selectedPackages.contains(a.packageName)
val sB = selectedPackages.contains(b.packageName)
if ( sA != sB ) {
when {
sA -> return@sorted -1
sB -> return@sorted 1
}
}
val result = when (sort) {
Sort.NAME -> a.label.compareTo(b.label, true)
Sort.PACKAGE -> a.packageName.compareTo(b.packageName)
Sort.INSTALL_TIME -> (a.installTime - b.installTime).toInt()
Sort.UPDATE_TIME -> (a.updateTime - b.updateTime).toInt()
}
if (decrease) -result else result
}
.toList()
val oldList = filteredCache
val result = DiffUtil.calculateDiff(object : DiffUtil.Callback() {
override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
return oldList[oldItemPosition].packageName == newList[newItemPosition].packageName
}
override fun getOldListSize(): Int {
return oldList.size
}
override fun getNewListSize(): Int {
return newList.size
}
override fun areContentsTheSame(
oldItemPosition: Int,
newItemPosition: Int
): Boolean {
return areItemsTheSame(oldItemPosition, newItemPosition) &&
(selectedPackages.contains(oldList[oldItemPosition].packageName) ==
selectedPackages.contains(newList[newItemPosition].packageName))
}
})
withContext(Dispatchers.Main) {
filteredCache = newList
result.dispatchUpdatesTo(this@PackagesAdapter)
}
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): Holder {
return Holder(LayoutInflater.from(context).inflate(R.layout.adapter_package, parent, false))
}
override fun getItemCount(): Int {
return filteredCache.size
}
override fun onBindViewHolder(holder: Holder, position: Int) {
val current = filteredCache[position]
holder.icon.setImageDrawable(current.icon)
holder.label.text = current.label
holder.packageName.text = current.packageName
holder.checkbox.isChecked = selectedPackages.contains(current.packageName)
holder.root.setOnClickListener {
if ( selectedPackages.contains(current.packageName) ) {
selectedPackages.remove(current.packageName)
}
else {
selectedPackages.add(current.packageName)
}
notifyItemChanged(filteredCache.indexOf(current))
}
}
}
@@ -1,137 +0,0 @@
package com.github.kr328.clash.adapter
import android.content.Context
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.RadioButton
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.utils.IntervalUtils
import kotlinx.coroutines.Dispatchers
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 onNewProfile()
}
private var entities: List<ClashProfileEntity> = emptyList()
class EntityHolder(view: View) : RecyclerView.ViewHolder(view) {
val root: View = view.findViewById(R.id.root)
val menu: View = view.findViewById(R.id.menu)
val radio: RadioButton = view.findViewById(R.id.radio)
val name: TextView = view.findViewById(R.id.name)
val type: TextView = view.findViewById(R.id.type)
val interval: TextView = view.findViewById(R.id.interval)
}
class FooterHolder(view: View) : RecyclerView.ViewHolder(view) {
val root: View = view.findViewById(R.id.root)
}
suspend fun setEntitiesAsync(new: List<ClashProfileEntity>) {
val old = withContext(Dispatchers.Main) {
entities
}
val result = withContext(Dispatchers.Default) {
DiffUtil.calculateDiff(object : DiffUtil.Callback() {
override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean =
old[oldItemPosition].id == new[newItemPosition].id
override fun areContentsTheSame(
oldItemPosition: Int,
newItemPosition: Int
): Boolean = old[oldItemPosition] == new[newItemPosition]
override fun getOldListSize(): Int = old.size
override fun getNewListSize(): Int = new.size
}, false)
}
withContext(Dispatchers.Main) {
entities = new
result.dispatchUpdatesTo(this@ProfileAdapter)
}
}
override fun getItemViewType(position: Int): Int {
return if (position == entities.size)
Int.MAX_VALUE
else
super.getItemViewType(position)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
if (viewType == Int.MAX_VALUE) {
return FooterHolder(
LayoutInflater.from(context).inflate(
R.layout.adapter_profile_footer,
parent,
false
)
)
}
return EntityHolder(
LayoutInflater.from(context).inflate(
R.layout.adapter_profile_entity,
parent,
false
)
)
}
override fun getItemCount(): Int {
return entities.size + 1
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
when (holder) {
is EntityHolder -> {
val current = entities[position]
holder.radio.isChecked = current.active
holder.name.text = current.name
holder.type.text = getTypeName(current.type)
holder.interval.text = offsetDate(current.lastUpdate)
holder.root.setOnClickListener {
callback.onProfileClicked(current)
}
holder.menu.setOnClickListener {
callback.onMenuClicked(current)
}
}
is FooterHolder -> {
holder.root.setOnClickListener {
callback.onNewProfile()
}
}
}
}
private fun getTypeName(type: Int): CharSequence {
return when (type) {
ClashProfileEntity.TYPE_FILE ->
context.getText(R.string.file)
ClashProfileEntity.TYPE_URL ->
context.getText(R.string.url)
ClashProfileEntity.TYPE_EXTERNAL ->
context.getText(R.string.external)
else ->
context.getText(R.string.unknown)
}
}
private fun offsetDate(date: Long): CharSequence {
return IntervalUtils.intervalString(context,System.currentTimeMillis() - date)
}
}
@@ -1,272 +0,0 @@
package com.github.kr328.clash.adapter
import android.content.Context
import android.graphics.Color
import android.util.TypedValue
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.annotation.ColorInt
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.github.kr328.clash.R
import com.google.android.material.card.MaterialCardView
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.withContext
class ProxyAdapter(
private val context: Context,
val onSelect: (String, String) -> Unit,
val onUrlTest: (String) -> Unit
) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
companion object {
const val DEFAULT_SPAN_COUNT = 2
}
data class ProxyGroupInfo(
val name: String,
val proxies: List<ProxyInfo>
)
data class ProxyInfo(
val name: String,
val group: String,
val prefix: String,
val content: String,
val delay: Short,
val selectable: Boolean,
val active: Boolean
)
interface RenderInfo {
val name: String
val group: String
}
private data class ProxyGroupRenderInfo(val info: ProxyGroupInfo) :
RenderInfo {
override val name: String
get() = info.name
override val group: String
get() = info.name
}
private data class ProxyRenderInfo(val info: ProxyInfo) : RenderInfo {
override val name: String
get() = info.name
override val group: String
get() = info.group
}
private var rootMutex = Mutex()
private var urlTesting: Set<String> = emptySet()
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
init {
val typedValue = TypedValue()
context.theme.resolveAttribute(R.attr.colorSurface, typedValue, true)
colorSurface = typedValue.data
context.theme.resolveAttribute(R.attr.colorOnSurface, typedValue, true)
colorOnSurface = typedValue.data
}
val layoutManager = GridLayoutManager(context, DEFAULT_SPAN_COUNT).apply {
spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() {
override fun getSpanSize(position: Int): Int {
return when (renderList[position]) {
is ProxyGroupRenderInfo -> spanCount
is ProxyRenderInfo -> 1
else -> throw IllegalArgumentException()
}
}
}
}
private var root = listOf<ProxyGroupInfo>()
private class ProxyGroupHeader(view: View) : RecyclerView.ViewHolder(view) {
val name: TextView = view.findViewById(R.id.name)
val urlTest: View = view.findViewById(R.id.urlTest)
val urlTestProgress: View = view.findViewById(R.id.urlTestProgress)
}
private class ProxyItem(view: View) : RecyclerView.ViewHolder(view) {
val root: MaterialCardView = view.findViewById(R.id.root)
val prefix: TextView = view.findViewById(R.id.prefix)
val content: TextView = view.findViewById(R.id.content)
val delay: TextView = view.findViewById(R.id.delay)
}
suspend fun applyChange(newList: List<ProxyGroupInfo>, testing: Set<String>) =
withContext(Dispatchers.Default) {
rootMutex.lock()
val newRenderList = newList
.flatMap {
listOf(ProxyGroupRenderInfo(it)) + it.proxies.map { p -> ProxyRenderInfo(p) }
}
val oldRenderList = renderList
val result = DiffUtil.calculateDiff(object : DiffUtil.Callback() {
override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int) =
oldRenderList[oldItemPosition]::class == newRenderList[newItemPosition]::class &&
oldRenderList[oldItemPosition].name == newRenderList[newItemPosition].name
override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int) =
oldRenderList[oldItemPosition] == newRenderList[newItemPosition]
override fun getOldListSize(): Int = oldRenderList.size
override fun getNewListSize(): Int = newRenderList.size
})
val groupCache: MutableMap<String, Int> = mutableMapOf()
val activeCache: MutableMap<String, Int> = mutableMapOf()
newRenderList.forEachIndexed { index, it ->
when (it) {
is ProxyGroupRenderInfo ->
groupCache[it.name] = index
is ProxyRenderInfo -> {
if (it.info.active)
activeCache[it.name] = index
}
}
}
withContext(Dispatchers.Main) {
root = newList
renderList = newRenderList.toMutableList()
urlTesting = testing
groupPosition = groupCache
activeList = activeCache
result.dispatchUpdatesTo(this@ProxyAdapter)
}
rootMutex.unlock()
}
fun getGroupPosition(name: String): Int {
return groupPosition[name] ?: -1
}
fun getCurrentGroup(): String {
val position = layoutManager.findFirstCompletelyVisibleItemPosition()
if (position < 0)
return ""
return renderList[position].group
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
val layoutInflater = LayoutInflater.from(context)
return when (viewType) {
1 -> ProxyGroupHeader(
layoutInflater
.inflate(R.layout.adapter_grid_proxy_group, parent, false)
)
2 -> ProxyItem(
layoutInflater
.inflate(R.layout.adapter_grid_proxy, parent, false)
)
else -> throw IllegalArgumentException()
}
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
when (holder) {
is ProxyGroupHeader -> {
val current = renderList[position] as ProxyGroupRenderInfo
groupPosition[current.name] = position
holder.name.text = current.info.name
holder.urlTest.setOnClickListener {
holder.urlTest.visibility = View.GONE
holder.urlTestProgress.visibility = View.VISIBLE
onUrlTest(current.name)
}
if (urlTesting.contains(current.name)) {
holder.urlTest.visibility = View.GONE
holder.urlTestProgress.visibility = View.VISIBLE
} else {
holder.urlTest.visibility = View.VISIBLE
holder.urlTestProgress.visibility = View.GONE
}
}
is ProxyItem -> {
val current = renderList[position] as ProxyRenderInfo
holder.prefix.text = current.info.prefix
holder.content.text = current.info.content
if (current.info.delay > 0)
holder.delay.text = current.info.delay.toString()
else
holder.delay.text = if (current.info.selectable) "" else "N/A"
if (current.info.active) {
activeList[current.group] = position
holder.prefix.setTextColor(Color.WHITE)
holder.content.setTextColor(Color.WHITE)
holder.delay.setTextColor(Color.WHITE)
holder.root.setCardBackgroundColor(context.getColor(R.color.primaryCardColorStarted))
} else {
holder.prefix.setTextColor(colorOnSurface)
holder.content.setTextColor(colorOnSurface)
holder.delay.setTextColor(colorOnSurface)
holder.root.setCardBackgroundColor(colorSurface)
}
if (current.info.selectable) {
holder.root.setOnClickListener {
val oldPosition = activeList[current.group] ?: return@setOnClickListener
val old = renderList[oldPosition] as ProxyRenderInfo
val new = renderList[position] as ProxyRenderInfo
renderList[oldPosition] = old.copy(info = old.info.copy(active = false))
renderList[position] = new.copy(info = new.info.copy(active = true))
notifyItemChanged(oldPosition)
notifyItemChanged(position)
onSelect(current.group, current.name)
}
holder.root.isClickable = true
holder.root.isFocusable = true
} else {
holder.root.setOnClickListener(null)
holder.root.isClickable = false
holder.root.isFocusable = false
}
}
}
}
override fun getItemCount(): Int = renderList.size
override fun getItemViewType(position: Int): Int {
return when (renderList[position]) {
is ProxyGroupRenderInfo -> 1
is ProxyRenderInfo -> 2
else -> throw IllegalArgumentException()
}
}
}
@@ -1,121 +0,0 @@
package com.github.kr328.clash.adapter
import android.animation.ValueAnimator
import android.content.Context
import android.graphics.Color
import android.util.TypedValue
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.annotation.ColorInt
import androidx.recyclerview.widget.RecyclerView
import com.github.kr328.clash.R
import com.google.android.material.card.MaterialCardView
class ProxyChipAdapter(
private val context: Context,
val onClick: (String) -> Unit
) :
RecyclerView.Adapter<ProxyChipAdapter.Holder>() {
var chips = listOf<String>()
var selected: String = ""
set(value) {
val lastIndex = chips.indexOf(field)
val newIndex = chips.indexOf(value)
field = value
if (lastIndex >= 0)
notifyItemChanged(lastIndex)
if (newIndex >= 0)
notifyItemChanged(newIndex)
}
@ColorInt
private val colorOnSurface: Int
init {
val typedValue = TypedValue()
context.theme.resolveAttribute(R.attr.colorOnSurface, typedValue, true)
colorOnSurface = typedValue.data
}
class Holder(root: View) : RecyclerView.ViewHolder(root) {
val card: MaterialCardView = root.findViewById(R.id.root)
val title: TextView = root.findViewById(android.R.id.title)
private var cardAnimator: ValueAnimator? = null
private var cardColor: Int = card.cardBackgroundColor.defaultColor
private var titleAnimator: ValueAnimator? = null
private var titleColor: Int = title.textColors.defaultColor
fun setCardColorAnimation(color: Int) {
if (cardColor == color)
return
cardAnimator?.cancel()
cardAnimator = ValueAnimator.ofArgb(cardColor, color).apply {
addUpdateListener {
val v = animatedValue as Int
card.setCardBackgroundColor(v)
cardColor = v
}
duration = 200
start()
}
}
fun setTitleColorAnimation(color: Int) {
if (color == titleColor)
return
titleAnimator?.cancel()
titleAnimator = ValueAnimator.ofArgb(titleColor, color).apply {
addUpdateListener {
val v = animatedValue as Int
title.setTextColor(v)
titleColor = v
}
duration = 200
start()
}
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): Holder {
val layoutInflater = LayoutInflater.from(context)
return Holder(layoutInflater.inflate(R.layout.adapter_proxies_chip, parent, false))
}
override fun getItemCount(): Int {
return chips.size
}
override fun onBindViewHolder(holder: Holder, position: Int) {
val current = chips[position]
holder.title.text = current
holder.card.setOnClickListener {
onClick(current)
}
if (selected == current) {
holder.setTitleColorAnimation(Color.WHITE)
holder.setCardColorAnimation(context.getColor(R.color.primaryCardColorStarted))
} else {
holder.setTitleColorAnimation(colorOnSurface)
holder.setCardColorAnimation(context.getColor(R.color.chipBackgroundColor))
}
}
}
@@ -0,0 +1,52 @@
package com.github.kr328.clash.log
import androidx.collection.CircularArray
import com.github.kr328.clash.core.model.LogMessage
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
class LogcatCache {
data class Snapshot(val messages: List<LogMessage>, val removed: Int, val appended: Int)
private val array = CircularArray<LogMessage>(CAPACITY)
private val lock = Mutex()
private var removed: Int = 0
private var appended: Int = 0
suspend fun append(msg: LogMessage) {
lock.withLock {
if (array.size() >= CAPACITY) {
array.removeFromStart(1)
removed++
appended--
}
array.addLast(msg)
appended++
}
}
suspend fun snapshot(full: Boolean): Snapshot? {
return lock.withLock {
if (!full && removed == 0 && appended == 0) {
return@withLock null
}
Snapshot(
List(array.size()) { array[it] },
removed,
if (full) array.size() + appended else appended
).also {
removed = 0
appended = 0
}
}
}
companion object {
const val CAPACITY = 128
}
}
@@ -0,0 +1,25 @@
package com.github.kr328.clash.log
import android.content.Context
import com.github.kr328.clash.core.model.LogMessage
import com.github.kr328.clash.design.util.format
import java.io.BufferedWriter
import java.io.Writer
import java.util.*
class LogcatFilter(output: Writer, private val context: Context) : BufferedWriter(output) {
fun writeHeader(time: Date) {
appendLine("# Capture on ${time.format(context)}")
}
fun writeMessage(message: LogMessage) {
val time = message.time.format(context, includeDate = false)
val level = message.level.name
appendLine(FORMAT.format(time, level, message.message))
}
companion object {
private const val FORMAT = "%12s %7s: %s"
}
}
@@ -0,0 +1,32 @@
package com.github.kr328.clash.log
import android.content.Context
import com.github.kr328.clash.core.model.LogMessage
import com.github.kr328.clash.design.model.LogFile
import com.github.kr328.clash.util.logsDir
import java.io.BufferedReader
import java.io.FileReader
import java.util.*
class LogcatReader(context: Context, file: LogFile) : AutoCloseable {
private val reader = BufferedReader(FileReader(context.logsDir.resolve(file.fileName)))
override fun close() {
reader.close()
}
fun readAll(): List<LogMessage> {
return reader.lineSequence()
.map { it.trim() }
.filter { !it.startsWith("#") }
.map { it.split(":", limit = 3) }
.map {
LogMessage(
time = Date(it[0].toLong()),
level = LogMessage.Level.valueOf(it[1]),
message = it[2]
)
}
.toList()
}
}
@@ -0,0 +1,25 @@
package com.github.kr328.clash.log
import android.content.Context
import com.github.kr328.clash.core.model.LogMessage
import com.github.kr328.clash.design.model.LogFile
import com.github.kr328.clash.util.logsDir
import java.io.BufferedWriter
import java.io.FileWriter
class LogcatWriter(context: Context) : AutoCloseable {
private val file = LogFile.generate()
private val writer = BufferedWriter(FileWriter(context.logsDir.resolve(file.fileName)))
override fun close() {
writer.close()
}
fun appendMessage(message: LogMessage) {
writer.appendLine(FORMAT.format(message.time.time, message.level.name, message.message))
}
companion object {
private const val FORMAT = "%d:%s:%s"
}
}
@@ -0,0 +1,32 @@
package com.github.kr328.clash.log
object SystemLogcat {
private val command = arrayOf(
"logcat",
"-d",
"-s",
"Go",
"DEBUG",
"AndroidRuntime",
"ClashForAndroid",
"LwIP",
)
fun dumpCrash(): String {
return try {
val process = Runtime.getRuntime().exec(command)
val result = process.inputStream.use { stream ->
stream.reader().readLines()
.filterNot { it.startsWith("------") }
.joinToString("\n")
}
process.waitFor()
result.trim()
} catch (e: Exception) {
""
}
}
}
@@ -1,20 +0,0 @@
package com.github.kr328.clash.model
data class LogFile(val fileName: String, val date: Long) {
companion object {
private val REGEX_FILE = Regex("clash-(\\d+).log")
private const val FORMAT_FILE_NAME = "clash-%d.log"
fun parseFromFileName(fileName: String): LogFile? {
return REGEX_FILE.matchEntire(fileName)?.run {
LogFile(fileName, groupValues[1].toLong())
}
}
fun generate(date: Long = System.currentTimeMillis()): LogFile {
val fileName = FORMAT_FILE_NAME.format(date)
return LogFile(fileName, date)
}
}
}
@@ -1,27 +0,0 @@
package com.github.kr328.clash.preference
import android.content.Context
import com.github.kr328.clash.service.settings.BaseSettings
class UiSettings(context: Context):
BaseSettings(context.getSharedPreferences(FILE_NAME, Context.MODE_PRIVATE)) {
companion object {
private const val FILE_NAME = "ui"
const val PROXY_SORT_DEFAULT = "default"
const val PROXY_SORT_NAME = "name"
const val PROXY_SORT_DELAY = "delay"
const val DARK_MODE_AUTO = "auto"
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", "")
val PROXY_MERGE_PREFIX = BooleanEntry("proxy_merge_prefix", false)
val LANGUAGE = StringEntry("language", "")
val DARK_MODE = StringEntry("dark_mode", DARK_MODE_AUTO)
}
}
@@ -5,97 +5,99 @@ 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.service.data.ClashProfileEntity
import com.github.kr328.clash.common.constants.Intents
import com.github.kr328.clash.common.log.Log
object Broadcasts {
interface Receiver {
class Broadcasts(private val context: Application) {
interface Observer {
fun onServiceRecreated()
fun onStarted()
fun onStopped(cause: String?)
fun onProfileChanged()
fun onProfileLoaded(profileEntity: ClashProfileEntity)
fun onProfileLoaded()
}
var clashRunning: Boolean = false
private val receivers = mutableListOf<Receiver>()
private var registered = false
private val receivers = mutableListOf<Observer>()
private val broadcastReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
if (intent?.`package` != context?.packageName)
return
when (intent?.action) {
Intents.INTENT_ACTION_CLASH_STARTED -> {
Intents.ACTION_SERVICE_RECREATED -> {
clashRunning = false
receivers.forEach {
it.onServiceRecreated()
}
}
Intents.ACTION_CLASH_STARTED -> {
clashRunning = true
receivers.forEach {
it.onStarted()
}
}
Intents.INTENT_ACTION_CLASH_STOPPED -> {
Intents.ACTION_CLASH_STOPPED -> {
clashRunning = false
receivers.forEach {
it.onStopped(intent.getStringExtra(Intents.INTENT_EXTRA_CLASH_STOP_REASON))
it.onStopped(intent.getStringExtra(Intents.EXTRA_STOP_REASON))
}
}
Intents.INTENT_ACTION_PROFILE_CHANGED ->
Intents.ACTION_PROFILE_CHANGED ->
receivers.forEach {
it.onProfileChanged()
}
Intents.INTENT_ACTION_PROFILE_LOADED -> {
val profile = intent
.getParcelableExtra<ClashProfileEntity>(Intents.INTENT_EXTRA_PROFILE) ?: return
Intents.ACTION_PROFILE_LOADED -> {
receivers.forEach {
it.onProfileLoaded(profile)
it.onProfileLoaded()
}
}
}
}
}
fun register(receiver: Receiver) {
receivers.add(receiver)
fun addObserver(observer: Observer) {
receivers.add(observer)
}
fun unregister(receiver: Receiver) {
receivers.remove(receiver)
fun removeObserver(observer: Observer) {
receivers.remove(observer)
}
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)
})
fun register() {
if (registered)
return
try {
context.registerReceiver(broadcastReceiver, IntentFilter().apply {
addAction(Intents.ACTION_SERVICE_RECREATED)
addAction(Intents.ACTION_CLASH_STARTED)
addAction(Intents.ACTION_CLASH_STOPPED)
addAction(Intents.ACTION_PROFILE_CHANGED)
addAction(Intents.ACTION_PROFILE_LOADED)
})
val current = RemoteUtils.detectClashRunning(application)
if (current != clashRunning) {
clashRunning = current
clashRunning = StatusClient(context).currentProfile() != null
} catch (e: Exception) {
Log.w("Register global receiver: $e", e)
}
}
if (current) {
receivers.forEach {
it.onStarted()
}
} else {
receivers.forEach {
it.onStopped(null)
}
}
}
}
fun unregister() {
if (!registered)
return
override fun onStop(owner: LifecycleOwner) {
application.unregisterReceiver(broadcastReceiver)
}
})
try {
context.unregisterReceiver(broadcastReceiver)
clashRunning = false
} catch (e: Exception) {
Log.w("Unregister global receiver: $e", e)
}
}
}
@@ -1,13 +0,0 @@
package com.github.kr328.clash.remote
suspend fun <T> withClash(block: suspend ClashClient.() -> T): T {
val client = Remote.clash.receive()
return client.block()
}
suspend fun <T> withProfile(block: suspend ProfileClient.() -> T): T {
val client = Remote.profile.receive()
return client.block()
}
@@ -1,50 +0,0 @@
package com.github.kr328.clash.remote
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 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)
}
suspend fun startHealthCheck(group: String) = withContext(Dispatchers.IO) {
CompletableDeferred<Unit>().apply {
service.startHealthCheck(group, object : IStreamCallback.Stub() {
override fun complete() {
this@apply.complete(Unit)
}
override fun completeExceptionally(reason: String?) {
this@apply.completeExceptionally(RemoteException(reason))
}
override fun send(data: ParcelableContainer?) {}
})
}
}.await()
suspend fun queryAllProxyGroups(): List<ProxyGroup> = withContext(Dispatchers.IO) {
service.queryAllProxies().list
}
suspend fun queryGeneral(): General = withContext(Dispatchers.IO) {
service.queryGeneral()
}
suspend fun queryBandwidth(): Long =
withContext(Dispatchers.IO) {
service.queryBandwidth()
}
suspend fun setProxyMode(mode: General.Mode) = withContext(Dispatchers.IO) {
service.setProxyMode(mode.toString())
}
}
@@ -0,0 +1,94 @@
@file:Suppress("BlockingMethodInNonBlockingContext")
package com.github.kr328.clash.remote
import android.content.Context
import android.net.Uri
import com.github.kr328.clash.common.constants.Authorities
import com.github.kr328.clash.design.model.File
import com.github.kr328.clash.util.copyContentTo
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import android.provider.DocumentsContract as DC
class FilesClient(private val context: Context) {
suspend fun list(parentDocumentId: String): List<File> = withContext(Dispatchers.IO) {
val uri = DC.buildChildDocumentsUri(Authorities.FILES_PROVIDER, parentDocumentId)
context.contentResolver.query(uri, FilesProjection, null, null, null)?.use { cursor ->
val idIndex = cursor.getColumnIndex(DC.Document.COLUMN_DOCUMENT_ID)
val nameIndex = cursor.getColumnIndex(DC.Document.COLUMN_DISPLAY_NAME)
val sizeIndex = cursor.getColumnIndex(DC.Document.COLUMN_SIZE)
val lastModified = cursor.getColumnIndex(DC.Document.COLUMN_LAST_MODIFIED)
val mimeTypeIndex = cursor.getColumnIndex(DC.Document.COLUMN_MIME_TYPE)
cursor.moveToFirst()
List(cursor.count) {
File(
id = cursor.getString(idIndex),
name = cursor.getString(nameIndex),
size = cursor.getLong(sizeIndex),
lastModified = cursor.getLong(lastModified),
isDirectory = cursor.getString(mimeTypeIndex) == DC.Document.MIME_TYPE_DIR,
).also {
cursor.moveToNext()
}
}.sortedWith(compareBy({ !it.isDirectory }, { it.name }))
} ?: emptyList()
}
suspend fun renameDocument(documentId: String, name: String) = withContext(Dispatchers.IO) {
val uri = buildDocumentUri(documentId)
DC.renameDocument(context.contentResolver, uri, name)
}
suspend fun deleteDocument(documentId: String) = withContext(Dispatchers.IO) {
val uri = buildDocumentUri(documentId)
DC.deleteDocument(context.contentResolver, uri)
}
suspend fun importDocument(
parentDocumentId: String,
source: Uri,
name: String
) = withContext(Dispatchers.IO) {
val target = buildDocumentUri("$parentDocumentId/$name")
context.contentResolver.copyContentTo(source, target)
}
suspend fun copyDocument(
documentId: String,
source: Uri
) {
val target = buildDocumentUri(documentId)
context.contentResolver.copyContentTo(source, target)
}
suspend fun copyDocument(
target: Uri,
documentId: String
) {
val source = buildDocumentUri(documentId)
context.contentResolver.copyContentTo(source, target)
}
fun buildDocumentUri(documentId: String): Uri {
return DC.buildDocumentUri(Authorities.FILES_PROVIDER, documentId)
}
companion object {
private val FilesProjection = arrayOf(
DC.Document.COLUMN_DOCUMENT_ID,
DC.Document.COLUMN_DISPLAY_NAME,
DC.Document.COLUMN_SIZE,
DC.Document.COLUMN_LAST_MODIFIED,
DC.Document.COLUMN_MIME_TYPE,
)
}
}
@@ -1,33 +0,0 @@
package com.github.kr328.clash.remote
import com.github.kr328.clash.service.IProfileService
import com.github.kr328.clash.service.data.ClashProfileEntity
import com.github.kr328.clash.service.transact.ProfileRequest
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 queryActiveProfile(): ClashProfileEntity? = withContext(Dispatchers.IO) {
service.queryActiveProfile()
}
suspend fun enqueueRequest(request: ProfileRequest) = withContext(Dispatchers.IO) {
service.enqueueRequest(request)
}
suspend fun setActiveProfile(id: Long) = withContext(Dispatchers.IO) {
service.setActiveProfile(id)
}
suspend fun requestProfileEditUri(id: Long): String? = withContext(Dispatchers.IO) {
service.requestProfileEditUri(id)
}
suspend fun commitProfileEditUri(uri: String) = withContext(Dispatchers.IO) {
service.commitProfileEditUri(uri)
}
}
@@ -1,162 +1,71 @@
package com.github.kr328.clash.remote
import android.app.Application
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.ServiceConnection
import android.os.Handler
import android.os.IBinder
import androidx.core.content.edit
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.ProcessLifecycleOwner
import com.github.kr328.clash.ApkBrokenActivity
import com.github.kr328.clash.Constants
import com.github.kr328.clash.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 kotlinx.coroutines.*
import com.github.kr328.clash.AppCrashedActivity
import com.github.kr328.clash.common.Global
import com.github.kr328.clash.common.util.intent
import com.github.kr328.clash.store.AppStore
import com.github.kr328.clash.util.ApplicationObserver
import com.github.kr328.clash.util.verifyApk
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.channels.Channel
import java.util.zip.ZipFile
import kotlinx.coroutines.launch
object Remote {
var clash = Channel<ClashClient>()
var profile = Channel<ProfileClient>()
val broadcasts: Broadcasts = Broadcasts(Global.application)
val service: Service = Service(Global.application) {
ApplicationObserver.createdActivities.forEach { it.finish() }
private var clashConnection: ClashConnection? = null
private var profileConnection: ProfileConnection? = null
val intent = AppCrashedActivity::class.intent
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
class ClashConnection : ServiceConnection {
private var instance: ClashClient? = null
private var sender: Job? = null
Global.application.startActivity(intent)
}
override fun onServiceDisconnected(name: ComponentName?) {
sender?.cancel()
instance = null
sender = null
private val visible = Channel<Boolean>(Channel.CONFLATED)
fun launch() {
ApplicationObserver.attach(Global.application)
ApplicationObserver.onVisibleChanged { visible.trySend(it) }
Global.launch(Dispatchers.IO) {
run()
}
}
private suspend fun run() {
val context = Global.application
val store = AppStore(context)
val updatedAt = getLastUpdated(context)
if (store.updatedAt != updatedAt) {
if (!context.verifyApk()) {
ApplicationObserver.createdActivities.forEach { it.finish() }
val intent = ApkBrokenActivity::class.intent
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
return context.startActivity(intent)
} else {
store.updatedAt = updatedAt
}
}
override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
if (service != null)
instance = ClashClient(IClashManager.Stub.asInterface(service))
service?.linkToDeath({
onServiceDisconnected(null)
}, 0)
sender = GlobalScope.launch {
while (isActive) {
val client = instance ?: return@launch
clash.send(client)
}
while (true) {
if (visible.receive()) {
service.bind()
broadcasts.register()
} else {
service.unbind()
broadcasts.unregister()
}
}
}
class ProfileConnection : ServiceConnection {
private var instance: ProfileClient? = null
private var sender: Job? = null
override fun onServiceDisconnected(name: ComponentName?) {
sender?.cancel()
instance = null
sender = null
}
override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
if (service != null)
instance = ProfileClient(IProfileService.Stub.asInterface(service))
service?.linkToDeath({
onServiceDisconnected(null)
}, 0)
sender = GlobalScope.launch {
while (isActive) {
val client = instance ?: return@launch
profile.send(client)
}
}
}
}
fun init(application: Application) {
val handler = Handler()
ProcessLifecycleOwner.get().lifecycle.addObserver(object : DefaultLifecycleObserver {
override fun onStart(owner: LifecycleOwner) {
handler.removeMessages(0)
GlobalScope.launch {
if (!verifyApk(application)) {
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
)
}
}
}
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)
}
})
}
private suspend fun verifyApk(application: Application): Boolean {
return withContext(Dispatchers.IO) {
val sp = application.getSharedPreferences(
Constants.PREFERENCE_NAME_APP,
Context.MODE_PRIVATE
)
val pkg = application.packageManager.getPackageInfo(application.packageName, 0)
if (sp.getLong(Constants.PREFERENCE_KEY_LAST_INSTALL, 0) == pkg.lastUpdateTime)
return@withContext true
val info = application.applicationInfo
val sources =
info.splitSourceDirs ?: arrayOf(info.sourceDir) ?: return@withContext 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
}
}
return@withContext false
}
private fun getLastUpdated(context: Context): Long {
return context.packageManager.getPackageInfo(context.packageName, 0).lastUpdateTime
}
}
@@ -1,24 +0,0 @@
package com.github.kr328.clash.remote
import android.content.Context
import android.net.Uri
import com.github.kr328.clash.service.Constants
import com.github.kr328.clash.service.ServiceStatusProvider
object RemoteUtils {
fun detectClashRunning(context: Context): Boolean {
val authority = Uri.Builder()
.scheme("content")
.authority("${context.packageName}${Constants.STATUS_PROVIDER_SUFFIX}")
.build()
val pong = context.contentResolver.call(
authority,
ServiceStatusProvider.METHOD_PING_CLASH_SERVICE,
null,
null
)
return pong != null
}
}
@@ -0,0 +1,74 @@
package com.github.kr328.clash.remote
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlin.coroutines.resume
class Resource<T> {
private interface Callback<T> {
fun accept(value: T)
}
private val pending: MutableSet<Callback<T>> = mutableSetOf()
private var value: T? = null
suspend fun get(): T {
return suspendCancellableCoroutine { ctx ->
val callback = object : Callback<T> {
override fun accept(value: T) {
ctx.resume(value)
}
}
ctx.invokeOnCancellation {
cancel(callback)
}
get(callback)
}
}
fun set(v: T?) {
setAndNotify(v)
}
fun reset(v: T) {
resetIfMatched(v)
}
@Synchronized
private fun get(callback: Callback<T>) {
val v = value
if (v == null) {
pending.add(callback)
} else {
callback.accept(v)
}
}
@Synchronized
private fun setAndNotify(value: T?) {
this.value = value
if (value != null) {
pending.forEach {
it.accept(value)
}
pending.clear()
}
}
@Synchronized
private fun resetIfMatched(value: T) {
if (this.value === value) {
this.value = null
}
}
@Synchronized
private fun cancel(callback: Callback<T>) {
pending.remove(callback)
}
}
@@ -0,0 +1,60 @@
package com.github.kr328.clash.remote
import android.app.Application
import android.content.ComponentName
import android.content.Context
import android.content.ServiceConnection
import android.os.IBinder
import com.github.kr328.clash.common.log.Log
import com.github.kr328.clash.common.util.intent
import com.github.kr328.clash.service.RemoteService
import com.github.kr328.clash.service.remote.IRemoteService
import com.github.kr328.clash.service.remote.unwrap
import com.github.kr328.clash.util.unbindServiceSilent
import java.util.concurrent.TimeUnit
class Service(private val context: Application, val crashed: () -> Unit) {
val remote = Resource<IRemoteService>()
private val connection = object : ServiceConnection {
private var lastCrashed: Long = -1
override fun onServiceConnected(name: ComponentName?, service: IBinder) {
remote.set(service.unwrap(IRemoteService::class))
}
override fun onServiceDisconnected(name: ComponentName?) {
remote.set(null)
if (System.currentTimeMillis() - lastCrashed < TOGGLE_CRASHED_INTERVAL) {
unbind()
crashed()
}
lastCrashed = System.currentTimeMillis()
Log.w("RemoteManager crashed")
}
}
fun bind() {
try {
context.bindService(RemoteService::class.intent, connection, Context.BIND_AUTO_CREATE)
} catch (e: Exception) {
unbind()
crashed()
}
}
fun unbind() {
context.unbindServiceSilent(connection)
remote.set(null)
}
companion object {
private val TOGGLE_CRASHED_INTERVAL = TimeUnit.SECONDS.toMillis(10)
}
}
@@ -0,0 +1,34 @@
package com.github.kr328.clash.remote
import android.content.Context
import android.net.Uri
import com.github.kr328.clash.common.constants.Authorities
import com.github.kr328.clash.common.log.Log
import com.github.kr328.clash.service.StatusProvider
class StatusClient(private val context: Context) {
private val uri: Uri
get() {
return Uri.Builder()
.scheme("content")
.authority(Authorities.STATUS_PROVIDER)
.build()
}
fun currentProfile(): String? {
return try {
val result = context.contentResolver.call(
uri,
StatusProvider.METHOD_CURRENT_PROFILE,
null,
null
)
result?.getString("name")
} catch (e: Exception) {
Log.w("Query current profile: $e", e)
null
}
}
}
@@ -1,36 +0,0 @@
package com.github.kr328.clash.settings
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
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 fun onCreateDataStore(): SettingsDataStore
abstract val xmlResourceId: Int
protected val service: ServiceSettings by lazy { ServiceSettings(requireActivity()) }
protected val ui: UiSettings by lazy { UiSettings(requireActivity()) }
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
preferenceManager.preferenceDataStore = onCreateDataStore()
setPreferencesFromResource(xmlResourceId, rootKey)
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val result = super.onCreateView(inflater, container, savedInstanceState)
setDivider(null)
setDividerHeight(0)
return result
}
}
@@ -1,55 +0,0 @@
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.remote.Broadcasts
import com.github.kr328.clash.service.settings.ServiceSettings
import com.github.kr328.clash.service.util.componentName
class BehaviorFragment: BaseSettingFragment() {
companion object {
private const val KEY_START_ON_BOOT = "start_on_boot"
private const val KEY_SHOW_TRAFFIC = "show_traffic"
}
override fun onCreateDataStore(): SettingsDataStore {
return SettingsDataStore().apply {
on(KEY_START_ON_BOOT, StartOnBootSource())
on(KEY_SHOW_TRAFFIC, ServiceSettings.NOTIFICATION_REFRESH.asSource(service))
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
findPreference(KEY_SHOW_TRAFFIC).isEnabled = !Broadcasts.clashRunning
}
override val xmlResourceId: Int
get() = R.xml.settings_behavior
private inner class StartOnBootSource: SettingsDataStore.Source {
override fun set(value: Any?) {
val v = value as Boolean? ?: return
val status = if ( v )
PackageManager.COMPONENT_ENABLED_STATE_ENABLED
else
PackageManager.COMPONENT_ENABLED_STATE_DISABLED
requireActivity().packageManager.setComponentEnabledSetting(
OnBootReceiver::class.componentName,
status,
PackageManager.DONT_KILL_APP)
}
override fun get(): Any? {
val status = requireActivity().packageManager
.getComponentEnabledSetting(OnBootReceiver::class.componentName)
return status == PackageManager.COMPONENT_ENABLED_STATE_ENABLED
}
}
}
@@ -1,30 +0,0 @@
package com.github.kr328.clash.settings
import com.github.kr328.clash.R
import com.github.kr328.clash.preference.UiSettings
import com.github.kr328.clash.service.settings.ServiceSettings
class InterfaceFragment: BaseSettingFragment() {
companion object {
private const val KEY_DARK_MODE = "dark_mode"
private const val KEY_LANGUAGE = "language"
}
override fun onCreateDataStore(): SettingsDataStore {
return SettingsDataStore().apply {
on(KEY_DARK_MODE, UiSettings.DARK_MODE.asSource(ui))
on(KEY_LANGUAGE, UiSettings.LANGUAGE.asSource(ui))
onApply {
service.commit {
put(ServiceSettings.LANGUAGE, ui.get(UiSettings.LANGUAGE))
}
requireActivity().recreate()
}
}
}
override val xmlResourceId: Int
get() = R.xml.settings_interface
}
@@ -1,49 +0,0 @@
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.remote.Broadcasts
import com.github.kr328.clash.service.settings.ServiceSettings
import com.github.kr328.clash.service.util.intent
class NetworkFragment: BaseSettingFragment() {
companion object {
private const val KEY_ENABLE_VPN_SERVICE = "enable_vpn_service"
private const val KEY_IPV6 = "ipv6"
private const val BYPASS_PRIVATE_NETWORK = "bypass_private_network"
private const val KEY_DNS_HIJACKING = "dns_hijacking"
private const val KEY_DNS_OVERRIDE = "dns_override"
private const val KEY_APPEND_SYS_DNS = "append_system_dns"
private const val KEY_ACCESS_CONTROL_MODE = "access_control_mode"
private const val KEY_ACCESS_CONTROL_PACKAGES = "access_control_packages"
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
preferenceScreen.isEnabled = !Broadcasts.clashRunning
findPreference(KEY_ACCESS_CONTROL_PACKAGES).setOnPreferenceClickListener {
startActivity(PackagesActivity::class.intent)
true
}
}
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(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))
on(KEY_APPEND_SYS_DNS, ServiceSettings.AUTO_ADD_SYSTEM_DNS.asSource(service))
on(KEY_ACCESS_CONTROL_MODE, ServiceSettings.ACCESS_CONTROL_MODE.asSource(service))
}
}
override val xmlResourceId: Int
get() = R.xml.settings_network
}
@@ -1,100 +0,0 @@
package com.github.kr328.clash.settings
import com.github.kr328.clash.service.settings.BaseSettings
import moe.shizuku.preference.PreferenceDataStore
class SettingsDataStore: PreferenceDataStore() {
interface Source {
fun set(value: Any?)
fun get(): Any?
}
private val sources: MutableMap<String, Source> = mutableMapOf()
var applyListener: () -> Unit = {}
fun on(key: String, source: Source) {
sources[key] = source
}
fun onApply(block: () -> Unit) {
this.applyListener = block
}
inline fun <reified T>BaseSettings.Entry<T>.asSource(settings: BaseSettings): Source {
return object: Source {
override fun set(value: Any?) {
val v = value ?: throw NullPointerException()
settings.commit {
put(this@asSource, v as T)
}
applyListener()
}
override fun get(): Any? {
return settings.get(this@asSource)
}
}
}
override fun getBoolean(key: String?, defValue: Boolean): Boolean {
val source = sources[key] ?: return defValue
return (source.get() as Boolean?) ?: defValue
}
override fun putLong(key: String?, value: Long) {
val source = sources[key] ?: throw NullPointerException()
source.set(value)
}
override fun putInt(key: String?, value: Int) {
val source = sources[key] ?: throw NullPointerException()
source.set(value)
}
override fun getInt(key: String?, defValue: Int): Int {
val source = sources[key] ?: return defValue
return (source.get() as Int?) ?: defValue
}
override fun putBoolean(key: String?, value: Boolean) {
val source = sources[key] ?: throw NullPointerException()
source.set(value)
}
override fun getLong(key: String?, defValue: Long): Long {
val source = sources[key] ?: return defValue
return (source.get() as Long?) ?: defValue
}
override fun getFloat(key: String?, defValue: Float): Float {
val source = sources[key] ?: return defValue
return (source.get() as Float?) ?: defValue
}
override fun putFloat(key: String?, value: Float) {
val source = sources[key] ?: throw NullPointerException()
source.set(value)
}
override fun getString(key: String?, defValue: String?): String? {
val source = sources[key] ?: return defValue
return (source.get() as String?) ?: defValue
}
override fun putString(key: String?, value: String?) {
val source = sources[key] ?: throw NullPointerException()
source.set(value)
}
}
@@ -0,0 +1,22 @@
package com.github.kr328.clash.store
import android.content.Context
import com.github.kr328.clash.common.store.Store
import com.github.kr328.clash.common.store.asStoreProvider
class AppStore(context: Context) {
private val store = Store(
context
.getSharedPreferences(FILE_NAME, Context.MODE_PRIVATE)
.asStoreProvider()
)
var updatedAt: Long by store.long(
key = "updated_at",
defaultValue = -1,
)
companion object {
private const val FILE_NAME = "app"
}
}
@@ -0,0 +1,29 @@
package com.github.kr328.clash.store
import android.content.Context
import com.github.kr328.clash.common.store.Store
import com.github.kr328.clash.common.store.asStoreProvider
class TipsStore(context: Context) {
private val store = Store(
context
.getSharedPreferences(FILE_NAME, Context.MODE_PRIVATE)
.asStoreProvider()
)
var requestDonate: Boolean by store.boolean(
key = "request_donate",
defaultValue = true,
)
var primaryVersion: Int by store.int(
key = "primary_version",
defaultValue = -1,
)
companion object {
const val CURRENT_PRIMARY_VERSION = 1
private const val FILE_NAME = "tips"
}
}
@@ -0,0 +1,44 @@
package com.github.kr328.clash.util
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.LifecycleRegistry
import kotlinx.coroutines.NonCancellable
import kotlinx.coroutines.withContext
class ActivityResultLifecycle : LifecycleOwner {
private val lifecycle = LifecycleRegistry(this)
init {
lifecycle.currentState = Lifecycle.State.INITIALIZED
}
override fun getLifecycle(): Lifecycle {
return lifecycle
}
suspend fun <T> use(block: suspend (lifecycle: ActivityResultLifecycle, start: () -> Unit) -> T): T {
return try {
markCreated()
block(this, this::markStarted)
} finally {
withContext(NonCancellable) {
markDestroy()
}
}
}
private fun markCreated() {
lifecycle.currentState = Lifecycle.State.CREATED
}
private fun markStarted() {
lifecycle.currentState = Lifecycle.State.STARTED
lifecycle.currentState = Lifecycle.State.RESUMED
}
private fun markDestroy() {
lifecycle.currentState = Lifecycle.State.DESTROYED
}
}
@@ -0,0 +1,78 @@
package com.github.kr328.clash.util
import android.app.Activity
import android.app.Application
import android.content.Context
import android.os.Build
import android.os.Bundle
import java.io.File
import java.util.zip.ZipFile
object ApplicationObserver {
private val activities: MutableSet<Activity> = mutableSetOf()
private var visibleChanged: (Boolean) -> Unit = {}
private var appVisible = false
private set(value) {
if (field != value) {
field = value
visibleChanged(value)
}
}
val createdActivities: Set<Activity>
get() = activities
private val activityObserver = object : Application.ActivityLifecycleCallbacks {
@Synchronized
override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {
activities.add(activity)
appVisible = true
}
@Synchronized
override fun onActivityDestroyed(activity: Activity) {
activities.remove(activity)
appVisible = activities.isNotEmpty()
}
override fun onActivityStarted(activity: Activity) {}
override fun onActivityStopped(activity: Activity) {}
override fun onActivityPaused(activity: Activity) {}
override fun onActivityResumed(activity: Activity) {}
override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) {}
}
fun onVisibleChanged(visibleChanged: (Boolean) -> Unit) {
this.visibleChanged = visibleChanged
}
fun attach(application: Application) {
application.registerActivityLifecycleCallbacks(activityObserver)
}
}
fun Context.verifyApk(): Boolean {
return try {
val info = applicationInfo
val sources = info.splitSourceDirs ?: arrayOf(info.sourceDir) ?: return false
val regexNativeLibrary = Regex("lib/(\\S+)/libclash.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()
availableAbi.intersect(apkAbi).isNotEmpty()
} catch (e: Exception) {
false
}
}
@@ -0,0 +1,32 @@
package com.github.kr328.clash.util
import android.content.Context
import android.content.Intent
import android.net.VpnService
import com.github.kr328.clash.common.compat.startForegroundServiceCompat
import com.github.kr328.clash.common.constants.Intents
import com.github.kr328.clash.common.util.intent
import com.github.kr328.clash.design.store.UiStore
import com.github.kr328.clash.service.ClashService
import com.github.kr328.clash.service.TunService
import com.github.kr328.clash.service.util.sendBroadcastSelf
fun Context.startClashService(): Intent? {
val startTun = UiStore(this).enableVpn
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.ACTION_CLASH_REQUEST_STOP))
}
@@ -0,0 +1,25 @@
package com.github.kr328.clash.util
import android.content.ContentResolver
import android.net.Uri
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.io.FileNotFoundException
private fun fileNotFound(file: Uri): FileNotFoundException {
return FileNotFoundException("$file not found")
}
@Suppress("BlockingMethodInNonBlockingContext")
suspend fun ContentResolver.copyContentTo(
source: Uri,
target: Uri
) {
withContext(Dispatchers.IO) {
(openInputStream(source) ?: throw fileNotFound(source)).use { input ->
(openOutputStream(target, "rwt") ?: throw fileNotFound(target)).use { output ->
input.copyTo(output)
}
}
}
}
@@ -0,0 +1,7 @@
package com.github.kr328.clash.util
import android.content.Context
import java.io.File
val Context.logsDir: File
get() = cacheDir.resolve("logs")
@@ -0,0 +1,46 @@
package com.github.kr328.clash.util
import android.os.DeadObjectException
import com.github.kr328.clash.common.log.Log
import com.github.kr328.clash.remote.Remote
import com.github.kr328.clash.service.remote.IClashManager
import com.github.kr328.clash.service.remote.IProfileManager
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import kotlin.coroutines.CoroutineContext
suspend fun <T> withClash(
context: CoroutineContext = Dispatchers.IO,
block: suspend IClashManager.() -> T
): T {
while (true) {
val remote = Remote.service.remote.get()
val client = remote.clash()
try {
return withContext(context) { client.block() }
} catch (e: DeadObjectException) {
Log.w("Remote services panic")
Remote.service.remote.reset(remote)
}
}
}
suspend fun <T> withProfile(
context: CoroutineContext = Dispatchers.IO,
block: suspend IProfileManager.() -> T
): T {
while (true) {
val remote = Remote.service.remote.get()
val client = remote.profile()
try {
return withContext(context) { client.block() }
} catch (e: DeadObjectException) {
Log.w("Remote services panic")
Remote.service.remote.reset(remote)
}
}
}
@@ -0,0 +1,12 @@
package com.github.kr328.clash.util
import android.content.Context
import android.content.ServiceConnection
fun Context.unbindServiceSilent(connection: ServiceConnection) {
try {
unbindService(connection)
} catch (e: Exception) {
// ignore
}
}
@@ -0,0 +1,6 @@
package com.github.kr328.clash.util
import android.net.Uri
val Uri.fileName: String?
get() = schemeSpecificPart.split("/").lastOrNull()
@@ -1,25 +0,0 @@
package com.github.kr328.clash.utils
import android.content.Context
import java.text.SimpleDateFormat
import java.util.*
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 {
val locale = context.resources.configuration.locales[0]
return when {
custom.isNotEmpty() ->
SimpleDateFormat(custom, locale).format(this)
includeDate && includeTime ->
SimpleDateFormat(DATE_ALL, locale).format(this)
includeDate ->
SimpleDateFormat(DATE_DATE_ONLY, locale).format(this)
includeTime ->
SimpleDateFormat(DATE_TIME_ONLY, locale).format(this)
else -> ""
}
}
@@ -1,8 +0,0 @@
package com.github.kr328.clash.utils
import android.content.Context
import com.github.kr328.clash.Constants
import java.io.File
val Context.logsDir: File
get() = (externalCacheDir ?: cacheDir).resolve(Constants.LOG_DIR_NAME)
@@ -1,32 +0,0 @@
package com.github.kr328.clash.utils
import android.content.Context
import com.github.kr328.clash.R
object IntervalUtils {
private const val MILLIS_SECOND = 1000L
private const val MILLIS_MINUTE = MILLIS_SECOND * 60
private const val MILLIS_HOUR = MILLIS_MINUTE * 60
private const val MILLIS_DAY = MILLIS_HOUR * 24
private const val MILLIS_MONTH = MILLIS_DAY * 30
private const val MILLIS_YEAR = MILLIS_MONTH * 12
fun intervalString(context: Context, interval: Long): String {
val year = interval / MILLIS_YEAR
val month = interval / MILLIS_MONTH
val day = interval / MILLIS_DAY
val hour = interval / MILLIS_HOUR
val minute = interval / MILLIS_MINUTE
System.currentTimeMillis()
return when {
year > 0 -> context.getString(R.string.format_years, year)
month > 0 -> context.getString(R.string.format_months, month)
day > 0 -> context.getString(R.string.format_days, day)
hour > 0 -> context.getString(R.string.format_hours, hour)
minute > 0 -> context.getString(R.string.format_minutes, minute)
else -> context.getString(R.string.recently)
}
}
}
@@ -1,66 +0,0 @@
package com.github.kr328.clash.utils
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
object PrefixMerger {
private val REGEX_PREFIX_TRIM = Regex("[-]*$")
data class Result<T>(val prefix: String, val content: String, val value: T)
suspend fun <T> merge(values: List<T>, transform: (T) -> String): List<Result<T>> =
withContext(Dispatchers.Default) {
val pairs = values.map {
transform(it).trim() to it
}
val groups = mutableListOf<List<Pair<String, T>>>()
var mergingGroup = mutableListOf<Pair<String, T>>()
var currentChar: Char = 0.toChar()
val result = mutableListOf<Result<T>>()
for (pair in pairs) {
if (pair.first[0] == currentChar) {
mergingGroup.add(pair)
} else {
if (mergingGroup.isNotEmpty()) {
groups.add(mergingGroup)
mergingGroup = mutableListOf()
}
currentChar = pair.first[0]
mergingGroup.add(pair)
}
}
if (mergingGroup.isNotEmpty())
groups.add(mergingGroup)
for (group in groups) {
var diffIndex = 0
val size = group.map { it.first.length }.min() ?: 0
diff@ for (charIndex in 0 until size) {
for (stringIndex in 0 until (group.size - 1)) {
if (group[stringIndex].first[charIndex] != group[stringIndex + 1].first[charIndex])
break@diff
}
diffIndex++
}
group.forEach {
result.add(
Result(
it.first.substring(0, diffIndex)
.replace(REGEX_PREFIX_TRIM, ""),
it.first.substring(diffIndex),
it.second
)
)
}
}
result
}
}
@@ -1,94 +0,0 @@
package com.github.kr328.clash.utils
import com.github.kr328.clash.core.model.Proxy
import com.github.kr328.clash.core.model.ProxyGroup
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
class ProxySorter(private val groupOrder: Order, private val proxyOrder: Order) {
enum class Order {
DEFAULT, DELAY_INCREASE, DELAY_DECREASE, NAME_INCREASE, NAME_DECREASE
}
suspend fun sort(proxyGroup: List<ProxyGroup>): List<ProxyGroup> =
withContext(Dispatchers.Default) {
val global = proxyGroup.singleOrNull {
it.name == "GLOBAL"
}
val sortedGroup = when (groupOrder) {
Order.DEFAULT -> groupSortWithDefault(global, proxyGroup)
Order.DELAY_INCREASE -> groupSortWithDelay(true, proxyGroup)
Order.DELAY_DECREASE -> groupSortWithDelay(false, proxyGroup)
Order.NAME_INCREASE -> groupSortWithName(true, proxyGroup)
Order.NAME_DECREASE -> groupSortWithName(false, proxyGroup)
}
sortedGroup.map {
val sortedProxy = when (proxyOrder) {
Order.DEFAULT -> it.proxies
Order.DELAY_INCREASE -> proxySortWithDelay(true, it.proxies)
Order.DELAY_DECREASE -> proxySortWithDelay(false, it.proxies)
Order.NAME_INCREASE -> proxySortWithName(true, it.proxies)
Order.NAME_DECREASE -> proxySortWithName(false, it.proxies)
}
it.copy(proxies = sortedProxy)
}
}
private fun groupSortWithDefault(
global: ProxyGroup?,
proxyGroup: List<ProxyGroup>
): List<ProxyGroup> {
if (global == null) return proxyGroup
val orderMap = global.proxies.mapIndexed { index, proxy ->
proxy.name to index
}.toMap()
return proxyGroup.sortedBy {
orderMap[it.name] ?: Int.MAX_VALUE
}
}
private fun groupSortWithName(
increase: Boolean,
proxyGroup: List<ProxyGroup>
): List<ProxyGroup> {
return if (increase)
proxyGroup.sortedBy { it.name }
else
proxyGroup.sortedByDescending { it.name }
}
private fun groupSortWithDelay(
increase: Boolean,
proxyGroup: List<ProxyGroup>
): List<ProxyGroup> {
return if (increase)
proxyGroup.sortedBy { it.delay }
else
proxyGroup.sortedByDescending { it.delay }
}
private fun proxySortWithName(
increase: Boolean,
proxies: List<Proxy>
): List<Proxy> {
return if (increase)
proxies.sortedBy { it.name }
else
proxies.sortedByDescending { it.name }
}
private fun proxySortWithDelay(
increase: Boolean,
proxies: List<Proxy>
): List<Proxy> {
return if (increase)
proxies.sortedBy { it.delay }
else
proxies.sortedByDescending { it.delay }
}
}
@@ -1,79 +0,0 @@
package com.github.kr328.clash.utils
import android.content.Context
import android.util.DisplayMetrics
import androidx.recyclerview.widget.LinearSmoothScroller
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.delay
class ScrollBinding(
private val context: Context,
private val callback: Callback
) {
interface Callback {
fun getCurrentMasterToken(): String
fun onMasterTokenChanged(token: String)
fun getMasterTokenPosition(token: String): Int
fun doMasterScroll(scroller: LinearSmoothScroller)
}
private val updateChannel = Channel<Unit>(Channel.CONFLATED)
private var preventSlaveScroll = false
fun sendMasterScrolled() {
updateChannel.offer(Unit)
}
fun scrollMaster(token: String) {
val position = callback.getMasterTokenPosition(token)
if (position < 0)
return
val scroller = (object : LinearSmoothScroller(context) {
override fun getVerticalSnapPreference(): Int {
return SNAP_TO_START
}
override fun onStop() {
super.onStop()
preventSlaveScroll = false
}
override fun onStart() {
super.onStart()
preventSlaveScroll = true
}
override fun calculateSpeedPerPixel(displayMetrics: DisplayMetrics?): Float {
return super.calculateSpeedPerPixel(displayMetrics) * 0.8f
}
init {
targetPosition = position
}
})
callback.doMasterScroll(scroller)
}
suspend fun exec() {
var lastToken: String? = null
while (true) {
updateChannel.receive()
val currentToken = callback.getCurrentMasterToken()
if (preventSlaveScroll || lastToken == currentToken)
continue
lastToken = currentToken
callback.onMasterTokenChanged(currentToken)
delay(200)
}
}
}
@@ -1,25 +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.util.intent
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(ClashService::class.intent
.putExtra(Intents.INTENT_EXTRA_START_TUN, startTun))
return null
}
Loaded 100 of 664 files, more files were not shown because too many files have changed in this diff. Show more