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 |
@@ -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
|
||||
|
||||
+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-02-27T18:27:01.347037Z">
|
||||
<Target type="DEFAULT_BOOT">
|
||||
<handle>
|
||||
<DeviceId pluginId="LocalEmulator" identifier="path=C:\Users\roman\.android\avd\Pixel_8_API_35.avd" />
|
||||
</handle>
|
||||
</Target>
|
||||
</DropdownSelection>
|
||||
<DialogSelection />
|
||||
</SelectionState>
|
||||
</selectionStates>
|
||||
</component>
|
||||
</project>
|
||||
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
-9
@@ -1,9 +0,0 @@
|
||||
<project version="4">
|
||||
<component name="ExternalStorageConfigurationManager" enabled="true" />
|
||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_17" default="true" project-jdk-name="jbr-17" project-jdk-type="JavaSDK">
|
||||
<output url="file://$PROJECT_DIR$/build/classes" />
|
||||
</component>
|
||||
<component name="ProjectType">
|
||||
<option name="id" value="Android" />
|
||||
</component>
|
||||
</project>
|
||||
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>
|
||||
+61
-30
@@ -1,46 +1,77 @@
|
||||
# ByeByeDPI Android
|
||||
[Русский](README.md) | English | [Türkçe](README-tr.md)
|
||||
|
||||
<div style="text-align: center;">
|
||||
<img alt="ByeDPI Logo" src="https://github.com/romanvht/ByeDPIAndroid/raw/master/.github/images/app.svg" width="100%" height="200px">
|
||||
<div align="center">
|
||||
<p>
|
||||
<img src="https://github.com/romanvht/ByeDPIAndroid/raw/master/.github/images/app.svg" alt="Логотип ByeDPI" width="200" />
|
||||
</p>
|
||||
<h1>ByeByeDPI Android</h1>
|
||||
<p>
|
||||
<a href="README.md">Русский</a> |
|
||||
English |
|
||||
<a href="README-tr.md">Türkçe</a>
|
||||
</p>
|
||||
<p>
|
||||
<a href="https://github.com/romanvht/ByeByeDPI/releases/latest"><img src="https://img.shields.io/github/v/release/romanvht/ByeByeDPI" alt="Latest Release" /></a>
|
||||
<a href="https://github.com/romanvht/ByeByeDPI/releases"><img src="https://img.shields.io/github/downloads/romanvht/ByeByeDPI/total" alt="Downloads" /></a>
|
||||
<a href="https://github.com/romanvht/ByeByeDPI/blob/master/LICENSE"><img src="https://img.shields.io/github/license/romanvht/ByeByeDPI" alt="License" /></a>
|
||||
<a href="https://github.com/romanvht/ByeByeDPI"><img src="https://img.shields.io/github/languages/code-size/romanvht/ByeByeDPI" alt="GitHub code size in bytes"/></a>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
---
|
||||
An Android application that runs ByeDPI locally and redirects all traffic through it.
|
||||
|
||||
An Android application that locally runs ByeDPI and routes all traffic through it.
|
||||
For stable operation, you may need to adjust the settings. More information about the available options can be found in the [ByeDPI documentation](https://github.com/hufrea/byedpi/blob/main/README.md).
|
||||
|
||||
For stable operation, you may need to adjust the settings. You can read more about different settings in the [ByeDPI documentation](https://github.com/hufrea/byedpi/blob/v0.13/README.md).
|
||||
This application is not a VPN. It uses 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.
|
||||
|
||||
This application is **not** a VPN. It uses Android's VPN mode to route traffic but does not transmit anything to a remote server. It does not encrypt traffic or hide your IP address.
|
||||
|
||||
This application is a fork of [ByeDPIAndroid](https://github.com/dovecoteescapee/ByeDPIAndroid).
|
||||
The application has only one official website:
|
||||
https://byebyedpi.xyz
|
||||
|
||||
---
|
||||
|
||||
### Features
|
||||
* Autostart service on device boot
|
||||
* Saving lists of command-line parameters
|
||||
* Improved compatibility with Android TV/BOX
|
||||
|
||||
* Automatically starts the service when the device boots
|
||||
* Saves command-line parameter lists
|
||||
* Improved compatibility with Android TV/BOX devices
|
||||
* Per-app split tunneling
|
||||
* Import/export settings
|
||||
* Settings import and export
|
||||
|
||||
### Usage
|
||||
* To enable auto-start, activate the option in settings.
|
||||
* It is recommended to connect to the VPN once to accept the request.
|
||||
* After that, upon device startup, the application will automatically launch the service based on settings (VPN/Proxy).
|
||||
* If you are using an Android TV/BOX and your Ethernet connection drops when connecting, enable whitelist mode and specify the apps that should work through the VPN (e.g., YouTube).
|
||||
* Guide for [SberBOX](sbox.md)
|
||||
|
||||
### How to use ByeByeDPI with AdGuard?
|
||||
* Start ByeByeDPI in proxy mode.
|
||||
* Add ByeByeDPI to AdGuard exclusions on the "App Management" tab.
|
||||
* In AdGuard settings, specify the proxy:
|
||||
```plaintext
|
||||
Proxy Type: SOCKS5
|
||||
Host: 127.0.0.1
|
||||
Port: 1080 (default)
|
||||
* Enable the corresponding option in the settings to use automatic startup.
|
||||
* It is recommended to connect to the VPN once in order to accept the permission request.
|
||||
* After that, the application will automatically start the service when the device boots, depending on the selected mode: VPN or Proxy.
|
||||
* Comprehensive community guide: [ByeByeDPI-Manual](https://byebyedpi.xyz)
|
||||
|
||||
### Building
|
||||
|
||||
1. Clone the repository with its submodules:
|
||||
|
||||
```bash
|
||||
git clone --recurse-submodules
|
||||
```
|
||||
|
||||
2. Run the build script from the repository root:
|
||||
|
||||
```bash
|
||||
./gradlew assembleRelease
|
||||
```
|
||||
|
||||
3. The APK will be located in:
|
||||
`app/build/outputs/apk/release/`
|
||||
|
||||
> Note: hev_socks5_tunnel cannot be built on Windows. You will need to use WSL.
|
||||
|
||||
### Signing Certificate Hash
|
||||
|
||||
SHA-256:
|
||||
`77:45:10:75:AC:EA:40:64:06:47:5D:74:D4:59:88:3A:49:A6:40:51:FA:F3:2E:42:F7:18:F3:F9:77:7A:8D:FB`
|
||||
|
||||
### Dependencies
|
||||
- [ByeDPI](https://github.com/hufrea/byedpi)
|
||||
- [hev-socks5-tunnel](https://github.com/heiher/hev-socks5-tunnel)
|
||||
|
||||
* [ByeDPI](https://github.com/hufrea/byedpi)
|
||||
* [hev-socks5-tunnel](https://github.com/heiher/hev-socks5-tunnel)
|
||||
|
||||
### Acknowledgements
|
||||
|
||||
* [hufrea](https://github.com/hufrea) — for [ByeDPI](https://github.com/hufrea/byedpi)
|
||||
* [dovecoteescapee](https://github.com/dovecoteescapee) — for the original implementation of [ByeDPIAndroid](https://github.com/dovecoteescapee/ByeDPIAndroid)
|
||||
|
||||
+62
-30
@@ -1,45 +1,77 @@
|
||||
# ByeByeDPI Android
|
||||
[Русский](README.md) | [English](README-en.md) | Türkçe
|
||||
|
||||
<div style="text-align: center;">
|
||||
<img alt="ByeDPI Logo" src="https://github.com/romanvht/ByeDPIAndroid/raw/master/.github/images/app.svg" width="100%" height="200px">
|
||||
<div align="center">
|
||||
<p>
|
||||
<img src="https://github.com/romanvht/ByeDPIAndroid/raw/master/.github/images/app.svg" alt="Логотип ByeDPI" width="200" />
|
||||
</p>
|
||||
<h1>ByeByeDPI Android</h1>
|
||||
<p>
|
||||
<a href="README.md">Русский</a> |
|
||||
<a href="README-en.md">English</a> |
|
||||
Türkçe
|
||||
</p>
|
||||
<p>
|
||||
<a href="https://github.com/romanvht/ByeByeDPI/releases/latest"><img src="https://img.shields.io/github/v/release/romanvht/ByeByeDPI" alt="Latest Release" /></a>
|
||||
<a href="https://github.com/romanvht/ByeByeDPI/releases"><img src="https://img.shields.io/github/downloads/romanvht/ByeByeDPI/total" alt="Downloads" /></a>
|
||||
<a href="https://github.com/romanvht/ByeByeDPI/blob/master/LICENSE"><img src="https://img.shields.io/github/license/romanvht/ByeByeDPI" alt="License" /></a>
|
||||
<a href="https://github.com/romanvht/ByeByeDPI"><img src="https://img.shields.io/github/languages/code-size/romanvht/ByeByeDPI" alt="GitHub code size in bytes"/></a>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
---
|
||||
ByeDPI’yi yerel olarak çalıştıran ve tüm trafiği üzerinden yönlendiren bir Android uygulaması.
|
||||
|
||||
ByeDPI'yi yerel olarak çalıştıran ve tüm trafiği bunun üzerinden yönlendiren bir Android uygulaması.
|
||||
Kararlı çalışması için ayarların değiştirilmesi gerekebilir. Çeşitli ayarlar hakkında daha fazla bilgiyi [ByeDPI belgelerinde](https://github.com/hufrea/byedpi/blob/main/README.md) bulabilirsiniz.
|
||||
|
||||
Kararlı bir çalışma için ayarları yapmanız gerekebilir. Farklı ayarlar hakkında daha fazla bilgiye [ByeDPI dökümantasyonundan](https://github.com/hufrea/byedpi/blob/v0.13/README.md) ulaşabilirsiniz.
|
||||
Bu uygulama bir VPN değildir. Trafiği yönlendirmek için Android’in VPN modunu kullanır, ancak hiçbir veriyi uzak bir sunucuya göndermez. Trafiği şifrelemez ve IP adresinizi gizlemez.
|
||||
|
||||
Bu uygulama **VPN** değildir. Trafiği yönlendirmek için Android'in VPN modunu kullanır ancak herhangi bir veriyi uzak bir sunucuya iletmez. Trafiği şifrelemez veya IP adresinizi gizlemez.
|
||||
|
||||
Bu uygulama, [ByeDPIAndroid](https://github.com/dovecoteescapee/ByeDPIAndroid) uygulamasının bir çatallamasıdır.
|
||||
Uygulamanın tek resmî web sitesi:
|
||||
https://byebyedpi.xyz
|
||||
|
||||
---
|
||||
|
||||
### Özellikler
|
||||
* Cihaz başlatıldığında hizmetin otomatik başlatılması
|
||||
* Komut satırı parametrelerinin listelerinin kaydedilmesi
|
||||
|
||||
* Cihaz açıldığında hizmeti otomatik başlatma
|
||||
* Komut satırı parametre listelerini kaydetme
|
||||
* Android TV/BOX ile geliştirilmiş uyumluluk
|
||||
* Uygulama başına bölünmüş tünelleme
|
||||
* Ayarları içe/dışa aktarma
|
||||
* Uygulama bazında bölünmüş tünelleme
|
||||
* Ayarları içe ve dışa aktarma
|
||||
|
||||
### Kullanım
|
||||
* Otomatik başlatmayı etkinleştirmek için ayarlarda seçeneği aktifleştirin.
|
||||
* İlk başta VPN'e bağlanarak isteği kabul etmeniz önerilir.
|
||||
* Bundan sonra, cihaz başlatıldığında, uygulama ayarlara göre (VPN/Proxy) hizmeti otomatik olarak başlatacaktır.
|
||||
* Android TV/BOX kullanıyorsanız ve Ethernet bağlantınız VPN'e bağlanırken kopuyorsa, beyaz liste modunu etkinleştirip VPN üzerinden çalışması gereken uygulamaları belirleyin (örneğin, YouTube).
|
||||
* [SberBOX](sbox.md) için kılavuz
|
||||
|
||||
### Türkiye İle İlgili Destek İçin
|
||||
* Discord: [nyaex](https://github.com/nyaexx) veya [shouyuma](https://github.com/Hamzahsl)
|
||||
* Otomatik başlatmanın çalışması için ayarlardaki ilgili seçeneği etkinleştirin.
|
||||
* İzin isteğini kabul etmek için VPN bağlantısını en az bir kez başlatmanız önerilir.
|
||||
* Bundan sonra uygulama, cihaz açıldığında ayarlara bağlı olarak hizmeti VPN veya Proxy modunda otomatik olarak başlatır.
|
||||
* Topluluk tarafından hazırlanan kapsamlı kılavuz: [ByeByeDPI-Manual](https://byebyedpi.xyz)
|
||||
|
||||
### Derleme
|
||||
|
||||
### ByeByeDPI'yi AdGuard ile nasıl kullanırım?
|
||||
* ByeByeDPI'yi proxy modunda başlatın.
|
||||
* ByeByeDPI'yi AdGuard dışlamalarına "Uygulama Yönetimi" sekmesinde ekleyin.
|
||||
* AdGuard ayarlarında, proxy'i belirtin:
|
||||
```plaintext
|
||||
Proxy Türü: SOCKS5
|
||||
Host: 127.0.0.1
|
||||
Port: 1080 (varsayılan)
|
||||
1. Depoyu alt modüllerle birlikte klonlayın:
|
||||
|
||||
```bash
|
||||
git clone --recurse-submodules
|
||||
```
|
||||
|
||||
2. Derleme betiğini deponun kök dizininden çalıştırın:
|
||||
|
||||
```bash
|
||||
./gradlew assembleRelease
|
||||
```
|
||||
|
||||
3. APK dosyası şu dizinde bulunacaktır:
|
||||
`app/build/outputs/apk/release/`
|
||||
|
||||
> Not: hev_socks5_tunnel Windows üzerinde derlenemez. WSL kullanmanız gerekir.
|
||||
|
||||
### İmza Özeti
|
||||
|
||||
SHA-256:
|
||||
`77:45:10:75:AC:EA:40:64:06:47:5D:74:D4:59:88:3A:49:A6:40:51:FA:F3:2E:42:F7:18:F3:F9:77:7A:8D:FB`
|
||||
|
||||
### Bağımlılıklar
|
||||
|
||||
* [ByeDPI](https://github.com/hufrea/byedpi)
|
||||
* [hev-socks5-tunnel](https://github.com/heiher/hev-socks5-tunnel)
|
||||
|
||||
### Teşekkürler
|
||||
|
||||
* [hufrea](https://github.com/hufrea) — [ByeDPI](https://github.com/hufrea/byedpi) için
|
||||
* [dovecoteescapee](https://github.com/dovecoteescapee) — [ByeDPIAndroid](https://github.com/dovecoteescapee/ByeDPIAndroid) uygulamasının ilk sürümü için
|
||||
|
||||
@@ -1,19 +1,28 @@
|
||||
# ByeByeDPI Android
|
||||
Русский | [English](README-en.md) | [Türkçe](README-tr.md)
|
||||
|
||||
<div style="text-align: center;">
|
||||
<img alt="Логотип ByeDPI" src="https://github.com/romanvht/ByeDPIAndroid/raw/master/.github/images/app.svg" width="100%" height="200px">
|
||||
<div align="center">
|
||||
<p>
|
||||
<img src="https://github.com/romanvht/ByeDPIAndroid/raw/master/.github/images/app.svg" alt="Логотип ByeDPI" width="200" />
|
||||
</p>
|
||||
<h1>ByeByeDPI Android</h1>
|
||||
<p>
|
||||
Русский |
|
||||
<a href="README-en.md">English</a> |
|
||||
<a href="README-tr.md">Türkçe</a>
|
||||
</p>
|
||||
<p>
|
||||
<a href="https://github.com/romanvht/ByeByeDPI/releases/latest"><img src="https://img.shields.io/github/v/release/romanvht/ByeByeDPI" alt="Latest Release" /></a>
|
||||
<a href="https://github.com/romanvht/ByeByeDPI/releases"><img src="https://img.shields.io/github/downloads/romanvht/ByeByeDPI/total" alt="Downloads" /></a>
|
||||
<a href="https://github.com/romanvht/ByeByeDPI/blob/master/LICENSE"><img src="https://img.shields.io/github/license/romanvht/ByeByeDPI" alt="License" /></a>
|
||||
<a href="https://github.com/romanvht/ByeByeDPI"><img src="https://img.shields.io/github/languages/code-size/romanvht/ByeByeDPI" alt="GitHub code size in bytes"/></a>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
---
|
||||
|
||||
Приложение для Android, которое локально запускает ByeDPI и перенаправляет весь трафик через него.
|
||||
|
||||
Для стабильной работы может потребоваться изменить настройки. Подробнее о различных настройках можно прочитать в [документации ByeDPI](https://github.com/hufrea/byedpi/blob/v0.13/README.md).
|
||||
Для стабильной работы может потребоваться изменить настройки. Подробнее о различных настройках можно прочитать в [документации ByeDPI](https://github.com/hufrea/byedpi/blob/main/README.md).
|
||||
|
||||
Приложение не является VPN. Оно использует VPN-режим на Android для перенаправления трафика, но не передает ничего на удаленный сервер. Оно не шифрует трафик и не скрывает ваш IP-адрес.
|
||||
|
||||
Приложения является форком [ByeDPIAndroid](https://github.com/dovecoteescapee/ByeDPIAndroid)
|
||||
У приложения есть единственный официальный сайт -> https://byebyedpi.xyz
|
||||
|
||||
---
|
||||
|
||||
@@ -28,19 +37,29 @@
|
||||
* Для работы автозапуска активируйте пункт в настройках.
|
||||
* Рекомендуется подключится один раз к VPN, чтобы принять запрос.
|
||||
* После этого, при загрузке устройства, приложение автоматически запустит сервис в зависимости от настроек (VPN/Proxy)
|
||||
* Если у вас Android TV/BOX, и при подключении пропадает соединение по Ethernet, активируйте режим белого списка и укажите нужные приложения, которые должны работать через VPN (например, YouTube)
|
||||
* Комплексная инструкция от комьюнити [ByeByeDPI-Manual](https://github.com/HideakiTaiki/ByeByeDPI-Manual)
|
||||
* Комплексная инструкция от комьюнити [ByeByeDPI-Manual](https://byebyedpi.xyz)
|
||||
|
||||
### Как использовать ByeByeDPI вместе с AdGuard?
|
||||
* Запустите ByeByeDPI в режиме прокси.
|
||||
* Добавьте ByeByeDPI в исключения AdGuard на вкладке "Управление приложениями".
|
||||
* В настройках AdGuard укажите прокси:
|
||||
```plaintext
|
||||
Тип прокси: SOCKS5
|
||||
Хост: 127.0.0.1
|
||||
Порт: 1080 (по умолчанию)
|
||||
### Сборка
|
||||
1. Клонируйте репозиторий с сабмодулями:
|
||||
```bash
|
||||
git clone --recurse-submodules
|
||||
```
|
||||
2. Запустите скрипт сборки из корня репозитория:
|
||||
```bash
|
||||
./gradlew assembleRelease
|
||||
```
|
||||
3. APK будет в `app/build/outputs/apk/release/`
|
||||
|
||||
> P.S.: hev_socks5_tunnel не соберется под Windows, вам нужно будет использовать WSL
|
||||
|
||||
### Хеш подписи
|
||||
SHA-256:
|
||||
`77:45:10:75:AC:EA:40:64:06:47:5D:74:D4:59:88:3A:49:A6:40:51:FA:F3:2E:42:F7:18:F3:F9:77:7A:8D:FB`
|
||||
|
||||
### Зависимости
|
||||
- [ByeDPI](https://github.com/hufrea/byedpi)
|
||||
- [hev-socks5-tunnel](https://github.com/heiher/hev-socks5-tunnel)
|
||||
|
||||
### Благодарность
|
||||
- [hufrea](https://github.com/hufrea) - за [ByeDPI](https://github.com/hufrea/byedpi)
|
||||
- [dovecoteescapee](https://github.com/dovecoteescapee) - за изначальную реализацию [ByeDPIAndroid](https://github.com/dovecoteescapee/ByeDPIAndroid)
|
||||
|
||||
+38
-28
@@ -1,62 +1,65 @@
|
||||
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
|
||||
|
||||
plugins {
|
||||
id("com.android.application")
|
||||
id("org.jetbrains.kotlin.android")
|
||||
}
|
||||
|
||||
val abis = setOf("armeabi-v7a", "arm64-v8a", "x86", "x86_64")
|
||||
|
||||
android {
|
||||
namespace = "io.github.dovecoteescapee.byedpi"
|
||||
compileSdk = 34
|
||||
namespace = "io.github.romanvht.byedpi"
|
||||
compileSdk = 36
|
||||
|
||||
defaultConfig {
|
||||
applicationId = "io.github.romanvht.byedpi"
|
||||
minSdk = 21
|
||||
//noinspection OldTargetApi
|
||||
targetSdk = 34
|
||||
versionCode = 1410
|
||||
versionName = "1.5.1"
|
||||
versionCode = 1770
|
||||
versionName = "1.7.7"
|
||||
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
|
||||
ndk {
|
||||
abiFilters.add("armeabi-v7a")
|
||||
abiFilters.add("arm64-v8a")
|
||||
abiFilters.add("x86")
|
||||
abiFilters.add("x86_64")
|
||||
abiFilters.addAll(abis)
|
||||
}
|
||||
}
|
||||
|
||||
buildFeatures {
|
||||
buildConfig = true
|
||||
viewBinding = true
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
buildConfigField("String", "VERSION_NAME", "\"${defaultConfig.versionName}\"")
|
||||
|
||||
isMinifyEnabled = false
|
||||
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
|
||||
signingConfig = signingConfigs.getByName("debug")
|
||||
isMinifyEnabled = true
|
||||
isShrinkResources = true
|
||||
}
|
||||
debug {
|
||||
buildConfigField("String", "VERSION_NAME", "\"${defaultConfig.versionName}-debug\"")
|
||||
}
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_1_8
|
||||
targetCompatibility = JavaVersion.VERSION_1_8
|
||||
}
|
||||
kotlinOptions {
|
||||
jvmTarget = "1.8"
|
||||
|
||||
kotlin {
|
||||
compilerOptions {
|
||||
jvmTarget.set(JvmTarget.JVM_1_8)
|
||||
}
|
||||
}
|
||||
|
||||
externalNativeBuild {
|
||||
cmake {
|
||||
path = file("src/main/cpp/CMakeLists.txt")
|
||||
version = "3.22.1"
|
||||
}
|
||||
}
|
||||
buildFeatures {
|
||||
viewBinding = true
|
||||
}
|
||||
|
||||
// https://android.izzysoft.de/articles/named/iod-scan-apkchecks?lang=en#blobs
|
||||
dependenciesInfo {
|
||||
@@ -65,22 +68,29 @@ android {
|
||||
// Disables dependency metadata when building Android App Bundles.
|
||||
includeInBundle = false
|
||||
}
|
||||
|
||||
splits {
|
||||
abi {
|
||||
isEnable = true
|
||||
reset()
|
||||
include(*abis.toTypedArray())
|
||||
isUniversalApk = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
//noinspection GradleDependency
|
||||
implementation("androidx.fragment:fragment-ktx:1.8.4")
|
||||
implementation("androidx.core:core-ktx:1.13.1")
|
||||
implementation("androidx.appcompat:appcompat:1.7.0")
|
||||
implementation("androidx.fragment:fragment-ktx:1.8.9")
|
||||
implementation("androidx.core:core-ktx:1.17.0")
|
||||
implementation("androidx.appcompat:appcompat:1.7.1")
|
||||
implementation("androidx.preference:preference-ktx:1.2.1")
|
||||
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.8.6")
|
||||
implementation("androidx.lifecycle:lifecycle-service:2.8.6")
|
||||
implementation("com.google.android.material:material:1.12.0")
|
||||
implementation("com.google.code.gson:gson:2.8.9")
|
||||
implementation("com.takisoft.preferencex:preferencex:1.1.0")
|
||||
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.9.4")
|
||||
implementation("androidx.lifecycle:lifecycle-service:2.9.4")
|
||||
implementation("com.google.android.material:material:1.13.0")
|
||||
implementation("com.google.code.gson:gson:2.13.2")
|
||||
testImplementation("junit:junit:4.13.2")
|
||||
androidTestImplementation("androidx.test.ext:junit:1.2.1")
|
||||
androidTestImplementation("androidx.test.espresso:espresso-core:3.6.1")
|
||||
androidTestImplementation("androidx.test.ext:junit:1.3.0")
|
||||
androidTestImplementation("androidx.test.espresso:espresso-core:3.7.0")
|
||||
}
|
||||
|
||||
tasks.register<Exec>("runNdkBuild") {
|
||||
@@ -104,4 +114,4 @@ tasks.register<Exec>("runNdkBuild") {
|
||||
|
||||
tasks.preBuild {
|
||||
dependsOn("runNdkBuild")
|
||||
}
|
||||
}
|
||||
|
||||
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,6 +33,7 @@
|
||||
android:label="@string/app_name"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/Theme.ByeDPI"
|
||||
android:requestLegacyExternalStorage="true"
|
||||
tools:targetApi="34">
|
||||
|
||||
<activity
|
||||
@@ -38,9 +46,6 @@
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
|
||||
</intent-filter>
|
||||
<meta-data
|
||||
android:name="android.app.shortcuts"
|
||||
android:resource="@xml/shortcuts" />
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
@@ -51,10 +56,6 @@
|
||||
android:noHistory="true"
|
||||
android:taskAffinity=""
|
||||
android:launchMode="singleTask">
|
||||
<intent-filter>
|
||||
<action android:name="io.github.romanvht.byedpi.TOGGLE_SERVICE" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
@@ -64,6 +65,7 @@
|
||||
|
||||
<activity
|
||||
android:name=".activities.TestActivity"
|
||||
android:configChanges="orientation|screenSize|keyboardHidden"
|
||||
android:label="@string/title_test"
|
||||
android:exported="false"/>
|
||||
|
||||
@@ -74,25 +76,22 @@
|
||||
|
||||
<service android:name=".services.ByeDpiVpnService"
|
||||
android:permission="android.permission.BIND_VPN_SERVICE"
|
||||
android:foregroundServiceType="specialUse"
|
||||
android:foregroundServiceType="systemExempted"
|
||||
android:exported="false">
|
||||
<intent-filter>
|
||||
<action android:name="android.net.VpnService"/>
|
||||
</intent-filter>
|
||||
<meta-data
|
||||
android:name="android.net.VpnService.SUPPORTS_ALWAYS_ON"
|
||||
android:value="false" />
|
||||
android:value="true" />
|
||||
<property
|
||||
android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE"
|
||||
android:name="android.app.PROPERTY_SYSTEM_EXEMPTED_FGS_SUBTYPE"
|
||||
android:value="vpn" />
|
||||
</service>
|
||||
|
||||
<service android:name=".services.ByeDpiProxyService"
|
||||
android:foregroundServiceType="specialUse"
|
||||
android:foregroundServiceType="dataSync"
|
||||
android:exported="false">
|
||||
<property
|
||||
android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE"
|
||||
android:value="proxy" />
|
||||
</service>
|
||||
|
||||
<service
|
||||
@@ -101,6 +100,9 @@
|
||||
android:label="@string/app_name"
|
||||
android:exported="true"
|
||||
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE">
|
||||
<meta-data
|
||||
android:name="android.service.quicksettings.TOGGLEABLE_TILE"
|
||||
android:value="true" />
|
||||
<intent-filter>
|
||||
<action
|
||||
android:name="android.service.quicksettings.action.QS_TILE"/>
|
||||
@@ -121,4 +123,4 @@
|
||||
</receiver>
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
</manifest>
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
cloudflare.net
|
||||
cloudflare.com
|
||||
cloudflarecn.net
|
||||
cloudflare-ech.com
|
||||
@@ -1,40 +0,0 @@
|
||||
-Ku -a1 -An -d1 -s0+s -d3+s -s6+s -d9+s -s12+s -d15+s -s20+s -d25+s -s30+s -d35+s -At,r,s -s1 -q1 -At,r,s -s5 -o25000+s -At,r,s -o1 -d1 -r1+s -t10 -b1500 -s0+s -d3+s -At,r,s -f-1 -r1+s -At,r,s -s1 -o1+s -s-1
|
||||
-Ku -a3 -An -Kt,h -n vk.com -d1 -d3+s -s6+s -d9+s -s12+s -d15+s -s20+s -d25+s -s30+s -d35+s -t15 -b4500 -r1+s -S -Mh,d -As -Kt,h -n vk.com -d1 -d3+s -s6+s -d9+s -s12+s -d15+s -s20+s -d25+s -s30+s -d35+s -t15 -b4500 -S -Mh,d -An
|
||||
-Ku -a3 -An -Kt,h -n vk.com -o1 -d1 -Mh,d -At,r,s -Kt -n vk.com -s0+sm -d1 -s5+s -s10+s -s15+s -s20+s -t15 -b4500 -r1+s -S -As -Kt -n vk.com -s0+sm -d1 -s5+s -s10+s -s15+s -s20+s -t15 -b4500 -S -An
|
||||
-d2+s -s 50+s -O 2 -a 10 -As -f20 -r2+s -At -d2 -s0+s -s5+s -s10+s -s15+s -s25+s -s35+s -s50+s -s60+s -An -n youtube.com -d2:5:2+h -f-3 -r2+sm -o2 -o50+s -r2+s -f-4 -r0+hm -t3
|
||||
-Ku -a3 -An -Kt,h -s1 -q1 -Y -Ar -s5 -o1+s -At -f-1 -r1+s -As -s1 -o1 +s -s-1 -An -b+500 -n google.com --ttl 3
|
||||
-Ku -l':\x16\x03\x01\x02\x87\x01\x00\x02\x83\x03\x03\x5f\x15\x63\xcb\x06' -a1 -An -s1 -q1 -Y -At -f-1 -r1+s -As
|
||||
-Ku -a3 -An -Kt,h -d1 -s1 -q1 -Y -Ar -s5 -o1+s -d3+s -s6+s -d9+s -s12+s -d15+s -s20+s -d25+s -s30+s -d35+s -An
|
||||
-Ku -a3 -An -Kt,h -s1 -q1 -Y -At -b1000 -S -f-1 -r1+s -As -n google.com -d1+s -O1 -s29+s -t 5 -An
|
||||
-Ku -a3 -An -Kt,h -s0 -o1 -d1 -r1+s -Ar -o1 -At -f-1 -r1+s -As -b+500 -n google.com --ttl 3 -An
|
||||
-Ku -a3 -An -Kt,h -s1 -q1 -Ar -s5 -o25000+s -At -f-1 -r1+s -As -s1 -o1+s -s-1 -An -b+500 -An
|
||||
-Ku -a3 -An -Kt,h -d1 -s0+s -d3+s -s6+s -d9+s -s12+s -d15+s -s20+s -d25+s -s30+s -d35+s -An
|
||||
-Ku -a3 -An -Kt,h -d1 -s0+s -d1+s -s3+s -d6+s -s12+s -d14+s -s20+s -d24+s -s30+s -d34+s -An
|
||||
-Ku -a3 -An -Kt,h -d1 -s0+s -d3+s -s6+s -d9+s -s12+s -d15+s -s20+s -d25+s -s30+s -An
|
||||
-Ku -a3 -An -Kt,h -s1 -q1 -Y -Ar -s5 -o1+s -At -f-1 -r1+s -As -s1 -o1+s -s-1 -An
|
||||
-Ku -a3 -An -Kt,h -d1 -s0+s -s3+s -s6+s -s9+s -s12+s -s15+s -s20+s -s30+s -An
|
||||
-Ku -a1 -An -Kt,h -s1 -q1 -Ar -s5 -o1+s -At -f-1 -r1+s -As -s1 -o1+s -s-1 -An
|
||||
-Ku -a3 -An -Kt,h -s0 -o1 -Ar -o1 -At -f-1 -r1+s -As -b+500 -n google.com -An
|
||||
-s1 -q1 -Y -Ar -s5 -o25000+s -At -f-1 -r1+s -As -s1 -o1+s -s-1 -An -b+500
|
||||
-Ku -a3 -An -Kt,h -q1+s -s29+s -s30+s -s14+s -o5+s -f-1 --md5sig -Asn
|
||||
-Ku -a3 -An -Kt,h -s0 -o1 -d1 -r1+s -Ar -o1 -At -f-1 -r1+s -As -An
|
||||
-s1 -q1 -Y -Ar -s5 -o1+s -At -f-1 -r1+s -As -s1 -o1+s -s-1 -An
|
||||
-Ku -a1 -An -Kt,h -d1 -s4 -d8 -s1+s -d5+s -s10+s -d20+s -An
|
||||
-Ku -a5 -An -Kt,h -o0 -d1 -r1+s -t10 -b1500 -S -s0+s -d3+s -An
|
||||
-Ku -a3 -An -Kt,h -s0 -o1 -Ar -o1 -At -f-1 -r1+s -As -An
|
||||
-Ku -a1 -An -Kt,h -s1 -q1 -Art -f-1 --md5sig -r1+s -An
|
||||
-Ku -a1 -An -Kt,h -s1 -q2 -a2 -Art -f-1 -r2+s -a3 -An
|
||||
-Ku -a1 -An -Kt,h -s1 -q1 -a2 -Art -f-1 -r1+s -a3 -An
|
||||
-Ku -a1 -An -Kt,h -s1 -d3+s -Mh,d -At -r1+s -r3+s -An
|
||||
-Ku -a1 -An -Kt,h -q1+s -s29+s -o5+s -f-1 -S -As
|
||||
-s0 -o1 -d1 -r1+s -Ar -o1 -At -f-1 -r1+s -As
|
||||
-s1 -q1 -Y -At -b1000 -S -f-1 -r1+s -As
|
||||
-s1 -q1 -Y -a2 -Art -f-1 -r1+s -a3 -Asn
|
||||
-Ku -a5 -An -Kt,h -s1 -f-1 -S -b661 -An
|
||||
-s1 -d1 -o1 -Ar -o3 -At -f-1 -r1+s -As
|
||||
-s1 -q1 -a2 -Art -f-1 -r1+s -a3 -Asn
|
||||
-s1 -o1 -Ar -o1 -At -f-1 -r1+s -As
|
||||
-s0 -o1 -Ar -o1 -At -f-1 -r1+s -As
|
||||
-Ku -a1 -An -Kt,h -q1 -r25+s -An
|
||||
-s1 -q1 -Y -At -f-1 -r1+s -As
|
||||
-Ku -a1 -An -Kt,h -d7 -s2 -An
|
||||
@@ -0,0 +1,21 @@
|
||||
dis.gd
|
||||
discord.co
|
||||
discord.gg
|
||||
discord.app
|
||||
discord.com
|
||||
discord.dev
|
||||
discord.new
|
||||
discord.gift
|
||||
discord.gifts
|
||||
discord.media
|
||||
discord.store
|
||||
discord.design
|
||||
discordapp.com
|
||||
discordcdn.com
|
||||
discordsez.com
|
||||
discordsays.com
|
||||
discordmerch.com
|
||||
discordpartygames.com
|
||||
discordactivities.com
|
||||
stable.dl2.discordapp.net
|
||||
discord-attachments-uploads-prd.storage.googleapis.com
|
||||
@@ -0,0 +1,6 @@
|
||||
rutracker.org
|
||||
nyaa.si
|
||||
rutor.org
|
||||
nnmclub.to
|
||||
speedtest.net
|
||||
ookla.com
|
||||
+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
|
||||
@@ -12,3 +12,4 @@ target_compile_options(byedpi PRIVATE -std=c99 -O2 -Wall -Wno-unused -Wextra -Wn
|
||||
target_compile_definitions(byedpi PRIVATE ANDROID_APP)
|
||||
|
||||
target_link_libraries(byedpi PRIVATE android log)
|
||||
set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} -Wl,-z,max-page-size=16384")
|
||||
|
||||
+1
-1
Submodule app/src/main/cpp/byedpi updated: 676817c068...ba532298de
@@ -1,2 +1,2 @@
|
||||
void clear_params(void);
|
||||
void clear_params(char *line, char **argv);
|
||||
int main(int argc, char **argv);
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
#include <string.h>
|
||||
|
||||
#include <jni.h>
|
||||
#include <malloc.h>
|
||||
#include <getopt.h>
|
||||
#include <signal.h>
|
||||
#include <setjmp.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
#include "byedpi/error.h"
|
||||
#include "main.h"
|
||||
@@ -10,65 +12,100 @@
|
||||
extern int server_fd;
|
||||
static int g_proxy_running = 0;
|
||||
|
||||
JNIEXPORT jint JNI_OnLoad(
|
||||
__attribute__((unused)) JavaVM *vm,
|
||||
__attribute__((unused)) void *reserved) {
|
||||
return JNI_VERSION_1_6;
|
||||
struct params default_params = {
|
||||
.await_int = 10,
|
||||
.ipv6 = 1,
|
||||
.resolve = 1,
|
||||
.udp = 1,
|
||||
.max_open = 512,
|
||||
.bfsize = 16384,
|
||||
.baddr = {
|
||||
.in6 = { .sin6_family = AF_INET6 }
|
||||
},
|
||||
.laddr = {
|
||||
.in = { .sin_family = AF_INET }
|
||||
},
|
||||
.debug = 0
|
||||
};
|
||||
|
||||
void reset_params(void) {
|
||||
clear_params(NULL, NULL);
|
||||
params = default_params;
|
||||
}
|
||||
|
||||
JNIEXPORT jint JNICALL
|
||||
Java_io_github_dovecoteescapee_byedpi_core_ByeDpiProxy_jniStartProxy(
|
||||
JNIEnv *env,
|
||||
__attribute__((unused)) jobject thiz,
|
||||
jobjectArray args) {
|
||||
|
||||
Java_io_github_romanvht_byedpi_core_ByeDpiProxy_jniStartProxy(JNIEnv *env, __attribute__((unused)) jobject thiz, jobjectArray args) {
|
||||
if (g_proxy_running) {
|
||||
LOG(LOG_S, "proxy already running");
|
||||
return -1;
|
||||
}
|
||||
|
||||
int argc = (*env)->GetArrayLength(env, args);
|
||||
char *argv[argc];
|
||||
char **argv = calloc(argc, sizeof(char *));
|
||||
|
||||
if (!argv) {
|
||||
LOG(LOG_S, "failed to allocate memory for argv");
|
||||
return -1;
|
||||
}
|
||||
|
||||
for (int i = 0; i < argc; i++) {
|
||||
jstring arg = (jstring) (*env)->GetObjectArrayElement(env, args, i);
|
||||
const char *arg_str = (*env)->GetStringUTFChars(env, arg, 0);
|
||||
argv[i] = strdup(arg_str);
|
||||
(*env)->ReleaseStringUTFChars(env, arg, arg_str);
|
||||
}
|
||||
|
||||
g_proxy_running = 1;
|
||||
optind = optreset = 1;
|
||||
if (!arg) {
|
||||
argv[i] = NULL;
|
||||
continue;
|
||||
}
|
||||
|
||||
const char *arg_str = (*env)->GetStringUTFChars(env, arg, 0);
|
||||
argv[i] = arg_str ? strdup(arg_str) : NULL;
|
||||
|
||||
if (arg_str) (*env)->ReleaseStringUTFChars(env, arg, arg_str);
|
||||
|
||||
(*env)->DeleteLocalRef(env, arg);
|
||||
}
|
||||
|
||||
LOG(LOG_S, "starting proxy with %d args", argc);
|
||||
reset_params();
|
||||
g_proxy_running = 1;
|
||||
optind = 1;
|
||||
|
||||
int result = main(argc, argv);
|
||||
|
||||
for (int i = 0; i < argc; i++) {
|
||||
free(argv[i]);
|
||||
LOG(LOG_S, "proxy return code %d", result);
|
||||
g_proxy_running = 0;
|
||||
|
||||
for (int i = 0; i < argc; i++) free(argv[i]);
|
||||
free(argv);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
JNIEXPORT jint JNICALL
|
||||
Java_io_github_romanvht_byedpi_core_ByeDpiProxy_jniStopProxy(__attribute__((unused)) JNIEnv *env, __attribute__((unused)) jobject thiz) {
|
||||
LOG(LOG_S, "send shutdown to proxy");
|
||||
|
||||
if (!g_proxy_running) {
|
||||
LOG(LOG_S, "proxy is not running");
|
||||
return -1;
|
||||
}
|
||||
|
||||
shutdown(server_fd, SHUT_RDWR);
|
||||
g_proxy_running = 0;
|
||||
if (result < 0) {
|
||||
LOG(LOG_S, "proxy failed to start");
|
||||
return result;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
JNIEXPORT jint JNICALL
|
||||
Java_io_github_dovecoteescapee_byedpi_core_ByeDpiProxy_jniStopProxy(
|
||||
__attribute__((unused)) JNIEnv *env,
|
||||
__attribute__((unused)) jobject thiz) {
|
||||
Java_io_github_romanvht_byedpi_core_ByeDpiProxy_jniForceClose(__attribute__((unused)) JNIEnv *env, __attribute__((unused)) jobject thiz) {
|
||||
LOG(LOG_S, "closing server socket (fd: %d)", server_fd);
|
||||
|
||||
LOG(LOG_S, "send shutdown to proxy");
|
||||
|
||||
if (!g_proxy_running) {
|
||||
LOG(LOG_S, "proxy is not running");
|
||||
return 0;
|
||||
if (close(server_fd) == -1) {
|
||||
LOG(LOG_S, "failed to close server socket (fd: %d)", server_fd);
|
||||
return -1;
|
||||
}
|
||||
|
||||
shutdown(server_fd, SHUT_RDWR);
|
||||
clear_params();
|
||||
|
||||
LOG(LOG_S, "proxy socket force close");
|
||||
g_proxy_running = 0;
|
||||
return 1;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -1,293 +0,0 @@
|
||||
package io.github.dovecoteescapee.byedpi.activities
|
||||
|
||||
import android.Manifest
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.content.pm.PackageManager
|
||||
import android.net.VpnService
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import android.widget.Toast
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import io.github.dovecoteescapee.byedpi.R
|
||||
import io.github.dovecoteescapee.byedpi.data.*
|
||||
import io.github.dovecoteescapee.byedpi.fragments.MainSettingsFragment
|
||||
import io.github.dovecoteescapee.byedpi.databinding.ActivityMainBinding
|
||||
import io.github.dovecoteescapee.byedpi.services.ServiceManager
|
||||
import io.github.dovecoteescapee.byedpi.services.appStatus
|
||||
import io.github.dovecoteescapee.byedpi.utility.*
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import java.io.IOException
|
||||
|
||||
class MainActivity : AppCompatActivity() {
|
||||
private lateinit var binding: ActivityMainBinding
|
||||
|
||||
companion object {
|
||||
private val TAG: String = MainActivity::class.java.simpleName
|
||||
|
||||
private fun collectLogs(): String? =
|
||||
try {
|
||||
Runtime.getRuntime()
|
||||
.exec("logcat *:D -d")
|
||||
.inputStream.bufferedReader()
|
||||
.use { it.readText() }
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Failed to collect logs", e)
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private val vpnRegister =
|
||||
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
|
||||
if (it.resultCode == RESULT_OK) {
|
||||
ServiceManager.start(this, Mode.VPN)
|
||||
} else {
|
||||
Toast.makeText(this, R.string.vpn_permission_denied, Toast.LENGTH_SHORT).show()
|
||||
updateStatus()
|
||||
}
|
||||
}
|
||||
|
||||
private val logsRegister =
|
||||
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
val logs = collectLogs()
|
||||
|
||||
if (logs == null) {
|
||||
Toast.makeText(
|
||||
this@MainActivity,
|
||||
R.string.logs_failed,
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
} else {
|
||||
val uri = it.data?.data ?: run {
|
||||
Log.e(TAG, "No data in result")
|
||||
return@launch
|
||||
}
|
||||
contentResolver.openOutputStream(uri)?.use {
|
||||
try {
|
||||
it.write(logs.toByteArray())
|
||||
} catch (e: IOException) {
|
||||
Log.e(TAG, "Failed to save logs", e)
|
||||
}
|
||||
} ?: run {
|
||||
Log.e(TAG, "Failed to open output stream")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val receiver = object : BroadcastReceiver() {
|
||||
override fun onReceive(context: Context?, intent: Intent?) {
|
||||
Log.d(TAG, "Received intent: ${intent?.action}")
|
||||
|
||||
if (intent == null) {
|
||||
Log.w(TAG, "Received null intent")
|
||||
return
|
||||
}
|
||||
|
||||
val senderOrd = intent.getIntExtra(SENDER, -1)
|
||||
val sender = Sender.entries.getOrNull(senderOrd)
|
||||
if (sender == null) {
|
||||
Log.w(TAG, "Received intent with unknown sender: $senderOrd")
|
||||
return
|
||||
}
|
||||
|
||||
when (val action = intent.action) {
|
||||
STARTED_BROADCAST,
|
||||
STOPPED_BROADCAST -> updateStatus()
|
||||
|
||||
FAILED_BROADCAST -> {
|
||||
Toast.makeText(
|
||||
context,
|
||||
getString(R.string.failed_to_start, sender.name),
|
||||
Toast.LENGTH_SHORT,
|
||||
).show()
|
||||
updateStatus()
|
||||
}
|
||||
|
||||
else -> Log.w(TAG, "Unknown action: $action")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
binding = ActivityMainBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
|
||||
val intentFilter = IntentFilter().apply {
|
||||
addAction(STARTED_BROADCAST)
|
||||
addAction(STOPPED_BROADCAST)
|
||||
addAction(FAILED_BROADCAST)
|
||||
}
|
||||
|
||||
@SuppressLint("UnspecifiedRegisterReceiverFlag")
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
registerReceiver(receiver, intentFilter, RECEIVER_EXPORTED)
|
||||
} else {
|
||||
registerReceiver(receiver, intentFilter)
|
||||
}
|
||||
|
||||
binding.statusButton.setOnClickListener {
|
||||
binding.statusButton.isClickable = false
|
||||
|
||||
val (status, _) = appStatus
|
||||
when (status) {
|
||||
AppStatus.Halted -> start()
|
||||
AppStatus.Running -> stop()
|
||||
}
|
||||
|
||||
binding.statusButton.postDelayed({
|
||||
binding.statusButton.isClickable = true
|
||||
}, 500)
|
||||
}
|
||||
|
||||
binding.openEditorLink.setOnClickListener {
|
||||
val (status, _) = appStatus
|
||||
|
||||
if (status == AppStatus.Halted) {
|
||||
val intent = Intent(this, SettingsActivity::class.java)
|
||||
val useCmdSettings = getPreferences().getBoolean("byedpi_enable_cmd_settings", false)
|
||||
intent.putExtra("open_fragment", if (useCmdSettings) "cmd" else "ui")
|
||||
startActivity(intent)
|
||||
} else {
|
||||
Toast.makeText(this, R.string.settings_unavailable, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
|
||||
val lang = getPreferences().getString("language", "system")
|
||||
MainSettingsFragment.setLang(lang ?: "system")
|
||||
|
||||
val theme = getPreferences().getString("app_theme", null)
|
||||
MainSettingsFragment.setTheme(theme ?: "system")
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU &&
|
||||
ContextCompat.checkSelfPermission(
|
||||
this,
|
||||
Manifest.permission.POST_NOTIFICATIONS
|
||||
) != PackageManager.PERMISSION_GRANTED
|
||||
) {
|
||||
requestPermissions(arrayOf(Manifest.permission.POST_NOTIFICATIONS), 1)
|
||||
}
|
||||
|
||||
if (getPreferences().getBoolean("auto_connect", false) && appStatus.first != AppStatus.Running) {
|
||||
this.start()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
updateStatus()
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
unregisterReceiver(receiver)
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
|
||||
menuInflater.inflate(R.menu.menu_main, menu)
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
val (status, _) = appStatus
|
||||
|
||||
return when (item.itemId) {
|
||||
R.id.action_settings -> {
|
||||
if (status == AppStatus.Halted) {
|
||||
val intent = Intent(this, SettingsActivity::class.java)
|
||||
startActivity(intent)
|
||||
} else {
|
||||
Toast.makeText(this, R.string.settings_unavailable, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
R.id.action_save_logs -> {
|
||||
val intent =
|
||||
Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
|
||||
addCategory(Intent.CATEGORY_OPENABLE)
|
||||
type = "text/plain"
|
||||
putExtra(Intent.EXTRA_TITLE, "byedpi.log")
|
||||
}
|
||||
|
||||
logsRegister.launch(intent)
|
||||
true
|
||||
}
|
||||
|
||||
else -> super.onOptionsItemSelected(item)
|
||||
}
|
||||
}
|
||||
|
||||
private fun start() {
|
||||
when (getPreferences().mode()) {
|
||||
Mode.VPN -> {
|
||||
val intentPrepare = VpnService.prepare(this)
|
||||
if (intentPrepare != null) {
|
||||
vpnRegister.launch(intentPrepare)
|
||||
} else {
|
||||
ServiceManager.start(this, Mode.VPN)
|
||||
}
|
||||
}
|
||||
|
||||
Mode.Proxy -> ServiceManager.start(this, Mode.Proxy)
|
||||
}
|
||||
}
|
||||
|
||||
private fun stop() {
|
||||
ServiceManager.stop(this)
|
||||
}
|
||||
|
||||
private fun updateStatus() {
|
||||
val (status, mode) = appStatus
|
||||
|
||||
Log.i(TAG, "Updating status: $status, $mode")
|
||||
|
||||
val preferences = getPreferences()
|
||||
val (ip, port) = preferences.getProxyIpAndPort()
|
||||
|
||||
binding.proxyAddress.text = getString(R.string.proxy_address, ip, port)
|
||||
|
||||
when (status) {
|
||||
AppStatus.Halted -> {
|
||||
when (preferences.mode()) {
|
||||
Mode.VPN -> {
|
||||
binding.statusText.setText(R.string.vpn_disconnected)
|
||||
binding.statusButton.setText(R.string.vpn_connect)
|
||||
}
|
||||
|
||||
Mode.Proxy -> {
|
||||
binding.statusText.setText(R.string.proxy_down)
|
||||
binding.statusButton.setText(R.string.proxy_start)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
AppStatus.Running -> {
|
||||
when (mode) {
|
||||
Mode.VPN -> {
|
||||
binding.statusText.setText(R.string.vpn_connected)
|
||||
binding.statusButton.setText(R.string.vpn_disconnect)
|
||||
}
|
||||
|
||||
Mode.Proxy -> {
|
||||
binding.statusText.setText(R.string.proxy_up)
|
||||
binding.statusButton.setText(R.string.proxy_stop)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,156 +0,0 @@
|
||||
package io.github.dovecoteescapee.byedpi.activities
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import android.widget.Toast
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import com.google.gson.Gson
|
||||
import io.github.dovecoteescapee.byedpi.BuildConfig
|
||||
import io.github.dovecoteescapee.byedpi.R
|
||||
import io.github.dovecoteescapee.byedpi.data.AppSettings
|
||||
import io.github.dovecoteescapee.byedpi.fragments.ByeDpiCommandLineSettingsFragment
|
||||
import io.github.dovecoteescapee.byedpi.fragments.ByeDpiUISettingsFragment
|
||||
import io.github.dovecoteescapee.byedpi.fragments.MainSettingsFragment
|
||||
import io.github.dovecoteescapee.byedpi.utility.HistoryUtils
|
||||
import io.github.dovecoteescapee.byedpi.utility.getPreferences
|
||||
import io.github.dovecoteescapee.byedpi.utility.getSelectedApps
|
||||
|
||||
class SettingsActivity : AppCompatActivity() {
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.activity_settings)
|
||||
|
||||
val openFragment = intent.getStringExtra("open_fragment")
|
||||
|
||||
when (openFragment) {
|
||||
"cmd" -> {
|
||||
supportFragmentManager
|
||||
.beginTransaction()
|
||||
.replace(R.id.settings, ByeDpiCommandLineSettingsFragment())
|
||||
.commit()
|
||||
}
|
||||
"ui" -> {
|
||||
supportFragmentManager
|
||||
.beginTransaction()
|
||||
.replace(R.id.settings, ByeDpiUISettingsFragment())
|
||||
.commit()
|
||||
}
|
||||
else -> {
|
||||
supportFragmentManager
|
||||
.beginTransaction()
|
||||
.replace(R.id.settings, MainSettingsFragment())
|
||||
.commit()
|
||||
}
|
||||
}
|
||||
|
||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
|
||||
menuInflater.inflate(R.menu.menu_settings, menu)
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean = when (item.itemId) {
|
||||
android.R.id.home -> {
|
||||
onBackPressedDispatcher.onBackPressed()
|
||||
true
|
||||
}
|
||||
|
||||
R.id.action_reset_settings -> {
|
||||
val prefs = getPreferences()
|
||||
val editor = prefs.edit()
|
||||
|
||||
editor.clear()
|
||||
editor.apply()
|
||||
|
||||
recreate()
|
||||
true
|
||||
}
|
||||
|
||||
R.id.action_export_settings -> {
|
||||
val fileName = "bbd_${System.currentTimeMillis().toReadableDateTime()}.json"
|
||||
exportSettingsLauncher.launch(fileName)
|
||||
true
|
||||
}
|
||||
|
||||
R.id.action_import_settings -> {
|
||||
importSettingsLauncher.launch(arrayOf("application/json"))
|
||||
true
|
||||
}
|
||||
|
||||
else -> super.onOptionsItemSelected(item)
|
||||
}
|
||||
|
||||
private val exportSettingsLauncher = registerForActivityResult(
|
||||
ActivityResultContracts.CreateDocument("application/json")
|
||||
) { uri ->
|
||||
uri?.let {
|
||||
val prefs = getPreferences()
|
||||
val history = HistoryUtils(this).getHistory()
|
||||
val apps = prefs.getSelectedApps()
|
||||
|
||||
val settings = prefs.all.filterKeys { key ->
|
||||
key !in setOf("byedpi_command_history", "selected_apps")
|
||||
}
|
||||
|
||||
val export = AppSettings(
|
||||
app = BuildConfig.APPLICATION_ID,
|
||||
version = BuildConfig.VERSION_NAME,
|
||||
history = history,
|
||||
apps = apps,
|
||||
settings = settings
|
||||
)
|
||||
|
||||
val json = Gson().toJson(export)
|
||||
|
||||
contentResolver.openOutputStream(it)?.use { outputStream ->
|
||||
outputStream.write(json.toByteArray())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val importSettingsLauncher = registerForActivityResult(
|
||||
ActivityResultContracts.OpenDocument()
|
||||
) { uri ->
|
||||
uri?.let {
|
||||
contentResolver.openInputStream(it)?.use { inputStream ->
|
||||
val json = inputStream.bufferedReader().readText()
|
||||
|
||||
val import = Gson().fromJson(json, AppSettings::class.java)
|
||||
|
||||
if (import.app != BuildConfig.APPLICATION_ID) {
|
||||
Toast.makeText(this, "Invalid config", Toast.LENGTH_LONG).show()
|
||||
return@use
|
||||
}
|
||||
|
||||
val prefs = getPreferences()
|
||||
val editor = prefs.edit()
|
||||
|
||||
editor.clear()
|
||||
import.settings.forEach { (key, value) ->
|
||||
when (value) {
|
||||
is Int -> editor.putInt(key, value)
|
||||
is Boolean -> editor.putBoolean(key, value)
|
||||
is String -> editor.putString(key, value)
|
||||
is Float -> editor.putFloat(key, value)
|
||||
is Long -> editor.putLong(key, value)
|
||||
}
|
||||
}
|
||||
editor.putStringSet("selected_apps", import.apps.toSet())
|
||||
editor.apply()
|
||||
HistoryUtils(this).saveHistory(import.history)
|
||||
|
||||
recreate()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun Long.toReadableDateTime(): String {
|
||||
val format = java.text.SimpleDateFormat("yyyyMMdd_HHmm", java.util.Locale.getDefault())
|
||||
return format.format(this)
|
||||
}
|
||||
}
|
||||
@@ -1,444 +0,0 @@
|
||||
package io.github.dovecoteescapee.byedpi.activities
|
||||
|
||||
import android.content.ClipData
|
||||
import android.content.ClipboardManager
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.ActivityInfo
|
||||
import android.content.res.Configuration
|
||||
import android.os.Bundle
|
||||
import android.text.SpannableString
|
||||
import android.text.Spanned
|
||||
import android.text.method.LinkMovementMethod
|
||||
import android.text.style.ClickableSpan
|
||||
import android.util.Log
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.view.WindowManager
|
||||
import android.widget.Button
|
||||
import android.widget.ScrollView
|
||||
import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
import androidx.activity.OnBackPressedCallback
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import io.github.dovecoteescapee.byedpi.R
|
||||
import io.github.dovecoteescapee.byedpi.data.Mode
|
||||
import io.github.dovecoteescapee.byedpi.data.ServiceStatus
|
||||
import io.github.dovecoteescapee.byedpi.services.ByeDpiProxyService
|
||||
import io.github.dovecoteescapee.byedpi.services.ServiceManager
|
||||
import io.github.dovecoteescapee.byedpi.utility.GoogleVideoUtils
|
||||
import io.github.dovecoteescapee.byedpi.utility.HistoryUtils
|
||||
import io.github.dovecoteescapee.byedpi.utility.getPreferences
|
||||
import kotlinx.coroutines.*
|
||||
import java.io.File
|
||||
import java.net.HttpURLConnection
|
||||
import java.net.InetSocketAddress
|
||||
import java.net.Proxy
|
||||
import java.net.URL
|
||||
|
||||
class TestActivity : AppCompatActivity() {
|
||||
|
||||
private lateinit var scrollTextView: ScrollView
|
||||
private lateinit var progressTextView: TextView
|
||||
private lateinit var resultsTextView: TextView
|
||||
private lateinit var startStopButton: Button
|
||||
|
||||
private lateinit var cmdHistoryUtils: HistoryUtils
|
||||
private lateinit var sites: MutableList<String>
|
||||
private lateinit var cmds: List<String>
|
||||
|
||||
private var originalCmdArgs: String = ""
|
||||
private var testJob: Job? = null
|
||||
|
||||
private val proxyIp: String = "127.0.0.1"
|
||||
private val proxyPort: Int = 10080
|
||||
|
||||
private var isTesting: Boolean
|
||||
get() = prefs.getBoolean("is_test_running", false)
|
||||
set(value) {
|
||||
prefs.edit().putBoolean("is_test_running", value).apply()
|
||||
}
|
||||
|
||||
private val prefs by lazy { getPreferences() }
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.activity_proxy_test)
|
||||
|
||||
cmdHistoryUtils = HistoryUtils(this)
|
||||
scrollTextView = findViewById(R.id.scrollView)
|
||||
startStopButton = findViewById(R.id.startStopButton)
|
||||
resultsTextView = findViewById(R.id.resultsTextView)
|
||||
progressTextView = findViewById(R.id.progressTextView)
|
||||
|
||||
resultsTextView.movementMethod = LinkMovementMethod.getInstance()
|
||||
|
||||
if (isTesting) {
|
||||
progressTextView.text = getString(R.string.test_proxy_error)
|
||||
resultsTextView.text = getString(R.string.test_crash)
|
||||
isTesting = false
|
||||
} else {
|
||||
lifecycleScope.launch {
|
||||
val previousLogs = loadLog()
|
||||
if (previousLogs.isNotEmpty()) {
|
||||
progressTextView.text = getString(R.string.test_complete)
|
||||
resultsTextView.text = ""
|
||||
displayLog(previousLogs)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
startStopButton.setOnClickListener {
|
||||
if (isTesting) {
|
||||
stopTesting()
|
||||
} else {
|
||||
startTesting()
|
||||
}
|
||||
}
|
||||
|
||||
onBackPressedDispatcher.addCallback(this, object : OnBackPressedCallback(true) {
|
||||
override fun handleOnBackPressed() {
|
||||
if (isTesting) {
|
||||
stopTesting()
|
||||
}
|
||||
finish()
|
||||
}
|
||||
})
|
||||
|
||||
requestedOrientation = when (resources.configuration.orientation) {
|
||||
Configuration.ORIENTATION_LANDSCAPE -> {
|
||||
ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
|
||||
}
|
||||
|
||||
Configuration.ORIENTATION_PORTRAIT -> {
|
||||
ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
|
||||
}
|
||||
|
||||
else -> {
|
||||
ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
|
||||
}
|
||||
}
|
||||
|
||||
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
|
||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
|
||||
menuInflater.inflate(R.menu.menu_test, menu)
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
return when (item.itemId) {
|
||||
R.id.action_settings -> {
|
||||
if (!isTesting) {
|
||||
val intent = Intent(this, TestSettingsActivity::class.java)
|
||||
startActivity(intent)
|
||||
} else {
|
||||
Toast.makeText(this, R.string.settings_unavailable, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
true
|
||||
}
|
||||
android.R.id.home -> {
|
||||
onBackPressedDispatcher.onBackPressed()
|
||||
true
|
||||
}
|
||||
else -> super.onOptionsItemSelected(item)
|
||||
}
|
||||
}
|
||||
|
||||
private fun startProxyService() {
|
||||
try {
|
||||
ServiceManager.start(this, Mode.Proxy)
|
||||
} catch (e: Exception) {
|
||||
Log.e("TestActivity", "Error start proxy service: ${e.message}")
|
||||
}
|
||||
}
|
||||
|
||||
private fun stopProxyService() {
|
||||
try {
|
||||
ServiceManager.stop(this)
|
||||
} catch (e: Exception) {
|
||||
Log.e("TestActivity", "Error stop proxy service: ${e.message}")
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun waitForProxyStatus(
|
||||
statusNeeded: ServiceStatus,
|
||||
timeoutMillis: Long = 5000L
|
||||
): Boolean {
|
||||
val startTime = System.currentTimeMillis()
|
||||
while (System.currentTimeMillis() - startTime < timeoutMillis) {
|
||||
if (isProxyRunning() == (statusNeeded == ServiceStatus.Connected)) {
|
||||
return true
|
||||
}
|
||||
delay(100)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
private suspend fun isProxyRunning(): Boolean = withContext(Dispatchers.IO) {
|
||||
ByeDpiProxyService.getStatus() == ServiceStatus.Connected
|
||||
}
|
||||
|
||||
private fun startTesting() {
|
||||
isTesting = true
|
||||
|
||||
startStopButton.text = getString(R.string.test_stop)
|
||||
resultsTextView.text = ""
|
||||
progressTextView.text = ""
|
||||
|
||||
originalCmdArgs = prefs.getString("byedpi_cmd_args", "").orEmpty()
|
||||
|
||||
sites = loadSites().toMutableList()
|
||||
cmds = loadCmds()
|
||||
|
||||
clearLog()
|
||||
|
||||
testJob = lifecycleScope.launch {
|
||||
val delaySec = prefs.getString("byedpi_proxytest_delay", "1")?.toIntOrNull() ?: 1
|
||||
val useGeneratedGoogleDomain = prefs.getBoolean("byedpi_proxytest_gdomain", true)
|
||||
val fullLog = prefs.getBoolean("byedpi_proxytest_fulllog", false)
|
||||
val logClickable = prefs.getBoolean("byedpi_proxytest_logclickable", false)
|
||||
val requestsCount =
|
||||
prefs.getString("byedpi_proxytest_requestsсount", "1")
|
||||
?.toIntOrNull()
|
||||
?.takeIf { it > 0 }
|
||||
?: 1
|
||||
|
||||
if (useGeneratedGoogleDomain) {
|
||||
val googleVideoDomain = GoogleVideoUtils().generateGoogleVideoDomain()
|
||||
if (googleVideoDomain != null) {
|
||||
sites.add(googleVideoDomain)
|
||||
appendTextToResults("--- $googleVideoDomain ---\n\n")
|
||||
Log.i("TestActivity", "Added auto-generated Google domain: $googleVideoDomain")
|
||||
} else {
|
||||
Log.e("TestActivity", "Failed to generate Google domain")
|
||||
}
|
||||
}
|
||||
|
||||
val successfulCmds = mutableListOf<Pair<String, Int>>()
|
||||
|
||||
for ((index, cmd) in cmds.withIndex()) {
|
||||
val cmdIndex = index + 1
|
||||
progressTextView.text = getString(R.string.test_process, cmdIndex, cmds.size)
|
||||
|
||||
val testCmd = "--ip $proxyIp --port $proxyPort $cmd"
|
||||
updateCmdInPreferences(testCmd)
|
||||
|
||||
if (!isProxyRunning()) startProxyService()
|
||||
waitForProxyStatus(ServiceStatus.Connected)
|
||||
|
||||
if (logClickable) {
|
||||
appendLinkToResults("$cmd\n")
|
||||
} else {
|
||||
appendTextToResults("$cmd\n")
|
||||
}
|
||||
|
||||
val totalRequests = sites.size * requestsCount
|
||||
val checkResults = checkSitesAsync(sites, requestsCount, fullLog)
|
||||
val successfulCount = checkResults.sumOf { it.second }
|
||||
val successPercentage = (successfulCount * 100) / totalRequests
|
||||
delay(delaySec * 1000L)
|
||||
|
||||
if (successPercentage >= 50) successfulCmds.add(cmd to successPercentage)
|
||||
appendTextToResults("$successfulCount/$totalRequests ($successPercentage%)\n\n")
|
||||
|
||||
if (isProxyRunning()) stopProxyService()
|
||||
waitForProxyStatus(ServiceStatus.Disconnected)
|
||||
delay(delaySec * 1000L)
|
||||
}
|
||||
|
||||
successfulCmds.sortByDescending { it.second }
|
||||
|
||||
progressTextView.text = getString(R.string.test_complete)
|
||||
appendTextToResults("${getString(R.string.test_good_cmds)}\n\n")
|
||||
|
||||
successfulCmds.forEachIndexed { index, (cmd, success) ->
|
||||
appendTextToResults("${index + 1}. ")
|
||||
appendLinkToResults("$cmd\n")
|
||||
appendTextToResults("$success%\n\n")
|
||||
}
|
||||
|
||||
appendTextToResults(getString(R.string.test_complete_info))
|
||||
stopTesting()
|
||||
}
|
||||
}
|
||||
|
||||
private fun stopTesting() {
|
||||
updateCmdInPreferences(originalCmdArgs)
|
||||
|
||||
isTesting = false
|
||||
testJob?.cancel()
|
||||
startStopButton.text = getString(R.string.test_start)
|
||||
|
||||
lifecycleScope.launch {
|
||||
if (isProxyRunning()) {
|
||||
stopProxyService()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun appendTextToResults(text: String) {
|
||||
resultsTextView.append(text)
|
||||
if (isTesting) saveLog(text)
|
||||
scrollToBottom()
|
||||
}
|
||||
|
||||
private fun appendLinkToResults(text: String) {
|
||||
val spannableString = SpannableString(text)
|
||||
val menuItems = arrayOf(
|
||||
getString(R.string.cmd_history_apply),
|
||||
getString(R.string.cmd_history_copy)
|
||||
)
|
||||
|
||||
spannableString.setSpan(
|
||||
object : ClickableSpan() {
|
||||
override fun onClick(widget: View) {
|
||||
AlertDialog.Builder(this@TestActivity)
|
||||
.setTitle(getString(R.string.cmd_history_menu))
|
||||
.setItems(menuItems) { _, which ->
|
||||
when (which) {
|
||||
0 -> addToHistory(text.trim())
|
||||
1 -> copyToClipboard(text.trim())
|
||||
}
|
||||
}
|
||||
.show()
|
||||
}
|
||||
},
|
||||
0,
|
||||
text.length,
|
||||
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
|
||||
)
|
||||
|
||||
resultsTextView.append(spannableString)
|
||||
if (isTesting) saveLog("{$text}")
|
||||
scrollToBottom()
|
||||
}
|
||||
|
||||
private fun scrollToBottom() {
|
||||
scrollTextView.post {
|
||||
scrollTextView.fullScroll(ScrollView.FOCUS_DOWN)
|
||||
}
|
||||
}
|
||||
|
||||
private fun copyToClipboard(text: String) {
|
||||
val clipboard = getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
||||
val clip = ClipData.newPlainText("command", text)
|
||||
clipboard.setPrimaryClip(clip)
|
||||
}
|
||||
|
||||
private fun addToHistory(command: String) {
|
||||
updateCmdInPreferences(command)
|
||||
cmdHistoryUtils.addCommand(command)
|
||||
}
|
||||
|
||||
private fun displayLog(log: String) {
|
||||
log.split("{", "}").forEachIndexed { index, part ->
|
||||
if (index % 2 == 0) {
|
||||
appendTextToResults(part)
|
||||
} else {
|
||||
appendLinkToResults(part)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun saveLog(text: String) {
|
||||
val file = File(filesDir, "proxy_test.log")
|
||||
file.appendText(text)
|
||||
}
|
||||
|
||||
private fun loadLog(): String {
|
||||
val file = File(filesDir, "proxy_test.log")
|
||||
return if (file.exists()) file.readText() else ""
|
||||
}
|
||||
|
||||
private fun clearLog() {
|
||||
val file = File(filesDir, "proxy_test.log")
|
||||
file.writeText("")
|
||||
}
|
||||
|
||||
private fun loadSites(): List<String> {
|
||||
val userDomains = prefs.getBoolean("byedpi_proxytest_userdomains", false)
|
||||
return if (userDomains) {
|
||||
val domains = prefs.getString("byedpi_proxytest_domains", "").orEmpty()
|
||||
domains.lines()
|
||||
.map { it.trim() }
|
||||
.filter { it.isNotEmpty() }
|
||||
} else {
|
||||
assets.open("proxytest_sites.txt").bufferedReader().useLines { it.toList() }
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadCmds(): List<String> {
|
||||
val userCommands = prefs.getBoolean("byedpi_proxytest_usercommands", false)
|
||||
return if (userCommands) {
|
||||
val commands = prefs.getString("byedpi_proxytest_commands", "").orEmpty()
|
||||
commands.lines()
|
||||
.map { it.trim() }
|
||||
.filter { it.isNotEmpty() }
|
||||
} else {
|
||||
assets.open("proxytest_cmds.txt").bufferedReader().useLines { it.toList() }
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun checkSitesAsync(
|
||||
sites: List<String>,
|
||||
requestsCount: Int,
|
||||
fullLog: Boolean
|
||||
): List<Pair<String, Int>> {
|
||||
return withContext(Dispatchers.IO) {
|
||||
sites.map { site ->
|
||||
async {
|
||||
if (!isProxyRunning()) return@async site to 0
|
||||
|
||||
val successCount = checkSiteAccess(site, requestsCount)
|
||||
if (fullLog) {
|
||||
withContext(Dispatchers.Main) {
|
||||
appendTextToResults("$site - $successCount/$requestsCount\n")
|
||||
}
|
||||
}
|
||||
site to successCount
|
||||
}
|
||||
}.awaitAll()
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun checkSiteAccess(
|
||||
site: String,
|
||||
requestsCount: Int
|
||||
): Int = withContext(Dispatchers.IO) {
|
||||
var responseCount = 0
|
||||
val formattedUrl = if (site.startsWith("http://") || site.startsWith("https://"))
|
||||
site
|
||||
else
|
||||
"https://$site"
|
||||
|
||||
repeat(requestsCount) { attempt ->
|
||||
Log.i("CheckSite", "Attempt ${attempt + 1}/$requestsCount for $site")
|
||||
try {
|
||||
val url = URL(formattedUrl)
|
||||
val proxy = Proxy(Proxy.Type.SOCKS, InetSocketAddress(proxyIp, proxyPort))
|
||||
val connection = url.openConnection(proxy) as HttpURLConnection
|
||||
connection.requestMethod = "GET"
|
||||
connection.connectTimeout = 2000
|
||||
connection.readTimeout = 2000
|
||||
|
||||
val responseCode = connection.responseCode
|
||||
responseCount++
|
||||
Log.i("CheckSite", "Response for $site: $responseCode")
|
||||
connection.disconnect()
|
||||
} catch (e: Exception) {
|
||||
Log.e("CheckSite", "Error accessing $site: ${e.message}")
|
||||
}
|
||||
}
|
||||
responseCount
|
||||
}
|
||||
|
||||
private fun updateCmdInPreferences(cmd: String) {
|
||||
prefs.edit().putString("byedpi_cmd_args", cmd).apply()
|
||||
}
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
package io.github.dovecoteescapee.byedpi.activities
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.MenuItem
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import io.github.dovecoteescapee.byedpi.R
|
||||
import io.github.dovecoteescapee.byedpi.fragments.ProxyTestSettingsFragment
|
||||
|
||||
class TestSettingsActivity : AppCompatActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.activity_test_settings)
|
||||
|
||||
supportFragmentManager
|
||||
.beginTransaction()
|
||||
.replace(R.id.test_settings, ProxyTestSettingsFragment())
|
||||
.commit()
|
||||
|
||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean = when (item.itemId) {
|
||||
android.R.id.home -> {
|
||||
onBackPressedDispatcher.onBackPressed()
|
||||
true
|
||||
}
|
||||
else -> super.onOptionsItemSelected(item)
|
||||
}
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
package io.github.dovecoteescapee.byedpi.activities
|
||||
|
||||
import android.net.VpnService
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import io.github.dovecoteescapee.byedpi.data.AppStatus
|
||||
import io.github.dovecoteescapee.byedpi.data.Mode
|
||||
import io.github.dovecoteescapee.byedpi.services.ServiceManager
|
||||
import io.github.dovecoteescapee.byedpi.services.appStatus
|
||||
import io.github.dovecoteescapee.byedpi.utility.getPreferences
|
||||
import io.github.dovecoteescapee.byedpi.utility.mode
|
||||
|
||||
class ToggleActivity : AppCompatActivity() {
|
||||
|
||||
companion object {
|
||||
private const val TAG = "ToggleServiceActivity"
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
toggleService()
|
||||
finish()
|
||||
}
|
||||
|
||||
private fun toggleService() {
|
||||
val (status) = appStatus
|
||||
when (status) {
|
||||
AppStatus.Halted -> {
|
||||
val mode = getPreferences().mode()
|
||||
|
||||
if (mode == Mode.VPN && VpnService.prepare(this) != null) {
|
||||
return
|
||||
}
|
||||
|
||||
ServiceManager.start(this, mode)
|
||||
Log.i(TAG, "Toggle start")
|
||||
}
|
||||
AppStatus.Running -> {
|
||||
ServiceManager.stop(this)
|
||||
Log.i(TAG, "Toggle stop")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,73 +0,0 @@
|
||||
package io.github.dovecoteescapee.byedpi.adapters
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.CheckBox
|
||||
import android.widget.Filter
|
||||
import android.widget.Filterable
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import io.github.dovecoteescapee.byedpi.R
|
||||
import io.github.dovecoteescapee.byedpi.data.AppInfo
|
||||
|
||||
class AppSelectionAdapter(
|
||||
private val allApps: List<AppInfo>,
|
||||
private val onAppSelected: (AppInfo, Boolean) -> Unit
|
||||
) : RecyclerView.Adapter<AppSelectionAdapter.ViewHolder>(), Filterable {
|
||||
|
||||
private val filteredApps: MutableList<AppInfo> = allApps
|
||||
.filter { !it.appName.contains("com.") }
|
||||
.toMutableList()
|
||||
|
||||
private val originalApps: List<AppInfo> = allApps
|
||||
.filter { !it.appName.contains("com.") }
|
||||
|
||||
class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {
|
||||
val appIcon: ImageView = view.findViewById(R.id.appIcon)
|
||||
val appName: TextView = view.findViewById(R.id.appName)
|
||||
val appCheckBox: CheckBox = view.findViewById(R.id.appCheckBox)
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
||||
val view = LayoutInflater.from(parent.context).inflate(R.layout.item_app, parent, false)
|
||||
return ViewHolder(view)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
||||
val app = filteredApps[position]
|
||||
holder.appIcon.setImageDrawable(app.icon)
|
||||
holder.appName.text = app.appName
|
||||
holder.appCheckBox.isChecked = app.isSelected
|
||||
|
||||
holder.itemView.setOnClickListener {
|
||||
app.isSelected = !app.isSelected
|
||||
holder.appCheckBox.isChecked = app.isSelected
|
||||
onAppSelected(app, app.isSelected)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getItemCount() = filteredApps.size
|
||||
|
||||
override fun getFilter(): Filter {
|
||||
return object : Filter() {
|
||||
override fun performFiltering(constraint: CharSequence?): FilterResults {
|
||||
val query = constraint?.toString()?.lowercase() ?: ""
|
||||
val filteredList = if (query.isEmpty()) {
|
||||
originalApps
|
||||
} else {
|
||||
originalApps.filter { it.appName.lowercase().contains(query) }
|
||||
}
|
||||
return FilterResults().apply { values = filteredList }
|
||||
}
|
||||
|
||||
override fun publishResults(constraint: CharSequence?, results: FilterResults?) {
|
||||
val newFilteredApps = (results?.values as? List<AppInfo>) ?: originalApps
|
||||
filteredApps.clear()
|
||||
filteredApps.addAll(newFilteredApps)
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,343 +0,0 @@
|
||||
package io.github.dovecoteescapee.byedpi.core
|
||||
|
||||
import android.content.SharedPreferences
|
||||
import android.util.Log
|
||||
import io.github.dovecoteescapee.byedpi.utility.checkIpAndPortInCmd
|
||||
import io.github.dovecoteescapee.byedpi.utility.getStringNotNull
|
||||
import io.github.dovecoteescapee.byedpi.utility.shellSplit
|
||||
|
||||
sealed interface ByeDpiProxyPreferences {
|
||||
companion object {
|
||||
fun fromSharedPreferences(preferences: SharedPreferences): ByeDpiProxyPreferences =
|
||||
when (preferences.getBoolean("byedpi_enable_cmd_settings", false)) {
|
||||
true -> ByeDpiProxyCmdPreferences(preferences)
|
||||
false -> ByeDpiProxyUIPreferences(preferences)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class ByeDpiProxyCmdPreferences(val args: Array<String>) : ByeDpiProxyPreferences {
|
||||
constructor(preferences: SharedPreferences) : this(
|
||||
cmdToArgs(
|
||||
preferences.getStringNotNull("byedpi_cmd_args", ""),
|
||||
preferences
|
||||
)
|
||||
)
|
||||
|
||||
companion object {
|
||||
private fun cmdToArgs(cmd: String, preferences: SharedPreferences): Array<String> {
|
||||
val firstArgIndex = cmd.indexOf("-")
|
||||
val args = (if (firstArgIndex > 0) cmd.substring(firstArgIndex) else cmd).trim()
|
||||
|
||||
Log.d("ProxyPref", "CMD: $args")
|
||||
|
||||
val (cmdIp, cmdPort) = preferences.checkIpAndPortInCmd()
|
||||
val hasIp = cmdIp != null
|
||||
val hasPort = cmdPort != null
|
||||
|
||||
val enableHttp = preferences.getBoolean("byedpi_http_connect", false)
|
||||
val hasHttp = args.contains("-G") || args.contains("--http-connect")
|
||||
|
||||
val ip = preferences.getStringNotNull("byedpi_proxy_ip", "127.0.0.1")
|
||||
val port = preferences.getStringNotNull("byedpi_proxy_port", "1080")
|
||||
|
||||
val prefix = buildString {
|
||||
if (!hasIp) append("--ip $ip ")
|
||||
if (!hasPort) append("--port $port ")
|
||||
if (enableHttp && !hasHttp) append("--http-connect ")
|
||||
}
|
||||
|
||||
Log.d("ProxyPref", "Added from settings: $prefix")
|
||||
|
||||
if (prefix.isNotEmpty()) {
|
||||
return arrayOf("ciadpi") + shellSplit("$prefix$args")
|
||||
}
|
||||
|
||||
return arrayOf("ciadpi") + shellSplit(args)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class ByeDpiProxyUIPreferences(
|
||||
ip: String? = null,
|
||||
port: Int? = null,
|
||||
httpConnect: Boolean? = null,
|
||||
maxConnections: Int? = null,
|
||||
bufferSize: Int? = null,
|
||||
defaultTtl: Int? = null,
|
||||
noDomain: Boolean? = null,
|
||||
desyncHttp: Boolean? = null,
|
||||
desyncHttps: Boolean? = null,
|
||||
desyncUdp: Boolean? = null,
|
||||
desyncMethod: DesyncMethod? = null,
|
||||
splitPosition: Int? = null,
|
||||
splitAtHost: Boolean? = null,
|
||||
fakeTtl: Int? = null,
|
||||
fakeSni: String? = null,
|
||||
oobChar: String? = null,
|
||||
hostMixedCase: Boolean? = null,
|
||||
domainMixedCase: Boolean? = null,
|
||||
hostRemoveSpaces: Boolean? = null,
|
||||
tlsRecordSplit: Boolean? = null,
|
||||
tlsRecordSplitPosition: Int? = null,
|
||||
tlsRecordSplitAtSni: Boolean? = null,
|
||||
hostsMode: HostsMode? = null,
|
||||
hosts: String? = null,
|
||||
tcpFastOpen: Boolean? = null,
|
||||
udpFakeCount: Int? = null,
|
||||
dropSack: Boolean? = null,
|
||||
byedpiFakeOffset: Int? = null,
|
||||
) : ByeDpiProxyPreferences {
|
||||
val ip: String = ip ?: "127.0.0.1"
|
||||
val port: Int = port ?: 1080
|
||||
val httpConnect: Boolean = httpConnect ?: false
|
||||
val maxConnections: Int = maxConnections ?: 512
|
||||
val bufferSize: Int = bufferSize ?: 16384
|
||||
val defaultTtl: Int = defaultTtl ?: 0
|
||||
val noDomain: Boolean = noDomain ?: false
|
||||
val desyncHttp: Boolean = desyncHttp ?: true
|
||||
val desyncHttps: Boolean = desyncHttps ?: true
|
||||
val desyncUdp: Boolean = desyncUdp ?: false
|
||||
val desyncMethod: DesyncMethod = desyncMethod ?: DesyncMethod.Disorder
|
||||
val splitPosition: Int = splitPosition ?: 1
|
||||
val splitAtHost: Boolean = splitAtHost ?: false
|
||||
val fakeTtl: Int = fakeTtl ?: 8
|
||||
val fakeSni: String = fakeSni ?: "www.iana.org"
|
||||
val oobChar: Byte = (oobChar ?: "a")[0].code.toByte()
|
||||
val hostMixedCase: Boolean = hostMixedCase ?: false
|
||||
val domainMixedCase: Boolean = domainMixedCase ?: false
|
||||
val hostRemoveSpaces: Boolean = hostRemoveSpaces ?: false
|
||||
val tlsRecordSplit: Boolean = tlsRecordSplit ?: false
|
||||
val tlsRecordSplitPosition: Int = tlsRecordSplitPosition ?: 0
|
||||
val tlsRecordSplitAtSni: Boolean = tlsRecordSplitAtSni ?: false
|
||||
val hostsMode: HostsMode =
|
||||
if (hosts?.isBlank() != false) HostsMode.Disable
|
||||
else hostsMode ?: HostsMode.Disable
|
||||
val hosts: String? =
|
||||
if (this.hostsMode == HostsMode.Disable) null
|
||||
else hosts?.trim()
|
||||
val tcpFastOpen: Boolean = tcpFastOpen ?: false
|
||||
val udpFakeCount: Int = udpFakeCount ?: 1
|
||||
val dropSack: Boolean = dropSack ?: false
|
||||
val fakeOffset: Int = byedpiFakeOffset ?: 0
|
||||
|
||||
constructor(preferences: SharedPreferences) : this(
|
||||
ip = preferences.getString("byedpi_proxy_ip", null),
|
||||
port = preferences.getString("byedpi_proxy_port", null)?.toIntOrNull(),
|
||||
httpConnect = preferences.getBoolean("byedpi_http_connect", false),
|
||||
maxConnections = preferences.getString("byedpi_max_connections", null)?.toIntOrNull(),
|
||||
bufferSize = preferences.getString("byedpi_buffer_size", null)?.toIntOrNull(),
|
||||
defaultTtl = preferences.getString("byedpi_default_ttl", null)?.toIntOrNull(),
|
||||
noDomain = preferences.getBoolean("byedpi_no_domain", false),
|
||||
desyncHttp = preferences.getBoolean("byedpi_desync_http", true),
|
||||
desyncHttps = preferences.getBoolean("byedpi_desync_https", true),
|
||||
desyncUdp = preferences.getBoolean("byedpi_desync_udp", false),
|
||||
desyncMethod = preferences.getString("byedpi_desync_method", null)
|
||||
?.let { DesyncMethod.fromName(it) },
|
||||
splitPosition = preferences.getString("byedpi_split_position", null)?.toIntOrNull(),
|
||||
splitAtHost = preferences.getBoolean("byedpi_split_at_host", false),
|
||||
fakeTtl = preferences.getString("byedpi_fake_ttl", null)?.toIntOrNull(),
|
||||
fakeSni = preferences.getString("byedpi_fake_sni", null),
|
||||
oobChar = preferences.getString("byedpi_oob_data", null),
|
||||
hostMixedCase = preferences.getBoolean("byedpi_host_mixed_case", false),
|
||||
domainMixedCase = preferences.getBoolean("byedpi_domain_mixed_case", false),
|
||||
hostRemoveSpaces = preferences.getBoolean("byedpi_host_remove_spaces", false),
|
||||
tlsRecordSplit = preferences.getBoolean("byedpi_tlsrec_enabled", false),
|
||||
tlsRecordSplitPosition = preferences.getString("byedpi_tlsrec_position", null)
|
||||
?.toIntOrNull(),
|
||||
tlsRecordSplitAtSni = preferences.getBoolean("byedpi_tlsrec_at_sni", false),
|
||||
hostsMode = preferences.getString("byedpi_hosts_mode", null)
|
||||
?.let { HostsMode.fromName(it) },
|
||||
hosts = preferences.getString("byedpi_hosts_mode", null)?.let {
|
||||
when (HostsMode.fromName(it)) {
|
||||
HostsMode.Blacklist -> preferences.getString("byedpi_hosts_blacklist", null)
|
||||
HostsMode.Whitelist -> preferences.getString("byedpi_hosts_whitelist", null)
|
||||
else -> null
|
||||
}
|
||||
},
|
||||
tcpFastOpen = preferences.getBoolean("byedpi_tcp_fast_open", false),
|
||||
udpFakeCount = preferences.getString("byedpi_udp_fake_count", null)?.toIntOrNull(),
|
||||
dropSack = preferences.getBoolean("byedpi_drop_sack", false),
|
||||
byedpiFakeOffset = preferences.getString("byedpi_fake_offset", null)?.toIntOrNull(),
|
||||
)
|
||||
|
||||
enum class DesyncMethod {
|
||||
None,
|
||||
Split,
|
||||
Disorder,
|
||||
Fake,
|
||||
OOB,
|
||||
DISOOB;
|
||||
|
||||
companion object {
|
||||
fun fromName(name: String): DesyncMethod {
|
||||
return when (name) {
|
||||
"none" -> None
|
||||
"split" -> Split
|
||||
"disorder" -> Disorder
|
||||
"fake" -> Fake
|
||||
"oob" -> OOB
|
||||
"disoob" -> DISOOB
|
||||
else -> throw IllegalArgumentException("Unknown desync method: $name")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum class HostsMode {
|
||||
Disable,
|
||||
Blacklist,
|
||||
Whitelist;
|
||||
|
||||
companion object {
|
||||
fun fromName(name: String): HostsMode {
|
||||
return when (name) {
|
||||
"disable" -> Disable
|
||||
"blacklist" -> Blacklist
|
||||
"whitelist" -> Whitelist
|
||||
else -> throw IllegalArgumentException("Unknown hosts mode: $name")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val uiargs: Array<String>
|
||||
get() {
|
||||
val args = mutableListOf("ciadpi")
|
||||
|
||||
ip.takeIf { it.isNotEmpty() }?.let {
|
||||
args.add("-i${it}")
|
||||
}
|
||||
|
||||
port.takeIf { it != 0 }?.let {
|
||||
args.add("-p${it}")
|
||||
}
|
||||
|
||||
maxConnections.takeIf { it != 0 }?.let {
|
||||
args.add("-c${it}")
|
||||
}
|
||||
|
||||
bufferSize.takeIf { it != 0 }?.let {
|
||||
args.add("-b${it}")
|
||||
}
|
||||
|
||||
if (httpConnect) args.add("-G")
|
||||
|
||||
val protocols = mutableListOf<String>()
|
||||
if (desyncHttps) protocols.add("t")
|
||||
if (desyncHttp) protocols.add("h")
|
||||
|
||||
if (!hosts.isNullOrBlank()) {
|
||||
val hostStr = ":${hosts}"
|
||||
val hostBlock = mutableListOf<String>()
|
||||
|
||||
when (hostsMode) {
|
||||
HostsMode.Blacklist -> {
|
||||
hostBlock.add("-H${hostStr}")
|
||||
hostBlock.add("-An")
|
||||
if (protocols.isNotEmpty()) {
|
||||
hostBlock.add("-K${protocols.joinToString(",")}")
|
||||
}
|
||||
}
|
||||
HostsMode.Whitelist -> {
|
||||
if (protocols.isNotEmpty()) {
|
||||
hostBlock.add("-K${protocols.joinToString(",")}")
|
||||
}
|
||||
hostBlock.add("-H${hostStr}")
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
args.addAll(hostBlock)
|
||||
} else {
|
||||
if (protocols.isNotEmpty()) {
|
||||
args.add("-K${protocols.joinToString(",")}")
|
||||
}
|
||||
}
|
||||
|
||||
defaultTtl.takeIf { it != 0 }?.let {
|
||||
args.add("-g${it}")
|
||||
}
|
||||
|
||||
if (noDomain) {
|
||||
args.add("-N")
|
||||
}
|
||||
|
||||
desyncMethod.let { method ->
|
||||
splitPosition.takeIf { it != 0 }?.let { pos ->
|
||||
var posArg = pos.toString()
|
||||
if (splitAtHost) {
|
||||
posArg += "+h"
|
||||
}
|
||||
val option = when (method) {
|
||||
DesyncMethod.Split -> "-s"
|
||||
DesyncMethod.Disorder -> "-d"
|
||||
DesyncMethod.OOB -> "-o"
|
||||
DesyncMethod.DISOOB -> "-q"
|
||||
DesyncMethod.Fake -> "-f"
|
||||
DesyncMethod.None -> ""
|
||||
}
|
||||
args.add("${option}${posArg}")
|
||||
}
|
||||
}
|
||||
|
||||
if (desyncMethod == DesyncMethod.Fake) {
|
||||
fakeTtl.takeIf { it != 0 }?.let {
|
||||
args.add("-t${it}")
|
||||
}
|
||||
|
||||
fakeSni.takeIf { it.isNotEmpty() }?.let {
|
||||
args.add("-n${it}")
|
||||
}
|
||||
|
||||
fakeOffset.takeIf { it != 0 }?.let {
|
||||
args.add("-O${it}")
|
||||
}
|
||||
}
|
||||
|
||||
if (desyncMethod == DesyncMethod.OOB ||
|
||||
desyncMethod == DesyncMethod.DISOOB) {
|
||||
args.add("-e${oobChar}")
|
||||
}
|
||||
|
||||
val modHttpFlags = mutableListOf<String>()
|
||||
if (hostMixedCase) modHttpFlags.add("h")
|
||||
if (domainMixedCase) modHttpFlags.add("d")
|
||||
if (hostRemoveSpaces) modHttpFlags.add("r")
|
||||
if (modHttpFlags.isNotEmpty()) {
|
||||
args.add("-M${modHttpFlags.joinToString(",")}")
|
||||
}
|
||||
|
||||
if (tlsRecordSplit) {
|
||||
tlsRecordSplitPosition.takeIf { it != 0 }?.let {
|
||||
var tlsRecArg = it.toString()
|
||||
if (tlsRecordSplitAtSni) {
|
||||
tlsRecArg += "+s"
|
||||
}
|
||||
args.add("-r${tlsRecArg}")
|
||||
}
|
||||
}
|
||||
|
||||
if (tcpFastOpen) {
|
||||
args.add("-F")
|
||||
}
|
||||
|
||||
if (dropSack) {
|
||||
args.add("-Y")
|
||||
}
|
||||
|
||||
args.add("-An")
|
||||
|
||||
if (desyncUdp) {
|
||||
args.add("-Ku")
|
||||
|
||||
udpFakeCount.takeIf { it != 0 }?.let {
|
||||
args.add("-a${it}")
|
||||
}
|
||||
|
||||
args.add("-An")
|
||||
}
|
||||
|
||||
Log.d("ProxyPref", "UI to cmd: ${args.joinToString(" ")}")
|
||||
return args.toTypedArray()
|
||||
}
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
package io.github.dovecoteescapee.byedpi.data
|
||||
|
||||
const val START_ACTION = "start"
|
||||
const val STOP_ACTION = "stop"
|
||||
@@ -1,9 +0,0 @@
|
||||
package io.github.dovecoteescapee.byedpi.data
|
||||
|
||||
data class AppSettings(
|
||||
val app: String,
|
||||
val version: String,
|
||||
val history: List<Command>,
|
||||
val apps: List<String>,
|
||||
val settings: Map<String, Any?>
|
||||
)
|
||||
@@ -1,12 +0,0 @@
|
||||
package io.github.dovecoteescapee.byedpi.data
|
||||
|
||||
const val STARTED_BROADCAST = "io.github.dovecoteescapee.byedpi.STARTED"
|
||||
const val STOPPED_BROADCAST = "io.github.dovecoteescapee.byedpi.STOPPED"
|
||||
const val FAILED_BROADCAST = "io.github.dovecoteescapee.byedpi.FAILED"
|
||||
|
||||
const val SENDER = "sender"
|
||||
|
||||
enum class Sender(val senderName: String) {
|
||||
Proxy("Proxy"),
|
||||
VPN("VPN")
|
||||
}
|
||||
-192
@@ -1,192 +0,0 @@
|
||||
package io.github.dovecoteescapee.byedpi.fragments
|
||||
|
||||
import android.content.ClipData
|
||||
import android.content.ClipboardManager
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import android.widget.EditText
|
||||
import android.widget.LinearLayout
|
||||
import androidx.preference.*
|
||||
import io.github.dovecoteescapee.byedpi.R
|
||||
import io.github.dovecoteescapee.byedpi.utility.findPreferenceNotNull
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import io.github.dovecoteescapee.byedpi.data.Command
|
||||
import io.github.dovecoteescapee.byedpi.utility.HistoryUtils
|
||||
|
||||
class ByeDpiCommandLineSettingsFragment : PreferenceFragmentCompat() {
|
||||
|
||||
private lateinit var cmdHistoryUtils: HistoryUtils
|
||||
private lateinit var editTextPreference: EditTextPreference
|
||||
private lateinit var historyHeader: Preference
|
||||
private lateinit var clearButton: Preference
|
||||
private val historyPreferences = mutableListOf<Preference>()
|
||||
|
||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||
setPreferencesFromResource(R.xml.byedpi_cmd_settings, rootKey)
|
||||
|
||||
cmdHistoryUtils = HistoryUtils(requireContext())
|
||||
|
||||
editTextPreference = findPreferenceNotNull("byedpi_cmd_args")
|
||||
historyHeader = findPreferenceNotNull("cmd_history_header")
|
||||
clearButton = findPreferenceNotNull("clear_cmd_args")
|
||||
|
||||
editTextPreference.setOnPreferenceChangeListener { _, newValue ->
|
||||
val newCommand = newValue.toString()
|
||||
if (newCommand.isNotBlank()) cmdHistoryUtils.addCommand(newCommand)
|
||||
updateHistoryItems()
|
||||
true
|
||||
}
|
||||
|
||||
clearButton.setOnPreferenceClickListener {
|
||||
editTextPreference.text = ""
|
||||
true
|
||||
}
|
||||
|
||||
historyHeader.setOnPreferenceClickListener {
|
||||
showHistoryClearDialog()
|
||||
true
|
||||
}
|
||||
|
||||
updateHistoryItems()
|
||||
}
|
||||
|
||||
private fun updateHistoryItems() {
|
||||
historyPreferences.forEach { preference ->
|
||||
preferenceScreen.removePreference(preference)
|
||||
}
|
||||
|
||||
historyPreferences.clear()
|
||||
val history = cmdHistoryUtils.getHistory()
|
||||
historyHeader.isVisible = history.isNotEmpty()
|
||||
|
||||
if (history.isNotEmpty()) {
|
||||
history.sortedWith(compareByDescending<Command> { it.pinned }.thenBy { history.indexOf(it) })
|
||||
.forEachIndexed { _, command ->
|
||||
val preference = createPreference(command)
|
||||
historyPreferences.add(preference)
|
||||
preferenceScreen.addPreference(preference)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun createPreference(command: Command) =
|
||||
Preference(requireContext()).apply {
|
||||
title = command.text
|
||||
summary = buildSummary(command)
|
||||
setOnPreferenceClickListener {
|
||||
showActionDialog(command)
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
private fun buildSummary(command: Command): String {
|
||||
val summary = StringBuilder()
|
||||
if (command.name != null) {
|
||||
summary.append(command.name)
|
||||
}
|
||||
if (command.pinned) {
|
||||
if (summary.isNotEmpty()) summary.append(" - ")
|
||||
summary.append(context?.getString(R.string.cmd_history_pinned))
|
||||
}
|
||||
return summary.toString()
|
||||
}
|
||||
|
||||
private fun showHistoryClearDialog() {
|
||||
val options = arrayOf(
|
||||
getString(R.string.cmd_history_delete_all),
|
||||
getString(R.string.cmd_history_delete_unpinned),
|
||||
)
|
||||
|
||||
AlertDialog.Builder(requireContext())
|
||||
.setTitle(getString(R.string.cmd_history_menu))
|
||||
.setItems(options) { _, which ->
|
||||
when (which) {
|
||||
0 -> deleteAllHistory()
|
||||
1 -> deleteUnpinnedHistory()
|
||||
}
|
||||
}
|
||||
.setNegativeButton(getString(android.R.string.cancel), null)
|
||||
.show()
|
||||
}
|
||||
|
||||
private fun deleteAllHistory() {
|
||||
cmdHistoryUtils.clearAllHistory()
|
||||
updateHistoryItems()
|
||||
}
|
||||
|
||||
private fun deleteUnpinnedHistory() {
|
||||
cmdHistoryUtils.clearUnpinnedHistory()
|
||||
updateHistoryItems()
|
||||
}
|
||||
|
||||
private fun showActionDialog(command: Command) {
|
||||
val options = arrayOf(
|
||||
getString(R.string.cmd_history_apply),
|
||||
if (command.pinned) getString(R.string.cmd_history_unpin) else getString(R.string.cmd_history_pin),
|
||||
getString(R.string.cmd_history_rename),
|
||||
getString(R.string.cmd_history_copy),
|
||||
getString(R.string.cmd_history_delete)
|
||||
)
|
||||
|
||||
AlertDialog.Builder(requireContext())
|
||||
.setTitle(getString(R.string.cmd_history_menu))
|
||||
.setItems(options) { _, which ->
|
||||
when (which) {
|
||||
0 -> applyCommand(command.text)
|
||||
1 -> if (command.pinned) unpinCommand(command.text) else pinCommand(command.text)
|
||||
2 -> showRenameDialog(command)
|
||||
3 -> copyToClipboard(command.text)
|
||||
4 -> deleteCommand(command.text)
|
||||
}
|
||||
}
|
||||
.show()
|
||||
}
|
||||
|
||||
private fun showRenameDialog(command: Command) {
|
||||
val input = EditText(requireContext()).apply {
|
||||
setText(command.name)
|
||||
}
|
||||
|
||||
val container = LinearLayout(context).apply {
|
||||
orientation = LinearLayout.VERTICAL
|
||||
setPadding(50, 20, 50, 20)
|
||||
addView(input)
|
||||
}
|
||||
|
||||
AlertDialog.Builder(requireContext())
|
||||
.setTitle(getString(R.string.cmd_history_rename))
|
||||
.setView(container)
|
||||
.setPositiveButton(getString(android.R.string.ok)) { _, _ ->
|
||||
val newName = input.text.toString()
|
||||
cmdHistoryUtils.renameCommand(command.text, newName)
|
||||
updateHistoryItems()
|
||||
}
|
||||
.setNegativeButton(getString(android.R.string.cancel), null)
|
||||
.show()
|
||||
}
|
||||
|
||||
private fun applyCommand(command: String) {
|
||||
editTextPreference.text = command
|
||||
}
|
||||
|
||||
private fun pinCommand(command: String) {
|
||||
cmdHistoryUtils.pinCommand(command)
|
||||
updateHistoryItems()
|
||||
}
|
||||
|
||||
private fun unpinCommand(command: String) {
|
||||
cmdHistoryUtils.unpinCommand(command)
|
||||
updateHistoryItems()
|
||||
}
|
||||
|
||||
private fun deleteCommand(command: String) {
|
||||
cmdHistoryUtils.deleteCommand(command)
|
||||
updateHistoryItems()
|
||||
}
|
||||
|
||||
private fun copyToClipboard(command: String) {
|
||||
val clipboard = requireContext().getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
||||
val clip = ClipData.newPlainText("Command", command)
|
||||
clipboard.setPrimaryClip(clip)
|
||||
}
|
||||
}
|
||||
@@ -1,167 +0,0 @@
|
||||
package io.github.dovecoteescapee.byedpi.fragments
|
||||
|
||||
import android.content.Intent
|
||||
import android.content.SharedPreferences
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import androidx.appcompat.app.AppCompatDelegate
|
||||
import androidx.core.os.LocaleListCompat
|
||||
import androidx.preference.*
|
||||
import io.github.dovecoteescapee.byedpi.BuildConfig
|
||||
import io.github.dovecoteescapee.byedpi.R
|
||||
import io.github.dovecoteescapee.byedpi.activities.TestActivity
|
||||
import io.github.dovecoteescapee.byedpi.data.Mode
|
||||
import io.github.dovecoteescapee.byedpi.utility.*
|
||||
|
||||
class MainSettingsFragment : PreferenceFragmentCompat() {
|
||||
companion object {
|
||||
private val TAG: String = MainSettingsFragment::class.java.simpleName
|
||||
|
||||
fun setLang(lang: String) {
|
||||
val appLocale = localeByName(lang) ?: throw IllegalStateException("Invalid value for language: $lang")
|
||||
AppCompatDelegate.setApplicationLocales(appLocale)
|
||||
}
|
||||
|
||||
private fun localeByName(lang: String): LocaleListCompat? = when (lang) {
|
||||
"system" -> LocaleListCompat.getEmptyLocaleList()
|
||||
"ru" -> LocaleListCompat.forLanguageTags("ru")
|
||||
"en" -> LocaleListCompat.forLanguageTags("en")
|
||||
"tr" -> LocaleListCompat.forLanguageTags("tr")
|
||||
else -> {
|
||||
Log.w(TAG, "Invalid value for language: $lang")
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
fun setTheme(name: String) =
|
||||
themeByName(name)?.let {
|
||||
AppCompatDelegate.setDefaultNightMode(it)
|
||||
} ?: throw IllegalStateException("Invalid value for app_theme: $name")
|
||||
|
||||
private fun themeByName(name: String): Int? = when (name) {
|
||||
"system" -> AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM
|
||||
"light" -> AppCompatDelegate.MODE_NIGHT_NO
|
||||
"dark" -> AppCompatDelegate.MODE_NIGHT_YES
|
||||
else -> {
|
||||
Log.w(TAG, "Invalid value for app_theme: $name")
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val preferenceListener =
|
||||
SharedPreferences.OnSharedPreferenceChangeListener { _, _ ->
|
||||
updatePreferences()
|
||||
}
|
||||
|
||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||
setPreferencesFromResource(R.xml.main_settings, rootKey)
|
||||
|
||||
setEditTextPreferenceListener("byedpi_proxy_ip") { checkIp(it) }
|
||||
setEditTestPreferenceListenerPort("byedpi_proxy_port")
|
||||
|
||||
setEditTextPreferenceListener("dns_ip") {
|
||||
it.isBlank() || checkNotLocalIp(it)
|
||||
}
|
||||
|
||||
findPreferenceNotNull<ListPreference>("language")
|
||||
.setOnPreferenceChangeListener { _, newValue ->
|
||||
setLang(newValue as String)
|
||||
true
|
||||
}
|
||||
|
||||
findPreferenceNotNull<ListPreference>("app_theme")
|
||||
.setOnPreferenceChangeListener { _, newValue ->
|
||||
setTheme(newValue as String)
|
||||
true
|
||||
}
|
||||
|
||||
val switchCmdSettings = findPreferenceNotNull<SwitchPreference>("byedpi_enable_cmd_settings")
|
||||
val uiSettings = findPreferenceNotNull<Preference>("byedpi_ui_settings")
|
||||
val cmdSettings = findPreferenceNotNull<Preference>("byedpi_cmd_settings")
|
||||
val proxyTest = findPreferenceNotNull<Preference>("proxy_test")
|
||||
|
||||
val setByeDpiSettingsMode = { enable: Boolean ->
|
||||
uiSettings.isEnabled = !enable
|
||||
cmdSettings.isEnabled = enable
|
||||
proxyTest.isEnabled = enable
|
||||
}
|
||||
|
||||
setByeDpiSettingsMode(switchCmdSettings.isChecked)
|
||||
|
||||
switchCmdSettings.setOnPreferenceChangeListener { _, newValue ->
|
||||
setByeDpiSettingsMode(newValue as Boolean)
|
||||
updatePreferences()
|
||||
true
|
||||
}
|
||||
|
||||
findPreferenceNotNull<Preference>("proxy_test")
|
||||
.setOnPreferenceClickListener {
|
||||
val intent = Intent(context, TestActivity::class.java)
|
||||
startActivity(intent)
|
||||
true
|
||||
}
|
||||
|
||||
findPreferenceNotNull<Preference>("version").summary = BuildConfig.VERSION_NAME
|
||||
|
||||
updatePreferences()
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
sharedPreferences?.registerOnSharedPreferenceChangeListener(preferenceListener)
|
||||
updatePreferences()
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
sharedPreferences?.unregisterOnSharedPreferenceChangeListener(preferenceListener)
|
||||
}
|
||||
|
||||
private fun updatePreferences() {
|
||||
val mode = findPreferenceNotNull<ListPreference>("byedpi_mode").value.let { Mode.fromString(it) }
|
||||
val dns = findPreferenceNotNull<EditTextPreference>("dns_ip")
|
||||
val ipv6 = findPreferenceNotNull<SwitchPreference>("ipv6_enable")
|
||||
val proxy = findPreferenceNotNull<PreferenceCategory>("byedpi_proxy_category")
|
||||
|
||||
val applistType = findPreferenceNotNull<ListPreference>("applist_type")
|
||||
val selectedApps = findPreferenceNotNull<Preference>("selected_apps")
|
||||
|
||||
if (sharedPreferences?.getBoolean("byedpi_enable_cmd_settings", false) == true) {
|
||||
val (cmdIp, cmdPort) = sharedPreferences?.checkIpAndPortInCmd() ?: Pair(null, null)
|
||||
proxy.isVisible = cmdIp == null && cmdPort == null
|
||||
} else {
|
||||
proxy.isVisible = true
|
||||
}
|
||||
|
||||
when (mode) {
|
||||
Mode.VPN -> {
|
||||
dns.isVisible = true
|
||||
ipv6.isVisible = true
|
||||
|
||||
when (applistType.value) {
|
||||
"disable" -> {
|
||||
applistType.isVisible = true
|
||||
selectedApps.isVisible = false
|
||||
}
|
||||
"blacklist", "whitelist" -> {
|
||||
applistType.isVisible = true
|
||||
selectedApps.isVisible = true
|
||||
}
|
||||
else -> {
|
||||
applistType.isVisible = true
|
||||
selectedApps.isVisible = false
|
||||
Log.w(TAG, "Unexpected applistType value: ${applistType.value}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Mode.Proxy -> {
|
||||
dns.isVisible = false
|
||||
ipv6.isVisible = false
|
||||
applistType.isVisible = false
|
||||
selectedApps.isVisible = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
-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,76 +0,0 @@
|
||||
package io.github.dovecoteescapee.byedpi.services
|
||||
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.net.VpnService
|
||||
import android.os.Build
|
||||
import android.service.quicksettings.Tile
|
||||
import android.service.quicksettings.TileService
|
||||
import android.util.Log
|
||||
import androidx.annotation.RequiresApi
|
||||
import io.github.dovecoteescapee.byedpi.data.*
|
||||
import io.github.dovecoteescapee.byedpi.utility.getPreferences
|
||||
import io.github.dovecoteescapee.byedpi.utility.mode
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.N)
|
||||
class QuickTileService : TileService() {
|
||||
|
||||
companion object {
|
||||
private const val TAG = "QuickTileService"
|
||||
|
||||
fun updateTile(context: Context) {
|
||||
requestListeningState(context, ComponentName(context, QuickTileService::class.java))
|
||||
}
|
||||
}
|
||||
|
||||
override fun onStartListening() {
|
||||
super.onStartListening()
|
||||
updateStatus()
|
||||
}
|
||||
|
||||
override fun onClick() {
|
||||
if (qsTile.state == Tile.STATE_UNAVAILABLE) return
|
||||
|
||||
unlockAndRun { handleClick() }
|
||||
}
|
||||
|
||||
private fun handleClick() {
|
||||
val (status) = appStatus
|
||||
when (status) {
|
||||
AppStatus.Halted -> {
|
||||
val mode = getPreferences().mode()
|
||||
|
||||
if (mode == Mode.VPN && VpnService.prepare(this) != null) {
|
||||
return
|
||||
}
|
||||
|
||||
ServiceManager.start(this, mode)
|
||||
setState(Tile.STATE_ACTIVE)
|
||||
}
|
||||
AppStatus.Running -> {
|
||||
ServiceManager.stop(this)
|
||||
setState(Tile.STATE_INACTIVE)
|
||||
}
|
||||
}
|
||||
|
||||
Log.i(TAG, "Toggle tile")
|
||||
updateTile(this)
|
||||
}
|
||||
|
||||
private fun updateStatus() {
|
||||
val (status) = appStatus
|
||||
|
||||
if (status == AppStatus.Running) {
|
||||
setState(Tile.STATE_ACTIVE)
|
||||
} else {
|
||||
setState(Tile.STATE_INACTIVE)
|
||||
}
|
||||
}
|
||||
|
||||
private fun setState(newState: Int) {
|
||||
qsTile.apply {
|
||||
state = newState
|
||||
updateTile()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,116 +0,0 @@
|
||||
package io.github.dovecoteescapee.byedpi.utility
|
||||
|
||||
import android.util.Log
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.io.BufferedReader
|
||||
import java.io.InputStreamReader
|
||||
import java.net.HttpURLConnection
|
||||
import java.net.URL
|
||||
|
||||
class GoogleVideoUtils {
|
||||
|
||||
companion object {
|
||||
private val lettersListA = listOf(
|
||||
'u', 'z', 'p', 'k', 'f', 'a', '5', '0', 'v', 'q', 'l', 'g',
|
||||
'b', '6', '1', 'w', 'r', 'm', 'h', 'c', '7', '2', 'x', 's',
|
||||
'n', 'i', 'd', '8', '3', 'y', 't', 'o', 'j', 'e', '9', '4', '-'
|
||||
)
|
||||
private val lettersListB = listOf(
|
||||
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b',
|
||||
'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n',
|
||||
'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '-'
|
||||
)
|
||||
private val lettersMap = lettersListA.zip(lettersListB).toMap()
|
||||
}
|
||||
|
||||
suspend fun generateGoogleVideoDomain(): String? {
|
||||
val clusterCodename = getClusterCodename()
|
||||
if (clusterCodename == null) {
|
||||
Log.e("AutoGCS", "Failed to obtain cluster codename")
|
||||
return null
|
||||
}
|
||||
Log.i("AutoGCS", "Cluster codename: $clusterCodename")
|
||||
val clusterName = convertClusterCodename(clusterCodename)
|
||||
Log.i("AutoGCS", "Cluster name: $clusterName")
|
||||
val autoGCS = buildAutoGCS(clusterName)
|
||||
Log.i("AutoGCS", "Generated domain: $autoGCS")
|
||||
return autoGCS
|
||||
}
|
||||
|
||||
private suspend fun getClusterCodename(): String? {
|
||||
val urls = listOf(
|
||||
"https://redirector.gvt1.com/report_mapping?di=no",
|
||||
"https://redirector.googlevideo.com/report_mapping?di=no"
|
||||
)
|
||||
for (url in urls) {
|
||||
try {
|
||||
val responseBody = withContext(Dispatchers.IO) { httpGet(url) }
|
||||
if (responseBody != null) {
|
||||
val clusterCodename = extractClusterCodenameFromBody(responseBody)
|
||||
if (clusterCodename != null) {
|
||||
return clusterCodename
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e("ClusterCodename", "Error fetching cluster codename from $url", e)
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
private fun extractClusterCodenameFromBody(body: String): String? {
|
||||
val regex = Regex("""=>\s*(\S+)\s*(?:\(|:)""")
|
||||
val matchResult = regex.find(body)
|
||||
val codename = matchResult?.groupValues?.get(1)
|
||||
if (codename != null) {
|
||||
return codename.trimEnd(':', ' ')
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
private fun convertClusterCodename(clusterCodename: String): String {
|
||||
val clusterNameBuilder = StringBuilder()
|
||||
for (char in clusterCodename) {
|
||||
val mappedChar = lettersMap[char]
|
||||
if (mappedChar != null) {
|
||||
clusterNameBuilder.append(mappedChar)
|
||||
} else {
|
||||
Log.w("ClusterCodename", "Character '$char' not found in mapping")
|
||||
}
|
||||
}
|
||||
return clusterNameBuilder.toString()
|
||||
}
|
||||
|
||||
private fun buildAutoGCS(clusterName: String): String {
|
||||
return "rr1---sn-$clusterName.googlevideo.com"
|
||||
}
|
||||
|
||||
private fun httpGet(urlStr: String): String? {
|
||||
var connection: HttpURLConnection? = null
|
||||
return try {
|
||||
val url = URL(urlStr)
|
||||
connection = url.openConnection() as HttpURLConnection
|
||||
connection.requestMethod = "GET"
|
||||
connection.connectTimeout = 2000
|
||||
connection.readTimeout = 2000
|
||||
|
||||
val responseCode = connection.responseCode
|
||||
if (responseCode == HttpURLConnection.HTTP_OK) {
|
||||
val inputStream = connection.inputStream
|
||||
val reader = BufferedReader(InputStreamReader(inputStream))
|
||||
val response = reader.readText()
|
||||
reader.close()
|
||||
response
|
||||
} else {
|
||||
Log.e("HTTPGet", "Non-OK response code: $responseCode")
|
||||
null
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e("HTTPGet", "Exception during HTTP GET", e)
|
||||
null
|
||||
} finally {
|
||||
connection?.disconnect()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
package io.github.dovecoteescapee.byedpi.utility
|
||||
|
||||
import android.app.Notification
|
||||
import android.app.NotificationChannel
|
||||
import android.app.NotificationManager
|
||||
import android.app.PendingIntent
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.core.app.NotificationCompat
|
||||
import io.github.dovecoteescapee.byedpi.R
|
||||
import io.github.dovecoteescapee.byedpi.activities.MainActivity
|
||||
import io.github.dovecoteescapee.byedpi.data.STOP_ACTION
|
||||
|
||||
fun registerNotificationChannel(context: Context, id: String, @StringRes name: Int) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
val manager = context.getSystemService(NotificationManager::class.java) ?: return
|
||||
|
||||
val channel = NotificationChannel(
|
||||
id,
|
||||
context.getString(name),
|
||||
NotificationManager.IMPORTANCE_DEFAULT
|
||||
)
|
||||
channel.enableLights(false)
|
||||
channel.enableVibration(false)
|
||||
channel.setShowBadge(false)
|
||||
|
||||
manager.createNotificationChannel(channel)
|
||||
}
|
||||
}
|
||||
|
||||
fun createConnectionNotification(
|
||||
context: Context,
|
||||
channelId: String,
|
||||
@StringRes title: Int,
|
||||
@StringRes content: Int,
|
||||
service: Class<*>,
|
||||
): Notification =
|
||||
NotificationCompat.Builder(context, channelId)
|
||||
.setSmallIcon(R.drawable.ic_notification)
|
||||
.setSilent(true)
|
||||
.setContentTitle(context.getString(title))
|
||||
.setContentText(context.getString(content))
|
||||
.addAction(0, "Stop",
|
||||
PendingIntent.getService(
|
||||
context,
|
||||
0,
|
||||
Intent(context, service).setAction(STOP_ACTION),
|
||||
PendingIntent.FLAG_IMMUTABLE,
|
||||
)
|
||||
)
|
||||
.setContentIntent(
|
||||
PendingIntent.getActivity(
|
||||
context,
|
||||
0,
|
||||
Intent(context, MainActivity::class.java),
|
||||
PendingIntent.FLAG_IMMUTABLE,
|
||||
)
|
||||
)
|
||||
.build()
|
||||
@@ -0,0 +1,36 @@
|
||||
package io.github.romanvht.byedpi.activities
|
||||
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import com.google.android.material.appbar.MaterialToolbar
|
||||
import com.google.android.material.color.DynamicColors
|
||||
import io.github.romanvht.byedpi.R
|
||||
import io.github.romanvht.byedpi.utility.SettingsUtils
|
||||
import io.github.romanvht.byedpi.utility.getPreferences
|
||||
import io.github.romanvht.byedpi.utility.getStringNotNull
|
||||
|
||||
abstract class BaseActivity : AppCompatActivity() {
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
val prefs = getPreferences()
|
||||
|
||||
val lang = prefs.getStringNotNull("language", "system")
|
||||
SettingsUtils.setLang(lang)
|
||||
|
||||
val theme = prefs.getStringNotNull("app_theme", "system")
|
||||
SettingsUtils.setTheme(theme)
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
DynamicColors.applyToActivityIfAvailable(this)
|
||||
}
|
||||
|
||||
super.onCreate(savedInstanceState)
|
||||
}
|
||||
|
||||
protected fun setupToolbar() {
|
||||
val toolbar: MaterialToolbar = findViewById(R.id.toolbar)
|
||||
setSupportActionBar(toolbar)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,466 @@
|
||||
package io.github.romanvht.byedpi.activities
|
||||
|
||||
import android.Manifest
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.content.pm.PackageManager
|
||||
import android.net.VpnService
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ArrayAdapter
|
||||
import android.widget.ListView
|
||||
import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import io.github.romanvht.byedpi.R
|
||||
import io.github.romanvht.byedpi.data.*
|
||||
import io.github.romanvht.byedpi.databinding.ActivityMainBinding
|
||||
import io.github.romanvht.byedpi.services.ServiceManager
|
||||
import io.github.romanvht.byedpi.services.appStatus
|
||||
import io.github.romanvht.byedpi.utility.*
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import java.io.IOException
|
||||
import kotlin.system.exitProcess
|
||||
import androidx.core.content.edit
|
||||
|
||||
class MainActivity : BaseActivity() {
|
||||
private lateinit var binding: ActivityMainBinding
|
||||
private lateinit var historyUtils: HistoryUtils
|
||||
|
||||
companion object {
|
||||
private val TAG: String = MainActivity::class.java.simpleName
|
||||
private const val BATTERY_OPTIMIZATION_REQUESTED = "battery_optimization_requested"
|
||||
|
||||
private fun collectLogs(): String? =
|
||||
try {
|
||||
Runtime.getRuntime()
|
||||
.exec("logcat *:D -d")
|
||||
.inputStream.bufferedReader()
|
||||
.use { it.readText() }
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Failed to collect logs", e)
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private val vpnRegister =
|
||||
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
|
||||
if (it.resultCode == RESULT_OK) {
|
||||
ServiceManager.start(this, Mode.VPN)
|
||||
} else {
|
||||
Toast.makeText(this, R.string.vpn_permission_denied, Toast.LENGTH_SHORT).show()
|
||||
updateStatus()
|
||||
}
|
||||
}
|
||||
|
||||
private val logsRegister =
|
||||
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { log ->
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
val logs = collectLogs()
|
||||
|
||||
if (logs == null) {
|
||||
Toast.makeText(
|
||||
this@MainActivity,
|
||||
R.string.logs_failed,
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
} else {
|
||||
val uri = log.data?.data ?: run {
|
||||
Log.e(TAG, "No data in result")
|
||||
return@launch
|
||||
}
|
||||
contentResolver.openOutputStream(uri)?.use {
|
||||
try {
|
||||
it.write(logs.toByteArray())
|
||||
} catch (e: IOException) {
|
||||
Log.e(TAG, "Failed to save logs", e)
|
||||
}
|
||||
} ?: run {
|
||||
Log.e(TAG, "Failed to open output stream")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val receiver = object : BroadcastReceiver() {
|
||||
override fun onReceive(context: Context?, intent: Intent?) {
|
||||
Log.d(TAG, "Received intent: ${intent?.action}")
|
||||
|
||||
if (intent == null) {
|
||||
Log.w(TAG, "Received null intent")
|
||||
return
|
||||
}
|
||||
|
||||
val senderOrd = intent.getIntExtra(SENDER, -1)
|
||||
val sender = Sender.entries.getOrNull(senderOrd)
|
||||
if (sender == null) {
|
||||
Log.w(TAG, "Received intent with unknown sender: $senderOrd")
|
||||
return
|
||||
}
|
||||
|
||||
when (val action = intent.action) {
|
||||
STARTED_BROADCAST,
|
||||
STOPPED_BROADCAST -> updateStatus()
|
||||
|
||||
FAILED_BROADCAST -> {
|
||||
Toast.makeText(
|
||||
context,
|
||||
getString(R.string.failed_to_start, sender.name),
|
||||
Toast.LENGTH_SHORT,
|
||||
).show()
|
||||
updateStatus()
|
||||
}
|
||||
|
||||
else -> Log.w(TAG, "Unknown action: $action")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
binding = ActivityMainBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
setupToolbar()
|
||||
|
||||
historyUtils = HistoryUtils(this)
|
||||
|
||||
val intentFilter = IntentFilter().apply {
|
||||
addAction(STARTED_BROADCAST)
|
||||
addAction(STOPPED_BROADCAST)
|
||||
addAction(FAILED_BROADCAST)
|
||||
}
|
||||
|
||||
@SuppressLint("UnspecifiedRegisterReceiverFlag")
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
registerReceiver(receiver, intentFilter, RECEIVER_EXPORTED)
|
||||
} else {
|
||||
registerReceiver(receiver, intentFilter)
|
||||
}
|
||||
|
||||
binding.statusButtonCard.setOnClickListener {
|
||||
binding.statusButtonCard.isClickable = false
|
||||
|
||||
val (status, _) = appStatus
|
||||
when (status) {
|
||||
AppStatus.Halted -> start()
|
||||
AppStatus.Running -> stop()
|
||||
}
|
||||
|
||||
binding.statusButtonCard.postDelayed({
|
||||
binding.statusButtonCard.isClickable = true
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
binding.statusButtonCard.setOnFocusChangeListener { _, hasFocus ->
|
||||
if (hasFocus) {
|
||||
binding.statusButtonCard.strokeWidth = 10
|
||||
binding.statusButtonCard.strokeColor = android.graphics.Color.argb(100, 0, 0, 0)
|
||||
} else {
|
||||
binding.statusButtonCard.strokeWidth = 0
|
||||
}
|
||||
}
|
||||
|
||||
binding.settingsButton.setOnClickListener {
|
||||
val (status, _) = appStatus
|
||||
|
||||
if (status == AppStatus.Halted) {
|
||||
val intent = Intent(this, SettingsActivity::class.java)
|
||||
startActivity(intent)
|
||||
} else {
|
||||
Toast.makeText(this, R.string.settings_unavailable, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
|
||||
binding.editorButton.setOnClickListener {
|
||||
val useCmdSettings = getPreferences().getCmdEnable()
|
||||
|
||||
if (!useCmdSettings && appStatus.first == AppStatus.Running) {
|
||||
Toast.makeText(this, R.string.settings_unavailable, Toast.LENGTH_SHORT).show()
|
||||
return@setOnClickListener
|
||||
}
|
||||
|
||||
val intent = Intent(this, SettingsActivity::class.java)
|
||||
intent.putExtra("open_fragment", if (useCmdSettings) "cmd" else "ui")
|
||||
startActivity(intent)
|
||||
}
|
||||
|
||||
binding.testProxyButton.setOnClickListener {
|
||||
startActivity(Intent(this, TestActivity::class.java))
|
||||
}
|
||||
|
||||
binding.domainListsButton.setOnClickListener {
|
||||
val intent = Intent(this, TestSettingsActivity::class.java)
|
||||
intent.putExtra("open_fragment", "domain_lists")
|
||||
startActivity(intent)
|
||||
}
|
||||
|
||||
binding.strategyButton.setOnClickListener {
|
||||
showStrategyPicker()
|
||||
}
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU &&
|
||||
ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) {
|
||||
requestPermissions(arrayOf(Manifest.permission.POST_NOTIFICATIONS), 1)
|
||||
} else {
|
||||
requestBatteryOptimization()
|
||||
}
|
||||
|
||||
if (getPreferences().getBoolean("auto_connect", false) && appStatus.first != AppStatus.Running) {
|
||||
this.start()
|
||||
}
|
||||
|
||||
ShortcutUtils.update(this)
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
updateStatus()
|
||||
updateStrategyButton()
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
unregisterReceiver(receiver)
|
||||
}
|
||||
|
||||
override fun onRequestPermissionsResult(
|
||||
requestCode: Int,
|
||||
permissions: Array<out String>,
|
||||
grantResults: IntArray
|
||||
) {
|
||||
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
|
||||
|
||||
if (requestCode == 1) {
|
||||
requestBatteryOptimization()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
|
||||
menuInflater.inflate(R.menu.menu_main, menu)
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
val (status, _) = appStatus
|
||||
|
||||
return when (item.itemId) {
|
||||
R.id.action_save_logs -> {
|
||||
val intent =
|
||||
Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
|
||||
addCategory(Intent.CATEGORY_OPENABLE)
|
||||
type = "text/plain"
|
||||
putExtra(Intent.EXTRA_TITLE, "byedpi.log")
|
||||
}
|
||||
|
||||
logsRegister.launch(intent)
|
||||
true
|
||||
}
|
||||
|
||||
R.id.action_close_app -> {
|
||||
if (status == AppStatus.Running) stop()
|
||||
finishAffinity()
|
||||
android.os.Process.killProcess(android.os.Process.myPid())
|
||||
exitProcess(0)
|
||||
}
|
||||
|
||||
else -> super.onOptionsItemSelected(item)
|
||||
}
|
||||
}
|
||||
|
||||
private fun start() {
|
||||
when (getPreferences().mode()) {
|
||||
Mode.VPN -> {
|
||||
val intentPrepare = VpnService.prepare(this)
|
||||
if (intentPrepare != null) {
|
||||
vpnRegister.launch(intentPrepare)
|
||||
} else {
|
||||
ServiceManager.start(this, Mode.VPN)
|
||||
}
|
||||
}
|
||||
|
||||
Mode.Proxy -> ServiceManager.start(this, Mode.Proxy)
|
||||
}
|
||||
}
|
||||
|
||||
private fun stop() {
|
||||
ServiceManager.stop(this)
|
||||
}
|
||||
|
||||
private fun updateStatus() {
|
||||
val (status, mode) = appStatus
|
||||
|
||||
Log.i(TAG, "Updating status: $status, $mode")
|
||||
|
||||
val preferences = getPreferences()
|
||||
val (ip, port) = preferences.getProxyIpAndPort()
|
||||
|
||||
binding.proxyAddress.text = getString(R.string.proxy_address, ip, port)
|
||||
|
||||
when (status) {
|
||||
AppStatus.Halted -> {
|
||||
val typedValue = android.util.TypedValue()
|
||||
theme.resolveAttribute(android.R.attr.colorPrimary, typedValue,true)
|
||||
binding.statusButtonCard.setCardBackgroundColor(typedValue.data)
|
||||
binding.statusButtonIcon.clearColorFilter()
|
||||
|
||||
when (preferences.mode()) {
|
||||
Mode.VPN -> {
|
||||
binding.statusText.setText(R.string.vpn_disconnected)
|
||||
}
|
||||
|
||||
Mode.Proxy -> {
|
||||
binding.statusText.setText(R.string.proxy_down)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
AppStatus.Running -> {
|
||||
binding.statusButtonCard.setCardBackgroundColor(ContextCompat.getColor(this, R.color.green_active))
|
||||
binding.statusButtonIcon.setColorFilter(ContextCompat.getColor(this, android.R.color.white))
|
||||
|
||||
when (mode) {
|
||||
Mode.VPN -> {
|
||||
binding.statusText.setText(R.string.vpn_connected)
|
||||
}
|
||||
|
||||
Mode.Proxy -> {
|
||||
binding.statusText.setText(R.string.proxy_up)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateStrategyButton() {
|
||||
val useCmdSettings = getPreferences().getCmdEnable()
|
||||
|
||||
if (!useCmdSettings) {
|
||||
binding.cmdButtonsRow.visibility = View.GONE
|
||||
binding.strategyButton.visibility = View.GONE
|
||||
return
|
||||
}
|
||||
|
||||
binding.cmdButtonsRow.visibility = View.VISIBLE
|
||||
|
||||
val pinned = historyUtils.getPinnedHistory()
|
||||
val currentCmdArgs = getPreferences().getCmdArgs()
|
||||
|
||||
val matched = pinned.find { it.text == currentCmdArgs }
|
||||
val name = matched?.name?.takeIf { it.isNotBlank() }
|
||||
|
||||
if (name != null) {
|
||||
binding.strategyButtonName.text = name
|
||||
binding.strategyButtonName.visibility = View.VISIBLE
|
||||
binding.strategyButtonText.maxLines = 1
|
||||
} else {
|
||||
binding.strategyButtonName.visibility = View.GONE
|
||||
binding.strategyButtonText.maxLines = 2
|
||||
}
|
||||
|
||||
binding.strategyButtonText.text = currentCmdArgs.ifBlank { getString(R.string.main_strategy_picker) }
|
||||
binding.strategyButton.visibility = View.VISIBLE
|
||||
}
|
||||
|
||||
private fun showStrategyPicker() {
|
||||
val pinned = historyUtils.getPinnedHistory()
|
||||
|
||||
if (pinned.isEmpty()) {
|
||||
Toast.makeText(this, R.string.main_strategy_no_pinned, Toast.LENGTH_SHORT).show()
|
||||
return
|
||||
}
|
||||
|
||||
val adapter = object : ArrayAdapter<Command>(
|
||||
this,
|
||||
R.layout.item_main_strategy,
|
||||
pinned
|
||||
) {
|
||||
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
|
||||
val view = convertView ?: layoutInflater.inflate(
|
||||
R.layout.item_main_strategy,
|
||||
parent,
|
||||
false
|
||||
)
|
||||
|
||||
val command = getItem(position)!!
|
||||
|
||||
val nameView = view.findViewById<TextView>(R.id.strategyName)
|
||||
val textView = view.findViewById<TextView>(R.id.strategyText)
|
||||
val dividerView = view.findViewById<View>(R.id.strategyDivider)
|
||||
|
||||
val name = command.name?.takeIf { it.isNotBlank() }
|
||||
|
||||
if (name != null) {
|
||||
nameView.visibility = View.VISIBLE
|
||||
nameView.text = name
|
||||
textView.maxLines = 2
|
||||
} else {
|
||||
nameView.visibility = View.GONE
|
||||
textView.maxLines = 3
|
||||
}
|
||||
|
||||
textView.text = command.text
|
||||
dividerView.visibility = if (position == count - 1) View.GONE else View.VISIBLE
|
||||
|
||||
return view
|
||||
}
|
||||
}
|
||||
|
||||
val listView = ListView(this).apply {
|
||||
divider = null
|
||||
setPadding(0, (10 * resources.displayMetrics.density).toInt(), 0, 0)
|
||||
this.adapter = adapter
|
||||
}
|
||||
|
||||
val dialog = AlertDialog.Builder(this)
|
||||
.setTitle(getString(R.string.main_strategy_picker))
|
||||
.setView(listView)
|
||||
.setNegativeButton(getString(android.R.string.cancel), null)
|
||||
.create()
|
||||
|
||||
listView.setOnItemClickListener { _, _, position, _ ->
|
||||
applyStrategy(pinned[position].text)
|
||||
dialog.dismiss()
|
||||
}
|
||||
|
||||
dialog.show()
|
||||
}
|
||||
|
||||
private fun applyStrategy(commandText: String) {
|
||||
getPreferences().edit { putString("byedpi_cmd_args", commandText) }
|
||||
|
||||
updateStrategyButton()
|
||||
|
||||
if (appStatus.first == AppStatus.Running) {
|
||||
val mode = getPreferences().mode()
|
||||
if (mode == Mode.VPN && VpnService.prepare(this) != null) {
|
||||
return
|
||||
}
|
||||
|
||||
ServiceManager.restart(this, mode)
|
||||
Toast.makeText(this, R.string.service_restart, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
|
||||
private fun requestBatteryOptimization() {
|
||||
val preferences = getPreferences()
|
||||
val alreadyRequested = preferences.getBoolean(BATTERY_OPTIMIZATION_REQUESTED, false)
|
||||
|
||||
if (!alreadyRequested && !BatteryUtils.isOptimizationDisabled(this)) {
|
||||
BatteryUtils.requestBatteryOptimization(this)
|
||||
preferences.edit { putBoolean(BATTERY_OPTIMIZATION_REQUESTED, true) }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,178 @@
|
||||
package io.github.romanvht.byedpi.activities
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import io.github.romanvht.byedpi.R
|
||||
import io.github.romanvht.byedpi.data.AppSettings
|
||||
import io.github.romanvht.byedpi.fragments.ByeDpiCMDSettingsFragment
|
||||
import io.github.romanvht.byedpi.fragments.ByeDpiUISettingsFragment
|
||||
import io.github.romanvht.byedpi.fragments.MainSettingsFragment
|
||||
import io.github.romanvht.byedpi.utility.SettingsUtils
|
||||
import io.github.romanvht.byedpi.utility.SettingsUtils.Section
|
||||
|
||||
class SettingsActivity : BaseActivity() {
|
||||
private var exportSections = Section.entries.toSet()
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.activity_settings)
|
||||
setupToolbar()
|
||||
|
||||
if (savedInstanceState == null) {
|
||||
val openFragment = intent.getStringExtra("open_fragment")
|
||||
|
||||
when (openFragment) {
|
||||
"cmd" -> {
|
||||
supportFragmentManager
|
||||
.beginTransaction()
|
||||
.replace(R.id.settings, ByeDpiCMDSettingsFragment())
|
||||
.commit()
|
||||
}
|
||||
"ui" -> {
|
||||
supportFragmentManager
|
||||
.beginTransaction()
|
||||
.replace(R.id.settings, ByeDpiUISettingsFragment())
|
||||
.commit()
|
||||
}
|
||||
else -> {
|
||||
supportFragmentManager
|
||||
.beginTransaction()
|
||||
.replace(R.id.settings, MainSettingsFragment())
|
||||
.commit()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
|
||||
menuInflater.inflate(R.menu.menu_settings, menu)
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean = when (item.itemId) {
|
||||
android.R.id.home -> {
|
||||
onBackPressedDispatcher.onBackPressed()
|
||||
true
|
||||
}
|
||||
|
||||
R.id.action_reset_settings -> {
|
||||
SettingsUtils.resetSettings(this) {
|
||||
recreate()
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
R.id.action_export_settings -> {
|
||||
showSectionSelectionDialog(
|
||||
title = R.string.export_settings,
|
||||
availableSections = Section.entries.toSet()
|
||||
) { sections ->
|
||||
exportSections = sections
|
||||
val fileName = "bbd_${System.currentTimeMillis().toReadableDateTime()}.json"
|
||||
exportSettingsLauncher.launch(fileName)
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
R.id.action_import_settings -> {
|
||||
importSettingsLauncher.launch(arrayOf("application/json"))
|
||||
true
|
||||
}
|
||||
|
||||
else -> super.onOptionsItemSelected(item)
|
||||
}
|
||||
|
||||
private val exportSettingsLauncher = registerForActivityResult(
|
||||
ActivityResultContracts.CreateDocument("application/json")
|
||||
) { uri ->
|
||||
uri?.let {
|
||||
SettingsUtils.exportSettings(this, it, exportSections)
|
||||
}
|
||||
}
|
||||
|
||||
private val importSettingsLauncher = registerForActivityResult(
|
||||
ActivityResultContracts.OpenDocument()
|
||||
) { uri ->
|
||||
uri?.let {
|
||||
val settings = SettingsUtils.readSettings(this, it) ?: return@let
|
||||
val availableSections = SettingsUtils.getAvailableSections(settings)
|
||||
|
||||
if (availableSections.isEmpty()) {
|
||||
return@let
|
||||
}
|
||||
|
||||
showImportDialog(settings, availableSections)
|
||||
}
|
||||
}
|
||||
|
||||
private fun showImportDialog(settings: AppSettings, availableSections: Set<Section>) {
|
||||
showSectionSelectionDialog(
|
||||
title = R.string.import_settings,
|
||||
availableSections = availableSections
|
||||
) { sections ->
|
||||
SettingsUtils.importSettings(this, settings, sections) {
|
||||
recreate()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun showSectionSelectionDialog(
|
||||
@StringRes title: Int,
|
||||
availableSections: Set<Section>,
|
||||
onConfirmed: (Set<Section>) -> Unit
|
||||
) {
|
||||
val sections = Section.entries.filter { it in availableSections }
|
||||
val checkedItems = BooleanArray(sections.size) { true }
|
||||
val labels = sections.map { getString(it.label) }.toTypedArray()
|
||||
|
||||
lateinit var dialog: AlertDialog
|
||||
dialog = AlertDialog.Builder(this)
|
||||
.setTitle(title)
|
||||
.setMultiChoiceItems(labels, checkedItems) { _, which, isChecked ->
|
||||
checkedItems[which] = isChecked
|
||||
dialog.getButton(AlertDialog.BUTTON_POSITIVE).isEnabled = checkedItems.any { it }
|
||||
}
|
||||
.setPositiveButton(android.R.string.ok, null)
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.create()
|
||||
|
||||
dialog.setOnShowListener {
|
||||
val positiveButton = dialog.getButton(AlertDialog.BUTTON_POSITIVE)
|
||||
|
||||
positiveButton.setOnClickListener {
|
||||
val selected = sections
|
||||
.filterIndexed { index, _ -> checkedItems[index] }
|
||||
.toSet()
|
||||
|
||||
if (selected.isNotEmpty()) {
|
||||
dialog.dismiss()
|
||||
onConfirmed(selected)
|
||||
}
|
||||
}
|
||||
|
||||
positiveButton.isEnabled = checkedItems.any { it }
|
||||
}
|
||||
|
||||
dialog.show()
|
||||
}
|
||||
|
||||
@get:StringRes
|
||||
private val Section.label: Int
|
||||
get() = when (this) {
|
||||
Section.SETTINGS -> R.string.backup_settings
|
||||
Section.HISTORY -> R.string.backup_command_history
|
||||
Section.APPS -> R.string.backup_selected_apps
|
||||
Section.DOMAIN_LISTS -> R.string.backup_domain_lists
|
||||
}
|
||||
|
||||
private fun Long.toReadableDateTime(): String {
|
||||
val format = java.text.SimpleDateFormat("yyyyMMdd_HHmm", java.util.Locale.getDefault())
|
||||
return format.format(this)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,410 @@
|
||||
package io.github.romanvht.byedpi.activities
|
||||
|
||||
import android.content.Intent
|
||||
import android.net.VpnService
|
||||
import android.os.Bundle
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.view.WindowManager
|
||||
import android.widget.Button
|
||||
import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
import androidx.activity.OnBackPressedCallback
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import io.github.romanvht.byedpi.R
|
||||
import io.github.romanvht.byedpi.adapters.StrategyResultAdapter
|
||||
import io.github.romanvht.byedpi.data.Mode
|
||||
import io.github.romanvht.byedpi.data.AppStatus
|
||||
import io.github.romanvht.byedpi.data.SiteResult
|
||||
import io.github.romanvht.byedpi.data.StrategyResult
|
||||
import io.github.romanvht.byedpi.services.appStatus
|
||||
import io.github.romanvht.byedpi.services.ServiceManager
|
||||
import io.github.romanvht.byedpi.utility.HistoryUtils
|
||||
import io.github.romanvht.byedpi.utility.getPreferences
|
||||
import io.github.romanvht.byedpi.utility.SiteCheckUtils
|
||||
import io.github.romanvht.byedpi.utility.getIntStringNotNull
|
||||
import io.github.romanvht.byedpi.utility.getLongStringNotNull
|
||||
import androidx.core.content.edit
|
||||
import io.github.romanvht.byedpi.utility.getStringNotNull
|
||||
import com.google.gson.Gson
|
||||
import com.google.gson.reflect.TypeToken
|
||||
import io.github.romanvht.byedpi.utility.DomainListUtils
|
||||
import io.github.romanvht.byedpi.utility.getCmdArgs
|
||||
import io.github.romanvht.byedpi.utility.mode
|
||||
import kotlinx.coroutines.*
|
||||
import java.io.File
|
||||
|
||||
class TestActivity : BaseActivity() {
|
||||
|
||||
private lateinit var strategiesRecyclerView: RecyclerView
|
||||
private lateinit var progressTextView: TextView
|
||||
private lateinit var disclaimerTextView: TextView
|
||||
private lateinit var startStopButton: Button
|
||||
private lateinit var strategyAdapter: StrategyResultAdapter
|
||||
|
||||
private lateinit var siteChecker: SiteCheckUtils
|
||||
private lateinit var cmdHistoryUtils: HistoryUtils
|
||||
private lateinit var sites: List<String>
|
||||
private lateinit var cmds: List<String>
|
||||
|
||||
private var savedCmd: String = ""
|
||||
private var testJob: Job? = null
|
||||
private val strategies = mutableListOf<StrategyResult>()
|
||||
private val gson = Gson()
|
||||
|
||||
private var isTesting: Boolean
|
||||
get() = prefs.getBoolean("is_test_running", false)
|
||||
set(value) {
|
||||
prefs.edit(commit = true) { putBoolean("is_test_running", value) }
|
||||
}
|
||||
|
||||
private val prefs by lazy { getPreferences() }
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.activity_proxy_test)
|
||||
setupToolbar()
|
||||
|
||||
val ip = prefs.getStringNotNull("byedpi_proxy_ip", "127.0.0.1")
|
||||
val port = prefs.getIntStringNotNull("byedpi_proxy_port", 1080)
|
||||
|
||||
siteChecker = SiteCheckUtils(ip, port)
|
||||
cmdHistoryUtils = HistoryUtils(this)
|
||||
|
||||
strategiesRecyclerView = findViewById(R.id.strategiesRecyclerView)
|
||||
startStopButton = findViewById(R.id.startStopButton)
|
||||
progressTextView = findViewById(R.id.progressTextView)
|
||||
disclaimerTextView = findViewById(R.id.disclaimerTextView)
|
||||
|
||||
strategyAdapter = StrategyResultAdapter(this,
|
||||
onApply = { command ->
|
||||
addToHistory(command)
|
||||
}
|
||||
)
|
||||
|
||||
strategiesRecyclerView.layoutManager = LinearLayoutManager(this)
|
||||
strategiesRecyclerView.adapter = strategyAdapter
|
||||
|
||||
lifecycleScope.launch {
|
||||
val previousResults = loadResults()
|
||||
|
||||
if (previousResults.isNotEmpty()) {
|
||||
progressTextView.text = getString(R.string.test_complete)
|
||||
disclaimerTextView.visibility = View.GONE
|
||||
|
||||
strategies.clear()
|
||||
strategies.addAll(previousResults)
|
||||
|
||||
strategyAdapter.updateStrategies(strategies)
|
||||
}
|
||||
|
||||
if (isTesting) {
|
||||
progressTextView.text = getString(R.string.test_proxy_error)
|
||||
disclaimerTextView.text = getString(R.string.test_crash)
|
||||
disclaimerTextView.visibility = View.VISIBLE
|
||||
isTesting = false
|
||||
}
|
||||
}
|
||||
|
||||
startStopButton.setOnClickListener {
|
||||
startStopButton.isClickable = false
|
||||
|
||||
if (isTesting) {
|
||||
stopTesting()
|
||||
} else {
|
||||
startTesting()
|
||||
}
|
||||
|
||||
startStopButton.postDelayed({ startStopButton.isClickable = true }, 1000)
|
||||
}
|
||||
|
||||
onBackPressedDispatcher.addCallback(this, object : OnBackPressedCallback(true) {
|
||||
override fun handleOnBackPressed() {
|
||||
if (isTesting) {
|
||||
stopTesting()
|
||||
} else {
|
||||
if (appStatus.first == AppStatus.Running) {
|
||||
val intent = Intent(this@TestActivity, MainActivity::class.java)
|
||||
intent.flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP
|
||||
startActivity(intent)
|
||||
}
|
||||
}
|
||||
|
||||
finish()
|
||||
}
|
||||
})
|
||||
|
||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
|
||||
menuInflater.inflate(R.menu.menu_test, menu)
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
return when (item.itemId) {
|
||||
R.id.action_copy_log -> {
|
||||
copyLog()
|
||||
true
|
||||
}
|
||||
R.id.action_settings -> {
|
||||
if (!isTesting) {
|
||||
val intent = Intent(this, TestSettingsActivity::class.java)
|
||||
startActivity(intent)
|
||||
} else {
|
||||
Toast.makeText(this, R.string.settings_unavailable, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
true
|
||||
}
|
||||
android.R.id.home -> {
|
||||
onBackPressedDispatcher.onBackPressed()
|
||||
true
|
||||
}
|
||||
else -> super.onOptionsItemSelected(item)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun waitForProxyStatus(statusNeeded: AppStatus): Boolean {
|
||||
val startTime = System.currentTimeMillis()
|
||||
while (System.currentTimeMillis() - startTime < 3000) {
|
||||
if (appStatus.first == statusNeeded) {
|
||||
delay(500)
|
||||
return true
|
||||
}
|
||||
delay(100)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
private suspend fun isProxyRunning(): Boolean = withContext(Dispatchers.IO) {
|
||||
appStatus.first == AppStatus.Running
|
||||
}
|
||||
|
||||
private fun updateCmdArgs(cmd: String) {
|
||||
prefs.edit(commit = true) { putString("byedpi_cmd_args", cmd) }
|
||||
}
|
||||
|
||||
private fun startTesting() {
|
||||
sites = loadSites()
|
||||
cmds = loadCmds()
|
||||
|
||||
if (sites.isEmpty()) {
|
||||
Toast.makeText(this, R.string.test_settings_domain_empty, Toast.LENGTH_LONG).show()
|
||||
return
|
||||
}
|
||||
|
||||
testJob = lifecycleScope.launch(Dispatchers.IO) {
|
||||
isTesting = true
|
||||
savedCmd = prefs.getCmdArgs()
|
||||
|
||||
strategies.clear()
|
||||
strategies.addAll(cmds.map { StrategyResult(command = it) })
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
disclaimerTextView.visibility = View.GONE
|
||||
|
||||
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
|
||||
startStopButton.text = getString(R.string.test_stop)
|
||||
progressTextView.text = ""
|
||||
|
||||
strategyAdapter.setTestingState(true)
|
||||
strategyAdapter.updateStrategies(strategies, sortByPercentage = false)
|
||||
}
|
||||
|
||||
if (isProxyRunning()) {
|
||||
ServiceManager.stop(this@TestActivity)
|
||||
waitForProxyStatus(AppStatus.Halted)
|
||||
}
|
||||
|
||||
val delaySec = prefs.getIntStringNotNull("byedpi_proxytest_delay", 1)
|
||||
val requestsCount = prefs.getIntStringNotNull("byedpi_proxytest_requests", 1)
|
||||
val requestTimeout = prefs.getLongStringNotNull("byedpi_proxytest_timeout", 5)
|
||||
val requestLimit = prefs.getIntStringNotNull("byedpi_proxytest_limit", 20)
|
||||
|
||||
for (strategyIndex in strategies.indices) {
|
||||
if (!isActive) break
|
||||
|
||||
val strategy = strategies[strategyIndex]
|
||||
val cmdIndex = strategyIndex + 1
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
progressTextView.text = getString(R.string.test_process, cmdIndex, cmds.size)
|
||||
}
|
||||
|
||||
updateCmdArgs(strategy.command)
|
||||
|
||||
if (isProxyRunning()) stopTesting()
|
||||
else ServiceManager.start(this@TestActivity, Mode.Proxy)
|
||||
|
||||
if (!waitForProxyStatus(AppStatus.Running)) {
|
||||
stopTesting()
|
||||
}
|
||||
|
||||
delay(delaySec * 500L)
|
||||
|
||||
val totalRequests = sites.size * requestsCount
|
||||
strategy.totalRequests = totalRequests
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
strategyAdapter.notifyItemChanged(strategyIndex)
|
||||
}
|
||||
|
||||
siteChecker.checkSitesAsync(
|
||||
sites = sites,
|
||||
requestsCount = requestsCount,
|
||||
requestTimeout = requestTimeout,
|
||||
concurrentRequests = requestLimit,
|
||||
fullLog = true,
|
||||
onSiteChecked = { site, successCount, countRequests ->
|
||||
lifecycleScope.launch(Dispatchers.Main) {
|
||||
strategy.currentProgress += countRequests
|
||||
strategy.successCount += successCount
|
||||
strategy.siteResults.add(SiteResult(site, successCount, countRequests))
|
||||
|
||||
strategyAdapter.notifyItemChanged(strategyIndex, "progress")
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
strategy.isCompleted = true
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
strategyAdapter.updateStrategies(strategies, sortByPercentage = true)
|
||||
saveResults(strategies)
|
||||
}
|
||||
|
||||
if (isProxyRunning()) ServiceManager.stop(this@TestActivity)
|
||||
else stopTesting()
|
||||
|
||||
if (!waitForProxyStatus(AppStatus.Halted)) {
|
||||
stopTesting()
|
||||
}
|
||||
|
||||
delay(delaySec * 500L)
|
||||
}
|
||||
|
||||
stopTesting()
|
||||
}
|
||||
}
|
||||
|
||||
private fun stopTesting() {
|
||||
if (!isTesting) {
|
||||
return
|
||||
}
|
||||
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
isTesting = false
|
||||
updateCmdArgs(savedCmd)
|
||||
|
||||
testJob?.cancel()
|
||||
testJob = null
|
||||
|
||||
if (isProxyRunning()) {
|
||||
ServiceManager.stop(this@TestActivity)
|
||||
}
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
|
||||
startStopButton.text = getString(R.string.test_start)
|
||||
progressTextView.text = getString(R.string.test_complete)
|
||||
|
||||
strategyAdapter.setTestingState(false)
|
||||
strategyAdapter.updateStrategies(strategies, sortByPercentage = true)
|
||||
|
||||
saveResults(strategies)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun addToHistory(command: String) {
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
updateCmdArgs(command)
|
||||
cmdHistoryUtils.addCommand(command)
|
||||
|
||||
val mode = prefs.mode()
|
||||
if (mode == Mode.VPN && VpnService.prepare(this@TestActivity) != null) return@launch
|
||||
|
||||
val toastText = if (appStatus.first == AppStatus.Running) {
|
||||
ServiceManager.restart(this@TestActivity, mode)
|
||||
R.string.service_restart
|
||||
} else {
|
||||
R.string.cmd_history_applied
|
||||
}
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
Toast.makeText(this@TestActivity, toastText, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun saveResults(results: List<StrategyResult>) {
|
||||
val file = File(filesDir, "proxy_test_results.json")
|
||||
val json = gson.toJson(results)
|
||||
file.writeText(json)
|
||||
}
|
||||
|
||||
private fun loadResults(): List<StrategyResult> {
|
||||
val file = File(filesDir, "proxy_test_results.json")
|
||||
return if (file.exists()) {
|
||||
try {
|
||||
val json = file.readText()
|
||||
val type = object : TypeToken<List<StrategyResult>>() {}.type
|
||||
gson.fromJson<List<StrategyResult>>(json, type) ?: emptyList()
|
||||
} catch (e: Exception) {
|
||||
emptyList()
|
||||
}
|
||||
} else {
|
||||
emptyList()
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadSites(): List<String> {
|
||||
DomainListUtils.syncLists(this)
|
||||
return DomainListUtils.getActiveDomains(this)
|
||||
}
|
||||
|
||||
private fun loadCmds(): List<String> {
|
||||
val userCommands = prefs.getBoolean("byedpi_proxytest_usercommands", false)
|
||||
val sniValue = prefs.getStringNotNull("byedpi_proxytest_sni", "google.com")
|
||||
|
||||
return if (userCommands) {
|
||||
val content = prefs.getStringNotNull("byedpi_proxytest_commands", "")
|
||||
content.replace("{sni}", "\"${sniValue}\"").lines().map { it.trim() }.filter { it.isNotEmpty() }
|
||||
} else {
|
||||
val content = assets.open("proxytest_strategies.list").bufferedReader().readText()
|
||||
content.replace("{sni}", "\"${sniValue}\"").lines().map { it.trim() }.filter { it.isNotEmpty() }
|
||||
}
|
||||
}
|
||||
|
||||
private fun copyLog() {
|
||||
val completeStrategies = strategies.filter { it.isCompleted }
|
||||
|
||||
if (completeStrategies.isEmpty()) {
|
||||
Toast.makeText(this, R.string.toast_copied, Toast.LENGTH_SHORT).show()
|
||||
return
|
||||
}
|
||||
|
||||
val sb = StringBuilder()
|
||||
|
||||
completeStrategies.forEach { strategy ->
|
||||
sb.appendLine("${strategy.command}\n")
|
||||
|
||||
strategy.siteResults.forEach { site ->
|
||||
sb.appendLine("${site.site} - ${site.successCount}/${site.totalCount}")
|
||||
}
|
||||
|
||||
sb.appendLine("\n${strategy.successCount}/${strategy.totalRequests}")
|
||||
sb.appendLine("-------------")
|
||||
}
|
||||
|
||||
val clipboard = getSystemService(CLIPBOARD_SERVICE) as android.content.ClipboardManager
|
||||
val clip = android.content.ClipData.newPlainText("proxy_test_log", sb.toString())
|
||||
clipboard.setPrimaryClip(clip)
|
||||
|
||||
Toast.makeText(this, R.string.toast_copied, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package io.github.romanvht.byedpi.activities
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.MenuItem
|
||||
import io.github.romanvht.byedpi.R
|
||||
import io.github.romanvht.byedpi.fragments.DomainListsFragment
|
||||
import io.github.romanvht.byedpi.fragments.ProxyTestSettingsFragment
|
||||
|
||||
class TestSettingsActivity : BaseActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.activity_test_settings)
|
||||
setupToolbar()
|
||||
|
||||
if (savedInstanceState == null) {
|
||||
val openFragment = intent.getStringExtra("open_fragment")
|
||||
|
||||
when (openFragment) {
|
||||
"domain_lists" -> {
|
||||
supportFragmentManager
|
||||
.beginTransaction()
|
||||
.replace(R.id.test_settings, DomainListsFragment())
|
||||
.commit()
|
||||
}
|
||||
else -> {
|
||||
supportFragmentManager
|
||||
.beginTransaction()
|
||||
.replace(R.id.test_settings, ProxyTestSettingsFragment())
|
||||
.commit()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean = when (item.itemId) {
|
||||
android.R.id.home -> {
|
||||
onBackPressedDispatcher.onBackPressed()
|
||||
true
|
||||
}
|
||||
else -> super.onOptionsItemSelected(item)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
package io.github.romanvht.byedpi.activities
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.SharedPreferences
|
||||
import android.net.VpnService
|
||||
import android.os.Bundle
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.util.Log
|
||||
import androidx.core.content.edit
|
||||
import io.github.romanvht.byedpi.data.AppStatus
|
||||
import io.github.romanvht.byedpi.data.Mode
|
||||
import io.github.romanvht.byedpi.services.ServiceManager
|
||||
import io.github.romanvht.byedpi.services.appStatus
|
||||
import io.github.romanvht.byedpi.utility.getCmdArgs
|
||||
import io.github.romanvht.byedpi.utility.getPreferences
|
||||
import io.github.romanvht.byedpi.utility.mode
|
||||
|
||||
class ToggleActivity : Activity() {
|
||||
|
||||
companion object {
|
||||
private const val TAG = "ToggleServiceActivity"
|
||||
}
|
||||
|
||||
private lateinit var prefs: SharedPreferences
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
prefs = getPreferences()
|
||||
val strategy = intent.getStringExtra("strategy")
|
||||
val updated = updateStrategy(strategy)
|
||||
|
||||
val onlyUpdate = intent.getBooleanExtra("only_update", false)
|
||||
val onlyStart = intent.getBooleanExtra("only_start", false)
|
||||
val onlyStop = intent.getBooleanExtra("only_stop", false)
|
||||
|
||||
when {
|
||||
onlyUpdate -> {
|
||||
Log.i(TAG, "Only update strategy")
|
||||
}
|
||||
onlyStart -> {
|
||||
val (status) = appStatus
|
||||
if (status == AppStatus.Halted) {
|
||||
startService()
|
||||
} else {
|
||||
Log.i(TAG, "Service already running")
|
||||
}
|
||||
}
|
||||
onlyStop -> {
|
||||
val (status) = appStatus
|
||||
if (status == AppStatus.Running) {
|
||||
stopService()
|
||||
} else {
|
||||
Log.i(TAG, "Service already stopped")
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
toggleService(updated)
|
||||
}
|
||||
}
|
||||
|
||||
finish()
|
||||
}
|
||||
|
||||
private fun startService() {
|
||||
val mode = prefs.mode()
|
||||
|
||||
if (mode == Mode.VPN && VpnService.prepare(this) != null) {
|
||||
return
|
||||
}
|
||||
|
||||
ServiceManager.start(this, mode)
|
||||
Log.i(TAG, "Toggle service start")
|
||||
}
|
||||
|
||||
private fun restartService() {
|
||||
val mode = prefs.mode()
|
||||
|
||||
if (mode == Mode.VPN && VpnService.prepare(this) != null) {
|
||||
return
|
||||
}
|
||||
|
||||
ServiceManager.restart(this, mode)
|
||||
Log.i(TAG, "Toggle service start")
|
||||
}
|
||||
|
||||
private fun stopService() {
|
||||
ServiceManager.stop(this)
|
||||
Log.i(TAG, "Toggle service stop")
|
||||
}
|
||||
|
||||
private fun toggleService(restart: Boolean) {
|
||||
val (status) = appStatus
|
||||
when (status) {
|
||||
AppStatus.Halted -> {
|
||||
startService()
|
||||
}
|
||||
AppStatus.Running -> {
|
||||
if (restart) {
|
||||
restartService()
|
||||
} else {
|
||||
stopService()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateStrategy(strategy: String?): Boolean {
|
||||
val current = prefs.getCmdArgs()
|
||||
if (strategy != null && strategy != current) {
|
||||
prefs.edit(commit = true) { putString("byedpi_cmd_args", strategy) }
|
||||
Log.i(TAG, "Strategy updated to: $strategy")
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
package io.github.romanvht.byedpi.adapters
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.CheckBox
|
||||
import android.widget.Filter
|
||||
import android.widget.Filterable
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import androidx.core.content.edit
|
||||
import androidx.preference.PreferenceManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import io.github.romanvht.byedpi.R
|
||||
import io.github.romanvht.byedpi.data.AppInfo
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
class AppSelectionAdapter(
|
||||
context: Context,
|
||||
allApps: List<AppInfo>
|
||||
) : RecyclerView.Adapter<AppSelectionAdapter.ViewHolder>(), Filterable {
|
||||
|
||||
private val context = context.applicationContext
|
||||
private val pm = context.packageManager
|
||||
private val originalApps: List<AppInfo> = allApps
|
||||
private val filteredApps: MutableList<AppInfo> = allApps.toMutableList()
|
||||
private val adapterScope = CoroutineScope(Dispatchers.Main + Job())
|
||||
|
||||
class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {
|
||||
val appIcon: ImageView = view.findViewById(R.id.appIcon)
|
||||
val appName: TextView = view.findViewById(R.id.appName)
|
||||
val appPackageName: TextView = view.findViewById(R.id.appPackageName)
|
||||
val appCheckBox: CheckBox = view.findViewById(R.id.appCheckBox)
|
||||
var iconLoadJob: Job? = null
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
||||
val view = LayoutInflater.from(parent.context).inflate(R.layout.app_selection_item, parent, false)
|
||||
val holder = ViewHolder(view)
|
||||
|
||||
holder.itemView.setOnClickListener {
|
||||
val position = holder.bindingAdapterPosition
|
||||
if (position != RecyclerView.NO_POSITION) {
|
||||
val app = filteredApps[position]
|
||||
app.isSelected = !app.isSelected
|
||||
notifyItemChanged(position)
|
||||
updateSelectedApps()
|
||||
}
|
||||
}
|
||||
|
||||
return holder
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
||||
val app = filteredApps[position]
|
||||
|
||||
if (app.appName == app.packageName) {
|
||||
holder.appName.text = app.packageName
|
||||
holder.appPackageName.visibility = View.GONE
|
||||
} else {
|
||||
holder.appName.text = app.appName
|
||||
holder.appPackageName.text = app.packageName
|
||||
holder.appPackageName.visibility = View.VISIBLE
|
||||
}
|
||||
|
||||
holder.appCheckBox.isChecked = app.isSelected
|
||||
|
||||
holder.iconLoadJob?.cancel()
|
||||
|
||||
if (app.icon != null) {
|
||||
holder.appIcon.setImageDrawable(app.icon)
|
||||
} else {
|
||||
holder.appIcon.setImageDrawable(pm.defaultActivityIcon)
|
||||
|
||||
holder.iconLoadJob = adapterScope.launch {
|
||||
val icon = withContext(Dispatchers.IO) {
|
||||
try {
|
||||
pm.getApplicationIcon(app.packageName)
|
||||
} catch (_: Exception) {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
if (icon != null) {
|
||||
app.icon = icon
|
||||
holder.appIcon.setImageDrawable(icon)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int {
|
||||
return filteredApps.size
|
||||
}
|
||||
|
||||
override fun onViewRecycled(holder: ViewHolder) {
|
||||
super.onViewRecycled(holder)
|
||||
holder.iconLoadJob?.cancel()
|
||||
}
|
||||
|
||||
override fun getFilter(): Filter {
|
||||
return object : Filter() {
|
||||
override fun performFiltering(constraint: CharSequence?): FilterResults {
|
||||
val query = constraint?.toString()?.lowercase().orEmpty()
|
||||
var filteredList = originalApps
|
||||
|
||||
if (query.isNotEmpty()) {
|
||||
filteredList = originalApps.filter {
|
||||
it.appName.lowercase().contains(query) ||
|
||||
it.packageName.lowercase().contains(query)
|
||||
}
|
||||
}
|
||||
|
||||
return FilterResults().apply {
|
||||
values = filteredList
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("NotifyDataSetChanged")
|
||||
override fun publishResults(constraint: CharSequence, results: FilterResults) {
|
||||
val newList = (results.values as List<*>).filterIsInstance<AppInfo>()
|
||||
filteredApps.clear()
|
||||
filteredApps.addAll(newList)
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getAllSelectedPackages(): Set<String> {
|
||||
return originalApps.filter { it.isSelected }.map { it.packageName }.toSet()
|
||||
}
|
||||
|
||||
private fun updateSelectedApps() {
|
||||
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
val selectedApps = getAllSelectedPackages()
|
||||
prefs.edit { putStringSet("selected_apps", selectedApps) }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
package io.github.romanvht.byedpi.adapters
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.CheckBox
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.TextView
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.ListAdapter
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import io.github.romanvht.byedpi.R
|
||||
import io.github.romanvht.byedpi.data.DomainList
|
||||
|
||||
class DomainListAdapter(
|
||||
private val onStateChanged: (DomainList) -> Unit,
|
||||
private val onEdit: (DomainList) -> Unit,
|
||||
private val onDelete: (DomainList) -> Unit,
|
||||
private val onCopy: (DomainList) -> Unit
|
||||
) : ListAdapter<DomainList, DomainListAdapter.DomainListViewHolder>(DiffCallback()) {
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): DomainListViewHolder {
|
||||
val view = LayoutInflater.from(parent.context).inflate(R.layout.item_domain_list, parent, false)
|
||||
val holder = DomainListViewHolder(view)
|
||||
|
||||
holder.checkbox.setOnClickListener {
|
||||
val position = holder.bindingAdapterPosition
|
||||
if (position != RecyclerView.NO_POSITION) {
|
||||
onStateChanged(getItem(position))
|
||||
}
|
||||
}
|
||||
|
||||
holder.contentLayout.setOnClickListener {
|
||||
val position = holder.bindingAdapterPosition
|
||||
if (position != RecyclerView.NO_POSITION) {
|
||||
holder.showActionDialog(getItem(position), onEdit, onCopy, onDelete)
|
||||
}
|
||||
}
|
||||
|
||||
return holder
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: DomainListViewHolder, position: Int) {
|
||||
holder.bind(getItem(position))
|
||||
}
|
||||
|
||||
class DomainListViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
|
||||
private val nameText: TextView = itemView.findViewById(R.id.list_name)
|
||||
private val countText: TextView = itemView.findViewById(R.id.list_count)
|
||||
val checkbox: CheckBox = itemView.findViewById(R.id.list_checkbox)
|
||||
val contentLayout: LinearLayout = itemView.findViewById(R.id.list_content)
|
||||
|
||||
fun bind(domainList: DomainList) {
|
||||
nameText.text = domainList.name
|
||||
|
||||
val domains = domainList.domains.take(5).joinToString("\n")
|
||||
val summary = if (domainList.domains.size > 5) "$domains\n..." else domains
|
||||
countText.text = summary
|
||||
|
||||
checkbox.isChecked = domainList.isActive
|
||||
}
|
||||
|
||||
fun showActionDialog(
|
||||
domainList: DomainList,
|
||||
onEdit: (DomainList) -> Unit,
|
||||
onCopy: (DomainList) -> Unit,
|
||||
onDelete: (DomainList) -> Unit
|
||||
) {
|
||||
val context = itemView.context
|
||||
val options = arrayOf(
|
||||
context.getString(R.string.domain_list_edit),
|
||||
context.getString(R.string.cmd_history_copy),
|
||||
context.getString(R.string.domain_list_delete)
|
||||
)
|
||||
|
||||
AlertDialog.Builder(context, R.style.CustomAlertDialog)
|
||||
.setTitle(domainList.name)
|
||||
.setItems(options) { _, which ->
|
||||
when (which) {
|
||||
0 -> onEdit(domainList)
|
||||
1 -> onCopy(domainList)
|
||||
2 -> onDelete(domainList)
|
||||
}
|
||||
}
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.show()
|
||||
}
|
||||
}
|
||||
|
||||
private class DiffCallback : DiffUtil.ItemCallback<DomainList>() {
|
||||
override fun areItemsTheSame(oldItem: DomainList, newItem: DomainList): Boolean {
|
||||
return oldItem.id == newItem.id
|
||||
}
|
||||
|
||||
override fun areContentsTheSame(oldItem: DomainList, newItem: DomainList): Boolean {
|
||||
return oldItem == newItem
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package io.github.romanvht.byedpi.adapters
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.TextView
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import io.github.romanvht.byedpi.R
|
||||
import io.github.romanvht.byedpi.data.SiteResult
|
||||
|
||||
class SiteResultAdapter : RecyclerView.Adapter<SiteResultAdapter.SiteViewHolder>() {
|
||||
|
||||
private val sites = mutableListOf<SiteResult>()
|
||||
|
||||
class SiteViewHolder(view: View) : RecyclerView.ViewHolder(view) {
|
||||
val siteTextView: TextView = view.findViewById(R.id.siteTextView)
|
||||
val resultTextView: TextView = view.findViewById(R.id.siteResultTextView)
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SiteViewHolder {
|
||||
val view = LayoutInflater.from(parent.context).inflate(R.layout.item_site_result, parent, false)
|
||||
return SiteViewHolder(view)
|
||||
}
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
override fun onBindViewHolder(holder: SiteViewHolder, position: Int) {
|
||||
val site = sites[position]
|
||||
holder.siteTextView.text = site.site
|
||||
holder.resultTextView.text = "${site.successCount}/${site.totalCount}"
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int {
|
||||
return sites.size
|
||||
}
|
||||
|
||||
@SuppressLint("NotifyDataSetChanged")
|
||||
fun updateSites(newSites: List<SiteResult>) {
|
||||
sites.clear()
|
||||
sites.addAll(newSites)
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,186 @@
|
||||
package io.github.romanvht.byedpi.adapters
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ImageView
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.ProgressBar
|
||||
import android.widget.TextView
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import io.github.romanvht.byedpi.R
|
||||
import io.github.romanvht.byedpi.data.StrategyResult
|
||||
import androidx.core.view.isGone
|
||||
import androidx.core.view.isVisible
|
||||
import io.github.romanvht.byedpi.utility.ClipboardUtils
|
||||
|
||||
class StrategyResultAdapter(
|
||||
private val context: Context,
|
||||
private val onApply: (String) -> Unit
|
||||
) : RecyclerView.Adapter<StrategyResultAdapter.StrategyViewHolder>() {
|
||||
|
||||
private var isTesting = false
|
||||
private val strategies = mutableListOf<StrategyResult>()
|
||||
|
||||
class StrategyViewHolder(view: View) : RecyclerView.ViewHolder(view) {
|
||||
val commandTextView: TextView = view.findViewById(R.id.commandTextView)
|
||||
val progressLayout: LinearLayout = view.findViewById(R.id.progressLayout)
|
||||
val progressBar: ProgressBar = view.findViewById(R.id.progressBar)
|
||||
val progressTextView: TextView = view.findViewById(R.id.progressTextView)
|
||||
val expandButton: LinearLayout = view.findViewById(R.id.expandButton)
|
||||
val expandTextView: TextView = view.findViewById(R.id.expandTextView)
|
||||
val expandIcon: ImageView = view.findViewById(R.id.expandIcon)
|
||||
val sitesRecyclerView: RecyclerView = view.findViewById(R.id.sitesRecyclerView)
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): StrategyViewHolder {
|
||||
val view = LayoutInflater.from(parent.context).inflate(R.layout.item_strategy_result, parent, false)
|
||||
val holder = StrategyViewHolder(view)
|
||||
|
||||
holder.commandTextView.setOnClickListener {
|
||||
val position = holder.bindingAdapterPosition
|
||||
if (position != RecyclerView.NO_POSITION) {
|
||||
showCommandMenu(strategies[position].command)
|
||||
}
|
||||
}
|
||||
|
||||
holder.expandButton.setOnClickListener {
|
||||
val position = holder.bindingAdapterPosition
|
||||
if (position != RecyclerView.NO_POSITION) {
|
||||
strategies[position].isExpanded = !strategies[position].isExpanded
|
||||
notifyItemChanged(position)
|
||||
}
|
||||
}
|
||||
|
||||
return holder
|
||||
}
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
override fun onBindViewHolder(holder: StrategyViewHolder, position: Int) {
|
||||
val strategy = strategies[position]
|
||||
|
||||
holder.commandTextView.text = strategy.command
|
||||
|
||||
if (strategy.totalRequests > 0) {
|
||||
holder.progressLayout.visibility = View.VISIBLE
|
||||
holder.progressTextView.visibility = View.VISIBLE
|
||||
|
||||
val isProgress = isTesting && !strategy.isCompleted
|
||||
|
||||
if (isProgress) {
|
||||
holder.progressBar.isIndeterminate = true
|
||||
holder.progressTextView.text = "${strategy.successCount}/${strategy.totalRequests}"
|
||||
} else {
|
||||
holder.progressBar.isIndeterminate = false
|
||||
holder.progressBar.max = strategy.totalRequests
|
||||
holder.progressBar.progress = strategy.successCount
|
||||
holder.progressTextView.text = "${strategy.successCount}/${strategy.totalRequests}"
|
||||
}
|
||||
} else {
|
||||
holder.progressLayout.visibility = View.GONE
|
||||
}
|
||||
|
||||
if (strategy.siteResults.isNotEmpty()) {
|
||||
holder.expandButton.visibility = View.VISIBLE
|
||||
|
||||
if (strategy.isExpanded) {
|
||||
holder.expandTextView.text = context.getString(R.string.test_hide_details)
|
||||
holder.expandIcon.rotation = 180f
|
||||
holder.sitesRecyclerView.visibility = View.VISIBLE
|
||||
|
||||
val siteAdapter = SiteResultAdapter()
|
||||
holder.sitesRecyclerView.layoutManager = LinearLayoutManager(context)
|
||||
holder.sitesRecyclerView.adapter = siteAdapter
|
||||
siteAdapter.updateSites(strategy.siteResults)
|
||||
} else {
|
||||
holder.expandTextView.text = context.getString(R.string.test_show_details)
|
||||
holder.expandIcon.rotation = 0f
|
||||
holder.sitesRecyclerView.visibility = View.GONE
|
||||
}
|
||||
} else {
|
||||
holder.expandButton.visibility = View.GONE
|
||||
holder.sitesRecyclerView.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
override fun onBindViewHolder(holder: StrategyViewHolder, position: Int, payloads: MutableList<Any>) {
|
||||
if (payloads.isEmpty()) {
|
||||
super.onBindViewHolder(holder, position, payloads)
|
||||
} else {
|
||||
val strategy = strategies[position]
|
||||
|
||||
if (strategy.totalRequests > 0) {
|
||||
val isProgress = isTesting && !strategy.isCompleted
|
||||
|
||||
if (isProgress) {
|
||||
holder.progressBar.isIndeterminate = true
|
||||
holder.progressTextView.text = "${strategy.successCount}/${strategy.totalRequests}"
|
||||
} else {
|
||||
holder.progressBar.isIndeterminate = false
|
||||
holder.progressBar.max = strategy.totalRequests
|
||||
holder.progressBar.progress = strategy.successCount
|
||||
holder.progressTextView.text = "${strategy.successCount}/${strategy.totalRequests}"
|
||||
}
|
||||
}
|
||||
|
||||
if (strategy.siteResults.isNotEmpty() && holder.expandButton.isGone) {
|
||||
holder.expandButton.visibility = View.VISIBLE
|
||||
}
|
||||
|
||||
if (strategy.isExpanded && holder.sitesRecyclerView.isVisible) {
|
||||
val adapter = holder.sitesRecyclerView.adapter
|
||||
if (adapter is SiteResultAdapter) {
|
||||
adapter.updateSites(strategy.siteResults)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int {
|
||||
return strategies.size
|
||||
}
|
||||
|
||||
@SuppressLint("NotifyDataSetChanged")
|
||||
fun setTestingState(testing: Boolean) {
|
||||
isTesting = testing
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
|
||||
@SuppressLint("NotifyDataSetChanged")
|
||||
fun updateStrategies(newStrategies: List<StrategyResult>, sortByPercentage: Boolean = true) {
|
||||
strategies.clear()
|
||||
|
||||
val sorted = if (sortByPercentage) {
|
||||
newStrategies.sortedWith(compareByDescending<StrategyResult> { it.isCompleted }
|
||||
.thenByDescending { it.successPercentage }
|
||||
.thenByDescending { it.successCount })
|
||||
} else {
|
||||
newStrategies
|
||||
}
|
||||
|
||||
strategies.addAll(sorted)
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
|
||||
private fun showCommandMenu(command: String) {
|
||||
val menuItems = arrayOf(
|
||||
context.getString(R.string.cmd_history_apply),
|
||||
context.getString(R.string.cmd_history_copy)
|
||||
)
|
||||
|
||||
AlertDialog.Builder(context)
|
||||
.setTitle(context.getString(R.string.cmd_history_menu))
|
||||
.setItems(menuItems) { _, which ->
|
||||
when (which) {
|
||||
0 -> onApply(command)
|
||||
1 -> ClipboardUtils.copy(context, command, "command")
|
||||
}
|
||||
}
|
||||
.show()
|
||||
}
|
||||
}
|
||||
+2
-1
@@ -1,4 +1,4 @@
|
||||
package io.github.dovecoteescapee.byedpi.core
|
||||
package io.github.romanvht.byedpi.core
|
||||
|
||||
class ByeDpiProxy {
|
||||
companion object {
|
||||
@@ -24,4 +24,5 @@ class ByeDpiProxy {
|
||||
|
||||
private external fun jniStartProxy(args: Array<String>): Int
|
||||
private external fun jniStopProxy(): Int
|
||||
external fun jniForceClose(): Int
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
package io.github.romanvht.byedpi.core
|
||||
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import android.util.Log
|
||||
import io.github.romanvht.byedpi.data.UISettings
|
||||
import io.github.romanvht.byedpi.utility.DomainListUtils
|
||||
import io.github.romanvht.byedpi.utility.checkIpAndPortInCmd
|
||||
import io.github.romanvht.byedpi.utility.getCmdArgs
|
||||
import io.github.romanvht.byedpi.utility.getCmdEnable
|
||||
import io.github.romanvht.byedpi.utility.getStringNotNull
|
||||
import io.github.romanvht.byedpi.utility.shellSplit
|
||||
|
||||
sealed interface ByeDpiProxyPreferences {
|
||||
companion object {
|
||||
fun fromSharedPreferences(preferences: SharedPreferences, context: Context): ByeDpiProxyPreferences =
|
||||
when (preferences.getCmdEnable()) {
|
||||
true -> ByeDpiProxyCmdPreferences(preferences, context)
|
||||
false -> ByeDpiProxyUIPreferences(preferences)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class ByeDpiProxyCmdPreferences(val args: Array<String>) : ByeDpiProxyPreferences {
|
||||
constructor(preferences: SharedPreferences, context: Context) : this(
|
||||
parseCmdToArguments(preferences, context)
|
||||
)
|
||||
|
||||
companion object {
|
||||
private fun parseCmdToArguments(preferences: SharedPreferences, context: Context): Array<String> {
|
||||
val cmd = preferences.getCmdArgs()
|
||||
val preparedCmd = getLists(cmd, context)
|
||||
|
||||
val firstArgIndex = preparedCmd.indexOf("-")
|
||||
val args = (if (firstArgIndex > 0) preparedCmd.substring(firstArgIndex) else preparedCmd).trim()
|
||||
|
||||
Log.d("ProxyPref", "CMD: $args")
|
||||
|
||||
val (cmdIp, cmdPort) = preferences.checkIpAndPortInCmd()
|
||||
val ip = preferences.getStringNotNull("byedpi_proxy_ip", "127.0.0.1")
|
||||
val port = preferences.getStringNotNull("byedpi_proxy_port", "1080")
|
||||
|
||||
val prefix = buildString {
|
||||
if (cmdIp == null) append("--ip $ip ")
|
||||
if (cmdPort == null) append("--port $port ")
|
||||
}
|
||||
|
||||
Log.d("ProxyPref", "Added from settings: $prefix")
|
||||
|
||||
val blacklist = setOf("--help", "--version", "-h", "-v")
|
||||
val splitArgs = shellSplit("$prefix$args").filter { it !in blacklist }
|
||||
|
||||
return arrayOf("ciadpi") + splitArgs
|
||||
}
|
||||
|
||||
private fun getLists(cmd: String, context: Context): String {
|
||||
return Regex("""\{list:([^}]+)\}""").replace(cmd) { matchResult ->
|
||||
val listName = matchResult.groupValues[1].trim()
|
||||
val lists = DomainListUtils.getLists(context)
|
||||
val domainList = lists.find { it.name.equals(listName, ignoreCase = true) }
|
||||
|
||||
if (domainList != null && domainList.domains.isNotEmpty()) {
|
||||
domainList.domains.joinToString(" ")
|
||||
} else {
|
||||
Log.w("ProxyPref", "List '$listName' not found or empty")
|
||||
""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class ByeDpiProxyUIPreferences(val settings: UISettings = UISettings()) : ByeDpiProxyPreferences {
|
||||
|
||||
constructor(preferences: SharedPreferences) : this(
|
||||
UISettings.fromSharedPreferences(preferences)
|
||||
)
|
||||
|
||||
val uiargs: Array<String>
|
||||
get() {
|
||||
val args = mutableListOf("ciadpi")
|
||||
|
||||
if (settings.ip.isNotEmpty()) args.add("-i${settings.ip}")
|
||||
if (settings.port != 0) args.add("-p${settings.port}")
|
||||
if (settings.maxConnections != 0) args.add("-c${settings.maxConnections}")
|
||||
if (settings.bufferSize != 0) args.add("-b${settings.bufferSize}")
|
||||
|
||||
val protocols = buildList {
|
||||
if (settings.desyncHttps) add("t")
|
||||
if (settings.desyncHttp) add("h")
|
||||
}
|
||||
|
||||
if (!settings.hosts.isNullOrBlank()) {
|
||||
val hostStr = ":${settings.hosts.replace("\n", " ")}"
|
||||
when (settings.hostsMode) {
|
||||
UISettings.HostsMode.Blacklist -> {
|
||||
args.add("-H$hostStr")
|
||||
args.add("-An")
|
||||
if (protocols.isNotEmpty()) args.add("-K${protocols.joinToString(",")}")
|
||||
}
|
||||
UISettings.HostsMode.Whitelist -> {
|
||||
if (protocols.isNotEmpty()) args.add("-K${protocols.joinToString(",")}")
|
||||
args.add("-H$hostStr")
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
} else {
|
||||
if (protocols.isNotEmpty()) args.add("-K${protocols.joinToString(",")}")
|
||||
}
|
||||
|
||||
if (settings.defaultTtl != 0) args.add("-g${settings.defaultTtl}")
|
||||
if (settings.noDomain) args.add("-N")
|
||||
|
||||
if (settings.splitPosition != 0) {
|
||||
val posArg = settings.splitPosition.toString() + if (settings.splitAtHost) "+h" else ""
|
||||
val option = when (settings.desyncMethod) {
|
||||
UISettings.DesyncMethod.Split -> "-s"
|
||||
UISettings.DesyncMethod.Disorder -> "-d"
|
||||
UISettings.DesyncMethod.OOB -> "-o"
|
||||
UISettings.DesyncMethod.DISOOB -> "-q"
|
||||
UISettings.DesyncMethod.Fake -> "-f"
|
||||
UISettings.DesyncMethod.None -> ""
|
||||
}
|
||||
if (option.isNotEmpty()) args.add("$option$posArg")
|
||||
}
|
||||
|
||||
if (settings.desyncMethod == UISettings.DesyncMethod.Fake) {
|
||||
if (settings.fakeTtl != 0) args.add("-t${settings.fakeTtl}")
|
||||
if (settings.fakeSni.isNotEmpty()) args.add("-n${settings.fakeSni}")
|
||||
if (settings.fakeOffset != 0) args.add("-O${settings.fakeOffset}")
|
||||
}
|
||||
|
||||
if (settings.desyncMethod == UISettings.DesyncMethod.OOB || settings.desyncMethod == UISettings.DesyncMethod.DISOOB) {
|
||||
args.add("-e${settings.oobChar[0].code.toByte()}")
|
||||
}
|
||||
|
||||
val modHttpFlags = buildList {
|
||||
if (settings.hostMixedCase) add("h")
|
||||
if (settings.domainMixedCase) add("d")
|
||||
if (settings.hostRemoveSpaces) add("r")
|
||||
}
|
||||
|
||||
if (modHttpFlags.isNotEmpty()) args.add("-M${modHttpFlags.joinToString(",")}")
|
||||
|
||||
if (settings.tlsRecordSplit && settings.tlsRecordSplitPosition != 0) {
|
||||
val tlsRecArg = settings.tlsRecordSplitPosition.toString() + if (settings.tlsRecordSplitAtSni) "+s" else ""
|
||||
args.add("-r$tlsRecArg")
|
||||
}
|
||||
|
||||
if (settings.tcpFastOpen) args.add("-F")
|
||||
if (settings.dropSack) args.add("-Y")
|
||||
|
||||
args.add("-An")
|
||||
|
||||
if (settings.desyncUdp) {
|
||||
args.add("-Ku")
|
||||
if (settings.udpFakeCount != 0) args.add("-a${settings.udpFakeCount}")
|
||||
args.add("-An")
|
||||
}
|
||||
|
||||
Log.d("ProxyPref", "UI to cmd: ${args.joinToString(" ")}")
|
||||
return args.toTypedArray()
|
||||
}
|
||||
}
|
||||
+1
-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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
+42
-41
@@ -1,5 +1,8 @@
|
||||
package io.github.dovecoteescapee.byedpi.fragments
|
||||
package io.github.romanvht.byedpi.fragments
|
||||
|
||||
import android.content.SharedPreferences
|
||||
import android.content.pm.ApplicationInfo
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
@@ -11,10 +14,12 @@ import androidx.lifecycle.lifecycleScope
|
||||
import androidx.preference.PreferenceManager
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import io.github.dovecoteescapee.byedpi.R
|
||||
import io.github.dovecoteescapee.byedpi.adapters.AppSelectionAdapter
|
||||
import io.github.dovecoteescapee.byedpi.data.AppInfo
|
||||
import io.github.romanvht.byedpi.R
|
||||
import io.github.romanvht.byedpi.adapters.AppSelectionAdapter
|
||||
import io.github.romanvht.byedpi.data.AppInfo
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.awaitAll
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
@@ -23,9 +28,15 @@ class AppSelectionFragment : Fragment() {
|
||||
private lateinit var searchView: SearchView
|
||||
private lateinit var progressBar: ProgressBar
|
||||
private lateinit var adapter: AppSelectionAdapter
|
||||
private lateinit var prefs: SharedPreferences
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||
val view = inflater.inflate(R.layout.app_selection, container, false)
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View {
|
||||
val view = inflater.inflate(R.layout.app_selection_layout, container, false)
|
||||
prefs = PreferenceManager.getDefaultSharedPreferences(requireContext())
|
||||
|
||||
recyclerView = view.findViewById(R.id.recyclerView)
|
||||
searchView = view.findViewById(R.id.searchView)
|
||||
@@ -33,7 +44,6 @@ class AppSelectionFragment : Fragment() {
|
||||
|
||||
setupRecyclerView()
|
||||
setupSearchView()
|
||||
|
||||
loadApps()
|
||||
|
||||
return view
|
||||
@@ -62,61 +72,52 @@ class AppSelectionFragment : Fragment() {
|
||||
|
||||
private fun loadApps() {
|
||||
progressBar.visibility = View.VISIBLE
|
||||
searchView.visibility = View.GONE
|
||||
|
||||
lifecycleScope.launch {
|
||||
val apps = withContext(Dispatchers.IO) {
|
||||
getInstalledApps()
|
||||
}
|
||||
adapter = AppSelectionAdapter(apps) { app, isChecked ->
|
||||
updateSelectedApps(app.packageName, isChecked)
|
||||
getInstalledAppsAsync()
|
||||
}
|
||||
|
||||
adapter = AppSelectionAdapter(requireContext(), apps)
|
||||
recyclerView.adapter = adapter
|
||||
progressBar.visibility = View.GONE
|
||||
searchView.visibility = View.VISIBLE
|
||||
}
|
||||
}
|
||||
|
||||
private fun getInstalledApps(): List<AppInfo> {
|
||||
private suspend fun getInstalledAppsAsync(): List<AppInfo> {
|
||||
val pm = requireContext().packageManager
|
||||
val installedApps = pm.getInstalledApplications(0)
|
||||
val selectedApps = PreferenceManager.getDefaultSharedPreferences(requireContext())
|
||||
.getStringSet("selected_apps", setOf()) ?: setOf()
|
||||
val selectedApps = prefs.getStringSet("selected_apps", setOf()) ?: setOf()
|
||||
|
||||
return installedApps
|
||||
.filter { it.packageName != requireContext().packageName }
|
||||
.map {
|
||||
val appName = try {
|
||||
pm.getApplicationLabel(it).toString()
|
||||
} catch (e: Exception) {
|
||||
it.packageName
|
||||
.map { appInfo ->
|
||||
lifecycleScope.async(Dispatchers.IO) {
|
||||
createAppInfo(appInfo, pm, selectedApps)
|
||||
}
|
||||
|
||||
val appIcon = try {
|
||||
pm.getApplicationIcon(it.packageName)
|
||||
} catch (e: Exception) {
|
||||
pm.defaultActivityIcon
|
||||
}
|
||||
|
||||
AppInfo(
|
||||
appName,
|
||||
it.packageName,
|
||||
appIcon,
|
||||
selectedApps.contains(it.packageName)
|
||||
)
|
||||
}
|
||||
.awaitAll()
|
||||
.sortedWith(compareBy({ !it.isSelected }, { it.appName.lowercase() }))
|
||||
}
|
||||
|
||||
private fun updateSelectedApps(packageName: String, isSelected: Boolean) {
|
||||
val prefs = PreferenceManager.getDefaultSharedPreferences(requireContext())
|
||||
val selectedApps = prefs.getStringSet("selected_apps", mutableSetOf())?.toMutableSet() ?: mutableSetOf()
|
||||
|
||||
if (isSelected) {
|
||||
selectedApps.add(packageName)
|
||||
} else {
|
||||
selectedApps.remove(packageName)
|
||||
private fun createAppInfo(
|
||||
appInfo: ApplicationInfo,
|
||||
pm: PackageManager,
|
||||
selectedApps: Set<String>
|
||||
): AppInfo {
|
||||
val appName = try {
|
||||
pm.getApplicationLabel(appInfo).toString()
|
||||
} catch (_: Exception) {
|
||||
appInfo.packageName
|
||||
}
|
||||
|
||||
prefs.edit().putStringSet("selected_apps", selectedApps).apply()
|
||||
return AppInfo(
|
||||
appName,
|
||||
appInfo.packageName,
|
||||
selectedApps.contains(appInfo.packageName),
|
||||
icon = null
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,286 @@
|
||||
package io.github.romanvht.byedpi.fragments
|
||||
|
||||
import android.net.VpnService
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.widget.EditText
|
||||
import android.widget.ImageView
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
import androidx.preference.*
|
||||
import io.github.romanvht.byedpi.R
|
||||
import io.github.romanvht.byedpi.utility.findPreferenceNotNull
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import io.github.romanvht.byedpi.data.AppStatus
|
||||
import io.github.romanvht.byedpi.data.Command
|
||||
import io.github.romanvht.byedpi.data.Mode
|
||||
import io.github.romanvht.byedpi.services.ServiceManager
|
||||
import io.github.romanvht.byedpi.services.appStatus
|
||||
import io.github.romanvht.byedpi.utility.ClipboardUtils
|
||||
import io.github.romanvht.byedpi.utility.HistoryUtils
|
||||
import io.github.romanvht.byedpi.utility.getCmdArgs
|
||||
import io.github.romanvht.byedpi.utility.getPreferences
|
||||
import io.github.romanvht.byedpi.utility.mode
|
||||
|
||||
class ByeDpiCMDSettingsFragment : PreferenceFragmentCompat() {
|
||||
|
||||
private lateinit var cmdHistoryUtils: HistoryUtils
|
||||
private lateinit var editTextPreference: EditTextPreference
|
||||
private lateinit var historyHeader: Preference
|
||||
private val historyPreferences = mutableListOf<Preference>()
|
||||
|
||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||
setPreferencesFromResource(R.xml.byedpi_cmd_settings, rootKey)
|
||||
|
||||
cmdHistoryUtils = HistoryUtils(requireContext())
|
||||
|
||||
editTextPreference = findPreferenceNotNull("byedpi_cmd_args")
|
||||
editTextPreference.text = requireContext().getPreferences().getCmdArgs()
|
||||
historyHeader = findPreferenceNotNull("cmd_history_header")
|
||||
|
||||
editTextPreference.setOnPreferenceChangeListener { _, newValue ->
|
||||
val newCommand = newValue.toString()
|
||||
|
||||
if (newCommand.isNotBlank()) {
|
||||
cmdHistoryUtils.addCommand(newCommand)
|
||||
restartService()
|
||||
}
|
||||
|
||||
updateHistoryItems()
|
||||
true
|
||||
}
|
||||
|
||||
historyHeader.setOnPreferenceClickListener {
|
||||
showHistoryClearDialog()
|
||||
true
|
||||
}
|
||||
|
||||
updateHistoryItems()
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
view.post {
|
||||
view.findViewById<View>(R.id.btn_clear)?.setOnClickListener {
|
||||
editTextPreference.text = ""
|
||||
updateHistoryItems()
|
||||
}
|
||||
|
||||
view.findViewById<View>(R.id.btn_paste)?.setOnClickListener {
|
||||
val text = ClipboardUtils.paste(requireContext())
|
||||
if (!text.isNullOrBlank()) {
|
||||
editTextPreference.text = text
|
||||
cmdHistoryUtils.addCommand(text)
|
||||
updateHistoryItems()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateHistoryItems() {
|
||||
if (listView !== null) {
|
||||
listView.itemAnimator = null
|
||||
}
|
||||
|
||||
historyPreferences.forEach { preference ->
|
||||
preferenceScreen.removePreference(preference)
|
||||
}
|
||||
|
||||
historyPreferences.clear()
|
||||
val history = cmdHistoryUtils.getHistory()
|
||||
historyHeader.isVisible = history.isNotEmpty()
|
||||
|
||||
if (history.isNotEmpty()) {
|
||||
history.sortedWith(compareByDescending<Command> { it.pinned }.thenBy { history.indexOf(it) })
|
||||
.forEachIndexed { _, command ->
|
||||
val preference = createPreference(command)
|
||||
historyPreferences.add(preference)
|
||||
preferenceScreen.addPreference(preference)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun createPreference(command: Command) =
|
||||
object : Preference(requireContext()) {
|
||||
override fun onBindViewHolder(holder: PreferenceViewHolder) {
|
||||
super.onBindViewHolder(holder)
|
||||
|
||||
val nameView = holder.itemView.findViewById<TextView>(R.id.command_name)
|
||||
val summaryView = holder.itemView.findViewById<TextView>(android.R.id.summary)
|
||||
val pinIcon = holder.itemView.findViewById<ImageView>(R.id.pin_icon)
|
||||
val commandName = command.name
|
||||
|
||||
if (!commandName.isNullOrBlank()) {
|
||||
nameView.visibility = View.VISIBLE
|
||||
nameView.text = command.name
|
||||
} else {
|
||||
nameView.visibility = View.GONE
|
||||
}
|
||||
|
||||
val summaryText = buildSummary(command)
|
||||
if (summaryText.isNotBlank()) {
|
||||
summaryView.visibility = View.VISIBLE
|
||||
summaryView.text = summaryText
|
||||
} else {
|
||||
summaryView.visibility = View.GONE
|
||||
}
|
||||
|
||||
pinIcon.visibility = if (command.pinned) View.VISIBLE else View.GONE
|
||||
}
|
||||
}.apply {
|
||||
title = command.text
|
||||
layoutResource = R.layout.history_item
|
||||
setOnPreferenceClickListener {
|
||||
showActionDialog(command)
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
private fun buildSummary(command: Command): String {
|
||||
val parts = mutableListOf<String>()
|
||||
|
||||
if (command.text == editTextPreference.text) {
|
||||
parts.add(getString(R.string.cmd_history_applied))
|
||||
}
|
||||
|
||||
return parts.joinToString(" • ")
|
||||
}
|
||||
|
||||
private fun showHistoryClearDialog() {
|
||||
val options = arrayOf(
|
||||
getString(R.string.cmd_history_delete_unpinned),
|
||||
getString(R.string.cmd_history_delete_all),
|
||||
)
|
||||
|
||||
AlertDialog.Builder(requireContext())
|
||||
.setTitle(getString(R.string.cmd_history_menu))
|
||||
.setItems(options) { _, which ->
|
||||
when (which) {
|
||||
0 -> deleteUnpinnedHistory()
|
||||
1 -> deleteAllHistory()
|
||||
}
|
||||
}
|
||||
.setNegativeButton(getString(android.R.string.cancel), null)
|
||||
.show()
|
||||
}
|
||||
|
||||
private fun deleteAllHistory() {
|
||||
cmdHistoryUtils.clearAllHistory()
|
||||
updateHistoryItems()
|
||||
}
|
||||
|
||||
private fun deleteUnpinnedHistory() {
|
||||
cmdHistoryUtils.clearUnpinnedHistory()
|
||||
updateHistoryItems()
|
||||
}
|
||||
|
||||
private fun showActionDialog(command: Command) {
|
||||
val options = arrayOf(
|
||||
getString(R.string.cmd_history_apply),
|
||||
if (command.pinned) getString(R.string.cmd_history_unpin) else getString(R.string.cmd_history_pin),
|
||||
getString(R.string.cmd_history_rename),
|
||||
getString(R.string.cmd_history_edit),
|
||||
getString(R.string.cmd_history_copy),
|
||||
getString(R.string.cmd_history_delete)
|
||||
)
|
||||
|
||||
AlertDialog.Builder(requireContext())
|
||||
.setTitle(getString(R.string.cmd_history_menu))
|
||||
.setItems(options) { _, which ->
|
||||
when (which) {
|
||||
0 -> applyCommand(command.text)
|
||||
1 -> if (command.pinned) unpinCommand(command.text) else pinCommand(command.text)
|
||||
2 -> showRenameDialog(command)
|
||||
3 -> showEditDialog(command)
|
||||
4 -> ClipboardUtils.copy(requireContext(), command.text, "command")
|
||||
5 -> deleteCommand(command.text)
|
||||
}
|
||||
}
|
||||
.show()
|
||||
}
|
||||
|
||||
private fun showRenameDialog(command: Command) {
|
||||
val input = EditText(requireContext()).apply {
|
||||
setText(command.name)
|
||||
}
|
||||
|
||||
val container = LinearLayout(context).apply {
|
||||
orientation = LinearLayout.VERTICAL
|
||||
setPadding(50, 20, 50, 20)
|
||||
addView(input)
|
||||
}
|
||||
|
||||
AlertDialog.Builder(requireContext())
|
||||
.setTitle(getString(R.string.cmd_history_rename))
|
||||
.setView(container)
|
||||
.setPositiveButton(getString(android.R.string.ok)) { _, _ ->
|
||||
val newName = input.text.toString()
|
||||
if (newName != command.name) {
|
||||
cmdHistoryUtils.renameCommand(command.text, newName)
|
||||
updateHistoryItems()
|
||||
}
|
||||
}
|
||||
.setNegativeButton(getString(android.R.string.cancel), null)
|
||||
.show()
|
||||
}
|
||||
|
||||
private fun showEditDialog(command: Command) {
|
||||
val input = EditText(requireContext()).apply {
|
||||
setText(command.text)
|
||||
}
|
||||
|
||||
val container = LinearLayout(context).apply {
|
||||
orientation = LinearLayout.VERTICAL
|
||||
setPadding(50, 20, 50, 20)
|
||||
addView(input)
|
||||
}
|
||||
|
||||
AlertDialog.Builder(requireContext())
|
||||
.setTitle(getString(R.string.cmd_history_edit))
|
||||
.setView(container)
|
||||
.setPositiveButton(getString(android.R.string.ok)) { _, _ ->
|
||||
val newText = input.text.toString()
|
||||
if (newText.isNotBlank() && newText != command.text) {
|
||||
cmdHistoryUtils.editCommand(command.text, newText)
|
||||
if (editTextPreference.text == command.text) applyCommand(newText)
|
||||
updateHistoryItems()
|
||||
}
|
||||
}
|
||||
.setNegativeButton(getString(android.R.string.cancel), null)
|
||||
.show()
|
||||
}
|
||||
|
||||
private fun applyCommand(command: String) {
|
||||
editTextPreference.text = command
|
||||
updateHistoryItems()
|
||||
restartService()
|
||||
}
|
||||
|
||||
private fun pinCommand(command: String) {
|
||||
cmdHistoryUtils.pinCommand(command)
|
||||
updateHistoryItems()
|
||||
}
|
||||
|
||||
private fun unpinCommand(command: String) {
|
||||
cmdHistoryUtils.unpinCommand(command)
|
||||
updateHistoryItems()
|
||||
}
|
||||
|
||||
private fun deleteCommand(command: String) {
|
||||
cmdHistoryUtils.deleteCommand(command)
|
||||
updateHistoryItems()
|
||||
}
|
||||
|
||||
private fun restartService() {
|
||||
if (appStatus.first == AppStatus.Running) {
|
||||
val ctx = context ?: return
|
||||
val mode = ctx.getPreferences().mode()
|
||||
if (mode == Mode.VPN && VpnService.prepare(ctx) != null) return
|
||||
|
||||
ServiceManager.restart(ctx, mode)
|
||||
Toast.makeText(ctx, R.string.service_restart, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
+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) {
|
||||
+87
-27
@@ -1,23 +1,26 @@
|
||||
package io.github.dovecoteescapee.byedpi.services
|
||||
package io.github.romanvht.byedpi.services
|
||||
|
||||
import android.app.Notification
|
||||
import android.app.NotificationManager
|
||||
import android.content.Intent
|
||||
import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE
|
||||
import android.content.pm.ServiceInfo
|
||||
import android.os.Build
|
||||
import android.util.Log
|
||||
import androidx.lifecycle.LifecycleService
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import io.github.dovecoteescapee.byedpi.R
|
||||
import io.github.dovecoteescapee.byedpi.core.ByeDpiProxy
|
||||
import io.github.dovecoteescapee.byedpi.core.ByeDpiProxyPreferences
|
||||
import io.github.dovecoteescapee.byedpi.data.*
|
||||
import io.github.dovecoteescapee.byedpi.utility.*
|
||||
import io.github.romanvht.byedpi.R
|
||||
import io.github.romanvht.byedpi.core.ByeDpiProxy
|
||||
import io.github.romanvht.byedpi.core.ByeDpiProxyPreferences
|
||||
import io.github.romanvht.byedpi.data.*
|
||||
import io.github.romanvht.byedpi.utility.*
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.coroutines.withTimeoutOrNull
|
||||
|
||||
class ByeDpiProxyService : LifecycleService() {
|
||||
private var proxy = ByeDpiProxy()
|
||||
@@ -27,13 +30,10 @@ class ByeDpiProxyService : LifecycleService() {
|
||||
companion object {
|
||||
private val TAG: String = ByeDpiProxyService::class.java.simpleName
|
||||
private const val FOREGROUND_SERVICE_ID: Int = 2
|
||||
private const val PAUSE_NOTIFICATION_ID: Int = 3
|
||||
private const val NOTIFICATION_CHANNEL_ID: String = "ByeDPI Proxy"
|
||||
|
||||
private var status: ServiceStatus = ServiceStatus.Disconnected
|
||||
|
||||
fun getStatus(): ServiceStatus {
|
||||
return status
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreate() {
|
||||
@@ -47,14 +47,36 @@ class ByeDpiProxyService : LifecycleService() {
|
||||
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
super.onStartCommand(intent, flags, startId)
|
||||
|
||||
startForeground()
|
||||
|
||||
return when (val action = intent?.action) {
|
||||
START_ACTION -> {
|
||||
lifecycleScope.launch { start() }
|
||||
lifecycleScope.launch {
|
||||
start()
|
||||
}
|
||||
START_STICKY
|
||||
}
|
||||
|
||||
STOP_ACTION -> {
|
||||
lifecycleScope.launch { stop() }
|
||||
lifecycleScope.launch {
|
||||
stop()
|
||||
}
|
||||
START_NOT_STICKY
|
||||
}
|
||||
|
||||
RESUME_ACTION -> {
|
||||
lifecycleScope.launch {
|
||||
start()
|
||||
}
|
||||
START_STICKY
|
||||
}
|
||||
|
||||
PAUSE_ACTION -> {
|
||||
lifecycleScope.launch {
|
||||
stop()
|
||||
createNotificationPause()
|
||||
}
|
||||
START_NOT_STICKY
|
||||
}
|
||||
|
||||
@@ -68,17 +90,20 @@ class ByeDpiProxyService : LifecycleService() {
|
||||
private suspend fun start() {
|
||||
Log.i(TAG, "Starting")
|
||||
|
||||
val notificationManager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager
|
||||
notificationManager.cancel(PAUSE_NOTIFICATION_ID)
|
||||
|
||||
if (status == ServiceStatus.Connected) {
|
||||
Log.w(TAG, "Proxy already connected")
|
||||
updateStatus(ServiceStatus.Connected)
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
startForeground()
|
||||
mutex.withLock {
|
||||
startProxy()
|
||||
updateStatus(ServiceStatus.Connected)
|
||||
}
|
||||
updateStatus(ServiceStatus.Connected)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Failed to start proxy", e)
|
||||
updateStatus(ServiceStatus.Failed)
|
||||
@@ -92,7 +117,7 @@ class ByeDpiProxyService : LifecycleService() {
|
||||
startForeground(
|
||||
FOREGROUND_SERVICE_ID,
|
||||
notification,
|
||||
FOREGROUND_SERVICE_TYPE_SPECIAL_USE,
|
||||
ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC,
|
||||
)
|
||||
} else {
|
||||
startForeground(FOREGROUND_SERVICE_ID, notification)
|
||||
@@ -102,15 +127,23 @@ class ByeDpiProxyService : LifecycleService() {
|
||||
private suspend fun stop() {
|
||||
Log.i(TAG, "Stopping")
|
||||
|
||||
if (status != ServiceStatus.Connected) {
|
||||
Log.w(TAG, "Proxy not connected")
|
||||
updateStatus(ServiceStatus.Disconnected)
|
||||
return
|
||||
}
|
||||
|
||||
mutex.withLock {
|
||||
stopProxy()
|
||||
withContext(Dispatchers.IO) {
|
||||
stopProxy()
|
||||
}
|
||||
}
|
||||
|
||||
updateStatus(ServiceStatus.Disconnected)
|
||||
stopSelf()
|
||||
}
|
||||
|
||||
private suspend fun startProxy() {
|
||||
private fun startProxy() {
|
||||
Log.i(TAG, "Starting proxy")
|
||||
|
||||
if (proxyJob != null) {
|
||||
@@ -124,13 +157,12 @@ class ByeDpiProxyService : LifecycleService() {
|
||||
proxyJob = lifecycleScope.launch(Dispatchers.IO) {
|
||||
val code = proxy.startProxy(preferences)
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
if (code != 0) {
|
||||
Log.e(TAG, "Proxy stopped with code $code")
|
||||
updateStatus(ServiceStatus.Failed)
|
||||
} else {
|
||||
updateStatus(ServiceStatus.Disconnected)
|
||||
}
|
||||
delay(500)
|
||||
|
||||
if (code != 0) {
|
||||
Log.e(TAG, "Proxy stopped with code $code")
|
||||
updateStatus(ServiceStatus.Failed)
|
||||
stopSelf()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -147,7 +179,18 @@ class ByeDpiProxyService : LifecycleService() {
|
||||
|
||||
try {
|
||||
proxy.stopProxy()
|
||||
proxyJob?.join()
|
||||
proxyJob?.cancel()
|
||||
|
||||
val completed = withTimeoutOrNull(2000) {
|
||||
proxyJob?.join()
|
||||
true
|
||||
}
|
||||
|
||||
if (completed == null) {
|
||||
Log.w(TAG, "proxy not finish in time, cancelling...")
|
||||
proxy.jniForceClose()
|
||||
}
|
||||
|
||||
proxyJob = null
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Failed to close proxyJob", e)
|
||||
@@ -157,7 +200,7 @@ class ByeDpiProxyService : LifecycleService() {
|
||||
}
|
||||
|
||||
private fun getByeDpiPreferences(): ByeDpiProxyPreferences =
|
||||
ByeDpiProxyPreferences.fromSharedPreferences(getPreferences())
|
||||
ByeDpiProxyPreferences.fromSharedPreferences(getPreferences(), this)
|
||||
|
||||
private fun updateStatus(newStatus: ServiceStatus) {
|
||||
Log.d(TAG, "Proxy status changed from $status to $newStatus")
|
||||
@@ -185,6 +228,10 @@ class ByeDpiProxyService : LifecycleService() {
|
||||
)
|
||||
intent.putExtra(SENDER, Sender.Proxy.ordinal)
|
||||
sendBroadcast(intent)
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
QuickTileService.updateTile()
|
||||
}
|
||||
}
|
||||
|
||||
private fun createNotification(): Notification =
|
||||
@@ -195,4 +242,17 @@ class ByeDpiProxyService : LifecycleService() {
|
||||
R.string.proxy_notification_content,
|
||||
ByeDpiProxyService::class.java,
|
||||
)
|
||||
|
||||
private fun createNotificationPause() {
|
||||
val notification = createPauseNotification(
|
||||
this,
|
||||
NOTIFICATION_CHANNEL_ID,
|
||||
R.string.notification_title,
|
||||
R.string.service_paused_text,
|
||||
ByeDpiProxyService::class.java,
|
||||
)
|
||||
|
||||
val notificationManager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager
|
||||
notificationManager.notify(PAUSE_NOTIFICATION_ID, notification)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package io.github.romanvht.byedpi.services
|
||||
|
||||
import io.github.romanvht.byedpi.data.AppStatus
|
||||
import io.github.romanvht.byedpi.data.Mode
|
||||
|
||||
var appStatus = AppStatus.Halted to Mode.VPN
|
||||
private set
|
||||
|
||||
fun setStatus(status: AppStatus, mode: Mode) {
|
||||
appStatus = status to mode
|
||||
}
|
||||
+135
-44
@@ -1,6 +1,7 @@
|
||||
package io.github.dovecoteescapee.byedpi.services
|
||||
package io.github.romanvht.byedpi.services
|
||||
|
||||
import android.app.Notification
|
||||
import android.app.NotificationManager
|
||||
import android.app.PendingIntent
|
||||
import android.content.Intent
|
||||
import android.content.pm.ServiceInfo
|
||||
@@ -8,19 +9,21 @@ import android.os.Build
|
||||
import android.os.ParcelFileDescriptor
|
||||
import android.util.Log
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import io.github.dovecoteescapee.byedpi.R
|
||||
import io.github.dovecoteescapee.byedpi.activities.MainActivity
|
||||
import io.github.dovecoteescapee.byedpi.core.ByeDpiProxy
|
||||
import io.github.dovecoteescapee.byedpi.core.ByeDpiProxyPreferences
|
||||
import io.github.dovecoteescapee.byedpi.core.TProxyService
|
||||
import io.github.dovecoteescapee.byedpi.data.*
|
||||
import io.github.dovecoteescapee.byedpi.utility.*
|
||||
import io.github.romanvht.byedpi.R
|
||||
import io.github.romanvht.byedpi.activities.MainActivity
|
||||
import io.github.romanvht.byedpi.core.ByeDpiProxy
|
||||
import io.github.romanvht.byedpi.core.ByeDpiProxyPreferences
|
||||
import io.github.romanvht.byedpi.core.TProxyService
|
||||
import io.github.romanvht.byedpi.data.*
|
||||
import io.github.romanvht.byedpi.utility.*
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.coroutines.withTimeoutOrNull
|
||||
import java.io.File
|
||||
|
||||
class ByeDpiVpnService : LifecycleVpnService() {
|
||||
@@ -28,11 +31,11 @@ class ByeDpiVpnService : LifecycleVpnService() {
|
||||
private var proxyJob: Job? = null
|
||||
private var tunFd: ParcelFileDescriptor? = null
|
||||
private val mutex = Mutex()
|
||||
private var stopping: Boolean = false
|
||||
|
||||
companion object {
|
||||
private val TAG: String = ByeDpiVpnService::class.java.simpleName
|
||||
private const val FOREGROUND_SERVICE_ID: Int = 1
|
||||
private const val PAUSE_NOTIFICATION_ID: Int = 3
|
||||
private const val NOTIFICATION_CHANNEL_ID: String = "ByeDPIVpn"
|
||||
|
||||
private var status: ServiceStatus = ServiceStatus.Disconnected
|
||||
@@ -47,19 +50,64 @@ class ByeDpiVpnService : LifecycleVpnService() {
|
||||
)
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
tunFd?.close()
|
||||
}
|
||||
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
super.onStartCommand(intent, flags, startId)
|
||||
|
||||
startForeground()
|
||||
|
||||
return when (val action = intent?.action) {
|
||||
START_ACTION -> {
|
||||
lifecycleScope.launch { start() }
|
||||
lifecycleScope.launch {
|
||||
start()
|
||||
}
|
||||
START_STICKY
|
||||
}
|
||||
|
||||
STOP_ACTION -> {
|
||||
lifecycleScope.launch { stop() }
|
||||
lifecycleScope.launch {
|
||||
stop()
|
||||
}
|
||||
START_NOT_STICKY
|
||||
}
|
||||
|
||||
RESUME_ACTION -> {
|
||||
lifecycleScope.launch {
|
||||
if (prepare(this@ByeDpiVpnService) == null) {
|
||||
start()
|
||||
}
|
||||
}
|
||||
START_STICKY
|
||||
}
|
||||
|
||||
PAUSE_ACTION -> {
|
||||
lifecycleScope.launch {
|
||||
stop()
|
||||
createNotificationPause()
|
||||
}
|
||||
START_NOT_STICKY
|
||||
}
|
||||
|
||||
SERVICE_INTERFACE -> {
|
||||
Log.i(TAG, "Started by Android")
|
||||
|
||||
if (getPreferences().mode() != Mode.VPN) {
|
||||
Log.w(TAG, "Always-On disabled in proxy mode")
|
||||
stopSelf()
|
||||
return START_NOT_STICKY
|
||||
}
|
||||
|
||||
lifecycleScope.launch {
|
||||
start()
|
||||
}
|
||||
|
||||
START_STICKY
|
||||
}
|
||||
|
||||
else -> {
|
||||
Log.w(TAG, "Unknown action: $action")
|
||||
START_NOT_STICKY
|
||||
@@ -75,18 +123,21 @@ class ByeDpiVpnService : LifecycleVpnService() {
|
||||
private suspend fun start() {
|
||||
Log.i(TAG, "Starting")
|
||||
|
||||
val notificationManager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager
|
||||
notificationManager.cancel(PAUSE_NOTIFICATION_ID)
|
||||
|
||||
if (status == ServiceStatus.Connected) {
|
||||
Log.w(TAG, "VPN already connected")
|
||||
updateStatus(ServiceStatus.Connected)
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
startForeground()
|
||||
mutex.withLock {
|
||||
startProxy()
|
||||
startTun2Socks()
|
||||
updateStatus(ServiceStatus.Connected)
|
||||
}
|
||||
updateStatus(ServiceStatus.Connected)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Failed to start VPN", e)
|
||||
updateStatus(ServiceStatus.Failed)
|
||||
@@ -100,7 +151,7 @@ class ByeDpiVpnService : LifecycleVpnService() {
|
||||
startForeground(
|
||||
FOREGROUND_SERVICE_ID,
|
||||
notification,
|
||||
ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE,
|
||||
ServiceInfo.FOREGROUND_SERVICE_TYPE_SYSTEM_EXEMPTED,
|
||||
)
|
||||
} else {
|
||||
startForeground(FOREGROUND_SERVICE_ID, notification)
|
||||
@@ -110,15 +161,20 @@ class ByeDpiVpnService : LifecycleVpnService() {
|
||||
private suspend fun stop() {
|
||||
Log.i(TAG, "Stopping")
|
||||
|
||||
if (status != ServiceStatus.Connected) {
|
||||
Log.w(TAG, "VPN not connected")
|
||||
updateStatus(ServiceStatus.Disconnected)
|
||||
return
|
||||
}
|
||||
|
||||
mutex.withLock {
|
||||
stopping = true
|
||||
try {
|
||||
stopTun2Socks()
|
||||
stopProxy()
|
||||
withContext(Dispatchers.IO) {
|
||||
stopProxy()
|
||||
stopTun2Socks()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Failed to stop VPN", e)
|
||||
} finally {
|
||||
stopping = false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -126,7 +182,7 @@ class ByeDpiVpnService : LifecycleVpnService() {
|
||||
stopSelf()
|
||||
}
|
||||
|
||||
private suspend fun startProxy() {
|
||||
private fun startProxy() {
|
||||
Log.i(TAG, "Starting proxy")
|
||||
|
||||
if (proxyJob != null) {
|
||||
@@ -139,17 +195,13 @@ class ByeDpiVpnService : LifecycleVpnService() {
|
||||
proxyJob = lifecycleScope.launch(Dispatchers.IO) {
|
||||
val code = byeDpiProxy.startProxy(preferences)
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
if (code != 0) {
|
||||
Log.e(TAG, "Proxy stopped with code $code")
|
||||
stopTun2Socks()
|
||||
updateStatus(ServiceStatus.Failed)
|
||||
} else {
|
||||
if (!stopping) {
|
||||
stop()
|
||||
updateStatus(ServiceStatus.Disconnected)
|
||||
}
|
||||
}
|
||||
delay(500)
|
||||
|
||||
if (code != 0) {
|
||||
Log.e(TAG, "Proxy stopped with code $code")
|
||||
updateStatus(ServiceStatus.Failed)
|
||||
stopTun2Socks()
|
||||
stopSelf()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -166,7 +218,18 @@ class ByeDpiVpnService : LifecycleVpnService() {
|
||||
|
||||
try {
|
||||
byeDpiProxy.stopProxy()
|
||||
proxyJob?.join()
|
||||
proxyJob?.cancel()
|
||||
|
||||
val completed = withTimeoutOrNull(2000) {
|
||||
proxyJob?.join()
|
||||
true
|
||||
}
|
||||
|
||||
if (completed == null) {
|
||||
Log.w(TAG, "proxy not finish in time, cancelling...")
|
||||
byeDpiProxy.jniForceClose()
|
||||
}
|
||||
|
||||
proxyJob = null
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Failed to close proxyJob", e)
|
||||
@@ -179,24 +242,28 @@ class ByeDpiVpnService : LifecycleVpnService() {
|
||||
Log.i(TAG, "Starting tun2socks")
|
||||
|
||||
if (tunFd != null) {
|
||||
Log.w(TAG, "VPN field not null")
|
||||
throw IllegalStateException("VPN field not null")
|
||||
}
|
||||
|
||||
val sharedPreferences = getPreferences()
|
||||
val (ip, port) = sharedPreferences.getProxyIpAndPort()
|
||||
|
||||
val dns = sharedPreferences.getStringNotNull("dns_ip", "8.8.8.8")
|
||||
val dns = sharedPreferences.getStringNotNull("dns_ip", "1.1.1.1")
|
||||
val ipv6 = sharedPreferences.getBoolean("ipv6_enable", false)
|
||||
|
||||
val tun2socksConfig = """
|
||||
| misc:
|
||||
| task-stack-size: 81920
|
||||
| socks5:
|
||||
| mtu: 8500
|
||||
| address: $ip
|
||||
| port: $port
|
||||
| udp: udp
|
||||
""".trimMargin("| ")
|
||||
val tun2socksConfig = buildString {
|
||||
appendLine("tunnel:")
|
||||
appendLine(" mtu: 8500")
|
||||
|
||||
appendLine("misc:")
|
||||
appendLine(" task-stack-size: 81920")
|
||||
|
||||
appendLine("socks5:")
|
||||
appendLine(" address: $ip")
|
||||
appendLine(" port: $port")
|
||||
appendLine(" udp: udp")
|
||||
}
|
||||
|
||||
val configPath = try {
|
||||
File.createTempFile("config", "tmp", cacheDir).apply {
|
||||
@@ -220,6 +287,11 @@ class ByeDpiVpnService : LifecycleVpnService() {
|
||||
private fun stopTun2Socks() {
|
||||
Log.i(TAG, "Stopping tun2socks")
|
||||
|
||||
if (tunFd == null) {
|
||||
Log.w(TAG, "VPN field is null, skipping")
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
TProxyService.TProxyStopService()
|
||||
} catch (e: Exception) {
|
||||
@@ -234,16 +306,17 @@ class ByeDpiVpnService : LifecycleVpnService() {
|
||||
|
||||
try {
|
||||
tunFd?.close()
|
||||
tunFd = null
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Failed to close tunFd", e)
|
||||
} finally {
|
||||
tunFd = null
|
||||
}
|
||||
|
||||
Log.i(TAG, "Tun2socks stopped")
|
||||
}
|
||||
|
||||
private fun getByeDpiPreferences(): ByeDpiProxyPreferences =
|
||||
ByeDpiProxyPreferences.fromSharedPreferences(getPreferences())
|
||||
ByeDpiProxyPreferences.fromSharedPreferences(getPreferences(), this)
|
||||
|
||||
private fun updateStatus(newStatus: ServiceStatus) {
|
||||
Log.d(TAG, "VPN status changed from $status to $newStatus")
|
||||
@@ -272,6 +345,10 @@ class ByeDpiVpnService : LifecycleVpnService() {
|
||||
)
|
||||
intent.putExtra(SENDER, Sender.VPN.ordinal)
|
||||
sendBroadcast(intent)
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
QuickTileService.updateTile()
|
||||
}
|
||||
}
|
||||
|
||||
private fun createNotification(): Notification =
|
||||
@@ -283,6 +360,19 @@ class ByeDpiVpnService : LifecycleVpnService() {
|
||||
ByeDpiVpnService::class.java,
|
||||
)
|
||||
|
||||
private fun createNotificationPause() {
|
||||
val notification = createPauseNotification(
|
||||
this,
|
||||
NOTIFICATION_CHANNEL_ID,
|
||||
R.string.notification_title,
|
||||
R.string.service_paused_text,
|
||||
ByeDpiVpnService::class.java,
|
||||
)
|
||||
|
||||
val notificationManager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager
|
||||
notificationManager.notify(PAUSE_NOTIFICATION_ID, notification)
|
||||
}
|
||||
|
||||
private fun createBuilder(dns: String, ipv6: Boolean): Builder {
|
||||
Log.d(TAG, "DNS: $dns")
|
||||
val builder = Builder()
|
||||
@@ -307,6 +397,7 @@ class ByeDpiVpnService : LifecycleVpnService() {
|
||||
if (dns.isNotBlank()) {
|
||||
builder.addDnsServer(dns)
|
||||
}
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
builder.setMetered(false)
|
||||
}
|
||||
+1
-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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package io.github.dovecoteescapee.byedpi.utility
|
||||
package io.github.romanvht.byedpi.utility
|
||||
|
||||
// Based on https://gist.github.com/raymyers/8077031
|
||||
fun shellSplit(string: CharSequence): List<String> {
|
||||
@@ -0,0 +1,44 @@
|
||||
package io.github.romanvht.byedpi.utility
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.provider.Settings
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.os.PowerManager
|
||||
import android.util.Log
|
||||
import androidx.core.net.toUri
|
||||
|
||||
object BatteryUtils {
|
||||
private const val TAG = "BatteryUtils"
|
||||
|
||||
fun isOptimizationDisabled(context: Context): Boolean {
|
||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
val powerManager = context.getSystemService(Context.POWER_SERVICE) as PowerManager
|
||||
powerManager.isIgnoringBatteryOptimizations(context.packageName)
|
||||
} else {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("BatteryLife")
|
||||
fun requestBatteryOptimization(context: Context) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
try {
|
||||
val intent = Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS).apply {
|
||||
data = "package:${context.packageName}".toUri()
|
||||
flags = Intent.FLAG_ACTIVITY_NEW_TASK
|
||||
}
|
||||
context.startActivity(intent)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Failed to request ignore battery optimizations", e)
|
||||
try {
|
||||
val intent = Intent(Settings.ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS)
|
||||
context.startActivity(intent)
|
||||
} catch (e2: Exception) {
|
||||
Log.e(TAG, "Failed to open battery optimization settings", e2)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package io.github.romanvht.byedpi.utility
|
||||
|
||||
import android.content.ClipData
|
||||
import android.content.ClipboardManager
|
||||
import android.content.Context
|
||||
import android.widget.Toast
|
||||
import io.github.romanvht.byedpi.R
|
||||
|
||||
object ClipboardUtils {
|
||||
fun copy(context: Context, text: String, label: String = "text", showToast: Boolean = true) {
|
||||
val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as? ClipboardManager
|
||||
val clip = ClipData.newPlainText(label, text)
|
||||
clipboard?.setPrimaryClip(clip)
|
||||
|
||||
if (showToast) {
|
||||
Toast.makeText(context, R.string.toast_copied, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
|
||||
fun paste(context: Context): String? {
|
||||
val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as? ClipboardManager
|
||||
val clipData = clipboard?.primaryClip
|
||||
|
||||
return if (clipData != null && clipData.itemCount > 0) {
|
||||
clipData.getItemAt(0).text?.toString()
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,216 @@
|
||||
package io.github.romanvht.byedpi.utility
|
||||
|
||||
import android.content.Context
|
||||
import com.google.gson.Gson
|
||||
import com.google.gson.reflect.TypeToken
|
||||
import io.github.romanvht.byedpi.data.DomainList
|
||||
import java.io.File
|
||||
|
||||
object DomainListUtils {
|
||||
private const val DOMAIN_LISTS_FILE = "domain_lists.json"
|
||||
|
||||
private val gson = Gson()
|
||||
|
||||
private fun getDefaultActiveIds(lang: String): Set<String> = when (lang) {
|
||||
"tr" -> setOf("türkiye", "discord")
|
||||
else -> setOf("youtube", "googlevideo")
|
||||
}
|
||||
|
||||
fun syncLists(context: Context) {
|
||||
val currentLists = getAllLists(context).toMutableList()
|
||||
|
||||
val builtInMap = currentLists
|
||||
.filter { it.isBuiltIn }
|
||||
.associateBy { it.id }
|
||||
|
||||
val assetFiles = context.assets.list("")?.filter {
|
||||
it.startsWith("proxytest_") && it.endsWith(".sites")
|
||||
} ?: emptyList()
|
||||
|
||||
val newBuiltInIds = mutableSetOf<String>()
|
||||
|
||||
for (assetFile in assetFiles) {
|
||||
val id = assetFile
|
||||
.removePrefix("proxytest_")
|
||||
.removeSuffix(".sites")
|
||||
|
||||
newBuiltInIds.add(id)
|
||||
|
||||
val domains = context.assets.open(assetFile)
|
||||
.bufferedReader()
|
||||
.useLines { it.toList() }
|
||||
.map { it.trim() }
|
||||
.filter { it.isNotEmpty() }
|
||||
|
||||
val existing = builtInMap[id]
|
||||
|
||||
when {
|
||||
existing == null -> {
|
||||
currentLists.add(
|
||||
DomainList(
|
||||
id = id,
|
||||
name = id.replaceFirstChar { it.uppercase() },
|
||||
domains = domains,
|
||||
isActive = id in getDefaultActiveIds(SettingsUtils.getCurrentLanguage(context)),
|
||||
isBuiltIn = true
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
existing.isDeleted -> {
|
||||
continue
|
||||
}
|
||||
|
||||
existing.isModified -> {
|
||||
continue
|
||||
}
|
||||
|
||||
else -> {
|
||||
val index = currentLists.indexOfFirst { it.id == id }
|
||||
|
||||
currentLists[index] = existing.copy(
|
||||
domains = domains
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
currentLists.removeAll {
|
||||
it.isBuiltIn && it.id !in newBuiltInIds
|
||||
}
|
||||
|
||||
saveLists(context, currentLists)
|
||||
}
|
||||
|
||||
fun getAllLists(context: Context): MutableList<DomainList> {
|
||||
val listsFile = File(context.filesDir, DOMAIN_LISTS_FILE)
|
||||
|
||||
if (!listsFile.exists()) {
|
||||
return mutableListOf()
|
||||
}
|
||||
|
||||
return try {
|
||||
val json = listsFile.readText()
|
||||
val type = object : TypeToken<List<DomainList>>() {}.type
|
||||
val lists: List<DomainList> = gson.fromJson(json, type) ?: emptyList()
|
||||
lists.toMutableList()
|
||||
} catch (e: Exception) {
|
||||
mutableListOf()
|
||||
}
|
||||
}
|
||||
|
||||
fun getLists(context: Context): List<DomainList> {
|
||||
return getAllLists(context).filter { !it.isDeleted }
|
||||
}
|
||||
|
||||
fun getActiveDomains(context: Context): List<String> {
|
||||
return getLists(context).filter { it.isActive }.flatMap { it.domains }.distinct()
|
||||
}
|
||||
|
||||
fun saveLists(context: Context, lists: List<DomainList>) {
|
||||
val listsFile = File(context.filesDir, DOMAIN_LISTS_FILE)
|
||||
val json = gson.toJson(lists)
|
||||
listsFile.writeText(json)
|
||||
}
|
||||
|
||||
fun addList(context: Context, name: String, domains: List<String>): Boolean {
|
||||
val lists = getAllLists(context).toMutableList()
|
||||
val id = name.lowercase().replace(" ", "_")
|
||||
|
||||
val existing = lists.find { it.id == id }
|
||||
|
||||
if (existing != null) {
|
||||
if (existing.isBuiltIn && existing.isDeleted) {
|
||||
val index = lists.indexOf(existing)
|
||||
|
||||
lists[index] = existing.copy(
|
||||
name = name,
|
||||
domains = domains,
|
||||
isDeleted = false,
|
||||
isModified = true,
|
||||
isActive = true
|
||||
)
|
||||
|
||||
saveLists(context, lists)
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
lists.add(
|
||||
DomainList(
|
||||
id = id,
|
||||
name = name,
|
||||
domains = domains,
|
||||
isActive = true,
|
||||
isBuiltIn = false
|
||||
)
|
||||
)
|
||||
|
||||
saveLists(context, lists)
|
||||
return true
|
||||
}
|
||||
|
||||
fun updateList(context: Context, id: String, name: String, domains: List<String>): Boolean {
|
||||
val lists = getAllLists(context).toMutableList()
|
||||
val index = lists.indexOfFirst { it.id == id }
|
||||
|
||||
if (index == -1) return false
|
||||
|
||||
val oldList = lists[index]
|
||||
|
||||
lists[index] = oldList.copy(
|
||||
name = name,
|
||||
domains = domains,
|
||||
isModified = oldList.isBuiltIn
|
||||
)
|
||||
|
||||
saveLists(context, lists)
|
||||
return true
|
||||
}
|
||||
|
||||
fun toggleListActive(context: Context, id: String): Boolean {
|
||||
val lists = getAllLists(context).toMutableList()
|
||||
val index = lists.indexOfFirst { it.id == id }
|
||||
|
||||
if (index == -1) return false
|
||||
|
||||
val list = lists[index]
|
||||
lists[index] = list.copy(isActive = !list.isActive)
|
||||
|
||||
saveLists(context, lists)
|
||||
return true
|
||||
}
|
||||
|
||||
fun deleteList(context: Context, id: String): Boolean {
|
||||
val lists = getAllLists(context).toMutableList()
|
||||
val index = lists.indexOfFirst { it.id == id }
|
||||
|
||||
if (index == -1) return false
|
||||
|
||||
val list = lists[index]
|
||||
|
||||
if (list.isBuiltIn) {
|
||||
lists[index] = list.copy(
|
||||
isDeleted = true,
|
||||
isActive = false
|
||||
)
|
||||
} else {
|
||||
lists.removeAt(index)
|
||||
}
|
||||
|
||||
saveLists(context, lists)
|
||||
return true
|
||||
}
|
||||
|
||||
fun resetLists(context: Context) {
|
||||
val file = File(context.filesDir, DOMAIN_LISTS_FILE)
|
||||
|
||||
if (file.exists()) {
|
||||
file.delete()
|
||||
}
|
||||
|
||||
syncLists(context)
|
||||
}
|
||||
}
|
||||
+21
-6
@@ -1,14 +1,15 @@
|
||||
package io.github.dovecoteescapee.byedpi.utility
|
||||
package io.github.romanvht.byedpi.utility
|
||||
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import androidx.preference.PreferenceManager
|
||||
import io.github.dovecoteescapee.byedpi.data.Command
|
||||
import io.github.romanvht.byedpi.data.Command
|
||||
import com.google.gson.Gson
|
||||
import androidx.core.content.edit
|
||||
|
||||
class HistoryUtils(context: Context) {
|
||||
|
||||
private val sharedPreferences: SharedPreferences = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
private val context = context.applicationContext
|
||||
private val sharedPreferences: SharedPreferences = context.getPreferences()
|
||||
private val historyKey = "byedpi_command_history"
|
||||
private val maxHistorySize = 40
|
||||
|
||||
@@ -16,12 +17,15 @@ class HistoryUtils(context: Context) {
|
||||
if (command.isBlank()) return
|
||||
|
||||
val history = getHistory().toMutableList()
|
||||
val unpinned = history.filter { !it.pinned }
|
||||
val search = history.find { it.text == command }
|
||||
|
||||
if (search == null) {
|
||||
history.add(0, Command(command))
|
||||
if (history.size > maxHistorySize) {
|
||||
history.removeAt(maxHistorySize)
|
||||
if (unpinned.isNotEmpty()) {
|
||||
history.remove(unpinned.last())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,6 +56,12 @@ class HistoryUtils(context: Context) {
|
||||
saveHistory(history)
|
||||
}
|
||||
|
||||
fun editCommand(command: String, newText: String) {
|
||||
val history = getHistory().toMutableList()
|
||||
history.find { it.text == command }?.text = newText
|
||||
saveHistory(history)
|
||||
}
|
||||
|
||||
fun getHistory(): List<Command> {
|
||||
val historyJson = sharedPreferences.getString(historyKey, null)
|
||||
return if (historyJson != null) {
|
||||
@@ -61,9 +71,14 @@ class HistoryUtils(context: Context) {
|
||||
}
|
||||
}
|
||||
|
||||
fun getPinnedHistory(): List<Command> {
|
||||
return getHistory().filter { it.pinned }
|
||||
}
|
||||
|
||||
fun saveHistory(history: List<Command>) {
|
||||
val historyJson = Gson().toJson(history)
|
||||
sharedPreferences.edit().putString(historyKey, historyJson).apply()
|
||||
sharedPreferences.edit { putString(historyKey, historyJson) }
|
||||
ShortcutUtils.update(context)
|
||||
}
|
||||
|
||||
fun clearAllHistory() {
|
||||
@@ -0,0 +1,101 @@
|
||||
package io.github.romanvht.byedpi.utility
|
||||
|
||||
import android.app.Notification
|
||||
import android.app.NotificationChannel
|
||||
import android.app.NotificationManager
|
||||
import android.app.PendingIntent
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.core.app.NotificationCompat
|
||||
import io.github.romanvht.byedpi.R
|
||||
import io.github.romanvht.byedpi.activities.MainActivity
|
||||
import io.github.romanvht.byedpi.data.PAUSE_ACTION
|
||||
import io.github.romanvht.byedpi.data.RESUME_ACTION
|
||||
import io.github.romanvht.byedpi.data.STOP_ACTION
|
||||
|
||||
fun registerNotificationChannel(context: Context, id: String, @StringRes name: Int) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
val manager = context.getSystemService(NotificationManager::class.java) ?: return
|
||||
|
||||
val channel = NotificationChannel(
|
||||
id,
|
||||
context.getString(name),
|
||||
NotificationManager.IMPORTANCE_DEFAULT
|
||||
)
|
||||
channel.enableLights(false)
|
||||
channel.enableVibration(false)
|
||||
channel.setShowBadge(false)
|
||||
|
||||
manager.createNotificationChannel(channel)
|
||||
}
|
||||
}
|
||||
|
||||
fun createConnectionNotification(
|
||||
context: Context,
|
||||
channelId: String,
|
||||
@StringRes title: Int,
|
||||
@StringRes content: Int,
|
||||
service: Class<*>,
|
||||
): Notification =
|
||||
NotificationCompat.Builder(context, channelId)
|
||||
.setSmallIcon(R.drawable.ic_notification)
|
||||
.setSilent(true)
|
||||
.setContentTitle(context.getString(title))
|
||||
.setContentText(context.getString(content))
|
||||
.addAction(0, context.getString(R.string.service_pause_btn),
|
||||
PendingIntent.getService(
|
||||
context,
|
||||
0,
|
||||
Intent(context, service).setAction(PAUSE_ACTION),
|
||||
PendingIntent.FLAG_IMMUTABLE,
|
||||
)
|
||||
)
|
||||
.addAction(0, context.getString(R.string.service_stop_btn),
|
||||
PendingIntent.getService(
|
||||
context,
|
||||
0,
|
||||
Intent(context, service).setAction(STOP_ACTION),
|
||||
PendingIntent.FLAG_IMMUTABLE,
|
||||
)
|
||||
)
|
||||
.setContentIntent(
|
||||
PendingIntent.getActivity(
|
||||
context,
|
||||
0,
|
||||
Intent(context, MainActivity::class.java),
|
||||
PendingIntent.FLAG_IMMUTABLE,
|
||||
)
|
||||
)
|
||||
.build()
|
||||
|
||||
fun createPauseNotification(
|
||||
context: Context,
|
||||
channelId: String,
|
||||
@StringRes title: Int,
|
||||
@StringRes content: Int,
|
||||
service: Class<*>,
|
||||
): Notification =
|
||||
NotificationCompat.Builder(context, channelId)
|
||||
.setSmallIcon(R.drawable.ic_notification)
|
||||
.setSilent(true)
|
||||
.setContentTitle(context.getString(title))
|
||||
.setContentText(context.getString(content))
|
||||
.addAction(0, context.getString(R.string.service_start_btn),
|
||||
PendingIntent.getService(
|
||||
context,
|
||||
0,
|
||||
Intent(context, service).setAction(RESUME_ACTION),
|
||||
PendingIntent.FLAG_IMMUTABLE,
|
||||
)
|
||||
)
|
||||
.setContentIntent(
|
||||
PendingIntent.getActivity(
|
||||
context,
|
||||
0,
|
||||
Intent(context, MainActivity::class.java),
|
||||
PendingIntent.FLAG_IMMUTABLE,
|
||||
)
|
||||
)
|
||||
.build()
|
||||
+20
-6
@@ -1,11 +1,13 @@
|
||||
package io.github.dovecoteescapee.byedpi.utility
|
||||
package io.github.romanvht.byedpi.utility
|
||||
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import androidx.preference.Preference
|
||||
import androidx.preference.PreferenceFragmentCompat
|
||||
import androidx.preference.PreferenceManager
|
||||
import io.github.dovecoteescapee.byedpi.data.Mode
|
||||
import io.github.romanvht.byedpi.data.Mode
|
||||
|
||||
private const val DEFAULT_CMD_ARGS = "-o1 -a1 -r-5+se"
|
||||
|
||||
val PreferenceFragmentCompat.sharedPreferences
|
||||
get() = preferenceScreen.sharedPreferences
|
||||
@@ -13,6 +15,12 @@ val PreferenceFragmentCompat.sharedPreferences
|
||||
fun Context.getPreferences(): SharedPreferences =
|
||||
PreferenceManager.getDefaultSharedPreferences(this)
|
||||
|
||||
fun SharedPreferences.getIntStringNotNull(key: String, defValue: Int): Int =
|
||||
getString(key, defValue.toString())?.toIntOrNull() ?: defValue
|
||||
|
||||
fun SharedPreferences.getLongStringNotNull(key: String, defValue: Long): Long =
|
||||
getString(key, defValue.toString())?.toLongOrNull() ?: defValue
|
||||
|
||||
fun SharedPreferences.getStringNotNull(key: String, defValue: String): String =
|
||||
getString(key, defValue) ?: defValue
|
||||
|
||||
@@ -26,11 +34,17 @@ fun SharedPreferences.getSelectedApps(): List<String> {
|
||||
return getStringSet("selected_apps", emptySet())?.toList() ?: emptyList()
|
||||
}
|
||||
|
||||
fun SharedPreferences.checkIpAndPortInCmd(): Pair<String?, String?> {
|
||||
val cmdEnable = getBoolean("byedpi_enable_cmd_settings", false)
|
||||
if (!cmdEnable) return Pair(null, null)
|
||||
fun SharedPreferences.getCmdEnable(): Boolean {
|
||||
return getBoolean("byedpi_enable_cmd_settings", true)
|
||||
}
|
||||
|
||||
val cmdArgs = getString("byedpi_cmd_args", "")?.let { shellSplit(it) } ?: emptyList()
|
||||
fun SharedPreferences.getCmdArgs(): String {
|
||||
return getStringNotNull("byedpi_cmd_args", DEFAULT_CMD_ARGS)
|
||||
}
|
||||
|
||||
fun SharedPreferences.checkIpAndPortInCmd(): Pair<String?, String?> {
|
||||
if (!getCmdEnable()) return Pair(null, null)
|
||||
val cmdArgs = shellSplit(getCmdArgs())
|
||||
|
||||
fun getArgValue(argsList: List<String>, keys: List<String>): String? {
|
||||
for (i in argsList.indices) {
|
||||
@@ -0,0 +1,256 @@
|
||||
package io.github.romanvht.byedpi.utility
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.util.Log
|
||||
import android.widget.Toast
|
||||
import androidx.appcompat.app.AppCompatDelegate
|
||||
import androidx.core.content.edit
|
||||
import androidx.core.os.LocaleListCompat
|
||||
import com.google.gson.Gson
|
||||
import io.github.romanvht.byedpi.BuildConfig
|
||||
import io.github.romanvht.byedpi.R
|
||||
import io.github.romanvht.byedpi.data.AppSettings
|
||||
|
||||
object SettingsUtils {
|
||||
private const val TAG = "SettingsUtils"
|
||||
private val separatePreferenceKeys = setOf("byedpi_command_history", "selected_apps")
|
||||
|
||||
enum class Section {
|
||||
SETTINGS,
|
||||
HISTORY,
|
||||
APPS,
|
||||
DOMAIN_LISTS
|
||||
}
|
||||
|
||||
fun getCurrentLanguage(context: Context): String {
|
||||
val lang = context.getPreferences().getStringNotNull("language", "system")
|
||||
if (lang != "system") return lang
|
||||
|
||||
val locales = AppCompatDelegate.getApplicationLocales()
|
||||
return locales[0]?.language ?: java.util.Locale.getDefault().language
|
||||
}
|
||||
|
||||
fun setLang(lang: String) {
|
||||
val appLocale = localeByName(lang) ?: LocaleListCompat.getEmptyLocaleList()
|
||||
|
||||
if (AppCompatDelegate.getApplicationLocales().toLanguageTags() != appLocale.toLanguageTags()) {
|
||||
AppCompatDelegate.setApplicationLocales(appLocale)
|
||||
}
|
||||
}
|
||||
|
||||
private fun localeByName(lang: String): LocaleListCompat? = when (lang) {
|
||||
"system" -> LocaleListCompat.getEmptyLocaleList()
|
||||
"ru" -> LocaleListCompat.forLanguageTags("ru")
|
||||
"en" -> LocaleListCompat.forLanguageTags("en")
|
||||
"tr" -> LocaleListCompat.forLanguageTags("tr")
|
||||
"kk" -> LocaleListCompat.forLanguageTags("kk")
|
||||
"vi" -> LocaleListCompat.forLanguageTags("vi")
|
||||
else -> {
|
||||
Log.w(TAG, "Invalid value for language: $lang")
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
fun setTheme(name: String) {
|
||||
val appTheme = themeByName(name) ?: AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM
|
||||
|
||||
if (AppCompatDelegate.getDefaultNightMode() != appTheme) {
|
||||
AppCompatDelegate.setDefaultNightMode(appTheme)
|
||||
}
|
||||
}
|
||||
|
||||
private fun themeByName(name: String): Int? = when (name) {
|
||||
"system" -> AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM
|
||||
"light" -> AppCompatDelegate.MODE_NIGHT_NO
|
||||
"dark" -> AppCompatDelegate.MODE_NIGHT_YES
|
||||
else -> {
|
||||
Log.w(TAG, "Invalid value for app_theme: $name")
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
fun exportSettings(context: Context, uri: Uri, sections: Set<Section>) {
|
||||
try {
|
||||
val prefs = context.getPreferences()
|
||||
|
||||
val export = AppSettings(
|
||||
app = BuildConfig.APPLICATION_ID,
|
||||
version = BuildConfig.VERSION_NAME,
|
||||
history = if (Section.HISTORY in sections) {
|
||||
HistoryUtils(context).getHistory()
|
||||
} else {
|
||||
null
|
||||
},
|
||||
apps = if (Section.APPS in sections) prefs.getSelectedApps() else null,
|
||||
domainLists = if (Section.DOMAIN_LISTS in sections) {
|
||||
DomainListUtils.getAllLists(context)
|
||||
} else {
|
||||
null
|
||||
},
|
||||
settings = if (Section.SETTINGS in sections) {
|
||||
prefs.all.filterKeys { it !in separatePreferenceKeys }
|
||||
} else {
|
||||
null
|
||||
}
|
||||
)
|
||||
|
||||
val json = Gson().toJson(export)
|
||||
|
||||
context.contentResolver.openOutputStream(uri)?.use { outputStream ->
|
||||
outputStream.write(json.toByteArray())
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Failed to export settings", e)
|
||||
Handler(Looper.getMainLooper()).post {
|
||||
Toast.makeText(context, R.string.export_failed, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun readSettings(context: Context, uri: Uri): AppSettings? {
|
||||
return try {
|
||||
val json = context.contentResolver.openInputStream(uri)?.use { inputStream ->
|
||||
inputStream.bufferedReader().readText()
|
||||
} ?: return null
|
||||
|
||||
val settings = Gson().fromJson(json, AppSettings::class.java)
|
||||
if (settings?.app == BuildConfig.APPLICATION_ID) {
|
||||
settings
|
||||
} else {
|
||||
Toast.makeText(context, R.string.import_failed, Toast.LENGTH_LONG).show()
|
||||
null
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Failed to read settings", e)
|
||||
Toast.makeText(context, R.string.import_failed, Toast.LENGTH_SHORT).show()
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
fun getAvailableSections(settings: AppSettings): Set<Section> = buildSet {
|
||||
if (settings.settings != null) add(Section.SETTINGS)
|
||||
if (settings.history != null) add(Section.HISTORY)
|
||||
if (settings.apps != null) add(Section.APPS)
|
||||
if (settings.domainLists != null) add(Section.DOMAIN_LISTS)
|
||||
}
|
||||
|
||||
fun importSettings(
|
||||
context: Context,
|
||||
import: AppSettings,
|
||||
sections: Set<Section>,
|
||||
onRestart: () -> Unit
|
||||
) {
|
||||
try {
|
||||
val prefs = context.getPreferences()
|
||||
|
||||
if (Section.SETTINGS in sections && import.settings != null) {
|
||||
prefs.edit(commit = true) {
|
||||
prefs.all.keys
|
||||
.filter { it !in separatePreferenceKeys }
|
||||
.forEach { remove(it) }
|
||||
|
||||
import.settings
|
||||
.filterKeys { it !in separatePreferenceKeys }
|
||||
.forEach { (key, value) ->
|
||||
when (value) {
|
||||
is Int -> putInt(key, value)
|
||||
is Boolean -> putBoolean(key, value)
|
||||
is String -> putString(key, value)
|
||||
is Float -> putFloat(key, value)
|
||||
is Long -> putLong(key, value)
|
||||
is Double -> {
|
||||
when (value) {
|
||||
value.toInt().toDouble() -> {
|
||||
putInt(key, value.toInt())
|
||||
}
|
||||
value.toLong().toDouble() -> {
|
||||
putLong(key, value.toLong())
|
||||
}
|
||||
else -> {
|
||||
putFloat(key, value.toFloat())
|
||||
}
|
||||
}
|
||||
}
|
||||
is Collection<*> -> {
|
||||
if (value.all { it is String }) {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
putStringSet(key, (value as Collection<String>).toSet())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (Section.APPS in sections && import.apps != null) {
|
||||
prefs.edit(commit = true) { putStringSet("selected_apps", import.apps.toSet()) }
|
||||
}
|
||||
|
||||
if (Section.HISTORY in sections && import.history != null) {
|
||||
HistoryUtils(context).saveHistory(import.history)
|
||||
}
|
||||
|
||||
if (Section.DOMAIN_LISTS in sections && import.domainLists != null) {
|
||||
val normalized = import.domainLists.map {
|
||||
if (it.isBuiltIn) {
|
||||
it.copy(
|
||||
isModified = it.isModified,
|
||||
isDeleted = it.isDeleted
|
||||
)
|
||||
} else {
|
||||
it.copy(
|
||||
isModified = false,
|
||||
isDeleted = false
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
DomainListUtils.saveLists(context, normalized)
|
||||
}
|
||||
|
||||
if (Section.SETTINGS in sections) {
|
||||
val newLang = prefs.getString("language", "system") ?: "system"
|
||||
val newTheme = prefs.getString("app_theme", "system") ?: "system"
|
||||
setLang(newLang)
|
||||
setTheme(newTheme)
|
||||
}
|
||||
|
||||
Handler(Looper.getMainLooper()).post {
|
||||
onRestart()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Failed to import settings", e)
|
||||
Handler(Looper.getMainLooper()).post {
|
||||
Toast.makeText(context, R.string.import_failed, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun resetSettings(context: Context, onRestart: () -> Unit) {
|
||||
try {
|
||||
val prefs = context.getPreferences()
|
||||
|
||||
prefs.edit(commit = true) {
|
||||
clear()
|
||||
}
|
||||
|
||||
HistoryUtils(context).saveHistory(emptyList())
|
||||
DomainListUtils.resetLists(context)
|
||||
|
||||
setLang("system")
|
||||
setTheme("system")
|
||||
|
||||
Handler(Looper.getMainLooper()).post {
|
||||
onRestart()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Failed to reset settings", e)
|
||||
Handler(Looper.getMainLooper()).post {
|
||||
Toast.makeText(context, "Failed to reset settings", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
package io.github.romanvht.byedpi.utility
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.ShortcutInfo
|
||||
import android.content.pm.ShortcutManager
|
||||
import android.graphics.drawable.Icon
|
||||
import android.os.Build
|
||||
import io.github.romanvht.byedpi.R
|
||||
import io.github.romanvht.byedpi.activities.ToggleActivity
|
||||
|
||||
object ShortcutUtils {
|
||||
|
||||
fun update(context: Context) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) {
|
||||
val shortcutManager = context.getSystemService(ShortcutManager::class.java)
|
||||
val shortcuts = mutableListOf<ShortcutInfo>()
|
||||
|
||||
val toggleIntent = Intent(context, ToggleActivity::class.java).apply {
|
||||
action = Intent.ACTION_VIEW
|
||||
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
|
||||
}
|
||||
|
||||
val toggleShortcut = ShortcutInfo.Builder(context, "toggle_service")
|
||||
.setShortLabel(context.getString(R.string.toggle_connect))
|
||||
.setLongLabel(context.getString(R.string.toggle_connect))
|
||||
.setIcon(Icon.createWithResource(context, R.drawable.ic_toggle))
|
||||
.setIntent(toggleIntent)
|
||||
.build()
|
||||
|
||||
shortcuts.add(toggleShortcut)
|
||||
|
||||
if (context.getPreferences().getCmdEnable()) {
|
||||
val history = HistoryUtils(context)
|
||||
val pinned = history.getHistory().filter { it.pinned }.take(3)
|
||||
|
||||
pinned.forEachIndexed { index, strategy ->
|
||||
val strategyIntent = Intent(context, ToggleActivity::class.java).apply {
|
||||
action = Intent.ACTION_VIEW
|
||||
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
|
||||
putExtra("strategy", strategy.text)
|
||||
}
|
||||
|
||||
val fullLabel = strategy.name?.takeIf { it.isNotBlank() } ?: strategy.text
|
||||
val shortLabel = if (fullLabel.length > 15) fullLabel.take(15) + "..." else fullLabel
|
||||
val longLabel = if (fullLabel.length > 30) fullLabel.take(30) + "..." else fullLabel
|
||||
|
||||
val commandShortcut = ShortcutInfo.Builder(context, "strategy_$index")
|
||||
.setShortLabel(shortLabel)
|
||||
.setLongLabel(longLabel)
|
||||
.setIcon(Icon.createWithResource(context, R.drawable.ic_pin))
|
||||
.setIntent(strategyIntent)
|
||||
.build()
|
||||
|
||||
shortcuts.add(commandShortcut)
|
||||
}
|
||||
}
|
||||
|
||||
shortcutManager.dynamicShortcuts = shortcuts
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user