Compare commits

...
133 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
168 changed files with 5215 additions and 2161 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
-6
View File
@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="AndroidProjectSystem">
<option name="providerId" value="com.android.tools.idea.GradleProjectSystem" />
</component>
</project>
-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="21" />
</component>
</project>
-18
View File
@@ -1,18 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="deploymentTargetSelector">
<selectionStates>
<SelectionState runConfigName="debug">
<option name="selectionMode" value="DROPDOWN" />
<DropdownSelection timestamp="2025-08-09T11:47:28.114175400Z">
<Target type="DEFAULT_BOOT">
<handle>
<DeviceId pluginId="LocalEmulator" identifier="path=C:\Users\romanvht\.android\avd\Medium_Phone.avd" />
</handle>
</Target>
</DropdownSelection>
<DialogSelection />
</SelectionState>
</selectionStates>
</component>
</project>
-13
View File
@@ -1,13 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="DeviceTable">
<option name="columnSorters">
<list>
<ColumnSorterState>
<option name="column" value="Name" />
<option name="order" value="ASCENDING" />
</ColumnSorterState>
</list>
</option>
</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="testRunner" value="CHOOSE_PER_TEST" />
<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>
</GradleProjectSettings>
</option>
</component>
</project>
-10
View File
@@ -1,10 +0,0 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="SpellCheckingInspection" enabled="false" level="TYPO" enabled_by_default="false">
<option name="processCode" value="true" />
<option name="processLiterals" value="true" />
<option name="processComments" value="true" />
</inspection_tool>
</profile>
</component>
-6
View File
@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="KotlinJpsPluginSettings">
<option name="version" value="1.9.22" />
</component>
</project>
-10
View File
@@ -1,10 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectMigrations">
<option name="MigrateToGradleLocalJavaHome">
<set>
<option value="$PROJECT_DIR$" />
</set>
</option>
</component>
</project>
-10
View File
@@ -1,10 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="jbr-21" 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>
-17
View File
@@ -1,17 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="RunConfigurationProducerService">
<option name="ignoredProducers">
<set>
<option value="com.intellij.execution.junit.AbstractAllInDirectoryConfigurationProducer" />
<option value="com.intellij.execution.junit.AllInPackageConfigurationProducer" />
<option value="com.intellij.execution.junit.PatternConfigurationProducer" />
<option value="com.intellij.execution.junit.TestInClassConfigurationProducer" />
<option value="com.intellij.execution.junit.UniqueIdConfigurationProducer" />
<option value="com.intellij.execution.junit.testDiscovery.JUnitTestDiscoveryConfigurationProducer" />
<option value="org.jetbrains.kotlin.idea.junit.KotlinJUnitRunConfigurationProducer" />
<option value="org.jetbrains.kotlin.idea.junit.KotlinPatternConfigurationProducer" />
</set>
</option>
</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>
+40 -35
View File
@@ -16,57 +16,62 @@
</p>
</div>
An Android application that locally runs ByeDPI and routes all traffic through it.
An Android application that runs ByeDPI locally and redirects all traffic through it.
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).
For stable operation, you may need to adjust the settings. More information about the available options can be found in the [ByeDPI documentation](https://github.com/hufrea/byedpi/blob/main/README.md).
This application is **not** a VPN. It uses 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 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 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).
* Comprehensive instruction from the community [ByeByeDPI-Manual (En)](https://github.com/BDManual/ByeByeDPI-Manual)
### 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 submodules:
```bash
git clone --recurse-submodules
```
2. Run the build script from the root of the repository:
```bash
./gradlew assembleRelease
```
3. The APK will be in `app/build/outputs/apk/release/`
> P.S.: hev_socks5_tunnel will not build under Windows, you will need to use WSL
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
### Signature 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)
+35 -35
View File
@@ -16,62 +16,62 @@
</p>
</div>
ByeDPI'yi yerel olarak çalıştıran ve tüm trafiği bunun üzerinden yönlendiren bir Android uygulaması.
ByeDPIyi yerel olarak çalıştıran ve tüm trafiği üzerinden yönlendiren bir Android uygulaması.
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.
Kararlı çalışması için ayarların değiştirilmesi gerekebilir. Çeşitli ayarlar hakkında daha fazla bilgiyi [ByeDPI belgelerinde](https://github.com/hufrea/byedpi/blob/main/README.md) bulabilirsiniz.
Bu uygulama **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 bir VPN değildir. Trafiği yönlendirmek için Androidin VPN modunu kullanır, ancak hbir veriyi uzak bir sunucuya göndermez. Trafiği şifrelemez ve 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, Discord).
* Topluluktan kapsamlı talimatlar [ByeByeDPI-Manual (İngilizce)](https://github.com/BDManual/ByeByeDPI-Manual)
### Türkiye İle İlgili
* Uygulama Türkiyede uygulanan DPI'ı aşmak için şuanlık yeterlidir. İlk başta çalıştırdığınızda uygulama DPI'ı aşamayabilir. UI editöründen rastgele taktikler deneyebilirsiniz veya şuan Deneysel özellik olan proxy modundan bazı argümanlar alıp onları Komut satırı editöründe deneyebilirsiniz.
* Türkiye ile alakalı destek için Discord: [nyaex](https://github.com/nyaexx)
* 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:
### Oluşturma
1. Depoyu alt modüllerle klonlayın:
```bash
git clone --recurse-submodules
```
2. Depo kökünden derleme betiğini çalıştırın:
```bash
./gradlew assemblyRelease
```
3. APK `app/build/outputs/apk/release/` dizininde olacaktır
> Not: hev_socks5_tunnel Windows altında derlenmeyecektir, WSL kullanmanız gerekecektir
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)
* [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
+8 -15
View File
@@ -18,11 +18,11 @@
Приложение для 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
---
@@ -37,18 +37,7 @@
* Для работы автозапуска активируйте пункт в настройках.
* Рекомендуется подключится один раз к VPN, чтобы принять запрос.
* После этого, при загрузке устройства, приложение автоматически запустит сервис в зависимости от настроек (VPN/Proxy)
* Если у вас Android TV/BOX, и при подключении пропадает соединение по Ethernet, активируйте режим белого списка и укажите нужные приложения, которые должны работать через VPN (например, YouTube)
* Комплексная инструкция от комьюнити [ByeByeDPI-Manual](https://github.com/BDManual/ByeByeDPI-Manual)
### Как использовать ByeByeDPI вместе с AdGuard?
* Запустите ByeByeDPI в режиме прокси.
* Добавьте ByeByeDPI в исключения AdGuard на вкладке "Управление приложениями".
* В настройках AdGuard укажите прокси:
```plaintext
Тип прокси: SOCKS5
Хост: 127.0.0.1
Порт: 1080 (по умолчанию)
```
* Комплексная инструкция от комьюнити [ByeByeDPI-Manual](https://byebyedpi.xyz)
### Сборка
1. Клонируйте репозиторий с сабмодулями:
@@ -64,9 +53,13 @@ git clone --recurse-submodules
> P.S.: hev_socks5_tunnel не соберется под Windows, вам нужно будет использовать WSL
### Хеш подписи
SHA-256:
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)
+5 -9
View File
@@ -8,7 +8,7 @@ plugins {
val abis = setOf("armeabi-v7a", "arm64-v8a", "x86", "x86_64")
android {
namespace = "io.github.dovecoteescapee.byedpi"
namespace = "io.github.romanvht.byedpi"
compileSdk = 36
defaultConfig {
@@ -16,8 +16,8 @@ android {
minSdk = 21
//noinspection OldTargetApi
targetSdk = 34
versionCode = 1690
versionName = "1.6.9"
versionCode = 1770
versionName = "1.7.7"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
@@ -28,6 +28,7 @@ android {
buildFeatures {
buildConfig = true
viewBinding = true
}
buildTypes {
@@ -35,6 +36,7 @@ android {
buildConfigField("String", "VERSION_NAME", "\"${defaultConfig.versionName}\"")
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
isMinifyEnabled = true
isShrinkResources = true
}
debug {
buildConfigField("String", "VERSION_NAME", "\"${defaultConfig.versionName}-debug\"")
@@ -59,10 +61,6 @@ android {
}
}
buildFeatures {
viewBinding = true
}
// https://android.izzysoft.de/articles/named/iod-scan-apkchecks?lang=en#blobs
dependenciesInfo {
// Disables dependency metadata when building APKs.
@@ -90,8 +88,6 @@ dependencies {
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")
implementation("com.takisoft.preferencex:preferencex:1.1.0")
implementation("com.squareup.okhttp3:okhttp:5.3.2")
testImplementation("junit:junit:4.13.2")
androidTestImplementation("androidx.test.ext:junit:1.3.0")
androidTestImplementation("androidx.test.espresso:espresso-core:3.7.0")
+34 -2
View File
@@ -1,2 +1,34 @@
-keep,allowoptimization class io.github.dovecoteescapee.byedpi.** { *; }
-dontobfuscate
# Keep JNI methods
-keepclasseswithmembernames class * {
native <methods>;
}
-keep class io.github.romanvht.byedpi.core.ByeDpiProxy { *; }
-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.** { *; }
-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)
}
}
+8 -8
View File
@@ -6,8 +6,11 @@
<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"
@@ -73,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
@@ -123,4 +123,4 @@
</receiver>
</application>
</manifest>
</manifest>
@@ -0,0 +1,6 @@
rutracker.org
nyaa.si
rutor.org
nnmclub.to
speedtest.net
ookla.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
+25 -25
View File
@@ -1,57 +1,57 @@
-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 -Y -a1 -Ar -s5 -o1+s -d3+s -s6+s -d9+s -s12+s -d15+s -s20+s -d25+s -s30+s -d35+s -a1
-s1 -o1 -a1 -Y -Ar -s5 -o1+s -a1 -At -f-1 -r1+s -a1 -As -s1 -o1+s -s-1 -a1
-s1 -d1 -a1 -Y -Ar -d5 -o1+s -a1 -At -f-1 -r1+s -a1 -As -d1 -o1+s -s-1 -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
-s1 -q1 -a1 -Y -Ar -a1 -s5 -o2 -At -f-1 -r1+s -a1 -As -s1 -o1+s -s-1 -a1
-s1 -q1 -a1 -Ar -s5 -o1+s -a1 -At -f-1 -d1+s -a1 -As -s1 -o1+s -s-1 -a1
-s1 -q1 -a1 -Ar -s5 -o2 -a1 -At -f-1 -r1+s -a1 -As -s1 -o1+s -s-1 -a1
-d1 -s1+s -d1+s -s3+s -d6+s -s12+s -d14+s -s20+s -d24+s -s30+s -a1
-s1 -q1 -a1 -Y -At -a1 -S -f-1 -r1+s -a1 -As -d1+s -O1 -s29+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
-s1 -d1 -r1+s -a1 -Ar -o1 -a1 -At -f-1 -r1+s -a1
-s1 -q1 -r1+s -a1 -Ar -o1 -a1 -At -f-1 -r1+s -a1
-s1 -o1 -r1+s -a1 -Ar -o1 -a1 -At -f-1 -r1+s -a1
--fake -1 --ttl 8 --split 1+s --disorder 3+s -a1
-n {sni} -Qr -f6+nr -d2 -d11 -f9+hm -o3 -t7 -a1
-s1 -d1 -o1 -a1 -Ar -o3 -a1 -At -f-1 -r1+s -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
-d9+s -q20+s -s 25+s -t5 -At,r,s -r1+h -a1
-s1 -o1 -a1 -Ar -o1 -a1 -At -f-1 -r1+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
-s1 -q1 -Y -a1 -At,r,s -f-1 -r1+s -a1
-s1 -o1 -Y -a1 -At,r,s -f-1 -r1+s -a1
-s1 -d1 -Y -a1 -At,r,s -f-1 -r1+s -a1
-n {sni} -Qr -f209 -s1+sm -R1-3 -a1
-s1 -q1 -a1 -At,r,s -f-1 -r1+s -a1
-s1 -o1 -a1 -At,r,s -f-1 -r1+s -a1
-s1 -d1 -a1 -At,r,s -f-1 -r1+s -a1
-s4+sn -r9+s -Qr -n {sni} -S -a1
-o1 -d1 -r1+s -S -s1+s -d3+s -a1
-d1 -r1+s -f-1 -S -t8 -o3+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
-q1 -a1 -At,r,s -d1 -a1
-n {sni} -Qr -d1 -f-1
-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
-s1+s -d3+s -a1
-s1 -f-1 -S -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
@@ -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
+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);
+4 -5
View File
@@ -14,7 +14,6 @@ static int g_proxy_running = 0;
struct params default_params = {
.await_int = 10,
.cache_ttl = 100800,
.ipv6 = 1,
.resolve = 1,
.udp = 1,
@@ -30,12 +29,12 @@ struct params default_params = {
};
void reset_params(void) {
clear_params();
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;
@@ -82,7 +81,7 @@ Java_io_github_dovecoteescapee_byedpi_core_ByeDpiProxy_jniStartProxy(JNIEnv *env
}
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_jniStopProxy(__attribute__((unused)) JNIEnv *env, __attribute__((unused)) jobject thiz) {
LOG(LOG_S, "send shutdown to proxy");
if (!g_proxy_running) {
@@ -97,7 +96,7 @@ Java_io_github_dovecoteescapee_byedpi_core_ByeDpiProxy_jniStopProxy(__attribute_
}
JNIEXPORT jint JNICALL
Java_io_github_dovecoteescapee_byedpi_core_ByeDpiProxy_jniForceClose(__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);
if (close(server_fd) == -1) {
@@ -1,155 +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 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
import androidx.core.content.edit
class SettingsActivity : BaseActivity() {
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()
prefs.edit {
clear()
import.settings.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)
}
}
putStringSet("selected_apps", import.apps.toSet())
}
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,28 +0,0 @@
package io.github.dovecoteescapee.byedpi.activities
import android.os.Bundle
import android.view.MenuItem
import io.github.dovecoteescapee.byedpi.R
import io.github.dovecoteescapee.byedpi.fragments.ProxyTestSettingsFragment
class TestSettingsActivity : BaseActivity() {
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,336 +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", "-Ku -a1 -An -o1 -At,r,s -d1"),
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 ?: true
val desyncMethod: DesyncMethod = desyncMethod ?: DesyncMethod.OOB
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 tcpFastOpen: Boolean = tcpFastOpen ?: false
val udpFakeCount: Int = udpFakeCount ?: 1
val dropSack: Boolean = dropSack ?: false
val fakeOffset: Int = byedpiFakeOffset ?: 0
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()
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", true),
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),
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(),
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
}
},
)
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,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,69 +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)
setEditTestPreferenceListenerInt(
"byedpi_proxytest_delay",
0,
10
)
setEditTestPreferenceListenerInt(
"byedpi_proxytest_requests",
1,
20
)
setEditTestPreferenceListenerInt(
"byedpi_proxytest_timeout",
1,
15
)
setEditTestPreferenceListenerDomain(
"byedpi_proxytest_sni"
)
updatePreferences()
}
override fun onResume() {
super.onResume()
sharedPreferences?.registerOnSharedPreferenceChangeListener(preferenceListener)
}
override fun onPause() {
super.onPause()
sharedPreferences?.unregisterOnSharedPreferenceChangeListener(preferenceListener)
}
private fun updatePreferences() {
val switchUserCommands = findPreferenceNotNull<SwitchPreference>("byedpi_proxytest_usercommands")
val textUserDomains = findPreferenceNotNull<EditTextPreference>("byedpi_proxytest_domains")
val textUserCommands = findPreferenceNotNull<EditTextPreference>("byedpi_proxytest_commands")
val domainLists = findPreferenceNotNull<MultiSelectListPreference>("byedpi_proxytest_domain_lists")
val setUserCommands = { enable: Boolean -> textUserCommands.isEnabled = enable }
textUserDomains.isEnabled = domainLists.values?.contains("custom") == true
setUserCommands(switchUserCommands.isChecked)
if (domainLists.values?.isNotEmpty() == true) {
domainLists.summary = domainLists.values.joinToString("\n")
}
}
}
@@ -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,87 +0,0 @@
package io.github.dovecoteescapee.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.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"
}
private var appTile: Tile? = null
override fun onTileAdded() {
super.onTileAdded()
Log.i(TAG, "Tile added")
}
override fun onTileRemoved() {
super.onTileRemoved()
Log.i(TAG, "Tile removed")
}
override fun onStartListening() {
super.onStartListening()
appTile = qsTile
updateStatus()
}
override fun onStopListening() {
super.onStopListening()
appTile = null
}
override fun onClick() {
super.onClick()
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")
}
private fun updateStatus() {
val (status) = appStatus
if (status == AppStatus.Running) {
setState(Tile.STATE_ACTIVE)
} else {
setState(Tile.STATE_INACTIVE)
}
}
private fun setState(newState: Int) {
appTile?.apply {
state = newState
updateTile()
}
}
}
@@ -1,87 +0,0 @@
package io.github.dovecoteescapee.byedpi.utility
import android.util.Log
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.withContext
import okhttp3.OkHttpClient
import okhttp3.Request
import java.net.InetSocketAddress
import java.net.Proxy
import java.util.concurrent.TimeUnit
class SiteCheckUtils(
private val proxyIp: String,
private val proxyPort: Int
) {
private fun createClient(timeout: Long) = OkHttpClient.Builder()
.proxy(Proxy(Proxy.Type.SOCKS, InetSocketAddress(proxyIp, proxyPort)))
.connectionPool(okhttp3.ConnectionPool(0, 1, TimeUnit.NANOSECONDS))
.connectTimeout(timeout, TimeUnit.SECONDS)
.readTimeout(timeout, TimeUnit.SECONDS)
.writeTimeout(timeout, TimeUnit.SECONDS)
.callTimeout(timeout, TimeUnit.SECONDS)
.followSslRedirects(true)
.followRedirects(true)
.build()
suspend fun checkSitesAsync(
sites: List<String>,
requestsCount: Int,
requestTimeout: Long,
fullLog: Boolean,
onSiteChecked: ((String, Int, Int) -> Unit)? = null
): List<Pair<String, Int>> {
return withContext(Dispatchers.IO) {
val client = createClient(requestTimeout)
sites.map { site ->
async {
val successCount = checkSiteAccess(client, site, requestsCount)
if (fullLog) {
onSiteChecked?.invoke(site, successCount, requestsCount)
}
site to successCount
}
}.awaitAll()
}
}
private suspend fun checkSiteAccess(
client: OkHttpClient,
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("SiteChecker", "Attempt ${attempt + 1}/$requestsCount for $site")
try {
val request = Request.Builder().url(formattedUrl).build()
client.newCall(request).execute().use { response ->
val declaredLength = response.body.contentLength()
val actualLength = response.body.bytes().size.toLong()
val responseCode = response.code
if (declaredLength <= 0 || actualLength >= declaredLength) {
Log.i("SiteChecker", "Response for $site: $responseCode, Declared: $declaredLength, Actual: $actualLength")
responseCount++
} else {
Log.w("SiteChecker", "Block detected for $site, Declared: $declaredLength, Actual: $actualLength")
}
response.body.close()
}
} catch (e: Exception) {
Log.e("SiteChecker", "Error accessing $site: ${e.message}")
}
}
responseCount
}
}
@@ -1,12 +1,14 @@
package io.github.dovecoteescapee.byedpi.activities
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.dovecoteescapee.byedpi.fragments.MainSettingsFragment
import io.github.dovecoteescapee.byedpi.utility.getPreferences
import io.github.dovecoteescapee.byedpi.utility.getStringNotNull
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() {
@@ -14,10 +16,10 @@ abstract class BaseActivity : AppCompatActivity() {
val prefs = getPreferences()
val lang = prefs.getStringNotNull("language", "system")
MainSettingsFragment.setLang(lang)
SettingsUtils.setLang(lang)
val theme = prefs.getStringNotNull("app_theme", "system")
MainSettingsFragment.setTheme(theme)
SettingsUtils.setTheme(theme)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
DynamicColors.applyToActivityIfAvailable(this)
@@ -26,4 +28,9 @@ abstract class BaseActivity : AppCompatActivity() {
super.onCreate(savedInstanceState)
}
}
protected fun setupToolbar() {
val toolbar: MaterialToolbar = findViewById(R.id.toolbar)
setSupportActionBar(toolbar)
}
}
@@ -1,4 +1,4 @@
package io.github.dovecoteescapee.byedpi.activities
package io.github.romanvht.byedpi.activities
import android.Manifest
import android.annotation.SuppressLint
@@ -13,26 +13,35 @@ 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.dovecoteescapee.byedpi.R
import io.github.dovecoteescapee.byedpi.data.*
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 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 {
@@ -124,6 +133,9 @@ class MainActivity : BaseActivity() {
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
setupToolbar()
historyUtils = HistoryUtils(this)
val intentFilter = IntentFilter().apply {
addAction(STARTED_BROADCAST)
@@ -138,8 +150,8 @@ class MainActivity : BaseActivity() {
registerReceiver(receiver, intentFilter)
}
binding.statusButton.setOnClickListener {
binding.statusButton.isClickable = false
binding.statusButtonCard.setOnClickListener {
binding.statusButtonCard.isClickable = false
val (status, _) = appStatus
when (status) {
@@ -147,26 +159,63 @@ class MainActivity : BaseActivity() {
AppStatus.Running -> stop()
}
binding.statusButton.postDelayed({
binding.statusButton.isClickable = true
binding.statusButtonCard.postDelayed({
binding.statusButtonCard.isClickable = true
}, 1000)
}
binding.openEditorLink.setOnClickListener {
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)
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()
}
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) {
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) {
@@ -179,6 +228,7 @@ class MainActivity : BaseActivity() {
override fun onResume() {
super.onResume()
updateStatus()
updateStrategyButton()
}
override fun onDestroy() {
@@ -186,6 +236,18 @@ class MainActivity : BaseActivity() {
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
@@ -195,16 +257,6 @@ class MainActivity : BaseActivity() {
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 {
@@ -218,7 +270,7 @@ class MainActivity : BaseActivity() {
}
R.id.action_close_app -> {
ServiceManager.stop(this)
if (status == AppStatus.Running) stop()
finishAffinity()
android.os.Process.killProcess(android.os.Process.myPid())
exitProcess(0)
@@ -259,32 +311,156 @@ class MainActivity : BaseActivity() {
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)
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 -> {
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)
binding.statusButton.setText(R.string.vpn_disconnect)
}
Mode.Proxy -> {
binding.statusText.setText(R.string.proxy_up)
binding.statusButton.setText(R.string.proxy_stop)
}
}
}
}
}
}
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)
}
}
@@ -1,45 +1,49 @@
package io.github.dovecoteescapee.byedpi.activities
package io.github.romanvht.byedpi.activities
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Intent
import android.net.VpnService
import android.os.Bundle
import android.text.SpannableString
import android.text.Spanned
import android.text.method.LinkMovementMethod
import android.text.style.ClickableSpan
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.lifecycle.lifecycleScope
import io.github.dovecoteescapee.byedpi.R
import io.github.dovecoteescapee.byedpi.data.Mode
import io.github.dovecoteescapee.byedpi.data.AppStatus
import io.github.dovecoteescapee.byedpi.services.appStatus
import io.github.dovecoteescapee.byedpi.services.ServiceManager
import io.github.dovecoteescapee.byedpi.utility.HistoryUtils
import io.github.dovecoteescapee.byedpi.utility.getPreferences
import io.github.dovecoteescapee.byedpi.utility.SiteCheckUtils
import io.github.dovecoteescapee.byedpi.utility.getIntStringNotNull
import io.github.dovecoteescapee.byedpi.utility.getLongStringNotNull
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.dovecoteescapee.byedpi.utility.getStringNotNull
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 scrollTextView: ScrollView
private lateinit var strategiesRecyclerView: RecyclerView
private lateinit var progressTextView: TextView
private lateinit var resultsTextView: 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
@@ -48,6 +52,8 @@ class TestActivity : BaseActivity() {
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)
@@ -60,31 +66,46 @@ class TestActivity : BaseActivity() {
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)
scrollTextView = findViewById(R.id.scrollView)
strategiesRecyclerView = findViewById(R.id.strategiesRecyclerView)
startStopButton = findViewById(R.id.startStopButton)
resultsTextView = findViewById(R.id.resultsTextView)
progressTextView = findViewById(R.id.progressTextView)
disclaimerTextView = findViewById(R.id.disclaimerTextView)
resultsTextView.movementMethod = LinkMovementMethod.getInstance()
strategyAdapter = StrategyResultAdapter(this,
onApply = { command ->
addToHistory(command)
}
)
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)
}
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
}
}
@@ -104,7 +125,14 @@ class TestActivity : BaseActivity() {
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()
}
})
@@ -119,6 +147,10 @@ class TestActivity : BaseActivity() {
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)
@@ -161,41 +193,49 @@ class TestActivity : BaseActivity() {
cmds = loadCmds()
if (sites.isEmpty()) {
resultsTextView.text = ""
appendTextToResults("${getString(R.string.test_settings_domain_empty)}\n")
Toast.makeText(this, R.string.test_settings_domain_empty, Toast.LENGTH_LONG).show()
return
}
testJob = lifecycleScope.launch(Dispatchers.IO) {
isTesting = true
savedCmd = prefs.getString("byedpi_cmd_args", "").orEmpty()
clearLog()
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 = ""
resultsTextView.text = ""
strategyAdapter.setTestingState(true)
strategyAdapter.updateStrategies(strategies, sortByPercentage = false)
}
if (isProxyRunning()) {
ServiceManager.stop(this@TestActivity)
waitForProxyStatus(AppStatus.Halted)
}
val fullLog = prefs.getBoolean("byedpi_proxytest_fulllog", false)
val logClickable = prefs.getBoolean("byedpi_proxytest_logclickable", false)
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)
val successfulCmds = mutableListOf<Triple<String, Int, Int>>()
for ((index, cmd) in cmds.withIndex()) {
for (strategyIndex in strategies.indices) {
if (!isActive) break
val cmdIndex = index + 1
val strategy = strategies[strategyIndex]
val cmdIndex = strategyIndex + 1
withContext(Dispatchers.Main) {
progressTextView.text = getString(R.string.test_process, cmdIndex, cmds.size)
}
updateCmdArgs(cmd)
updateCmdArgs(strategy.command)
if (isProxyRunning()) stopTesting()
else ServiceManager.start(this@TestActivity, Mode.Proxy)
@@ -204,38 +244,37 @@ class TestActivity : BaseActivity() {
stopTesting()
}
withContext(Dispatchers.Main) {
if (logClickable) {
appendLinkToResults("$cmd\n")
} else {
appendTextToResults("$cmd\n")
}
}
delay(delaySec * 500L)
val totalRequests = sites.size * requestsCount
val checkResults = siteChecker.checkSitesAsync(
strategy.totalRequests = totalRequests
withContext(Dispatchers.Main) {
strategyAdapter.notifyItemChanged(strategyIndex)
}
siteChecker.checkSitesAsync(
sites = sites,
requestsCount = requestsCount,
requestTimeout = requestTimeout,
fullLog = fullLog,
concurrentRequests = requestLimit,
fullLog = true,
onSiteChecked = { site, successCount, countRequests ->
lifecycleScope.launch(Dispatchers.Main) {
appendTextToResults("$site - $successCount/$countRequests\n")
strategy.currentProgress += countRequests
strategy.successCount += successCount
strategy.siteResults.add(SiteResult(site, successCount, countRequests))
strategyAdapter.notifyItemChanged(strategyIndex, "progress")
}
}
)
val successfulCount = checkResults.sumOf { it.second }
val successPercentage = (successfulCount * 100) / totalRequests
if (successPercentage >= 50) successfulCmds.add(Triple(cmd, successfulCount, totalRequests))
delay(delaySec * 500L)
strategy.isCompleted = true
withContext(Dispatchers.Main) {
appendTextToResults("$successfulCount/$totalRequests ($successPercentage%)\n\n")
strategyAdapter.updateStrategies(strategies, sortByPercentage = true)
saveResults(strategies)
}
if (isProxyRunning()) ServiceManager.stop(this@TestActivity)
@@ -245,21 +284,7 @@ class TestActivity : BaseActivity() {
stopTesting()
}
delay(1000L)
}
successfulCmds.sortByDescending { it.second }
withContext(Dispatchers.Main) {
appendTextToResults("${getString(R.string.test_good_cmds)}\n\n")
successfulCmds.forEachIndexed { index, (cmd, successCount, total) ->
appendTextToResults("${index + 1}. ")
appendLinkToResults("$cmd\n")
appendTextToResults("$successCount/$total\n\n")
}
appendTextToResults(getString(R.string.test_complete_info))
delay(delaySec * 500L)
}
stopTesting()
@@ -271,10 +296,10 @@ class TestActivity : BaseActivity() {
return
}
isTesting = false
updateCmdArgs(savedCmd)
lifecycleScope.launch(Dispatchers.IO) {
isTesting = false
updateCmdArgs(savedCmd)
testJob?.cancel()
testJob = null
@@ -286,109 +311,60 @@ class TestActivity : BaseActivity() {
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 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(CLIPBOARD_SERVICE) as ClipboardManager
val clip = ClipData.newPlainText("command", text)
clipboard.setPrimaryClip(clip)
}
private fun addToHistory(command: String) {
updateCmdArgs(command)
cmdHistoryUtils.addCommand(command)
}
lifecycleScope.launch(Dispatchers.IO) {
updateCmdArgs(command)
cmdHistoryUtils.addCommand(command)
private fun displayLog(log: String) {
log.split("{", "}").forEachIndexed { index, part ->
if (index % 2 == 0) {
appendTextToResults(part)
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 {
appendLinkToResults(part)
R.string.cmd_history_applied
}
withContext(Dispatchers.Main) {
Toast.makeText(this@TestActivity, toastText, Toast.LENGTH_SHORT).show()
}
}
}
private fun saveLog(text: String) {
val file = File(filesDir, "proxy_test.log")
file.appendText(text)
private fun saveResults(results: List<StrategyResult>) {
val file = File(filesDir, "proxy_test_results.json")
val json = gson.toJson(results)
file.writeText(json)
}
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 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> {
val defaultDomainLists = setOf("youtube", "googlevideo")
val selectedDomainLists = prefs.getStringSet("byedpi_proxytest_domain_lists", defaultDomainLists)?: return emptyList()
val allDomains = mutableListOf<String>()
for (domainList in selectedDomainLists) {
val domains = when (domainList) {
"custom" -> {
val customDomains = prefs.getString("byedpi_proxytest_domains", "").orEmpty()
customDomains.lines().map { it.trim() }.filter { it.isNotEmpty() }
}
else -> {
assets.open("proxytest_$domainList.sites").bufferedReader().useLines { it.toList() }
}
}
allDomains.addAll(domains)
}
return allDomains.distinct()
DomainListUtils.syncLists(this)
return DomainListUtils.getActiveDomains(this)
}
private fun loadCmds(): List<String> {
@@ -397,10 +373,38 @@ class TestActivity : BaseActivity() {
return if (userCommands) {
val content = prefs.getStringNotNull("byedpi_proxytest_commands", "")
content.replace("{sni}", sniValue).lines().map { it.trim() }.filter { it.isNotEmpty() }
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() }
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)
}
}
@@ -1,4 +1,4 @@
package io.github.dovecoteescapee.byedpi.activities
package io.github.romanvht.byedpi.activities
import android.app.Activity
import android.content.SharedPreferences
@@ -8,12 +8,13 @@ import android.os.Handler
import android.os.Looper
import android.util.Log
import androidx.core.content.edit
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
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() {
@@ -28,7 +29,7 @@ class ToggleActivity : Activity() {
prefs = getPreferences()
val strategy = intent.getStringExtra("strategy")
val current = prefs.getString("byedpi_cmd_args", null)
val updated = updateStrategy(strategy)
val onlyUpdate = intent.getBooleanExtra("only_update", false)
val onlyStart = intent.getBooleanExtra("only_start", false)
@@ -36,10 +37,7 @@ class ToggleActivity : Activity() {
when {
onlyUpdate -> {
if (strategy != null && strategy != current) {
prefs.edit(commit = true) { putString("byedpi_cmd_args", strategy) }
Log.i(TAG, "Strategy updated to: $strategy")
}
Log.i(TAG, "Only update strategy")
}
onlyStart -> {
val (status) = appStatus
@@ -58,13 +56,7 @@ class ToggleActivity : Activity() {
}
}
else -> {
if (strategy != null && strategy != current) {
prefs.edit(commit = true) { putString("byedpi_cmd_args", strategy) }
Log.i(TAG, "Strategy updated to: $strategy")
toggleService(strategy)
} else {
toggleService(null)
}
toggleService(updated)
}
}
@@ -82,24 +74,31 @@ class ToggleActivity : Activity() {
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(strategy: String?) {
private fun toggleService(restart: Boolean) {
val (status) = appStatus
when (status) {
AppStatus.Halted -> {
startService()
}
AppStatus.Running -> {
if (strategy != null) {
stopService()
waitForServiceStop { success ->
Log.i(TAG, "Service stop: $success")
startService()
}
if (restart) {
restartService()
} else {
stopService()
}
@@ -107,27 +106,13 @@ class ToggleActivity : Activity() {
}
}
private fun waitForServiceStop(onComplete: (Boolean) -> Unit) {
val startTime = System.currentTimeMillis()
val handler = Handler(Looper.getMainLooper())
fun check() {
val (status) = appStatus
val elapsed = System.currentTimeMillis() - startTime
when {
status == AppStatus.Halted -> {
Log.i(TAG, "Service stopped")
onComplete(true)
}
elapsed >= 3000L -> {
Log.w(TAG, "Timeout waiting for service to stop")
onComplete(false)
}
else -> handler.postDelayed({ check() }, 100L)
}
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
}
check()
return false
}
}
}
@@ -1,4 +1,4 @@
package io.github.dovecoteescapee.byedpi.adapters
package io.github.romanvht.byedpi.adapters
import android.annotation.SuppressLint
import android.content.Context
@@ -13,8 +13,13 @@ import android.widget.TextView
import androidx.core.content.edit
import androidx.preference.PreferenceManager
import androidx.recyclerview.widget.RecyclerView
import io.github.dovecoteescapee.byedpi.R
import io.github.dovecoteescapee.byedpi.data.AppInfo
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,
@@ -22,30 +27,71 @@ class AppSelectionAdapter(
) : 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)
return ViewHolder(view)
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]
holder.appIcon.setImageDrawable(app.icon)
holder.appName.text = app.appName
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.itemView.setOnClickListener {
app.isSelected = !app.isSelected
holder.appCheckBox.isChecked = app.isSelected
updateSelectedApps()
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)
}
}
}
}
@@ -53,6 +99,11 @@ class AppSelectionAdapter(
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 {
@@ -61,7 +112,8 @@ class AppSelectionAdapter(
if (query.isNotEmpty()) {
filteredList = originalApps.filter {
it.appName.lowercase().contains(query)
it.appName.lowercase().contains(query) ||
it.packageName.lowercase().contains(query)
}
}
@@ -89,4 +141,4 @@ class AppSelectionAdapter(
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 {
@@ -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 {
@@ -1,4 +1,4 @@
package io.github.dovecoteescapee.byedpi.data
package io.github.romanvht.byedpi.data
const val START_ACTION = "start"
const val STOP_ACTION = "stop"
@@ -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,
@@ -1,6 +1,6 @@
package io.github.dovecoteescapee.byedpi.data
package io.github.romanvht.byedpi.data
import io.github.dovecoteescapee.byedpi.BuildConfig
import io.github.romanvht.byedpi.BuildConfig
const val STARTED_BROADCAST = "${BuildConfig.APPLICATION_ID}.STARTED"
const val STOPPED_BROADCAST = "${BuildConfig.APPLICATION_ID}.STOPPED"
@@ -1,4 +1,4 @@
package io.github.dovecoteescapee.byedpi.data
package io.github.romanvht.byedpi.data
data class Command(
var text: String,
@@ -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,4 +1,4 @@
package io.github.dovecoteescapee.byedpi.fragments
package io.github.romanvht.byedpi.fragments
import android.content.SharedPreferences
import android.content.pm.ApplicationInfo
@@ -14,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
@@ -42,7 +44,6 @@ class AppSelectionFragment : Fragment() {
setupRecyclerView()
setupSearchView()
loadApps()
return view
@@ -71,10 +72,11 @@ class AppSelectionFragment : Fragment() {
private fun loadApps() {
progressBar.visibility = View.VISIBLE
searchView.visibility = View.GONE
lifecycleScope.launch {
val apps = withContext(Dispatchers.IO) {
getInstalledApps()
getInstalledAppsAsync()
}
adapter = AppSelectionAdapter(requireContext(), apps)
@@ -84,14 +86,19 @@ class AppSelectionFragment : Fragment() {
}
}
private fun getInstalledApps(): List<AppInfo> {
private suspend fun getInstalledAppsAsync(): List<AppInfo> {
val pm = requireContext().packageManager
val installedApps = pm.getInstalledApplications(0)
val selectedApps = prefs.getStringSet("selected_apps", setOf()) ?: setOf()
return installedApps
.filter { it.packageName != requireContext().packageName }
.map { createAppInfo(it, pm, selectedApps) }
.map { appInfo ->
lifecycleScope.async(Dispatchers.IO) {
createAppInfo(appInfo, pm, selectedApps)
}
}
.awaitAll()
.sortedWith(compareBy({ !it.isSelected }, { it.appName.lowercase() }))
}
@@ -106,17 +113,11 @@ class AppSelectionFragment : Fragment() {
appInfo.packageName
}
val appIcon = try {
pm.getApplicationIcon(appInfo.packageName)
} catch (_: Exception) {
pm.defaultActivityIcon
}
return AppInfo(
appName,
appInfo.packageName,
appIcon,
selectedApps.contains(appInfo.packageName)
selectedApps.contains(appInfo.packageName),
icon = null
)
}
}
@@ -1,51 +1,53 @@
package io.github.dovecoteescapee.byedpi.fragments
package io.github.romanvht.byedpi.fragments
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
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.dovecoteescapee.byedpi.R
import io.github.dovecoteescapee.byedpi.utility.findPreferenceNotNull
import io.github.romanvht.byedpi.R
import io.github.romanvht.byedpi.utility.findPreferenceNotNull
import androidx.appcompat.app.AlertDialog
import io.github.dovecoteescapee.byedpi.data.Command
import io.github.dovecoteescapee.byedpi.utility.HistoryUtils
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 ByeDpiCommandLineSettingsFragment : PreferenceFragmentCompat() {
class ByeDpiCMDSettingsFragment : 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?) {
val uiScale = resources.displayMetrics.density
val threshold = 3f
val overlay = if (uiScale > threshold) R.style.Theme_ByeDPI_History_NoPaddingLeft
else R.style.Theme_ByeDPI_History_PaddingLeft
requireActivity().theme.applyStyle(overlay, true)
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")
clearButton = findPreferenceNotNull("clear_cmd_args")
editTextPreference.setOnPreferenceChangeListener { _, newValue ->
val newCommand = newValue.toString()
if (newCommand.isNotBlank()) cmdHistoryUtils.addCommand(newCommand)
updateHistoryItems()
true
}
clearButton.setOnPreferenceClickListener {
editTextPreference.text = ""
if (newCommand.isNotBlank()) {
cmdHistoryUtils.addCommand(newCommand)
restartService()
}
updateHistoryItems()
true
}
@@ -57,7 +59,31 @@ class ByeDpiCommandLineSettingsFragment : PreferenceFragmentCompat() {
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)
}
@@ -77,9 +103,34 @@ class ByeDpiCommandLineSettingsFragment : PreferenceFragmentCompat() {
}
private fun createPreference(command: Command) =
Preference(requireContext()).apply {
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
summary = buildSummary(command)
layoutResource = R.layout.history_item
setOnPreferenceClickListener {
showActionDialog(command)
@@ -88,15 +139,13 @@ class ByeDpiCommandLineSettingsFragment : PreferenceFragmentCompat() {
}
private fun buildSummary(command: Command): String {
val summary = StringBuilder()
if (command.name != null) {
summary.append(command.name)
val parts = mutableListOf<String>()
if (command.text == editTextPreference.text) {
parts.add(getString(R.string.cmd_history_applied))
}
if (command.pinned) {
if (summary.isNotEmpty()) summary.append(" - ")
summary.append(context?.getString(R.string.cmd_history_pinned))
}
return summary.toString()
return parts.joinToString(" ")
}
private fun showHistoryClearDialog() {
@@ -145,7 +194,7 @@ class ByeDpiCommandLineSettingsFragment : PreferenceFragmentCompat() {
1 -> if (command.pinned) unpinCommand(command.text) else pinCommand(command.text)
2 -> showRenameDialog(command)
3 -> showEditDialog(command)
4 -> copyToClipboard(command.text)
4 -> ClipboardUtils.copy(requireContext(), command.text, "command")
5 -> deleteCommand(command.text)
}
}
@@ -205,6 +254,8 @@ class ByeDpiCommandLineSettingsFragment : PreferenceFragmentCompat() {
private fun applyCommand(command: String) {
editTextPreference.text = command
updateHistoryItems()
restartService()
}
private fun pinCommand(command: String) {
@@ -222,9 +273,14 @@ class ByeDpiCommandLineSettingsFragment : PreferenceFragmentCompat() {
updateHistoryItems()
}
private fun copyToClipboard(command: String) {
val clipboard = requireContext().getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
val clip = ClipData.newPlainText("Command", command)
clipboard.setPrimaryClip(clip)
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()
}
}
@@ -1,68 +1,22 @@
package io.github.dovecoteescapee.byedpi.fragments
package io.github.romanvht.byedpi.fragments
import android.Manifest
import android.content.Intent
import android.content.SharedPreferences
import android.content.pm.PackageManager
import androidx.core.net.toUri
import android.os.Build
import android.os.Bundle
import android.os.Environment
import android.provider.Settings
import android.util.Log
import androidx.appcompat.app.AppCompatDelegate
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import androidx.core.os.LocaleListCompat
import androidx.appcompat.app.AlertDialog
import androidx.core.content.edit
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.*
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 const val STORAGE_PERMISSION_REQUEST = 1001
fun setLang(lang: String) {
val appLocale = localeByName(lang) ?: throw IllegalStateException("Invalid value for language: $lang")
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")
else -> {
Log.w(TAG, "Invalid value for language: $lang")
null
}
}
fun setTheme(name: String) {
val appTheme = themeByName(name) ?: throw IllegalStateException("Invalid value for app_theme: $name")
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
}
}
}
private val preferenceListener =
@@ -79,13 +33,13 @@ class MainSettingsFragment : PreferenceFragmentCompat() {
findPreferenceNotNull<ListPreference>("language")
.setOnPreferenceChangeListener { _, newValue ->
setLang(newValue as String)
SettingsUtils.setLang(newValue as String)
true
}
findPreferenceNotNull<ListPreference>("app_theme")
.setOnPreferenceChangeListener { _, newValue ->
setTheme(newValue as String)
SettingsUtils.setTheme(newValue as String)
true
}
@@ -96,14 +50,32 @@ class MainSettingsFragment : PreferenceFragmentCompat() {
true
}
findPreferenceNotNull<Preference>("battery_optimization")
.setOnPreferenceClickListener {
BatteryUtils.requestBatteryOptimization(requireContext())
true
}
findPreferenceNotNull<Preference>("storage_access")
.setOnPreferenceClickListener {
requestStoragePermission()
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"
findPreferenceNotNull<Preference>("byedpi_version").summary = "0.17.3 (ba53229)"
updatePreferences()
}
@@ -119,56 +91,17 @@ class MainSettingsFragment : PreferenceFragmentCompat() {
sharedPreferences?.unregisterOnSharedPreferenceChangeListener(preferenceListener)
}
private fun hasStoragePermission(): Boolean {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
Environment.isExternalStorageManager()
} else {
val readPermission = ContextCompat.checkSelfPermission(
requireContext(),
Manifest.permission.READ_EXTERNAL_STORAGE
) == PackageManager.PERMISSION_GRANTED
val writePermission = ContextCompat.checkSelfPermission(
requireContext(),
Manifest.permission.WRITE_EXTERNAL_STORAGE
) == PackageManager.PERMISSION_GRANTED
readPermission && writePermission
}
}
private fun requestStoragePermission() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
try {
val intent = Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION).apply {
data = "package:${requireContext().packageName}".toUri()
}
startActivity(intent)
} catch (e: Exception) {
val intent = Intent(Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION)
startActivity(intent)
}
} else {
ActivityCompat.requestPermissions(
requireActivity(),
arrayOf(
Manifest.permission.READ_EXTERNAL_STORAGE,
Manifest.permission.WRITE_EXTERNAL_STORAGE
),
STORAGE_PERMISSION_REQUEST
)
}
}
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<EditTextPreference>("dns_ip")
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")
@@ -186,6 +119,9 @@ class MainSettingsFragment : PreferenceFragmentCompat() {
cmdSettings.isEnabled = cmdEnable
proxyTest.isEnabled = cmdEnable
dns.summary = dnsSummary()
dnsEdit.isVisible = false
when (mode) {
Mode.VPN -> {
dns.isVisible = true
@@ -216,10 +152,77 @@ class MainSettingsFragment : PreferenceFragmentCompat() {
}
}
if (hasStoragePermission()) {
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,18 +1,18 @@
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
@@ -47,6 +47,9 @@ 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 {
@@ -92,15 +95,15 @@ class ByeDpiProxyService : LifecycleService() {
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)
@@ -114,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)
@@ -124,6 +127,12 @@ 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 {
withContext(Dispatchers.IO) {
stopProxy()
@@ -147,16 +156,14 @@ class ByeDpiProxyService : LifecycleService() {
proxyJob = lifecycleScope.launch(Dispatchers.IO) {
val code = proxy.startProxy(preferences)
delay(500)
if (code != 0) {
Log.e(TAG, "Proxy stopped with code $code")
updateStatus(ServiceStatus.Failed)
} else {
updateStatus(ServiceStatus.Disconnected)
stopSelf()
}
stopSelf()
}
Log.i(TAG, "Proxy started")
@@ -193,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")
@@ -221,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 =
@@ -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,4 +1,4 @@
package io.github.dovecoteescapee.byedpi.services
package io.github.romanvht.byedpi.services
import android.app.Notification
import android.app.NotificationManager
@@ -9,13 +9,13 @@ 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
@@ -57,6 +57,9 @@ class ByeDpiVpnService : LifecycleVpnService() {
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 {
@@ -89,6 +92,22 @@ class ByeDpiVpnService : LifecycleVpnService() {
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
@@ -109,16 +128,16 @@ class ByeDpiVpnService : LifecycleVpnService() {
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)
@@ -132,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)
@@ -142,6 +161,12 @@ 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 {
try {
withContext(Dispatchers.IO) {
@@ -169,17 +194,15 @@ class ByeDpiVpnService : LifecycleVpnService() {
proxyJob = lifecycleScope.launch(Dispatchers.IO) {
val code = byeDpiProxy.startProxy(preferences)
delay(500)
if (code != 0) {
Log.e(TAG, "Proxy stopped with code $code")
updateStatus(ServiceStatus.Failed)
} else {
updateStatus(ServiceStatus.Disconnected)
stopTun2Socks()
stopSelf()
}
stopTun2Socks()
stopSelf()
}
Log.i(TAG, "Proxy started")
@@ -219,13 +242,14 @@ 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 = buildString {
@@ -263,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) {
@@ -287,7 +316,7 @@ class ByeDpiVpnService : LifecycleVpnService() {
}
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")
@@ -316,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 =
@@ -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,8 +1,8 @@
package io.github.dovecoteescapee.byedpi.utility
package io.github.romanvht.byedpi.utility
import android.content.Context
import android.content.SharedPreferences
import io.github.dovecoteescapee.byedpi.data.Command
import io.github.romanvht.byedpi.data.Command
import com.google.gson.Gson
import androidx.core.content.edit
@@ -71,6 +71,10 @@ 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) }
@@ -1,4 +1,4 @@
package io.github.dovecoteescapee.byedpi.utility
package io.github.romanvht.byedpi.utility
import android.app.Notification
import android.app.NotificationChannel
@@ -9,11 +9,11 @@ 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.PAUSE_ACTION
import io.github.dovecoteescapee.byedpi.data.RESUME_ACTION
import io.github.dovecoteescapee.byedpi.data.STOP_ACTION
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) {
@@ -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
@@ -32,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()
}
}
}
}
@@ -1,4 +1,4 @@
package io.github.dovecoteescapee.byedpi.utility
package io.github.romanvht.byedpi.utility
import android.content.Context
import android.content.Intent
@@ -6,8 +6,8 @@ import android.content.pm.ShortcutInfo
import android.content.pm.ShortcutManager
import android.graphics.drawable.Icon
import android.os.Build
import io.github.dovecoteescapee.byedpi.R
import io.github.dovecoteescapee.byedpi.activities.ToggleActivity
import io.github.romanvht.byedpi.R
import io.github.romanvht.byedpi.activities.ToggleActivity
object ShortcutUtils {
@@ -30,7 +30,7 @@ object ShortcutUtils {
shortcuts.add(toggleShortcut)
if (context.getPreferences().getBoolean("byedpi_enable_cmd_settings", false)) {
if (context.getPreferences().getCmdEnable()) {
val history = HistoryUtils(context)
val pinned = history.getHistory().filter { it.pinned }.take(3)
@@ -0,0 +1,119 @@
package io.github.romanvht.byedpi.utility
import android.util.Log
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.sync.Semaphore
import kotlinx.coroutines.sync.withPermit
import kotlinx.coroutines.withContext
import java.io.IOException
import java.net.HttpURLConnection
import java.net.InetSocketAddress
import java.net.Proxy
import java.net.URL
class SiteCheckUtils(
private val proxyIp: String,
private val proxyPort: Int
) {
suspend fun checkSitesAsync(
sites: List<String>,
requestsCount: Int,
requestTimeout: Long,
concurrentRequests: Int = 20,
fullLog: Boolean,
onSiteChecked: ((String, Int, Int) -> Unit)? = null
): List<Pair<String, Int>> {
val semaphore = Semaphore(concurrentRequests)
return withContext(Dispatchers.IO) {
sites.map { site ->
async {
semaphore.withPermit {
val successCount = checkSiteAccess(site, requestsCount, requestTimeout)
if (fullLog) {
onSiteChecked?.invoke(site, successCount, requestsCount)
}
site to successCount
}
}
}.awaitAll()
}
}
private suspend fun checkSiteAccess(
site: String,
requestsCount: Int,
timeout: Long
): Int = withContext(Dispatchers.IO) {
var responseCount = 0
val formattedUrl = if (site.startsWith("http://") || site.startsWith("https://")) site
else "https://$site"
val url = try {
URL(formattedUrl)
} catch (_: Exception) {
Log.e("SiteChecker", "Invalid URL: $formattedUrl")
return@withContext 0
}
val proxy = Proxy(Proxy.Type.SOCKS, InetSocketAddress(proxyIp, proxyPort))
repeat(requestsCount) { attempt ->
Log.i("SiteChecker", "Attempt ${attempt + 1}/$requestsCount for $site")
var connection: HttpURLConnection? = null
try {
connection = url.openConnection(proxy) as HttpURLConnection
connection.connectTimeout = (timeout * 1000).toInt()
connection.readTimeout = (timeout * 1000).toInt()
connection.instanceFollowRedirects = true
connection.setRequestProperty("Connection", "close")
val responseCode = connection.responseCode
val declaredLength = if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.N) {
connection.contentLengthLong
} else {
connection.contentLength.toLong()
}
var actualLength = 0L
try {
val inputStream = if (responseCode in 200..299) connection.inputStream else connection.errorStream
if (inputStream != null) {
val buffer = ByteArray(8192)
var bytesRead: Int
val limit = if (declaredLength > 0) declaredLength else 1024L * 1024
while (actualLength < limit) {
val remaining = limit - actualLength
val toRead = if (remaining > buffer.size) buffer.size else remaining.toInt()
bytesRead = inputStream.read(buffer, 0, toRead)
if (bytesRead == -1) break
actualLength += bytesRead
}
}
} catch (_: IOException) {
// Stream reading failed
}
if (declaredLength <= 0L || actualLength >= declaredLength) {
Log.i("SiteChecker", "Response for $site: $responseCode, Declared: $declaredLength, Actual: $actualLength")
responseCount++
} else {
Log.w("SiteChecker", "Block detected for $site, Declared: $declaredLength, Actual: $actualLength")
}
} catch (e: Exception) {
Log.e("SiteChecker", "Error accessing $site: ${e.message}")
} finally {
connection?.disconnect()
}
}
responseCount
}
}
@@ -0,0 +1,66 @@
package io.github.romanvht.byedpi.utility
import android.Manifest
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.os.Build
import android.os.Environment
import android.provider.Settings
import android.util.Log
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import androidx.core.net.toUri
import androidx.fragment.app.Fragment
object StorageUtils {
private const val TAG = "StorageUtils"
private const val STORAGE_PERMISSION_REQUEST = 1001
fun hasStoragePermission(context: Context): Boolean {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
Environment.isExternalStorageManager()
} else {
val readPermission = ContextCompat.checkSelfPermission(
context,
Manifest.permission.READ_EXTERNAL_STORAGE
) == PackageManager.PERMISSION_GRANTED
val writePermission = ContextCompat.checkSelfPermission(
context,
Manifest.permission.WRITE_EXTERNAL_STORAGE
) == PackageManager.PERMISSION_GRANTED
readPermission && writePermission
}
}
fun requestStoragePermission(fragment: Fragment) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
try {
val intent = Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION).apply {
data = "package:${fragment.requireContext().packageName}".toUri()
}
fragment.startActivity(intent)
} catch (e: Exception) {
Log.e(TAG, "Failed to request storage permission", e)
try {
val intent = Intent(Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION)
fragment.startActivity(intent)
} catch (e2: Exception) {
Log.e(TAG, "Failed to open storage settings", e2)
}
}
} else {
Log.d(TAG, "Requesting storage permission for Android 10 and below")
ActivityCompat.requestPermissions(
fragment.requireActivity(),
arrayOf(
Manifest.permission.READ_EXTERNAL_STORAGE,
Manifest.permission.WRITE_EXTERNAL_STORAGE
),
STORAGE_PERMISSION_REQUEST
)
}
}
}
@@ -1,4 +1,4 @@
package io.github.dovecoteescapee.byedpi.utility
package io.github.romanvht.byedpi.utility
import android.net.InetAddresses
import android.os.Build
+2 -1
View File
@@ -16,6 +16,7 @@
APP_OPTIM := release
APP_PLATFORM := android-21
APP_ABI := armeabi-v7a arm64-v8a x86 x86_64
APP_CFLAGS := -O3 -DPKGNAME=io/github/dovecoteescapee/byedpi/core
APP_CFLAGS := -O3 -DPKGNAME=io/github/romanvht/byedpi/core
APP_CPPFLAGS := -O3 -std=c++11
NDK_TOOLCHAIN_VERSION := clang
APP_LDFLAGS := -Wl,-z,max-page-size=16384
@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M16,1H4C2.9,1 2,1.9 2,3v14h2V3h12V1zm3,4H8C6.9,5 6,5.9 6,7v14c0,1.1 0.9,2 2,2h11c1.1,0 2,-0.9 2,-2V7c0,-1.1 -0.9,-2 -2,-2zm0,16H8V7h11v14z"/>
</vector>
@@ -2,8 +2,9 @@
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="?attr/colorControlNormal"
android:fillColor="@android:color/white"
android:pathData="M19.14,12.94c0.04,-0.3 0.06,-0.61 0.06,-0.94c0,-0.32 -0.02,-0.64 -0.07,-0.94l2.03,-1.58c0.18,-0.14 0.23,-0.41 0.12,-0.61l-1.92,-3.32c-0.12,-0.22 -0.37,-0.29 -0.59,-0.22l-2.39,0.96c-0.5,-0.38 -1.03,-0.7 -1.62,-0.94L14.4,2.81c-0.04,-0.24 -0.24,-0.41 -0.48,-0.41h-3.84c-0.24,0 -0.43,0.17 -0.47,0.41L9.25,5.35C8.66,5.59 8.12,5.92 7.63,6.29L5.24,5.33c-0.22,-0.08 -0.47,0 -0.59,0.22L2.74,8.87C2.62,9.08 2.66,9.34 2.86,9.48l2.03,1.58C4.84,11.36 4.8,11.69 4.8,12s0.02,0.64 0.07,0.94l-2.03,1.58c-0.18,0.14 -0.23,0.41 -0.12,0.61l1.92,3.32c0.12,0.22 0.37,0.29 0.59,0.22l2.39,-0.96c0.5,0.38 1.03,0.7 1.62,0.94l0.36,2.54c0.05,0.24 0.24,0.41 0.48,0.41h3.84c0.24,0 0.44,-0.17 0.47,-0.41l0.36,-2.54c0.59,-0.24 1.13,-0.56 1.62,-0.94l2.39,0.96c0.22,0.08 0.47,0 0.59,-0.22l1.92,-3.32c0.12,-0.22 0.07,-0.47 -0.12,-0.61L19.14,12.94zM12,15.6c-1.98,0 -3.6,-1.62 -3.6,-3.6s1.62,-3.6 3.6,-3.6s3.6,1.62 3.6,3.6S13.98,15.6 12,15.6z"/>
</vector>
</vector>
+7
View File
@@ -0,0 +1,7 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp" android:height="24dp"
android:viewportWidth="24" android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path android:fillColor="@android:color/white"
android:pathData="M4,8h4L8,4L4,4v4zM10,20h4v-4h-4v4zM4,20h4v-4L4,16v4zM4,14h4v-4L4,10v4zM10,14h4v-4h-4v4zM16,4v4h4L20,4h-4zM10,8h4L14,4h-4v4zM16,14h4v-4h-4v4zM16,20h4v-4h-4v4z"/>
</vector>
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="?attr/colorControlNormal"
android:pathData="M7,10l5,5 5,-5z"/>
</vector>
@@ -0,0 +1,7 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp" android:height="24dp"
android:viewportWidth="24" android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path android:fillColor="@android:color/white"
android:pathData="M12,6v3l4,-4 -4,-4v3c-4.42,0 -8,3.58 -8,8 0,1.57 0.46,3.03 1.24,4.26L6.7,14.8c-0.45,-0.83 -0.7,-1.79 -0.7,-2.8 0,-3.31 2.69,-6 6,-6zM18.76,7.74L17.3,9.2c0.44,0.84 0.7,1.79 0.7,2.8 0,3.31 -2.69,6 -6,6v-3l-4,4 4,4v-3c4.42,0 8,-3.58 8,-8 0,-1.57 -0.46,-3.03 -1.24,-4.26z"/>
</vector>

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