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