Compare commits

...
268 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
189 changed files with 7532 additions and 3308 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
+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-02-27T18:27:01.347037Z">
<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>
-9
View File
@@ -1,9 +0,0 @@
<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>
+61 -30
View File
@@ -1,46 +1,77 @@
# ByeByeDPI Android
[Русский](README.md) | English | [Türkçe](README-tr.md)
<div style="text-align: center;">
<img alt="ByeDPI Logo" src="https://github.com/romanvht/ByeDPIAndroid/raw/master/.github/images/app.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.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.
An Android application that locally runs ByeDPI and routes 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).
For stable operation, you may need to adjust the settings. You can read more about different settings in the [ByeDPI documentation](https://github.com/hufrea/byedpi/blob/v0.13/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.
This application is **not** a VPN. It uses Android's VPN mode to route traffic but does not transmit anything to a remote server. It does not encrypt traffic or hide your IP address.
This application is a fork of [ByeDPIAndroid](https://github.com/dovecoteescapee/ByeDPIAndroid).
The application has only one official website:
https://byebyedpi.xyz
---
### Features
* Autostart service on device boot
* Saving lists of command-line parameters
* Improved compatibility with Android TV/BOX
* Automatically starts the service when the device boots
* Saves command-line parameter lists
* Improved compatibility with Android TV/BOX devices
* Per-app split tunneling
* Import/export settings
* Settings import and export
### Usage
* To enable auto-start, activate the option in settings.
* It is recommended to connect to the VPN once to accept the request.
* After that, upon device startup, the application will automatically launch the service based on settings (VPN/Proxy).
* If you are using an Android TV/BOX and your Ethernet connection drops when connecting, enable whitelist mode and specify the apps that should work through the VPN (e.g., YouTube).
* Guide for [SberBOX](sbox.md)
### How to use ByeByeDPI with AdGuard?
* Start ByeByeDPI in proxy mode.
* Add ByeByeDPI to AdGuard exclusions on the "App Management" tab.
* In AdGuard settings, specify the proxy:
```plaintext
Proxy Type: SOCKS5
Host: 127.0.0.1
Port: 1080 (default)
* 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)
* [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)
+62 -30
View File
@@ -1,45 +1,77 @@
# ByeByeDPI Android
[Русский](README.md) | [English](README-en.md) | Türkçe
<div style="text-align: center;">
<img alt="ByeDPI Logo" src="https://github.com/romanvht/ByeDPIAndroid/raw/master/.github/images/app.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.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ı.
ByeDPI'yi yerel olarak çalıştıran ve tüm trafiği bunun ü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.
Kararlı bir çalışma için ayarları yapmanız gerekebilir. Farklı ayarlar hakkında daha fazla bilgiye [ByeDPI dökümantasyonundan](https://github.com/hufrea/byedpi/blob/v0.13/README.md) ulaşabilirsiniz.
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.
Bu uygulama **VPN** değildir. Trafiği yönlendirmek için Android'in VPN modunu kullanır ancak herhangi bir veriyi uzak bir sunucuya iletmez. Trafiği şifrelemez veya IP adresinizi gizlemez.
Bu uygulama, [ByeDPIAndroid](https://github.com/dovecoteescapee/ByeDPIAndroid) uygulamasının bir çatallamasıdır.
Uygulamanın tek resmî web sitesi:
https://byebyedpi.xyz
---
### Özellikler
* Cihaz başlatıldığında hizmetin otomatik başlatılması
* Komut satırı parametrelerinin listelerinin kaydedilmesi
* Cihaz açıldığında hizmeti otomatik başlatma
* Komut satırı parametre listelerini kaydetme
* Android TV/BOX ile geliştirilmiş uyumluluk
* Uygulama başına bölünmüş tünelleme
* Ayarları içe/dışa aktarma
* Uygulama bazında bölünmüş tünelleme
* Ayarları içe ve dışa aktarma
### Kullanım
* Otomatik başlatmayı etkinleştirmek için ayarlarda seçeneği aktifleştirin.
* İlk başta VPN'e bağlanarak isteği kabul etmeniz önerilir.
* Bundan sonra, cihaz başlatıldığında, uygulama ayarlara göre (VPN/Proxy) hizmeti otomatik olarak başlatacaktır.
* Android TV/BOX kullanıyorsanız ve Ethernet bağlantınız VPN'e bağlanırken kopuyorsa, beyaz liste modunu etkinleştirip VPN üzerinden çalışması gereken uygulamaları belirleyin (örneğin, YouTube).
* [SberBOX](sbox.md) için kılavuz
### Türkiye İle İlgili Destek İçin
* Discord: [nyaex](https://github.com/nyaexx) veya [shouyuma](https://github.com/Hamzahsl)
* 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
### ByeByeDPI'yi AdGuard ile nasıl kullanırım?
* ByeByeDPI'yi proxy modunda başlatın.
* ByeByeDPI'yi AdGuard dışlamalarına "Uygulama Yönetimi" sekmesinde ekleyin.
* AdGuard ayarlarında, proxy'i belirtin:
```plaintext
Proxy Türü: SOCKS5
Host: 127.0.0.1
Port: 1080 (varsayılan)
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
+38 -19
View File
@@ -1,19 +1,28 @@
# ByeByeDPI Android
Русский | [English](README-en.md) | [Türkçe](README-tr.md)
<div style="text-align: center;">
<img alt="Логотип ByeDPI" src="https://github.com/romanvht/ByeDPIAndroid/raw/master/.github/images/app.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 и перенаправляет весь трафик через него.
Для стабильной работы может потребоваться изменить настройки. Подробнее о различных настройках можно прочитать в [документации 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
---
@@ -28,19 +37,29 @@
* Для работы автозапуска активируйте пункт в настройках.
* Рекомендуется подключится один раз к VPN, чтобы принять запрос.
* После этого, при загрузке устройства, приложение автоматически запустит сервис в зависимости от настроек (VPN/Proxy)
* Если у вас Android TV/BOX, и при подключении пропадает соединение по Ethernet, активируйте режим белого списка и укажите нужные приложения, которые должны работать через VPN (например, YouTube)
* Комплексная инструкция от комьюнити [ByeByeDPI-Manual](https://github.com/HideakiTaiki/ByeByeDPI-Manual)
* Комплексная инструкция от комьюнити [ByeByeDPI-Manual](https://byebyedpi.xyz)
### Как использовать ByeByeDPI вместе с AdGuard?
* Запустите ByeByeDPI в режиме прокси.
* Добавьте ByeByeDPI в исключения 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 = 1410
versionName = "1.5.1"
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("androidx.lifecycle:lifecycle-runtime-ktx:2.8.6")
implementation("androidx.lifecycle:lifecycle-service:2.8.6")
implementation("com.google.android.material:material:1.12.0")
implementation("com.google.code.gson:gson:2.8.9")
implementation("com.takisoft.preferencex:preferencex:1.1.0")
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)
}
}
+17 -15
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,6 +33,7 @@
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/Theme.ByeDPI"
android:requestLegacyExternalStorage="true"
tools:targetApi="34">
<activity
@@ -38,9 +46,6 @@
<category android:name="android.intent.category.LAUNCHER" />
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
</intent-filter>
<meta-data
android:name="android.app.shortcuts"
android:resource="@xml/shortcuts" />
</activity>
<activity
@@ -51,10 +56,6 @@
android:noHistory="true"
android:taskAffinity=""
android:launchMode="singleTask">
<intent-filter>
<action android:name="io.github.romanvht.byedpi.TOGGLE_SERVICE" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>
<activity
@@ -64,6 +65,7 @@
<activity
android:name=".activities.TestActivity"
android:configChanges="orientation|screenSize|keyboardHidden"
android:label="@string/title_test"
android:exported="false"/>
@@ -74,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
@@ -101,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"/>
@@ -121,4 +123,4 @@
</receiver>
</application>
</manifest>
</manifest>
@@ -0,0 +1,4 @@
cloudflare.net
cloudflare.com
cloudflarecn.net
cloudflare-ech.com
-40
View File
@@ -1,40 +0,0 @@
-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
-Ku -a3 -An -Kt,h -n vk.com -d1 -d3+s -s6+s -d9+s -s12+s -d15+s -s20+s -d25+s -s30+s -d35+s -t15 -b4500 -r1+s -S -Mh,d -As -Kt,h -n vk.com -d1 -d3+s -s6+s -d9+s -s12+s -d15+s -s20+s -d25+s -s30+s -d35+s -t15 -b4500 -S -Mh,d -An
-Ku -a3 -An -Kt,h -n vk.com -o1 -d1 -Mh,d -At,r,s -Kt -n vk.com -s0+sm -d1 -s5+s -s10+s -s15+s -s20+s -t15 -b4500 -r1+s -S -As -Kt -n vk.com -s0+sm -d1 -s5+s -s10+s -s15+s -s20+s -t15 -b4500 -S -An
-d2+s -s 50+s -O 2 -a 10 -As -f20 -r2+s -At -d2 -s0+s -s5+s -s10+s -s15+s -s25+s -s35+s -s50+s -s60+s -An -n youtube.com -d2:5:2+h -f-3 -r2+sm -o2 -o50+s -r2+s -f-4 -r0+hm -t3
-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 -n google.com --ttl 3
-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 -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 -a3 -An -Kt,h -s1 -q1 -Y -At -b1000 -S -f-1 -r1+s -As -n 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 -n google.com --ttl 3 -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 -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 -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 -An
-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 -s3+s -s6+s -s9+s -s12+s -s15+s -s20+s -s30+s -An
-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 -s0 -o1 -Ar -o1 -At -f-1 -r1+s -As -b+500 -n google.com -An
-s1 -q1 -Y -Ar -s5 -o25000+s -At -f-1 -r1+s -As -s1 -o1+s -s-1 -An -b+500
-Ku -a3 -An -Kt,h -q1+s -s29+s -s30+s -s14+s -o5+s -f-1 --md5sig -Asn
-Ku -a3 -An -Kt,h -s0 -o1 -d1 -r1+s -Ar -o1 -At -f-1 -r1+s -As -An
-s1 -q1 -Y -Ar -s5 -o1+s -At -f-1 -r1+s -As -s1 -o1+s -s-1 -An
-Ku -a1 -An -Kt,h -d1 -s4 -d8 -s1+s -d5+s -s10+s -d20+s -An
-Ku -a5 -An -Kt,h -o0 -d1 -r1+s -t10 -b1500 -S -s0+s -d3+s -An
-Ku -a3 -An -Kt,h -s0 -o1 -Ar -o1 -At -f-1 -r1+s -As -An
-Ku -a1 -An -Kt,h -s1 -q1 -Art -f-1 --md5sig -r1+s -An
-Ku -a1 -An -Kt,h -s1 -q2 -a2 -Art -f-1 -r2+s -a3 -An
-Ku -a1 -An -Kt,h -s1 -q1 -a2 -Art -f-1 -r1+s -a3 -An
-Ku -a1 -An -Kt,h -s1 -d3+s -Mh,d -At -r1+s -r3+s -An
-Ku -a1 -An -Kt,h -q1+s -s29+s -o5+s -f-1 -S -As
-s0 -o1 -d1 -r1+s -Ar -o1 -At -f-1 -r1+s -As
-s1 -q1 -Y -At -b1000 -S -f-1 -r1+s -As
-s1 -q1 -Y -a2 -Art -f-1 -r1+s -a3 -Asn
-Ku -a5 -An -Kt,h -s1 -f-1 -S -b661 -An
-s1 -d1 -o1 -Ar -o3 -At -f-1 -r1+s -As
-s1 -q1 -a2 -Art -f-1 -r1+s -a3 -Asn
-s1 -o1 -Ar -o1 -At -f-1 -r1+s -As
-s0 -o1 -Ar -o1 -At -f-1 -r1+s -As
-Ku -a1 -An -Kt,h -q1 -r25+s -An
-s1 -q1 -Y -At -f-1 -r1+s -As
-Ku -a1 -An -Kt,h -d7 -s2 -An
@@ -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
+1
View File
@@ -12,3 +12,4 @@ target_compile_options(byedpi PRIVATE -std=c99 -O2 -Wall -Wno-unused -Wextra -Wn
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")
+1 -1
View File
@@ -1,2 +1,2 @@
void clear_params(void);
void clear_params(char *line, char **argv);
int main(int argc, char **argv);
+72 -35
View File
@@ -1,8 +1,10 @@
#include <string.h>
#include <jni.h>
#include <malloc.h>
#include <getopt.h>
#include <signal.h>
#include <setjmp.h>
#include <stdlib.h>
#include "byedpi/error.h"
#include "main.h"
@@ -10,65 +12,100 @@
extern int server_fd;
static int g_proxy_running = 0;
JNIEXPORT jint JNI_OnLoad(
__attribute__((unused)) JavaVM *vm,
__attribute__((unused)) void *reserved) {
return JNI_VERSION_1_6;
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
};
void reset_params(void) {
clear_params(NULL, NULL);
params = default_params;
}
JNIEXPORT jint JNICALL
Java_io_github_dovecoteescapee_byedpi_core_ByeDpiProxy_jniStartProxy(
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);
const char *arg_str = (*env)->GetStringUTFChars(env, arg, 0);
argv[i] = strdup(arg_str);
(*env)->ReleaseStringUTFChars(env, arg, arg_str);
}
g_proxy_running = 1;
optind = optreset = 1;
if (!arg) {
argv[i] = NULL;
continue;
}
const char *arg_str = (*env)->GetStringUTFChars(env, arg, 0);
argv[i] = arg_str ? strdup(arg_str) : NULL;
if (arg_str) (*env)->ReleaseStringUTFChars(env, arg, arg_str);
(*env)->DeleteLocalRef(env, arg);
}
LOG(LOG_S, "starting proxy with %d args", argc);
reset_params();
g_proxy_running = 1;
optind = 1;
int result = main(argc, argv);
for (int i = 0; i < argc; i++) {
free(argv[i]);
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;
if (result < 0) {
LOG(LOG_S, "proxy failed to start");
return result;
}
return 0;
}
JNIEXPORT jint JNICALL
Java_io_github_dovecoteescapee_byedpi_core_ByeDpiProxy_jniStopProxy(
__attribute__((unused)) JNIEnv *env,
__attribute__((unused)) jobject thiz) {
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);
LOG(LOG_S, "send shutdown to proxy");
if (!g_proxy_running) {
LOG(LOG_S, "proxy is not running");
return 0;
if (close(server_fd) == -1) {
LOG(LOG_S, "failed to close server socket (fd: %d)", server_fd);
return -1;
}
shutdown(server_fd, SHUT_RDWR);
clear_params();
LOG(LOG_S, "proxy socket force close");
g_proxy_running = 0;
return 1;
return 0;
}
@@ -1,293 +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 {
binding.statusButton.isClickable = false
val (status, _) = appStatus
when (status) {
AppStatus.Halted -> start()
AppStatus.Running -> stop()
}
binding.statusButton.postDelayed({
binding.statusButton.isClickable = true
}, 500)
}
binding.openEditorLink.setOnClickListener {
val (status, _) = appStatus
if (status == AppStatus.Halted) {
val intent = Intent(this, SettingsActivity::class.java)
val useCmdSettings = getPreferences().getBoolean("byedpi_enable_cmd_settings", false)
intent.putExtra("open_fragment", if (useCmdSettings) "cmd" else "ui")
startActivity(intent)
} else {
Toast.makeText(this, R.string.settings_unavailable, Toast.LENGTH_SHORT).show()
}
}
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)
}
}
}
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)
}
}
}
}
}
}
@@ -1,156 +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.ByeDpiCommandLineSettingsFragment
import io.github.dovecoteescapee.byedpi.fragments.ByeDpiUISettingsFragment
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)
val openFragment = intent.getStringExtra("open_fragment")
when (openFragment) {
"cmd" -> {
supportFragmentManager
.beginTransaction()
.replace(R.id.settings, ByeDpiCommandLineSettingsFragment())
.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 -> {
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,444 +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 = 10080
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)
isTesting = false
} 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")
}
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)
delay(delaySec * 1000L)
}
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,45 +0,0 @@
package io.github.dovecoteescapee.byedpi.activities
import android.net.VpnService
import android.os.Bundle
import android.util.Log
import androidx.appcompat.app.AppCompatActivity
import io.github.dovecoteescapee.byedpi.data.AppStatus
import io.github.dovecoteescapee.byedpi.data.Mode
import io.github.dovecoteescapee.byedpi.services.ServiceManager
import io.github.dovecoteescapee.byedpi.services.appStatus
import io.github.dovecoteescapee.byedpi.utility.getPreferences
import io.github.dovecoteescapee.byedpi.utility.mode
class ToggleActivity : AppCompatActivity() {
companion object {
private const val TAG = "ToggleServiceActivity"
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
toggleService()
finish()
}
private fun toggleService() {
val (status) = appStatus
when (status) {
AppStatus.Halted -> {
val mode = getPreferences().mode()
if (mode == Mode.VPN && VpnService.prepare(this) != null) {
return
}
ServiceManager.start(this, mode)
Log.i(TAG, "Toggle start")
}
AppStatus.Running -> {
ServiceManager.stop(this)
Log.i(TAG, "Toggle stop")
}
}
}
}
@@ -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,343 +0,0 @@
package io.github.dovecoteescapee.byedpi.core
import android.content.SharedPreferences
import android.util.Log
import io.github.dovecoteescapee.byedpi.utility.checkIpAndPortInCmd
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.d("ProxyPref", "CMD: $args")
val (cmdIp, cmdPort) = preferences.checkIpAndPortInCmd()
val hasIp = cmdIp != null
val hasPort = cmdPort != null
val enableHttp = preferences.getBoolean("byedpi_http_connect", false)
val hasHttp = args.contains("-G") || args.contains("--http-connect")
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("--ip $ip ")
if (!hasPort) append("--port $port ")
if (enableHttp && !hasHttp) append("--http-connect ")
}
Log.d("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,
httpConnect: Boolean? = 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 httpConnect: Boolean = httpConnect ?: false
val maxConnections: Int = maxConnections ?: 512
val bufferSize: Int = bufferSize ?: 16384
val defaultTtl: Int = defaultTtl ?: 0
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 ?: 1
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(),
httpConnect = preferences.getBoolean("byedpi_http_connect", false),
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")
}
}
}
}
val uiargs: Array<String>
get() {
val args = mutableListOf("ciadpi")
ip.takeIf { it.isNotEmpty() }?.let {
args.add("-i${it}")
}
port.takeIf { it != 0 }?.let {
args.add("-p${it}")
}
maxConnections.takeIf { it != 0 }?.let {
args.add("-c${it}")
}
bufferSize.takeIf { it != 0 }?.let {
args.add("-b${it}")
}
if (httpConnect) args.add("-G")
val protocols = mutableListOf<String>()
if (desyncHttps) protocols.add("t")
if (desyncHttp) protocols.add("h")
if (!hosts.isNullOrBlank()) {
val hostStr = ":${hosts}"
val hostBlock = mutableListOf<String>()
when (hostsMode) {
HostsMode.Blacklist -> {
hostBlock.add("-H${hostStr}")
hostBlock.add("-An")
if (protocols.isNotEmpty()) {
hostBlock.add("-K${protocols.joinToString(",")}")
}
}
HostsMode.Whitelist -> {
if (protocols.isNotEmpty()) {
hostBlock.add("-K${protocols.joinToString(",")}")
}
hostBlock.add("-H${hostStr}")
}
else -> {}
}
args.addAll(hostBlock)
} else {
if (protocols.isNotEmpty()) {
args.add("-K${protocols.joinToString(",")}")
}
}
defaultTtl.takeIf { it != 0 }?.let {
args.add("-g${it}")
}
if (noDomain) {
args.add("-N")
}
desyncMethod.let { method ->
splitPosition.takeIf { it != 0 }?.let { pos ->
var posArg = pos.toString()
if (splitAtHost) {
posArg += "+h"
}
val option = when (method) {
DesyncMethod.Split -> "-s"
DesyncMethod.Disorder -> "-d"
DesyncMethod.OOB -> "-o"
DesyncMethod.DISOOB -> "-q"
DesyncMethod.Fake -> "-f"
DesyncMethod.None -> ""
}
args.add("${option}${posArg}")
}
}
if (desyncMethod == DesyncMethod.Fake) {
fakeTtl.takeIf { it != 0 }?.let {
args.add("-t${it}")
}
fakeSni.takeIf { it.isNotEmpty() }?.let {
args.add("-n${it}")
}
fakeOffset.takeIf { it != 0 }?.let {
args.add("-O${it}")
}
}
if (desyncMethod == DesyncMethod.OOB ||
desyncMethod == DesyncMethod.DISOOB) {
args.add("-e${oobChar}")
}
val modHttpFlags = mutableListOf<String>()
if (hostMixedCase) modHttpFlags.add("h")
if (domainMixedCase) modHttpFlags.add("d")
if (hostRemoveSpaces) modHttpFlags.add("r")
if (modHttpFlags.isNotEmpty()) {
args.add("-M${modHttpFlags.joinToString(",")}")
}
if (tlsRecordSplit) {
tlsRecordSplitPosition.takeIf { it != 0 }?.let {
var tlsRecArg = it.toString()
if (tlsRecordSplitAtSni) {
tlsRecArg += "+s"
}
args.add("-r${tlsRecArg}")
}
}
if (tcpFastOpen) {
args.add("-F")
}
if (dropSack) {
args.add("-Y")
}
args.add("-An")
if (desyncUdp) {
args.add("-Ku")
udpFakeCount.takeIf { it != 0 }?.let {
args.add("-a${it}")
}
args.add("-An")
}
Log.d("ProxyPref", "UI to cmd: ${args.joinToString(" ")}")
return args.toTypedArray()
}
}
@@ -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,192 +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 historyHeader: Preference
private lateinit var clearButton: 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")
historyHeader = findPreferenceNotNull("cmd_history_header")
clearButton = findPreferenceNotNull("clear_cmd_args")
editTextPreference.setOnPreferenceChangeListener { _, newValue ->
val newCommand = newValue.toString()
if (newCommand.isNotBlank()) cmdHistoryUtils.addCommand(newCommand)
updateHistoryItems()
true
}
clearButton.setOnPreferenceClickListener {
editTextPreference.text = ""
true
}
historyHeader.setOnPreferenceClickListener {
showHistoryClearDialog()
true
}
updateHistoryItems()
}
private fun updateHistoryItems() {
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) =
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 showHistoryClearDialog() {
val options = arrayOf(
getString(R.string.cmd_history_delete_all),
getString(R.string.cmd_history_delete_unpinned),
)
AlertDialog.Builder(requireContext())
.setTitle(getString(R.string.cmd_history_menu))
.setItems(options) { _, which ->
when (which) {
0 -> deleteAllHistory()
1 -> deleteUnpinnedHistory()
}
}
.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_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()
cmdHistoryUtils.renameCommand(command.text, newName)
updateHistoryItems()
}
.setNegativeButton(getString(android.R.string.cancel), null)
.show()
}
private fun applyCommand(command: String) {
editTextPreference.text = command
}
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 copyToClipboard(command: String) {
val clipboard = requireContext().getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
val clip = ClipData.newPlainText("Command", command)
clipboard.setPrimaryClip(clip)
}
}
@@ -1,167 +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")
"tr" -> LocaleListCompat.forLanguageTags("tr")
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 (cmdIp, cmdPort) = sharedPreferences?.checkIpAndPortInCmd() ?: Pair(null, null)
proxy.isVisible = cmdIp == null && cmdPort == null
} 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,76 +0,0 @@
package io.github.dovecoteescapee.byedpi.services
import android.content.ComponentName
import android.content.Context
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.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 const val TAG = "QuickTileService"
fun updateTile(context: Context) {
requestListeningState(context, ComponentName(context, QuickTileService::class.java))
}
}
override fun onStartListening() {
super.onStartListening()
updateStatus()
}
override fun onClick() {
if (qsTile.state == Tile.STATE_UNAVAILABLE) return
unlockAndRun { handleClick() }
}
private fun handleClick() {
val (status) = appStatus
when (status) {
AppStatus.Halted -> {
val mode = getPreferences().mode()
if (mode == Mode.VPN && VpnService.prepare(this) != null) {
return
}
ServiceManager.start(this, mode)
setState(Tile.STATE_ACTIVE)
}
AppStatus.Running -> {
ServiceManager.stop(this)
setState(Tile.STATE_INACTIVE)
}
}
Log.i(TAG, "Toggle tile")
updateTile(this)
}
private fun updateStatus() {
val (status) = appStatus
if (status == AppStatus.Running) {
setState(Tile.STATE_ACTIVE)
} else {
setState(Tile.STATE_INACTIVE)
}
}
private fun setState(newState: Int) {
qsTile.apply {
state = newState
updateTile()
}
}
}
@@ -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()
@@ -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()
}
}
@@ -1,4 +1,4 @@
package io.github.dovecoteescapee.byedpi.core
package io.github.romanvht.byedpi.core
class ByeDpiProxy {
companion object {
@@ -24,4 +24,5 @@ class ByeDpiProxy {
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,61 +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 {
val appName = try {
pm.getApplicationLabel(it).toString()
} catch (e: Exception) {
it.packageName
.map { appInfo ->
lifecycleScope.async(Dispatchers.IO) {
createAppInfo(appInfo, pm, selectedApps)
}
val appIcon = try {
pm.getApplicationIcon(it.packageName)
} catch (e: Exception) {
pm.defaultActivityIcon
}
AppInfo(
appName,
it.packageName,
appIcon,
selectedApps.contains(it.packageName)
)
}
.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,17 +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 {
startForeground()
mutex.withLock {
startProxy()
updateStatus(ServiceStatus.Connected)
}
updateStatus(ServiceStatus.Connected)
} catch (e: Exception) {
Log.e(TAG, "Failed to start proxy", e)
updateStatus(ServiceStatus.Failed)
@@ -92,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)
@@ -102,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) {
@@ -124,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()
}
}
@@ -147,7 +179,18 @@ class ByeDpiProxyService : LifecycleService() {
try {
proxy.stopProxy()
proxyJob?.join()
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)
@@ -157,7 +200,7 @@ class ByeDpiProxyService : LifecycleService() {
}
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")
@@ -185,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 =
@@ -195,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,18 +123,21 @@ 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
}
try {
startForeground()
mutex.withLock {
startProxy()
startTun2Socks()
updateStatus(ServiceStatus.Connected)
}
updateStatus(ServiceStatus.Connected)
} 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")
stopTun2Socks()
updateStatus(ServiceStatus.Failed)
} 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()
}
}
@@ -166,7 +218,18 @@ class ByeDpiVpnService : LifecycleVpnService() {
try {
byeDpiProxy.stopProxy()
proxyJob?.join()
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)
@@ -179,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 {
@@ -220,6 +287,11 @@ class ByeDpiVpnService : LifecycleVpnService() {
private fun stopTun2Socks() {
Log.i(TAG, "Stopping tun2socks")
if (tunFd == null) {
Log.w(TAG, "VPN field is null, skipping")
return
}
try {
TProxyService.TProxyStopService()
} catch (e: Exception) {
@@ -234,16 +306,17 @@ class ByeDpiVpnService : LifecycleVpnService() {
try {
tunFd?.close()
tunFd = null
} 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")
@@ -272,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 =
@@ -283,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()
@@ -307,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,4 +1,4 @@
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> {
@@ -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)
}
}
}
}
}
@@ -0,0 +1,30 @@
package io.github.romanvht.byedpi.utility
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
import android.widget.Toast
import io.github.romanvht.byedpi.R
object ClipboardUtils {
fun copy(context: Context, text: String, label: String = "text", showToast: Boolean = true) {
val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as? ClipboardManager
val clip = ClipData.newPlainText(label, text)
clipboard?.setPrimaryClip(clip)
if (showToast) {
Toast.makeText(context, R.string.toast_copied, Toast.LENGTH_SHORT).show()
}
}
fun paste(context: Context): String? {
val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as? ClipboardManager
val clipData = clipboard?.primaryClip
return if (clipData != null && clipData.itemCount > 0) {
clipData.getItemAt(0).text?.toString()
} else {
null
}
}
}
@@ -0,0 +1,216 @@
package io.github.romanvht.byedpi.utility
import android.content.Context
import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
import io.github.romanvht.byedpi.data.DomainList
import java.io.File
object DomainListUtils {
private const val DOMAIN_LISTS_FILE = "domain_lists.json"
private val gson = Gson()
private fun getDefaultActiveIds(lang: String): Set<String> = when (lang) {
"tr" -> setOf("türkiye", "discord")
else -> setOf("youtube", "googlevideo")
}
fun syncLists(context: Context) {
val currentLists = getAllLists(context).toMutableList()
val builtInMap = currentLists
.filter { it.isBuiltIn }
.associateBy { it.id }
val assetFiles = context.assets.list("")?.filter {
it.startsWith("proxytest_") && it.endsWith(".sites")
} ?: emptyList()
val newBuiltInIds = mutableSetOf<String>()
for (assetFile in assetFiles) {
val id = assetFile
.removePrefix("proxytest_")
.removeSuffix(".sites")
newBuiltInIds.add(id)
val domains = context.assets.open(assetFile)
.bufferedReader()
.useLines { it.toList() }
.map { it.trim() }
.filter { it.isNotEmpty() }
val existing = builtInMap[id]
when {
existing == null -> {
currentLists.add(
DomainList(
id = id,
name = id.replaceFirstChar { it.uppercase() },
domains = domains,
isActive = id in getDefaultActiveIds(SettingsUtils.getCurrentLanguage(context)),
isBuiltIn = true
)
)
}
existing.isDeleted -> {
continue
}
existing.isModified -> {
continue
}
else -> {
val index = currentLists.indexOfFirst { it.id == id }
currentLists[index] = existing.copy(
domains = domains
)
}
}
}
currentLists.removeAll {
it.isBuiltIn && it.id !in newBuiltInIds
}
saveLists(context, currentLists)
}
fun getAllLists(context: Context): MutableList<DomainList> {
val listsFile = File(context.filesDir, DOMAIN_LISTS_FILE)
if (!listsFile.exists()) {
return mutableListOf()
}
return try {
val json = listsFile.readText()
val type = object : TypeToken<List<DomainList>>() {}.type
val lists: List<DomainList> = gson.fromJson(json, type) ?: emptyList()
lists.toMutableList()
} catch (e: Exception) {
mutableListOf()
}
}
fun getLists(context: Context): List<DomainList> {
return getAllLists(context).filter { !it.isDeleted }
}
fun getActiveDomains(context: Context): List<String> {
return getLists(context).filter { it.isActive }.flatMap { it.domains }.distinct()
}
fun saveLists(context: Context, lists: List<DomainList>) {
val listsFile = File(context.filesDir, DOMAIN_LISTS_FILE)
val json = gson.toJson(lists)
listsFile.writeText(json)
}
fun addList(context: Context, name: String, domains: List<String>): Boolean {
val lists = getAllLists(context).toMutableList()
val id = name.lowercase().replace(" ", "_")
val existing = lists.find { it.id == id }
if (existing != null) {
if (existing.isBuiltIn && existing.isDeleted) {
val index = lists.indexOf(existing)
lists[index] = existing.copy(
name = name,
domains = domains,
isDeleted = false,
isModified = true,
isActive = true
)
saveLists(context, lists)
return true
}
return false
}
lists.add(
DomainList(
id = id,
name = name,
domains = domains,
isActive = true,
isBuiltIn = false
)
)
saveLists(context, lists)
return true
}
fun updateList(context: Context, id: String, name: String, domains: List<String>): Boolean {
val lists = getAllLists(context).toMutableList()
val index = lists.indexOfFirst { it.id == id }
if (index == -1) return false
val oldList = lists[index]
lists[index] = oldList.copy(
name = name,
domains = domains,
isModified = oldList.isBuiltIn
)
saveLists(context, lists)
return true
}
fun toggleListActive(context: Context, id: String): Boolean {
val lists = getAllLists(context).toMutableList()
val index = lists.indexOfFirst { it.id == id }
if (index == -1) return false
val list = lists[index]
lists[index] = list.copy(isActive = !list.isActive)
saveLists(context, lists)
return true
}
fun deleteList(context: Context, id: String): Boolean {
val lists = getAllLists(context).toMutableList()
val index = lists.indexOfFirst { it.id == id }
if (index == -1) return false
val list = lists[index]
if (list.isBuiltIn) {
lists[index] = list.copy(
isDeleted = true,
isActive = false
)
} else {
lists.removeAt(index)
}
saveLists(context, lists)
return true
}
fun resetLists(context: Context) {
val file = File(context.filesDir, DOMAIN_LISTS_FILE)
if (file.exists()) {
file.delete()
}
syncLists(context)
}
}
@@ -1,14 +1,15 @@
package io.github.dovecoteescapee.byedpi.utility
package io.github.romanvht.byedpi.utility
import android.content.Context
import android.content.SharedPreferences
import androidx.preference.PreferenceManager
import io.github.dovecoteescapee.byedpi.data.Command
import io.github.romanvht.byedpi.data.Command
import com.google.gson.Gson
import androidx.core.content.edit
class HistoryUtils(context: Context) {
private val sharedPreferences: SharedPreferences = PreferenceManager.getDefaultSharedPreferences(context)
private val context = context.applicationContext
private val sharedPreferences: SharedPreferences = context.getPreferences()
private val historyKey = "byedpi_command_history"
private val maxHistorySize = 40
@@ -16,12 +17,15 @@ class HistoryUtils(context: Context) {
if (command.isBlank()) return
val history = getHistory().toMutableList()
val unpinned = history.filter { !it.pinned }
val search = history.find { it.text == command }
if (search == null) {
history.add(0, Command(command))
if (history.size > maxHistorySize) {
history.removeAt(maxHistorySize)
if (unpinned.isNotEmpty()) {
history.remove(unpinned.last())
}
}
}
@@ -52,6 +56,12 @@ class HistoryUtils(context: Context) {
saveHistory(history)
}
fun editCommand(command: String, newText: String) {
val history = getHistory().toMutableList()
history.find { it.text == command }?.text = newText
saveHistory(history)
}
fun getHistory(): List<Command> {
val historyJson = sharedPreferences.getString(historyKey, null)
return if (historyJson != null) {
@@ -61,9 +71,14 @@ class HistoryUtils(context: Context) {
}
}
fun getPinnedHistory(): List<Command> {
return getHistory().filter { it.pinned }
}
fun saveHistory(history: List<Command>) {
val historyJson = Gson().toJson(history)
sharedPreferences.edit().putString(historyKey, historyJson).apply()
sharedPreferences.edit { putString(historyKey, historyJson) }
ShortcutUtils.update(context)
}
fun clearAllHistory() {
@@ -0,0 +1,101 @@
package io.github.romanvht.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.romanvht.byedpi.R
import io.github.romanvht.byedpi.activities.MainActivity
import io.github.romanvht.byedpi.data.PAUSE_ACTION
import io.github.romanvht.byedpi.data.RESUME_ACTION
import io.github.romanvht.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, context.getString(R.string.service_pause_btn),
PendingIntent.getService(
context,
0,
Intent(context, service).setAction(PAUSE_ACTION),
PendingIntent.FLAG_IMMUTABLE,
)
)
.addAction(0, context.getString(R.string.service_stop_btn),
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()
fun createPauseNotification(
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, context.getString(R.string.service_start_btn),
PendingIntent.getService(
context,
0,
Intent(context, service).setAction(RESUME_ACTION),
PendingIntent.FLAG_IMMUTABLE,
)
)
.setContentIntent(
PendingIntent.getActivity(
context,
0,
Intent(context, MainActivity::class.java),
PendingIntent.FLAG_IMMUTABLE,
)
)
.build()
@@ -1,11 +1,13 @@
package io.github.dovecoteescapee.byedpi.utility
package io.github.romanvht.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
import io.github.romanvht.byedpi.data.Mode
private const val DEFAULT_CMD_ARGS = "-o1 -a1 -r-5+se"
val PreferenceFragmentCompat.sharedPreferences
get() = preferenceScreen.sharedPreferences
@@ -13,6 +15,12 @@ val PreferenceFragmentCompat.sharedPreferences
fun Context.getPreferences(): SharedPreferences =
PreferenceManager.getDefaultSharedPreferences(this)
fun SharedPreferences.getIntStringNotNull(key: String, defValue: Int): Int =
getString(key, defValue.toString())?.toIntOrNull() ?: defValue
fun SharedPreferences.getLongStringNotNull(key: String, defValue: Long): Long =
getString(key, defValue.toString())?.toLongOrNull() ?: defValue
fun SharedPreferences.getStringNotNull(key: String, defValue: String): String =
getString(key, defValue) ?: defValue
@@ -26,11 +34,17 @@ fun SharedPreferences.getSelectedApps(): List<String> {
return getStringSet("selected_apps", emptySet())?.toList() ?: emptyList()
}
fun SharedPreferences.checkIpAndPortInCmd(): Pair<String?, String?> {
val cmdEnable = getBoolean("byedpi_enable_cmd_settings", false)
if (!cmdEnable) return Pair(null, null)
fun SharedPreferences.getCmdEnable(): Boolean {
return getBoolean("byedpi_enable_cmd_settings", true)
}
val cmdArgs = getString("byedpi_cmd_args", "")?.let { shellSplit(it) } ?: emptyList()
fun SharedPreferences.getCmdArgs(): String {
return getStringNotNull("byedpi_cmd_args", DEFAULT_CMD_ARGS)
}
fun SharedPreferences.checkIpAndPortInCmd(): Pair<String?, String?> {
if (!getCmdEnable()) return Pair(null, null)
val cmdArgs = shellSplit(getCmdArgs())
fun getArgValue(argsList: List<String>, keys: List<String>): String? {
for (i in argsList.indices) {
@@ -0,0 +1,256 @@
package io.github.romanvht.byedpi.utility
import android.content.Context
import android.net.Uri
import android.os.Handler
import android.os.Looper
import android.util.Log
import android.widget.Toast
import androidx.appcompat.app.AppCompatDelegate
import androidx.core.content.edit
import androidx.core.os.LocaleListCompat
import com.google.gson.Gson
import io.github.romanvht.byedpi.BuildConfig
import io.github.romanvht.byedpi.R
import io.github.romanvht.byedpi.data.AppSettings
object SettingsUtils {
private const val TAG = "SettingsUtils"
private val separatePreferenceKeys = setOf("byedpi_command_history", "selected_apps")
enum class Section {
SETTINGS,
HISTORY,
APPS,
DOMAIN_LISTS
}
fun getCurrentLanguage(context: Context): String {
val lang = context.getPreferences().getStringNotNull("language", "system")
if (lang != "system") return lang
val locales = AppCompatDelegate.getApplicationLocales()
return locales[0]?.language ?: java.util.Locale.getDefault().language
}
fun setLang(lang: String) {
val appLocale = localeByName(lang) ?: LocaleListCompat.getEmptyLocaleList()
if (AppCompatDelegate.getApplicationLocales().toLanguageTags() != appLocale.toLanguageTags()) {
AppCompatDelegate.setApplicationLocales(appLocale)
}
}
private fun localeByName(lang: String): LocaleListCompat? = when (lang) {
"system" -> LocaleListCompat.getEmptyLocaleList()
"ru" -> LocaleListCompat.forLanguageTags("ru")
"en" -> LocaleListCompat.forLanguageTags("en")
"tr" -> LocaleListCompat.forLanguageTags("tr")
"kk" -> LocaleListCompat.forLanguageTags("kk")
"vi" -> LocaleListCompat.forLanguageTags("vi")
else -> {
Log.w(TAG, "Invalid value for language: $lang")
null
}
}
fun setTheme(name: String) {
val appTheme = themeByName(name) ?: AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM
if (AppCompatDelegate.getDefaultNightMode() != appTheme) {
AppCompatDelegate.setDefaultNightMode(appTheme)
}
}
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
}
}
fun exportSettings(context: Context, uri: Uri, sections: Set<Section>) {
try {
val prefs = context.getPreferences()
val export = AppSettings(
app = BuildConfig.APPLICATION_ID,
version = BuildConfig.VERSION_NAME,
history = if (Section.HISTORY in sections) {
HistoryUtils(context).getHistory()
} else {
null
},
apps = if (Section.APPS in sections) prefs.getSelectedApps() else null,
domainLists = if (Section.DOMAIN_LISTS in sections) {
DomainListUtils.getAllLists(context)
} else {
null
},
settings = if (Section.SETTINGS in sections) {
prefs.all.filterKeys { it !in separatePreferenceKeys }
} else {
null
}
)
val json = Gson().toJson(export)
context.contentResolver.openOutputStream(uri)?.use { outputStream ->
outputStream.write(json.toByteArray())
}
} catch (e: Exception) {
Log.e(TAG, "Failed to export settings", e)
Handler(Looper.getMainLooper()).post {
Toast.makeText(context, R.string.export_failed, Toast.LENGTH_SHORT).show()
}
}
}
fun readSettings(context: Context, uri: Uri): AppSettings? {
return try {
val json = context.contentResolver.openInputStream(uri)?.use { inputStream ->
inputStream.bufferedReader().readText()
} ?: return null
val settings = Gson().fromJson(json, AppSettings::class.java)
if (settings?.app == BuildConfig.APPLICATION_ID) {
settings
} else {
Toast.makeText(context, R.string.import_failed, Toast.LENGTH_LONG).show()
null
}
} catch (e: Exception) {
Log.e(TAG, "Failed to read settings", e)
Toast.makeText(context, R.string.import_failed, Toast.LENGTH_SHORT).show()
null
}
}
fun getAvailableSections(settings: AppSettings): Set<Section> = buildSet {
if (settings.settings != null) add(Section.SETTINGS)
if (settings.history != null) add(Section.HISTORY)
if (settings.apps != null) add(Section.APPS)
if (settings.domainLists != null) add(Section.DOMAIN_LISTS)
}
fun importSettings(
context: Context,
import: AppSettings,
sections: Set<Section>,
onRestart: () -> Unit
) {
try {
val prefs = context.getPreferences()
if (Section.SETTINGS in sections && import.settings != null) {
prefs.edit(commit = true) {
prefs.all.keys
.filter { it !in separatePreferenceKeys }
.forEach { remove(it) }
import.settings
.filterKeys { it !in separatePreferenceKeys }
.forEach { (key, value) ->
when (value) {
is Int -> putInt(key, value)
is Boolean -> putBoolean(key, value)
is String -> putString(key, value)
is Float -> putFloat(key, value)
is Long -> putLong(key, value)
is Double -> {
when (value) {
value.toInt().toDouble() -> {
putInt(key, value.toInt())
}
value.toLong().toDouble() -> {
putLong(key, value.toLong())
}
else -> {
putFloat(key, value.toFloat())
}
}
}
is Collection<*> -> {
if (value.all { it is String }) {
@Suppress("UNCHECKED_CAST")
putStringSet(key, (value as Collection<String>).toSet())
}
}
}
}
}
}
if (Section.APPS in sections && import.apps != null) {
prefs.edit(commit = true) { putStringSet("selected_apps", import.apps.toSet()) }
}
if (Section.HISTORY in sections && import.history != null) {
HistoryUtils(context).saveHistory(import.history)
}
if (Section.DOMAIN_LISTS in sections && import.domainLists != null) {
val normalized = import.domainLists.map {
if (it.isBuiltIn) {
it.copy(
isModified = it.isModified,
isDeleted = it.isDeleted
)
} else {
it.copy(
isModified = false,
isDeleted = false
)
}
}
DomainListUtils.saveLists(context, normalized)
}
if (Section.SETTINGS in sections) {
val newLang = prefs.getString("language", "system") ?: "system"
val newTheme = prefs.getString("app_theme", "system") ?: "system"
setLang(newLang)
setTheme(newTheme)
}
Handler(Looper.getMainLooper()).post {
onRestart()
}
} catch (e: Exception) {
Log.e(TAG, "Failed to import settings", e)
Handler(Looper.getMainLooper()).post {
Toast.makeText(context, R.string.import_failed, Toast.LENGTH_SHORT).show()
}
}
}
fun resetSettings(context: Context, onRestart: () -> Unit) {
try {
val prefs = context.getPreferences()
prefs.edit(commit = true) {
clear()
}
HistoryUtils(context).saveHistory(emptyList())
DomainListUtils.resetLists(context)
setLang("system")
setTheme("system")
Handler(Looper.getMainLooper()).post {
onRestart()
}
} catch (e: Exception) {
Log.e(TAG, "Failed to reset settings", e)
Handler(Looper.getMainLooper()).post {
Toast.makeText(context, "Failed to reset settings", Toast.LENGTH_SHORT).show()
}
}
}
}
@@ -0,0 +1,63 @@
package io.github.romanvht.byedpi.utility
import android.content.Context
import android.content.Intent
import android.content.pm.ShortcutInfo
import android.content.pm.ShortcutManager
import android.graphics.drawable.Icon
import android.os.Build
import io.github.romanvht.byedpi.R
import io.github.romanvht.byedpi.activities.ToggleActivity
object ShortcutUtils {
fun update(context: Context) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) {
val shortcutManager = context.getSystemService(ShortcutManager::class.java)
val shortcuts = mutableListOf<ShortcutInfo>()
val toggleIntent = Intent(context, ToggleActivity::class.java).apply {
action = Intent.ACTION_VIEW
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
}
val toggleShortcut = ShortcutInfo.Builder(context, "toggle_service")
.setShortLabel(context.getString(R.string.toggle_connect))
.setLongLabel(context.getString(R.string.toggle_connect))
.setIcon(Icon.createWithResource(context, R.drawable.ic_toggle))
.setIntent(toggleIntent)
.build()
shortcuts.add(toggleShortcut)
if (context.getPreferences().getCmdEnable()) {
val history = HistoryUtils(context)
val pinned = history.getHistory().filter { it.pinned }.take(3)
pinned.forEachIndexed { index, strategy ->
val strategyIntent = Intent(context, ToggleActivity::class.java).apply {
action = Intent.ACTION_VIEW
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
putExtra("strategy", strategy.text)
}
val fullLabel = strategy.name?.takeIf { it.isNotBlank() } ?: strategy.text
val shortLabel = if (fullLabel.length > 15) fullLabel.take(15) + "..." else fullLabel
val longLabel = if (fullLabel.length > 30) fullLabel.take(30) + "..." else fullLabel
val commandShortcut = ShortcutInfo.Builder(context, "strategy_$index")
.setShortLabel(shortLabel)
.setLongLabel(longLabel)
.setIcon(Icon.createWithResource(context, R.drawable.ic_pin))
.setIntent(strategyIntent)
.build()
shortcuts.add(commandShortcut)
}
}
shortcutManager.dynamicShortcuts = shortcuts
}
}
}

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