Compare commits

...
312 Commits
Author SHA1 Message Date
romanvht ffda4fa93d fix preference dialogs crash on orientation change 2026-07-23 18:11:25 +10:00
romanvht 3e00c6abb2 bump version 2026-07-23 17:02:35 +10:00
romanvht d61d7f8329 migrate to material toolbar in layout 2026-07-23 16:15:14 +10:00
romanvht 36a750100c add getCmdArgs to preference utils 2026-07-23 15:39:02 +10:00
romanvht 8d35278629 update strategy picker style, minor changes 2026-07-23 15:30:39 +10:00
romanvht f497cd615d select setting section in import/export 2026-07-23 14:58:47 +10:00
romanvht efa32b199a update readme 2026-07-23 14:44:04 +10:00
RomanandGitHub 4a0dabe942 Merge pull request #32 from HideakiTaiki/patch-1
Update main_settings.xml
2026-07-09 23:27:10 +10:00
HideakiTaikiandGitHub dab9d2f432 Update main_settings.xml
The link to the instructions has been changed.

Signed-off-by: HideakiTaiki <4259299+HideakiTaiki@users.noreply.github.com>
2026-07-09 16:25:02 +03:00
romanvht 882388362a bump version 2026-06-29 17:16:27 +10:00
romanvht 34e8dfa252 add strategy picker in main 2026-06-29 17:09:59 +10:00
romanvht dff9a72b9b remove dnsPref.text in showDnsDialog 2026-06-29 15:19:00 +10:00
romanvht 8aa110e292 add dns picker dialog 2026-06-29 15:17:32 +10:00
romanvht 101c4da09d set cmd editor to default 2026-06-29 14:06:07 +10:00
RomanandGitHub 92f4fdb68a Merge pull request #31 from soulspark666/master
chore(android): migrate foreground service types
2026-06-29 13:36:58 +10:00
RomanandGitHub 9d80b4611a Merge pull request #30 from nyaexx/master
Update README-tr.md and proxytest_türkiye.sites
2026-06-21 10:56:31 +09:00
Soul Spark 64c8ec6964 chore(android): migrate foreground service types
Update service types to systemExempted and dataSync to fix fgs crash on
new android 14+. Also apply 16KB page size alignment for
native binaries.

Signed-off-by: Soul Spark <spark_soul@mail.ru>
2026-06-17 18:47:25 +05:30
nyaexxandGitHub 18838a0df2 Update documentation link in README-tr.md
Signed-off-by: nyaexx <70057364+nyaexx@users.noreply.github.com>
2026-06-15 18:53:01 +03:00
nyaexxandGitHub 87aa49b818 Add new sites to proxytest_türkiye.sites
Signed-off-by: nyaexx <70057364+nyaexx@users.noreply.github.com>
2026-06-15 18:48:45 +03:00
nyaexxandGitHub b55026a2ab Update README-tr.md
Signed-off-by: nyaexx <70057364+nyaexx@users.noreply.github.com>
2026-06-15 18:42:13 +03:00
RomanandGitHub e7fad95a79 Merge pull request #29 from re1coded/master
Official website
2026-06-15 18:54:45 +09:00
re1codedandGitHub 17813ad459 Update README-en.md
Signed-off-by: re1coded <gerget2006@gmail.com>
2026-06-15 16:30:58 +07:00
re1codedandGitHub e34b6a1c8f Update README.md
Signed-off-by: re1coded <gerget2006@gmail.com>
2026-06-15 16:26:00 +07:00
romanvht c9d1454c53 update strategies 2026-06-06 08:40:56 +10:00
RomanandGitHub 47ae0226f1 Merge pull request #28 from pelmeshkagrom/patch-1
Update README.md
2026-05-29 13:34:58 +10:00
PelmeshkaGromandGitHub 9e70325728 Update README.md
Updated the link to the ByeDPI documentation

Signed-off-by: PelmeshkaGrom <65809873+pelmeshkagrom@users.noreply.github.com>
2026-05-28 15:37:33 +05:00
romanvht 5db0cef475 check mode before always-on start 2026-05-28 18:08:55 +10:00
romanvht 383a97e78f update default strategy, refactore always on vpn 2026-05-28 17:53:26 +10:00
RomanandGitHub d13d502f79 Merge pull request #27 from erdem-akgun/master
Always On Vpn Support
2026-05-28 17:35:29 +10:00
Erdem AkgünandGitHub 5cbdce9a9e Always On Vpn Support
Added always on vpn support

Signed-off-by: Erdem Akgün <149613684+erdem-akgun@users.noreply.github.com>
2026-05-25 17:28:19 +03:00
Erdem AkgünandGitHub 0d5700bd82 Enable always-on VPN support in AndroidManifest
Signed-off-by: Erdem Akgün <149613684+erdem-akgun@users.noreply.github.com>
2026-05-25 17:22:01 +03:00
romanvht c482940bdc update strategies 2026-05-07 23:23:31 +10:00
romanvht 145c1b44c7 bump version 2026-05-07 22:58:57 +10:00
RomanandGitHub e8e70814e8 Merge pull request #26 from nyaexx/master
fix 2 typos in turkish strings.xml
2026-04-14 09:55:19 +10:00
nyaexandGitHub d53f158153 fix 2 typos in turkish strings.xml 2026-04-14 00:10:58 +03:00
romanvht be04f84a74 update setting utils and domainlistutils 2026-04-13 15:52:04 +10:00
RomanandGitHub 568ac84240 Merge pull request #25 from nyaexx/master
Add Turkey blocked domains list and update Turkish translations
2026-04-13 15:32:58 +10:00
nyaexandGitHub 92bbf4ad76 Merge pull request #4 from nyaexx/copilot/fix-default-active-ids-method
Move getDefaultActiveIds from SettingsUtils to DomainListUtils

Agent-Logs-Url: https://github.com/nyaexx/ByeByeDPI/sessions/cf520bd8-d681-4dc1-846d-1eef362d1e41

Co-authored-by: nyaexx <70057364+nyaexx@users.noreply.github.com>
2026-04-13 08:17:11 +03:00
465a201af2 Move getDefaultActiveIds from SettingsUtils to DomainListUtils
Agent-Logs-Url: https://github.com/nyaexx/ByeByeDPI/sessions/cf520bd8-d681-4dc1-846d-1eef362d1e41

Co-authored-by: nyaexx <70057364+nyaexx@users.noreply.github.com>
2026-04-13 05:11:06 +00:00
ece163eb00 fix: resolve system language to actual locale in getCurrentLanguage
Agent-Logs-Url: https://github.com/nyaexx/ByeByeDPI/sessions/fd265ccc-51ef-4652-850e-43d493231954

Co-authored-by: nyaexx <70057364+nyaexx@users.noreply.github.com>
2026-04-13 05:02:29 +00:00
28b483d906 fix: add getCurrentLanguage to SettingsUtils and use it in DomainListUtils.syncLists
Agent-Logs-Url: https://github.com/nyaexx/ByeByeDPI/sessions/fd265ccc-51ef-4652-850e-43d493231954

Co-authored-by: nyaexx <70057364+nyaexx@users.noreply.github.com>
2026-04-13 05:01:53 +00:00
nyaexandGitHub 25b5c861c8 Merge pull request #1 from nyaexx/copilot/update-settings-utils-domainutils
Move language-aware default active IDs out of DomainListUtils into SettingsUtils
2026-04-13 07:36:21 +03:00
c01c094dbb revert DomainListUtils changes, add getDefaultActiveIds to SettingsUtils
Agent-Logs-Url: https://github.com/nyaexx/ByeByeDPI/sessions/ff93858c-f278-4035-aedd-fd351e6d4228

Co-authored-by: nyaexx <70057364+nyaexx@users.noreply.github.com>
2026-04-13 04:34:25 +00:00
nyaexx edbb458801 update arrays 2026-04-12 16:44:34 +03:00
nyaexx 22a636c1ad update proxy test settings on resume 2026-04-12 16:43:43 +03:00
nyaexx 5825f0e2b4 remove wrong string 2026-04-12 16:42:34 +03:00
nyaexx 03d02c35d9 update README-tr.md instructions 2026-04-12 16:38:42 +03:00
nyaexx 0c8b3f9fa8 add turkey blocked domains list, update turkish translations 2026-04-12 16:31:54 +03:00
romanvht 1b9d2daa87 update manual links 2026-04-11 12:28:33 +10:00
romanvht c2309a02e8 update lists 2026-04-09 20:25:04 +10:00
romanvht 6bae66a1cc update strings 2026-04-09 19:30:50 +10:00
romanvht 05a544ec1f update strings, strategies 2026-04-09 19:19:53 +10:00
romanvht 014c00921f add reset lists btn 2026-04-09 18:41:12 +10:00
romanvht 7c706dc180 refactore lists, add donate links 2026-04-09 18:16:38 +10:00
romanvht bcadabc419 update services, add blacklist to help and version args 2026-04-02 20:26:59 +10:00
romanvht ffc1d13abd update ByeDpiVpnService 2026-04-02 17:53:57 +10:00
romanvht 92451a1036 add proxy test copy log btn 2026-04-02 16:41:43 +10:00
romanvht 2f60970ea5 remove http connetc toggle, bump byedpi to ba53229 2026-04-02 15:12:51 +10:00
romanvht 0dd7a98b01 update byedpi 2026-02-25 20:39:52 +10:00
romanvht a9a7c0036a bump version 2026-02-25 20:18:40 +10:00
romanvht 1f98e28ca4 update hs5t 2026-02-25 20:15:29 +10:00
romanvht 3a1c2979b2 refactore ByeDpiProxyPreferences, add lists to cmd 2026-02-25 20:01:56 +10:00
romanvht f7179d2660 auto restart service after apply cmd 2026-02-25 18:53:45 +10:00
romanvht cf8502bc8b add test and list btns to main screen 2026-02-25 17:54:31 +10:00
romanvht 9a72061a52 show active lists in settings 2026-02-25 17:02:20 +10:00
romanvht 71885543ce add Vietnamese language 2026-02-25 16:53:43 +10:00
romanvht 2ee2082041 move startForeground to onStart 2026-02-17 19:27:12 +10:00
romanvht 6f55038701 remove unused import 2026-02-15 14:18:44 +10:00
romanvht e6479b3fee remove broadcast in tile service 2026-02-15 14:16:44 +10:00
romanvht db24723e09 update tile service 2026-02-01 01:34:38 +10:00
romanvht 7efe23fffb update domains lists strings 2026-01-31 21:55:19 +10:00
romanvht a35125db9a fix copy string id 2026-01-31 21:51:47 +10:00
romanvht da6fd1c0b3 fix import domain lists 2026-01-31 21:48:31 +10:00
romanvht 92adae2e80 sorted domain list by name 2026-01-31 21:25:40 +10:00
romanvht e26cec600c fix domain list checkbox 2026-01-31 21:01:41 +10:00
romanvht edc06919f2 add domain list editor 2026-01-31 20:35:30 +10:00
romanvht 22aef6e421 item site result is made clickable 2026-01-31 18:41:13 +10:00
romanvht 3cab469b39 update strategies 2026-01-20 20:25:04 +10:00
romanvht a172e4541f rename cmd settings fragment 2026-01-20 19:37:25 +10:00
romanvht 3871244f9e bump version 2026-01-20 19:23:23 +10:00
romanvht 822d2e45f1 update getItemCount in app and site adapter 2026-01-20 19:03:36 +10:00
romanvht 1fcda32165 update app selection adapter 2026-01-20 19:01:48 +10:00
romanvht 9e3a9421e9 add clipboard utils, update strategy adapter 2026-01-20 18:57:02 +10:00
romanvht 8c1c1edef6 remove isSucceful in SiteCheckUtils 2026-01-20 16:50:15 +10:00
romanvht a2816fc183 bump version 2026-01-18 19:06:29 +10:00
romanvht 3566fe435f fix history item padding 2026-01-17 03:16:33 +10:00
romanvht a66b1284da update byedpi 2026-01-17 02:07:46 +10:00
romanvht 527f49d03a update hev socks5 tunnel 2026-01-17 02:04:32 +10:00
romanvht 53f2a0e2c3 connect from a proxy test 2026-01-17 01:55:18 +10:00
romanvht c3299b0701 update history layout 2026-01-16 20:28:14 +10:00
romanvht 632f193eda fix settings button margin 2026-01-16 19:20:55 +10:00
romanvht ccc509dee5 change namespace 2026-01-16 02:16:13 +10:00
romanvht 54205002e9 update gitignore 2026-01-16 01:46:14 +10:00
romanvht f16d2687dd remove .idea from repository 2026-01-16 01:45:27 +10:00
romanvht c2828e47c8 update readme 2026-01-15 23:22:19 +10:00
RomanandGitHub 044b8c6b51 Merge pull request #23 from nyaexx/master
Improvements for Turkish translation
2026-01-15 23:17:16 +10:00
romanvht e0107bbc04 bump version 2026-01-15 22:08:46 +10:00
romanvht 15c99ae147 add focus stroke to connect btn 2026-01-15 22:07:29 +10:00
nyaexandGitHub 08c77696d1 Typo fixed for Turkish translation
Signed-off-by: nyaex <70057364+nyaexx@users.noreply.github.com>
2026-01-15 14:26:54 +03:00
nyaexandGitHub 55fb347a28 Merge branch 'romanvht:master' into master 2026-01-15 14:25:50 +03:00
romanvht ea42ae7fe8 redesign main screen 2026-01-15 21:09:15 +10:00
romanvht 4abc723322 update settings utils, add check connect status before start 2026-01-15 18:08:51 +10:00
nyaexandGitHub cc773c780f Improvements for Turkish translation
Signed-off-by: nyaex <70057364+nyaexx@users.noreply.github.com>
2026-01-14 22:13:22 +03:00
nyaexandGitHub 7f30a06946 Improvements for Turkish translation
Signed-off-by: nyaex <70057364+nyaexx@users.noreply.github.com>
2026-01-14 22:11:02 +03:00
nyaexandGitHub 1f694ac5f2 Improvements for Turkish translation
Signed-off-by: nyaex <70057364+nyaexx@users.noreply.github.com>
2026-01-14 22:06:34 +03:00
romanvht a77fd628f8 Merge branch 'master' of https://github.com/romanvht/ByeDPIAndroid 2026-01-15 03:11:46 +10:00
romanvht 7c47f46729 update history item layout 2026-01-15 03:11:35 +10:00
github-actions 326d40d3a3 Update READMEs with latest CONTRIBUTORS 2026-01-14 16:06:27 +00:00
romanvht e76413ba00 update action 2026-01-15 02:05:53 +10:00
romanvht e21daeadbc update readme 2026-01-15 01:46:45 +10:00
romanvht b38e001d34 update contributors action 2026-01-15 01:45:05 +10:00
romanvht c7033423c3 add contributors.md and update readme action 2026-01-15 01:33:15 +10:00
romanvht 68ec5a90f3 fix show crash disclaimer 2026-01-15 00:30:02 +10:00
romanvht fa50474399 fix show test log after proxy test crash 2026-01-15 00:26:26 +10:00
romanvht 261ba99885 update proguard, cteate settings utils 2026-01-15 00:12:30 +10:00
romanvht 26608e7a70 change proxytest settings summary 2026-01-14 23:38:13 +10:00
romanvht 9d1aa8da38 add proxytest settings summary 2026-01-14 23:35:54 +10:00
romanvht 51eb5c2e28 bump version 2026-01-14 22:57:45 +10:00
romanvht bdc5daf6bd add settings icon 2026-01-14 22:44:55 +10:00
romanvht 824561ff8f proxy test redesign and refactor 2026-01-14 21:03:17 +10:00
romanvht a665807db7 update test sites, history styles 2026-01-14 19:11:42 +10:00
romanvht 4ed61f1e27 update gitignore 2026-01-14 17:50:23 +10:00
romanvht a86b25caee add storage utils 2026-01-14 17:49:23 +10:00
romanvht 48c365af91 show battery optimization request 2026-01-14 17:35:22 +10:00
romanvht a0605d4ab3 show package name in apps screen 2026-01-14 16:49:19 +10:00
romanvht 2e16e09c0d async loader in apps screen 2026-01-14 16:34:55 +10:00
romanvht d17f54ff0b update ui threshold in history fragment 2026-01-13 23:06:11 +10:00
romanvht e247b328b0 update quick tile status after change connection status 2026-01-13 23:01:34 +10:00
romanvht 11473993c6 add material-switch if sdk > 31 2026-01-13 22:16:10 +10:00
romanvht 37c214fefe update build.gradle 2025-12-29 00:29:44 +10:00
romanvht bdba6fd159 update toggle activity 2025-12-28 23:30:36 +10:00
romanvht b4a8ed1d62 update locale 2025-12-28 23:15:16 +10:00
RomanandGitHub 4ab86b23c2 Merge pull request #19 from romanvht/revert-18-master
Revert "Update dependencies in app/build.gradle.kts"
2025-12-28 23:12:20 +10:00
RomanandGitHub 961435dd89 Revert "Update dependencies in app/build.gradle.kts" 2025-12-28 23:11:08 +10:00
RomanandGitHub 133adf847f Merge pull request #18 from mxtherfxcker/master
Update dependencies in app/build.gradle.kts
2025-12-28 23:01:55 +10:00
IvanandGitHub 04fb1df0a8 Update dependencies in app/build.gradle.kts
Signed-off-by: Ivan <ctapiuxleb3@gmail.com>
2025-12-27 18:47:43 +03:00
romanvht 76d2bf523c Merge branch 'master' of https://github.com/romanvht/ByeDPIAndroid 2025-12-20 18:17:50 +10:00
romanvht 595247568d add force stop btn, update vpn service 2025-12-20 18:17:34 +10:00
romanvhtandGitHub a9dcf229f1 Merge pull request #17 from VadimTotok/patch-2
Добавлен бейдж "GitHub code size in bytes" (README-en.md)
2025-12-11 13:54:42 +10:00
romanvhtandGitHub d90eec77b3 Merge pull request #16 from VadimTotok/patch-3
Добавлен бейдж "GitHub code size in bytes" (README-tr.md)
2025-12-11 13:54:29 +10:00
romanvhtandGitHub 88590e1a21 Merge pull request #15 from VadimTotok/patch-1
Добавлен бейдж "GitHub code size in bytes"
2025-12-11 13:54:16 +10:00
Vadim TotokandGitHub ac09e56121 Update README-tr.md
Signed-off-by: Vadim Totok <112573798+VadimTotok@users.noreply.github.com>
2025-12-10 10:19:00 +03:00
Vadim TotokandGitHub 4d067796d0 Update README-en.md
Signed-off-by: Vadim Totok <112573798+VadimTotok@users.noreply.github.com>
2025-12-10 10:18:31 +03:00
Vadim TotokandGitHub 9ad3507913 Добавлен бейдж "GitHub code size in bytes"
Показывает общий объём исходного кода в репозитории

Signed-off-by: Vadim Totok <112573798+VadimTotok@users.noreply.github.com>
2025-12-09 20:37:21 +03:00
romanvht 682cd3196c bump version, update pause notification, minor changes 2025-12-05 16:32:14 +10:00
romanvht 863c166335 fix resume button, get proxy ip and port in proxytest 2025-11-25 01:55:00 +10:00
romanvht 6bff33c2c4 add delay after stop test 2025-11-13 19:19:46 +10:00
romanvht 95f4d8d55c add delay to start test 2025-11-13 18:51:30 +10:00
romanvht 0d9fc9b630 add stdlib include to native_lib 2025-11-13 18:24:33 +10:00
romanvhtandGitHub b6087ac465 Merge pull request #14 from VadimTotok/master
Улучшение безопасности и стабильности
2025-11-13 18:21:15 +10:00
Vadim TotokandGitHub ffb65d21ea Улучшение безопасности и стабильности 2025-11-12 00:14:51 +03:00
romanvht c6c030fd2d add pause notification, Қазақша language 2025-11-04 16:42:50 +10:00
romanvht a7d831b874 bump version, update test activity 2025-11-04 15:57:37 +10:00
romanvht 036e325a5f update depends, byedpi, hs5t 2025-11-04 15:40:25 +10:00
romanvht a49617cab0 fix remove pinned strategy 2025-09-29 12:07:51 +04:00
romanvht ea23207315 update strategies 2025-09-27 01:11:42 +04:00
romanvht 034ef283b1 delay moved to the proxy start 2025-09-26 14:29:18 +04:00
romanvht 517afeb5d3 update strings 2025-09-26 13:01:19 +04:00
romanvht 602c216566 update byedpi 2025-09-22 23:15:04 +04:00
romanvht 78af316c7a update dependencies, fix duplicate key in main settings 2025-09-20 18:25:23 +04:00
romanvht f92dbb1e95 update app selection 2025-09-10 03:09:40 +04:00
romanvht 7466cf675d fix crash after paste in alert dialog on vivo devices 2025-09-09 21:17:24 +04:00
romanvht ddb4760b6d update hev-socks5-tunnel config 2025-09-08 20:16:22 +04:00
romanvht 5da675bd9f return true if proxy stop without timeout 2025-09-08 19:31:14 +04:00
romanvht 42e014a508 validate sni before save 2025-09-08 17:44:30 +04:00
romanvht e243f8ea2a add edit sni in proxy test settings, minor fixes 2025-09-08 17:27:58 +04:00
romanvht ee14f368bd update strategies 2025-08-31 20:21:57 +04:00
romanvht 936bb39de2 updating dependencies 2025-08-29 19:43:16 +04:00
romanvht 9ae92b78e3 desync udp by default 2025-08-29 19:40:39 +04:00
romanvht bbdbfe9943 update strategies 2025-08-18 03:13:14 +04:00
romanvht 3e58bc2feb removed duplicate flag from strategy 2025-08-15 18:22:42 +04:00
romanvht d0f1ef1bd5 update strategies 2025-08-15 17:41:16 +04:00
romanvht ea4d3b2d7f update manual links 2025-08-15 15:14:06 +04:00
romanvht 39a051e9f0 move set lang and set theme to base activity 2025-08-09 15:48:40 +04:00
romanvht 6214a2a1d6 change default timeout to 3 2025-08-09 04:46:03 +04:00
romanvht fcb3f9f00d update validation in proxy test settings 2025-08-09 04:40:04 +04:00
romanvht b8b87ca761 add request timeout in proxy test 2025-08-09 04:36:39 +04:00
romanvht 16b248a4f5 directly open app settings for all files access 2025-08-09 02:54:23 +04:00
romanvht 874f5dc9ee change active cmd after editing 2025-08-09 00:41:41 +04:00
romanvht c41c75d8f3 fix enable cmd check 2025-08-09 00:26:46 +04:00
romanvht 67ce573cda update main settings fragment 2025-08-09 00:23:06 +04:00
romanvht cb354bf98d update native-lib 2025-08-08 23:46:43 +04:00
romanvht 8dbe0f3763 update strings 2025-08-08 23:27:53 +04:00
romanvht b5f0ee493a add storage permission support 2025-08-08 23:18:20 +04:00
romanvht 6997b99e56 update hev socks5 tunnel 2025-07-26 21:51:27 +04:00
romanvht cc94853d9b change toggle service shortcut id 2025-07-25 18:38:56 +04:00
romanvht d5d87618dd add edit strategy, bump version 2025-07-25 12:46:54 +04:00
romanvht 35400aa961 fix empty short label in shortcut 2025-07-25 12:15:14 +04:00
romanvht 3692641bba add onlyStart, onlyStop, onlyUpdate flags in ToggleActivity 2025-07-25 02:48:35 +04:00
romanvht f7562704fa add dynamic shortcuts 2025-07-25 01:29:48 +04:00
romanvht 81b153b499 update site check logic, edit domains 2025-07-20 01:05:42 +04:00
romanvht 6550f37db6 move abis to val 2025-07-18 21:30:55 +04:00
romanvht ba2a799131 update build.gradle 2025-07-18 21:24:28 +04:00
romanvhtandGitHub 386e553513 Merge pull request #12 from topminipie/pr
Optimize app
2025-07-18 21:20:54 +04:00
romanvht 1e43d61abb bump version, update proxytest 2025-07-18 19:37:17 +04:00
romanvht 99b9de7fcb update readme 2025-07-18 17:13:00 +04:00
romanvht 671ce50fe4 update readme, bump dependencies 2025-07-18 17:11:37 +04:00
topminipie 2007b54445 Optimize app 2025-07-16 10:45:12 +03:00
romanvht dcb546a585 bump version 2025-07-12 13:09:46 +04:00
romanvht e5a69e6b86 update hev_socks5_tunnel 2025-07-12 13:06:35 +04:00
romanvht 52cdf9141a bump version 2025-07-11 13:57:17 +04:00
romanvht 02b9c7e685 update status to failed if proxy not run 2025-07-11 12:24:30 +04:00
romanvht d3331828ad move startTun2Socks to lifecycle scope 2025-07-10 04:25:34 +04:00
romanvht 44fcfbfcdb check proxy before launching tun 2025-07-10 03:55:44 +04:00
romanvht 7754020772 update byedpi vpn service 2025-07-10 02:09:07 +04:00
romanvht 08ea7ecef4 add delay to update status 2025-07-10 01:58:26 +04:00
romanvht 62a0fa9ba3 increase start button delay to one second 2025-07-10 00:36:00 +04:00
romanvht cb585daf39 fix app prize after invalid arguments 2025-07-10 00:24:37 +04:00
romanvht 41a12cfee2 add strategies 2025-07-08 21:47:02 +04:00
romanvht da1c2da98c edit strategies 2025-07-08 19:49:30 +04:00
romanvht ebaa4636b8 edit control highlight color 2025-07-08 19:33:03 +04:00
romanvht 88c9652989 rename xml's 2025-07-08 18:25:12 +04:00
romanvht 7a6261bbb4 add dynamic padding in history items 2025-07-08 18:03:27 +04:00
romanvht e77cb52308 update delay in test 2025-07-08 13:56:12 +04:00
romanvht 5262e77fd2 update native-lib 2025-07-08 01:18:00 +04:00
romanvht 06c2796d24 move clien create to check sites func 2025-07-08 00:19:29 +04:00
romanvht f36243ac63 move site check utils to okhttp 2025-07-07 23:58:14 +04:00
romanvht ac539e2393 update app selection 2025-07-07 09:13:32 +04:00
romanvht 5f5b4500d3 remove unused import 2025-07-07 09:12:15 +04:00
romanvht 31e72f51fd remove recreate in proxy test 2025-07-07 09:10:42 +04:00
romanvht 4ed6c16f06 update dependencies 2025-07-07 08:59:47 +04:00
romanvht bc8dddc610 fix icon size 2025-07-07 08:04:11 +04:00
romanvht da6649a638 add text appearance styles 2025-07-07 07:40:48 +04:00
romanvht 67597ea3dc fix app selection 2025-07-07 06:03:11 +04:00
romanvht 5b4f985cd5 edit preference font size 2025-07-07 05:48:55 +04:00
romanvht b3fe8843f5 edit clear btn, cmd history padding 2025-07-07 05:25:55 +04:00
romanvht 9db0d09735 update styles, app filter, test activity 2025-07-07 05:09:00 +04:00
romanvht d2a09f54d1 bump version 2025-07-07 02:40:53 +04:00
romanvht 7f7ded095b update byedpi 2025-07-07 02:39:47 +04:00
romanvht 06227751b9 update old styles, proxy test btn 2025-07-07 02:26:32 +04:00
romanvht 69d6c7268a update material styles 2025-07-07 02:05:10 +04:00
romanvht 66a7aeef3d add night theme v31 2025-07-07 00:15:47 +04:00
romanvhtandGitHub c1f02116c3 Merge pull request #11 from nyaexx/master
Add dynamic colors and themed app icon support for Android 12+
2025-07-07 00:14:47 +04:00
nyaex 065195cf9e Some description changes for Turkish Readme 2025-07-06 19:05:44 +03:00
nyaex 4fe13c3f6e Merge remote-tracking branch 'origin/master'
# Conflicts:
#	.idea/deploymentTargetSelector.xml
2025-07-06 18:51:37 +03:00
nyaex 65b1461297 Add dynamic colors support and adaptive app icon 2025-07-06 18:44:29 +03:00
romanvht f91b9ee13b recreate activity test in android tv/box after stop test 2025-07-06 04:35:08 +04:00
romanvht c1f7005e11 update proxy test 2025-07-06 04:02:39 +04:00
romanvht 1ac4b24869 emergency socket close when proxy freeze 2025-07-06 03:04:05 +04:00
romanvht 3eec2aa99d change default desync method in ui and default arguments in cmd 2025-07-05 18:02:32 +04:00
romanvht b286b808cd add disconnect connection after site check 2025-07-05 15:09:18 +04:00
romanvht e1e40ddae8 update readme 2025-07-05 06:03:59 +04:00
romanvht 192a18d3f3 add force stop test afrer error start proxy 2025-07-05 02:52:48 +04:00
romanvht 7686f3bedf up version code 2025-07-05 01:00:06 +04:00
romanvhtandGitHub d7126c3a00 Merge pull request #9 from KG7x/patch-1
Clear duplicate
2025-07-05 00:42:16 +04:00
romanvhtandGitHub f482d87dd2 Merge pull request #10 from KG7x/patch-2
More clear
2025-07-05 00:42:03 +04:00
KG7xandGitHub 1a7e27d3f7 More clear
Signed-off-by: KG7x <Cados7x@gmail.com>
2025-07-04 23:37:10 +03:00
KG7xandGitHub 9f7cc4c4e6 Clear duplicate
Signed-off-by: KG7x <Cados7x@gmail.com>
2025-07-04 23:34:15 +03:00
romanvht 19b87f6588 update byedpi to 45d8c7e, bump version 2025-07-04 23:32:23 +04:00
romanvht 404582aad4 update strategies 2025-07-04 22:40:56 +04:00
romanvht 4a9086adb3 edit quick tile service 2025-07-04 18:07:44 +04:00
romanvht 9a20e24a3c update native-lib 2025-07-03 15:18:36 +04:00
romanvht 399262468d fix reset params after proxy stop, add delay test start btn 2025-07-03 15:03:40 +04:00
romanvht 81e4061519 fix test crash on android 7 2025-07-02 17:08:26 +04:00
romanvht c6944fadfe bump version 2025-07-01 19:44:30 +04:00
romanvht 7e03ac6a09 edit test delay to 1 by default 2025-07-01 07:19:38 +04:00
romanvht 7bc6708397 remove suspend in proxy/vpn service 2025-07-01 06:02:04 +04:00
romanvht 018e9cc543 edit stopTesting 2025-07-01 05:49:57 +04:00
romanvht 66bf15e09e update TestActivity, edit TestJob 2025-07-01 04:14:42 +04:00
romanvht 0fbbc139c2 add domains list in proxy test, update native-lib 2025-07-01 03:01:44 +04:00
romanvht 65d1ce8b80 update native-lib 2025-06-28 02:07:48 +04:00
romanvht afba022162 update native lib, test proxy 2025-06-28 01:59:36 +04:00
romanvht ee6e564781 update byedpi, cmds 2025-06-27 22:57:06 +04:00
romanvht cc2ede88c6 fix qstile 2025-06-25 17:47:58 +04:00
romanvht 850ca810d4 update readme 2025-06-24 13:24:36 +04:00
romanvht f86f5962dd add p.s. build 2025-06-24 13:22:08 +04:00
romanvht af2759d2c5 update readme, update byedpi 2025-06-24 13:18:12 +04:00
romanvht 6b480ffc26 update readme 2025-06-15 21:27:33 +04:00
romanvht 19fedcebad add history manager 2025-06-15 20:47:03 +04:00
romanvht 1badc97de6 fix main.h 2025-06-15 18:22:22 +04:00
romanvht a34f546148 refactoring native-lib, remove utils 2025-06-15 18:18:41 +04:00
romanvht 6ebe472dba edit cmds 2025-06-12 15:29:50 +04:00
romanvht 10c6b5c1a7 update byedpi, fix appselection, update cmds 2025-06-12 15:05:43 +04:00
romanvht 06e58b0352 edit readme logo 2025-05-29 16:57:18 +04:00
romanvht 0a380c8540 update Türkçe readme 2025-05-29 16:53:18 +04:00
romanvht 9657ac17f9 replace readme logo 2025-05-29 16:50:20 +04:00
romanvht 6562c5e582 fix connect btn focus, replace notif icon 2025-05-29 15:56:36 +04:00
romanvht 444b7f43e9 edit build.gradle.kts 2025-04-25 15:54:58 +04:00
romanvhtandGitHub e95b151b83 Merge pull request #8 from nyaexx/master
Turkish language added
2025-04-25 15:40:52 +04:00
nyaexandGitHub 4962c63b1e Update strings.xml
Signed-off-by: nyaex <70057364+nyaexx@users.noreply.github.com>
2025-04-20 17:10:07 +03:00
nyaexandGitHub 16371f7db7 Update strings.xml
Signed-off-by: nyaex <70057364+nyaexx@users.noreply.github.com>
2025-04-20 14:39:25 +03:00
nyaexandGitHub e7252a700a Update README.md
Signed-off-by: nyaex <70057364+nyaexx@users.noreply.github.com>
2025-04-20 14:37:01 +03:00
nyaexandGitHub a9d0e7b467 Update README-en.md
Signed-off-by: nyaex <70057364+nyaexx@users.noreply.github.com>
2025-04-20 14:36:07 +03:00
nyaexandGitHub 82a8db97b7 Create README-tr.md
Türkçe readme

Signed-off-by: nyaex <70057364+nyaexx@users.noreply.github.com>
2025-04-20 14:36:02 +03:00
nyaex 4d923af739 Türkçe dil desteği ve diğer güncellemeler 2025-04-20 14:15:38 +03:00
romanvht 6c42ed49b1 Fix ip and port parse 2025-04-12 20:27:27 +04:00
romanvht 3581731c1f add link to manual 2025-03-23 21:52:10 +04:00
romanvht d68beebed5 edit shellsplit 2025-03-10 10:40:15 +04:00
romanvht 48887d1d99 add http_connect toggle, edit strings, update hev-socks5-tunnel 2025-03-09 03:25:28 +04:00
romanvht ede0e80fb7 bump byedpi version 2025-02-27 22:48:31 +04:00
romanvht 1b981828e7 Add open editor link, update byedpi 2025-02-27 22:39:57 +04:00
romanvht 0f786b8630 edit readme 2025-02-13 14:14:58 +04:00
romanvht c763d039b0 add en readme 2025-02-13 14:08:56 +04:00
romanvht 4a17b31ecd edit native-lib and byedpiproxy 2025-02-09 22:08:41 +04:00
romanvht 242f9c74c2 add is_testing = false if proxy crash 2025-02-09 16:39:42 +04:00
romanvht d780827013 edit ByeDpiProxy 2025-02-09 16:33:38 +04:00
romanvht 1dae8241a1 move ui fake offset to DesyncMethod.Fake 2025-02-09 14:50:37 +04:00
romanvht 956a8bc206 move ui cmd args to proxy preferences 2025-02-09 14:45:40 +04:00
romanvht 2ada124778 fix blacklist hosts in UI 2025-02-09 06:10:47 +04:00
romanvht bda09f7ae0 bump version, edit native-lib 2025-02-09 05:58:25 +04:00
romanvht a8299ecc23 set default udp fake count to 1 2025-02-09 02:58:35 +04:00
romanvht d37e9c16d7 convert ui editor to cmd args 2025-02-09 02:42:12 +04:00
romanvht ccc20bd681 add shortcut toggle and refactoring quicktile 2025-02-07 20:56:48 +04:00
romanvht 94aa59a05f update byedpi version in settings 2025-02-06 21:45:18 +04:00
romanvht 46b2ee70e7 update byedpi to last commit 2025-02-06 21:31:04 +04:00
romanvht 122c96657a update byedpi to 0.16.5 2025-02-06 00:43:02 +04:00
romanvht 9ae2dfced8 update byedpi to 0.16.4 2025-02-03 18:52:43 +04:00
romanvht f08fb1701d edit strings, cmake flags, history limit 2025-02-03 18:05:21 +04:00
romanvht 6a498b1aca fix reset_params 2025-02-03 00:13:54 +04:00
romanvht e3fdf629e4 update byedpi to 0.16.3, fix tile for emui 2025-02-02 23:56:56 +04:00
romanvht 37ebe83719 update readme 2025-01-25 00:20:07 +04:00
romanvht 5c7c7b5271 update byedpi to 1d85d01 2025-01-18 18:31:10 +04:00
194 changed files with 7947 additions and 3950 deletions
+1 -1
View File
@@ -1,7 +1,7 @@
name: Bug Report / Сообщение об ошибке
description: File a bug report / Сообщить об ошибке в программе
labels: [ bug ]
assignees: dovecoteescapee
assignees: romanvht
body:
- type: markdown
attributes:
+1 -1
View File
@@ -1,7 +1,7 @@
name: Feature request / Предложить новую функциональность
description: Suggest an idea for this project / Предложить идею для проекта
labels: [ enhancement ]
assignees: dovecoteescapee
assignees: romanvht
body:
- type: textarea
id: problem
File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 5.8 KiB

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

+18 -13
View File
@@ -1,17 +1,22 @@
*.iml
.gradle
/local.properties
/.idea/caches
/.idea/deploymentTargetDropDown.xml
/.idea/libraries
/.idea/modules.xml
/.idea/workspace.xml
/.idea/navEditor.xml
/.idea/assetWizardSettings.xml
# OS
.DS_Store
/build
# IDE
.idea/
.kotlin/
*.iml
# Gradle
.gradle/
build/
# Local config
local.properties
# Android
/captures
.externalNativeBuild
.cxx
local.properties
/app/src/main/jniLibs
# JNI
app/src/main/jniLibs/
-3
View File
@@ -1,3 +0,0 @@
# Default ignored files
/shelf/
/workspace.xml
Generated
-1
View File
@@ -1 +0,0 @@
ByeDpi
-22
View File
@@ -1,22 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="AndroidTestResultsUserPreferences">
<option name="androidTestResultsTableState">
<map>
<entry key="28629151">
<value>
<AndroidTestResultsTableState>
<option name="preferredColumnWidths">
<map>
<entry key="Duration" value="90" />
<entry key="Pixel" value="120" />
<entry key="Tests" value="360" />
</map>
</option>
</AndroidTestResultsTableState>
</value>
</entry>
</map>
</option>
</component>
</project>
-26
View File
@@ -1,26 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="AppInsightsSettings">
<option name="tabSettings">
<map>
<entry key="Firebase Crashlytics">
<value>
<InsightsFilterSettings>
<option name="connection">
<ConnectionSetting>
<option name="appId" value="PLACEHOLDER" />
<option name="mobileSdkAppId" value="" />
<option name="projectId" value="" />
<option name="projectNumber" value="" />
</ConnectionSetting>
</option>
<option name="signal" value="SIGNAL_UNSPECIFIED" />
<option name="timeIntervalDays" value="THIRTY_DAYS" />
<option name="visibilityType" value="ALL" />
</InsightsFilterSettings>
</value>
</entry>
</map>
</option>
</component>
</project>
-123
View File
@@ -1,123 +0,0 @@
<component name="ProjectCodeStyleConfiguration">
<code_scheme name="Project" version="173">
<JetCodeStyleSettings>
<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
@@ -1,5 +0,0 @@
<component name="ProjectCodeStyleConfiguration">
<state>
<option name="USE_PER_PROJECT_SETTINGS" value="true" />
</state>
</component>
-6
View File
@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="CompilerConfiguration">
<bytecodeTargetLevel target="17" />
</component>
</project>
-21
View File
@@ -1,21 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="deploymentTargetSelector">
<selectionStates>
<SelectionState runConfigName="All Tests">
<option name="selectionMode" value="DROPDOWN" />
</SelectionState>
<SelectionState runConfigName="debug">
<option name="selectionMode" value="DROPDOWN" />
<DropdownSelection timestamp="2025-01-17T16:42:30.985316100Z">
<Target type="DEFAULT_BOOT">
<handle>
<DeviceId pluginId="LocalEmulator" identifier="path=C:\Users\roman\.android\avd\Pixel_8_API_35.avd" />
</handle>
</Target>
</DropdownSelection>
<DialogSelection />
</SelectionState>
</selectionStates>
</component>
</project>
-19
View File
@@ -1,19 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="GradleMigrationSettings" migrationVersion="1" />
<component name="GradleSettings">
<option name="linkedExternalProjectsSettings">
<GradleProjectSettings>
<option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="gradleJvm" value="#GRADLE_LOCAL_JAVA_HOME" />
<option name="modules">
<set>
<option value="$PROJECT_DIR$" />
<option value="$PROJECT_DIR$/app" />
</set>
</option>
<option name="resolveExternalAnnotations" value="false" />
</GradleProjectSettings>
</option>
</component>
</project>
-6
View File
@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="KotlinJpsPluginSettings">
<option name="version" value="1.9.22" />
</component>
</project>
-10
View File
@@ -1,10 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectMigrations">
<option name="MigrateToGradleLocalJavaHome">
<set>
<option value="$PROJECT_DIR$" />
</set>
</option>
</component>
</project>
-10
View File
@@ -1,10 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="ProjectRootManager" version="2" languageLevel="JDK_17" default="true" project-jdk-name="jbr-17" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/build/classes" />
</component>
<component name="ProjectType">
<option name="id" value="Android" />
</component>
</project>
-6
View File
@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="RenderSettings">
<option name="showDecorations" value="true" />
</component>
</project>
Generated
-12
View File
@@ -1,12 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
<mapping directory="$PROJECT_DIR$/app/src/main/cpp/byedpi" vcs="Git" />
<mapping directory="$PROJECT_DIR$/app/src/main/jni/hev-socks5-tunnel" vcs="Git" />
<mapping directory="$PROJECT_DIR$/app/src/main/jni/hev-socks5-tunnel/src/core" vcs="Git" />
<mapping directory="$PROJECT_DIR$/app/src/main/jni/hev-socks5-tunnel/third-part/hev-task-system" vcs="Git" />
<mapping directory="$PROJECT_DIR$/app/src/main/jni/hev-socks5-tunnel/third-part/lwip" vcs="Git" />
<mapping directory="$PROJECT_DIR$/app/src/main/jni/hev-socks5-tunnel/third-part/yaml" vcs="Git" />
</component>
</project>
+77
View File
@@ -0,0 +1,77 @@
<div align="center">
<p>
<img src="https://github.com/romanvht/ByeDPIAndroid/raw/master/.github/images/app.svg" alt="Логотип ByeDPI" width="200" />
</p>
<h1>ByeByeDPI Android</h1>
<p>
<a href="README.md">Русский</a> |
English |
<a href="README-tr.md">Türkçe</a>
</p>
<p>
<a href="https://github.com/romanvht/ByeByeDPI/releases/latest"><img src="https://img.shields.io/github/v/release/romanvht/ByeByeDPI" alt="Latest Release" /></a>
<a href="https://github.com/romanvht/ByeByeDPI/releases"><img src="https://img.shields.io/github/downloads/romanvht/ByeByeDPI/total" alt="Downloads" /></a>
<a href="https://github.com/romanvht/ByeByeDPI/blob/master/LICENSE"><img src="https://img.shields.io/github/license/romanvht/ByeByeDPI" alt="License" /></a>
<a href="https://github.com/romanvht/ByeByeDPI"><img src="https://img.shields.io/github/languages/code-size/romanvht/ByeByeDPI" alt="GitHub code size in bytes"/></a>
</p>
</div>
An Android application that runs ByeDPI locally and redirects all traffic through it.
For stable operation, you may need to adjust the settings. More information about the available options can be found in the [ByeDPI documentation](https://github.com/hufrea/byedpi/blob/main/README.md).
This application is not a VPN. It uses Androids VPN mode to redirect traffic, but it does not send any data to a remote server. It does not encrypt your traffic or hide your IP address.
The application has only one official website:
https://byebyedpi.xyz
---
### Features
* Automatically starts the service when the device boots
* Saves command-line parameter lists
* Improved compatibility with Android TV/BOX devices
* Per-app split tunneling
* Settings import and export
### Usage
* Enable the corresponding option in the settings to use automatic startup.
* It is recommended to connect to the VPN once in order to accept the permission request.
* After that, the application will automatically start the service when the device boots, depending on the selected mode: VPN or Proxy.
* Comprehensive community guide: [ByeByeDPI-Manual](https://byebyedpi.xyz)
### Building
1. Clone the repository with its submodules:
```bash
git clone --recurse-submodules
```
2. Run the build script from the repository root:
```bash
./gradlew assembleRelease
```
3. The APK will be located in:
`app/build/outputs/apk/release/`
> Note: hev_socks5_tunnel cannot be built on Windows. You will need to use WSL.
### Signing Certificate Hash
SHA-256:
`77:45:10:75:AC:EA:40:64:06:47:5D:74:D4:59:88:3A:49:A6:40:51:FA:F3:2E:42:F7:18:F3:F9:77:7A:8D:FB`
### Dependencies
* [ByeDPI](https://github.com/hufrea/byedpi)
* [hev-socks5-tunnel](https://github.com/heiher/hev-socks5-tunnel)
### Acknowledgements
* [hufrea](https://github.com/hufrea) — for [ByeDPI](https://github.com/hufrea/byedpi)
* [dovecoteescapee](https://github.com/dovecoteescapee) — for the original implementation of [ByeDPIAndroid](https://github.com/dovecoteescapee/ByeDPIAndroid)
+77
View File
@@ -0,0 +1,77 @@
<div align="center">
<p>
<img src="https://github.com/romanvht/ByeDPIAndroid/raw/master/.github/images/app.svg" alt="Логотип ByeDPI" width="200" />
</p>
<h1>ByeByeDPI Android</h1>
<p>
<a href="README.md">Русский</a> |
<a href="README-en.md">English</a> |
Türkçe
</p>
<p>
<a href="https://github.com/romanvht/ByeByeDPI/releases/latest"><img src="https://img.shields.io/github/v/release/romanvht/ByeByeDPI" alt="Latest Release" /></a>
<a href="https://github.com/romanvht/ByeByeDPI/releases"><img src="https://img.shields.io/github/downloads/romanvht/ByeByeDPI/total" alt="Downloads" /></a>
<a href="https://github.com/romanvht/ByeByeDPI/blob/master/LICENSE"><img src="https://img.shields.io/github/license/romanvht/ByeByeDPI" alt="License" /></a>
<a href="https://github.com/romanvht/ByeByeDPI"><img src="https://img.shields.io/github/languages/code-size/romanvht/ByeByeDPI" alt="GitHub code size in bytes"/></a>
</p>
</div>
ByeDPIyi yerel olarak çalıştıran ve tüm trafiği üzerinden yönlendiren bir Android uygulaması.
Kararlı çalışması için ayarların değiştirilmesi gerekebilir. Çeşitli ayarlar hakkında daha fazla bilgiyi [ByeDPI belgelerinde](https://github.com/hufrea/byedpi/blob/main/README.md) bulabilirsiniz.
Bu uygulama bir VPN değildir. Trafiği yönlendirmek için Androidin VPN modunu kullanır, ancak hiçbir veriyi uzak bir sunucuya göndermez. Trafiği şifrelemez ve IP adresinizi gizlemez.
Uygulamanın tek resmî web sitesi:
https://byebyedpi.xyz
---
### Özellikler
* Cihaz açıldığında hizmeti otomatik başlatma
* Komut satırı parametre listelerini kaydetme
* Android TV/BOX ile geliştirilmiş uyumluluk
* Uygulama bazında bölünmüş tünelleme
* Ayarları içe ve dışa aktarma
### Kullanım
* Otomatik başlatmanın çalışması için ayarlardaki ilgili seçeneği etkinleştirin.
* İzin isteğini kabul etmek için VPN bağlantısını en az bir kez başlatmanız önerilir.
* Bundan sonra uygulama, cihaz açıldığında ayarlara bağlı olarak hizmeti VPN veya Proxy modunda otomatik olarak başlatır.
* Topluluk tarafından hazırlanan kapsamlı kılavuz: [ByeByeDPI-Manual](https://byebyedpi.xyz)
### Derleme
1. Depoyu alt modüllerle birlikte klonlayın:
```bash
git clone --recurse-submodules
```
2. Derleme betiğini deponun kök dizininden çalıştırın:
```bash
./gradlew assembleRelease
```
3. APK dosyası şu dizinde bulunacaktır:
`app/build/outputs/apk/release/`
> Not: hev_socks5_tunnel Windows üzerinde derlenemez. WSL kullanmanız gerekir.
### İmza Özeti
SHA-256:
`77:45:10:75:AC:EA:40:64:06:47:5D:74:D4:59:88:3A:49:A6:40:51:FA:F3:2E:42:F7:18:F3:F9:77:7A:8D:FB`
### Bağımlılıklar
* [ByeDPI](https://github.com/hufrea/byedpi)
* [hev-socks5-tunnel](https://github.com/heiher/hev-socks5-tunnel)
### Teşekkürler
* [hufrea](https://github.com/hufrea) — [ByeDPI](https://github.com/hufrea/byedpi) için
* [dovecoteescapee](https://github.com/dovecoteescapee) — [ByeDPIAndroid](https://github.com/dovecoteescapee/ByeDPIAndroid) uygulamasının ilk sürümü için
+49 -31
View File
@@ -1,47 +1,65 @@
# ByeByeDPI Android
<div style="text-align: center;">
<img alt="Логотип ByeDPI" src="https://github.com/romanvht/ByeDPIAndroid/raw/master/.github/images/logo.svg" width="100%" height="200px">
<div align="center">
<p>
<img src="https://github.com/romanvht/ByeDPIAndroid/raw/master/.github/images/app.svg" alt="Логотип ByeDPI" width="200" />
</p>
<h1>ByeByeDPI Android</h1>
<p>
Русский |
<a href="README-en.md">English</a> |
<a href="README-tr.md">Türkçe</a>
</p>
<p>
<a href="https://github.com/romanvht/ByeByeDPI/releases/latest"><img src="https://img.shields.io/github/v/release/romanvht/ByeByeDPI" alt="Latest Release" /></a>
<a href="https://github.com/romanvht/ByeByeDPI/releases"><img src="https://img.shields.io/github/downloads/romanvht/ByeByeDPI/total" alt="Downloads" /></a>
<a href="https://github.com/romanvht/ByeByeDPI/blob/master/LICENSE"><img src="https://img.shields.io/github/license/romanvht/ByeByeDPI" alt="License" /></a>
<a href="https://github.com/romanvht/ByeByeDPI"><img src="https://img.shields.io/github/languages/code-size/romanvht/ByeByeDPI" alt="GitHub code size in bytes"/></a>
</p>
</div>
---
Приложение для Android, которое локально запускает ByeDPI и перенаправляет весь трафик через него.
Приложение для Android, которое локально запускает ByeDPI и перенаправляет весь TCP трафик через него.
Для стабильной работы может потребоваться изменить настройки. Подробнее о различных настройках можно прочитать в [документации ByeDPI](https://github.com/hufrea/byedpi/blob/v0.13/README.md).
Для стабильной работы может потребоваться изменить настройки. Подробнее о различных настройках можно прочитать в [документации ByeDPI](https://github.com/hufrea/byedpi/blob/main/README.md).
Приложение не является VPN. Оно использует VPN-режим на Android для перенаправления трафика, но не передает ничего на удаленный сервер. Оно не шифрует трафик и не скрывает ваш IP-адрес.
Приложения является форком [ByeDPIAndroid](https://github.com/dovecoteescapee/ByeDPIAndroid)
У приложения есть единственный официальный сайт -> https://byebyedpi.xyz
---
### Возможности
* Поддержка фильтра приложений для режима VPN (рездельное туннелирование через whitelist/blacklist)
* Автозапуск VPN/Proxy при старте устройства
* Автоподключение к VPN/Proxy при запуске приложения
* Сохранение списков параметров командной строки с возможностью переключения
* Улучшена совместимость с Android TV/BOX, исправлены некоторые ошибки оригинала
<div style="text-align: center;">
<img alt="Скриншот-1" src="https://github.com/romanvht/ByeDPIAndroid/raw/master/.github/images/settings_screen_2.png">
<img alt="Скриншот-2" src="https://github.com/romanvht/ByeDPIAndroid/raw/master/.github/images/apps_screen_2.png">
</div>
* Автозапуск сервиса при старте устройства
* Сохранение списков параметров командной строки
* Улучшена совместимость с Android TV/BOX
* Раздельное туннелирование приложений
* Импорт/экспорт настроек
### Использование
* Для работы автозапуска активируйте пункт в настройках.
* Рекомендуется подключится один раз к VPN, чтобы принять запрос.
* После этого, при загрузке устройства, приложение автоматически запустит сервис в зависимости от настроек (VPN/Proxy)
* Если у вас Android TV/BOX, и при подключении пропадает соединение по Ethernet, активируйте режим белого списка и укажите нужные приложения, которые должны работать через VPN (например, YouTube)
* Инструкция для [SberBOX](sbox.md)
* Комплексная инструкция от комьюнити [ByeByeDPI-Manual](https://byebyedpi.xyz)
### Как использовать ByeDPI вместе с AdGuard?
* Запустите ByeDPI в режиме прокси.
* Добавьте ByeDPI в исключения AdGuard на вкладке "Управление приложениями".
* В настройках AdGuard укажите прокси:
```plaintext
Тип прокси: SOCKS5
Хост: 127.0.0.1
Порт: 1080 (по умолчанию)
```
### Сборка
1. Клонируйте репозиторий с сабмодулями:
```bash
git clone --recurse-submodules
```
2. Запустите скрипт сборки из корня репозитория:
```bash
./gradlew assembleRelease
```
3. APK будет в `app/build/outputs/apk/release/`
> P.S.: hev_socks5_tunnel не соберется под Windows, вам нужно будет использовать WSL
### Хеш подписи
SHA-256:
`77:45:10:75:AC:EA:40:64:06:47:5D:74:D4:59:88:3A:49:A6:40:51:FA:F3:2E:42:F7:18:F3:F9:77:7A:8D:FB`
### Зависимости
- [ByeDPI](https://github.com/hufrea/byedpi)
- [hev-socks5-tunnel](https://github.com/heiher/hev-socks5-tunnel)
### Благодарность
- [hufrea](https://github.com/hufrea) - за [ByeDPI](https://github.com/hufrea/byedpi)
- [dovecoteescapee](https://github.com/dovecoteescapee) - за изначальную реализацию [ByeDPIAndroid](https://github.com/dovecoteescapee/ByeDPIAndroid)
+38 -28
View File
@@ -1,62 +1,65 @@
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
plugins {
id("com.android.application")
id("org.jetbrains.kotlin.android")
}
val abis = setOf("armeabi-v7a", "arm64-v8a", "x86", "x86_64")
android {
namespace = "io.github.dovecoteescapee.byedpi"
compileSdk = 34
namespace = "io.github.romanvht.byedpi"
compileSdk = 36
defaultConfig {
applicationId = "io.github.romanvht.byedpi"
minSdk = 21
//noinspection OldTargetApi
targetSdk = 34
versionCode = 10
versionName = "1.4.3"
versionCode = 1770
versionName = "1.7.7"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
ndk {
abiFilters.add("armeabi-v7a")
abiFilters.add("arm64-v8a")
abiFilters.add("x86")
abiFilters.add("x86_64")
abiFilters.addAll(abis)
}
}
buildFeatures {
buildConfig = true
viewBinding = true
}
buildTypes {
release {
buildConfigField("String", "VERSION_NAME", "\"${defaultConfig.versionName}\"")
isMinifyEnabled = false
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
signingConfig = signingConfigs.getByName("debug")
isMinifyEnabled = true
isShrinkResources = true
}
debug {
buildConfigField("String", "VERSION_NAME", "\"${defaultConfig.versionName}-debug\"")
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = "1.8"
kotlin {
compilerOptions {
jvmTarget.set(JvmTarget.JVM_1_8)
}
}
externalNativeBuild {
cmake {
path = file("src/main/cpp/CMakeLists.txt")
version = "3.22.1"
}
}
buildFeatures {
viewBinding = true
}
// https://android.izzysoft.de/articles/named/iod-scan-apkchecks?lang=en#blobs
dependenciesInfo {
@@ -65,22 +68,29 @@ android {
// Disables dependency metadata when building Android App Bundles.
includeInBundle = false
}
splits {
abi {
isEnable = true
reset()
include(*abis.toTypedArray())
isUniversalApk = true
}
}
}
dependencies {
//noinspection GradleDependency
implementation("androidx.fragment:fragment-ktx:1.8.4")
implementation("androidx.core:core-ktx:1.13.1")
implementation("androidx.appcompat:appcompat:1.7.0")
implementation("androidx.fragment:fragment-ktx:1.8.9")
implementation("androidx.core:core-ktx:1.17.0")
implementation("androidx.appcompat:appcompat:1.7.1")
implementation("androidx.preference:preference-ktx:1.2.1")
implementation("com.takisoft.preferencex:preferencex:1.1.0")
implementation("com.google.android.material:material:1.12.0")
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.8.6")
implementation("androidx.lifecycle:lifecycle-service:2.8.6")
implementation("com.google.code.gson:gson:2.8.9")
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.9.4")
implementation("androidx.lifecycle:lifecycle-service:2.9.4")
implementation("com.google.android.material:material:1.13.0")
implementation("com.google.code.gson:gson:2.13.2")
testImplementation("junit:junit:4.13.2")
androidTestImplementation("androidx.test.ext:junit:1.2.1")
androidTestImplementation("androidx.test.espresso:espresso-core:3.6.1")
androidTestImplementation("androidx.test.ext:junit:1.3.0")
androidTestImplementation("androidx.test.espresso:espresso-core:3.7.0")
}
tasks.register<Exec>("runNdkBuild") {
@@ -104,4 +114,4 @@ tasks.register<Exec>("runNdkBuild") {
tasks.preBuild {
dependsOn("runNdkBuild")
}
}
+31 -18
View File
@@ -1,21 +1,34 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# Keep JNI methods
-keepclasseswithmembernames class * {
native <methods>;
}
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
-keep class io.github.romanvht.byedpi.core.ByeDpiProxy { *; }
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
-keep,allowoptimization class io.github.romanvht.byedpi.core.TProxyService { *; }
-keep,allowoptimization class io.github.romanvht.byedpi.activities.** { *; }
-keep,allowoptimization class io.github.romanvht.byedpi.services.** { *; }
-keep,allowoptimization class io.github.romanvht.byedpi.receiver.** { *; }
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
-keep class io.github.romanvht.byedpi.fragments.** {
<init>();
}
-keep,allowoptimization class io.github.romanvht.byedpi.data.** {
<fields>;
}
-keepattributes Signature
-keepattributes *Annotation*
-repackageclasses 'ru.romanvht'
-renamesourcefileattribute ''
-keepattributes SourceFile,InnerClasses,EnclosingMethod,Signature,RuntimeVisibleAnnotations,*Annotation*,*Parcelable*
-allowaccessmodification
-overloadaggressively
-optimizationpasses 5
-verbose
-dontusemixedcaseclassnames
-adaptclassstrings
-adaptresourcefilecontents **.xml,**.json
-adaptresourcefilenames **.xml,**.json
@@ -1,4 +1,4 @@
package io.github.dovecoteescapee.byedpi
package io.github.romanvht.byedpi
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.ext.junit.runners.AndroidJUnit4
@@ -19,6 +19,6 @@ class ExampleInstrumentedTest {
fun useAppContext() {
// Context of the app under test.
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
assertEquals("io.github.dovecoteescapee.byedpi", appContext.packageName)
assertEquals("io.github.romanvht.byedpi", appContext.packageName)
}
}
+28 -8
View File
@@ -6,8 +6,15 @@
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SYSTEM_EXEMPTED" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="android.permission.QUICKBOOT_POWERON" />
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE"
tools:ignore="ScopedStorage" />
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES"
tools:ignore="QueryAllPackagesPermission" />
<uses-feature android:name="android.software.leanback"
@@ -26,7 +33,9 @@
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/Theme.ByeDPI"
android:requestLegacyExternalStorage="true"
tools:targetApi="34">
<activity
android:name=".activities.MainActivity"
android:exported="true">
@@ -39,6 +48,16 @@
</intent-filter>
</activity>
<activity
android:name=".activities.ToggleActivity"
android:theme="@android:style/Theme.NoDisplay"
android:exported="true"
android:excludeFromRecents="true"
android:noHistory="true"
android:taskAffinity=""
android:launchMode="singleTask">
</activity>
<activity
android:name=".activities.SettingsActivity"
android:label="@string/title_settings"
@@ -46,6 +65,7 @@
<activity
android:name=".activities.TestActivity"
android:configChanges="orientation|screenSize|keyboardHidden"
android:label="@string/title_test"
android:exported="false"/>
@@ -56,25 +76,22 @@
<service android:name=".services.ByeDpiVpnService"
android:permission="android.permission.BIND_VPN_SERVICE"
android:foregroundServiceType="specialUse"
android:foregroundServiceType="systemExempted"
android:exported="false">
<intent-filter>
<action android:name="android.net.VpnService"/>
</intent-filter>
<meta-data
android:name="android.net.VpnService.SUPPORTS_ALWAYS_ON"
android:value="false" />
android:value="true" />
<property
android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE"
android:name="android.app.PROPERTY_SYSTEM_EXEMPTED_FGS_SUBTYPE"
android:value="vpn" />
</service>
<service android:name=".services.ByeDpiProxyService"
android:foregroundServiceType="specialUse"
android:foregroundServiceType="dataSync"
android:exported="false">
<property
android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE"
android:value="proxy" />
</service>
<service
@@ -83,6 +100,9 @@
android:label="@string/app_name"
android:exported="true"
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE">
<meta-data
android:name="android.service.quicksettings.TOGGLEABLE_TILE"
android:value="true" />
<intent-filter>
<action
android:name="android.service.quicksettings.action.QS_TILE"/>
@@ -103,4 +123,4 @@
</receiver>
</application>
</manifest>
</manifest>
@@ -0,0 +1,4 @@
cloudflare.net
cloudflare.com
cloudflarecn.net
cloudflare-ech.com
-52
View File
@@ -1,52 +0,0 @@
-q1+s -s25+s -o5+s -f-1 -S
-q1+s -s29+s -o5+s -f-1 -S
-Ku -a1 -An -Kt,h -d7 -s2 -An
-s1 -q1 -Y -At -f-1 -r1+s -As
-q1+s -s25+s -o5+s -f-1 -S -As
-q1+s -s29+s -o5+s -f-1 -S -As
-Ku -a1 -An -Kt,h -q1 -r25+s -An
-n vk.com -q 1+s -O 1 -s 25+s -t 5
-n vk.com -q 1+s -O 1 -s 29+s -t 5
-s0 -o1 -Ar -o1 -At -f-1 -r1+s -As
-s1 -o1 -Ar -o1 -At -f-1 -r1+s -As
-s1 -q1 -a2 -Art -f-1 -r1+s -a3 -Asn
-s1 -d1 -o1 -Ar -o3 -At -f-1 -r1+s -As
-Ku -a5 -An -Kt,h -s1 -f-1 -S -b661 -An
-s1 -q1 -Y -a2 -Art -f-1 -r1+s -a3 -Asn
--split 2 --disorder 3 --fake -1 --ttl 5
-n www.google.com -q1+s -O1 -s25+s -t5
-n www.google.com -q1+s -O1 -s29+s -t5
-n www.google.com -q2+s -O1 -s25+s -t5
-n www.google.com -q2+s -O1 -s29+s -t5
-s1 -q1 -Y -At -T5 -b1000 -S -f-1 -r1+s -As
-s0 -o1 -d1 -r1+s -Ar -o1 -At -f-1 -r1+s -As
--disoob 3+sni --split -1+host --tlsrec 1+sni
-Ku -a1 -An -Kt,h -q1+s -s29+s -o5+s -f-1 -S -As
--split 2 --fake 1 --ttl 5 --tls-sni=www.google.com
-Ku -a1 -An -Kt,h -s1 -d3+s -Mh,d -At -r1+s -r3+s -An
-Ku -a1 -An -Kt,h -s1 -q1 -a2 -Art -f-1 -r1+s -a3 -An
-Ku -a1 -An -Kt,h -s1 -q2 -a2 -Art -f-1 -r2+s -a3 -An
-Ku -a1 -An -Kt,h -s1 -q1 -Art -f-1 --md5sig -r1+s -An
-Ku -a3 -An -Kt,h -s0 -o1 -Ar -o1 -At -f-1 -r1+s -As -An
-Ku -a5 -An -Kt,h -o0 -d1 -r1+s -t10 -b1500 -S -s0+s -d3+s -An
-Ku -a1 -An -Kt,h -d1 -s4 -d8 -s1+s -d5+s -s10+s -d20+s -An
-s1 -q1 -Y -Ar -s5 -o1+s -At -f-1 -r1+s -As -s1 -o1+s -s-1 -An
--split 1 --disorder 3+s --mod-http=h,d --auto=torst --tlsrec 1+s
-Ku -a3 -An -Kt,h -s0 -o1 -d1 -r1+s -Ar -o1 -At -f-1 -r1+s -As -An
-Ku -a3 -An -Kt,h -q1+s -s29+s -s30+s -s14+s -o5+s -f-1 --md5sig -Asn
-s1 -q1 -Y -Ar -s5 -o25000+s -At -f-1 -r1+s -As -s1 -o1+s -s-1 -An -b+500
-Ku -a1 -An -Kt,h -s1 -q1 -Ar -s5 -o1+s -At -f-1 -r1+s -As -s1 -o1+s -s-1 -An
-Ku -a3 -An -Kt,h -d1 -s0+s -s3+s -s6+s -s9+s -s12+s -s15+s -s20+s -s30+s -An
--split 1 --disorder 3+s --mod-http=h,d --auto=torst --tlsrec 1+s --tlsrec 3+sni
-Ku -a3 -An -Kt,h -s1 -q1 -Y -Ar -s5 -o1+s -At -f-1 -r1+s -As -s1 -o1+s -s-1 -An
-Ku -a3 -An -Kt,h -d1 -s0+s -d3+s -s6+s -d9+s -s12+s -d15+s -s20+s -d25+s -s30+s -An
-Ku -a3 -An -Kt,h -s0 -o1 -Ar -o1 -At -f-1 -r1+s -As -b+500 --tls-sni=www.google.com -An
-Ku -a3 -An -Kt,h -d1 -s0+s -d1+s -s3+s -d6+s -s12+s -d14+s -s20+s -d24+s -s30+s -d34+s -An
-Ku -a3 -An -Kt,h -d1 -s0+s -d3+s -s6+s -d9+s -s12+s -d15+s -s20+s -d25+s -s30+s -d35+s -An
-Ku -a3 -An -Kt,h -s1 -q1 -Ar -s5 -o25000+s -At -f-1 -r1+s -As -s1 -o1+s -s-1 -An -b+500 -An
-Ku -a3 -An -Kt,h -s1 -q1 -Y -At -T5 -b1000 -S -f-1 -r1+s -As -n www.google.com -d1+s -O1 -s29+s -t 5 -An
-Ku -a3 -An -Kt,h -s0 -o1 -d1 -r1+s -Ar -o1 -At -f-1 -r1+s -As -b+500 --tls-sni=www.google.com --ttl 3 -An
-Ku -a3 -An -Kt,h -d1 -s1 -q1 -Y -Ar -s5 -o1+s -d3+s -s6+s -d9+s -s12+s -d15+s -s20+s -d25+s -s30+s -d35+s -An
-Ku -l':\x16\x03\x01\x02\x87\x01\x00\x02\x83\x03\x03\x5f\x15\x63\xcb\x06' -a1 -An -s1 -q1 -Y -At -f-1 -r1+s -As
-Ku -a3 -An -Kt,h -s1 -q1 -Y -Ar -s5 -o1+s -At -f-1 -r1+s -As -s1 -o1 +s -s-1 -An -b+500 --tls-sni=www.google.com --ttl 3
-Ku -a1 -An -d1 -s0+s -d3+s -s6+s -d9+s -s12+s -d15+s -s20+s -d25+s -s30+s -d35+s -At,r,s -s1 -q1 -At,r,s -s5 -o25000+s -At,r,s -o1 -d1 -r1+s -t10 -b1500 -s0+s -d3+s -At,r,s -f-1 -r1+s -At,r,s -s1 -o1+s -s-1
@@ -0,0 +1,21 @@
dis.gd
discord.co
discord.gg
discord.app
discord.com
discord.dev
discord.new
discord.gift
discord.gifts
discord.media
discord.store
discord.design
discordapp.com
discordcdn.com
discordsez.com
discordsays.com
discordmerch.com
discordpartygames.com
discordactivities.com
stable.dl2.discordapp.net
discord-attachments-uploads-prd.storage.googleapis.com
@@ -0,0 +1,6 @@
rutracker.org
nyaa.si
rutor.org
nnmclub.to
speedtest.net
ookla.com
@@ -1,10 +1,3 @@
youtube.com
manifest.googlevideo.com
i.ytimg.com
yt3.ggpht.com
yt4.ggpht.com
signaler-pa.youtube.com
jnn-pa.googleapis.com
rr1---sn-4axm-n8vs.googlevideo.com
rr1---sn-gvnuxaxjvh-o8ge.googlevideo.com
rr1---sn-ug5onuxaxjvh-p3ul.googlevideo.com
@@ -22,4 +15,5 @@ rr2---sn-q4fl6ndl.googlevideo.com
rr5---sn-gvnuxaxjvh-n8vk.googlevideo.com
rr4---sn-jvhnu5g-c35d.googlevideo.com
rr1---sn-q4fl6n6y.googlevideo.com
rr2---sn-hgn7ynek.googlevideo.com
rr2---sn-hgn7ynek.googlevideo.com
rr1---sn-xguxaxjvh-gufl.googlevideo.com
@@ -0,0 +1,16 @@
snapchat.com
snap.com
linkedin.com
facebook.com
fb.com
fb.me
fbcdn.net
messenger.com
meta.com
instagram.com
static.cdninstagram.com
proton.me
medium.com
x.com
twitter.com
soundcloud.com
@@ -0,0 +1,60 @@
-f-200 -Qr -s3:5+sm -a1 -As -d1 -s4+sm -s8+sh -f-300 -d6+sh -a1 -At,r,s -o2 -f-30 -As -r5 -Mh -r6+sh -f-250 -s2:7+s -s3:6+sm -a1 -At,r,s -s3:5+sm -s6+s -s7:9+s -q30+sm -a1
-d1 -d3+s -s6+s -d9+s -s12+s -d15+s -s20+s -d25+s -s30+s -d35+s -r1+s -S -a1 -As -d1 -d3+s -s6+s -d9+s -s12+s -d15+s -s20+s -d25+s -s30+s -d35+s -S -a1
-q2 -s2 -s3+s -r3 -s4 -r4 -s5+s -r5+s -s6 -s7+s -r8 -s9+s -Qr -Mh,d,r -a1 -At,r -s2+s -r2 -d2 -s3 -r3 -r4 -s4 -d5+s -r5 -d6 -s7+s -d7 -a1
-o1 -d1 -a1 -At,r,s -s1 -d1 -s5+s -s10+s -s15+s -s20+s -r1+s -S -a1 -As -s1 -d1 -s5+s -s10+s -s15+s -s20+s -S -a1
-n {sni} -Qr -f-204 -s1:5+sm -a1 -As -d1 -s3+s -s5+s -q7 -a1 -As -o2 -f-43 -a1 -As -r5 -Mh -s1:5+s -s3:7+sm -a1
-n {sni} -Qr -f-205 -a1 -As -s1:3+sm -a1 -As -s5:8+sm -a1 -As -d3 -q7 -o2 -f-43 -f-85 -f-165 -r5 -Mh -a1
-d1+s -s50+s -a1 -As -f20 -r2+s -a1 -At -d2 -s1+s -s5+s -s10+s -s15+s -s25+s -s35+s -s50+s -s60+s -a1
-o1 -a1 -At,r,s -f-1 -a1 -At,r,s -d1:11+sm -S -a1 -At,r,s -n {sni} -Qr -f1 -d1:11+sm -s1:11+sm -S -a1
-d1 -s1 -q1 -a1 -Ar -s5 -o1+s -d3+s -s6+s -d9+s -s12+s -d15+s -s20+s -d25+s -s30+s -d35+s -a1
-f1+nme -t6 -a1 -As -n {sni} -Qr -s1:6+sm -a1 -As -s5:12+sm -a1 -As -d3 -q7 -r6 -Mh -a1
-d1 -s1+s -d3+s -s6+s -d9+s -s12+s -d15+s -s20+s -d25+s -s30+s -d35+s -a1
-d1 -s1+s -d1+s -s3+s -d6+s -s12+s -d14+s -s20+s -d24+s -s30+s -a1
-o1 -a1 -At,r,s -f-1 -a1 -Ar,s -o1 -a1 -At -r1+s -f-1 -t6 -a1
-d1 -s1+s -s3+s -s6+s -s9+s -s12+s -s15+s -s20+s -s30+s -a1
-d1 -d3+s -s6+s -d6+s -s7+s -d8+s -s10+s -a1 -t12 -At,s -r3
-f1 -t5 -n {sni} -q3+h -Qr -f2 -q1 -r1+s -t15 -q1 -o2 -a1
-n {sni} -d2:5:2+h -f-3 -r2+sm -o2 -o50+s -r2+s -f-4 -a1
-f-1 -Qr -s1+sm -d3+s -s5+sm -o2 -a1 -As -r1+s -d8+s -a1
-r-1+s -o20+sm -s3:7+sm -d5:3+sm -f300+s -Qr -f-1 -a1
-o2 -O4 -s1 -q1 -a1 -Ar -s5 -o1+s -f1+s -r20+s -a1
-o1 -r-5+se -a1 -At,r,s -d1 -n {sni} -Qr -f-1 -a1
--fake -1 --ttl 8 --split 1+s --disorder 3+s -a1
-n {sni} -Qr -f6+nr -d2 -d11 -f9+hm -o3 -t7 -a1
-r5+s -s25+s -a1 -At,r,s -s50 -r5+s -s50+s -a1
-d1 -d3+s -s6+s -d9+s -s20+s -d25+s -s30+s -a1
-d9+s -q20+s -s25+s -t5 -a1 -At,r,s -r1+h -a1
-q1+s -s29+s -s30+s -s14+s -o5+s -f-1 -S -a1
-d1 -s1+s -r1+s -e1 -m1 -o1+s -f-1 -t2 -a1
-d1 -o1 -a1 -Ar -o1 -a1 -At -f-1 -r1+s -a1
-d1 -s4 -d8 -s1+s -d5+s -s10+s -d20+s -a1
-f-1 -n {sni} -Qr -s2+s -r3 -o20 -t4 -a1
-n {sni} -Qr -d5+sm -f3+sm -o2 -t4 -a1
-o1 -a1 -Ar -q1 -a1 -At -f-1 -r1+s -a1
-q1 -a1 -Ar -o1 -a1 -At -f-1 -r1+s -a1
-s4+sn -r9+s -Qr -n {sni} -S -a1
-o1 -d1 -r1+s -S -s1+s -d3+s -a1
-q1+s -s29+s -o5+s -f-1 -S -a1
-n {sni} -Qr -m2 -f-1 -d7 -a1
-d1 -s1+s -r1+s -f-1 -t8 -a1
-o1 -a1 -An -f1+nme -t6 -a1
-n {sni} -Qr -f-1 -r1+s -a1
-n {sni} -Qr -d1:3 -f-1 -a1
-s1 -d3+s -a1 -At -r1+s -a1
-f-1 -t8 -n {sni} -s1+s -a1
-n {sni} -Qr -d1 -f-1 -a1
-f64+se -n {sni} -t5 -a1
-o1 -a1 -At,r,s -d1 -a1
-d1+s -o2 -s5 -r5 -a1
-r8 -o2 -s7 -q4+s -a1
-o1 -f-1 -r-5+se -a1
-d6+s -q4+hm -o2 -a1
-s5+s -s35+s -m4 -a1
-f-1+sm -t7 -m2 -a1
-o1 -r-5+se -a1
-o1+s -d3+s -a1
-o1 -s4 -s6 -a1
-q1 -r25+s -a1
-d1 -s3+s -a1
-o3 -d7 -a1
-d7 -s2 -a1
@@ -0,0 +1,52 @@
telegram.org
core.telegram.org
web.telegram.org
webk.telegram.org
my.telegram.org
translations.telegram.org
instantview.telegram.org
blog.telegram.org
comments.telegram.org
verify.telegram.org
login.telegram.org
auth.telegram.org
api.telegram.org
promo.telegram.org
desktop.telegram.org
macos.telegram.org
ios.telegram.org
android.telegram.org
reactions.telegram.org
claims.telegram.org
x.telegram.org
help.telegram.org
docs.telegram.org
schema.telegram.org
dev.telegram.org
contest.telegram.org
premium.telegram.org
settings.telegram.org
qr.telegram.org
stickers.telegram.org
emoji.telegram.org
themes.telegram.org
donate.telegram.org
fragment.telegram.org
ton.telegram.org
wallet.telegram.org
pay.telegram.org
telegram.me
telegram.dog
telegra.ph
telesco.pe
web.telegram.me
zws1.web.telegram.org
zws2.web.telegram.org
zws1.web.telegram.me
zws2.web.telegram.me
venus.web.telegram.org
pluto.web.telegram.org
aurora.web.telegram.org
vesta.web.telegram.org
voice.telegram.org
cdn.telegram.org
@@ -0,0 +1,8 @@
roblox.com
wattpad.com
pastebin.com
4shared.com
wikileaks.org
bitly.com
cutt.ly
t2m.io
@@ -0,0 +1,13 @@
youtu.be
youtube.com
i.ytimg.com
i9.ytimg.com
yt3.ggpht.com
yt4.ggpht.com
googleapis.com
jnn-pa.googleapis.com
googleusercontent.com
signaler-pa.youtube.com
youtubei.googleapis.com
manifest.googlevideo.com
yt3.googleusercontent.com
+3 -2
View File
@@ -5,10 +5,11 @@ project(byedpi_native)
file(GLOB BYE_DPI_SRC byedpi/*.c)
list(REMOVE_ITEM BYE_DPI_SRC ${CMAKE_CURRENT_SOURCE_DIR}/byedpi/win_service.c)
add_library(byedpi SHARED ${BYE_DPI_SRC} native-lib.c utils.c)
add_library(byedpi SHARED ${BYE_DPI_SRC} native-lib.c)
target_include_directories(byedpi PRIVATE byedpi)
target_compile_options(byedpi PRIVATE -std=c99 -O2 -D_XOPEN_SOURCE=500)
target_compile_options(byedpi PRIVATE -std=c99 -O2 -Wall -Wno-unused -Wextra -Wno-unused-parameter -pedantic)
target_compile_definitions(byedpi PRIVATE ANDROID_APP)
target_link_libraries(byedpi PRIVATE android log)
set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} -Wl,-z,max-page-size=16384")
+2 -24
View File
@@ -1,24 +1,2 @@
extern char *oob_char;
extern int NOT_EXIT;
union sockaddr_u;
int get_default_ttl();
int get_addr(const char *str, union sockaddr_u *addr);
void *add(void **root, int *n, size_t ss);
void clear_params(void);
char *ftob(const char *str, ssize_t *sl);
char *data_from_str(const char *str, ssize_t *size);
size_t parse_cform(char *buffer, size_t blen, const char *str, size_t slen);
struct mphdr *parse_hosts(char *buffer, size_t size);
struct mphdr *parse_ipset(char *buffer, size_t size);
int parse_offset(struct part *part, const char *str);
void clear_params(char *line, char **argv);
int main(int argc, char **argv);
+90 -275
View File
@@ -1,296 +1,111 @@
#include <string.h>
#include <netdb.h>
#include <jni.h>
#include <android/log.h>
#include <getopt.h>
#include <signal.h>
#include <setjmp.h>
#include <stdlib.h>
#include "byedpi/error.h"
#include "byedpi/proxy.h"
#include "byedpi/params.h"
#include "byedpi/packets.h"
#include "main.h"
#include "utils.h"
const enum demode DESYNC_METHODS[] = {
DESYNC_NONE,
DESYNC_SPLIT,
DESYNC_DISORDER,
DESYNC_FAKE,
DESYNC_OOB,
DESYNC_DISOOB,
extern int server_fd;
static int g_proxy_running = 0;
struct params default_params = {
.await_int = 10,
.ipv6 = 1,
.resolve = 1,
.udp = 1,
.max_open = 512,
.bfsize = 16384,
.baddr = {
.in6 = { .sin6_family = AF_INET6 }
},
.laddr = {
.in = { .sin_family = AF_INET }
},
.debug = 0
};
enum hosts_mode {
HOSTS_DISABLE,
HOSTS_BLACKLIST,
HOSTS_WHITELIST,
};
JNIEXPORT jint JNI_OnLoad(
__attribute__((unused)) JavaVM *vm,
__attribute__((unused)) void *reserved) {
default_params = params;
return JNI_VERSION_1_6;
void reset_params(void) {
clear_params(NULL, NULL);
params = default_params;
}
JNIEXPORT jint JNICALL
Java_io_github_dovecoteescapee_byedpi_core_ByeDpiProxy_jniCreateSocketWithCommandLine(
JNIEnv *env,
__attribute__((unused)) jobject thiz,
jobjectArray args) {
Java_io_github_romanvht_byedpi_core_ByeDpiProxy_jniStartProxy(JNIEnv *env, __attribute__((unused)) jobject thiz, jobjectArray args) {
if (g_proxy_running) {
LOG(LOG_S, "proxy already running");
return -1;
}
int argc = (*env)->GetArrayLength(env, args);
char *argv[argc];
char **argv = calloc(argc, sizeof(char *));
if (!argv) {
LOG(LOG_S, "failed to allocate memory for argv");
return -1;
}
for (int i = 0; i < argc; i++) {
jstring arg = (jstring) (*env)->GetObjectArrayElement(env, args, i);
if (!arg) {
argv[i] = NULL;
continue;
}
const char *arg_str = (*env)->GetStringUTFChars(env, arg, 0);
argv[i] = strdup(arg_str);
(*env)->ReleaseStringUTFChars(env, arg, arg_str);
argv[i] = arg_str ? strdup(arg_str) : NULL;
if (arg_str) (*env)->ReleaseStringUTFChars(env, arg, arg_str);
(*env)->DeleteLocalRef(env, arg);
}
int res = parse_args(argc, argv);
if (res < 0) {
uniperror("parse_args");
return -1;
}
int fd = listen_socket((union sockaddr_u *)&params.laddr);
if (fd < 0) {
uniperror("listen_socket");
return -1;
}
LOG(LOG_S, "listen_socket, fd: %d", fd);
return fd;
}
JNIEXPORT jint JNICALL
Java_io_github_dovecoteescapee_byedpi_core_ByeDpiProxy_jniCreateSocket(
JNIEnv *env,
__attribute__((unused)) jobject thiz,
jstring ip,
jint port,
jint max_connections,
jint buffer_size,
jint default_ttl,
jboolean custom_ttl,
jboolean no_domain,
jboolean desync_http,
jboolean desync_https,
jboolean desync_udp,
jint desync_method,
jint split_position,
jboolean split_at_host,
jint fake_ttl,
jstring fake_sni,
jbyte custom_oob_char,
jboolean host_mixed_case,
jboolean domain_mixed_case,
jboolean host_remove_spaces,
jboolean tls_record_split,
jint tls_record_split_position,
jboolean tls_record_split_at_sni,
jint hosts_mode,
jstring hosts,
jboolean tfo,
jint udp_fake_count,
jboolean drop_sack,
jint fake_offset) {
union sockaddr_u s;
const char *address = (*env)->GetStringUTFChars(env, ip, 0);
int res = get_addr(address, &s);
(*env)->ReleaseStringUTFChars(env, ip, address);
if (res < 0) {
uniperror("get_addr");
return -1;
}
s.in.sin_port = htons(port);
params.max_open = max_connections;
params.bfsize = buffer_size;
params.resolve = !no_domain;
params.tfo = tfo;
if (custom_ttl) {
params.def_ttl = default_ttl;
params.custom_ttl = 1;
}
if (!params.def_ttl) {
if ((params.def_ttl = get_default_ttl()) < 1) {
uniperror("get_default_ttl");
reset_params();
return -1;
}
}
if (hosts_mode == HOSTS_WHITELIST) {
struct desync_params *dp = add(
(void *) &params.dp,
&params.dp_count,
sizeof(struct desync_params)
);
if (!dp) {
uniperror("add");
reset_params();
return -1;
}
const char *str = (*env)->GetStringUTFChars(env, hosts, 0);
dp->file_ptr = data_from_str(str, &dp->file_size);
(*env)->ReleaseStringUTFChars(env, hosts, str);
dp->hosts = parse_hosts(dp->file_ptr, dp->file_size);
if (!dp->hosts) {
perror("parse_hosts");
clear_params();
return -1;
}
}
struct desync_params *dp = add(
(void *) &params.dp,
&params.dp_count,
sizeof(struct desync_params)
);
if (!dp) {
uniperror("add");
reset_params();
return -1;
}
if (hosts_mode == HOSTS_BLACKLIST) {
const char *str = (*env)->GetStringUTFChars(env, hosts, 0);
dp->file_ptr = data_from_str(str, &dp->file_size);
(*env)->ReleaseStringUTFChars(env, hosts, str);
dp->hosts = parse_hosts(dp->file_ptr, dp->file_size);
if (!dp->hosts) {
perror("parse_hosts");
clear_params();
return -1;
}
}
dp->ttl = fake_ttl;
dp->udp_fake_count = udp_fake_count;
dp->drop_sack = drop_sack;
dp->proto =
IS_HTTP * desync_http |
IS_HTTPS * desync_https |
IS_UDP * desync_udp;
dp->mod_http =
MH_HMIX * host_mixed_case |
MH_DMIX * domain_mixed_case |
MH_SPACE * host_remove_spaces;
struct part *part = add(
(void *) &dp->parts,
&dp->parts_n,
sizeof(struct part)
);
if (!part) {
uniperror("add");
reset_params();
return -1;
}
enum demode mode = DESYNC_METHODS[desync_method];
int offset_flag = dp->proto || desync_https ? OFFSET_SNI : OFFSET_HOST;
part->flag = split_at_host ? offset_flag : 0;
part->pos = split_position;
part->m = mode;
if (tls_record_split) {
struct part *tlsrec_part = add(
(void *) &dp->tlsrec,
&dp->tlsrec_n,
sizeof(struct part)
);
if (!tlsrec_part) {
uniperror("add");
reset_params();
return -1;
}
tlsrec_part->flag = tls_record_split_at_sni ? offset_flag : 0;
tlsrec_part->pos = tls_record_split_position;
}
if (mode == DESYNC_FAKE) {
dp->fake_offset = fake_offset;
const char *sni = (*env)->GetStringUTFChars(env, fake_sni, 0);
LOG(LOG_S, "fake_sni: %s", sni);
res = change_tls_sni(sni, fake_tls.data, fake_tls.size);
(*env)->ReleaseStringUTFChars(env, fake_sni, sni);
if (res) {
fprintf(stderr, "error chsni\n");
return -1;
}
}
if (mode == DESYNC_OOB) {
dp->oob_char[0] = custom_oob_char;
dp->oob_char[1] = 1;
}
if (dp->proto) {
dp = add((void *)&params.dp,
&params.dp_count, sizeof(struct desync_params));
if (!dp) {
uniperror("add");
clear_params();
return -1;
}
}
params.mempool = mem_pool(0, CMP_BYTES);
if (!params.mempool) {
uniperror("mem_pool");
clear_params();
return -1;
}
int fd = listen_socket(&s);
if (fd < 0) {
uniperror("listen_socket");
return -1;
}
LOG(LOG_S, "listen_socket, fd: %d", fd);
return fd;
}
JNIEXPORT jint JNICALL
Java_io_github_dovecoteescapee_byedpi_core_ByeDpiProxy_jniStartProxy(
__attribute__((unused)) JNIEnv *env,
__attribute__((unused)) jobject thiz,
jint fd) {
LOG(LOG_S, "start_proxy, fd: %d", fd);
NOT_EXIT = 1;
if (event_loop(fd) < 0) {
uniperror("event_loop");
return get_e();
}
return 0;
}
JNIEXPORT jint JNICALL
Java_io_github_dovecoteescapee_byedpi_core_ByeDpiProxy_jniStopProxy(
__attribute__((unused)) JNIEnv *env,
__attribute__((unused)) jobject thiz,
jint fd) {
LOG(LOG_S, "stop_proxy, fd: %d", fd);
int res = shutdown(fd, SHUT_RDWR);
LOG(LOG_S, "starting proxy with %d args", argc);
reset_params();
g_proxy_running = 1;
optind = 1;
if (res < 0) {
uniperror("shutdown");
return get_e();
int result = main(argc, argv);
LOG(LOG_S, "proxy return code %d", result);
g_proxy_running = 0;
for (int i = 0; i < argc; i++) free(argv[i]);
free(argv);
return result;
}
JNIEXPORT jint JNICALL
Java_io_github_romanvht_byedpi_core_ByeDpiProxy_jniStopProxy(__attribute__((unused)) JNIEnv *env, __attribute__((unused)) jobject thiz) {
LOG(LOG_S, "send shutdown to proxy");
if (!g_proxy_running) {
LOG(LOG_S, "proxy is not running");
return -1;
}
shutdown(server_fd, SHUT_RDWR);
g_proxy_running = 0;
return 0;
}
}
JNIEXPORT jint JNICALL
Java_io_github_romanvht_byedpi_core_ByeDpiProxy_jniForceClose(__attribute__((unused)) JNIEnv *env, __attribute__((unused)) jobject thiz) {
LOG(LOG_S, "closing server socket (fd: %d)", server_fd);
if (close(server_fd) == -1) {
LOG(LOG_S, "failed to close server socket (fd: %d)", server_fd);
return -1;
}
LOG(LOG_S, "proxy socket force close");
g_proxy_running = 0;
return 0;
}
-494
View File
@@ -1,494 +0,0 @@
#include <getopt.h>
#include <stdlib.h>
#include <string.h>
#include "byedpi/params.h"
#include "error.h"
#include "main.h"
#include "packets.h"
#include "utils.h"
struct params default_params;
void reset_params(void) {
clear_params();
params = default_params;
}
extern const struct option options[44];
int parse_args(int argc, char **argv)
{
int optc = sizeof(options)/sizeof(*options);
for (int i = 0, e = optc; i < e; i++)
optc += options[i].has_arg;
char opt[optc + 1];
opt[optc] = 0;
for (int i = 0, o = 0; o < optc; i++, o++) {
opt[o] = options[i].val;
for (int c = options[i].has_arg; c; c--) {
o++;
opt[o] = ':';
}
}
params.laddr.in.sin_port = htons(1080);
if (!ipv6_support()) {
params.baddr.sa.sa_family = AF_INET;
}
int rez;
int invalid = 0;
long val = 0;
char *end = 0;
bool all_limited = 1;
struct desync_params *dp = add((void *)&params.dp, &params.dp_count, sizeof(struct desync_params));
if (!dp) {
reset_params();
return -1;
}
optind = optreset = 1;
while (!invalid && (rez = getopt_long(
argc, argv, opt, options, 0)) != -1) {
switch (rez) {
case 'N':
params.resolve = 0;
break;
case 'X':
params.ipv6 = 0;
break;
case 'U':
params.udp = 0;
break;
case 'G':
params.http_connect = 1;
break;
#ifdef __linux__
case 'E':
params.transparent = 1;
break;
#endif
case 'i':
if (get_addr(optarg, &params.laddr) < 0)
invalid = 1;
break;
case 'p':
val = strtol(optarg, &end, 0);
if (val <= 0 || val > 0xffff || *end)
invalid = 1;
else
params.laddr.in.sin_port = htons(val);
break;
case 'I':
if (get_addr(optarg, &params.baddr) < 0)
invalid = 1;
break;
case 'b':
val = strtol(optarg, &end, 0);
if (val <= 0 || val > INT_MAX/4 || *end)
invalid = 1;
else
params.bfsize = val;
break;
case 'c':
val = strtol(optarg, &end, 0);
if (val <= 0 || val >= (0xffff/2) || *end)
invalid = 1;
else
params.max_open = val;
break;
case 'x': //
params.debug = strtol(optarg, 0, 0);
if (params.debug < 0)
invalid = 1;
break;
// desync options
case 'F':
params.tfo = 1;
break;
case 'L':
val = strtol(optarg, &end, 0);
if (val < 0 || val > 1 || *end)
invalid = 1;
else
params.auto_level = val;
break;
case 'A':
if (!(dp->hosts || dp->proto || dp->pf[0] || dp->detect || dp->ipset)) {
all_limited = 0;
}
dp = add((void *)&params.dp, &params.dp_count,
sizeof(struct desync_params));
if (!dp) {
reset_params();
return -1;
}
end = optarg;
while (end && !invalid) {
switch (*end) {
case 't':
dp->detect |= DETECT_TORST;
break;
case 'r':
dp->detect |= DETECT_HTTP_LOCAT;
break;
case 'a':
case 's':
dp->detect |= DETECT_TLS_ERR;
break;
case 'n':
break;
default:
invalid = 1;
continue;
}
end = strchr(end, ',');
if (end) end++;
}
if (dp->detect && params.auto_level == AUTO_NOBUFF) {
params.auto_level = AUTO_NOSAVE;
}
break;
case 'u':
val = strtol(optarg, &end, 0);
if (val <= 0 || *end)
invalid = 1;
else
params.cache_ttl = val;
break;
case 'T':;
#ifdef __linux__
float f = strtof(optarg, &end);
val = (long)(f * 1000);
#else
val = strtol(optarg, &end, 0);
#endif
if (val <= 0 || val > UINT_MAX || *end)
invalid = 1;
else
params.timeout = val;
break;
case 'K':
end = optarg;
while (end && !invalid) {
switch (*end) {
case 't':
dp->proto |= IS_TCP | IS_HTTPS;
break;
case 'h':
dp->proto |= IS_TCP | IS_HTTP;
break;
case 'u':
dp->proto |= IS_UDP;
break;
case 'i':
dp->proto |= IS_IPV4;
break;
default:
invalid = 1;
continue;
}
end = strchr(end, ',');
if (end) end++;
}
break;
case 'H':;
if (dp->file_ptr) {
continue;
}
dp->file_ptr = ftob(optarg, &dp->file_size);
if (!dp->file_ptr) {
uniperror("read/parse");
invalid = 1;
continue;
}
dp->hosts = parse_hosts(dp->file_ptr, dp->file_size);
if (!dp->hosts) {
uniperror("parse_hosts");
reset_params();
return -1;
}
break;
case 'j':;
if (dp->ipset) {
continue;
}
ssize_t size;
char *data = ftob(optarg, &size);
if (!data) {
uniperror("read/parse");
invalid = 1;
continue;
}
dp->ipset = parse_ipset(data, size);
if (!dp->ipset) {
uniperror("parse_ipset");
invalid = 1;
}
free(data);
break;
case 's':
case 'd':
case 'o':
case 'q':
case 'f':
;
struct part *part = add((void *)&dp->parts,
&dp->parts_n, sizeof(struct part));
if (!part) {
reset_params();
return -1;
}
if (parse_offset(part, optarg)) {
invalid = 1;
break;
}
switch (rez) {
case 's': part->m = DESYNC_SPLIT;
break;
case 'd': part->m = DESYNC_DISORDER;
break;
case 'o': part->m = DESYNC_OOB;
break;
case 'q': part->m = DESYNC_DISOOB;
break;
case 'f': part->m = DESYNC_FAKE;
}
break;
case 't':
val = strtol(optarg, &end, 0);
if (val <= 0 || val > 255 || *end)
invalid = 1;
else
dp->ttl = val;
break;
case 'k':
if (dp->ip_options) {
continue;
}
if (optarg)
dp->ip_options = ftob(optarg, &dp->ip_options_len);
else {
dp->ip_options = ip_option;
dp->ip_options_len = sizeof(ip_option);
}
if (!dp->ip_options) {
uniperror("read/parse");
invalid = 1;
}
break;
case 'S':
dp->md5sig = 1;
break;
case 'O':
val = strtol(optarg, &end, 0);
if (val <= 0 || *end)
invalid = 1;
else
dp->fake_offset = val;
break;
case 'n':
if (change_tls_sni(optarg, fake_tls.data, fake_tls.size)) {
perror("change_tls_sni");
reset_params();
return -1;
}
LOG(LOG_S, "sni: %s", optarg);
break;
case 'l':
if (dp->fake_data.data) {
continue;
}
dp->fake_data.data = ftob(optarg, &dp->fake_data.size);
if (!dp->fake_data.data) {
uniperror("read/parse");
invalid = 1;
}
break;
case 'e':
val = parse_cform(dp->oob_char, 1, optarg, strlen(optarg));
if (val != 1) {
invalid = 1;
}
else dp->oob_char[1] = 1;
break;
case 'M':
end = optarg;
while (end && !invalid) {
switch (*end) {
case 'r':
dp->mod_http |= MH_SPACE;
break;
case 'h':
dp->mod_http |= MH_HMIX;
break;
case 'd':
dp->mod_http |= MH_DMIX;
break;
default:
invalid = 1;
continue;
}
end = strchr(end, ',');
if (end) end++;
}
break;
case 'r':
part = add((void *)&dp->tlsrec,
&dp->tlsrec_n, sizeof(struct part));
if (!part) {
reset_params();
return -1;
}
if (parse_offset(part, optarg)
|| part->pos > 0xffff) {
invalid = 1;
break;
}
break;
case 'a':
val = strtol(optarg, &end, 0);
if (val < 0 || val > INT_MAX || *end)
invalid = 1;
else
dp->udp_fake_count = val;
break;
case 'V':
val = strtol(optarg, &end, 0);
if (val <= 0 || val > USHRT_MAX)
invalid = 1;
else {
dp->pf[0] = htons(val);
if (*end == '-') {
val = strtol(end + 1, &end, 0);
if (val <= 0 || val > USHRT_MAX)
invalid = 1;
}
if (*end)
invalid = 1;
else
dp->pf[1] = htons(val);
}
break;
case 'R':
val = strtol(optarg, &end, 0);
if (val <= 0 || val > INT_MAX)
invalid = 1;
else {
dp->rounds[0] = val;
if (*end == '-') {
val = strtol(end + 1, &end, 0);
if (val <= 0 || val > INT_MAX)
invalid = 1;
}
if (*end)
invalid = 1;
else
dp->rounds[1] = val;
}
break;
case 'g':
val = strtol(optarg, &end, 0);
if (val <= 0 || val > 255 || *end)
invalid = 1;
else {
params.def_ttl = val;
params.custom_ttl = 1;
}
break;
case 'Y':
dp->drop_sack = 1;
break;
case 'W':
params.wait_send = 0;
break;
#ifdef __linux__
case 'P':
params.protect_path = optarg;
break;
#endif
case 0:
break;
case '?':
reset_params();
return -1;
default:
LOG(LOG_S, "Unknown option: -%c", rez);
reset_params();
return -1;
}
}
if (invalid) {
LOG(LOG_S, "invalid value: -%c %s", rez, optarg);
reset_params();
return -1;
}
if (all_limited) {
dp = add((void *)&params.dp,
&params.dp_count, sizeof(struct desync_params));
if (!dp) {
reset_params();
return -1;
}
}
if (params.baddr.sa.sa_family != AF_INET6) {
params.ipv6 = 0;
}
if (!params.def_ttl) {
if ((params.def_ttl = get_default_ttl()) < 1) {
reset_params();
return -1;
}
}
params.mempool = mem_pool(0, CMP_BYTES);
if (!params.mempool) {
uniperror("mem_pool");
reset_params();
return -1;
}
srand((unsigned int)time(0));
return 0;
}
-5
View File
@@ -1,5 +0,0 @@
extern struct params default_params;
void reset_params(void);
int parse_args(int argc, char **argv);
bool ipv6_support(void);
@@ -1,277 +0,0 @@
package io.github.dovecoteescapee.byedpi.activities
import android.Manifest
import android.annotation.SuppressLint
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.content.pm.PackageManager
import android.net.VpnService
import android.os.Build
import android.os.Bundle
import android.util.Log
import android.view.Menu
import android.view.MenuItem
import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
import androidx.lifecycle.lifecycleScope
import io.github.dovecoteescapee.byedpi.R
import io.github.dovecoteescapee.byedpi.data.*
import io.github.dovecoteescapee.byedpi.fragments.MainSettingsFragment
import io.github.dovecoteescapee.byedpi.databinding.ActivityMainBinding
import io.github.dovecoteescapee.byedpi.services.ServiceManager
import io.github.dovecoteescapee.byedpi.services.appStatus
import io.github.dovecoteescapee.byedpi.utility.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import java.io.IOException
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
companion object {
private val TAG: String = MainActivity::class.java.simpleName
private fun collectLogs(): String? =
try {
Runtime.getRuntime()
.exec("logcat *:D -d")
.inputStream.bufferedReader()
.use { it.readText() }
} catch (e: Exception) {
Log.e(TAG, "Failed to collect logs", e)
null
}
}
private val vpnRegister =
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
if (it.resultCode == RESULT_OK) {
ServiceManager.start(this, Mode.VPN)
} else {
Toast.makeText(this, R.string.vpn_permission_denied, Toast.LENGTH_SHORT).show()
updateStatus()
}
}
private val logsRegister =
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
lifecycleScope.launch(Dispatchers.IO) {
val logs = collectLogs()
if (logs == null) {
Toast.makeText(
this@MainActivity,
R.string.logs_failed,
Toast.LENGTH_SHORT
).show()
} else {
val uri = it.data?.data ?: run {
Log.e(TAG, "No data in result")
return@launch
}
contentResolver.openOutputStream(uri)?.use {
try {
it.write(logs.toByteArray())
} catch (e: IOException) {
Log.e(TAG, "Failed to save logs", e)
}
} ?: run {
Log.e(TAG, "Failed to open output stream")
}
}
}
}
private val receiver = object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
Log.d(TAG, "Received intent: ${intent?.action}")
if (intent == null) {
Log.w(TAG, "Received null intent")
return
}
val senderOrd = intent.getIntExtra(SENDER, -1)
val sender = Sender.entries.getOrNull(senderOrd)
if (sender == null) {
Log.w(TAG, "Received intent with unknown sender: $senderOrd")
return
}
when (val action = intent.action) {
STARTED_BROADCAST,
STOPPED_BROADCAST -> updateStatus()
FAILED_BROADCAST -> {
Toast.makeText(
context,
getString(R.string.failed_to_start, sender.name),
Toast.LENGTH_SHORT,
).show()
updateStatus()
}
else -> Log.w(TAG, "Unknown action: $action")
}
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
val intentFilter = IntentFilter().apply {
addAction(STARTED_BROADCAST)
addAction(STOPPED_BROADCAST)
addAction(FAILED_BROADCAST)
}
@SuppressLint("UnspecifiedRegisterReceiverFlag")
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
registerReceiver(receiver, intentFilter, RECEIVER_EXPORTED)
} else {
registerReceiver(receiver, intentFilter)
}
binding.statusButton.setOnClickListener {
val (status, _) = appStatus
when (status) {
AppStatus.Halted -> start()
AppStatus.Running -> stop()
}
}
val lang = getPreferences().getString("language", "system")
MainSettingsFragment.setLang(lang ?: "system")
val theme = getPreferences().getString("app_theme", null)
MainSettingsFragment.setTheme(theme ?: "system")
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU &&
ContextCompat.checkSelfPermission(
this,
Manifest.permission.POST_NOTIFICATIONS
) != PackageManager.PERMISSION_GRANTED
) {
requestPermissions(arrayOf(Manifest.permission.POST_NOTIFICATIONS), 1)
}
if (getPreferences().getBoolean("auto_connect", false) && appStatus.first != AppStatus.Running) {
this.start()
}
}
override fun onResume() {
super.onResume()
updateStatus()
}
override fun onDestroy() {
super.onDestroy()
unregisterReceiver(receiver)
}
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
menuInflater.inflate(R.menu.menu_main, menu)
return true
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
val (status, _) = appStatus
return when (item.itemId) {
R.id.action_settings -> {
if (status == AppStatus.Halted) {
val intent = Intent(this, SettingsActivity::class.java)
startActivity(intent)
} else {
Toast.makeText(this, R.string.settings_unavailable, Toast.LENGTH_SHORT)
.show()
}
true
}
R.id.action_save_logs -> {
val intent =
Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
addCategory(Intent.CATEGORY_OPENABLE)
type = "text/plain"
putExtra(Intent.EXTRA_TITLE, "byedpi.log")
}
logsRegister.launch(intent)
true
}
else -> super.onOptionsItemSelected(item)
}
}
private fun start() {
when (getPreferences().mode()) {
Mode.VPN -> {
val intentPrepare = VpnService.prepare(this)
if (intentPrepare != null) {
vpnRegister.launch(intentPrepare)
} else {
ServiceManager.start(this, Mode.VPN)
}
}
Mode.Proxy -> ServiceManager.start(this, Mode.Proxy)
}
}
private fun stop() {
ServiceManager.stop(this)
}
private fun updateStatus() {
val (status, mode) = appStatus
Log.i(TAG, "Updating status: $status, $mode")
val preferences = getPreferences()
val (ip, port) = preferences.getProxyIpAndPort()
binding.proxyAddress.text = getString(R.string.proxy_address, ip, port)
when (status) {
AppStatus.Halted -> {
when (preferences.mode()) {
Mode.VPN -> {
binding.statusText.setText(R.string.vpn_disconnected)
binding.statusButton.setText(R.string.vpn_connect)
}
Mode.Proxy -> {
binding.statusText.setText(R.string.proxy_down)
binding.statusButton.setText(R.string.proxy_start)
}
}
binding.statusButton.isEnabled = true
}
AppStatus.Running -> {
when (mode) {
Mode.VPN -> {
binding.statusText.setText(R.string.vpn_connected)
binding.statusButton.setText(R.string.vpn_disconnect)
}
Mode.Proxy -> {
binding.statusText.setText(R.string.proxy_up)
binding.statusButton.setText(R.string.proxy_stop)
}
}
binding.statusButton.isEnabled = true
}
}
}
}
@@ -1,136 +0,0 @@
package io.github.dovecoteescapee.byedpi.activities
import android.os.Bundle
import android.view.Menu
import android.view.MenuItem
import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity
import com.google.gson.Gson
import io.github.dovecoteescapee.byedpi.BuildConfig
import io.github.dovecoteescapee.byedpi.R
import io.github.dovecoteescapee.byedpi.data.AppSettings
import io.github.dovecoteescapee.byedpi.fragments.MainSettingsFragment
import io.github.dovecoteescapee.byedpi.utility.HistoryUtils
import io.github.dovecoteescapee.byedpi.utility.getPreferences
import io.github.dovecoteescapee.byedpi.utility.getSelectedApps
class SettingsActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_settings)
supportFragmentManager
.beginTransaction()
.replace(R.id.settings, MainSettingsFragment())
.commit()
supportActionBar?.setDisplayHomeAsUpEnabled(true)
}
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
menuInflater.inflate(R.menu.menu_settings, menu)
return true
}
override fun onOptionsItemSelected(item: MenuItem): Boolean = when (item.itemId) {
android.R.id.home -> {
onBackPressedDispatcher.onBackPressed()
true
}
R.id.action_reset_settings -> {
val prefs = getPreferences()
val editor = prefs.edit()
editor.clear()
editor.apply()
recreate()
true
}
R.id.action_export_settings -> {
val fileName = "bbd_${System.currentTimeMillis().toReadableDateTime()}.json"
exportSettingsLauncher.launch(fileName)
true
}
R.id.action_import_settings -> {
importSettingsLauncher.launch(arrayOf("application/json"))
true
}
else -> super.onOptionsItemSelected(item)
}
private val exportSettingsLauncher = registerForActivityResult(
ActivityResultContracts.CreateDocument("application/json")
) { uri ->
uri?.let {
val prefs = getPreferences()
val history = HistoryUtils(this).getHistory()
val apps = prefs.getSelectedApps()
val settings = prefs.all.filterKeys { key ->
key !in setOf("byedpi_command_history", "selected_apps")
}
val export = AppSettings(
app = BuildConfig.APPLICATION_ID,
version = BuildConfig.VERSION_NAME,
history = history,
apps = apps,
settings = settings
)
val json = Gson().toJson(export)
contentResolver.openOutputStream(it)?.use { outputStream ->
outputStream.write(json.toByteArray())
}
}
}
private val importSettingsLauncher = registerForActivityResult(
ActivityResultContracts.OpenDocument()
) { uri ->
uri?.let {
contentResolver.openInputStream(it)?.use { inputStream ->
val json = inputStream.bufferedReader().readText()
val import = Gson().fromJson(json, AppSettings::class.java)
if (import.app != BuildConfig.APPLICATION_ID) {
Toast.makeText(this, "Invalid config", Toast.LENGTH_LONG).show()
return@use
}
val prefs = getPreferences()
val editor = prefs.edit()
editor.clear()
import.settings.forEach { (key, value) ->
when (value) {
is Int -> editor.putInt(key, value)
is Boolean -> editor.putBoolean(key, value)
is String -> editor.putString(key, value)
is Float -> editor.putFloat(key, value)
is Long -> editor.putLong(key, value)
}
}
editor.putStringSet("selected_apps", import.apps.toSet())
editor.apply()
HistoryUtils(this).saveHistory(import.history)
recreate()
}
}
}
private fun Long.toReadableDateTime(): String {
val format = java.text.SimpleDateFormat("yyyyMMdd_HHmm", java.util.Locale.getDefault())
return format.format(this)
}
}
@@ -1,443 +0,0 @@
package io.github.dovecoteescapee.byedpi.activities
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
import android.content.Intent
import android.content.pm.ActivityInfo
import android.content.res.Configuration
import android.os.Bundle
import android.text.SpannableString
import android.text.Spanned
import android.text.method.LinkMovementMethod
import android.text.style.ClickableSpan
import android.util.Log
import android.view.Menu
import android.view.MenuItem
import android.view.View
import android.view.WindowManager
import android.widget.Button
import android.widget.ScrollView
import android.widget.TextView
import android.widget.Toast
import androidx.activity.OnBackPressedCallback
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.lifecycleScope
import io.github.dovecoteescapee.byedpi.R
import io.github.dovecoteescapee.byedpi.data.Mode
import io.github.dovecoteescapee.byedpi.data.ServiceStatus
import io.github.dovecoteescapee.byedpi.services.ByeDpiProxyService
import io.github.dovecoteescapee.byedpi.services.ServiceManager
import io.github.dovecoteescapee.byedpi.utility.GoogleVideoUtils
import io.github.dovecoteescapee.byedpi.utility.HistoryUtils
import io.github.dovecoteescapee.byedpi.utility.getPreferences
import kotlinx.coroutines.*
import java.io.File
import java.net.HttpURLConnection
import java.net.InetSocketAddress
import java.net.Proxy
import java.net.URL
class TestActivity : AppCompatActivity() {
private lateinit var scrollTextView: ScrollView
private lateinit var progressTextView: TextView
private lateinit var resultsTextView: TextView
private lateinit var startStopButton: Button
private lateinit var cmdHistoryUtils: HistoryUtils
private lateinit var sites: MutableList<String>
private lateinit var cmds: List<String>
private var originalCmdArgs: String = ""
private var testJob: Job? = null
private val proxyIp: String = "127.0.0.1"
private val proxyPort: Int = 1080
private var isTesting: Boolean
get() = prefs.getBoolean("is_test_running", false)
set(value) {
prefs.edit().putBoolean("is_test_running", value).apply()
}
private val prefs by lazy { getPreferences() }
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_proxy_test)
cmdHistoryUtils = HistoryUtils(this)
scrollTextView = findViewById(R.id.scrollView)
startStopButton = findViewById(R.id.startStopButton)
resultsTextView = findViewById(R.id.resultsTextView)
progressTextView = findViewById(R.id.progressTextView)
resultsTextView.movementMethod = LinkMovementMethod.getInstance()
if (isTesting) {
progressTextView.text = getString(R.string.test_proxy_error)
resultsTextView.text = getString(R.string.test_crash)
} else {
lifecycleScope.launch {
val previousLogs = loadLog()
if (previousLogs.isNotEmpty()) {
progressTextView.text = getString(R.string.test_complete)
resultsTextView.text = ""
displayLog(previousLogs)
}
}
}
startStopButton.setOnClickListener {
if (isTesting) {
stopTesting()
} else {
startTesting()
}
}
onBackPressedDispatcher.addCallback(this, object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
if (isTesting) {
stopTesting()
}
finish()
}
})
requestedOrientation = when (resources.configuration.orientation) {
Configuration.ORIENTATION_LANDSCAPE -> {
ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
}
Configuration.ORIENTATION_PORTRAIT -> {
ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
}
else -> {
ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
}
}
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
supportActionBar?.setDisplayHomeAsUpEnabled(true)
}
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
menuInflater.inflate(R.menu.menu_test, menu)
return true
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return when (item.itemId) {
R.id.action_settings -> {
if (!isTesting) {
val intent = Intent(this, TestSettingsActivity::class.java)
startActivity(intent)
} else {
Toast.makeText(this, R.string.settings_unavailable, Toast.LENGTH_SHORT).show()
}
true
}
android.R.id.home -> {
onBackPressedDispatcher.onBackPressed()
true
}
else -> super.onOptionsItemSelected(item)
}
}
private fun startProxyService() {
try {
ServiceManager.start(this, Mode.Proxy)
} catch (e: Exception) {
Log.e("TestActivity", "Error start proxy service: ${e.message}")
}
}
private fun stopProxyService() {
try {
ServiceManager.stop(this)
} catch (e: Exception) {
Log.e("TestActivity", "Error stop proxy service: ${e.message}")
}
}
private suspend fun waitForProxyStatus(
statusNeeded: ServiceStatus,
timeoutMillis: Long = 5000L
): Boolean {
val startTime = System.currentTimeMillis()
while (System.currentTimeMillis() - startTime < timeoutMillis) {
if (isProxyRunning() == (statusNeeded == ServiceStatus.Connected)) {
return true
}
delay(100)
}
return false
}
private suspend fun isProxyRunning(): Boolean = withContext(Dispatchers.IO) {
ByeDpiProxyService.getStatus() == ServiceStatus.Connected
}
private fun startTesting() {
isTesting = true
startStopButton.text = getString(R.string.test_stop)
resultsTextView.text = ""
progressTextView.text = ""
originalCmdArgs = prefs.getString("byedpi_cmd_args", "").orEmpty()
sites = loadSites().toMutableList()
cmds = loadCmds()
clearLog()
testJob = lifecycleScope.launch {
val delaySec = prefs.getString("byedpi_proxytest_delay", "1")?.toIntOrNull() ?: 1
val useGeneratedGoogleDomain = prefs.getBoolean("byedpi_proxytest_gdomain", true)
val fullLog = prefs.getBoolean("byedpi_proxytest_fulllog", false)
val logClickable = prefs.getBoolean("byedpi_proxytest_logclickable", false)
val requestsCount =
prefs.getString("byedpi_proxytest_requestsсount", "1")
?.toIntOrNull()
?.takeIf { it > 0 }
?: 1
if (useGeneratedGoogleDomain) {
val googleVideoDomain = GoogleVideoUtils().generateGoogleVideoDomain()
if (googleVideoDomain != null) {
sites.add(googleVideoDomain)
appendTextToResults("--- $googleVideoDomain ---\n\n")
Log.i("TestActivity", "Added auto-generated Google domain: $googleVideoDomain")
} else {
Log.e("TestActivity", "Failed to generate Google domain")
}
}
val successfulCmds = mutableListOf<Pair<String, Int>>()
for ((index, cmd) in cmds.withIndex()) {
val cmdIndex = index + 1
progressTextView.text = getString(R.string.test_process, cmdIndex, cmds.size)
val testCmd = "--ip $proxyIp --port $proxyPort $cmd"
updateCmdInPreferences(testCmd)
if (!isProxyRunning()) startProxyService()
waitForProxyStatus(ServiceStatus.Connected)
if (logClickable) {
appendLinkToResults("$cmd\n")
} else {
appendTextToResults("$cmd\n")
}
delay(delaySec * 1000L)
val totalRequests = sites.size * requestsCount
val checkResults = checkSitesAsync(sites, requestsCount, fullLog)
val successfulCount = checkResults.sumOf { it.second }
val successPercentage = (successfulCount * 100) / totalRequests
delay(delaySec * 1000L)
if (successPercentage >= 50) successfulCmds.add(cmd to successPercentage)
appendTextToResults("$successfulCount/$totalRequests ($successPercentage%)\n\n")
if (isProxyRunning()) stopProxyService()
waitForProxyStatus(ServiceStatus.Disconnected)
}
successfulCmds.sortByDescending { it.second }
progressTextView.text = getString(R.string.test_complete)
appendTextToResults("${getString(R.string.test_good_cmds)}\n\n")
successfulCmds.forEachIndexed { index, (cmd, success) ->
appendTextToResults("${index + 1}. ")
appendLinkToResults("$cmd\n")
appendTextToResults("$success%\n\n")
}
appendTextToResults(getString(R.string.test_complete_info))
stopTesting()
}
}
private fun stopTesting() {
updateCmdInPreferences(originalCmdArgs)
isTesting = false
testJob?.cancel()
startStopButton.text = getString(R.string.test_start)
lifecycleScope.launch {
if (isProxyRunning()) {
stopProxyService()
}
}
}
private fun appendTextToResults(text: String) {
resultsTextView.append(text)
if (isTesting) saveLog(text)
scrollToBottom()
}
private fun appendLinkToResults(text: String) {
val spannableString = SpannableString(text)
val menuItems = arrayOf(
getString(R.string.cmd_history_apply),
getString(R.string.cmd_history_copy)
)
spannableString.setSpan(
object : ClickableSpan() {
override fun onClick(widget: View) {
AlertDialog.Builder(this@TestActivity)
.setTitle(getString(R.string.cmd_history_menu))
.setItems(menuItems) { _, which ->
when (which) {
0 -> addToHistory(text.trim())
1 -> copyToClipboard(text.trim())
}
}
.show()
}
},
0,
text.length,
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
)
resultsTextView.append(spannableString)
if (isTesting) saveLog("{$text}")
scrollToBottom()
}
private fun scrollToBottom() {
scrollTextView.post {
scrollTextView.fullScroll(ScrollView.FOCUS_DOWN)
}
}
private fun copyToClipboard(text: String) {
val clipboard = getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
val clip = ClipData.newPlainText("command", text)
clipboard.setPrimaryClip(clip)
}
private fun addToHistory(command: String) {
updateCmdInPreferences(command)
cmdHistoryUtils.addCommand(command)
}
private fun displayLog(log: String) {
log.split("{", "}").forEachIndexed { index, part ->
if (index % 2 == 0) {
appendTextToResults(part)
} else {
appendLinkToResults(part)
}
}
}
private fun saveLog(text: String) {
val file = File(filesDir, "proxy_test.log")
file.appendText(text)
}
private fun loadLog(): String {
val file = File(filesDir, "proxy_test.log")
return if (file.exists()) file.readText() else ""
}
private fun clearLog() {
val file = File(filesDir, "proxy_test.log")
file.writeText("")
}
private fun loadSites(): List<String> {
val userDomains = prefs.getBoolean("byedpi_proxytest_userdomains", false)
return if (userDomains) {
val domains = prefs.getString("byedpi_proxytest_domains", "").orEmpty()
domains.lines()
.map { it.trim() }
.filter { it.isNotEmpty() }
} else {
assets.open("proxytest_sites.txt").bufferedReader().useLines { it.toList() }
}
}
private fun loadCmds(): List<String> {
val userCommands = prefs.getBoolean("byedpi_proxytest_usercommands", false)
return if (userCommands) {
val commands = prefs.getString("byedpi_proxytest_commands", "").orEmpty()
commands.lines()
.map { it.trim() }
.filter { it.isNotEmpty() }
} else {
assets.open("proxytest_cmds.txt").bufferedReader().useLines { it.toList() }
}
}
private suspend fun checkSitesAsync(
sites: List<String>,
requestsCount: Int,
fullLog: Boolean
): List<Pair<String, Int>> {
return withContext(Dispatchers.IO) {
sites.map { site ->
async {
if (!isProxyRunning()) return@async site to 0
val successCount = checkSiteAccess(site, requestsCount)
if (fullLog) {
withContext(Dispatchers.Main) {
appendTextToResults("$site - $successCount/$requestsCount\n")
}
}
site to successCount
}
}.awaitAll()
}
}
private suspend fun checkSiteAccess(
site: String,
requestsCount: Int
): Int = withContext(Dispatchers.IO) {
var responseCount = 0
val formattedUrl = if (site.startsWith("http://") || site.startsWith("https://"))
site
else
"https://$site"
repeat(requestsCount) { attempt ->
Log.i("CheckSite", "Attempt ${attempt + 1}/$requestsCount for $site")
try {
val url = URL(formattedUrl)
val proxy = Proxy(Proxy.Type.SOCKS, InetSocketAddress(proxyIp, proxyPort))
val connection = url.openConnection(proxy) as HttpURLConnection
connection.requestMethod = "GET"
connection.connectTimeout = 2000
connection.readTimeout = 2000
val responseCode = connection.responseCode
responseCount++
Log.i("CheckSite", "Response for $site: $responseCode")
connection.disconnect()
} catch (e: Exception) {
Log.e("CheckSite", "Error accessing $site: ${e.message}")
}
}
responseCount
}
private fun updateCmdInPreferences(cmd: String) {
prefs.edit().putString("byedpi_cmd_args", cmd).apply()
}
}
@@ -1,29 +0,0 @@
package io.github.dovecoteescapee.byedpi.activities
import android.os.Bundle
import android.view.MenuItem
import androidx.appcompat.app.AppCompatActivity
import io.github.dovecoteescapee.byedpi.R
import io.github.dovecoteescapee.byedpi.fragments.ProxyTestSettingsFragment
class TestSettingsActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_test_settings)
supportFragmentManager
.beginTransaction()
.replace(R.id.test_settings, ProxyTestSettingsFragment())
.commit()
supportActionBar?.setDisplayHomeAsUpEnabled(true)
}
override fun onOptionsItemSelected(item: MenuItem): Boolean = when (item.itemId) {
android.R.id.home -> {
onBackPressedDispatcher.onBackPressed()
true
}
else -> super.onOptionsItemSelected(item)
}
}
@@ -1,73 +0,0 @@
package io.github.dovecoteescapee.byedpi.adapters
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.CheckBox
import android.widget.Filter
import android.widget.Filterable
import android.widget.ImageView
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
import io.github.dovecoteescapee.byedpi.R
import io.github.dovecoteescapee.byedpi.data.AppInfo
class AppSelectionAdapter(
private val allApps: List<AppInfo>,
private val onAppSelected: (AppInfo, Boolean) -> Unit
) : RecyclerView.Adapter<AppSelectionAdapter.ViewHolder>(), Filterable {
private val filteredApps: MutableList<AppInfo> = allApps
.filter { !it.appName.contains("com.") }
.toMutableList()
private val originalApps: List<AppInfo> = allApps
.filter { !it.appName.contains("com.") }
class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {
val appIcon: ImageView = view.findViewById(R.id.appIcon)
val appName: TextView = view.findViewById(R.id.appName)
val appCheckBox: CheckBox = view.findViewById(R.id.appCheckBox)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val view = LayoutInflater.from(parent.context).inflate(R.layout.item_app, parent, false)
return ViewHolder(view)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val app = filteredApps[position]
holder.appIcon.setImageDrawable(app.icon)
holder.appName.text = app.appName
holder.appCheckBox.isChecked = app.isSelected
holder.itemView.setOnClickListener {
app.isSelected = !app.isSelected
holder.appCheckBox.isChecked = app.isSelected
onAppSelected(app, app.isSelected)
}
}
override fun getItemCount() = filteredApps.size
override fun getFilter(): Filter {
return object : Filter() {
override fun performFiltering(constraint: CharSequence?): FilterResults {
val query = constraint?.toString()?.lowercase() ?: ""
val filteredList = if (query.isEmpty()) {
originalApps
} else {
originalApps.filter { it.appName.lowercase().contains(query) }
}
return FilterResults().apply { values = filteredList }
}
override fun publishResults(constraint: CharSequence?, results: FilterResults?) {
val newFilteredApps = (results?.values as? List<AppInfo>) ?: originalApps
filteredApps.clear()
filteredApps.addAll(newFilteredApps)
notifyDataSetChanged()
}
}
}
}
@@ -1,117 +0,0 @@
package io.github.dovecoteescapee.byedpi.core
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
class ByeDpiProxy {
companion object {
init {
System.loadLibrary("byedpi")
}
}
private val mutex = Mutex()
private var fd = -1
suspend fun startProxy(preferences: ByeDpiProxyPreferences): Int {
val fd = createSocket(preferences)
if (fd < 0) return -1
return jniStartProxy(fd)
}
suspend fun stopProxy(): Int {
mutex.withLock {
if (fd < 0) throw IllegalStateException("Proxy is not running")
val result = jniStopProxy(fd)
fd = -1
return result
}
}
private suspend fun createSocket(preferences: ByeDpiProxyPreferences): Int =
mutex.withLock {
if (fd >= 0) throw IllegalStateException("Proxy is already running")
val result = createSocketFromPreferences(preferences)
if (result < 0) return -1
fd = result
return result
}
private fun createSocketFromPreferences(preferences: ByeDpiProxyPreferences) =
when (preferences) {
is ByeDpiProxyCmdPreferences -> jniCreateSocketWithCommandLine(preferences.args)
is ByeDpiProxyUIPreferences -> jniCreateSocket(
ip = preferences.ip,
port = preferences.port,
maxConnections = preferences.maxConnections,
bufferSize = preferences.bufferSize,
defaultTtl = preferences.defaultTtl,
customTtl = preferences.customTtl,
noDomain = preferences.noDomain,
desyncHttp = preferences.desyncHttp,
desyncHttps = preferences.desyncHttps,
desyncUdp = preferences.desyncUdp,
desyncMethod = preferences.desyncMethod.ordinal,
splitPosition = preferences.splitPosition,
splitAtHost = preferences.splitAtHost,
fakeTtl = preferences.fakeTtl,
fakeSni = preferences.fakeSni,
oobChar = preferences.oobChar,
hostMixedCase = preferences.hostMixedCase,
domainMixedCase = preferences.domainMixedCase,
hostRemoveSpaces = preferences.hostRemoveSpaces,
tlsRecordSplit = preferences.tlsRecordSplit,
tlsRecordSplitPosition = preferences.tlsRecordSplitPosition,
tlsRecordSplitAtSni = preferences.tlsRecordSplitAtSni,
hostsMode = preferences.hostsMode.ordinal,
hosts = preferences.hosts,
tcpFastOpen = preferences.tcpFastOpen,
udpFakeCount = preferences.udpFakeCount,
dropSack = preferences.dropSack,
fakeOffset = preferences.fakeOffset,
)
}
private external fun jniCreateSocketWithCommandLine(args: Array<String>): Int
private external fun jniCreateSocket(
ip: String,
port: Int,
maxConnections: Int,
bufferSize: Int,
defaultTtl: Int,
customTtl: Boolean,
noDomain: Boolean,
desyncHttp: Boolean,
desyncHttps: Boolean,
desyncUdp: Boolean,
desyncMethod: Int,
splitPosition: Int,
splitAtHost: Boolean,
fakeTtl: Int,
fakeSni: String,
oobChar: Byte,
hostMixedCase: Boolean,
domainMixedCase: Boolean,
hostRemoveSpaces: Boolean,
tlsRecordSplit: Boolean,
tlsRecordSplitPosition: Int,
tlsRecordSplitAtSni: Boolean,
hostsMode: Int,
hosts: String?,
tcpFastOpen: Boolean,
udpFakeCount: Int,
dropSack: Boolean,
fakeOffset: Int,
): Int
private external fun jniStartProxy(fd: Int): Int
private external fun jniStopProxy(fd: Int): Int
}
@@ -1,195 +0,0 @@
package io.github.dovecoteescapee.byedpi.core
import android.content.SharedPreferences
import android.util.Log
import io.github.dovecoteescapee.byedpi.utility.getStringNotNull
import io.github.dovecoteescapee.byedpi.utility.shellSplit
sealed interface ByeDpiProxyPreferences {
companion object {
fun fromSharedPreferences(preferences: SharedPreferences): ByeDpiProxyPreferences =
when (preferences.getBoolean("byedpi_enable_cmd_settings", false)) {
true -> ByeDpiProxyCmdPreferences(preferences)
false -> ByeDpiProxyUIPreferences(preferences)
}
}
}
class ByeDpiProxyCmdPreferences(val args: Array<String>) : ByeDpiProxyPreferences {
constructor(preferences: SharedPreferences) : this(
cmdToArgs(
preferences.getStringNotNull("byedpi_cmd_args", ""),
preferences
)
)
companion object {
private fun cmdToArgs(cmd: String, preferences: SharedPreferences): Array<String> {
val firstArgIndex = cmd.indexOf("-")
val args = (if (firstArgIndex > 0) cmd.substring(firstArgIndex) else cmd).trim()
Log.i("ProxyPref", "Parse args: $args")
val hasIp = args.contains("-i ") || args.contains("--ip ")
val hasPort = args.contains("-p ") || args.contains("--port ")
val ip = preferences.getStringNotNull("byedpi_proxy_ip", "127.0.0.1")
val port = preferences.getStringNotNull("byedpi_proxy_port", "1080")
val prefix = buildString {
if (!hasIp) append("-i $ip ")
if (!hasPort) append("-p $port ")
}
Log.i("ProxyPref", "Added from settings: $prefix")
if (prefix.isNotEmpty()) {
return arrayOf("ciadpi") + shellSplit("$prefix$args")
}
return arrayOf("ciadpi") + shellSplit(args)
}
}
}
class ByeDpiProxyUIPreferences(
ip: String? = null,
port: Int? = null,
maxConnections: Int? = null,
bufferSize: Int? = null,
defaultTtl: Int? = null,
noDomain: Boolean? = null,
desyncHttp: Boolean? = null,
desyncHttps: Boolean? = null,
desyncUdp: Boolean? = null,
desyncMethod: DesyncMethod? = null,
splitPosition: Int? = null,
splitAtHost: Boolean? = null,
fakeTtl: Int? = null,
fakeSni: String? = null,
oobChar: String? = null,
hostMixedCase: Boolean? = null,
domainMixedCase: Boolean? = null,
hostRemoveSpaces: Boolean? = null,
tlsRecordSplit: Boolean? = null,
tlsRecordSplitPosition: Int? = null,
tlsRecordSplitAtSni: Boolean? = null,
hostsMode: HostsMode? = null,
hosts: String? = null,
tcpFastOpen: Boolean? = null,
udpFakeCount: Int? = null,
dropSack: Boolean? = null,
byedpiFakeOffset: Int? = null,
) : ByeDpiProxyPreferences {
val ip: String = ip ?: "127.0.0.1"
val port: Int = port ?: 1080
val maxConnections: Int = maxConnections ?: 512
val bufferSize: Int = bufferSize ?: 16384
val defaultTtl: Int = defaultTtl ?: 0
val customTtl: Boolean = defaultTtl != null
val noDomain: Boolean = noDomain ?: false
val desyncHttp: Boolean = desyncHttp ?: true
val desyncHttps: Boolean = desyncHttps ?: true
val desyncUdp: Boolean = desyncUdp ?: false
val desyncMethod: DesyncMethod = desyncMethod ?: DesyncMethod.Disorder
val splitPosition: Int = splitPosition ?: 1
val splitAtHost: Boolean = splitAtHost ?: false
val fakeTtl: Int = fakeTtl ?: 8
val fakeSni: String = fakeSni ?: "www.iana.org"
val oobChar: Byte = (oobChar ?: "a")[0].code.toByte()
val hostMixedCase: Boolean = hostMixedCase ?: false
val domainMixedCase: Boolean = domainMixedCase ?: false
val hostRemoveSpaces: Boolean = hostRemoveSpaces ?: false
val tlsRecordSplit: Boolean = tlsRecordSplit ?: false
val tlsRecordSplitPosition: Int = tlsRecordSplitPosition ?: 0
val tlsRecordSplitAtSni: Boolean = tlsRecordSplitAtSni ?: false
val hostsMode: HostsMode =
if (hosts?.isBlank() != false) HostsMode.Disable
else hostsMode ?: HostsMode.Disable
val hosts: String? =
if (this.hostsMode == HostsMode.Disable) null
else hosts?.trim()
val tcpFastOpen: Boolean = tcpFastOpen ?: false
val udpFakeCount: Int = udpFakeCount ?: 0
val dropSack: Boolean = dropSack ?: false
val fakeOffset: Int = byedpiFakeOffset ?: 0
constructor(preferences: SharedPreferences) : this(
ip = preferences.getString("byedpi_proxy_ip", null),
port = preferences.getString("byedpi_proxy_port", null)?.toIntOrNull(),
maxConnections = preferences.getString("byedpi_max_connections", null)?.toIntOrNull(),
bufferSize = preferences.getString("byedpi_buffer_size", null)?.toIntOrNull(),
defaultTtl = preferences.getString("byedpi_default_ttl", null)?.toIntOrNull(),
noDomain = preferences.getBoolean("byedpi_no_domain", false),
desyncHttp = preferences.getBoolean("byedpi_desync_http", true),
desyncHttps = preferences.getBoolean("byedpi_desync_https", true),
desyncUdp = preferences.getBoolean("byedpi_desync_udp", false),
desyncMethod = preferences.getString("byedpi_desync_method", null)
?.let { DesyncMethod.fromName(it) },
splitPosition = preferences.getString("byedpi_split_position", null)?.toIntOrNull(),
splitAtHost = preferences.getBoolean("byedpi_split_at_host", false),
fakeTtl = preferences.getString("byedpi_fake_ttl", null)?.toIntOrNull(),
fakeSni = preferences.getString("byedpi_fake_sni", null),
oobChar = preferences.getString("byedpi_oob_data", null),
hostMixedCase = preferences.getBoolean("byedpi_host_mixed_case", false),
domainMixedCase = preferences.getBoolean("byedpi_domain_mixed_case", false),
hostRemoveSpaces = preferences.getBoolean("byedpi_host_remove_spaces", false),
tlsRecordSplit = preferences.getBoolean("byedpi_tlsrec_enabled", false),
tlsRecordSplitPosition = preferences.getString("byedpi_tlsrec_position", null)
?.toIntOrNull(),
tlsRecordSplitAtSni = preferences.getBoolean("byedpi_tlsrec_at_sni", false),
hostsMode = preferences.getString("byedpi_hosts_mode", null)
?.let { HostsMode.fromName(it) },
hosts = preferences.getString("byedpi_hosts_mode", null)?.let {
when (HostsMode.fromName(it)) {
HostsMode.Blacklist -> preferences.getString("byedpi_hosts_blacklist", null)
HostsMode.Whitelist -> preferences.getString("byedpi_hosts_whitelist", null)
else -> null
}
},
tcpFastOpen = preferences.getBoolean("byedpi_tcp_fast_open", false),
udpFakeCount = preferences.getString("byedpi_udp_fake_count", null)?.toIntOrNull(),
dropSack = preferences.getBoolean("byedpi_drop_sack", false),
byedpiFakeOffset = preferences.getString("byedpi_fake_offset", null)?.toIntOrNull(),
)
enum class DesyncMethod {
None,
Split,
Disorder,
Fake,
OOB,
DISOOB;
companion object {
fun fromName(name: String): DesyncMethod {
return when (name) {
"none" -> None
"split" -> Split
"disorder" -> Disorder
"fake" -> Fake
"oob" -> OOB
"disoob" -> DISOOB
else -> throw IllegalArgumentException("Unknown desync method: $name")
}
}
}
}
enum class HostsMode {
Disable,
Blacklist,
Whitelist;
companion object {
fun fromName(name: String): HostsMode {
return when (name) {
"disable" -> Disable
"blacklist" -> Blacklist
"whitelist" -> Whitelist
else -> throw IllegalArgumentException("Unknown hosts mode: $name")
}
}
}
}
}
@@ -1,4 +0,0 @@
package io.github.dovecoteescapee.byedpi.data
const val START_ACTION = "start"
const val STOP_ACTION = "stop"
@@ -1,9 +0,0 @@
package io.github.dovecoteescapee.byedpi.data
data class AppSettings(
val app: String,
val version: String,
val history: List<Command>,
val apps: List<String>,
val settings: Map<String, Any?>
)
@@ -1,12 +0,0 @@
package io.github.dovecoteescapee.byedpi.data
const val STARTED_BROADCAST = "io.github.dovecoteescapee.byedpi.STARTED"
const val STOPPED_BROADCAST = "io.github.dovecoteescapee.byedpi.STOPPED"
const val FAILED_BROADCAST = "io.github.dovecoteescapee.byedpi.FAILED"
const val SENDER = "sender"
enum class Sender(val senderName: String) {
Proxy("Proxy"),
VPN("VPN")
}
@@ -1,145 +0,0 @@
package io.github.dovecoteescapee.byedpi.fragments
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
import android.os.Bundle
import android.widget.EditText
import android.widget.LinearLayout
import androidx.preference.*
import io.github.dovecoteescapee.byedpi.R
import io.github.dovecoteescapee.byedpi.utility.findPreferenceNotNull
import androidx.appcompat.app.AlertDialog
import io.github.dovecoteescapee.byedpi.data.Command
import io.github.dovecoteescapee.byedpi.utility.HistoryUtils
class ByeDpiCommandLineSettingsFragment : PreferenceFragmentCompat() {
private lateinit var cmdHistoryUtils: HistoryUtils
private lateinit var editTextPreference: EditTextPreference
private lateinit var historyCategory: PreferenceCategory
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
setPreferencesFromResource(R.xml.byedpi_cmd_settings, rootKey)
cmdHistoryUtils = HistoryUtils(requireContext())
editTextPreference = findPreferenceNotNull("byedpi_cmd_args")
historyCategory = findPreferenceNotNull("cmd_history_category")
editTextPreference.setOnPreferenceChangeListener { _, newValue ->
val newCommand = newValue.toString()
if (newCommand.isNotBlank()) cmdHistoryUtils.addCommand(newCommand)
updateHistoryCategory()
true
}
updateHistoryCategory()
}
private fun updateHistoryCategory() {
historyCategory.removeAll()
val history = cmdHistoryUtils.getHistory()
history.sortedWith(compareByDescending<Command> { it.pinned }.thenBy { history.indexOf(it) })
.forEach { command ->
val preference = createPreference(command)
historyCategory.addPreference(preference)
}
}
private fun createPreference(command: Command) =
Preference(requireContext()).apply {
title = command.text
summary = buildSummary(command)
setOnPreferenceClickListener {
showActionDialog(command)
true
}
}
private fun buildSummary(command: Command): String {
val summary = StringBuilder()
if (command.name != null) {
summary.append(command.name)
}
if (command.pinned) {
if (summary.isNotEmpty()) summary.append(" - ")
summary.append(context?.getString(R.string.cmd_history_pinned))
}
return summary.toString()
}
private fun showActionDialog(command: Command) {
val options = arrayOf(
getString(R.string.cmd_history_apply),
if (command.pinned) getString(R.string.cmd_history_unpin) else getString(R.string.cmd_history_pin),
getString(R.string.cmd_history_rename),
getString(R.string.cmd_history_copy),
getString(R.string.cmd_history_delete)
)
AlertDialog.Builder(requireContext())
.setTitle(getString(R.string.cmd_history_menu))
.setItems(options) { _, which ->
when (which) {
0 -> applyCommand(command.text)
1 -> if (command.pinned) unpinCommand(command.text) else pinCommand(command.text)
2 -> showRenameDialog(command)
3 -> copyToClipboard(command.text)
4 -> deleteCommand(command.text)
}
}
.show()
}
private fun showRenameDialog(command: Command) {
val input = EditText(requireContext()).apply {
setText(command.name)
}
val container = LinearLayout(context).apply {
orientation = LinearLayout.VERTICAL
setPadding(50, 20, 50, 20)
addView(input)
}
AlertDialog.Builder(requireContext())
.setTitle(getString(R.string.cmd_history_rename))
.setView(container)
.setPositiveButton(getString(android.R.string.ok)) { _, _ ->
val newName = input.text.toString()
if (newName.isNotBlank()) {
cmdHistoryUtils.renameCommand(command.text, newName)
updateHistoryCategory()
}
}
.setNegativeButton(getString(android.R.string.cancel), null)
.show()
}
private fun applyCommand(command: String) {
editTextPreference.text = command
}
private fun pinCommand(command: String) {
cmdHistoryUtils.pinCommand(command)
updateHistoryCategory()
}
private fun unpinCommand(command: String) {
cmdHistoryUtils.unpinCommand(command)
updateHistoryCategory()
}
private fun deleteCommand(command: String) {
cmdHistoryUtils.deleteCommand(command)
updateHistoryCategory()
}
private fun copyToClipboard(command: String) {
val clipboard = requireContext().getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
val clip = ClipData.newPlainText("Command", command)
clipboard.setPrimaryClip(clip)
}
}
@@ -1,169 +0,0 @@
package io.github.dovecoteescapee.byedpi.fragments
import android.content.Intent
import android.content.SharedPreferences
import android.os.Bundle
import android.util.Log
import androidx.appcompat.app.AppCompatDelegate
import androidx.core.os.LocaleListCompat
import androidx.preference.*
import io.github.dovecoteescapee.byedpi.BuildConfig
import io.github.dovecoteescapee.byedpi.R
import io.github.dovecoteescapee.byedpi.activities.TestActivity
import io.github.dovecoteescapee.byedpi.data.Mode
import io.github.dovecoteescapee.byedpi.utility.*
class MainSettingsFragment : PreferenceFragmentCompat() {
companion object {
private val TAG: String = MainSettingsFragment::class.java.simpleName
fun setLang(lang: String) {
val appLocale = localeByName(lang) ?: throw IllegalStateException("Invalid value for language: $lang")
AppCompatDelegate.setApplicationLocales(appLocale)
}
private fun localeByName(lang: String): LocaleListCompat? = when (lang) {
"system" -> LocaleListCompat.getEmptyLocaleList()
"ru" -> LocaleListCompat.forLanguageTags("ru")
"en" -> LocaleListCompat.forLanguageTags("en")
else -> {
Log.w(TAG, "Invalid value for language: $lang")
null
}
}
fun setTheme(name: String) =
themeByName(name)?.let {
AppCompatDelegate.setDefaultNightMode(it)
} ?: throw IllegalStateException("Invalid value for app_theme: $name")
private fun themeByName(name: String): Int? = when (name) {
"system" -> AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM
"light" -> AppCompatDelegate.MODE_NIGHT_NO
"dark" -> AppCompatDelegate.MODE_NIGHT_YES
else -> {
Log.w(TAG, "Invalid value for app_theme: $name")
null
}
}
}
private val preferenceListener =
SharedPreferences.OnSharedPreferenceChangeListener { _, _ ->
updatePreferences()
}
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
setPreferencesFromResource(R.xml.main_settings, rootKey)
setEditTextPreferenceListener("byedpi_proxy_ip") { checkIp(it) }
setEditTestPreferenceListenerPort("byedpi_proxy_port")
setEditTextPreferenceListener("dns_ip") {
it.isBlank() || checkNotLocalIp(it)
}
findPreferenceNotNull<ListPreference>("language")
.setOnPreferenceChangeListener { _, newValue ->
setLang(newValue as String)
true
}
findPreferenceNotNull<ListPreference>("app_theme")
.setOnPreferenceChangeListener { _, newValue ->
setTheme(newValue as String)
true
}
val switchCmdSettings = findPreferenceNotNull<SwitchPreference>("byedpi_enable_cmd_settings")
val uiSettings = findPreferenceNotNull<Preference>("byedpi_ui_settings")
val cmdSettings = findPreferenceNotNull<Preference>("byedpi_cmd_settings")
val proxyTest = findPreferenceNotNull<Preference>("proxy_test")
val setByeDpiSettingsMode = { enable: Boolean ->
uiSettings.isEnabled = !enable
cmdSettings.isEnabled = enable
proxyTest.isEnabled = enable
}
setByeDpiSettingsMode(switchCmdSettings.isChecked)
switchCmdSettings.setOnPreferenceChangeListener { _, newValue ->
setByeDpiSettingsMode(newValue as Boolean)
updatePreferences()
true
}
findPreferenceNotNull<Preference>("proxy_test")
.setOnPreferenceClickListener {
val intent = Intent(context, TestActivity::class.java)
startActivity(intent)
true
}
findPreferenceNotNull<Preference>("version").summary = BuildConfig.VERSION_NAME
updatePreferences()
}
override fun onResume() {
super.onResume()
sharedPreferences?.registerOnSharedPreferenceChangeListener(preferenceListener)
updatePreferences()
}
override fun onPause() {
super.onPause()
sharedPreferences?.unregisterOnSharedPreferenceChangeListener(preferenceListener)
}
private fun updatePreferences() {
val mode = findPreferenceNotNull<ListPreference>("byedpi_mode").value.let { Mode.fromString(it) }
val dns = findPreferenceNotNull<EditTextPreference>("dns_ip")
val ipv6 = findPreferenceNotNull<SwitchPreference>("ipv6_enable")
val proxy = findPreferenceNotNull<PreferenceCategory>("byedpi_proxy_category")
val applistType = findPreferenceNotNull<ListPreference>("applist_type")
val selectedApps = findPreferenceNotNull<Preference>("selected_apps")
if (sharedPreferences?.getBoolean("byedpi_enable_cmd_settings", false) == true) {
val cmdArgs = sharedPreferences?.getStringNotNull("byedpi_cmd_args", "")?.split(" ")
val ipIndex = cmdArgs?.indexOfFirst { it == "-i" || it == "--ip" }
val portIndex = cmdArgs?.indexOfFirst { it == "-p" || it == "--port" }
proxy.isVisible = ipIndex == -1 && portIndex == -1
} else {
proxy.isVisible = true
}
when (mode) {
Mode.VPN -> {
dns.isVisible = true
ipv6.isVisible = true
when (applistType.value) {
"disable" -> {
applistType.isVisible = true
selectedApps.isVisible = false
}
"blacklist", "whitelist" -> {
applistType.isVisible = true
selectedApps.isVisible = true
}
else -> {
applistType.isVisible = true
selectedApps.isVisible = false
Log.w(TAG, "Unexpected applistType value: ${applistType.value}")
}
}
}
Mode.Proxy -> {
dns.isVisible = false
ipv6.isVisible = false
applistType.isVisible = false
selectedApps.isVisible = false
}
}
}
}
@@ -1,43 +0,0 @@
package io.github.dovecoteescapee.byedpi.fragments
import android.content.SharedPreferences
import android.os.Bundle
import androidx.preference.*
import io.github.dovecoteescapee.byedpi.R
import io.github.dovecoteescapee.byedpi.utility.*
class ProxyTestSettingsFragment : PreferenceFragmentCompat() {
private val preferenceListener =
SharedPreferences.OnSharedPreferenceChangeListener { _, _ ->
updatePreferences()
}
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
setPreferencesFromResource(R.xml.proxy_test_settings, rootKey)
updatePreferences()
}
override fun onResume() {
super.onResume()
sharedPreferences?.registerOnSharedPreferenceChangeListener(preferenceListener)
}
override fun onPause() {
super.onPause()
sharedPreferences?.unregisterOnSharedPreferenceChangeListener(preferenceListener)
}
private fun updatePreferences() {
val switchUserDomains = findPreferenceNotNull<SwitchPreference>("byedpi_proxytest_userdomains")
val switchUserCommands = findPreferenceNotNull<SwitchPreference>("byedpi_proxytest_usercommands")
val textUserDomains = findPreferenceNotNull<EditTextPreference>("byedpi_proxytest_domains")
val textUserCommands = findPreferenceNotNull<EditTextPreference>("byedpi_proxytest_commands")
val setUserDomains = { enable: Boolean -> textUserDomains.isEnabled = enable }
val setUserCommands = { enable: Boolean -> textUserCommands.isEnabled = enable }
setUserDomains(switchUserDomains.isChecked)
setUserCommands(switchUserCommands.isChecked)
}
}
@@ -1,11 +0,0 @@
package io.github.dovecoteescapee.byedpi.services
import io.github.dovecoteescapee.byedpi.data.AppStatus
import io.github.dovecoteescapee.byedpi.data.Mode
var appStatus = AppStatus.Halted to Mode.VPN
private set
fun setStatus(status: AppStatus, mode: Mode) {
appStatus = status to mode
}
@@ -1,127 +0,0 @@
package io.github.dovecoteescapee.byedpi.services
import android.app.PendingIntent
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.net.VpnService
import android.os.Build
import android.service.quicksettings.Tile
import android.service.quicksettings.TileService
import android.util.Log
import android.widget.Toast
import androidx.annotation.RequiresApi
import androidx.core.content.ContextCompat
import androidx.core.service.quicksettings.PendingIntentActivityWrapper
import androidx.core.service.quicksettings.TileServiceCompat
import io.github.dovecoteescapee.byedpi.R
import io.github.dovecoteescapee.byedpi.activities.MainActivity
import io.github.dovecoteescapee.byedpi.data.*
import io.github.dovecoteescapee.byedpi.utility.getPreferences
import io.github.dovecoteescapee.byedpi.utility.mode
@RequiresApi(Build.VERSION_CODES.N)
class QuickTileService : TileService() {
companion object {
private val TAG: String = QuickTileService::class.java.simpleName
}
private val receiver: BroadcastReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
val senderOrd = intent.getIntExtra(SENDER, -1)
val sender = Sender.entries.getOrNull(senderOrd)
if (sender == null) {
Log.w(TAG, "Received intent with unknown sender: $senderOrd")
return
}
when (val action = intent.action) {
STARTED_BROADCAST,
STOPPED_BROADCAST -> updateStatus()
FAILED_BROADCAST -> {
Toast.makeText(
context,
getString(R.string.failed_to_start, sender.name),
Toast.LENGTH_SHORT,
).show()
updateStatus()
}
else -> Log.w(TAG, "Unknown action: $action")
}
}
}
override fun onStartListening() {
updateStatus()
ContextCompat.registerReceiver(
this,
receiver,
IntentFilter().apply {
addAction(STARTED_BROADCAST)
addAction(STOPPED_BROADCAST)
addAction(FAILED_BROADCAST)
},
ContextCompat.RECEIVER_EXPORTED,
)
}
override fun onStopListening() {
unregisterReceiver(receiver)
}
private fun launchActivity() {
TileServiceCompat.startActivityAndCollapse(
this, PendingIntentActivityWrapper(
this, 0, Intent(this, MainActivity::class.java),
PendingIntent.FLAG_UPDATE_CURRENT, false
)
)
}
override fun onClick() {
if (qsTile.state == Tile.STATE_UNAVAILABLE) {
return
}
unlockAndRun(this::handleClick)
}
private fun setState(newState: Int) {
qsTile.apply {
state = newState
updateTile()
}
}
private fun updateStatus() {
val (status) = appStatus
setState(if (status == AppStatus.Halted) Tile.STATE_INACTIVE else Tile.STATE_ACTIVE)
}
private fun handleClick() {
setState(Tile.STATE_ACTIVE)
setState(Tile.STATE_UNAVAILABLE)
val (status) = appStatus
when (status) {
AppStatus.Halted -> {
val mode = getPreferences().mode()
if (mode == Mode.VPN && VpnService.prepare(this) != null) {
updateStatus()
launchActivity()
return
}
ServiceManager.start(this, mode)
}
AppStatus.Running -> ServiceManager.stop(this)
}
}
}
@@ -1,116 +0,0 @@
package io.github.dovecoteescapee.byedpi.utility
import android.util.Log
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.io.BufferedReader
import java.io.InputStreamReader
import java.net.HttpURLConnection
import java.net.URL
class GoogleVideoUtils {
companion object {
private val lettersListA = listOf(
'u', 'z', 'p', 'k', 'f', 'a', '5', '0', 'v', 'q', 'l', 'g',
'b', '6', '1', 'w', 'r', 'm', 'h', 'c', '7', '2', 'x', 's',
'n', 'i', 'd', '8', '3', 'y', 't', 'o', 'j', 'e', '9', '4', '-'
)
private val lettersListB = listOf(
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b',
'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n',
'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '-'
)
private val lettersMap = lettersListA.zip(lettersListB).toMap()
}
suspend fun generateGoogleVideoDomain(): String? {
val clusterCodename = getClusterCodename()
if (clusterCodename == null) {
Log.e("AutoGCS", "Failed to obtain cluster codename")
return null
}
Log.i("AutoGCS", "Cluster codename: $clusterCodename")
val clusterName = convertClusterCodename(clusterCodename)
Log.i("AutoGCS", "Cluster name: $clusterName")
val autoGCS = buildAutoGCS(clusterName)
Log.i("AutoGCS", "Generated domain: $autoGCS")
return autoGCS
}
private suspend fun getClusterCodename(): String? {
val urls = listOf(
"https://redirector.gvt1.com/report_mapping?di=no",
"https://redirector.googlevideo.com/report_mapping?di=no"
)
for (url in urls) {
try {
val responseBody = withContext(Dispatchers.IO) { httpGet(url) }
if (responseBody != null) {
val clusterCodename = extractClusterCodenameFromBody(responseBody)
if (clusterCodename != null) {
return clusterCodename
}
}
} catch (e: Exception) {
Log.e("ClusterCodename", "Error fetching cluster codename from $url", e)
}
}
return null
}
private fun extractClusterCodenameFromBody(body: String): String? {
val regex = Regex("""=>\s*(\S+)\s*(?:\(|:)""")
val matchResult = regex.find(body)
val codename = matchResult?.groupValues?.get(1)
if (codename != null) {
return codename.trimEnd(':', ' ')
}
return null
}
private fun convertClusterCodename(clusterCodename: String): String {
val clusterNameBuilder = StringBuilder()
for (char in clusterCodename) {
val mappedChar = lettersMap[char]
if (mappedChar != null) {
clusterNameBuilder.append(mappedChar)
} else {
Log.w("ClusterCodename", "Character '$char' not found in mapping")
}
}
return clusterNameBuilder.toString()
}
private fun buildAutoGCS(clusterName: String): String {
return "rr1---sn-$clusterName.googlevideo.com"
}
private fun httpGet(urlStr: String): String? {
var connection: HttpURLConnection? = null
return try {
val url = URL(urlStr)
connection = url.openConnection() as HttpURLConnection
connection.requestMethod = "GET"
connection.connectTimeout = 2000
connection.readTimeout = 2000
val responseCode = connection.responseCode
if (responseCode == HttpURLConnection.HTTP_OK) {
val inputStream = connection.inputStream
val reader = BufferedReader(InputStreamReader(inputStream))
val response = reader.readText()
reader.close()
response
} else {
Log.e("HTTPGet", "Non-OK response code: $responseCode")
null
}
} catch (e: Exception) {
Log.e("HTTPGet", "Exception during HTTP GET", e)
null
} finally {
connection?.disconnect()
}
}
}
@@ -1,61 +0,0 @@
package io.github.dovecoteescapee.byedpi.utility
import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.os.Build
import androidx.annotation.StringRes
import androidx.core.app.NotificationCompat
import io.github.dovecoteescapee.byedpi.R
import io.github.dovecoteescapee.byedpi.activities.MainActivity
import io.github.dovecoteescapee.byedpi.data.STOP_ACTION
fun registerNotificationChannel(context: Context, id: String, @StringRes name: Int) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val manager = context.getSystemService(NotificationManager::class.java) ?: return
val channel = NotificationChannel(
id,
context.getString(name),
NotificationManager.IMPORTANCE_DEFAULT
)
channel.enableLights(false)
channel.enableVibration(false)
channel.setShowBadge(false)
manager.createNotificationChannel(channel)
}
}
fun createConnectionNotification(
context: Context,
channelId: String,
@StringRes title: Int,
@StringRes content: Int,
service: Class<*>,
): Notification =
NotificationCompat.Builder(context, channelId)
.setSmallIcon(R.drawable.ic_notification)
.setSilent(true)
.setContentTitle(context.getString(title))
.setContentText(context.getString(content))
.addAction(0, "Stop",
PendingIntent.getService(
context,
0,
Intent(context, service).setAction(STOP_ACTION),
PendingIntent.FLAG_IMMUTABLE,
)
)
.setContentIntent(
PendingIntent.getActivity(
context,
0,
Intent(context, MainActivity::class.java),
PendingIntent.FLAG_IMMUTABLE,
)
)
.build()
@@ -1,51 +0,0 @@
package io.github.dovecoteescapee.byedpi.utility
import android.content.Context
import android.content.SharedPreferences
import androidx.preference.Preference
import androidx.preference.PreferenceFragmentCompat
import androidx.preference.PreferenceManager
import io.github.dovecoteescapee.byedpi.data.Mode
val PreferenceFragmentCompat.sharedPreferences
get() = preferenceScreen.sharedPreferences
fun Context.getPreferences(): SharedPreferences =
PreferenceManager.getDefaultSharedPreferences(this)
fun SharedPreferences.getStringNotNull(key: String, defValue: String): String =
getString(key, defValue) ?: defValue
fun SharedPreferences.mode(): Mode =
Mode.fromString(getStringNotNull("byedpi_mode", "vpn"))
fun <T : Preference> PreferenceFragmentCompat.findPreferenceNotNull(key: CharSequence): T =
findPreference(key) ?: throw IllegalStateException("Preference $key not found")
fun SharedPreferences.getSelectedApps(): List<String> {
return getStringSet("selected_apps", emptySet())?.toList() ?: emptyList()
}
fun SharedPreferences.getProxyIpAndPort(): Pair<String, String> {
val cmdEnable = getBoolean("byedpi_enable_cmd_settings", false)
val cmdArgs = if (cmdEnable) getString("byedpi_cmd_args", "") else null
val args = cmdArgs?.split(" ") ?: emptyList()
fun getArgValue(argsList: List<String>, keys: List<String>): String? {
for (key in keys) {
val index = argsList.indexOf(key)
if (index != -1 && index + 1 < argsList.size) {
return argsList[index + 1]
}
}
return null
}
val cmdIp = getArgValue(args, listOf("-i", "--ip"))
val cmdPort = getArgValue(args, listOf("-p", "--port"))
val ip = cmdIp ?: getStringNotNull("byedpi_proxy_ip", "127.0.0.1")
val port = cmdPort ?: getStringNotNull("byedpi_proxy_port", "1080")
return Pair(ip, port)
}
@@ -0,0 +1,36 @@
package io.github.romanvht.byedpi.activities
import android.os.Build
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import com.google.android.material.appbar.MaterialToolbar
import com.google.android.material.color.DynamicColors
import io.github.romanvht.byedpi.R
import io.github.romanvht.byedpi.utility.SettingsUtils
import io.github.romanvht.byedpi.utility.getPreferences
import io.github.romanvht.byedpi.utility.getStringNotNull
abstract class BaseActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
val prefs = getPreferences()
val lang = prefs.getStringNotNull("language", "system")
SettingsUtils.setLang(lang)
val theme = prefs.getStringNotNull("app_theme", "system")
SettingsUtils.setTheme(theme)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
DynamicColors.applyToActivityIfAvailable(this)
}
super.onCreate(savedInstanceState)
}
protected fun setupToolbar() {
val toolbar: MaterialToolbar = findViewById(R.id.toolbar)
setSupportActionBar(toolbar)
}
}
@@ -0,0 +1,466 @@
package io.github.romanvht.byedpi.activities
import android.Manifest
import android.annotation.SuppressLint
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.content.pm.PackageManager
import android.net.VpnService
import android.os.Build
import android.os.Bundle
import android.util.Log
import android.view.Menu
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import android.widget.ArrayAdapter
import android.widget.ListView
import android.widget.TextView
import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AlertDialog
import androidx.core.content.ContextCompat
import androidx.lifecycle.lifecycleScope
import io.github.romanvht.byedpi.R
import io.github.romanvht.byedpi.data.*
import io.github.romanvht.byedpi.databinding.ActivityMainBinding
import io.github.romanvht.byedpi.services.ServiceManager
import io.github.romanvht.byedpi.services.appStatus
import io.github.romanvht.byedpi.utility.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import java.io.IOException
import kotlin.system.exitProcess
import androidx.core.content.edit
class MainActivity : BaseActivity() {
private lateinit var binding: ActivityMainBinding
private lateinit var historyUtils: HistoryUtils
companion object {
private val TAG: String = MainActivity::class.java.simpleName
private const val BATTERY_OPTIMIZATION_REQUESTED = "battery_optimization_requested"
private fun collectLogs(): String? =
try {
Runtime.getRuntime()
.exec("logcat *:D -d")
.inputStream.bufferedReader()
.use { it.readText() }
} catch (e: Exception) {
Log.e(TAG, "Failed to collect logs", e)
null
}
}
private val vpnRegister =
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
if (it.resultCode == RESULT_OK) {
ServiceManager.start(this, Mode.VPN)
} else {
Toast.makeText(this, R.string.vpn_permission_denied, Toast.LENGTH_SHORT).show()
updateStatus()
}
}
private val logsRegister =
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { log ->
lifecycleScope.launch(Dispatchers.IO) {
val logs = collectLogs()
if (logs == null) {
Toast.makeText(
this@MainActivity,
R.string.logs_failed,
Toast.LENGTH_SHORT
).show()
} else {
val uri = log.data?.data ?: run {
Log.e(TAG, "No data in result")
return@launch
}
contentResolver.openOutputStream(uri)?.use {
try {
it.write(logs.toByteArray())
} catch (e: IOException) {
Log.e(TAG, "Failed to save logs", e)
}
} ?: run {
Log.e(TAG, "Failed to open output stream")
}
}
}
}
private val receiver = object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
Log.d(TAG, "Received intent: ${intent?.action}")
if (intent == null) {
Log.w(TAG, "Received null intent")
return
}
val senderOrd = intent.getIntExtra(SENDER, -1)
val sender = Sender.entries.getOrNull(senderOrd)
if (sender == null) {
Log.w(TAG, "Received intent with unknown sender: $senderOrd")
return
}
when (val action = intent.action) {
STARTED_BROADCAST,
STOPPED_BROADCAST -> updateStatus()
FAILED_BROADCAST -> {
Toast.makeText(
context,
getString(R.string.failed_to_start, sender.name),
Toast.LENGTH_SHORT,
).show()
updateStatus()
}
else -> Log.w(TAG, "Unknown action: $action")
}
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
setupToolbar()
historyUtils = HistoryUtils(this)
val intentFilter = IntentFilter().apply {
addAction(STARTED_BROADCAST)
addAction(STOPPED_BROADCAST)
addAction(FAILED_BROADCAST)
}
@SuppressLint("UnspecifiedRegisterReceiverFlag")
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
registerReceiver(receiver, intentFilter, RECEIVER_EXPORTED)
} else {
registerReceiver(receiver, intentFilter)
}
binding.statusButtonCard.setOnClickListener {
binding.statusButtonCard.isClickable = false
val (status, _) = appStatus
when (status) {
AppStatus.Halted -> start()
AppStatus.Running -> stop()
}
binding.statusButtonCard.postDelayed({
binding.statusButtonCard.isClickable = true
}, 1000)
}
binding.statusButtonCard.setOnFocusChangeListener { _, hasFocus ->
if (hasFocus) {
binding.statusButtonCard.strokeWidth = 10
binding.statusButtonCard.strokeColor = android.graphics.Color.argb(100, 0, 0, 0)
} else {
binding.statusButtonCard.strokeWidth = 0
}
}
binding.settingsButton.setOnClickListener {
val (status, _) = appStatus
if (status == AppStatus.Halted) {
val intent = Intent(this, SettingsActivity::class.java)
startActivity(intent)
} else {
Toast.makeText(this, R.string.settings_unavailable, Toast.LENGTH_SHORT).show()
}
}
binding.editorButton.setOnClickListener {
val useCmdSettings = getPreferences().getCmdEnable()
if (!useCmdSettings && appStatus.first == AppStatus.Running) {
Toast.makeText(this, R.string.settings_unavailable, Toast.LENGTH_SHORT).show()
return@setOnClickListener
}
val intent = Intent(this, SettingsActivity::class.java)
intent.putExtra("open_fragment", if (useCmdSettings) "cmd" else "ui")
startActivity(intent)
}
binding.testProxyButton.setOnClickListener {
startActivity(Intent(this, TestActivity::class.java))
}
binding.domainListsButton.setOnClickListener {
val intent = Intent(this, TestSettingsActivity::class.java)
intent.putExtra("open_fragment", "domain_lists")
startActivity(intent)
}
binding.strategyButton.setOnClickListener {
showStrategyPicker()
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU &&
ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) {
requestPermissions(arrayOf(Manifest.permission.POST_NOTIFICATIONS), 1)
} else {
requestBatteryOptimization()
}
if (getPreferences().getBoolean("auto_connect", false) && appStatus.first != AppStatus.Running) {
this.start()
}
ShortcutUtils.update(this)
}
override fun onResume() {
super.onResume()
updateStatus()
updateStrategyButton()
}
override fun onDestroy() {
super.onDestroy()
unregisterReceiver(receiver)
}
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<out String>,
grantResults: IntArray
) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
if (requestCode == 1) {
requestBatteryOptimization()
}
}
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
menuInflater.inflate(R.menu.menu_main, menu)
return true
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
val (status, _) = appStatus
return when (item.itemId) {
R.id.action_save_logs -> {
val intent =
Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
addCategory(Intent.CATEGORY_OPENABLE)
type = "text/plain"
putExtra(Intent.EXTRA_TITLE, "byedpi.log")
}
logsRegister.launch(intent)
true
}
R.id.action_close_app -> {
if (status == AppStatus.Running) stop()
finishAffinity()
android.os.Process.killProcess(android.os.Process.myPid())
exitProcess(0)
}
else -> super.onOptionsItemSelected(item)
}
}
private fun start() {
when (getPreferences().mode()) {
Mode.VPN -> {
val intentPrepare = VpnService.prepare(this)
if (intentPrepare != null) {
vpnRegister.launch(intentPrepare)
} else {
ServiceManager.start(this, Mode.VPN)
}
}
Mode.Proxy -> ServiceManager.start(this, Mode.Proxy)
}
}
private fun stop() {
ServiceManager.stop(this)
}
private fun updateStatus() {
val (status, mode) = appStatus
Log.i(TAG, "Updating status: $status, $mode")
val preferences = getPreferences()
val (ip, port) = preferences.getProxyIpAndPort()
binding.proxyAddress.text = getString(R.string.proxy_address, ip, port)
when (status) {
AppStatus.Halted -> {
val typedValue = android.util.TypedValue()
theme.resolveAttribute(android.R.attr.colorPrimary, typedValue,true)
binding.statusButtonCard.setCardBackgroundColor(typedValue.data)
binding.statusButtonIcon.clearColorFilter()
when (preferences.mode()) {
Mode.VPN -> {
binding.statusText.setText(R.string.vpn_disconnected)
}
Mode.Proxy -> {
binding.statusText.setText(R.string.proxy_down)
}
}
}
AppStatus.Running -> {
binding.statusButtonCard.setCardBackgroundColor(ContextCompat.getColor(this, R.color.green_active))
binding.statusButtonIcon.setColorFilter(ContextCompat.getColor(this, android.R.color.white))
when (mode) {
Mode.VPN -> {
binding.statusText.setText(R.string.vpn_connected)
}
Mode.Proxy -> {
binding.statusText.setText(R.string.proxy_up)
}
}
}
}
}
private fun updateStrategyButton() {
val useCmdSettings = getPreferences().getCmdEnable()
if (!useCmdSettings) {
binding.cmdButtonsRow.visibility = View.GONE
binding.strategyButton.visibility = View.GONE
return
}
binding.cmdButtonsRow.visibility = View.VISIBLE
val pinned = historyUtils.getPinnedHistory()
val currentCmdArgs = getPreferences().getCmdArgs()
val matched = pinned.find { it.text == currentCmdArgs }
val name = matched?.name?.takeIf { it.isNotBlank() }
if (name != null) {
binding.strategyButtonName.text = name
binding.strategyButtonName.visibility = View.VISIBLE
binding.strategyButtonText.maxLines = 1
} else {
binding.strategyButtonName.visibility = View.GONE
binding.strategyButtonText.maxLines = 2
}
binding.strategyButtonText.text = currentCmdArgs.ifBlank { getString(R.string.main_strategy_picker) }
binding.strategyButton.visibility = View.VISIBLE
}
private fun showStrategyPicker() {
val pinned = historyUtils.getPinnedHistory()
if (pinned.isEmpty()) {
Toast.makeText(this, R.string.main_strategy_no_pinned, Toast.LENGTH_SHORT).show()
return
}
val adapter = object : ArrayAdapter<Command>(
this,
R.layout.item_main_strategy,
pinned
) {
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
val view = convertView ?: layoutInflater.inflate(
R.layout.item_main_strategy,
parent,
false
)
val command = getItem(position)!!
val nameView = view.findViewById<TextView>(R.id.strategyName)
val textView = view.findViewById<TextView>(R.id.strategyText)
val dividerView = view.findViewById<View>(R.id.strategyDivider)
val name = command.name?.takeIf { it.isNotBlank() }
if (name != null) {
nameView.visibility = View.VISIBLE
nameView.text = name
textView.maxLines = 2
} else {
nameView.visibility = View.GONE
textView.maxLines = 3
}
textView.text = command.text
dividerView.visibility = if (position == count - 1) View.GONE else View.VISIBLE
return view
}
}
val listView = ListView(this).apply {
divider = null
setPadding(0, (10 * resources.displayMetrics.density).toInt(), 0, 0)
this.adapter = adapter
}
val dialog = AlertDialog.Builder(this)
.setTitle(getString(R.string.main_strategy_picker))
.setView(listView)
.setNegativeButton(getString(android.R.string.cancel), null)
.create()
listView.setOnItemClickListener { _, _, position, _ ->
applyStrategy(pinned[position].text)
dialog.dismiss()
}
dialog.show()
}
private fun applyStrategy(commandText: String) {
getPreferences().edit { putString("byedpi_cmd_args", commandText) }
updateStrategyButton()
if (appStatus.first == AppStatus.Running) {
val mode = getPreferences().mode()
if (mode == Mode.VPN && VpnService.prepare(this) != null) {
return
}
ServiceManager.restart(this, mode)
Toast.makeText(this, R.string.service_restart, Toast.LENGTH_SHORT).show()
}
}
private fun requestBatteryOptimization() {
val preferences = getPreferences()
val alreadyRequested = preferences.getBoolean(BATTERY_OPTIMIZATION_REQUESTED, false)
if (!alreadyRequested && !BatteryUtils.isOptimizationDisabled(this)) {
BatteryUtils.requestBatteryOptimization(this)
preferences.edit { putBoolean(BATTERY_OPTIMIZATION_REQUESTED, true) }
}
}
}
@@ -0,0 +1,178 @@
package io.github.romanvht.byedpi.activities
import android.os.Bundle
import android.view.Menu
import android.view.MenuItem
import androidx.activity.result.contract.ActivityResultContracts
import androidx.annotation.StringRes
import androidx.appcompat.app.AlertDialog
import io.github.romanvht.byedpi.R
import io.github.romanvht.byedpi.data.AppSettings
import io.github.romanvht.byedpi.fragments.ByeDpiCMDSettingsFragment
import io.github.romanvht.byedpi.fragments.ByeDpiUISettingsFragment
import io.github.romanvht.byedpi.fragments.MainSettingsFragment
import io.github.romanvht.byedpi.utility.SettingsUtils
import io.github.romanvht.byedpi.utility.SettingsUtils.Section
class SettingsActivity : BaseActivity() {
private var exportSections = Section.entries.toSet()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_settings)
setupToolbar()
if (savedInstanceState == null) {
val openFragment = intent.getStringExtra("open_fragment")
when (openFragment) {
"cmd" -> {
supportFragmentManager
.beginTransaction()
.replace(R.id.settings, ByeDpiCMDSettingsFragment())
.commit()
}
"ui" -> {
supportFragmentManager
.beginTransaction()
.replace(R.id.settings, ByeDpiUISettingsFragment())
.commit()
}
else -> {
supportFragmentManager
.beginTransaction()
.replace(R.id.settings, MainSettingsFragment())
.commit()
}
}
}
supportActionBar?.setDisplayHomeAsUpEnabled(true)
}
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
menuInflater.inflate(R.menu.menu_settings, menu)
return true
}
override fun onOptionsItemSelected(item: MenuItem): Boolean = when (item.itemId) {
android.R.id.home -> {
onBackPressedDispatcher.onBackPressed()
true
}
R.id.action_reset_settings -> {
SettingsUtils.resetSettings(this) {
recreate()
}
true
}
R.id.action_export_settings -> {
showSectionSelectionDialog(
title = R.string.export_settings,
availableSections = Section.entries.toSet()
) { sections ->
exportSections = sections
val fileName = "bbd_${System.currentTimeMillis().toReadableDateTime()}.json"
exportSettingsLauncher.launch(fileName)
}
true
}
R.id.action_import_settings -> {
importSettingsLauncher.launch(arrayOf("application/json"))
true
}
else -> super.onOptionsItemSelected(item)
}
private val exportSettingsLauncher = registerForActivityResult(
ActivityResultContracts.CreateDocument("application/json")
) { uri ->
uri?.let {
SettingsUtils.exportSettings(this, it, exportSections)
}
}
private val importSettingsLauncher = registerForActivityResult(
ActivityResultContracts.OpenDocument()
) { uri ->
uri?.let {
val settings = SettingsUtils.readSettings(this, it) ?: return@let
val availableSections = SettingsUtils.getAvailableSections(settings)
if (availableSections.isEmpty()) {
return@let
}
showImportDialog(settings, availableSections)
}
}
private fun showImportDialog(settings: AppSettings, availableSections: Set<Section>) {
showSectionSelectionDialog(
title = R.string.import_settings,
availableSections = availableSections
) { sections ->
SettingsUtils.importSettings(this, settings, sections) {
recreate()
}
}
}
private fun showSectionSelectionDialog(
@StringRes title: Int,
availableSections: Set<Section>,
onConfirmed: (Set<Section>) -> Unit
) {
val sections = Section.entries.filter { it in availableSections }
val checkedItems = BooleanArray(sections.size) { true }
val labels = sections.map { getString(it.label) }.toTypedArray()
lateinit var dialog: AlertDialog
dialog = AlertDialog.Builder(this)
.setTitle(title)
.setMultiChoiceItems(labels, checkedItems) { _, which, isChecked ->
checkedItems[which] = isChecked
dialog.getButton(AlertDialog.BUTTON_POSITIVE).isEnabled = checkedItems.any { it }
}
.setPositiveButton(android.R.string.ok, null)
.setNegativeButton(android.R.string.cancel, null)
.create()
dialog.setOnShowListener {
val positiveButton = dialog.getButton(AlertDialog.BUTTON_POSITIVE)
positiveButton.setOnClickListener {
val selected = sections
.filterIndexed { index, _ -> checkedItems[index] }
.toSet()
if (selected.isNotEmpty()) {
dialog.dismiss()
onConfirmed(selected)
}
}
positiveButton.isEnabled = checkedItems.any { it }
}
dialog.show()
}
@get:StringRes
private val Section.label: Int
get() = when (this) {
Section.SETTINGS -> R.string.backup_settings
Section.HISTORY -> R.string.backup_command_history
Section.APPS -> R.string.backup_selected_apps
Section.DOMAIN_LISTS -> R.string.backup_domain_lists
}
private fun Long.toReadableDateTime(): String {
val format = java.text.SimpleDateFormat("yyyyMMdd_HHmm", java.util.Locale.getDefault())
return format.format(this)
}
}
@@ -0,0 +1,410 @@
package io.github.romanvht.byedpi.activities
import android.content.Intent
import android.net.VpnService
import android.os.Bundle
import android.view.Menu
import android.view.MenuItem
import android.view.View
import android.view.WindowManager
import android.widget.Button
import android.widget.TextView
import android.widget.Toast
import androidx.activity.OnBackPressedCallback
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import io.github.romanvht.byedpi.R
import io.github.romanvht.byedpi.adapters.StrategyResultAdapter
import io.github.romanvht.byedpi.data.Mode
import io.github.romanvht.byedpi.data.AppStatus
import io.github.romanvht.byedpi.data.SiteResult
import io.github.romanvht.byedpi.data.StrategyResult
import io.github.romanvht.byedpi.services.appStatus
import io.github.romanvht.byedpi.services.ServiceManager
import io.github.romanvht.byedpi.utility.HistoryUtils
import io.github.romanvht.byedpi.utility.getPreferences
import io.github.romanvht.byedpi.utility.SiteCheckUtils
import io.github.romanvht.byedpi.utility.getIntStringNotNull
import io.github.romanvht.byedpi.utility.getLongStringNotNull
import androidx.core.content.edit
import io.github.romanvht.byedpi.utility.getStringNotNull
import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
import io.github.romanvht.byedpi.utility.DomainListUtils
import io.github.romanvht.byedpi.utility.getCmdArgs
import io.github.romanvht.byedpi.utility.mode
import kotlinx.coroutines.*
import java.io.File
class TestActivity : BaseActivity() {
private lateinit var strategiesRecyclerView: RecyclerView
private lateinit var progressTextView: TextView
private lateinit var disclaimerTextView: TextView
private lateinit var startStopButton: Button
private lateinit var strategyAdapter: StrategyResultAdapter
private lateinit var siteChecker: SiteCheckUtils
private lateinit var cmdHistoryUtils: HistoryUtils
private lateinit var sites: List<String>
private lateinit var cmds: List<String>
private var savedCmd: String = ""
private var testJob: Job? = null
private val strategies = mutableListOf<StrategyResult>()
private val gson = Gson()
private var isTesting: Boolean
get() = prefs.getBoolean("is_test_running", false)
set(value) {
prefs.edit(commit = true) { putBoolean("is_test_running", value) }
}
private val prefs by lazy { getPreferences() }
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_proxy_test)
setupToolbar()
val ip = prefs.getStringNotNull("byedpi_proxy_ip", "127.0.0.1")
val port = prefs.getIntStringNotNull("byedpi_proxy_port", 1080)
siteChecker = SiteCheckUtils(ip, port)
cmdHistoryUtils = HistoryUtils(this)
strategiesRecyclerView = findViewById(R.id.strategiesRecyclerView)
startStopButton = findViewById(R.id.startStopButton)
progressTextView = findViewById(R.id.progressTextView)
disclaimerTextView = findViewById(R.id.disclaimerTextView)
strategyAdapter = StrategyResultAdapter(this,
onApply = { command ->
addToHistory(command)
}
)
strategiesRecyclerView.layoutManager = LinearLayoutManager(this)
strategiesRecyclerView.adapter = strategyAdapter
lifecycleScope.launch {
val previousResults = loadResults()
if (previousResults.isNotEmpty()) {
progressTextView.text = getString(R.string.test_complete)
disclaimerTextView.visibility = View.GONE
strategies.clear()
strategies.addAll(previousResults)
strategyAdapter.updateStrategies(strategies)
}
if (isTesting) {
progressTextView.text = getString(R.string.test_proxy_error)
disclaimerTextView.text = getString(R.string.test_crash)
disclaimerTextView.visibility = View.VISIBLE
isTesting = false
}
}
startStopButton.setOnClickListener {
startStopButton.isClickable = false
if (isTesting) {
stopTesting()
} else {
startTesting()
}
startStopButton.postDelayed({ startStopButton.isClickable = true }, 1000)
}
onBackPressedDispatcher.addCallback(this, object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
if (isTesting) {
stopTesting()
} else {
if (appStatus.first == AppStatus.Running) {
val intent = Intent(this@TestActivity, MainActivity::class.java)
intent.flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP
startActivity(intent)
}
}
finish()
}
})
supportActionBar?.setDisplayHomeAsUpEnabled(true)
}
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
menuInflater.inflate(R.menu.menu_test, menu)
return true
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return when (item.itemId) {
R.id.action_copy_log -> {
copyLog()
true
}
R.id.action_settings -> {
if (!isTesting) {
val intent = Intent(this, TestSettingsActivity::class.java)
startActivity(intent)
} else {
Toast.makeText(this, R.string.settings_unavailable, Toast.LENGTH_SHORT).show()
}
true
}
android.R.id.home -> {
onBackPressedDispatcher.onBackPressed()
true
}
else -> super.onOptionsItemSelected(item)
}
}
private suspend fun waitForProxyStatus(statusNeeded: AppStatus): Boolean {
val startTime = System.currentTimeMillis()
while (System.currentTimeMillis() - startTime < 3000) {
if (appStatus.first == statusNeeded) {
delay(500)
return true
}
delay(100)
}
return false
}
private suspend fun isProxyRunning(): Boolean = withContext(Dispatchers.IO) {
appStatus.first == AppStatus.Running
}
private fun updateCmdArgs(cmd: String) {
prefs.edit(commit = true) { putString("byedpi_cmd_args", cmd) }
}
private fun startTesting() {
sites = loadSites()
cmds = loadCmds()
if (sites.isEmpty()) {
Toast.makeText(this, R.string.test_settings_domain_empty, Toast.LENGTH_LONG).show()
return
}
testJob = lifecycleScope.launch(Dispatchers.IO) {
isTesting = true
savedCmd = prefs.getCmdArgs()
strategies.clear()
strategies.addAll(cmds.map { StrategyResult(command = it) })
withContext(Dispatchers.Main) {
disclaimerTextView.visibility = View.GONE
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
startStopButton.text = getString(R.string.test_stop)
progressTextView.text = ""
strategyAdapter.setTestingState(true)
strategyAdapter.updateStrategies(strategies, sortByPercentage = false)
}
if (isProxyRunning()) {
ServiceManager.stop(this@TestActivity)
waitForProxyStatus(AppStatus.Halted)
}
val delaySec = prefs.getIntStringNotNull("byedpi_proxytest_delay", 1)
val requestsCount = prefs.getIntStringNotNull("byedpi_proxytest_requests", 1)
val requestTimeout = prefs.getLongStringNotNull("byedpi_proxytest_timeout", 5)
val requestLimit = prefs.getIntStringNotNull("byedpi_proxytest_limit", 20)
for (strategyIndex in strategies.indices) {
if (!isActive) break
val strategy = strategies[strategyIndex]
val cmdIndex = strategyIndex + 1
withContext(Dispatchers.Main) {
progressTextView.text = getString(R.string.test_process, cmdIndex, cmds.size)
}
updateCmdArgs(strategy.command)
if (isProxyRunning()) stopTesting()
else ServiceManager.start(this@TestActivity, Mode.Proxy)
if (!waitForProxyStatus(AppStatus.Running)) {
stopTesting()
}
delay(delaySec * 500L)
val totalRequests = sites.size * requestsCount
strategy.totalRequests = totalRequests
withContext(Dispatchers.Main) {
strategyAdapter.notifyItemChanged(strategyIndex)
}
siteChecker.checkSitesAsync(
sites = sites,
requestsCount = requestsCount,
requestTimeout = requestTimeout,
concurrentRequests = requestLimit,
fullLog = true,
onSiteChecked = { site, successCount, countRequests ->
lifecycleScope.launch(Dispatchers.Main) {
strategy.currentProgress += countRequests
strategy.successCount += successCount
strategy.siteResults.add(SiteResult(site, successCount, countRequests))
strategyAdapter.notifyItemChanged(strategyIndex, "progress")
}
}
)
strategy.isCompleted = true
withContext(Dispatchers.Main) {
strategyAdapter.updateStrategies(strategies, sortByPercentage = true)
saveResults(strategies)
}
if (isProxyRunning()) ServiceManager.stop(this@TestActivity)
else stopTesting()
if (!waitForProxyStatus(AppStatus.Halted)) {
stopTesting()
}
delay(delaySec * 500L)
}
stopTesting()
}
}
private fun stopTesting() {
if (!isTesting) {
return
}
lifecycleScope.launch(Dispatchers.IO) {
isTesting = false
updateCmdArgs(savedCmd)
testJob?.cancel()
testJob = null
if (isProxyRunning()) {
ServiceManager.stop(this@TestActivity)
}
withContext(Dispatchers.Main) {
window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
startStopButton.text = getString(R.string.test_start)
progressTextView.text = getString(R.string.test_complete)
strategyAdapter.setTestingState(false)
strategyAdapter.updateStrategies(strategies, sortByPercentage = true)
saveResults(strategies)
}
}
}
private fun addToHistory(command: String) {
lifecycleScope.launch(Dispatchers.IO) {
updateCmdArgs(command)
cmdHistoryUtils.addCommand(command)
val mode = prefs.mode()
if (mode == Mode.VPN && VpnService.prepare(this@TestActivity) != null) return@launch
val toastText = if (appStatus.first == AppStatus.Running) {
ServiceManager.restart(this@TestActivity, mode)
R.string.service_restart
} else {
R.string.cmd_history_applied
}
withContext(Dispatchers.Main) {
Toast.makeText(this@TestActivity, toastText, Toast.LENGTH_SHORT).show()
}
}
}
private fun saveResults(results: List<StrategyResult>) {
val file = File(filesDir, "proxy_test_results.json")
val json = gson.toJson(results)
file.writeText(json)
}
private fun loadResults(): List<StrategyResult> {
val file = File(filesDir, "proxy_test_results.json")
return if (file.exists()) {
try {
val json = file.readText()
val type = object : TypeToken<List<StrategyResult>>() {}.type
gson.fromJson<List<StrategyResult>>(json, type) ?: emptyList()
} catch (e: Exception) {
emptyList()
}
} else {
emptyList()
}
}
private fun loadSites(): List<String> {
DomainListUtils.syncLists(this)
return DomainListUtils.getActiveDomains(this)
}
private fun loadCmds(): List<String> {
val userCommands = prefs.getBoolean("byedpi_proxytest_usercommands", false)
val sniValue = prefs.getStringNotNull("byedpi_proxytest_sni", "google.com")
return if (userCommands) {
val content = prefs.getStringNotNull("byedpi_proxytest_commands", "")
content.replace("{sni}", "\"${sniValue}\"").lines().map { it.trim() }.filter { it.isNotEmpty() }
} else {
val content = assets.open("proxytest_strategies.list").bufferedReader().readText()
content.replace("{sni}", "\"${sniValue}\"").lines().map { it.trim() }.filter { it.isNotEmpty() }
}
}
private fun copyLog() {
val completeStrategies = strategies.filter { it.isCompleted }
if (completeStrategies.isEmpty()) {
Toast.makeText(this, R.string.toast_copied, Toast.LENGTH_SHORT).show()
return
}
val sb = StringBuilder()
completeStrategies.forEach { strategy ->
sb.appendLine("${strategy.command}\n")
strategy.siteResults.forEach { site ->
sb.appendLine("${site.site} - ${site.successCount}/${site.totalCount}")
}
sb.appendLine("\n${strategy.successCount}/${strategy.totalRequests}")
sb.appendLine("-------------")
}
val clipboard = getSystemService(CLIPBOARD_SERVICE) as android.content.ClipboardManager
val clip = android.content.ClipData.newPlainText("proxy_test_log", sb.toString())
clipboard.setPrimaryClip(clip)
Toast.makeText(this, R.string.toast_copied, Toast.LENGTH_SHORT).show()
}
}
@@ -0,0 +1,44 @@
package io.github.romanvht.byedpi.activities
import android.os.Bundle
import android.view.MenuItem
import io.github.romanvht.byedpi.R
import io.github.romanvht.byedpi.fragments.DomainListsFragment
import io.github.romanvht.byedpi.fragments.ProxyTestSettingsFragment
class TestSettingsActivity : BaseActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_test_settings)
setupToolbar()
if (savedInstanceState == null) {
val openFragment = intent.getStringExtra("open_fragment")
when (openFragment) {
"domain_lists" -> {
supportFragmentManager
.beginTransaction()
.replace(R.id.test_settings, DomainListsFragment())
.commit()
}
else -> {
supportFragmentManager
.beginTransaction()
.replace(R.id.test_settings, ProxyTestSettingsFragment())
.commit()
}
}
}
supportActionBar?.setDisplayHomeAsUpEnabled(true)
}
override fun onOptionsItemSelected(item: MenuItem): Boolean = when (item.itemId) {
android.R.id.home -> {
onBackPressedDispatcher.onBackPressed()
true
}
else -> super.onOptionsItemSelected(item)
}
}
@@ -0,0 +1,118 @@
package io.github.romanvht.byedpi.activities
import android.app.Activity
import android.content.SharedPreferences
import android.net.VpnService
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.util.Log
import androidx.core.content.edit
import io.github.romanvht.byedpi.data.AppStatus
import io.github.romanvht.byedpi.data.Mode
import io.github.romanvht.byedpi.services.ServiceManager
import io.github.romanvht.byedpi.services.appStatus
import io.github.romanvht.byedpi.utility.getCmdArgs
import io.github.romanvht.byedpi.utility.getPreferences
import io.github.romanvht.byedpi.utility.mode
class ToggleActivity : Activity() {
companion object {
private const val TAG = "ToggleServiceActivity"
}
private lateinit var prefs: SharedPreferences
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
prefs = getPreferences()
val strategy = intent.getStringExtra("strategy")
val updated = updateStrategy(strategy)
val onlyUpdate = intent.getBooleanExtra("only_update", false)
val onlyStart = intent.getBooleanExtra("only_start", false)
val onlyStop = intent.getBooleanExtra("only_stop", false)
when {
onlyUpdate -> {
Log.i(TAG, "Only update strategy")
}
onlyStart -> {
val (status) = appStatus
if (status == AppStatus.Halted) {
startService()
} else {
Log.i(TAG, "Service already running")
}
}
onlyStop -> {
val (status) = appStatus
if (status == AppStatus.Running) {
stopService()
} else {
Log.i(TAG, "Service already stopped")
}
}
else -> {
toggleService(updated)
}
}
finish()
}
private fun startService() {
val mode = prefs.mode()
if (mode == Mode.VPN && VpnService.prepare(this) != null) {
return
}
ServiceManager.start(this, mode)
Log.i(TAG, "Toggle service start")
}
private fun restartService() {
val mode = prefs.mode()
if (mode == Mode.VPN && VpnService.prepare(this) != null) {
return
}
ServiceManager.restart(this, mode)
Log.i(TAG, "Toggle service start")
}
private fun stopService() {
ServiceManager.stop(this)
Log.i(TAG, "Toggle service stop")
}
private fun toggleService(restart: Boolean) {
val (status) = appStatus
when (status) {
AppStatus.Halted -> {
startService()
}
AppStatus.Running -> {
if (restart) {
restartService()
} else {
stopService()
}
}
}
}
private fun updateStrategy(strategy: String?): Boolean {
val current = prefs.getCmdArgs()
if (strategy != null && strategy != current) {
prefs.edit(commit = true) { putString("byedpi_cmd_args", strategy) }
Log.i(TAG, "Strategy updated to: $strategy")
return true
}
return false
}
}
@@ -0,0 +1,144 @@
package io.github.romanvht.byedpi.adapters
import android.annotation.SuppressLint
import android.content.Context
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.CheckBox
import android.widget.Filter
import android.widget.Filterable
import android.widget.ImageView
import android.widget.TextView
import androidx.core.content.edit
import androidx.preference.PreferenceManager
import androidx.recyclerview.widget.RecyclerView
import io.github.romanvht.byedpi.R
import io.github.romanvht.byedpi.data.AppInfo
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
class AppSelectionAdapter(
context: Context,
allApps: List<AppInfo>
) : RecyclerView.Adapter<AppSelectionAdapter.ViewHolder>(), Filterable {
private val context = context.applicationContext
private val pm = context.packageManager
private val originalApps: List<AppInfo> = allApps
private val filteredApps: MutableList<AppInfo> = allApps.toMutableList()
private val adapterScope = CoroutineScope(Dispatchers.Main + Job())
class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {
val appIcon: ImageView = view.findViewById(R.id.appIcon)
val appName: TextView = view.findViewById(R.id.appName)
val appPackageName: TextView = view.findViewById(R.id.appPackageName)
val appCheckBox: CheckBox = view.findViewById(R.id.appCheckBox)
var iconLoadJob: Job? = null
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val view = LayoutInflater.from(parent.context).inflate(R.layout.app_selection_item, parent, false)
val holder = ViewHolder(view)
holder.itemView.setOnClickListener {
val position = holder.bindingAdapterPosition
if (position != RecyclerView.NO_POSITION) {
val app = filteredApps[position]
app.isSelected = !app.isSelected
notifyItemChanged(position)
updateSelectedApps()
}
}
return holder
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val app = filteredApps[position]
if (app.appName == app.packageName) {
holder.appName.text = app.packageName
holder.appPackageName.visibility = View.GONE
} else {
holder.appName.text = app.appName
holder.appPackageName.text = app.packageName
holder.appPackageName.visibility = View.VISIBLE
}
holder.appCheckBox.isChecked = app.isSelected
holder.iconLoadJob?.cancel()
if (app.icon != null) {
holder.appIcon.setImageDrawable(app.icon)
} else {
holder.appIcon.setImageDrawable(pm.defaultActivityIcon)
holder.iconLoadJob = adapterScope.launch {
val icon = withContext(Dispatchers.IO) {
try {
pm.getApplicationIcon(app.packageName)
} catch (_: Exception) {
null
}
}
if (icon != null) {
app.icon = icon
holder.appIcon.setImageDrawable(icon)
}
}
}
}
override fun getItemCount(): Int {
return filteredApps.size
}
override fun onViewRecycled(holder: ViewHolder) {
super.onViewRecycled(holder)
holder.iconLoadJob?.cancel()
}
override fun getFilter(): Filter {
return object : Filter() {
override fun performFiltering(constraint: CharSequence?): FilterResults {
val query = constraint?.toString()?.lowercase().orEmpty()
var filteredList = originalApps
if (query.isNotEmpty()) {
filteredList = originalApps.filter {
it.appName.lowercase().contains(query) ||
it.packageName.lowercase().contains(query)
}
}
return FilterResults().apply {
values = filteredList
}
}
@SuppressLint("NotifyDataSetChanged")
override fun publishResults(constraint: CharSequence, results: FilterResults) {
val newList = (results.values as List<*>).filterIsInstance<AppInfo>()
filteredApps.clear()
filteredApps.addAll(newList)
notifyDataSetChanged()
}
}
}
private fun getAllSelectedPackages(): Set<String> {
return originalApps.filter { it.isSelected }.map { it.packageName }.toSet()
}
private fun updateSelectedApps() {
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
val selectedApps = getAllSelectedPackages()
prefs.edit { putStringSet("selected_apps", selectedApps) }
}
}
@@ -0,0 +1,100 @@
package io.github.romanvht.byedpi.adapters
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.CheckBox
import android.widget.LinearLayout
import android.widget.TextView
import androidx.appcompat.app.AlertDialog
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import io.github.romanvht.byedpi.R
import io.github.romanvht.byedpi.data.DomainList
class DomainListAdapter(
private val onStateChanged: (DomainList) -> Unit,
private val onEdit: (DomainList) -> Unit,
private val onDelete: (DomainList) -> Unit,
private val onCopy: (DomainList) -> Unit
) : ListAdapter<DomainList, DomainListAdapter.DomainListViewHolder>(DiffCallback()) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): DomainListViewHolder {
val view = LayoutInflater.from(parent.context).inflate(R.layout.item_domain_list, parent, false)
val holder = DomainListViewHolder(view)
holder.checkbox.setOnClickListener {
val position = holder.bindingAdapterPosition
if (position != RecyclerView.NO_POSITION) {
onStateChanged(getItem(position))
}
}
holder.contentLayout.setOnClickListener {
val position = holder.bindingAdapterPosition
if (position != RecyclerView.NO_POSITION) {
holder.showActionDialog(getItem(position), onEdit, onCopy, onDelete)
}
}
return holder
}
override fun onBindViewHolder(holder: DomainListViewHolder, position: Int) {
holder.bind(getItem(position))
}
class DomainListViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
private val nameText: TextView = itemView.findViewById(R.id.list_name)
private val countText: TextView = itemView.findViewById(R.id.list_count)
val checkbox: CheckBox = itemView.findViewById(R.id.list_checkbox)
val contentLayout: LinearLayout = itemView.findViewById(R.id.list_content)
fun bind(domainList: DomainList) {
nameText.text = domainList.name
val domains = domainList.domains.take(5).joinToString("\n")
val summary = if (domainList.domains.size > 5) "$domains\n..." else domains
countText.text = summary
checkbox.isChecked = domainList.isActive
}
fun showActionDialog(
domainList: DomainList,
onEdit: (DomainList) -> Unit,
onCopy: (DomainList) -> Unit,
onDelete: (DomainList) -> Unit
) {
val context = itemView.context
val options = arrayOf(
context.getString(R.string.domain_list_edit),
context.getString(R.string.cmd_history_copy),
context.getString(R.string.domain_list_delete)
)
AlertDialog.Builder(context, R.style.CustomAlertDialog)
.setTitle(domainList.name)
.setItems(options) { _, which ->
when (which) {
0 -> onEdit(domainList)
1 -> onCopy(domainList)
2 -> onDelete(domainList)
}
}
.setNegativeButton(android.R.string.cancel, null)
.show()
}
}
private class DiffCallback : DiffUtil.ItemCallback<DomainList>() {
override fun areItemsTheSame(oldItem: DomainList, newItem: DomainList): Boolean {
return oldItem.id == newItem.id
}
override fun areContentsTheSame(oldItem: DomainList, newItem: DomainList): Boolean {
return oldItem == newItem
}
}
}
@@ -0,0 +1,43 @@
package io.github.romanvht.byedpi.adapters
import android.annotation.SuppressLint
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
import io.github.romanvht.byedpi.R
import io.github.romanvht.byedpi.data.SiteResult
class SiteResultAdapter : RecyclerView.Adapter<SiteResultAdapter.SiteViewHolder>() {
private val sites = mutableListOf<SiteResult>()
class SiteViewHolder(view: View) : RecyclerView.ViewHolder(view) {
val siteTextView: TextView = view.findViewById(R.id.siteTextView)
val resultTextView: TextView = view.findViewById(R.id.siteResultTextView)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SiteViewHolder {
val view = LayoutInflater.from(parent.context).inflate(R.layout.item_site_result, parent, false)
return SiteViewHolder(view)
}
@SuppressLint("SetTextI18n")
override fun onBindViewHolder(holder: SiteViewHolder, position: Int) {
val site = sites[position]
holder.siteTextView.text = site.site
holder.resultTextView.text = "${site.successCount}/${site.totalCount}"
}
override fun getItemCount(): Int {
return sites.size
}
@SuppressLint("NotifyDataSetChanged")
fun updateSites(newSites: List<SiteResult>) {
sites.clear()
sites.addAll(newSites)
notifyDataSetChanged()
}
}
@@ -0,0 +1,186 @@
package io.github.romanvht.byedpi.adapters
import android.annotation.SuppressLint
import android.content.Context
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.ProgressBar
import android.widget.TextView
import androidx.appcompat.app.AlertDialog
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import io.github.romanvht.byedpi.R
import io.github.romanvht.byedpi.data.StrategyResult
import androidx.core.view.isGone
import androidx.core.view.isVisible
import io.github.romanvht.byedpi.utility.ClipboardUtils
class StrategyResultAdapter(
private val context: Context,
private val onApply: (String) -> Unit
) : RecyclerView.Adapter<StrategyResultAdapter.StrategyViewHolder>() {
private var isTesting = false
private val strategies = mutableListOf<StrategyResult>()
class StrategyViewHolder(view: View) : RecyclerView.ViewHolder(view) {
val commandTextView: TextView = view.findViewById(R.id.commandTextView)
val progressLayout: LinearLayout = view.findViewById(R.id.progressLayout)
val progressBar: ProgressBar = view.findViewById(R.id.progressBar)
val progressTextView: TextView = view.findViewById(R.id.progressTextView)
val expandButton: LinearLayout = view.findViewById(R.id.expandButton)
val expandTextView: TextView = view.findViewById(R.id.expandTextView)
val expandIcon: ImageView = view.findViewById(R.id.expandIcon)
val sitesRecyclerView: RecyclerView = view.findViewById(R.id.sitesRecyclerView)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): StrategyViewHolder {
val view = LayoutInflater.from(parent.context).inflate(R.layout.item_strategy_result, parent, false)
val holder = StrategyViewHolder(view)
holder.commandTextView.setOnClickListener {
val position = holder.bindingAdapterPosition
if (position != RecyclerView.NO_POSITION) {
showCommandMenu(strategies[position].command)
}
}
holder.expandButton.setOnClickListener {
val position = holder.bindingAdapterPosition
if (position != RecyclerView.NO_POSITION) {
strategies[position].isExpanded = !strategies[position].isExpanded
notifyItemChanged(position)
}
}
return holder
}
@SuppressLint("SetTextI18n")
override fun onBindViewHolder(holder: StrategyViewHolder, position: Int) {
val strategy = strategies[position]
holder.commandTextView.text = strategy.command
if (strategy.totalRequests > 0) {
holder.progressLayout.visibility = View.VISIBLE
holder.progressTextView.visibility = View.VISIBLE
val isProgress = isTesting && !strategy.isCompleted
if (isProgress) {
holder.progressBar.isIndeterminate = true
holder.progressTextView.text = "${strategy.successCount}/${strategy.totalRequests}"
} else {
holder.progressBar.isIndeterminate = false
holder.progressBar.max = strategy.totalRequests
holder.progressBar.progress = strategy.successCount
holder.progressTextView.text = "${strategy.successCount}/${strategy.totalRequests}"
}
} else {
holder.progressLayout.visibility = View.GONE
}
if (strategy.siteResults.isNotEmpty()) {
holder.expandButton.visibility = View.VISIBLE
if (strategy.isExpanded) {
holder.expandTextView.text = context.getString(R.string.test_hide_details)
holder.expandIcon.rotation = 180f
holder.sitesRecyclerView.visibility = View.VISIBLE
val siteAdapter = SiteResultAdapter()
holder.sitesRecyclerView.layoutManager = LinearLayoutManager(context)
holder.sitesRecyclerView.adapter = siteAdapter
siteAdapter.updateSites(strategy.siteResults)
} else {
holder.expandTextView.text = context.getString(R.string.test_show_details)
holder.expandIcon.rotation = 0f
holder.sitesRecyclerView.visibility = View.GONE
}
} else {
holder.expandButton.visibility = View.GONE
holder.sitesRecyclerView.visibility = View.GONE
}
}
@SuppressLint("SetTextI18n")
override fun onBindViewHolder(holder: StrategyViewHolder, position: Int, payloads: MutableList<Any>) {
if (payloads.isEmpty()) {
super.onBindViewHolder(holder, position, payloads)
} else {
val strategy = strategies[position]
if (strategy.totalRequests > 0) {
val isProgress = isTesting && !strategy.isCompleted
if (isProgress) {
holder.progressBar.isIndeterminate = true
holder.progressTextView.text = "${strategy.successCount}/${strategy.totalRequests}"
} else {
holder.progressBar.isIndeterminate = false
holder.progressBar.max = strategy.totalRequests
holder.progressBar.progress = strategy.successCount
holder.progressTextView.text = "${strategy.successCount}/${strategy.totalRequests}"
}
}
if (strategy.siteResults.isNotEmpty() && holder.expandButton.isGone) {
holder.expandButton.visibility = View.VISIBLE
}
if (strategy.isExpanded && holder.sitesRecyclerView.isVisible) {
val adapter = holder.sitesRecyclerView.adapter
if (adapter is SiteResultAdapter) {
adapter.updateSites(strategy.siteResults)
}
}
}
}
override fun getItemCount(): Int {
return strategies.size
}
@SuppressLint("NotifyDataSetChanged")
fun setTestingState(testing: Boolean) {
isTesting = testing
notifyDataSetChanged()
}
@SuppressLint("NotifyDataSetChanged")
fun updateStrategies(newStrategies: List<StrategyResult>, sortByPercentage: Boolean = true) {
strategies.clear()
val sorted = if (sortByPercentage) {
newStrategies.sortedWith(compareByDescending<StrategyResult> { it.isCompleted }
.thenByDescending { it.successPercentage }
.thenByDescending { it.successCount })
} else {
newStrategies
}
strategies.addAll(sorted)
notifyDataSetChanged()
}
private fun showCommandMenu(command: String) {
val menuItems = arrayOf(
context.getString(R.string.cmd_history_apply),
context.getString(R.string.cmd_history_copy)
)
AlertDialog.Builder(context)
.setTitle(context.getString(R.string.cmd_history_menu))
.setItems(menuItems) { _, which ->
when (which) {
0 -> onApply(command)
1 -> ClipboardUtils.copy(context, command, "command")
}
}
.show()
}
}
@@ -0,0 +1,28 @@
package io.github.romanvht.byedpi.core
class ByeDpiProxy {
companion object {
init {
System.loadLibrary("byedpi")
}
}
fun startProxy(preferences: ByeDpiProxyPreferences): Int {
val args = prepareArgs(preferences)
return jniStartProxy(args)
}
fun stopProxy(): Int {
return jniStopProxy()
}
private fun prepareArgs(preferences: ByeDpiProxyPreferences): Array<String> =
when (preferences) {
is ByeDpiProxyCmdPreferences -> preferences.args
is ByeDpiProxyUIPreferences -> preferences.uiargs
}
private external fun jniStartProxy(args: Array<String>): Int
private external fun jniStopProxy(): Int
external fun jniForceClose(): Int
}
@@ -0,0 +1,164 @@
package io.github.romanvht.byedpi.core
import android.content.Context
import android.content.SharedPreferences
import android.util.Log
import io.github.romanvht.byedpi.data.UISettings
import io.github.romanvht.byedpi.utility.DomainListUtils
import io.github.romanvht.byedpi.utility.checkIpAndPortInCmd
import io.github.romanvht.byedpi.utility.getCmdArgs
import io.github.romanvht.byedpi.utility.getCmdEnable
import io.github.romanvht.byedpi.utility.getStringNotNull
import io.github.romanvht.byedpi.utility.shellSplit
sealed interface ByeDpiProxyPreferences {
companion object {
fun fromSharedPreferences(preferences: SharedPreferences, context: Context): ByeDpiProxyPreferences =
when (preferences.getCmdEnable()) {
true -> ByeDpiProxyCmdPreferences(preferences, context)
false -> ByeDpiProxyUIPreferences(preferences)
}
}
}
class ByeDpiProxyCmdPreferences(val args: Array<String>) : ByeDpiProxyPreferences {
constructor(preferences: SharedPreferences, context: Context) : this(
parseCmdToArguments(preferences, context)
)
companion object {
private fun parseCmdToArguments(preferences: SharedPreferences, context: Context): Array<String> {
val cmd = preferences.getCmdArgs()
val preparedCmd = getLists(cmd, context)
val firstArgIndex = preparedCmd.indexOf("-")
val args = (if (firstArgIndex > 0) preparedCmd.substring(firstArgIndex) else preparedCmd).trim()
Log.d("ProxyPref", "CMD: $args")
val (cmdIp, cmdPort) = preferences.checkIpAndPortInCmd()
val ip = preferences.getStringNotNull("byedpi_proxy_ip", "127.0.0.1")
val port = preferences.getStringNotNull("byedpi_proxy_port", "1080")
val prefix = buildString {
if (cmdIp == null) append("--ip $ip ")
if (cmdPort == null) append("--port $port ")
}
Log.d("ProxyPref", "Added from settings: $prefix")
val blacklist = setOf("--help", "--version", "-h", "-v")
val splitArgs = shellSplit("$prefix$args").filter { it !in blacklist }
return arrayOf("ciadpi") + splitArgs
}
private fun getLists(cmd: String, context: Context): String {
return Regex("""\{list:([^}]+)\}""").replace(cmd) { matchResult ->
val listName = matchResult.groupValues[1].trim()
val lists = DomainListUtils.getLists(context)
val domainList = lists.find { it.name.equals(listName, ignoreCase = true) }
if (domainList != null && domainList.domains.isNotEmpty()) {
domainList.domains.joinToString(" ")
} else {
Log.w("ProxyPref", "List '$listName' not found or empty")
""
}
}
}
}
}
class ByeDpiProxyUIPreferences(val settings: UISettings = UISettings()) : ByeDpiProxyPreferences {
constructor(preferences: SharedPreferences) : this(
UISettings.fromSharedPreferences(preferences)
)
val uiargs: Array<String>
get() {
val args = mutableListOf("ciadpi")
if (settings.ip.isNotEmpty()) args.add("-i${settings.ip}")
if (settings.port != 0) args.add("-p${settings.port}")
if (settings.maxConnections != 0) args.add("-c${settings.maxConnections}")
if (settings.bufferSize != 0) args.add("-b${settings.bufferSize}")
val protocols = buildList {
if (settings.desyncHttps) add("t")
if (settings.desyncHttp) add("h")
}
if (!settings.hosts.isNullOrBlank()) {
val hostStr = ":${settings.hosts.replace("\n", " ")}"
when (settings.hostsMode) {
UISettings.HostsMode.Blacklist -> {
args.add("-H$hostStr")
args.add("-An")
if (protocols.isNotEmpty()) args.add("-K${protocols.joinToString(",")}")
}
UISettings.HostsMode.Whitelist -> {
if (protocols.isNotEmpty()) args.add("-K${protocols.joinToString(",")}")
args.add("-H$hostStr")
}
else -> {}
}
} else {
if (protocols.isNotEmpty()) args.add("-K${protocols.joinToString(",")}")
}
if (settings.defaultTtl != 0) args.add("-g${settings.defaultTtl}")
if (settings.noDomain) args.add("-N")
if (settings.splitPosition != 0) {
val posArg = settings.splitPosition.toString() + if (settings.splitAtHost) "+h" else ""
val option = when (settings.desyncMethod) {
UISettings.DesyncMethod.Split -> "-s"
UISettings.DesyncMethod.Disorder -> "-d"
UISettings.DesyncMethod.OOB -> "-o"
UISettings.DesyncMethod.DISOOB -> "-q"
UISettings.DesyncMethod.Fake -> "-f"
UISettings.DesyncMethod.None -> ""
}
if (option.isNotEmpty()) args.add("$option$posArg")
}
if (settings.desyncMethod == UISettings.DesyncMethod.Fake) {
if (settings.fakeTtl != 0) args.add("-t${settings.fakeTtl}")
if (settings.fakeSni.isNotEmpty()) args.add("-n${settings.fakeSni}")
if (settings.fakeOffset != 0) args.add("-O${settings.fakeOffset}")
}
if (settings.desyncMethod == UISettings.DesyncMethod.OOB || settings.desyncMethod == UISettings.DesyncMethod.DISOOB) {
args.add("-e${settings.oobChar[0].code.toByte()}")
}
val modHttpFlags = buildList {
if (settings.hostMixedCase) add("h")
if (settings.domainMixedCase) add("d")
if (settings.hostRemoveSpaces) add("r")
}
if (modHttpFlags.isNotEmpty()) args.add("-M${modHttpFlags.joinToString(",")}")
if (settings.tlsRecordSplit && settings.tlsRecordSplitPosition != 0) {
val tlsRecArg = settings.tlsRecordSplitPosition.toString() + if (settings.tlsRecordSplitAtSni) "+s" else ""
args.add("-r$tlsRecArg")
}
if (settings.tcpFastOpen) args.add("-F")
if (settings.dropSack) args.add("-Y")
args.add("-An")
if (settings.desyncUdp) {
args.add("-Ku")
if (settings.udpFakeCount != 0) args.add("-a${settings.udpFakeCount}")
args.add("-An")
}
Log.d("ProxyPref", "UI to cmd: ${args.joinToString(" ")}")
return args.toTypedArray()
}
}
@@ -1,4 +1,4 @@
package io.github.dovecoteescapee.byedpi.core
package io.github.romanvht.byedpi.core
object TProxyService {
init {
@@ -0,0 +1,6 @@
package io.github.romanvht.byedpi.data
const val START_ACTION = "start"
const val STOP_ACTION = "stop"
const val RESUME_ACTION = "resume"
const val PAUSE_ACTION = "pause"
@@ -1,10 +1,10 @@
package io.github.dovecoteescapee.byedpi.data
package io.github.romanvht.byedpi.data
import android.graphics.drawable.Drawable
data class AppInfo(
val appName: String,
val packageName: String,
val icon: Drawable,
var isSelected: Boolean
var isSelected: Boolean,
var icon: Drawable? = null
)
@@ -0,0 +1,10 @@
package io.github.romanvht.byedpi.data
data class AppSettings(
val app: String,
val version: String,
val history: List<Command>?,
val apps: List<String>?,
val domainLists: List<DomainList>?,
val settings: Map<String, Any?>?
)
@@ -1,4 +1,4 @@
package io.github.dovecoteescapee.byedpi.data
package io.github.romanvht.byedpi.data
enum class AppStatus {
Halted,
@@ -0,0 +1,14 @@
package io.github.romanvht.byedpi.data
import io.github.romanvht.byedpi.BuildConfig
const val STARTED_BROADCAST = "${BuildConfig.APPLICATION_ID}.STARTED"
const val STOPPED_BROADCAST = "${BuildConfig.APPLICATION_ID}.STOPPED"
const val FAILED_BROADCAST = "${BuildConfig.APPLICATION_ID}.FAILED"
const val SENDER = "sender"
enum class Sender(val senderName: String) {
Proxy("Proxy"),
VPN("VPN")
}
@@ -1,7 +1,7 @@
package io.github.dovecoteescapee.byedpi.data
package io.github.romanvht.byedpi.data
data class Command(
val text: String,
var text: String,
var pinned: Boolean = false,
var name: String? = null
)
@@ -0,0 +1,12 @@
package io.github.romanvht.byedpi.data
data class DomainList(
val id: String,
val name: String,
val domains: List<String>,
val isActive: Boolean = true,
val isBuiltIn: Boolean = false,
val isModified: Boolean = false,
val isDeleted: Boolean = false
)
@@ -1,4 +1,4 @@
package io.github.dovecoteescapee.byedpi.data
package io.github.romanvht.byedpi.data
enum class ServiceStatus {
Disconnected,
@@ -0,0 +1,20 @@
package io.github.romanvht.byedpi.data
data class StrategyResult(
val command: String,
var successCount: Int = 0,
var totalRequests: Int = 0,
var currentProgress: Int = 0,
var isCompleted: Boolean = false,
val siteResults: MutableList<SiteResult> = mutableListOf(),
var isExpanded: Boolean = false
) {
val successPercentage: Int
get() = if (totalRequests > 0) (successCount * 100) / totalRequests else 0
}
data class SiteResult(
val site: String,
val successCount: Int,
val totalCount: Int
)
@@ -0,0 +1,104 @@
package io.github.romanvht.byedpi.data
import android.content.SharedPreferences
data class UISettings(
val ip: String = "127.0.0.1",
val port: Int = 1080,
val maxConnections: Int = 512,
val bufferSize: Int = 16384,
val defaultTtl: Int = 0,
val noDomain: Boolean = false,
val desyncHttp: Boolean = true,
val desyncHttps: Boolean = true,
val desyncUdp: Boolean = true,
val desyncMethod: DesyncMethod = DesyncMethod.OOB,
val splitPosition: Int = 1,
val splitAtHost: Boolean = false,
val fakeTtl: Int = 8,
val fakeSni: String = "www.iana.org",
val oobChar: String = "a",
val hostMixedCase: Boolean = false,
val domainMixedCase: Boolean = false,
val hostRemoveSpaces: Boolean = false,
val tlsRecordSplit: Boolean = false,
val tlsRecordSplitPosition: Int = 0,
val tlsRecordSplitAtSni: Boolean = false,
val hostsMode: HostsMode = HostsMode.Disable,
val hosts: String? = null,
val tcpFastOpen: Boolean = false,
val udpFakeCount: Int = 1,
val dropSack: Boolean = false,
val fakeOffset: Int = 0,
) {
enum class DesyncMethod {
None, Split, Disorder, Fake, OOB, DISOOB;
companion object {
fun fromName(name: String): DesyncMethod = when (name) {
"none" -> None
"split" -> Split
"disorder" -> Disorder
"fake" -> Fake
"oob" -> OOB
"disoob" -> DISOOB
else -> throw IllegalArgumentException("Unknown desync method: $name")
}
}
}
enum class HostsMode {
Disable, Blacklist, Whitelist;
companion object {
fun fromName(name: String): HostsMode = when (name) {
"disable" -> Disable
"blacklist" -> Blacklist
"whitelist" -> Whitelist
else -> throw IllegalArgumentException("Unknown hosts mode: $name")
}
}
}
companion object {
fun fromSharedPreferences(preferences: SharedPreferences): UISettings {
val hostsMode = preferences.getString("byedpi_hosts_mode", null) ?.let { HostsMode.fromName(it) } ?: HostsMode.Disable
val hosts = when (hostsMode) {
HostsMode.Blacklist -> preferences.getString("byedpi_hosts_blacklist", null)
HostsMode.Whitelist -> preferences.getString("byedpi_hosts_whitelist", null)
else -> null
}
return UISettings(
ip = preferences.getString("byedpi_proxy_ip", null) ?: "127.0.0.1",
port = preferences.getString("byedpi_proxy_port", null)?.toIntOrNull() ?: 1080,
maxConnections = preferences.getString("byedpi_max_connections", null)?.toIntOrNull() ?: 512,
bufferSize = preferences.getString("byedpi_buffer_size", null)?.toIntOrNull() ?: 16384,
defaultTtl = preferences.getString("byedpi_default_ttl", null)?.toIntOrNull() ?: 0,
noDomain = preferences.getBoolean("byedpi_no_domain", false),
desyncHttp = preferences.getBoolean("byedpi_desync_http", true),
desyncHttps = preferences.getBoolean("byedpi_desync_https", true),
desyncUdp = preferences.getBoolean("byedpi_desync_udp", true),
desyncMethod = preferences.getString("byedpi_desync_method", null) ?.let { DesyncMethod.fromName(it) } ?: DesyncMethod.OOB,
splitPosition = preferences.getString("byedpi_split_position", null)?.toIntOrNull() ?: 1,
splitAtHost = preferences.getBoolean("byedpi_split_at_host", false),
fakeTtl = preferences.getString("byedpi_fake_ttl", null)?.toIntOrNull() ?: 8,
fakeSni = preferences.getString("byedpi_fake_sni", null) ?: "www.iana.org",
oobChar = preferences.getString("byedpi_oob_data", null) ?: "a",
hostMixedCase = preferences.getBoolean("byedpi_host_mixed_case", false),
domainMixedCase = preferences.getBoolean("byedpi_domain_mixed_case", false),
hostRemoveSpaces = preferences.getBoolean("byedpi_host_remove_spaces", false),
tlsRecordSplit = preferences.getBoolean("byedpi_tlsrec_enabled", false),
tlsRecordSplitPosition = preferences.getString("byedpi_tlsrec_position", null)?.toIntOrNull() ?: 0,
tlsRecordSplitAtSni = preferences.getBoolean("byedpi_tlsrec_at_sni", false),
tcpFastOpen = preferences.getBoolean("byedpi_tcp_fast_open", false),
udpFakeCount = preferences.getString("byedpi_udp_fake_count", null)?.toIntOrNull() ?: 1,
dropSack = preferences.getBoolean("byedpi_drop_sack", false),
fakeOffset = preferences.getString("byedpi_fake_offset", null)?.toIntOrNull() ?: 0,
hostsMode = hostsMode,
hosts = hosts,
)
}
}
}
@@ -1,5 +1,8 @@
package io.github.dovecoteescapee.byedpi.fragments
package io.github.romanvht.byedpi.fragments
import android.content.SharedPreferences
import android.content.pm.ApplicationInfo
import android.content.pm.PackageManager
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
@@ -11,10 +14,12 @@ import androidx.lifecycle.lifecycleScope
import androidx.preference.PreferenceManager
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import io.github.dovecoteescapee.byedpi.R
import io.github.dovecoteescapee.byedpi.adapters.AppSelectionAdapter
import io.github.dovecoteescapee.byedpi.data.AppInfo
import io.github.romanvht.byedpi.R
import io.github.romanvht.byedpi.adapters.AppSelectionAdapter
import io.github.romanvht.byedpi.data.AppInfo
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
@@ -23,9 +28,15 @@ class AppSelectionFragment : Fragment() {
private lateinit var searchView: SearchView
private lateinit var progressBar: ProgressBar
private lateinit var adapter: AppSelectionAdapter
private lateinit var prefs: SharedPreferences
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val view = inflater.inflate(R.layout.app_selection, container, false)
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
val view = inflater.inflate(R.layout.app_selection_layout, container, false)
prefs = PreferenceManager.getDefaultSharedPreferences(requireContext())
recyclerView = view.findViewById(R.id.recyclerView)
searchView = view.findViewById(R.id.searchView)
@@ -33,7 +44,6 @@ class AppSelectionFragment : Fragment() {
setupRecyclerView()
setupSearchView()
loadApps()
return view
@@ -62,49 +72,52 @@ class AppSelectionFragment : Fragment() {
private fun loadApps() {
progressBar.visibility = View.VISIBLE
searchView.visibility = View.GONE
lifecycleScope.launch {
val apps = withContext(Dispatchers.IO) {
getInstalledApps()
}
adapter = AppSelectionAdapter(apps) { app, isChecked ->
updateSelectedApps(app.packageName, isChecked)
getInstalledAppsAsync()
}
adapter = AppSelectionAdapter(requireContext(), apps)
recyclerView.adapter = adapter
progressBar.visibility = View.GONE
searchView.visibility = View.VISIBLE
}
}
private fun getInstalledApps(): List<AppInfo> {
private suspend fun getInstalledAppsAsync(): List<AppInfo> {
val pm = requireContext().packageManager
val installedApps = pm.getInstalledApplications(0)
val selectedApps = PreferenceManager.getDefaultSharedPreferences(requireContext())
.getStringSet("selected_apps", setOf()) ?: setOf()
val selectedApps = prefs.getStringSet("selected_apps", setOf()) ?: setOf()
return installedApps
.filter { it.packageName != requireContext().packageName }
.map {
AppInfo(
pm.getApplicationLabel(it).toString(),
it.packageName,
pm.getApplicationIcon(it.packageName),
selectedApps.contains(it.packageName)
)
.map { appInfo ->
lifecycleScope.async(Dispatchers.IO) {
createAppInfo(appInfo, pm, selectedApps)
}
}
.awaitAll()
.sortedWith(compareBy({ !it.isSelected }, { it.appName.lowercase() }))
}
private fun updateSelectedApps(packageName: String, isSelected: Boolean) {
val prefs = PreferenceManager.getDefaultSharedPreferences(requireContext())
val selectedApps = prefs.getStringSet("selected_apps", mutableSetOf())?.toMutableSet() ?: mutableSetOf()
if (isSelected) {
selectedApps.add(packageName)
} else {
selectedApps.remove(packageName)
private fun createAppInfo(
appInfo: ApplicationInfo,
pm: PackageManager,
selectedApps: Set<String>
): AppInfo {
val appName = try {
pm.getApplicationLabel(appInfo).toString()
} catch (_: Exception) {
appInfo.packageName
}
prefs.edit().putStringSet("selected_apps", selectedApps).apply()
return AppInfo(
appName,
appInfo.packageName,
selectedApps.contains(appInfo.packageName),
icon = null
)
}
}
@@ -0,0 +1,286 @@
package io.github.romanvht.byedpi.fragments
import android.net.VpnService
import android.os.Bundle
import android.view.View
import android.widget.EditText
import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.TextView
import android.widget.Toast
import androidx.preference.*
import io.github.romanvht.byedpi.R
import io.github.romanvht.byedpi.utility.findPreferenceNotNull
import androidx.appcompat.app.AlertDialog
import io.github.romanvht.byedpi.data.AppStatus
import io.github.romanvht.byedpi.data.Command
import io.github.romanvht.byedpi.data.Mode
import io.github.romanvht.byedpi.services.ServiceManager
import io.github.romanvht.byedpi.services.appStatus
import io.github.romanvht.byedpi.utility.ClipboardUtils
import io.github.romanvht.byedpi.utility.HistoryUtils
import io.github.romanvht.byedpi.utility.getCmdArgs
import io.github.romanvht.byedpi.utility.getPreferences
import io.github.romanvht.byedpi.utility.mode
class ByeDpiCMDSettingsFragment : PreferenceFragmentCompat() {
private lateinit var cmdHistoryUtils: HistoryUtils
private lateinit var editTextPreference: EditTextPreference
private lateinit var historyHeader: Preference
private val historyPreferences = mutableListOf<Preference>()
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
setPreferencesFromResource(R.xml.byedpi_cmd_settings, rootKey)
cmdHistoryUtils = HistoryUtils(requireContext())
editTextPreference = findPreferenceNotNull("byedpi_cmd_args")
editTextPreference.text = requireContext().getPreferences().getCmdArgs()
historyHeader = findPreferenceNotNull("cmd_history_header")
editTextPreference.setOnPreferenceChangeListener { _, newValue ->
val newCommand = newValue.toString()
if (newCommand.isNotBlank()) {
cmdHistoryUtils.addCommand(newCommand)
restartService()
}
updateHistoryItems()
true
}
historyHeader.setOnPreferenceClickListener {
showHistoryClearDialog()
true
}
updateHistoryItems()
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
view.post {
view.findViewById<View>(R.id.btn_clear)?.setOnClickListener {
editTextPreference.text = ""
updateHistoryItems()
}
view.findViewById<View>(R.id.btn_paste)?.setOnClickListener {
val text = ClipboardUtils.paste(requireContext())
if (!text.isNullOrBlank()) {
editTextPreference.text = text
cmdHistoryUtils.addCommand(text)
updateHistoryItems()
}
}
}
}
private fun updateHistoryItems() {
if (listView !== null) {
listView.itemAnimator = null
}
historyPreferences.forEach { preference ->
preferenceScreen.removePreference(preference)
}
historyPreferences.clear()
val history = cmdHistoryUtils.getHistory()
historyHeader.isVisible = history.isNotEmpty()
if (history.isNotEmpty()) {
history.sortedWith(compareByDescending<Command> { it.pinned }.thenBy { history.indexOf(it) })
.forEachIndexed { _, command ->
val preference = createPreference(command)
historyPreferences.add(preference)
preferenceScreen.addPreference(preference)
}
}
}
private fun createPreference(command: Command) =
object : Preference(requireContext()) {
override fun onBindViewHolder(holder: PreferenceViewHolder) {
super.onBindViewHolder(holder)
val nameView = holder.itemView.findViewById<TextView>(R.id.command_name)
val summaryView = holder.itemView.findViewById<TextView>(android.R.id.summary)
val pinIcon = holder.itemView.findViewById<ImageView>(R.id.pin_icon)
val commandName = command.name
if (!commandName.isNullOrBlank()) {
nameView.visibility = View.VISIBLE
nameView.text = command.name
} else {
nameView.visibility = View.GONE
}
val summaryText = buildSummary(command)
if (summaryText.isNotBlank()) {
summaryView.visibility = View.VISIBLE
summaryView.text = summaryText
} else {
summaryView.visibility = View.GONE
}
pinIcon.visibility = if (command.pinned) View.VISIBLE else View.GONE
}
}.apply {
title = command.text
layoutResource = R.layout.history_item
setOnPreferenceClickListener {
showActionDialog(command)
true
}
}
private fun buildSummary(command: Command): String {
val parts = mutableListOf<String>()
if (command.text == editTextPreference.text) {
parts.add(getString(R.string.cmd_history_applied))
}
return parts.joinToString("")
}
private fun showHistoryClearDialog() {
val options = arrayOf(
getString(R.string.cmd_history_delete_unpinned),
getString(R.string.cmd_history_delete_all),
)
AlertDialog.Builder(requireContext())
.setTitle(getString(R.string.cmd_history_menu))
.setItems(options) { _, which ->
when (which) {
0 -> deleteUnpinnedHistory()
1 -> deleteAllHistory()
}
}
.setNegativeButton(getString(android.R.string.cancel), null)
.show()
}
private fun deleteAllHistory() {
cmdHistoryUtils.clearAllHistory()
updateHistoryItems()
}
private fun deleteUnpinnedHistory() {
cmdHistoryUtils.clearUnpinnedHistory()
updateHistoryItems()
}
private fun showActionDialog(command: Command) {
val options = arrayOf(
getString(R.string.cmd_history_apply),
if (command.pinned) getString(R.string.cmd_history_unpin) else getString(R.string.cmd_history_pin),
getString(R.string.cmd_history_rename),
getString(R.string.cmd_history_edit),
getString(R.string.cmd_history_copy),
getString(R.string.cmd_history_delete)
)
AlertDialog.Builder(requireContext())
.setTitle(getString(R.string.cmd_history_menu))
.setItems(options) { _, which ->
when (which) {
0 -> applyCommand(command.text)
1 -> if (command.pinned) unpinCommand(command.text) else pinCommand(command.text)
2 -> showRenameDialog(command)
3 -> showEditDialog(command)
4 -> ClipboardUtils.copy(requireContext(), command.text, "command")
5 -> deleteCommand(command.text)
}
}
.show()
}
private fun showRenameDialog(command: Command) {
val input = EditText(requireContext()).apply {
setText(command.name)
}
val container = LinearLayout(context).apply {
orientation = LinearLayout.VERTICAL
setPadding(50, 20, 50, 20)
addView(input)
}
AlertDialog.Builder(requireContext())
.setTitle(getString(R.string.cmd_history_rename))
.setView(container)
.setPositiveButton(getString(android.R.string.ok)) { _, _ ->
val newName = input.text.toString()
if (newName != command.name) {
cmdHistoryUtils.renameCommand(command.text, newName)
updateHistoryItems()
}
}
.setNegativeButton(getString(android.R.string.cancel), null)
.show()
}
private fun showEditDialog(command: Command) {
val input = EditText(requireContext()).apply {
setText(command.text)
}
val container = LinearLayout(context).apply {
orientation = LinearLayout.VERTICAL
setPadding(50, 20, 50, 20)
addView(input)
}
AlertDialog.Builder(requireContext())
.setTitle(getString(R.string.cmd_history_edit))
.setView(container)
.setPositiveButton(getString(android.R.string.ok)) { _, _ ->
val newText = input.text.toString()
if (newText.isNotBlank() && newText != command.text) {
cmdHistoryUtils.editCommand(command.text, newText)
if (editTextPreference.text == command.text) applyCommand(newText)
updateHistoryItems()
}
}
.setNegativeButton(getString(android.R.string.cancel), null)
.show()
}
private fun applyCommand(command: String) {
editTextPreference.text = command
updateHistoryItems()
restartService()
}
private fun pinCommand(command: String) {
cmdHistoryUtils.pinCommand(command)
updateHistoryItems()
}
private fun unpinCommand(command: String) {
cmdHistoryUtils.unpinCommand(command)
updateHistoryItems()
}
private fun deleteCommand(command: String) {
cmdHistoryUtils.deleteCommand(command)
updateHistoryItems()
}
private fun restartService() {
if (appStatus.first == AppStatus.Running) {
val ctx = context ?: return
val mode = ctx.getPreferences().mode()
if (mode == Mode.VPN && VpnService.prepare(ctx) != null) return
ServiceManager.restart(ctx, mode)
Toast.makeText(ctx, R.string.service_restart, Toast.LENGTH_SHORT).show()
}
}
}
@@ -1,13 +1,13 @@
package io.github.dovecoteescapee.byedpi.fragments
package io.github.romanvht.byedpi.fragments
import android.content.SharedPreferences
import android.os.Bundle
import androidx.preference.*
import io.github.dovecoteescapee.byedpi.R
import io.github.dovecoteescapee.byedpi.core.ByeDpiProxyUIPreferences
import io.github.dovecoteescapee.byedpi.core.ByeDpiProxyUIPreferences.DesyncMethod.*
import io.github.dovecoteescapee.byedpi.core.ByeDpiProxyUIPreferences.HostsMode.*
import io.github.dovecoteescapee.byedpi.utility.*
import io.github.romanvht.byedpi.R
import io.github.romanvht.byedpi.data.UISettings
import io.github.romanvht.byedpi.data.UISettings.DesyncMethod.*
import io.github.romanvht.byedpi.data.UISettings.HostsMode.*
import io.github.romanvht.byedpi.utility.*
class ByeDpiUISettingsFragment : PreferenceFragmentCompat() {
@@ -61,11 +61,8 @@ class ByeDpiUISettingsFragment : PreferenceFragmentCompat() {
}
private fun updatePreferences() {
val desyncMethod =
findPreferenceNotNull<ListPreference>("byedpi_desync_method")
.value.let { ByeDpiProxyUIPreferences.DesyncMethod.fromName(it) }
val hostsMode = findPreferenceNotNull<ListPreference>("byedpi_hosts_mode")
.value.let { ByeDpiProxyUIPreferences.HostsMode.fromName(it) }
val desyncMethod = findPreferenceNotNull<ListPreference>("byedpi_desync_method").value.let { UISettings.DesyncMethod.fromName(it) }
val hostsMode = findPreferenceNotNull<ListPreference>("byedpi_hosts_mode").value.let { UISettings.HostsMode.fromName(it) }
val hostsBlacklist = findPreferenceNotNull<EditTextPreference>("byedpi_hosts_blacklist")
val hostsWhitelist = findPreferenceNotNull<EditTextPreference>("byedpi_hosts_whitelist")
@@ -81,11 +78,9 @@ class ByeDpiUISettingsFragment : PreferenceFragmentCompat() {
val udpFakeCount = findPreferenceNotNull<EditTextPreference>("byedpi_udp_fake_count")
val hostMixedCase = findPreferenceNotNull<CheckBoxPreference>("byedpi_host_mixed_case")
val domainMixedCase = findPreferenceNotNull<CheckBoxPreference>("byedpi_domain_mixed_case")
val hostRemoveSpaces =
findPreferenceNotNull<CheckBoxPreference>("byedpi_host_remove_spaces")
val hostRemoveSpaces = findPreferenceNotNull<CheckBoxPreference>("byedpi_host_remove_spaces")
val splitTlsRec = findPreferenceNotNull<CheckBoxPreference>("byedpi_tlsrec_enabled")
val splitTlsRecPosition =
findPreferenceNotNull<EditTextPreference>("byedpi_tlsrec_position")
val splitTlsRecPosition = findPreferenceNotNull<EditTextPreference>("byedpi_tlsrec_position")
val splitTlsRecAtSni = findPreferenceNotNull<CheckBoxPreference>("byedpi_tlsrec_at_sni")
hostsBlacklist.isVisible = hostsMode == Blacklist
@@ -103,8 +98,7 @@ class ByeDpiUISettingsFragment : PreferenceFragmentCompat() {
val isOob = desyncMethod == OOB || desyncMethod == DISOOB
oobChar.isVisible = isOob
val desyncAllProtocols =
!desyncHttp.isChecked && !desyncHttps.isChecked && !desyncUdp.isChecked
val desyncAllProtocols = !desyncHttp.isChecked && !desyncHttps.isChecked && !desyncUdp.isChecked
val desyncHttpEnabled = desyncAllProtocols || desyncHttp.isChecked
hostMixedCase.isEnabled = desyncHttpEnabled
@@ -0,0 +1,203 @@
package io.github.romanvht.byedpi.fragments
import android.os.Bundle
import android.view.LayoutInflater
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import android.widget.EditText
import android.widget.LinearLayout
import android.widget.Toast
import androidx.appcompat.app.AlertDialog
import androidx.core.view.MenuProvider
import androidx.fragment.app.Fragment
import androidx.lifecycle.Lifecycle
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import io.github.romanvht.byedpi.R
import io.github.romanvht.byedpi.adapters.DomainListAdapter
import io.github.romanvht.byedpi.data.DomainList
import io.github.romanvht.byedpi.utility.ClipboardUtils
import io.github.romanvht.byedpi.utility.DomainListUtils
class DomainListsFragment : Fragment() {
private lateinit var recyclerView: RecyclerView
private lateinit var adapter: DomainListAdapter
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.domain_lists, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
requireActivity().addMenuProvider(object : MenuProvider {
override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {
menuInflater.inflate(R.menu.menu_domains, menu)
}
override fun onMenuItemSelected(menuItem: MenuItem): Boolean {
return when (menuItem.itemId) {
R.id.action_reset_lists -> {
DomainListUtils.resetLists(requireContext())
Toast.makeText(requireContext(), R.string.domain_list_reset_done, Toast.LENGTH_SHORT).show()
refreshLists()
true
}
else -> false
}
}
}, viewLifecycleOwner, Lifecycle.State.RESUMED)
DomainListUtils.syncLists(requireContext())
recyclerView = view.findViewById(R.id.domain_lists_recycler)
view.findViewById<View>(R.id.add_new_list_button).setOnClickListener {
showAddDialog()
}
adapter = DomainListAdapter(
onStateChanged = { domainList ->
DomainListUtils.toggleListActive(requireContext(), domainList.id)
refreshLists()
},
onEdit = { domainList ->
showEditDialog(domainList)
},
onDelete = { domainList ->
DomainListUtils.deleteList(requireContext(), domainList.id)
refreshLists()
},
onCopy = { domainList ->
copyDomainsToClipboard(domainList)
}
)
recyclerView.layoutManager = LinearLayoutManager(requireContext())
recyclerView.adapter = adapter
refreshLists()
}
private fun refreshLists() {
val lists = DomainListUtils.getLists(requireContext())
adapter.submitList(lists.sortedBy { it.name.lowercase() })
}
private fun copyDomainsToClipboard(domainList: DomainList) {
val domainsText = domainList.domains.joinToString("\n")
ClipboardUtils.copy(requireContext(), domainsText, domainList.name, showToast = false)
Toast.makeText(requireContext(), R.string.toast_copied, Toast.LENGTH_SHORT).show()
}
private fun showAddDialog() {
val context = requireContext()
val layout = LinearLayout(context).apply {
orientation = LinearLayout.VERTICAL
setPadding(50, 40, 50, 10)
}
val nameInput = EditText(context).apply {
hint = getString(R.string.domain_list_name_hint)
}
val domainsInput = EditText(context).apply {
hint = getString(R.string.domain_list_domains_hint)
minLines = 5
gravity = android.view.Gravity.BOTTOM
}
layout.addView(nameInput)
layout.addView(domainsInput)
AlertDialog.Builder(context, R.style.CustomAlertDialog)
.setTitle(R.string.domain_list_add_new)
.setView(layout)
.setPositiveButton(android.R.string.ok) { _, _ ->
val name = nameInput.text.toString().trim()
val domainsText = domainsInput.text.toString()
val domains = domainsText.lines()
.map { it.trim() }
.filter { it.isNotEmpty() }
if (name.isEmpty()) {
Toast.makeText(context, R.string.domain_list_name_empty, Toast.LENGTH_SHORT).show()
return@setPositiveButton
}
if (domains.isEmpty()) {
Toast.makeText(context, R.string.domain_list_domains_empty, Toast.LENGTH_SHORT).show()
return@setPositiveButton
}
if (DomainListUtils.addList(context, name, domains)) {
Toast.makeText(context, R.string.domain_list_added, Toast.LENGTH_SHORT).show()
refreshLists()
} else {
Toast.makeText(context, R.string.domain_list_already_exists, Toast.LENGTH_SHORT).show()
}
}
.setNegativeButton(android.R.string.cancel, null)
.show()
}
private fun showEditDialog(domainList: DomainList) {
val context = requireContext()
val layout = LinearLayout(context).apply {
orientation = LinearLayout.VERTICAL
setPadding(50, 40, 50, 10)
}
val nameInput = EditText(context).apply {
hint = getString(R.string.domain_list_name_hint)
setText(domainList.name)
}
val domainsInput = EditText(context).apply {
hint = getString(R.string.domain_list_domains_hint)
minLines = 5
gravity = android.view.Gravity.BOTTOM
setText(domainList.domains.joinToString("\n"))
}
layout.addView(nameInput)
layout.addView(domainsInput)
AlertDialog.Builder(context, R.style.CustomAlertDialog)
.setTitle(R.string.domain_list_edit)
.setView(layout)
.setPositiveButton(android.R.string.ok) { _, _ ->
val name = nameInput.text.toString().trim()
val domainsText = domainsInput.text.toString()
val domains = domainsText.lines()
.map { it.trim() }
.filter { it.isNotEmpty() }
if (name.isEmpty()) {
Toast.makeText(context, R.string.domain_list_name_empty, Toast.LENGTH_SHORT).show()
return@setPositiveButton
}
if (domains.isEmpty()) {
Toast.makeText(context, R.string.domain_list_domains_empty, Toast.LENGTH_SHORT).show()
return@setPositiveButton
}
if (DomainListUtils.updateList(context, domainList.id, name, domains)) {
Toast.makeText(context, R.string.domain_list_updated, Toast.LENGTH_SHORT).show()
refreshLists()
} else {
Toast.makeText(context, R.string.domain_list_update_failed, Toast.LENGTH_SHORT).show()
}
}
.setNegativeButton(android.R.string.cancel, null)
.show()
}
}
@@ -0,0 +1,228 @@
package io.github.romanvht.byedpi.fragments
import android.content.Intent
import android.content.SharedPreferences
import android.os.Bundle
import android.util.Log
import androidx.appcompat.app.AlertDialog
import androidx.core.content.edit
import androidx.preference.*
import io.github.romanvht.byedpi.R
import io.github.romanvht.byedpi.BuildConfig
import io.github.romanvht.byedpi.activities.TestActivity
import io.github.romanvht.byedpi.data.Mode
import io.github.romanvht.byedpi.utility.*
import androidx.core.net.toUri
class MainSettingsFragment : PreferenceFragmentCompat() {
companion object {
private val TAG: String = MainSettingsFragment::class.java.simpleName
}
private val preferenceListener =
SharedPreferences.OnSharedPreferenceChangeListener { _, _ ->
updatePreferences()
}
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
setPreferencesFromResource(R.xml.main_settings, rootKey)
setEditTextPreferenceListener("byedpi_proxy_ip") { checkIp(it) }
setEditTestPreferenceListenerPort("byedpi_proxy_port")
setEditTextPreferenceListener("dns_ip") { it.isBlank() || checkNotLocalIp(it) }
findPreferenceNotNull<ListPreference>("language")
.setOnPreferenceChangeListener { _, newValue ->
SettingsUtils.setLang(newValue as String)
true
}
findPreferenceNotNull<ListPreference>("app_theme")
.setOnPreferenceChangeListener { _, newValue ->
SettingsUtils.setTheme(newValue as String)
true
}
findPreferenceNotNull<Preference>("proxy_test")
.setOnPreferenceClickListener {
val intent = Intent(context, TestActivity::class.java)
startActivity(intent)
true
}
findPreferenceNotNull<Preference>("battery_optimization")
.setOnPreferenceClickListener {
BatteryUtils.requestBatteryOptimization(requireContext())
true
}
findPreferenceNotNull<Preference>("storage_access")
.setOnPreferenceClickListener {
StorageUtils.requestStoragePermission(this)
true
}
findPreferenceNotNull<Preference>("donate")
.setOnPreferenceClickListener {
showDonateDialog()
true
}
findPreferenceNotNull<Preference>("dns_ip_picker")
.setOnPreferenceClickListener {
showDnsDialog()
true
}
findPreferenceNotNull<Preference>("version").summary = BuildConfig.VERSION_NAME
findPreferenceNotNull<Preference>("byedpi_version").summary = "0.17.3 (ba53229)"
updatePreferences()
}
override fun onResume() {
super.onResume()
sharedPreferences?.registerOnSharedPreferenceChangeListener(preferenceListener)
updatePreferences()
}
override fun onPause() {
super.onPause()
sharedPreferences?.unregisterOnSharedPreferenceChangeListener(preferenceListener)
}
private fun updatePreferences() {
val cmdEnable = findPreferenceNotNull<SwitchPreference>("byedpi_enable_cmd_settings").isChecked
val mode = findPreferenceNotNull<ListPreference>("byedpi_mode").value.let { Mode.fromString(it) }
val dns = findPreferenceNotNull<Preference>("dns_ip_picker")
val dnsEdit = findPreferenceNotNull<EditTextPreference>("dns_ip")
val ipv6 = findPreferenceNotNull<SwitchPreference>("ipv6_enable")
val proxy = findPreferenceNotNull<PreferenceCategory>("byedpi_proxy_category")
val applistType = findPreferenceNotNull<ListPreference>("applist_type")
val selectedApps = findPreferenceNotNull<Preference>("selected_apps")
val batteryOptimization = findPreferenceNotNull<Preference>("battery_optimization")
val storageAccess = findPreferenceNotNull<Preference>("storage_access")
val uiSettings = findPreferenceNotNull<Preference>("byedpi_ui_settings")
val cmdSettings = findPreferenceNotNull<Preference>("byedpi_cmd_settings")
val proxyTest = findPreferenceNotNull<Preference>("proxy_test")
if (cmdEnable) {
val (cmdIp, cmdPort) = sharedPreferences?.checkIpAndPortInCmd() ?: Pair(null, null)
proxy.isVisible = cmdIp == null && cmdPort == null
} else {
proxy.isVisible = true
}
uiSettings.isEnabled = !cmdEnable
cmdSettings.isEnabled = cmdEnable
proxyTest.isEnabled = cmdEnable
dns.summary = dnsSummary()
dnsEdit.isVisible = false
when (mode) {
Mode.VPN -> {
dns.isVisible = true
ipv6.isVisible = true
when (applistType.value) {
"disable" -> {
applistType.isVisible = true
selectedApps.isVisible = false
}
"blacklist", "whitelist" -> {
applistType.isVisible = true
selectedApps.isVisible = true
}
else -> {
applistType.isVisible = true
selectedApps.isVisible = false
Log.w(TAG, "Unexpected applistType value: ${applistType.value}")
}
}
}
Mode.Proxy -> {
dns.isVisible = false
ipv6.isVisible = false
applistType.isVisible = false
selectedApps.isVisible = false
}
}
if (BatteryUtils.isOptimizationDisabled(requireContext())) {
batteryOptimization.summary = getString(R.string.battery_optimization_disabled_summary)
} else {
batteryOptimization.summary = getString(R.string.battery_optimization_summary)
}
if (StorageUtils.hasStoragePermission(requireContext())) {
storageAccess.summary = getString(R.string.storage_access_allowed_summary)
} else {
storageAccess.summary = getString(R.string.storage_access_summary)
}
}
private val dnsPresets: List<Pair<String, String>>
get() = listOf(
"" to getString(R.string.dns_system),
"8.8.8.8" to getString(R.string.dns_google),
"1.1.1.1" to getString(R.string.dns_cloudflare),
"9.9.9.9" to getString(R.string.dns_quad9),
)
private fun dnsSummary(): String {
val ip = sharedPreferences?.getStringNotNull("dns_ip", "1.1.1.1") ?: "1.1.1.1"
val preset = dnsPresets.firstOrNull { it.first == ip } ?: return ip
return if (preset.first.isBlank()) preset.second else "${preset.second} ($ip)"
}
private fun showDnsDialog() {
val context = requireContext()
val presets = dnsPresets
val items = (presets.map { it.second } + getString(R.string.dns_custom)).toTypedArray()
val dnsPref = findPreferenceNotNull<EditTextPreference>("dns_ip")
AlertDialog.Builder(context)
.setTitle(getString(R.string.dbs_ip_setting))
.setItems(items) { _, which ->
if (which < presets.size) {
sharedPreferences?.edit { putString("dns_ip", presets[which].first) }
} else {
onDisplayPreferenceDialog(dnsPref)
}
}
.setNegativeButton(android.R.string.cancel, null)
.show()
}
private fun showDonateDialog() {
val context = requireContext()
val items = arrayOf(
getString(R.string.donate_message),
getString(R.string.donate_cloudtips),
getString(R.string.donate_telegram_bot),
getString(R.string.donate_thanks),
)
AlertDialog.Builder(context)
.setTitle(getString(R.string.donate))
.setItems(items) { _, which ->
when (which) {
1 -> openUrl("https://pay.cloudtips.ru/p/92c754db")
2 -> openUrl("https://t.me/romanvht_donate_bot")
}
}
.setNegativeButton(android.R.string.cancel, null)
.show()
}
private fun openUrl(url: String) {
val intent = Intent(Intent.ACTION_VIEW, url.toUri())
startActivity(intent)
}
}
@@ -0,0 +1,69 @@
package io.github.romanvht.byedpi.fragments
import android.content.SharedPreferences
import android.os.Bundle
import androidx.preference.*
import io.github.romanvht.byedpi.R
import io.github.romanvht.byedpi.utility.*
class ProxyTestSettingsFragment : PreferenceFragmentCompat() {
private val preferenceListener =
SharedPreferences.OnSharedPreferenceChangeListener { _, _ ->
updatePreferences()
}
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
setPreferencesFromResource(R.xml.proxy_test_settings, rootKey)
setupNumberSummary("byedpi_proxytest_delay", R.string.proxytest_delay_desc)
setupNumberSummary("byedpi_proxytest_requests", R.string.proxytest_requests_desc)
setupNumberSummary("byedpi_proxytest_limit", R.string.proxytest_limit_desc)
setupNumberSummary("byedpi_proxytest_timeout", R.string.proxytest_timeout_desc)
setEditTestPreferenceListenerInt("byedpi_proxytest_delay", 0, 10)
setEditTestPreferenceListenerInt("byedpi_proxytest_requests", 1, 20)
setEditTestPreferenceListenerInt("byedpi_proxytest_timeout", 1, 15)
setEditTestPreferenceListenerInt("byedpi_proxytest_limit", 1, 50)
setEditTestPreferenceListenerDomain("byedpi_proxytest_sni")
updatePreferences()
}
override fun onResume() {
super.onResume()
updatePreferences()
sharedPreferences?.registerOnSharedPreferenceChangeListener(preferenceListener)
}
override fun onPause() {
super.onPause()
sharedPreferences?.unregisterOnSharedPreferenceChangeListener(preferenceListener)
}
private fun updatePreferences() {
val switchUserCommands = findPreferenceNotNull<SwitchPreference>("byedpi_proxytest_usercommands")
val textUserCommands = findPreferenceNotNull<EditTextPreference>("byedpi_proxytest_commands")
val manageDomainLists = findPreferenceNotNull<Preference>("manage_domain_lists")
val activeLists = DomainListUtils.getLists(requireContext()).filter { it.isActive }
manageDomainLists.summary = if (activeLists.isEmpty()) {
getString(R.string.domain_lists_summary)
} else {
activeLists.joinToString(", ") { it.name }
}
textUserCommands.isEnabled = switchUserCommands.isChecked
}
private fun setupNumberSummary(key: String, descriptionResId: Int) {
val pref = findPreference<EditTextPreference>(key) ?: return
pref.summaryProvider = Preference.SummaryProvider<EditTextPreference> { p ->
val value = p.text ?: ""
val description = getString(descriptionResId)
"$value - $description"
}
}
}
@@ -1,14 +1,14 @@
package io.github.dovecoteescapee.byedpi.receiver
package io.github.romanvht.byedpi.receiver
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.net.VpnService
import android.os.SystemClock
import io.github.dovecoteescapee.byedpi.data.Mode
import io.github.dovecoteescapee.byedpi.services.ServiceManager
import io.github.dovecoteescapee.byedpi.utility.getPreferences
import io.github.dovecoteescapee.byedpi.utility.mode
import io.github.romanvht.byedpi.data.Mode
import io.github.romanvht.byedpi.services.ServiceManager
import io.github.romanvht.byedpi.utility.getPreferences
import io.github.romanvht.byedpi.utility.mode
class BootReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
@@ -1,23 +1,26 @@
package io.github.dovecoteescapee.byedpi.services
package io.github.romanvht.byedpi.services
import android.app.Notification
import android.app.NotificationManager
import android.content.Intent
import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE
import android.content.pm.ServiceInfo
import android.os.Build
import android.util.Log
import androidx.lifecycle.LifecycleService
import androidx.lifecycle.lifecycleScope
import io.github.dovecoteescapee.byedpi.R
import io.github.dovecoteescapee.byedpi.core.ByeDpiProxy
import io.github.dovecoteescapee.byedpi.core.ByeDpiProxyPreferences
import io.github.dovecoteescapee.byedpi.data.*
import io.github.dovecoteescapee.byedpi.utility.*
import io.github.romanvht.byedpi.R
import io.github.romanvht.byedpi.core.ByeDpiProxy
import io.github.romanvht.byedpi.core.ByeDpiProxyPreferences
import io.github.romanvht.byedpi.data.*
import io.github.romanvht.byedpi.utility.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext
import kotlinx.coroutines.withTimeoutOrNull
class ByeDpiProxyService : LifecycleService() {
private var proxy = ByeDpiProxy()
@@ -27,13 +30,10 @@ class ByeDpiProxyService : LifecycleService() {
companion object {
private val TAG: String = ByeDpiProxyService::class.java.simpleName
private const val FOREGROUND_SERVICE_ID: Int = 2
private const val PAUSE_NOTIFICATION_ID: Int = 3
private const val NOTIFICATION_CHANNEL_ID: String = "ByeDPI Proxy"
private var status: ServiceStatus = ServiceStatus.Disconnected
fun getStatus(): ServiceStatus {
return status
}
}
override fun onCreate() {
@@ -47,14 +47,36 @@ class ByeDpiProxyService : LifecycleService() {
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
super.onStartCommand(intent, flags, startId)
startForeground()
return when (val action = intent?.action) {
START_ACTION -> {
lifecycleScope.launch { start() }
lifecycleScope.launch {
start()
}
START_STICKY
}
STOP_ACTION -> {
lifecycleScope.launch { stop() }
lifecycleScope.launch {
stop()
}
START_NOT_STICKY
}
RESUME_ACTION -> {
lifecycleScope.launch {
start()
}
START_STICKY
}
PAUSE_ACTION -> {
lifecycleScope.launch {
stop()
createNotificationPause()
}
START_NOT_STICKY
}
@@ -68,18 +90,20 @@ class ByeDpiProxyService : LifecycleService() {
private suspend fun start() {
Log.i(TAG, "Starting")
val notificationManager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager
notificationManager.cancel(PAUSE_NOTIFICATION_ID)
if (status == ServiceStatus.Connected) {
Log.w(TAG, "Proxy already connected")
updateStatus(ServiceStatus.Connected)
return
}
try {
mutex.withLock {
startProxy()
updateStatus(ServiceStatus.Connected)
}
updateStatus(ServiceStatus.Connected)
startForeground()
} catch (e: Exception) {
Log.e(TAG, "Failed to start proxy", e)
updateStatus(ServiceStatus.Failed)
@@ -93,7 +117,7 @@ class ByeDpiProxyService : LifecycleService() {
startForeground(
FOREGROUND_SERVICE_ID,
notification,
FOREGROUND_SERVICE_TYPE_SPECIAL_USE,
ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC,
)
} else {
startForeground(FOREGROUND_SERVICE_ID, notification)
@@ -103,15 +127,23 @@ class ByeDpiProxyService : LifecycleService() {
private suspend fun stop() {
Log.i(TAG, "Stopping")
if (status != ServiceStatus.Connected) {
Log.w(TAG, "Proxy not connected")
updateStatus(ServiceStatus.Disconnected)
return
}
mutex.withLock {
stopProxy()
withContext(Dispatchers.IO) {
stopProxy()
}
}
updateStatus(ServiceStatus.Disconnected)
stopSelf()
}
private suspend fun startProxy() {
private fun startProxy() {
Log.i(TAG, "Starting proxy")
if (proxyJob != null) {
@@ -125,13 +157,12 @@ class ByeDpiProxyService : LifecycleService() {
proxyJob = lifecycleScope.launch(Dispatchers.IO) {
val code = proxy.startProxy(preferences)
withContext(Dispatchers.Main) {
if (code != 0) {
Log.e(TAG, "Proxy stopped with code $code")
updateStatus(ServiceStatus.Failed)
} else {
updateStatus(ServiceStatus.Disconnected)
}
delay(500)
if (code != 0) {
Log.e(TAG, "Proxy stopped with code $code")
updateStatus(ServiceStatus.Failed)
stopSelf()
}
}
@@ -146,15 +177,30 @@ class ByeDpiProxyService : LifecycleService() {
return
}
proxy.stopProxy()
proxyJob?.join()
proxyJob = null
try {
proxy.stopProxy()
proxyJob?.cancel()
val completed = withTimeoutOrNull(2000) {
proxyJob?.join()
true
}
if (completed == null) {
Log.w(TAG, "proxy not finish in time, cancelling...")
proxy.jniForceClose()
}
proxyJob = null
} catch (e: Exception) {
Log.e(TAG, "Failed to close proxyJob", e)
}
Log.i(TAG, "Proxy stopped")
}
private fun getByeDpiPreferences(): ByeDpiProxyPreferences =
ByeDpiProxyPreferences.fromSharedPreferences(getPreferences())
ByeDpiProxyPreferences.fromSharedPreferences(getPreferences(), this)
private fun updateStatus(newStatus: ServiceStatus) {
Log.d(TAG, "Proxy status changed from $status to $newStatus")
@@ -182,6 +228,10 @@ class ByeDpiProxyService : LifecycleService() {
)
intent.putExtra(SENDER, Sender.Proxy.ordinal)
sendBroadcast(intent)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
QuickTileService.updateTile()
}
}
private fun createNotification(): Notification =
@@ -192,4 +242,17 @@ class ByeDpiProxyService : LifecycleService() {
R.string.proxy_notification_content,
ByeDpiProxyService::class.java,
)
private fun createNotificationPause() {
val notification = createPauseNotification(
this,
NOTIFICATION_CHANNEL_ID,
R.string.notification_title,
R.string.service_paused_text,
ByeDpiProxyService::class.java,
)
val notificationManager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager
notificationManager.notify(PAUSE_NOTIFICATION_ID, notification)
}
}
@@ -0,0 +1,11 @@
package io.github.romanvht.byedpi.services
import io.github.romanvht.byedpi.data.AppStatus
import io.github.romanvht.byedpi.data.Mode
var appStatus = AppStatus.Halted to Mode.VPN
private set
fun setStatus(status: AppStatus, mode: Mode) {
appStatus = status to mode
}
@@ -1,6 +1,7 @@
package io.github.dovecoteescapee.byedpi.services
package io.github.romanvht.byedpi.services
import android.app.Notification
import android.app.NotificationManager
import android.app.PendingIntent
import android.content.Intent
import android.content.pm.ServiceInfo
@@ -8,19 +9,21 @@ import android.os.Build
import android.os.ParcelFileDescriptor
import android.util.Log
import androidx.lifecycle.lifecycleScope
import io.github.dovecoteescapee.byedpi.R
import io.github.dovecoteescapee.byedpi.activities.MainActivity
import io.github.dovecoteescapee.byedpi.core.ByeDpiProxy
import io.github.dovecoteescapee.byedpi.core.ByeDpiProxyPreferences
import io.github.dovecoteescapee.byedpi.core.TProxyService
import io.github.dovecoteescapee.byedpi.data.*
import io.github.dovecoteescapee.byedpi.utility.*
import io.github.romanvht.byedpi.R
import io.github.romanvht.byedpi.activities.MainActivity
import io.github.romanvht.byedpi.core.ByeDpiProxy
import io.github.romanvht.byedpi.core.ByeDpiProxyPreferences
import io.github.romanvht.byedpi.core.TProxyService
import io.github.romanvht.byedpi.data.*
import io.github.romanvht.byedpi.utility.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext
import kotlinx.coroutines.withTimeoutOrNull
import java.io.File
class ByeDpiVpnService : LifecycleVpnService() {
@@ -28,11 +31,11 @@ class ByeDpiVpnService : LifecycleVpnService() {
private var proxyJob: Job? = null
private var tunFd: ParcelFileDescriptor? = null
private val mutex = Mutex()
private var stopping: Boolean = false
companion object {
private val TAG: String = ByeDpiVpnService::class.java.simpleName
private const val FOREGROUND_SERVICE_ID: Int = 1
private const val PAUSE_NOTIFICATION_ID: Int = 3
private const val NOTIFICATION_CHANNEL_ID: String = "ByeDPIVpn"
private var status: ServiceStatus = ServiceStatus.Disconnected
@@ -47,19 +50,64 @@ class ByeDpiVpnService : LifecycleVpnService() {
)
}
override fun onDestroy() {
super.onDestroy()
tunFd?.close()
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
super.onStartCommand(intent, flags, startId)
startForeground()
return when (val action = intent?.action) {
START_ACTION -> {
lifecycleScope.launch { start() }
lifecycleScope.launch {
start()
}
START_STICKY
}
STOP_ACTION -> {
lifecycleScope.launch { stop() }
lifecycleScope.launch {
stop()
}
START_NOT_STICKY
}
RESUME_ACTION -> {
lifecycleScope.launch {
if (prepare(this@ByeDpiVpnService) == null) {
start()
}
}
START_STICKY
}
PAUSE_ACTION -> {
lifecycleScope.launch {
stop()
createNotificationPause()
}
START_NOT_STICKY
}
SERVICE_INTERFACE -> {
Log.i(TAG, "Started by Android")
if (getPreferences().mode() != Mode.VPN) {
Log.w(TAG, "Always-On disabled in proxy mode")
stopSelf()
return START_NOT_STICKY
}
lifecycleScope.launch {
start()
}
START_STICKY
}
else -> {
Log.w(TAG, "Unknown action: $action")
START_NOT_STICKY
@@ -75,8 +123,12 @@ class ByeDpiVpnService : LifecycleVpnService() {
private suspend fun start() {
Log.i(TAG, "Starting")
val notificationManager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager
notificationManager.cancel(PAUSE_NOTIFICATION_ID)
if (status == ServiceStatus.Connected) {
Log.w(TAG, "VPN already connected")
updateStatus(ServiceStatus.Connected)
return
}
@@ -84,9 +136,8 @@ class ByeDpiVpnService : LifecycleVpnService() {
mutex.withLock {
startProxy()
startTun2Socks()
updateStatus(ServiceStatus.Connected)
}
updateStatus(ServiceStatus.Connected)
startForeground()
} catch (e: Exception) {
Log.e(TAG, "Failed to start VPN", e)
updateStatus(ServiceStatus.Failed)
@@ -100,7 +151,7 @@ class ByeDpiVpnService : LifecycleVpnService() {
startForeground(
FOREGROUND_SERVICE_ID,
notification,
ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE,
ServiceInfo.FOREGROUND_SERVICE_TYPE_SYSTEM_EXEMPTED,
)
} else {
startForeground(FOREGROUND_SERVICE_ID, notification)
@@ -110,15 +161,20 @@ class ByeDpiVpnService : LifecycleVpnService() {
private suspend fun stop() {
Log.i(TAG, "Stopping")
if (status != ServiceStatus.Connected) {
Log.w(TAG, "VPN not connected")
updateStatus(ServiceStatus.Disconnected)
return
}
mutex.withLock {
stopping = true
try {
stopTun2Socks()
stopProxy()
withContext(Dispatchers.IO) {
stopProxy()
stopTun2Socks()
}
} catch (e: Exception) {
Log.e(TAG, "Failed to stop VPN", e)
} finally {
stopping = false
}
}
@@ -126,7 +182,7 @@ class ByeDpiVpnService : LifecycleVpnService() {
stopSelf()
}
private suspend fun startProxy() {
private fun startProxy() {
Log.i(TAG, "Starting proxy")
if (proxyJob != null) {
@@ -139,17 +195,13 @@ class ByeDpiVpnService : LifecycleVpnService() {
proxyJob = lifecycleScope.launch(Dispatchers.IO) {
val code = byeDpiProxy.startProxy(preferences)
withContext(Dispatchers.Main) {
if (code != 0) {
Log.e(TAG, "Proxy stopped with code $code")
updateStatus(ServiceStatus.Failed)
stopTun2Socks()
} else {
if (!stopping) {
stop()
updateStatus(ServiceStatus.Disconnected)
}
}
delay(500)
if (code != 0) {
Log.e(TAG, "Proxy stopped with code $code")
updateStatus(ServiceStatus.Failed)
stopTun2Socks()
stopSelf()
}
}
@@ -164,9 +216,24 @@ class ByeDpiVpnService : LifecycleVpnService() {
return
}
byeDpiProxy.stopProxy()
proxyJob?.join() ?: throw IllegalStateException("ProxyJob field null")
proxyJob = null
try {
byeDpiProxy.stopProxy()
proxyJob?.cancel()
val completed = withTimeoutOrNull(2000) {
proxyJob?.join()
true
}
if (completed == null) {
Log.w(TAG, "proxy not finish in time, cancelling...")
byeDpiProxy.jniForceClose()
}
proxyJob = null
} catch (e: Exception) {
Log.e(TAG, "Failed to close proxyJob", e)
}
Log.i(TAG, "Proxy stopped")
}
@@ -175,24 +242,28 @@ class ByeDpiVpnService : LifecycleVpnService() {
Log.i(TAG, "Starting tun2socks")
if (tunFd != null) {
Log.w(TAG, "VPN field not null")
throw IllegalStateException("VPN field not null")
}
val sharedPreferences = getPreferences()
val (ip, port) = sharedPreferences.getProxyIpAndPort()
val dns = sharedPreferences.getStringNotNull("dns_ip", "8.8.8.8")
val dns = sharedPreferences.getStringNotNull("dns_ip", "1.1.1.1")
val ipv6 = sharedPreferences.getBoolean("ipv6_enable", false)
val tun2socksConfig = """
| misc:
| task-stack-size: 81920
| socks5:
| mtu: 8500
| address: $ip
| port: $port
| udp: udp
""".trimMargin("| ")
val tun2socksConfig = buildString {
appendLine("tunnel:")
appendLine(" mtu: 8500")
appendLine("misc:")
appendLine(" task-stack-size: 81920")
appendLine("socks5:")
appendLine(" address: $ip")
appendLine(" port: $port")
appendLine(" udp: udp")
}
val configPath = try {
File.createTempFile("config", "tmp", cacheDir).apply {
@@ -216,7 +287,16 @@ class ByeDpiVpnService : LifecycleVpnService() {
private fun stopTun2Socks() {
Log.i(TAG, "Stopping tun2socks")
TProxyService.TProxyStopService()
if (tunFd == null) {
Log.w(TAG, "VPN field is null, skipping")
return
}
try {
TProxyService.TProxyStopService()
} catch (e: Exception) {
Log.e(TAG, "Failed to stop TProxyService", e)
}
try {
File(cacheDir, "config.tmp").delete()
@@ -224,14 +304,19 @@ class ByeDpiVpnService : LifecycleVpnService() {
Log.e(TAG, "Failed to delete config file", e)
}
tunFd?.close() ?: Log.w(TAG, "VPN not running")
tunFd = null
try {
tunFd?.close()
} catch (e: Exception) {
Log.e(TAG, "Failed to close tunFd", e)
} finally {
tunFd = null
}
Log.i(TAG, "Tun2socks stopped")
}
private fun getByeDpiPreferences(): ByeDpiProxyPreferences =
ByeDpiProxyPreferences.fromSharedPreferences(getPreferences())
ByeDpiProxyPreferences.fromSharedPreferences(getPreferences(), this)
private fun updateStatus(newStatus: ServiceStatus) {
Log.d(TAG, "VPN status changed from $status to $newStatus")
@@ -260,6 +345,10 @@ class ByeDpiVpnService : LifecycleVpnService() {
)
intent.putExtra(SENDER, Sender.VPN.ordinal)
sendBroadcast(intent)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
QuickTileService.updateTile()
}
}
private fun createNotification(): Notification =
@@ -271,6 +360,19 @@ class ByeDpiVpnService : LifecycleVpnService() {
ByeDpiVpnService::class.java,
)
private fun createNotificationPause() {
val notification = createPauseNotification(
this,
NOTIFICATION_CHANNEL_ID,
R.string.notification_title,
R.string.service_paused_text,
ByeDpiVpnService::class.java,
)
val notificationManager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager
notificationManager.notify(PAUSE_NOTIFICATION_ID, notification)
}
private fun createBuilder(dns: String, ipv6: Boolean): Builder {
Log.d(TAG, "DNS: $dns")
val builder = Builder()
@@ -295,6 +397,7 @@ class ByeDpiVpnService : LifecycleVpnService() {
if (dns.isNotBlank()) {
builder.addDnsServer(dns)
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
builder.setMetered(false)
}
@@ -1,4 +1,4 @@
package io.github.dovecoteescapee.byedpi.services
package io.github.romanvht.byedpi.services
import android.content.Intent
import android.net.VpnService
@@ -0,0 +1,91 @@
package io.github.romanvht.byedpi.services
import android.net.VpnService
import android.os.Build
import android.service.quicksettings.Tile
import android.service.quicksettings.TileService
import android.util.Log
import androidx.annotation.RequiresApi
import io.github.romanvht.byedpi.data.*
import io.github.romanvht.byedpi.utility.getPreferences
import io.github.romanvht.byedpi.utility.mode
@RequiresApi(Build.VERSION_CODES.N)
class QuickTileService : TileService() {
companion object {
private const val TAG = "QuickTileService"
private var instance: QuickTileService? = null
fun updateTile() {
instance?.updateStatus()
}
}
private var appTile: Tile? = null
override fun onStartListening() {
super.onStartListening()
instance = this
appTile = qsTile
updateStatus()
}
override fun onStopListening() {
super.onStopListening()
instance = null
appTile = null
}
override fun onClick() {
super.onClick()
handleClick()
}
private fun handleClick() {
val (status) = appStatus
val mode = getPreferences().mode()
when (status) {
AppStatus.Halted -> startService(mode)
AppStatus.Running -> stopService()
}
Log.i(TAG, "Tile clicked")
}
private fun startService(mode: Mode) {
if (mode == Mode.VPN && VpnService.prepare(this) != null) {
return
}
ServiceManager.start(this, mode)
setState(Tile.STATE_ACTIVE)
}
private fun stopService() {
ServiceManager.stop(this)
setState(Tile.STATE_INACTIVE)
}
private fun updateStatus() {
val (status) = appStatus
val newState = when (status) {
AppStatus.Running -> Tile.STATE_ACTIVE
AppStatus.Halted -> Tile.STATE_INACTIVE
}
setState(newState)
}
private fun setState(state: Int) {
appTile?.apply {
this.state = state
updateTile()
}
}
}
@@ -1,15 +1,22 @@
package io.github.dovecoteescapee.byedpi.services
package io.github.romanvht.byedpi.services
import android.content.Context
import android.content.Intent
import android.util.Log
import androidx.core.content.ContextCompat
import io.github.dovecoteescapee.byedpi.data.Mode
import io.github.dovecoteescapee.byedpi.data.START_ACTION
import io.github.dovecoteescapee.byedpi.data.STOP_ACTION
import io.github.romanvht.byedpi.data.AppStatus
import io.github.romanvht.byedpi.data.Mode
import io.github.romanvht.byedpi.data.START_ACTION
import io.github.romanvht.byedpi.data.STOP_ACTION
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
object ServiceManager {
private val TAG: String = ServiceManager::class.java.simpleName
private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
fun start(context: Context, mode: Mode) {
when (mode) {
@@ -47,4 +54,18 @@ object ServiceManager {
}
}
}
fun restart(context: Context, mode: Mode) {
if (appStatus.first == AppStatus.Running) {
stop(context)
scope.launch {
val startTime = System.currentTimeMillis()
while (System.currentTimeMillis() - startTime < 3000L) {
if (appStatus.first == AppStatus.Halted) break
delay(100)
}
start(context, mode)
}
}
}
}
@@ -1,10 +1,10 @@
package io.github.dovecoteescapee.byedpi.utility
package io.github.romanvht.byedpi.utility
// Based on https://gist.github.com/raymyers/8077031
fun shellSplit(string: CharSequence): List<String> {
val tokens: MutableList<String> = ArrayList()
var escaping = false
var quoteChar = ' '
var escaping = false
var quoting = false
var lastCloseQuoteIndex = Int.MIN_VALUE
var current = StringBuilder()
@@ -15,8 +15,12 @@ fun shellSplit(string: CharSequence): List<String> {
if (escaping) {
current.append(c)
escaping = false
} else if (c == '\\' && !(quoting && quoteChar == '\'')) {
escaping = true
} else if (c == '\\' && quoting) {
if (i + 1 < string.length && string[i + 1] == quoteChar) {
escaping = true
} else {
current.append(c)
}
} else if (quoting && c == quoteChar) {
quoting = false
lastCloseQuoteIndex = i
@@ -0,0 +1,44 @@
package io.github.romanvht.byedpi.utility
import android.annotation.SuppressLint
import android.provider.Settings
import android.content.Context
import android.content.Intent
import android.os.Build
import android.os.PowerManager
import android.util.Log
import androidx.core.net.toUri
object BatteryUtils {
private const val TAG = "BatteryUtils"
fun isOptimizationDisabled(context: Context): Boolean {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
val powerManager = context.getSystemService(Context.POWER_SERVICE) as PowerManager
powerManager.isIgnoringBatteryOptimizations(context.packageName)
} else {
true
}
}
@SuppressLint("BatteryLife")
fun requestBatteryOptimization(context: Context) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
try {
val intent = Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS).apply {
data = "package:${context.packageName}".toUri()
flags = Intent.FLAG_ACTIVITY_NEW_TASK
}
context.startActivity(intent)
} catch (e: Exception) {
Log.e(TAG, "Failed to request ignore battery optimizations", e)
try {
val intent = Intent(Settings.ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS)
context.startActivity(intent)
} catch (e2: Exception) {
Log.e(TAG, "Failed to open battery optimization settings", e2)
}
}
}
}
}

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