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 |
@@ -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
|
||||
Generated
-6
@@ -1,6 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="AndroidProjectSystem">
|
||||
<option name="providerId" value="com.android.tools.idea.GradleProjectSystem" />
|
||||
</component>
|
||||
</project>
|
||||
-22
@@ -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="21" />
|
||||
</component>
|
||||
</project>
|
||||
Generated
-18
@@ -1,18 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="deploymentTargetSelector">
|
||||
<selectionStates>
|
||||
<SelectionState runConfigName="debug">
|
||||
<option name="selectionMode" value="DROPDOWN" />
|
||||
<DropdownSelection timestamp="2025-08-09T11:47:28.114175400Z">
|
||||
<Target type="DEFAULT_BOOT">
|
||||
<handle>
|
||||
<DeviceId pluginId="LocalEmulator" identifier="path=C:\Users\romanvht\.android\avd\Medium_Phone.avd" />
|
||||
</handle>
|
||||
</Target>
|
||||
</DropdownSelection>
|
||||
<DialogSelection />
|
||||
</SelectionState>
|
||||
</selectionStates>
|
||||
</component>
|
||||
</project>
|
||||
Generated
-13
@@ -1,13 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="DeviceTable">
|
||||
<option name="columnSorters">
|
||||
<list>
|
||||
<ColumnSorterState>
|
||||
<option name="column" value="Name" />
|
||||
<option name="order" value="ASCENDING" />
|
||||
</ColumnSorterState>
|
||||
</list>
|
||||
</option>
|
||||
</component>
|
||||
</project>
|
||||
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="testRunner" value="CHOOSE_PER_TEST" />
|
||||
<option name="externalProjectPath" value="$PROJECT_DIR$" />
|
||||
<option name="gradleJvm" value="#GRADLE_LOCAL_JAVA_HOME" />
|
||||
<option name="modules">
|
||||
<set>
|
||||
<option value="$PROJECT_DIR$" />
|
||||
<option value="$PROJECT_DIR$/app" />
|
||||
</set>
|
||||
</option>
|
||||
</GradleProjectSettings>
|
||||
</option>
|
||||
</component>
|
||||
</project>
|
||||
-10
@@ -1,10 +0,0 @@
|
||||
<component name="InspectionProjectProfileManager">
|
||||
<profile version="1.0">
|
||||
<option name="myName" value="Project Default" />
|
||||
<inspection_tool class="SpellCheckingInspection" enabled="false" level="TYPO" enabled_by_default="false">
|
||||
<option name="processCode" value="true" />
|
||||
<option name="processLiterals" value="true" />
|
||||
<option name="processComments" value="true" />
|
||||
</inspection_tool>
|
||||
</profile>
|
||||
</component>
|
||||
Generated
-6
@@ -1,6 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="KotlinJpsPluginSettings">
|
||||
<option name="version" value="1.9.22" />
|
||||
</component>
|
||||
</project>
|
||||
Generated
-10
@@ -1,10 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectMigrations">
|
||||
<option name="MigrateToGradleLocalJavaHome">
|
||||
<set>
|
||||
<option value="$PROJECT_DIR$" />
|
||||
</set>
|
||||
</option>
|
||||
</component>
|
||||
</project>
|
||||
Generated
-10
@@ -1,10 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ExternalStorageConfigurationManager" enabled="true" />
|
||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="jbr-21" project-jdk-type="JavaSDK">
|
||||
<output url="file://$PROJECT_DIR$/build/classes" />
|
||||
</component>
|
||||
<component name="ProjectType">
|
||||
<option name="id" value="Android" />
|
||||
</component>
|
||||
</project>
|
||||
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
-17
@@ -1,17 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="RunConfigurationProducerService">
|
||||
<option name="ignoredProducers">
|
||||
<set>
|
||||
<option value="com.intellij.execution.junit.AbstractAllInDirectoryConfigurationProducer" />
|
||||
<option value="com.intellij.execution.junit.AllInPackageConfigurationProducer" />
|
||||
<option value="com.intellij.execution.junit.PatternConfigurationProducer" />
|
||||
<option value="com.intellij.execution.junit.TestInClassConfigurationProducer" />
|
||||
<option value="com.intellij.execution.junit.UniqueIdConfigurationProducer" />
|
||||
<option value="com.intellij.execution.junit.testDiscovery.JUnitTestDiscoveryConfigurationProducer" />
|
||||
<option value="org.jetbrains.kotlin.idea.junit.KotlinJUnitRunConfigurationProducer" />
|
||||
<option value="org.jetbrains.kotlin.idea.junit.KotlinPatternConfigurationProducer" />
|
||||
</set>
|
||||
</option>
|
||||
</component>
|
||||
</project>
|
||||
Generated
-12
@@ -1,12 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
||||
<mapping directory="$PROJECT_DIR$/app/src/main/cpp/byedpi" vcs="Git" />
|
||||
<mapping directory="$PROJECT_DIR$/app/src/main/jni/hev-socks5-tunnel" vcs="Git" />
|
||||
<mapping directory="$PROJECT_DIR$/app/src/main/jni/hev-socks5-tunnel/src/core" vcs="Git" />
|
||||
<mapping directory="$PROJECT_DIR$/app/src/main/jni/hev-socks5-tunnel/third-part/hev-task-system" vcs="Git" />
|
||||
<mapping directory="$PROJECT_DIR$/app/src/main/jni/hev-socks5-tunnel/third-part/lwip" vcs="Git" />
|
||||
<mapping directory="$PROJECT_DIR$/app/src/main/jni/hev-socks5-tunnel/third-part/yaml" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
||||
+40
-35
@@ -16,57 +16,62 @@
|
||||
</p>
|
||||
</div>
|
||||
|
||||
An Android application that locally runs ByeDPI and routes all traffic through it.
|
||||
An Android application that runs ByeDPI locally and redirects all traffic through it.
|
||||
|
||||
For stable operation, you may need to adjust the settings. You can read more about different settings in the [ByeDPI documentation](https://github.com/hufrea/byedpi/blob/v0.13/README.md).
|
||||
For stable operation, you may need to adjust the settings. More information about the available options can be found in the [ByeDPI documentation](https://github.com/hufrea/byedpi/blob/main/README.md).
|
||||
|
||||
This application is **not** a VPN. It uses Android's VPN mode to route traffic but does not transmit anything to a remote server. It does not encrypt traffic or hide your IP address.
|
||||
This application is not a VPN. It uses 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 a fork of [ByeDPIAndroid](https://github.com/dovecoteescapee/ByeDPIAndroid).
|
||||
The application has only one official website:
|
||||
https://byebyedpi.xyz
|
||||
|
||||
---
|
||||
|
||||
### Features
|
||||
* Autostart service on device boot
|
||||
* Saving lists of command-line parameters
|
||||
* Improved compatibility with Android TV/BOX
|
||||
|
||||
* Automatically starts the service when the device boots
|
||||
* Saves command-line parameter lists
|
||||
* Improved compatibility with Android TV/BOX devices
|
||||
* Per-app split tunneling
|
||||
* Import/export settings
|
||||
* Settings import and export
|
||||
|
||||
### Usage
|
||||
* To enable auto-start, activate the option in settings.
|
||||
* It is recommended to connect to the VPN once to accept the request.
|
||||
* After that, upon device startup, the application will automatically launch the service based on settings (VPN/Proxy).
|
||||
* If you are using an Android TV/BOX and your Ethernet connection drops when connecting, enable whitelist mode and specify the apps that should work through the VPN (e.g., YouTube).
|
||||
* Comprehensive instruction from the community [ByeByeDPI-Manual (En)](https://github.com/BDManual/ByeByeDPI-Manual)
|
||||
|
||||
### How to use ByeByeDPI with AdGuard?
|
||||
* Start ByeByeDPI in proxy mode.
|
||||
* Add ByeByeDPI to AdGuard exclusions on the "App Management" tab.
|
||||
* In AdGuard settings, specify the proxy:
|
||||
```plaintext
|
||||
Proxy Type: SOCKS5
|
||||
Host: 127.0.0.1
|
||||
Port: 1080 (default)
|
||||
```
|
||||
* Enable the corresponding option in the settings to use automatic startup.
|
||||
* It is recommended to connect to the VPN once in order to accept the permission request.
|
||||
* After that, the application will automatically start the service when the device boots, depending on the selected mode: VPN or Proxy.
|
||||
* Comprehensive community guide: [ByeByeDPI-Manual](https://byebyedpi.xyz)
|
||||
|
||||
### Building
|
||||
1. Clone the repository with submodules:
|
||||
```bash
|
||||
git clone --recurse-submodules
|
||||
```
|
||||
2. Run the build script from the root of the repository:
|
||||
```bash
|
||||
./gradlew assembleRelease
|
||||
```
|
||||
3. The APK will be in `app/build/outputs/apk/release/`
|
||||
|
||||
> P.S.: hev_socks5_tunnel will not build under Windows, you will need to use WSL
|
||||
1. Clone the repository with its submodules:
|
||||
|
||||
```bash
|
||||
git clone --recurse-submodules
|
||||
```
|
||||
|
||||
2. Run the build script from the repository root:
|
||||
|
||||
```bash
|
||||
./gradlew assembleRelease
|
||||
```
|
||||
|
||||
3. The APK will be located in:
|
||||
`app/build/outputs/apk/release/`
|
||||
|
||||
> Note: hev_socks5_tunnel cannot be built on Windows. You will need to use WSL.
|
||||
|
||||
### Signing Certificate Hash
|
||||
|
||||
### Signature Hash
|
||||
SHA-256:
|
||||
`77:45:10:75:AC:EA:40:64:06:47:5D:74:D4:59:88:3A:49:A6:40:51:FA:F3:2E:42:F7:18:F3:F9:77:7A:8D:FB`
|
||||
|
||||
### Dependencies
|
||||
- [ByeDPI](https://github.com/hufrea/byedpi)
|
||||
- [hev-socks5-tunnel](https://github.com/heiher/hev-socks5-tunnel)
|
||||
|
||||
* [ByeDPI](https://github.com/hufrea/byedpi)
|
||||
* [hev-socks5-tunnel](https://github.com/heiher/hev-socks5-tunnel)
|
||||
|
||||
### Acknowledgements
|
||||
|
||||
* [hufrea](https://github.com/hufrea) — for [ByeDPI](https://github.com/hufrea/byedpi)
|
||||
* [dovecoteescapee](https://github.com/dovecoteescapee) — for the original implementation of [ByeDPIAndroid](https://github.com/dovecoteescapee/ByeDPIAndroid)
|
||||
|
||||
+35
-35
@@ -16,62 +16,62 @@
|
||||
</p>
|
||||
</div>
|
||||
|
||||
ByeDPI'yi yerel olarak çalıştıran ve tüm trafiği bunun üzerinden yönlendiren bir Android uygulaması.
|
||||
ByeDPI’yi yerel olarak çalıştıran ve tüm trafiği üzerinden yönlendiren bir Android uygulaması.
|
||||
|
||||
Kararlı bir çalışma için ayarları yapmanız gerekebilir. Farklı ayarlar hakkında daha fazla bilgiye [ByeDPI dökümantasyonundan](https://github.com/hufrea/byedpi/blob/v0.13/README.md) ulaşabilirsiniz.
|
||||
Kararlı çalışması için ayarların değiştirilmesi gerekebilir. Çeşitli ayarlar hakkında daha fazla bilgiyi [ByeDPI belgelerinde](https://github.com/hufrea/byedpi/blob/main/README.md) bulabilirsiniz.
|
||||
|
||||
Bu uygulama **VPN** değildir. Trafiği yönlendirmek için Android'in VPN modunu kullanır ancak herhangi bir veriyi uzak bir sunucuya iletmez. Trafiği şifrelemez veya IP adresinizi gizlemez.
|
||||
Bu uygulama bir VPN değildir. Trafiği yönlendirmek için Android’in VPN modunu kullanır, ancak hiçbir veriyi uzak bir sunucuya göndermez. Trafiği şifrelemez ve IP adresinizi gizlemez.
|
||||
|
||||
Bu uygulama, [ByeDPIAndroid](https://github.com/dovecoteescapee/ByeDPIAndroid) uygulamasının bir çatallamasıdır.
|
||||
Uygulamanın tek resmî web sitesi:
|
||||
https://byebyedpi.xyz
|
||||
|
||||
---
|
||||
|
||||
### Özellikler
|
||||
* Cihaz başlatıldığında hizmetin otomatik başlatılması
|
||||
* Komut satırı parametrelerinin listelerinin kaydedilmesi
|
||||
|
||||
* Cihaz açıldığında hizmeti otomatik başlatma
|
||||
* Komut satırı parametre listelerini kaydetme
|
||||
* Android TV/BOX ile geliştirilmiş uyumluluk
|
||||
* Uygulama başına bölünmüş tünelleme
|
||||
* Ayarları içe/dışa aktarma
|
||||
* Uygulama bazında bölünmüş tünelleme
|
||||
* Ayarları içe ve dışa aktarma
|
||||
|
||||
### Kullanım
|
||||
* Otomatik başlatmayı etkinleştirmek için ayarlarda seçeneği aktifleştirin.
|
||||
* İlk başta VPN'e bağlanarak isteği kabul etmeniz önerilir.
|
||||
* Bundan sonra, cihaz başlatıldığında, uygulama ayarlara göre (VPN/Proxy) hizmeti otomatik olarak başlatacaktır.
|
||||
* Android TV/BOX kullanıyorsanız ve Ethernet bağlantınız VPN'e bağlanırken kopuyorsa, beyaz liste modunu etkinleştirip VPN üzerinden çalışması gereken uygulamaları belirleyin (örneğin, Discord).
|
||||
* Topluluktan kapsamlı talimatlar [ByeByeDPI-Manual (İngilizce)](https://github.com/BDManual/ByeByeDPI-Manual)
|
||||
|
||||
### Türkiye İle İlgili
|
||||
* Uygulama Türkiyede uygulanan DPI'ı aşmak için şuanlık yeterlidir. İlk başta çalıştırdığınızda uygulama DPI'ı aşamayabilir. UI editöründen rastgele taktikler deneyebilirsiniz veya şuan Deneysel özellik olan proxy modundan bazı argümanlar alıp onları Komut satırı editöründe deneyebilirsiniz.
|
||||
* Türkiye ile alakalı destek için Discord: [nyaex](https://github.com/nyaexx)
|
||||
* Otomatik başlatmanın çalışması için ayarlardaki ilgili seçeneği etkinleştirin.
|
||||
* İzin isteğini kabul etmek için VPN bağlantısını en az bir kez başlatmanız önerilir.
|
||||
* Bundan sonra uygulama, cihaz açıldığında ayarlara bağlı olarak hizmeti VPN veya Proxy modunda otomatik olarak başlatır.
|
||||
* Topluluk tarafından hazırlanan kapsamlı kılavuz: [ByeByeDPI-Manual](https://byebyedpi.xyz)
|
||||
|
||||
### Derleme
|
||||
|
||||
### ByeByeDPI'yi AdGuard ile nasıl kullanırım?
|
||||
* ByeByeDPI'yi proxy modunda başlatın.
|
||||
* ByeByeDPI'yi AdGuard dışlamalarına "Uygulama Yönetimi" sekmesinde ekleyin.
|
||||
* AdGuard ayarlarında, proxy'i belirtin:
|
||||
```plaintext
|
||||
Proxy Türü: SOCKS5
|
||||
Host: 127.0.0.1
|
||||
Port: 1080 (varsayılan)
|
||||
```
|
||||
1. Depoyu alt modüllerle birlikte klonlayın:
|
||||
|
||||
### Oluşturma
|
||||
1. Depoyu alt modüllerle klonlayın:
|
||||
```bash
|
||||
git clone --recurse-submodules
|
||||
```
|
||||
2. Depo kökünden derleme betiğini çalıştırın:
|
||||
```bash
|
||||
./gradlew assemblyRelease
|
||||
```
|
||||
3. APK `app/build/outputs/apk/release/` dizininde olacaktır
|
||||
|
||||
> Not: hev_socks5_tunnel Windows altında derlenmeyecektir, WSL kullanmanız gerekecektir
|
||||
2. Derleme betiğini deponun kök dizininden çalıştırın:
|
||||
|
||||
```bash
|
||||
./gradlew assembleRelease
|
||||
```
|
||||
|
||||
3. APK dosyası şu dizinde bulunacaktır:
|
||||
`app/build/outputs/apk/release/`
|
||||
|
||||
> Not: hev_socks5_tunnel Windows üzerinde derlenemez. WSL kullanmanız gerekir.
|
||||
|
||||
### İmza Özeti
|
||||
|
||||
SHA-256:
|
||||
`77:45:10:75:AC:EA:40:64:06:47:5D:74:D4:59:88:3A:49:A6:40:51:FA:F3:2E:42:F7:18:F3:F9:77:7A:8D:FB`
|
||||
|
||||
### Bağımlılıklar
|
||||
- [ByeDPI](https://github.com/hufrea/byedpi)
|
||||
- [hev-socks5-tunnel](https://github.com/heiher/hev-socks5-tunnel)
|
||||
|
||||
* [ByeDPI](https://github.com/hufrea/byedpi)
|
||||
* [hev-socks5-tunnel](https://github.com/heiher/hev-socks5-tunnel)
|
||||
|
||||
### Teşekkürler
|
||||
|
||||
* [hufrea](https://github.com/hufrea) — [ByeDPI](https://github.com/hufrea/byedpi) için
|
||||
* [dovecoteescapee](https://github.com/dovecoteescapee) — [ByeDPIAndroid](https://github.com/dovecoteescapee/ByeDPIAndroid) uygulamasının ilk sürümü için
|
||||
|
||||
@@ -18,11 +18,11 @@
|
||||
|
||||
Приложение для Android, которое локально запускает ByeDPI и перенаправляет весь трафик через него.
|
||||
|
||||
Для стабильной работы может потребоваться изменить настройки. Подробнее о различных настройках можно прочитать в [документации ByeDPI](https://github.com/hufrea/byedpi/blob/v0.13/README.md).
|
||||
Для стабильной работы может потребоваться изменить настройки. Подробнее о различных настройках можно прочитать в [документации ByeDPI](https://github.com/hufrea/byedpi/blob/main/README.md).
|
||||
|
||||
Приложение не является VPN. Оно использует VPN-режим на Android для перенаправления трафика, но не передает ничего на удаленный сервер. Оно не шифрует трафик и не скрывает ваш IP-адрес.
|
||||
|
||||
Приложения является форком [ByeDPIAndroid](https://github.com/dovecoteescapee/ByeDPIAndroid)
|
||||
У приложения есть единственный официальный сайт -> https://byebyedpi.xyz
|
||||
|
||||
---
|
||||
|
||||
@@ -37,18 +37,7 @@
|
||||
* Для работы автозапуска активируйте пункт в настройках.
|
||||
* Рекомендуется подключится один раз к VPN, чтобы принять запрос.
|
||||
* После этого, при загрузке устройства, приложение автоматически запустит сервис в зависимости от настроек (VPN/Proxy)
|
||||
* Если у вас Android TV/BOX, и при подключении пропадает соединение по Ethernet, активируйте режим белого списка и укажите нужные приложения, которые должны работать через VPN (например, YouTube)
|
||||
* Комплексная инструкция от комьюнити [ByeByeDPI-Manual](https://github.com/BDManual/ByeByeDPI-Manual)
|
||||
|
||||
### Как использовать ByeByeDPI вместе с AdGuard?
|
||||
* Запустите ByeByeDPI в режиме прокси.
|
||||
* Добавьте ByeByeDPI в исключения AdGuard на вкладке "Управление приложениями".
|
||||
* В настройках AdGuard укажите прокси:
|
||||
```plaintext
|
||||
Тип прокси: SOCKS5
|
||||
Хост: 127.0.0.1
|
||||
Порт: 1080 (по умолчанию)
|
||||
```
|
||||
* Комплексная инструкция от комьюнити [ByeByeDPI-Manual](https://byebyedpi.xyz)
|
||||
|
||||
### Сборка
|
||||
1. Клонируйте репозиторий с сабмодулями:
|
||||
@@ -64,9 +53,13 @@ git clone --recurse-submodules
|
||||
> P.S.: hev_socks5_tunnel не соберется под Windows, вам нужно будет использовать WSL
|
||||
|
||||
### Хеш подписи
|
||||
SHA-256:
|
||||
SHA-256:
|
||||
`77:45:10:75:AC:EA:40:64:06:47:5D:74:D4:59:88:3A:49:A6:40:51:FA:F3:2E:42:F7:18:F3:F9:77:7A:8D:FB`
|
||||
|
||||
### Зависимости
|
||||
- [ByeDPI](https://github.com/hufrea/byedpi)
|
||||
- [hev-socks5-tunnel](https://github.com/heiher/hev-socks5-tunnel)
|
||||
|
||||
### Благодарность
|
||||
- [hufrea](https://github.com/hufrea) - за [ByeDPI](https://github.com/hufrea/byedpi)
|
||||
- [dovecoteescapee](https://github.com/dovecoteescapee) - за изначальную реализацию [ByeDPIAndroid](https://github.com/dovecoteescapee/ByeDPIAndroid)
|
||||
|
||||
@@ -8,7 +8,7 @@ plugins {
|
||||
val abis = setOf("armeabi-v7a", "arm64-v8a", "x86", "x86_64")
|
||||
|
||||
android {
|
||||
namespace = "io.github.dovecoteescapee.byedpi"
|
||||
namespace = "io.github.romanvht.byedpi"
|
||||
compileSdk = 36
|
||||
|
||||
defaultConfig {
|
||||
@@ -16,8 +16,8 @@ android {
|
||||
minSdk = 21
|
||||
//noinspection OldTargetApi
|
||||
targetSdk = 34
|
||||
versionCode = 1690
|
||||
versionName = "1.6.9"
|
||||
versionCode = 1770
|
||||
versionName = "1.7.7"
|
||||
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
|
||||
@@ -28,6 +28,7 @@ android {
|
||||
|
||||
buildFeatures {
|
||||
buildConfig = true
|
||||
viewBinding = true
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
@@ -35,6 +36,7 @@ android {
|
||||
buildConfigField("String", "VERSION_NAME", "\"${defaultConfig.versionName}\"")
|
||||
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
|
||||
isMinifyEnabled = true
|
||||
isShrinkResources = true
|
||||
}
|
||||
debug {
|
||||
buildConfigField("String", "VERSION_NAME", "\"${defaultConfig.versionName}-debug\"")
|
||||
@@ -59,10 +61,6 @@ android {
|
||||
}
|
||||
}
|
||||
|
||||
buildFeatures {
|
||||
viewBinding = true
|
||||
}
|
||||
|
||||
// https://android.izzysoft.de/articles/named/iod-scan-apkchecks?lang=en#blobs
|
||||
dependenciesInfo {
|
||||
// Disables dependency metadata when building APKs.
|
||||
@@ -90,8 +88,6 @@ dependencies {
|
||||
implementation("androidx.lifecycle:lifecycle-service:2.9.4")
|
||||
implementation("com.google.android.material:material:1.13.0")
|
||||
implementation("com.google.code.gson:gson:2.13.2")
|
||||
implementation("com.takisoft.preferencex:preferencex:1.1.0")
|
||||
implementation("com.squareup.okhttp3:okhttp:5.3.2")
|
||||
testImplementation("junit:junit:4.13.2")
|
||||
androidTestImplementation("androidx.test.ext:junit:1.3.0")
|
||||
androidTestImplementation("androidx.test.espresso:espresso-core:3.7.0")
|
||||
|
||||
Vendored
+34
-2
@@ -1,2 +1,34 @@
|
||||
-keep,allowoptimization class io.github.dovecoteescapee.byedpi.** { *; }
|
||||
-dontobfuscate
|
||||
# Keep JNI methods
|
||||
-keepclasseswithmembernames class * {
|
||||
native <methods>;
|
||||
}
|
||||
|
||||
-keep class io.github.romanvht.byedpi.core.ByeDpiProxy { *; }
|
||||
|
||||
-keep,allowoptimization class io.github.romanvht.byedpi.core.TProxyService { *; }
|
||||
-keep,allowoptimization class io.github.romanvht.byedpi.activities.** { *; }
|
||||
-keep,allowoptimization class io.github.romanvht.byedpi.services.** { *; }
|
||||
-keep,allowoptimization class io.github.romanvht.byedpi.receiver.** { *; }
|
||||
|
||||
-keep class io.github.romanvht.byedpi.fragments.** {
|
||||
<init>();
|
||||
}
|
||||
|
||||
-keep,allowoptimization class io.github.romanvht.byedpi.data.** {
|
||||
<fields>;
|
||||
}
|
||||
|
||||
-keepattributes Signature
|
||||
-keepattributes *Annotation*
|
||||
|
||||
-repackageclasses 'ru.romanvht'
|
||||
-renamesourcefileattribute ''
|
||||
-keepattributes SourceFile,InnerClasses,EnclosingMethod,Signature,RuntimeVisibleAnnotations,*Annotation*,*Parcelable*
|
||||
-allowaccessmodification
|
||||
-overloadaggressively
|
||||
-optimizationpasses 5
|
||||
-verbose
|
||||
-dontusemixedcaseclassnames
|
||||
-adaptclassstrings
|
||||
-adaptresourcefilecontents **.xml,**.json
|
||||
-adaptresourcefilenames **.xml,**.json
|
||||
+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,11 @@
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SYSTEM_EXEMPTED" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
|
||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
||||
<uses-permission android:name="android.permission.QUICKBOOT_POWERON" />
|
||||
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
|
||||
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE"
|
||||
@@ -73,25 +76,22 @@
|
||||
|
||||
<service android:name=".services.ByeDpiVpnService"
|
||||
android:permission="android.permission.BIND_VPN_SERVICE"
|
||||
android:foregroundServiceType="specialUse"
|
||||
android:foregroundServiceType="systemExempted"
|
||||
android:exported="false">
|
||||
<intent-filter>
|
||||
<action android:name="android.net.VpnService"/>
|
||||
</intent-filter>
|
||||
<meta-data
|
||||
android:name="android.net.VpnService.SUPPORTS_ALWAYS_ON"
|
||||
android:value="false" />
|
||||
android:value="true" />
|
||||
<property
|
||||
android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE"
|
||||
android:name="android.app.PROPERTY_SYSTEM_EXEMPTED_FGS_SUBTYPE"
|
||||
android:value="vpn" />
|
||||
</service>
|
||||
|
||||
<service android:name=".services.ByeDpiProxyService"
|
||||
android:foregroundServiceType="specialUse"
|
||||
android:foregroundServiceType="dataSync"
|
||||
android:exported="false">
|
||||
<property
|
||||
android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE"
|
||||
android:value="proxy" />
|
||||
</service>
|
||||
|
||||
<service
|
||||
@@ -123,4 +123,4 @@
|
||||
</receiver>
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
</manifest>
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
rutracker.org
|
||||
nyaa.si
|
||||
rutor.org
|
||||
nnmclub.to
|
||||
speedtest.net
|
||||
ookla.com
|
||||
@@ -0,0 +1,16 @@
|
||||
snapchat.com
|
||||
snap.com
|
||||
linkedin.com
|
||||
facebook.com
|
||||
fb.com
|
||||
fb.me
|
||||
fbcdn.net
|
||||
messenger.com
|
||||
meta.com
|
||||
instagram.com
|
||||
static.cdninstagram.com
|
||||
proton.me
|
||||
medium.com
|
||||
x.com
|
||||
twitter.com
|
||||
soundcloud.com
|
||||
@@ -1,57 +1,57 @@
|
||||
-f-200 -Qr -s3:5+sm -a1 -As -d1 -s4+sm -s8+sh -f-300 -d6+sh -a1 -At,r,s -o2 -f-30 -As -r5 -Mh -r6+sh -f-250 -s2:7+s -s3:6+sm -a1 -At,r,s -s3:5+sm -s6+s -s7:9+s -q30+sm -a1
|
||||
-d1 -d3+s -s6+s -d9+s -s12+s -d15+s -s20+s -d25+s -s30+s -d35+s -r1+s -S -a1 -As -d1 -d3+s -s6+s -d9+s -s12+s -d15+s -s20+s -d25+s -s30+s -d35+s -S -a1
|
||||
-q2 -s2 -s3+s -r3 -s4 -r4 -s5+s -r5+s -s6 -s7+s -r8 -s9+s -Qr -Mh,d,r -a1 -At,r -s2+s -r2 -d2 -s3 -r3 -r4 -s4 -d5+s -r5 -d6 -s7+s -d7 -a1
|
||||
-o1 -d1 -a1 -At,r,s -s1 -d1 -s5+s -s10+s -s15+s -s20+s -r1+s -S -a1 -As -s1 -d1 -s5+s -s10+s -s15+s -s20+s -S -a1
|
||||
-n {sni} -Qr -f-204 -s1:5+sm -a1 -As -d1 -s3+s -s5+s -q7 -a1 -As -o2 -f-43 -a1 -As -r5 -Mh -s1:5+s -s3:7+sm -a1
|
||||
-n {sni} -Qr -f-205 -a1 -As -s1:3+sm -a1 -As -s5:8+sm -a1 -As -d3 -q7 -o2 -f-43 -f-85 -f-165 -r5 -Mh -a1
|
||||
-d1+s -s50+s -a1 -As -f20 -r2+s -a1 -At -d2 -s1+s -s5+s -s10+s -s15+s -s25+s -s35+s -s50+s -s60+s -a1
|
||||
-o1 -a1 -At,r,s -f-1 -a1 -At,r,s -d1:11+sm -S -a1 -At,r,s -n {sni} -Qr -f1 -d1:11+sm -s1:11+sm -S -a1
|
||||
-d1 -s1 -q1 -Y -a1 -Ar -s5 -o1+s -d3+s -s6+s -d9+s -s12+s -d15+s -s20+s -d25+s -s30+s -d35+s -a1
|
||||
-s1 -o1 -a1 -Y -Ar -s5 -o1+s -a1 -At -f-1 -r1+s -a1 -As -s1 -o1+s -s-1 -a1
|
||||
-s1 -d1 -a1 -Y -Ar -d5 -o1+s -a1 -At -f-1 -r1+s -a1 -As -d1 -o1+s -s-1 -a1
|
||||
-d1 -s1 -q1 -a1 -Ar -s5 -o1+s -d3+s -s6+s -d9+s -s12+s -d15+s -s20+s -d25+s -s30+s -d35+s -a1
|
||||
-f1+nme -t6 -a1 -As -n {sni} -Qr -s1:6+sm -a1 -As -s5:12+sm -a1 -As -d3 -q7 -r6 -Mh -a1
|
||||
-d1 -s1+s -d3+s -s6+s -d9+s -s12+s -d15+s -s20+s -d25+s -s30+s -d35+s -a1
|
||||
-s1 -q1 -a1 -Y -Ar -a1 -s5 -o2 -At -f-1 -r1+s -a1 -As -s1 -o1+s -s-1 -a1
|
||||
-s1 -q1 -a1 -Ar -s5 -o1+s -a1 -At -f-1 -d1+s -a1 -As -s1 -o1+s -s-1 -a1
|
||||
-s1 -q1 -a1 -Ar -s5 -o2 -a1 -At -f-1 -r1+s -a1 -As -s1 -o1+s -s-1 -a1
|
||||
-d1 -s1+s -d1+s -s3+s -d6+s -s12+s -d14+s -s20+s -d24+s -s30+s -a1
|
||||
-s1 -q1 -a1 -Y -At -a1 -S -f-1 -r1+s -a1 -As -d1+s -O1 -s29+s -a1
|
||||
-o1 -a1 -At,r,s -f-1 -a1 -Ar,s -o1 -a1 -At -r1+s -f-1 -t6 -a1
|
||||
-d1 -s1+s -s3+s -s6+s -s9+s -s12+s -s15+s -s20+s -s30+s -a1
|
||||
-d1 -d3+s -s6+s -d6+s -s7+s -d8+s -s10+s -a1 -t12 -At,s -r3
|
||||
-f1 -t5 -n {sni} -q3+h -Qr -f2 -q1 -r1+s -t15 -q1 -o2 -a1
|
||||
-n {sni} -d2:5:2+h -f-3 -r2+sm -o2 -o50+s -r2+s -f-4 -a1
|
||||
-f-1 -Qr -s1+sm -d3+s -s5+sm -o2 -a1 -As -r1+s -d8+s -a1
|
||||
-r-1+s -o20+sm -s3:7+sm -d5:3+sm -f300+s -Qr -f-1 -a1
|
||||
-o2 -O4 -s1 -q1 -a1 -Ar -s5 -o1+s -f1+s -r20+s -a1
|
||||
-o1 -r-5+se -a1 -At,r,s -d1 -n {sni} -Qr -f-1 -a1
|
||||
-s1 -d1 -r1+s -a1 -Ar -o1 -a1 -At -f-1 -r1+s -a1
|
||||
-s1 -q1 -r1+s -a1 -Ar -o1 -a1 -At -f-1 -r1+s -a1
|
||||
-s1 -o1 -r1+s -a1 -Ar -o1 -a1 -At -f-1 -r1+s -a1
|
||||
--fake -1 --ttl 8 --split 1+s --disorder 3+s -a1
|
||||
-n {sni} -Qr -f6+nr -d2 -d11 -f9+hm -o3 -t7 -a1
|
||||
-s1 -d1 -o1 -a1 -Ar -o3 -a1 -At -f-1 -r1+s -a1
|
||||
-r5+s -s25+s -a1 -At,r,s -s50 -r5+s -s50+s -a1
|
||||
-d1 -d3+s -s6+s -d9+s -s20+s -d25+s -s30+s -a1
|
||||
-d9+s -q20+s -s25+s -t5 -a1 -At,r,s -r1+h -a1
|
||||
-q1+s -s29+s -s30+s -s14+s -o5+s -f-1 -S -a1
|
||||
-d9+s -q20+s -s 25+s -t5 -At,r,s -r1+h -a1
|
||||
-s1 -o1 -a1 -Ar -o1 -a1 -At -f-1 -r1+s -a1
|
||||
-d1 -s1+s -r1+s -e1 -m1 -o1+s -f-1 -t2 -a1
|
||||
-d1 -o1 -a1 -Ar -o1 -a1 -At -f-1 -r1+s -a1
|
||||
-d1 -s4 -d8 -s1+s -d5+s -s10+s -d20+s -a1
|
||||
-f-1 -n {sni} -Qr -s2+s -r3 -o20 -t4 -a1
|
||||
-n {sni} -Qr -d5+sm -f3+sm -o2 -t4 -a1
|
||||
-o1 -a1 -Ar -q1 -a1 -At -f-1 -r1+s -a1
|
||||
-q1 -a1 -Ar -o1 -a1 -At -f-1 -r1+s -a1
|
||||
-s1 -q1 -Y -a1 -At,r,s -f-1 -r1+s -a1
|
||||
-s1 -o1 -Y -a1 -At,r,s -f-1 -r1+s -a1
|
||||
-s1 -d1 -Y -a1 -At,r,s -f-1 -r1+s -a1
|
||||
-n {sni} -Qr -f209 -s1+sm -R1-3 -a1
|
||||
-s1 -q1 -a1 -At,r,s -f-1 -r1+s -a1
|
||||
-s1 -o1 -a1 -At,r,s -f-1 -r1+s -a1
|
||||
-s1 -d1 -a1 -At,r,s -f-1 -r1+s -a1
|
||||
-s4+sn -r9+s -Qr -n {sni} -S -a1
|
||||
-o1 -d1 -r1+s -S -s1+s -d3+s -a1
|
||||
-d1 -r1+s -f-1 -S -t8 -o3+s -a1
|
||||
-q1+s -s29+s -o5+s -f-1 -S -a1
|
||||
-n {sni} -Qr -m2 -f-1 -d7 -a1
|
||||
-d1 -s1+s -r1+s -f-1 -t8 -a1
|
||||
-o1 -a1 -An -f1+nme -t6 -a1
|
||||
-n {sni} -Qr -f-1 -r1+s -a1
|
||||
-n {sni} -Qr -d1:3 -f-1 -a1
|
||||
-s1 -d3+s -a1 -At -r1+s -a1
|
||||
-f-1 -t8 -n {sni} -s1+s -a1
|
||||
-n {sni} -Qr -d1 -f-1 -a1
|
||||
-f64+se -n {sni} -t5 -a1
|
||||
-o1 -a1 -At,r,s -d1 -a1
|
||||
-q1 -a1 -At,r,s -d1 -a1
|
||||
-n {sni} -Qr -d1 -f-1
|
||||
-d1+s -o2 -s5 -r5 -a1
|
||||
-r8 -o2 -s7 -q4+s -a1
|
||||
-o1 -f-1 -r-5+se -a1
|
||||
-d6+s -q4+hm -o2 -a1
|
||||
-s1+s -d3+s -a1
|
||||
-s1 -f-1 -S -a1
|
||||
-s5+s -s35+s -m4 -a1
|
||||
-f-1+sm -t7 -m2 -a1
|
||||
-o1 -r-5+se -a1
|
||||
-o1+s -d3+s -a1
|
||||
-o1 -s4 -s6 -a1
|
||||
-q1 -r25+s -a1
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
telegram.org
|
||||
core.telegram.org
|
||||
web.telegram.org
|
||||
webk.telegram.org
|
||||
my.telegram.org
|
||||
translations.telegram.org
|
||||
instantview.telegram.org
|
||||
blog.telegram.org
|
||||
comments.telegram.org
|
||||
verify.telegram.org
|
||||
login.telegram.org
|
||||
auth.telegram.org
|
||||
api.telegram.org
|
||||
promo.telegram.org
|
||||
desktop.telegram.org
|
||||
macos.telegram.org
|
||||
ios.telegram.org
|
||||
android.telegram.org
|
||||
reactions.telegram.org
|
||||
claims.telegram.org
|
||||
x.telegram.org
|
||||
help.telegram.org
|
||||
docs.telegram.org
|
||||
schema.telegram.org
|
||||
dev.telegram.org
|
||||
contest.telegram.org
|
||||
premium.telegram.org
|
||||
settings.telegram.org
|
||||
qr.telegram.org
|
||||
stickers.telegram.org
|
||||
emoji.telegram.org
|
||||
themes.telegram.org
|
||||
donate.telegram.org
|
||||
fragment.telegram.org
|
||||
ton.telegram.org
|
||||
wallet.telegram.org
|
||||
pay.telegram.org
|
||||
telegram.me
|
||||
telegram.dog
|
||||
telegra.ph
|
||||
telesco.pe
|
||||
web.telegram.me
|
||||
zws1.web.telegram.org
|
||||
zws2.web.telegram.org
|
||||
zws1.web.telegram.me
|
||||
zws2.web.telegram.me
|
||||
venus.web.telegram.org
|
||||
pluto.web.telegram.org
|
||||
aurora.web.telegram.org
|
||||
vesta.web.telegram.org
|
||||
voice.telegram.org
|
||||
cdn.telegram.org
|
||||
@@ -0,0 +1,8 @@
|
||||
roblox.com
|
||||
wattpad.com
|
||||
pastebin.com
|
||||
4shared.com
|
||||
wikileaks.org
|
||||
bitly.com
|
||||
cutt.ly
|
||||
t2m.io
|
||||
@@ -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: 9938d7435d...ba532298de
@@ -1,2 +1,2 @@
|
||||
void clear_params(void);
|
||||
void clear_params(char *line, char **argv);
|
||||
int main(int argc, char **argv);
|
||||
|
||||
@@ -14,7 +14,6 @@ static int g_proxy_running = 0;
|
||||
|
||||
struct params default_params = {
|
||||
.await_int = 10,
|
||||
.cache_ttl = 100800,
|
||||
.ipv6 = 1,
|
||||
.resolve = 1,
|
||||
.udp = 1,
|
||||
@@ -30,12 +29,12 @@ struct params default_params = {
|
||||
};
|
||||
|
||||
void reset_params(void) {
|
||||
clear_params();
|
||||
clear_params(NULL, NULL);
|
||||
params = default_params;
|
||||
}
|
||||
|
||||
JNIEXPORT jint JNICALL
|
||||
Java_io_github_dovecoteescapee_byedpi_core_ByeDpiProxy_jniStartProxy(JNIEnv *env, __attribute__((unused)) jobject thiz, jobjectArray args) {
|
||||
Java_io_github_romanvht_byedpi_core_ByeDpiProxy_jniStartProxy(JNIEnv *env, __attribute__((unused)) jobject thiz, jobjectArray args) {
|
||||
if (g_proxy_running) {
|
||||
LOG(LOG_S, "proxy already running");
|
||||
return -1;
|
||||
@@ -82,7 +81,7 @@ Java_io_github_dovecoteescapee_byedpi_core_ByeDpiProxy_jniStartProxy(JNIEnv *env
|
||||
}
|
||||
|
||||
JNIEXPORT jint JNICALL
|
||||
Java_io_github_dovecoteescapee_byedpi_core_ByeDpiProxy_jniStopProxy(__attribute__((unused)) JNIEnv *env, __attribute__((unused)) jobject thiz) {
|
||||
Java_io_github_romanvht_byedpi_core_ByeDpiProxy_jniStopProxy(__attribute__((unused)) JNIEnv *env, __attribute__((unused)) jobject thiz) {
|
||||
LOG(LOG_S, "send shutdown to proxy");
|
||||
|
||||
if (!g_proxy_running) {
|
||||
@@ -97,7 +96,7 @@ Java_io_github_dovecoteescapee_byedpi_core_ByeDpiProxy_jniStopProxy(__attribute_
|
||||
}
|
||||
|
||||
JNIEXPORT jint JNICALL
|
||||
Java_io_github_dovecoteescapee_byedpi_core_ByeDpiProxy_jniForceClose(__attribute__((unused)) JNIEnv *env, __attribute__((unused)) jobject thiz) {
|
||||
Java_io_github_romanvht_byedpi_core_ByeDpiProxy_jniForceClose(__attribute__((unused)) JNIEnv *env, __attribute__((unused)) jobject thiz) {
|
||||
LOG(LOG_S, "closing server socket (fd: %d)", server_fd);
|
||||
|
||||
if (close(server_fd) == -1) {
|
||||
|
||||
@@ -1,155 +0,0 @@
|
||||
package io.github.dovecoteescapee.byedpi.activities
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import android.widget.Toast
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import com.google.gson.Gson
|
||||
import io.github.dovecoteescapee.byedpi.BuildConfig
|
||||
import io.github.dovecoteescapee.byedpi.R
|
||||
import io.github.dovecoteescapee.byedpi.data.AppSettings
|
||||
import io.github.dovecoteescapee.byedpi.fragments.ByeDpiCommandLineSettingsFragment
|
||||
import io.github.dovecoteescapee.byedpi.fragments.ByeDpiUISettingsFragment
|
||||
import io.github.dovecoteescapee.byedpi.fragments.MainSettingsFragment
|
||||
import io.github.dovecoteescapee.byedpi.utility.HistoryUtils
|
||||
import io.github.dovecoteescapee.byedpi.utility.getPreferences
|
||||
import io.github.dovecoteescapee.byedpi.utility.getSelectedApps
|
||||
import androidx.core.content.edit
|
||||
|
||||
class SettingsActivity : BaseActivity() {
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.activity_settings)
|
||||
|
||||
val openFragment = intent.getStringExtra("open_fragment")
|
||||
|
||||
when (openFragment) {
|
||||
"cmd" -> {
|
||||
supportFragmentManager
|
||||
.beginTransaction()
|
||||
.replace(R.id.settings, ByeDpiCommandLineSettingsFragment())
|
||||
.commit()
|
||||
}
|
||||
"ui" -> {
|
||||
supportFragmentManager
|
||||
.beginTransaction()
|
||||
.replace(R.id.settings, ByeDpiUISettingsFragment())
|
||||
.commit()
|
||||
}
|
||||
else -> {
|
||||
supportFragmentManager
|
||||
.beginTransaction()
|
||||
.replace(R.id.settings, MainSettingsFragment())
|
||||
.commit()
|
||||
}
|
||||
}
|
||||
|
||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
|
||||
menuInflater.inflate(R.menu.menu_settings, menu)
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean = when (item.itemId) {
|
||||
android.R.id.home -> {
|
||||
onBackPressedDispatcher.onBackPressed()
|
||||
true
|
||||
}
|
||||
|
||||
R.id.action_reset_settings -> {
|
||||
val prefs = getPreferences()
|
||||
val editor = prefs.edit()
|
||||
|
||||
editor.clear()
|
||||
editor.apply()
|
||||
|
||||
recreate()
|
||||
true
|
||||
}
|
||||
|
||||
R.id.action_export_settings -> {
|
||||
val fileName = "bbd_${System.currentTimeMillis().toReadableDateTime()}.json"
|
||||
exportSettingsLauncher.launch(fileName)
|
||||
true
|
||||
}
|
||||
|
||||
R.id.action_import_settings -> {
|
||||
importSettingsLauncher.launch(arrayOf("application/json"))
|
||||
true
|
||||
}
|
||||
|
||||
else -> super.onOptionsItemSelected(item)
|
||||
}
|
||||
|
||||
private val exportSettingsLauncher = registerForActivityResult(
|
||||
ActivityResultContracts.CreateDocument("application/json")
|
||||
) { uri ->
|
||||
uri?.let {
|
||||
val prefs = getPreferences()
|
||||
val history = HistoryUtils(this).getHistory()
|
||||
val apps = prefs.getSelectedApps()
|
||||
|
||||
val settings = prefs.all.filterKeys { key ->
|
||||
key !in setOf("byedpi_command_history", "selected_apps")
|
||||
}
|
||||
|
||||
val export = AppSettings(
|
||||
app = BuildConfig.APPLICATION_ID,
|
||||
version = BuildConfig.VERSION_NAME,
|
||||
history = history,
|
||||
apps = apps,
|
||||
settings = settings
|
||||
)
|
||||
|
||||
val json = Gson().toJson(export)
|
||||
|
||||
contentResolver.openOutputStream(it)?.use { outputStream ->
|
||||
outputStream.write(json.toByteArray())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val importSettingsLauncher = registerForActivityResult(
|
||||
ActivityResultContracts.OpenDocument()
|
||||
) { uri ->
|
||||
uri?.let {
|
||||
contentResolver.openInputStream(it)?.use { inputStream ->
|
||||
val json = inputStream.bufferedReader().readText()
|
||||
|
||||
val import = Gson().fromJson(json, AppSettings::class.java)
|
||||
|
||||
if (import.app != BuildConfig.APPLICATION_ID) {
|
||||
Toast.makeText(this, "Invalid config", Toast.LENGTH_LONG).show()
|
||||
return@use
|
||||
}
|
||||
|
||||
val prefs = getPreferences()
|
||||
prefs.edit {
|
||||
clear()
|
||||
import.settings.forEach { (key, value) ->
|
||||
when (value) {
|
||||
is Int -> putInt(key, value)
|
||||
is Boolean -> putBoolean(key, value)
|
||||
is String -> putString(key, value)
|
||||
is Float -> putFloat(key, value)
|
||||
is Long -> putLong(key, value)
|
||||
}
|
||||
}
|
||||
putStringSet("selected_apps", import.apps.toSet())
|
||||
}
|
||||
HistoryUtils(this).saveHistory(import.history)
|
||||
|
||||
recreate()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun Long.toReadableDateTime(): String {
|
||||
val format = java.text.SimpleDateFormat("yyyyMMdd_HHmm", java.util.Locale.getDefault())
|
||||
return format.format(this)
|
||||
}
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
package io.github.dovecoteescapee.byedpi.activities
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.MenuItem
|
||||
import io.github.dovecoteescapee.byedpi.R
|
||||
import io.github.dovecoteescapee.byedpi.fragments.ProxyTestSettingsFragment
|
||||
|
||||
class TestSettingsActivity : BaseActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.activity_test_settings)
|
||||
|
||||
supportFragmentManager
|
||||
.beginTransaction()
|
||||
.replace(R.id.test_settings, ProxyTestSettingsFragment())
|
||||
.commit()
|
||||
|
||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean = when (item.itemId) {
|
||||
android.R.id.home -> {
|
||||
onBackPressedDispatcher.onBackPressed()
|
||||
true
|
||||
}
|
||||
else -> super.onOptionsItemSelected(item)
|
||||
}
|
||||
}
|
||||
@@ -1,336 +0,0 @@
|
||||
package io.github.dovecoteescapee.byedpi.core
|
||||
|
||||
import android.content.SharedPreferences
|
||||
import android.util.Log
|
||||
import io.github.dovecoteescapee.byedpi.utility.checkIpAndPortInCmd
|
||||
import io.github.dovecoteescapee.byedpi.utility.getStringNotNull
|
||||
import io.github.dovecoteescapee.byedpi.utility.shellSplit
|
||||
|
||||
sealed interface ByeDpiProxyPreferences {
|
||||
companion object {
|
||||
fun fromSharedPreferences(preferences: SharedPreferences): ByeDpiProxyPreferences =
|
||||
when (preferences.getBoolean("byedpi_enable_cmd_settings", false)) {
|
||||
true -> ByeDpiProxyCmdPreferences(preferences)
|
||||
false -> ByeDpiProxyUIPreferences(preferences)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class ByeDpiProxyCmdPreferences(val args: Array<String>) : ByeDpiProxyPreferences {
|
||||
constructor(preferences: SharedPreferences) : this(
|
||||
cmdToArgs(
|
||||
preferences.getStringNotNull("byedpi_cmd_args", "-Ku -a1 -An -o1 -At,r,s -d1"),
|
||||
preferences
|
||||
)
|
||||
)
|
||||
|
||||
companion object {
|
||||
private fun cmdToArgs(cmd: String, preferences: SharedPreferences): Array<String> {
|
||||
val firstArgIndex = cmd.indexOf("-")
|
||||
val args = (if (firstArgIndex > 0) cmd.substring(firstArgIndex) else cmd).trim()
|
||||
|
||||
Log.d("ProxyPref", "CMD: $args")
|
||||
|
||||
val (cmdIp, cmdPort) = preferences.checkIpAndPortInCmd()
|
||||
val hasIp = cmdIp != null
|
||||
val hasPort = cmdPort != null
|
||||
|
||||
val enableHttp = preferences.getBoolean("byedpi_http_connect", false)
|
||||
val hasHttp = args.contains("-G") || args.contains("--http-connect")
|
||||
|
||||
val ip = preferences.getStringNotNull("byedpi_proxy_ip", "127.0.0.1")
|
||||
val port = preferences.getStringNotNull("byedpi_proxy_port", "1080")
|
||||
|
||||
val prefix = buildString {
|
||||
if (!hasIp) append("--ip $ip ")
|
||||
if (!hasPort) append("--port $port ")
|
||||
if (enableHttp && !hasHttp) append("--http-connect ")
|
||||
}
|
||||
|
||||
Log.d("ProxyPref", "Added from settings: $prefix")
|
||||
|
||||
if (prefix.isNotEmpty()) {
|
||||
return arrayOf("ciadpi") + shellSplit("$prefix$args")
|
||||
}
|
||||
|
||||
return arrayOf("ciadpi") + shellSplit(args)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class ByeDpiProxyUIPreferences(
|
||||
ip: String? = null,
|
||||
port: Int? = null,
|
||||
httpConnect: Boolean? = null,
|
||||
maxConnections: Int? = null,
|
||||
bufferSize: Int? = null,
|
||||
defaultTtl: Int? = null,
|
||||
noDomain: Boolean? = null,
|
||||
desyncHttp: Boolean? = null,
|
||||
desyncHttps: Boolean? = null,
|
||||
desyncUdp: Boolean? = null,
|
||||
desyncMethod: DesyncMethod? = null,
|
||||
splitPosition: Int? = null,
|
||||
splitAtHost: Boolean? = null,
|
||||
fakeTtl: Int? = null,
|
||||
fakeSni: String? = null,
|
||||
oobChar: String? = null,
|
||||
hostMixedCase: Boolean? = null,
|
||||
domainMixedCase: Boolean? = null,
|
||||
hostRemoveSpaces: Boolean? = null,
|
||||
tlsRecordSplit: Boolean? = null,
|
||||
tlsRecordSplitPosition: Int? = null,
|
||||
tlsRecordSplitAtSni: Boolean? = null,
|
||||
hostsMode: HostsMode? = null,
|
||||
hosts: String? = null,
|
||||
tcpFastOpen: Boolean? = null,
|
||||
udpFakeCount: Int? = null,
|
||||
dropSack: Boolean? = null,
|
||||
byedpiFakeOffset: Int? = null,
|
||||
) : ByeDpiProxyPreferences {
|
||||
val ip: String = ip ?: "127.0.0.1"
|
||||
val port: Int = port ?: 1080
|
||||
val httpConnect: Boolean = httpConnect ?: false
|
||||
val maxConnections: Int = maxConnections ?: 512
|
||||
val bufferSize: Int = bufferSize ?: 16384
|
||||
val defaultTtl: Int = defaultTtl ?: 0
|
||||
val noDomain: Boolean = noDomain ?: false
|
||||
val desyncHttp: Boolean = desyncHttp ?: true
|
||||
val desyncHttps: Boolean = desyncHttps ?: true
|
||||
val desyncUdp: Boolean = desyncUdp ?: true
|
||||
val desyncMethod: DesyncMethod = desyncMethod ?: DesyncMethod.OOB
|
||||
val splitPosition: Int = splitPosition ?: 1
|
||||
val splitAtHost: Boolean = splitAtHost ?: false
|
||||
val fakeTtl: Int = fakeTtl ?: 8
|
||||
val fakeSni: String = fakeSni ?: "www.iana.org"
|
||||
val oobChar: Byte = (oobChar ?: "a")[0].code.toByte()
|
||||
val hostMixedCase: Boolean = hostMixedCase ?: false
|
||||
val domainMixedCase: Boolean = domainMixedCase ?: false
|
||||
val hostRemoveSpaces: Boolean = hostRemoveSpaces ?: false
|
||||
val tlsRecordSplit: Boolean = tlsRecordSplit ?: false
|
||||
val tlsRecordSplitPosition: Int = tlsRecordSplitPosition ?: 0
|
||||
val tlsRecordSplitAtSni: Boolean = tlsRecordSplitAtSni ?: false
|
||||
val tcpFastOpen: Boolean = tcpFastOpen ?: false
|
||||
val udpFakeCount: Int = udpFakeCount ?: 1
|
||||
val dropSack: Boolean = dropSack ?: false
|
||||
val fakeOffset: Int = byedpiFakeOffset ?: 0
|
||||
val hostsMode: HostsMode = if (hosts?.isBlank() != false) HostsMode.Disable else hostsMode ?: HostsMode.Disable
|
||||
val hosts: String? = if (this.hostsMode == HostsMode.Disable) null else hosts?.trim()
|
||||
|
||||
constructor(preferences: SharedPreferences) : this(
|
||||
ip = preferences.getString("byedpi_proxy_ip", null),
|
||||
port = preferences.getString("byedpi_proxy_port", null)?.toIntOrNull(),
|
||||
httpConnect = preferences.getBoolean("byedpi_http_connect", false),
|
||||
maxConnections = preferences.getString("byedpi_max_connections", null)?.toIntOrNull(),
|
||||
bufferSize = preferences.getString("byedpi_buffer_size", null)?.toIntOrNull(),
|
||||
defaultTtl = preferences.getString("byedpi_default_ttl", null)?.toIntOrNull(),
|
||||
noDomain = preferences.getBoolean("byedpi_no_domain", false),
|
||||
desyncHttp = preferences.getBoolean("byedpi_desync_http", true),
|
||||
desyncHttps = preferences.getBoolean("byedpi_desync_https", true),
|
||||
desyncUdp = preferences.getBoolean("byedpi_desync_udp", true),
|
||||
desyncMethod = preferences.getString("byedpi_desync_method", null)?.let { DesyncMethod.fromName(it) },
|
||||
splitPosition = preferences.getString("byedpi_split_position", null)?.toIntOrNull(),
|
||||
splitAtHost = preferences.getBoolean("byedpi_split_at_host", false),
|
||||
fakeTtl = preferences.getString("byedpi_fake_ttl", null)?.toIntOrNull(),
|
||||
fakeSni = preferences.getString("byedpi_fake_sni", null),
|
||||
oobChar = preferences.getString("byedpi_oob_data", null),
|
||||
hostMixedCase = preferences.getBoolean("byedpi_host_mixed_case", false),
|
||||
domainMixedCase = preferences.getBoolean("byedpi_domain_mixed_case", false),
|
||||
hostRemoveSpaces = preferences.getBoolean("byedpi_host_remove_spaces", false),
|
||||
tlsRecordSplit = preferences.getBoolean("byedpi_tlsrec_enabled", false),
|
||||
tlsRecordSplitPosition = preferences.getString("byedpi_tlsrec_position", null)?.toIntOrNull(),
|
||||
tlsRecordSplitAtSni = preferences.getBoolean("byedpi_tlsrec_at_sni", false),
|
||||
tcpFastOpen = preferences.getBoolean("byedpi_tcp_fast_open", false),
|
||||
udpFakeCount = preferences.getString("byedpi_udp_fake_count", null)?.toIntOrNull(),
|
||||
dropSack = preferences.getBoolean("byedpi_drop_sack", false),
|
||||
byedpiFakeOffset = preferences.getString("byedpi_fake_offset", null)?.toIntOrNull(),
|
||||
hostsMode = preferences.getString("byedpi_hosts_mode", null)?.let { HostsMode.fromName(it) },
|
||||
hosts = preferences.getString("byedpi_hosts_mode", null)?.let {
|
||||
when (HostsMode.fromName(it)) {
|
||||
HostsMode.Blacklist -> preferences.getString("byedpi_hosts_blacklist", null)
|
||||
HostsMode.Whitelist -> preferences.getString("byedpi_hosts_whitelist", null)
|
||||
else -> null
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
enum class DesyncMethod {
|
||||
None,
|
||||
Split,
|
||||
Disorder,
|
||||
Fake,
|
||||
OOB,
|
||||
DISOOB;
|
||||
|
||||
companion object {
|
||||
fun fromName(name: String): DesyncMethod {
|
||||
return when (name) {
|
||||
"none" -> None
|
||||
"split" -> Split
|
||||
"disorder" -> Disorder
|
||||
"fake" -> Fake
|
||||
"oob" -> OOB
|
||||
"disoob" -> DISOOB
|
||||
else -> throw IllegalArgumentException("Unknown desync method: $name")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum class HostsMode {
|
||||
Disable,
|
||||
Blacklist,
|
||||
Whitelist;
|
||||
|
||||
companion object {
|
||||
fun fromName(name: String): HostsMode {
|
||||
return when (name) {
|
||||
"disable" -> Disable
|
||||
"blacklist" -> Blacklist
|
||||
"whitelist" -> Whitelist
|
||||
else -> throw IllegalArgumentException("Unknown hosts mode: $name")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val uiargs: Array<String>
|
||||
get() {
|
||||
val args = mutableListOf("ciadpi")
|
||||
|
||||
ip.takeIf { it.isNotEmpty() }?.let {
|
||||
args.add("-i${it}")
|
||||
}
|
||||
|
||||
port.takeIf { it != 0 }?.let {
|
||||
args.add("-p${it}")
|
||||
}
|
||||
|
||||
maxConnections.takeIf { it != 0 }?.let {
|
||||
args.add("-c${it}")
|
||||
}
|
||||
|
||||
bufferSize.takeIf { it != 0 }?.let {
|
||||
args.add("-b${it}")
|
||||
}
|
||||
|
||||
if (httpConnect) args.add("-G")
|
||||
|
||||
val protocols = mutableListOf<String>()
|
||||
if (desyncHttps) protocols.add("t")
|
||||
if (desyncHttp) protocols.add("h")
|
||||
|
||||
if (!hosts.isNullOrBlank()) {
|
||||
val hostStr = ":${hosts}"
|
||||
val hostBlock = mutableListOf<String>()
|
||||
|
||||
when (hostsMode) {
|
||||
HostsMode.Blacklist -> {
|
||||
hostBlock.add("-H${hostStr}")
|
||||
hostBlock.add("-An")
|
||||
if (protocols.isNotEmpty()) {
|
||||
hostBlock.add("-K${protocols.joinToString(",")}")
|
||||
}
|
||||
}
|
||||
HostsMode.Whitelist -> {
|
||||
if (protocols.isNotEmpty()) {
|
||||
hostBlock.add("-K${protocols.joinToString(",")}")
|
||||
}
|
||||
hostBlock.add("-H${hostStr}")
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
args.addAll(hostBlock)
|
||||
} else {
|
||||
if (protocols.isNotEmpty()) {
|
||||
args.add("-K${protocols.joinToString(",")}")
|
||||
}
|
||||
}
|
||||
|
||||
defaultTtl.takeIf { it != 0 }?.let {
|
||||
args.add("-g${it}")
|
||||
}
|
||||
|
||||
if (noDomain) {
|
||||
args.add("-N")
|
||||
}
|
||||
|
||||
desyncMethod.let { method ->
|
||||
splitPosition.takeIf { it != 0 }?.let { pos ->
|
||||
var posArg = pos.toString()
|
||||
if (splitAtHost) {
|
||||
posArg += "+h"
|
||||
}
|
||||
val option = when (method) {
|
||||
DesyncMethod.Split -> "-s"
|
||||
DesyncMethod.Disorder -> "-d"
|
||||
DesyncMethod.OOB -> "-o"
|
||||
DesyncMethod.DISOOB -> "-q"
|
||||
DesyncMethod.Fake -> "-f"
|
||||
DesyncMethod.None -> ""
|
||||
}
|
||||
args.add("${option}${posArg}")
|
||||
}
|
||||
}
|
||||
|
||||
if (desyncMethod == DesyncMethod.Fake) {
|
||||
fakeTtl.takeIf { it != 0 }?.let {
|
||||
args.add("-t${it}")
|
||||
}
|
||||
|
||||
fakeSni.takeIf { it.isNotEmpty() }?.let {
|
||||
args.add("-n${it}")
|
||||
}
|
||||
|
||||
fakeOffset.takeIf { it != 0 }?.let {
|
||||
args.add("-O${it}")
|
||||
}
|
||||
}
|
||||
|
||||
if (desyncMethod == DesyncMethod.OOB ||
|
||||
desyncMethod == DesyncMethod.DISOOB) {
|
||||
args.add("-e${oobChar}")
|
||||
}
|
||||
|
||||
val modHttpFlags = mutableListOf<String>()
|
||||
if (hostMixedCase) modHttpFlags.add("h")
|
||||
if (domainMixedCase) modHttpFlags.add("d")
|
||||
if (hostRemoveSpaces) modHttpFlags.add("r")
|
||||
if (modHttpFlags.isNotEmpty()) {
|
||||
args.add("-M${modHttpFlags.joinToString(",")}")
|
||||
}
|
||||
|
||||
if (tlsRecordSplit) {
|
||||
tlsRecordSplitPosition.takeIf { it != 0 }?.let {
|
||||
var tlsRecArg = it.toString()
|
||||
if (tlsRecordSplitAtSni) {
|
||||
tlsRecArg += "+s"
|
||||
}
|
||||
args.add("-r${tlsRecArg}")
|
||||
}
|
||||
}
|
||||
|
||||
if (tcpFastOpen) {
|
||||
args.add("-F")
|
||||
}
|
||||
|
||||
if (dropSack) {
|
||||
args.add("-Y")
|
||||
}
|
||||
|
||||
args.add("-An")
|
||||
|
||||
if (desyncUdp) {
|
||||
args.add("-Ku")
|
||||
|
||||
udpFakeCount.takeIf { it != 0 }?.let {
|
||||
args.add("-a${it}")
|
||||
}
|
||||
|
||||
args.add("-An")
|
||||
}
|
||||
|
||||
Log.d("ProxyPref", "UI to cmd: ${args.joinToString(" ")}")
|
||||
return args.toTypedArray()
|
||||
}
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
package io.github.dovecoteescapee.byedpi.data
|
||||
|
||||
data class AppSettings(
|
||||
val app: String,
|
||||
val version: String,
|
||||
val history: List<Command>,
|
||||
val apps: List<String>,
|
||||
val settings: Map<String, Any?>
|
||||
)
|
||||
-69
@@ -1,69 +0,0 @@
|
||||
package io.github.dovecoteescapee.byedpi.fragments
|
||||
|
||||
import android.content.SharedPreferences
|
||||
import android.os.Bundle
|
||||
import androidx.preference.*
|
||||
import io.github.dovecoteescapee.byedpi.R
|
||||
import io.github.dovecoteescapee.byedpi.utility.*
|
||||
|
||||
class ProxyTestSettingsFragment : PreferenceFragmentCompat() {
|
||||
|
||||
private val preferenceListener =
|
||||
SharedPreferences.OnSharedPreferenceChangeListener { _, _ ->
|
||||
updatePreferences()
|
||||
}
|
||||
|
||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||
setPreferencesFromResource(R.xml.proxy_test_settings, rootKey)
|
||||
|
||||
setEditTestPreferenceListenerInt(
|
||||
"byedpi_proxytest_delay",
|
||||
0,
|
||||
10
|
||||
)
|
||||
|
||||
setEditTestPreferenceListenerInt(
|
||||
"byedpi_proxytest_requests",
|
||||
1,
|
||||
20
|
||||
)
|
||||
|
||||
setEditTestPreferenceListenerInt(
|
||||
"byedpi_proxytest_timeout",
|
||||
1,
|
||||
15
|
||||
)
|
||||
|
||||
setEditTestPreferenceListenerDomain(
|
||||
"byedpi_proxytest_sni"
|
||||
)
|
||||
|
||||
updatePreferences()
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
sharedPreferences?.registerOnSharedPreferenceChangeListener(preferenceListener)
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
sharedPreferences?.unregisterOnSharedPreferenceChangeListener(preferenceListener)
|
||||
}
|
||||
|
||||
private fun updatePreferences() {
|
||||
val switchUserCommands = findPreferenceNotNull<SwitchPreference>("byedpi_proxytest_usercommands")
|
||||
val textUserDomains = findPreferenceNotNull<EditTextPreference>("byedpi_proxytest_domains")
|
||||
val textUserCommands = findPreferenceNotNull<EditTextPreference>("byedpi_proxytest_commands")
|
||||
val domainLists = findPreferenceNotNull<MultiSelectListPreference>("byedpi_proxytest_domain_lists")
|
||||
|
||||
val setUserCommands = { enable: Boolean -> textUserCommands.isEnabled = enable }
|
||||
|
||||
textUserDomains.isEnabled = domainLists.values?.contains("custom") == true
|
||||
setUserCommands(switchUserCommands.isChecked)
|
||||
|
||||
if (domainLists.values?.isNotEmpty() == true) {
|
||||
domainLists.summary = domainLists.values.joinToString("\n")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
package io.github.dovecoteescapee.byedpi.services
|
||||
|
||||
import io.github.dovecoteescapee.byedpi.data.AppStatus
|
||||
import io.github.dovecoteescapee.byedpi.data.Mode
|
||||
|
||||
var appStatus = AppStatus.Halted to Mode.VPN
|
||||
private set
|
||||
|
||||
fun setStatus(status: AppStatus, mode: Mode) {
|
||||
appStatus = status to mode
|
||||
}
|
||||
@@ -1,87 +0,0 @@
|
||||
package io.github.dovecoteescapee.byedpi.services
|
||||
|
||||
import android.net.VpnService
|
||||
import android.os.Build
|
||||
import android.service.quicksettings.Tile
|
||||
import android.service.quicksettings.TileService
|
||||
import android.util.Log
|
||||
import androidx.annotation.RequiresApi
|
||||
import io.github.dovecoteescapee.byedpi.data.*
|
||||
import io.github.dovecoteescapee.byedpi.utility.getPreferences
|
||||
import io.github.dovecoteescapee.byedpi.utility.mode
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.N)
|
||||
class QuickTileService : TileService() {
|
||||
|
||||
companion object {
|
||||
private const val TAG = "QuickTileService"
|
||||
}
|
||||
|
||||
private var appTile: Tile? = null
|
||||
|
||||
override fun onTileAdded() {
|
||||
super.onTileAdded()
|
||||
Log.i(TAG, "Tile added")
|
||||
}
|
||||
|
||||
override fun onTileRemoved() {
|
||||
super.onTileRemoved()
|
||||
Log.i(TAG, "Tile removed")
|
||||
}
|
||||
|
||||
override fun onStartListening() {
|
||||
super.onStartListening()
|
||||
appTile = qsTile
|
||||
updateStatus()
|
||||
}
|
||||
|
||||
override fun onStopListening() {
|
||||
super.onStopListening()
|
||||
appTile = null
|
||||
}
|
||||
|
||||
override fun onClick() {
|
||||
super.onClick()
|
||||
handleClick()
|
||||
}
|
||||
|
||||
private fun handleClick() {
|
||||
val (status) = appStatus
|
||||
|
||||
when (status) {
|
||||
AppStatus.Halted -> {
|
||||
val mode = getPreferences().mode()
|
||||
|
||||
if (mode == Mode.VPN && VpnService.prepare(this) != null) {
|
||||
return
|
||||
}
|
||||
|
||||
ServiceManager.start(this, mode)
|
||||
setState(Tile.STATE_ACTIVE)
|
||||
}
|
||||
AppStatus.Running -> {
|
||||
ServiceManager.stop(this)
|
||||
setState(Tile.STATE_INACTIVE)
|
||||
}
|
||||
}
|
||||
|
||||
Log.i(TAG, "Toggle tile")
|
||||
}
|
||||
|
||||
private fun updateStatus() {
|
||||
val (status) = appStatus
|
||||
|
||||
if (status == AppStatus.Running) {
|
||||
setState(Tile.STATE_ACTIVE)
|
||||
} else {
|
||||
setState(Tile.STATE_INACTIVE)
|
||||
}
|
||||
}
|
||||
|
||||
private fun setState(newState: Int) {
|
||||
appTile?.apply {
|
||||
state = newState
|
||||
updateTile()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,87 +0,0 @@
|
||||
package io.github.dovecoteescapee.byedpi.utility
|
||||
|
||||
import android.util.Log
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.awaitAll
|
||||
import kotlinx.coroutines.withContext
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import java.net.InetSocketAddress
|
||||
import java.net.Proxy
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
class SiteCheckUtils(
|
||||
private val proxyIp: String,
|
||||
private val proxyPort: Int
|
||||
) {
|
||||
|
||||
private fun createClient(timeout: Long) = OkHttpClient.Builder()
|
||||
.proxy(Proxy(Proxy.Type.SOCKS, InetSocketAddress(proxyIp, proxyPort)))
|
||||
.connectionPool(okhttp3.ConnectionPool(0, 1, TimeUnit.NANOSECONDS))
|
||||
.connectTimeout(timeout, TimeUnit.SECONDS)
|
||||
.readTimeout(timeout, TimeUnit.SECONDS)
|
||||
.writeTimeout(timeout, TimeUnit.SECONDS)
|
||||
.callTimeout(timeout, TimeUnit.SECONDS)
|
||||
.followSslRedirects(true)
|
||||
.followRedirects(true)
|
||||
.build()
|
||||
|
||||
suspend fun checkSitesAsync(
|
||||
sites: List<String>,
|
||||
requestsCount: Int,
|
||||
requestTimeout: Long,
|
||||
fullLog: Boolean,
|
||||
onSiteChecked: ((String, Int, Int) -> Unit)? = null
|
||||
): List<Pair<String, Int>> {
|
||||
return withContext(Dispatchers.IO) {
|
||||
val client = createClient(requestTimeout)
|
||||
sites.map { site ->
|
||||
async {
|
||||
val successCount = checkSiteAccess(client, site, requestsCount)
|
||||
if (fullLog) {
|
||||
onSiteChecked?.invoke(site, successCount, requestsCount)
|
||||
}
|
||||
site to successCount
|
||||
}
|
||||
}.awaitAll()
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun checkSiteAccess(
|
||||
client: OkHttpClient,
|
||||
site: String,
|
||||
requestsCount: Int
|
||||
): Int = withContext(Dispatchers.IO) {
|
||||
var responseCount = 0
|
||||
|
||||
val formattedUrl = if (site.startsWith("http://") || site.startsWith("https://")) site
|
||||
else "https://$site"
|
||||
|
||||
repeat(requestsCount) { attempt ->
|
||||
Log.i("SiteChecker", "Attempt ${attempt + 1}/$requestsCount for $site")
|
||||
|
||||
try {
|
||||
val request = Request.Builder().url(formattedUrl).build()
|
||||
client.newCall(request).execute().use { response ->
|
||||
val declaredLength = response.body.contentLength()
|
||||
val actualLength = response.body.bytes().size.toLong()
|
||||
val responseCode = response.code
|
||||
|
||||
if (declaredLength <= 0 || actualLength >= declaredLength) {
|
||||
Log.i("SiteChecker", "Response for $site: $responseCode, Declared: $declaredLength, Actual: $actualLength")
|
||||
responseCount++
|
||||
} else {
|
||||
Log.w("SiteChecker", "Block detected for $site, Declared: $declaredLength, Actual: $actualLength")
|
||||
}
|
||||
|
||||
response.body.close()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e("SiteChecker", "Error accessing $site: ${e.message}")
|
||||
}
|
||||
}
|
||||
|
||||
responseCount
|
||||
}
|
||||
}
|
||||
+14
-7
@@ -1,12 +1,14 @@
|
||||
package io.github.dovecoteescapee.byedpi.activities
|
||||
package io.github.romanvht.byedpi.activities
|
||||
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import com.google.android.material.appbar.MaterialToolbar
|
||||
import com.google.android.material.color.DynamicColors
|
||||
import io.github.dovecoteescapee.byedpi.fragments.MainSettingsFragment
|
||||
import io.github.dovecoteescapee.byedpi.utility.getPreferences
|
||||
import io.github.dovecoteescapee.byedpi.utility.getStringNotNull
|
||||
import io.github.romanvht.byedpi.R
|
||||
import io.github.romanvht.byedpi.utility.SettingsUtils
|
||||
import io.github.romanvht.byedpi.utility.getPreferences
|
||||
import io.github.romanvht.byedpi.utility.getStringNotNull
|
||||
|
||||
abstract class BaseActivity : AppCompatActivity() {
|
||||
|
||||
@@ -14,10 +16,10 @@ abstract class BaseActivity : AppCompatActivity() {
|
||||
val prefs = getPreferences()
|
||||
|
||||
val lang = prefs.getStringNotNull("language", "system")
|
||||
MainSettingsFragment.setLang(lang)
|
||||
SettingsUtils.setLang(lang)
|
||||
|
||||
val theme = prefs.getStringNotNull("app_theme", "system")
|
||||
MainSettingsFragment.setTheme(theme)
|
||||
SettingsUtils.setTheme(theme)
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
DynamicColors.applyToActivityIfAvailable(this)
|
||||
@@ -26,4 +28,9 @@ abstract class BaseActivity : AppCompatActivity() {
|
||||
super.onCreate(savedInstanceState)
|
||||
}
|
||||
|
||||
}
|
||||
protected fun setupToolbar() {
|
||||
val toolbar: MaterialToolbar = findViewById(R.id.toolbar)
|
||||
setSupportActionBar(toolbar)
|
||||
}
|
||||
|
||||
}
|
||||
+206
-30
@@ -1,4 +1,4 @@
|
||||
package io.github.dovecoteescapee.byedpi.activities
|
||||
package io.github.romanvht.byedpi.activities
|
||||
|
||||
import android.Manifest
|
||||
import android.annotation.SuppressLint
|
||||
@@ -13,26 +13,35 @@ import android.os.Bundle
|
||||
import android.util.Log
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ArrayAdapter
|
||||
import android.widget.ListView
|
||||
import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import io.github.dovecoteescapee.byedpi.R
|
||||
import io.github.dovecoteescapee.byedpi.data.*
|
||||
import io.github.dovecoteescapee.byedpi.databinding.ActivityMainBinding
|
||||
import io.github.dovecoteescapee.byedpi.services.ServiceManager
|
||||
import io.github.dovecoteescapee.byedpi.services.appStatus
|
||||
import io.github.dovecoteescapee.byedpi.utility.*
|
||||
import io.github.romanvht.byedpi.R
|
||||
import io.github.romanvht.byedpi.data.*
|
||||
import io.github.romanvht.byedpi.databinding.ActivityMainBinding
|
||||
import io.github.romanvht.byedpi.services.ServiceManager
|
||||
import io.github.romanvht.byedpi.services.appStatus
|
||||
import io.github.romanvht.byedpi.utility.*
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import java.io.IOException
|
||||
import kotlin.system.exitProcess
|
||||
import androidx.core.content.edit
|
||||
|
||||
class MainActivity : BaseActivity() {
|
||||
private lateinit var binding: ActivityMainBinding
|
||||
private lateinit var historyUtils: HistoryUtils
|
||||
|
||||
companion object {
|
||||
private val TAG: String = MainActivity::class.java.simpleName
|
||||
private const val BATTERY_OPTIMIZATION_REQUESTED = "battery_optimization_requested"
|
||||
|
||||
private fun collectLogs(): String? =
|
||||
try {
|
||||
@@ -124,6 +133,9 @@ class MainActivity : BaseActivity() {
|
||||
|
||||
binding = ActivityMainBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
setupToolbar()
|
||||
|
||||
historyUtils = HistoryUtils(this)
|
||||
|
||||
val intentFilter = IntentFilter().apply {
|
||||
addAction(STARTED_BROADCAST)
|
||||
@@ -138,8 +150,8 @@ class MainActivity : BaseActivity() {
|
||||
registerReceiver(receiver, intentFilter)
|
||||
}
|
||||
|
||||
binding.statusButton.setOnClickListener {
|
||||
binding.statusButton.isClickable = false
|
||||
binding.statusButtonCard.setOnClickListener {
|
||||
binding.statusButtonCard.isClickable = false
|
||||
|
||||
val (status, _) = appStatus
|
||||
when (status) {
|
||||
@@ -147,26 +159,63 @@ class MainActivity : BaseActivity() {
|
||||
AppStatus.Running -> stop()
|
||||
}
|
||||
|
||||
binding.statusButton.postDelayed({
|
||||
binding.statusButton.isClickable = true
|
||||
binding.statusButtonCard.postDelayed({
|
||||
binding.statusButtonCard.isClickable = true
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
binding.openEditorLink.setOnClickListener {
|
||||
binding.statusButtonCard.setOnFocusChangeListener { _, hasFocus ->
|
||||
if (hasFocus) {
|
||||
binding.statusButtonCard.strokeWidth = 10
|
||||
binding.statusButtonCard.strokeColor = android.graphics.Color.argb(100, 0, 0, 0)
|
||||
} else {
|
||||
binding.statusButtonCard.strokeWidth = 0
|
||||
}
|
||||
}
|
||||
|
||||
binding.settingsButton.setOnClickListener {
|
||||
val (status, _) = appStatus
|
||||
|
||||
if (status == AppStatus.Halted) {
|
||||
val intent = Intent(this, SettingsActivity::class.java)
|
||||
val useCmdSettings = getPreferences().getBoolean("byedpi_enable_cmd_settings", false)
|
||||
intent.putExtra("open_fragment", if (useCmdSettings) "cmd" else "ui")
|
||||
startActivity(intent)
|
||||
} else {
|
||||
Toast.makeText(this, R.string.settings_unavailable, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) {
|
||||
binding.editorButton.setOnClickListener {
|
||||
val useCmdSettings = getPreferences().getCmdEnable()
|
||||
|
||||
if (!useCmdSettings && appStatus.first == AppStatus.Running) {
|
||||
Toast.makeText(this, R.string.settings_unavailable, Toast.LENGTH_SHORT).show()
|
||||
return@setOnClickListener
|
||||
}
|
||||
|
||||
val intent = Intent(this, SettingsActivity::class.java)
|
||||
intent.putExtra("open_fragment", if (useCmdSettings) "cmd" else "ui")
|
||||
startActivity(intent)
|
||||
}
|
||||
|
||||
binding.testProxyButton.setOnClickListener {
|
||||
startActivity(Intent(this, TestActivity::class.java))
|
||||
}
|
||||
|
||||
binding.domainListsButton.setOnClickListener {
|
||||
val intent = Intent(this, TestSettingsActivity::class.java)
|
||||
intent.putExtra("open_fragment", "domain_lists")
|
||||
startActivity(intent)
|
||||
}
|
||||
|
||||
binding.strategyButton.setOnClickListener {
|
||||
showStrategyPicker()
|
||||
}
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU &&
|
||||
ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) {
|
||||
requestPermissions(arrayOf(Manifest.permission.POST_NOTIFICATIONS), 1)
|
||||
} else {
|
||||
requestBatteryOptimization()
|
||||
}
|
||||
|
||||
if (getPreferences().getBoolean("auto_connect", false) && appStatus.first != AppStatus.Running) {
|
||||
@@ -179,6 +228,7 @@ class MainActivity : BaseActivity() {
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
updateStatus()
|
||||
updateStrategyButton()
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
@@ -186,6 +236,18 @@ class MainActivity : BaseActivity() {
|
||||
unregisterReceiver(receiver)
|
||||
}
|
||||
|
||||
override fun onRequestPermissionsResult(
|
||||
requestCode: Int,
|
||||
permissions: Array<out String>,
|
||||
grantResults: IntArray
|
||||
) {
|
||||
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
|
||||
|
||||
if (requestCode == 1) {
|
||||
requestBatteryOptimization()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
|
||||
menuInflater.inflate(R.menu.menu_main, menu)
|
||||
return true
|
||||
@@ -195,16 +257,6 @@ class MainActivity : BaseActivity() {
|
||||
val (status, _) = appStatus
|
||||
|
||||
return when (item.itemId) {
|
||||
R.id.action_settings -> {
|
||||
if (status == AppStatus.Halted) {
|
||||
val intent = Intent(this, SettingsActivity::class.java)
|
||||
startActivity(intent)
|
||||
} else {
|
||||
Toast.makeText(this, R.string.settings_unavailable, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
R.id.action_save_logs -> {
|
||||
val intent =
|
||||
Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
|
||||
@@ -259,32 +311,156 @@ class MainActivity : BaseActivity() {
|
||||
|
||||
when (status) {
|
||||
AppStatus.Halted -> {
|
||||
val typedValue = android.util.TypedValue()
|
||||
theme.resolveAttribute(android.R.attr.colorPrimary, typedValue,true)
|
||||
binding.statusButtonCard.setCardBackgroundColor(typedValue.data)
|
||||
binding.statusButtonIcon.clearColorFilter()
|
||||
|
||||
when (preferences.mode()) {
|
||||
Mode.VPN -> {
|
||||
binding.statusText.setText(R.string.vpn_disconnected)
|
||||
binding.statusButton.setText(R.string.vpn_connect)
|
||||
}
|
||||
|
||||
Mode.Proxy -> {
|
||||
binding.statusText.setText(R.string.proxy_down)
|
||||
binding.statusButton.setText(R.string.proxy_start)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
AppStatus.Running -> {
|
||||
binding.statusButtonCard.setCardBackgroundColor(ContextCompat.getColor(this, R.color.green_active))
|
||||
binding.statusButtonIcon.setColorFilter(ContextCompat.getColor(this, android.R.color.white))
|
||||
|
||||
when (mode) {
|
||||
Mode.VPN -> {
|
||||
binding.statusText.setText(R.string.vpn_connected)
|
||||
binding.statusButton.setText(R.string.vpn_disconnect)
|
||||
}
|
||||
|
||||
Mode.Proxy -> {
|
||||
binding.statusText.setText(R.string.proxy_up)
|
||||
binding.statusButton.setText(R.string.proxy_stop)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateStrategyButton() {
|
||||
val useCmdSettings = getPreferences().getCmdEnable()
|
||||
|
||||
if (!useCmdSettings) {
|
||||
binding.cmdButtonsRow.visibility = View.GONE
|
||||
binding.strategyButton.visibility = View.GONE
|
||||
return
|
||||
}
|
||||
|
||||
binding.cmdButtonsRow.visibility = View.VISIBLE
|
||||
|
||||
val pinned = historyUtils.getPinnedHistory()
|
||||
val currentCmdArgs = getPreferences().getCmdArgs()
|
||||
|
||||
val matched = pinned.find { it.text == currentCmdArgs }
|
||||
val name = matched?.name?.takeIf { it.isNotBlank() }
|
||||
|
||||
if (name != null) {
|
||||
binding.strategyButtonName.text = name
|
||||
binding.strategyButtonName.visibility = View.VISIBLE
|
||||
binding.strategyButtonText.maxLines = 1
|
||||
} else {
|
||||
binding.strategyButtonName.visibility = View.GONE
|
||||
binding.strategyButtonText.maxLines = 2
|
||||
}
|
||||
|
||||
binding.strategyButtonText.text = currentCmdArgs.ifBlank { getString(R.string.main_strategy_picker) }
|
||||
binding.strategyButton.visibility = View.VISIBLE
|
||||
}
|
||||
|
||||
private fun showStrategyPicker() {
|
||||
val pinned = historyUtils.getPinnedHistory()
|
||||
|
||||
if (pinned.isEmpty()) {
|
||||
Toast.makeText(this, R.string.main_strategy_no_pinned, Toast.LENGTH_SHORT).show()
|
||||
return
|
||||
}
|
||||
|
||||
val adapter = object : ArrayAdapter<Command>(
|
||||
this,
|
||||
R.layout.item_main_strategy,
|
||||
pinned
|
||||
) {
|
||||
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
|
||||
val view = convertView ?: layoutInflater.inflate(
|
||||
R.layout.item_main_strategy,
|
||||
parent,
|
||||
false
|
||||
)
|
||||
|
||||
val command = getItem(position)!!
|
||||
|
||||
val nameView = view.findViewById<TextView>(R.id.strategyName)
|
||||
val textView = view.findViewById<TextView>(R.id.strategyText)
|
||||
val dividerView = view.findViewById<View>(R.id.strategyDivider)
|
||||
|
||||
val name = command.name?.takeIf { it.isNotBlank() }
|
||||
|
||||
if (name != null) {
|
||||
nameView.visibility = View.VISIBLE
|
||||
nameView.text = name
|
||||
textView.maxLines = 2
|
||||
} else {
|
||||
nameView.visibility = View.GONE
|
||||
textView.maxLines = 3
|
||||
}
|
||||
|
||||
textView.text = command.text
|
||||
dividerView.visibility = if (position == count - 1) View.GONE else View.VISIBLE
|
||||
|
||||
return view
|
||||
}
|
||||
}
|
||||
|
||||
val listView = ListView(this).apply {
|
||||
divider = null
|
||||
setPadding(0, (10 * resources.displayMetrics.density).toInt(), 0, 0)
|
||||
this.adapter = adapter
|
||||
}
|
||||
|
||||
val dialog = AlertDialog.Builder(this)
|
||||
.setTitle(getString(R.string.main_strategy_picker))
|
||||
.setView(listView)
|
||||
.setNegativeButton(getString(android.R.string.cancel), null)
|
||||
.create()
|
||||
|
||||
listView.setOnItemClickListener { _, _, position, _ ->
|
||||
applyStrategy(pinned[position].text)
|
||||
dialog.dismiss()
|
||||
}
|
||||
|
||||
dialog.show()
|
||||
}
|
||||
|
||||
private fun applyStrategy(commandText: String) {
|
||||
getPreferences().edit { putString("byedpi_cmd_args", commandText) }
|
||||
|
||||
updateStrategyButton()
|
||||
|
||||
if (appStatus.first == AppStatus.Running) {
|
||||
val mode = getPreferences().mode()
|
||||
if (mode == Mode.VPN && VpnService.prepare(this) != null) {
|
||||
return
|
||||
}
|
||||
|
||||
ServiceManager.restart(this, mode)
|
||||
Toast.makeText(this, R.string.service_restart, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
|
||||
private fun requestBatteryOptimization() {
|
||||
val preferences = getPreferences()
|
||||
val alreadyRequested = preferences.getBoolean(BATTERY_OPTIMIZATION_REQUESTED, false)
|
||||
|
||||
if (!alreadyRequested && !BatteryUtils.isOptimizationDisabled(this)) {
|
||||
BatteryUtils.requestBatteryOptimization(this)
|
||||
preferences.edit { putBoolean(BATTERY_OPTIMIZATION_REQUESTED, true) }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,178 @@
|
||||
package io.github.romanvht.byedpi.activities
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import io.github.romanvht.byedpi.R
|
||||
import io.github.romanvht.byedpi.data.AppSettings
|
||||
import io.github.romanvht.byedpi.fragments.ByeDpiCMDSettingsFragment
|
||||
import io.github.romanvht.byedpi.fragments.ByeDpiUISettingsFragment
|
||||
import io.github.romanvht.byedpi.fragments.MainSettingsFragment
|
||||
import io.github.romanvht.byedpi.utility.SettingsUtils
|
||||
import io.github.romanvht.byedpi.utility.SettingsUtils.Section
|
||||
|
||||
class SettingsActivity : BaseActivity() {
|
||||
private var exportSections = Section.entries.toSet()
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.activity_settings)
|
||||
setupToolbar()
|
||||
|
||||
if (savedInstanceState == null) {
|
||||
val openFragment = intent.getStringExtra("open_fragment")
|
||||
|
||||
when (openFragment) {
|
||||
"cmd" -> {
|
||||
supportFragmentManager
|
||||
.beginTransaction()
|
||||
.replace(R.id.settings, ByeDpiCMDSettingsFragment())
|
||||
.commit()
|
||||
}
|
||||
"ui" -> {
|
||||
supportFragmentManager
|
||||
.beginTransaction()
|
||||
.replace(R.id.settings, ByeDpiUISettingsFragment())
|
||||
.commit()
|
||||
}
|
||||
else -> {
|
||||
supportFragmentManager
|
||||
.beginTransaction()
|
||||
.replace(R.id.settings, MainSettingsFragment())
|
||||
.commit()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
|
||||
menuInflater.inflate(R.menu.menu_settings, menu)
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean = when (item.itemId) {
|
||||
android.R.id.home -> {
|
||||
onBackPressedDispatcher.onBackPressed()
|
||||
true
|
||||
}
|
||||
|
||||
R.id.action_reset_settings -> {
|
||||
SettingsUtils.resetSettings(this) {
|
||||
recreate()
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
R.id.action_export_settings -> {
|
||||
showSectionSelectionDialog(
|
||||
title = R.string.export_settings,
|
||||
availableSections = Section.entries.toSet()
|
||||
) { sections ->
|
||||
exportSections = sections
|
||||
val fileName = "bbd_${System.currentTimeMillis().toReadableDateTime()}.json"
|
||||
exportSettingsLauncher.launch(fileName)
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
R.id.action_import_settings -> {
|
||||
importSettingsLauncher.launch(arrayOf("application/json"))
|
||||
true
|
||||
}
|
||||
|
||||
else -> super.onOptionsItemSelected(item)
|
||||
}
|
||||
|
||||
private val exportSettingsLauncher = registerForActivityResult(
|
||||
ActivityResultContracts.CreateDocument("application/json")
|
||||
) { uri ->
|
||||
uri?.let {
|
||||
SettingsUtils.exportSettings(this, it, exportSections)
|
||||
}
|
||||
}
|
||||
|
||||
private val importSettingsLauncher = registerForActivityResult(
|
||||
ActivityResultContracts.OpenDocument()
|
||||
) { uri ->
|
||||
uri?.let {
|
||||
val settings = SettingsUtils.readSettings(this, it) ?: return@let
|
||||
val availableSections = SettingsUtils.getAvailableSections(settings)
|
||||
|
||||
if (availableSections.isEmpty()) {
|
||||
return@let
|
||||
}
|
||||
|
||||
showImportDialog(settings, availableSections)
|
||||
}
|
||||
}
|
||||
|
||||
private fun showImportDialog(settings: AppSettings, availableSections: Set<Section>) {
|
||||
showSectionSelectionDialog(
|
||||
title = R.string.import_settings,
|
||||
availableSections = availableSections
|
||||
) { sections ->
|
||||
SettingsUtils.importSettings(this, settings, sections) {
|
||||
recreate()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun showSectionSelectionDialog(
|
||||
@StringRes title: Int,
|
||||
availableSections: Set<Section>,
|
||||
onConfirmed: (Set<Section>) -> Unit
|
||||
) {
|
||||
val sections = Section.entries.filter { it in availableSections }
|
||||
val checkedItems = BooleanArray(sections.size) { true }
|
||||
val labels = sections.map { getString(it.label) }.toTypedArray()
|
||||
|
||||
lateinit var dialog: AlertDialog
|
||||
dialog = AlertDialog.Builder(this)
|
||||
.setTitle(title)
|
||||
.setMultiChoiceItems(labels, checkedItems) { _, which, isChecked ->
|
||||
checkedItems[which] = isChecked
|
||||
dialog.getButton(AlertDialog.BUTTON_POSITIVE).isEnabled = checkedItems.any { it }
|
||||
}
|
||||
.setPositiveButton(android.R.string.ok, null)
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.create()
|
||||
|
||||
dialog.setOnShowListener {
|
||||
val positiveButton = dialog.getButton(AlertDialog.BUTTON_POSITIVE)
|
||||
|
||||
positiveButton.setOnClickListener {
|
||||
val selected = sections
|
||||
.filterIndexed { index, _ -> checkedItems[index] }
|
||||
.toSet()
|
||||
|
||||
if (selected.isNotEmpty()) {
|
||||
dialog.dismiss()
|
||||
onConfirmed(selected)
|
||||
}
|
||||
}
|
||||
|
||||
positiveButton.isEnabled = checkedItems.any { it }
|
||||
}
|
||||
|
||||
dialog.show()
|
||||
}
|
||||
|
||||
@get:StringRes
|
||||
private val Section.label: Int
|
||||
get() = when (this) {
|
||||
Section.SETTINGS -> R.string.backup_settings
|
||||
Section.HISTORY -> R.string.backup_command_history
|
||||
Section.APPS -> R.string.backup_selected_apps
|
||||
Section.DOMAIN_LISTS -> R.string.backup_domain_lists
|
||||
}
|
||||
|
||||
private fun Long.toReadableDateTime(): String {
|
||||
val format = java.text.SimpleDateFormat("yyyyMMdd_HHmm", java.util.Locale.getDefault())
|
||||
return format.format(this)
|
||||
}
|
||||
}
|
||||
+178
-174
@@ -1,45 +1,49 @@
|
||||
package io.github.dovecoteescapee.byedpi.activities
|
||||
package io.github.romanvht.byedpi.activities
|
||||
|
||||
import android.content.ClipData
|
||||
import android.content.ClipboardManager
|
||||
import android.content.Intent
|
||||
import android.net.VpnService
|
||||
import android.os.Bundle
|
||||
import android.text.SpannableString
|
||||
import android.text.Spanned
|
||||
import android.text.method.LinkMovementMethod
|
||||
import android.text.style.ClickableSpan
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.view.WindowManager
|
||||
import android.widget.Button
|
||||
import android.widget.ScrollView
|
||||
import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
import androidx.activity.OnBackPressedCallback
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import io.github.dovecoteescapee.byedpi.R
|
||||
import io.github.dovecoteescapee.byedpi.data.Mode
|
||||
import io.github.dovecoteescapee.byedpi.data.AppStatus
|
||||
import io.github.dovecoteescapee.byedpi.services.appStatus
|
||||
import io.github.dovecoteescapee.byedpi.services.ServiceManager
|
||||
import io.github.dovecoteescapee.byedpi.utility.HistoryUtils
|
||||
import io.github.dovecoteescapee.byedpi.utility.getPreferences
|
||||
import io.github.dovecoteescapee.byedpi.utility.SiteCheckUtils
|
||||
import io.github.dovecoteescapee.byedpi.utility.getIntStringNotNull
|
||||
import io.github.dovecoteescapee.byedpi.utility.getLongStringNotNull
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import io.github.romanvht.byedpi.R
|
||||
import io.github.romanvht.byedpi.adapters.StrategyResultAdapter
|
||||
import io.github.romanvht.byedpi.data.Mode
|
||||
import io.github.romanvht.byedpi.data.AppStatus
|
||||
import io.github.romanvht.byedpi.data.SiteResult
|
||||
import io.github.romanvht.byedpi.data.StrategyResult
|
||||
import io.github.romanvht.byedpi.services.appStatus
|
||||
import io.github.romanvht.byedpi.services.ServiceManager
|
||||
import io.github.romanvht.byedpi.utility.HistoryUtils
|
||||
import io.github.romanvht.byedpi.utility.getPreferences
|
||||
import io.github.romanvht.byedpi.utility.SiteCheckUtils
|
||||
import io.github.romanvht.byedpi.utility.getIntStringNotNull
|
||||
import io.github.romanvht.byedpi.utility.getLongStringNotNull
|
||||
import androidx.core.content.edit
|
||||
import io.github.dovecoteescapee.byedpi.utility.getStringNotNull
|
||||
import io.github.romanvht.byedpi.utility.getStringNotNull
|
||||
import com.google.gson.Gson
|
||||
import com.google.gson.reflect.TypeToken
|
||||
import io.github.romanvht.byedpi.utility.DomainListUtils
|
||||
import io.github.romanvht.byedpi.utility.getCmdArgs
|
||||
import io.github.romanvht.byedpi.utility.mode
|
||||
import kotlinx.coroutines.*
|
||||
import java.io.File
|
||||
|
||||
class TestActivity : BaseActivity() {
|
||||
|
||||
private lateinit var scrollTextView: ScrollView
|
||||
private lateinit var strategiesRecyclerView: RecyclerView
|
||||
private lateinit var progressTextView: TextView
|
||||
private lateinit var resultsTextView: TextView
|
||||
private lateinit var disclaimerTextView: TextView
|
||||
private lateinit var startStopButton: Button
|
||||
private lateinit var strategyAdapter: StrategyResultAdapter
|
||||
|
||||
private lateinit var siteChecker: SiteCheckUtils
|
||||
private lateinit var cmdHistoryUtils: HistoryUtils
|
||||
@@ -48,6 +52,8 @@ class TestActivity : BaseActivity() {
|
||||
|
||||
private var savedCmd: String = ""
|
||||
private var testJob: Job? = null
|
||||
private val strategies = mutableListOf<StrategyResult>()
|
||||
private val gson = Gson()
|
||||
|
||||
private var isTesting: Boolean
|
||||
get() = prefs.getBoolean("is_test_running", false)
|
||||
@@ -60,31 +66,46 @@ class TestActivity : BaseActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.activity_proxy_test)
|
||||
setupToolbar()
|
||||
|
||||
val ip = prefs.getStringNotNull("byedpi_proxy_ip", "127.0.0.1")
|
||||
val port = prefs.getIntStringNotNull("byedpi_proxy_port", 1080)
|
||||
|
||||
siteChecker = SiteCheckUtils(ip, port)
|
||||
cmdHistoryUtils = HistoryUtils(this)
|
||||
scrollTextView = findViewById(R.id.scrollView)
|
||||
|
||||
strategiesRecyclerView = findViewById(R.id.strategiesRecyclerView)
|
||||
startStopButton = findViewById(R.id.startStopButton)
|
||||
resultsTextView = findViewById(R.id.resultsTextView)
|
||||
progressTextView = findViewById(R.id.progressTextView)
|
||||
disclaimerTextView = findViewById(R.id.disclaimerTextView)
|
||||
|
||||
resultsTextView.movementMethod = LinkMovementMethod.getInstance()
|
||||
strategyAdapter = StrategyResultAdapter(this,
|
||||
onApply = { command ->
|
||||
addToHistory(command)
|
||||
}
|
||||
)
|
||||
|
||||
if (isTesting) {
|
||||
progressTextView.text = getString(R.string.test_proxy_error)
|
||||
resultsTextView.text = getString(R.string.test_crash)
|
||||
isTesting = false
|
||||
} else {
|
||||
lifecycleScope.launch {
|
||||
val previousLogs = loadLog()
|
||||
if (previousLogs.isNotEmpty()) {
|
||||
progressTextView.text = getString(R.string.test_complete)
|
||||
resultsTextView.text = ""
|
||||
displayLog(previousLogs)
|
||||
}
|
||||
strategiesRecyclerView.layoutManager = LinearLayoutManager(this)
|
||||
strategiesRecyclerView.adapter = strategyAdapter
|
||||
|
||||
lifecycleScope.launch {
|
||||
val previousResults = loadResults()
|
||||
|
||||
if (previousResults.isNotEmpty()) {
|
||||
progressTextView.text = getString(R.string.test_complete)
|
||||
disclaimerTextView.visibility = View.GONE
|
||||
|
||||
strategies.clear()
|
||||
strategies.addAll(previousResults)
|
||||
|
||||
strategyAdapter.updateStrategies(strategies)
|
||||
}
|
||||
|
||||
if (isTesting) {
|
||||
progressTextView.text = getString(R.string.test_proxy_error)
|
||||
disclaimerTextView.text = getString(R.string.test_crash)
|
||||
disclaimerTextView.visibility = View.VISIBLE
|
||||
isTesting = false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -104,7 +125,14 @@ class TestActivity : BaseActivity() {
|
||||
override fun handleOnBackPressed() {
|
||||
if (isTesting) {
|
||||
stopTesting()
|
||||
} else {
|
||||
if (appStatus.first == AppStatus.Running) {
|
||||
val intent = Intent(this@TestActivity, MainActivity::class.java)
|
||||
intent.flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP
|
||||
startActivity(intent)
|
||||
}
|
||||
}
|
||||
|
||||
finish()
|
||||
}
|
||||
})
|
||||
@@ -119,6 +147,10 @@ class TestActivity : BaseActivity() {
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
return when (item.itemId) {
|
||||
R.id.action_copy_log -> {
|
||||
copyLog()
|
||||
true
|
||||
}
|
||||
R.id.action_settings -> {
|
||||
if (!isTesting) {
|
||||
val intent = Intent(this, TestSettingsActivity::class.java)
|
||||
@@ -161,41 +193,49 @@ class TestActivity : BaseActivity() {
|
||||
cmds = loadCmds()
|
||||
|
||||
if (sites.isEmpty()) {
|
||||
resultsTextView.text = ""
|
||||
appendTextToResults("${getString(R.string.test_settings_domain_empty)}\n")
|
||||
Toast.makeText(this, R.string.test_settings_domain_empty, Toast.LENGTH_LONG).show()
|
||||
return
|
||||
}
|
||||
|
||||
testJob = lifecycleScope.launch(Dispatchers.IO) {
|
||||
isTesting = true
|
||||
savedCmd = prefs.getString("byedpi_cmd_args", "").orEmpty()
|
||||
clearLog()
|
||||
savedCmd = prefs.getCmdArgs()
|
||||
|
||||
strategies.clear()
|
||||
strategies.addAll(cmds.map { StrategyResult(command = it) })
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
disclaimerTextView.visibility = View.GONE
|
||||
|
||||
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
|
||||
startStopButton.text = getString(R.string.test_stop)
|
||||
progressTextView.text = ""
|
||||
resultsTextView.text = ""
|
||||
|
||||
strategyAdapter.setTestingState(true)
|
||||
strategyAdapter.updateStrategies(strategies, sortByPercentage = false)
|
||||
}
|
||||
|
||||
if (isProxyRunning()) {
|
||||
ServiceManager.stop(this@TestActivity)
|
||||
waitForProxyStatus(AppStatus.Halted)
|
||||
}
|
||||
|
||||
val fullLog = prefs.getBoolean("byedpi_proxytest_fulllog", false)
|
||||
val logClickable = prefs.getBoolean("byedpi_proxytest_logclickable", false)
|
||||
val delaySec = prefs.getIntStringNotNull("byedpi_proxytest_delay", 1)
|
||||
val requestsCount = prefs.getIntStringNotNull("byedpi_proxytest_requests", 1)
|
||||
val requestTimeout = prefs.getLongStringNotNull("byedpi_proxytest_timeout", 5)
|
||||
val requestLimit = prefs.getIntStringNotNull("byedpi_proxytest_limit", 20)
|
||||
|
||||
val successfulCmds = mutableListOf<Triple<String, Int, Int>>()
|
||||
|
||||
for ((index, cmd) in cmds.withIndex()) {
|
||||
for (strategyIndex in strategies.indices) {
|
||||
if (!isActive) break
|
||||
|
||||
val cmdIndex = index + 1
|
||||
val strategy = strategies[strategyIndex]
|
||||
val cmdIndex = strategyIndex + 1
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
progressTextView.text = getString(R.string.test_process, cmdIndex, cmds.size)
|
||||
}
|
||||
|
||||
updateCmdArgs(cmd)
|
||||
updateCmdArgs(strategy.command)
|
||||
|
||||
if (isProxyRunning()) stopTesting()
|
||||
else ServiceManager.start(this@TestActivity, Mode.Proxy)
|
||||
@@ -204,38 +244,37 @@ class TestActivity : BaseActivity() {
|
||||
stopTesting()
|
||||
}
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
if (logClickable) {
|
||||
appendLinkToResults("$cmd\n")
|
||||
} else {
|
||||
appendTextToResults("$cmd\n")
|
||||
}
|
||||
}
|
||||
|
||||
delay(delaySec * 500L)
|
||||
|
||||
val totalRequests = sites.size * requestsCount
|
||||
val checkResults = siteChecker.checkSitesAsync(
|
||||
strategy.totalRequests = totalRequests
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
strategyAdapter.notifyItemChanged(strategyIndex)
|
||||
}
|
||||
|
||||
siteChecker.checkSitesAsync(
|
||||
sites = sites,
|
||||
requestsCount = requestsCount,
|
||||
requestTimeout = requestTimeout,
|
||||
fullLog = fullLog,
|
||||
concurrentRequests = requestLimit,
|
||||
fullLog = true,
|
||||
onSiteChecked = { site, successCount, countRequests ->
|
||||
lifecycleScope.launch(Dispatchers.Main) {
|
||||
appendTextToResults("$site - $successCount/$countRequests\n")
|
||||
strategy.currentProgress += countRequests
|
||||
strategy.successCount += successCount
|
||||
strategy.siteResults.add(SiteResult(site, successCount, countRequests))
|
||||
|
||||
strategyAdapter.notifyItemChanged(strategyIndex, "progress")
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
val successfulCount = checkResults.sumOf { it.second }
|
||||
val successPercentage = (successfulCount * 100) / totalRequests
|
||||
|
||||
if (successPercentage >= 50) successfulCmds.add(Triple(cmd, successfulCount, totalRequests))
|
||||
|
||||
delay(delaySec * 500L)
|
||||
strategy.isCompleted = true
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
appendTextToResults("$successfulCount/$totalRequests ($successPercentage%)\n\n")
|
||||
strategyAdapter.updateStrategies(strategies, sortByPercentage = true)
|
||||
saveResults(strategies)
|
||||
}
|
||||
|
||||
if (isProxyRunning()) ServiceManager.stop(this@TestActivity)
|
||||
@@ -245,21 +284,7 @@ class TestActivity : BaseActivity() {
|
||||
stopTesting()
|
||||
}
|
||||
|
||||
delay(1000L)
|
||||
}
|
||||
|
||||
successfulCmds.sortByDescending { it.second }
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
appendTextToResults("${getString(R.string.test_good_cmds)}\n\n")
|
||||
|
||||
successfulCmds.forEachIndexed { index, (cmd, successCount, total) ->
|
||||
appendTextToResults("${index + 1}. ")
|
||||
appendLinkToResults("$cmd\n")
|
||||
appendTextToResults("$successCount/$total\n\n")
|
||||
}
|
||||
|
||||
appendTextToResults(getString(R.string.test_complete_info))
|
||||
delay(delaySec * 500L)
|
||||
}
|
||||
|
||||
stopTesting()
|
||||
@@ -271,10 +296,10 @@ class TestActivity : BaseActivity() {
|
||||
return
|
||||
}
|
||||
|
||||
isTesting = false
|
||||
updateCmdArgs(savedCmd)
|
||||
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
isTesting = false
|
||||
updateCmdArgs(savedCmd)
|
||||
|
||||
testJob?.cancel()
|
||||
testJob = null
|
||||
|
||||
@@ -286,109 +311,60 @@ class TestActivity : BaseActivity() {
|
||||
window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
|
||||
startStopButton.text = getString(R.string.test_start)
|
||||
progressTextView.text = getString(R.string.test_complete)
|
||||
|
||||
strategyAdapter.setTestingState(false)
|
||||
strategyAdapter.updateStrategies(strategies, sortByPercentage = true)
|
||||
|
||||
saveResults(strategies)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun appendTextToResults(text: String) {
|
||||
resultsTextView.append(text)
|
||||
if (isTesting) saveLog(text)
|
||||
scrollToBottom()
|
||||
}
|
||||
|
||||
private fun appendLinkToResults(text: String) {
|
||||
val spannableString = SpannableString(text)
|
||||
val menuItems = arrayOf(
|
||||
getString(R.string.cmd_history_apply),
|
||||
getString(R.string.cmd_history_copy)
|
||||
)
|
||||
|
||||
spannableString.setSpan(
|
||||
object : ClickableSpan() {
|
||||
override fun onClick(widget: View) {
|
||||
AlertDialog.Builder(this@TestActivity)
|
||||
.setTitle(getString(R.string.cmd_history_menu))
|
||||
.setItems(menuItems) { _, which ->
|
||||
when (which) {
|
||||
0 -> addToHistory(text.trim())
|
||||
1 -> copyToClipboard(text.trim())
|
||||
}
|
||||
}
|
||||
.show()
|
||||
}
|
||||
},
|
||||
0,
|
||||
text.length,
|
||||
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
|
||||
)
|
||||
|
||||
resultsTextView.append(spannableString)
|
||||
if (isTesting) saveLog("{$text}")
|
||||
scrollToBottom()
|
||||
}
|
||||
|
||||
private fun scrollToBottom() {
|
||||
scrollTextView.post {
|
||||
scrollTextView.fullScroll(ScrollView.FOCUS_DOWN)
|
||||
}
|
||||
}
|
||||
|
||||
private fun copyToClipboard(text: String) {
|
||||
val clipboard = getSystemService(CLIPBOARD_SERVICE) as ClipboardManager
|
||||
val clip = ClipData.newPlainText("command", text)
|
||||
clipboard.setPrimaryClip(clip)
|
||||
}
|
||||
|
||||
private fun addToHistory(command: String) {
|
||||
updateCmdArgs(command)
|
||||
cmdHistoryUtils.addCommand(command)
|
||||
}
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
updateCmdArgs(command)
|
||||
cmdHistoryUtils.addCommand(command)
|
||||
|
||||
private fun displayLog(log: String) {
|
||||
log.split("{", "}").forEachIndexed { index, part ->
|
||||
if (index % 2 == 0) {
|
||||
appendTextToResults(part)
|
||||
val mode = prefs.mode()
|
||||
if (mode == Mode.VPN && VpnService.prepare(this@TestActivity) != null) return@launch
|
||||
|
||||
val toastText = if (appStatus.first == AppStatus.Running) {
|
||||
ServiceManager.restart(this@TestActivity, mode)
|
||||
R.string.service_restart
|
||||
} else {
|
||||
appendLinkToResults(part)
|
||||
R.string.cmd_history_applied
|
||||
}
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
Toast.makeText(this@TestActivity, toastText, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun saveLog(text: String) {
|
||||
val file = File(filesDir, "proxy_test.log")
|
||||
file.appendText(text)
|
||||
private fun saveResults(results: List<StrategyResult>) {
|
||||
val file = File(filesDir, "proxy_test_results.json")
|
||||
val json = gson.toJson(results)
|
||||
file.writeText(json)
|
||||
}
|
||||
|
||||
private fun loadLog(): String {
|
||||
val file = File(filesDir, "proxy_test.log")
|
||||
return if (file.exists()) file.readText() else ""
|
||||
}
|
||||
|
||||
private fun clearLog() {
|
||||
val file = File(filesDir, "proxy_test.log")
|
||||
file.writeText("")
|
||||
private fun loadResults(): List<StrategyResult> {
|
||||
val file = File(filesDir, "proxy_test_results.json")
|
||||
return if (file.exists()) {
|
||||
try {
|
||||
val json = file.readText()
|
||||
val type = object : TypeToken<List<StrategyResult>>() {}.type
|
||||
gson.fromJson<List<StrategyResult>>(json, type) ?: emptyList()
|
||||
} catch (e: Exception) {
|
||||
emptyList()
|
||||
}
|
||||
} else {
|
||||
emptyList()
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadSites(): List<String> {
|
||||
val defaultDomainLists = setOf("youtube", "googlevideo")
|
||||
val selectedDomainLists = prefs.getStringSet("byedpi_proxytest_domain_lists", defaultDomainLists)?: return emptyList()
|
||||
|
||||
val allDomains = mutableListOf<String>()
|
||||
|
||||
for (domainList in selectedDomainLists) {
|
||||
val domains = when (domainList) {
|
||||
"custom" -> {
|
||||
val customDomains = prefs.getString("byedpi_proxytest_domains", "").orEmpty()
|
||||
customDomains.lines().map { it.trim() }.filter { it.isNotEmpty() }
|
||||
}
|
||||
else -> {
|
||||
assets.open("proxytest_$domainList.sites").bufferedReader().useLines { it.toList() }
|
||||
}
|
||||
}
|
||||
allDomains.addAll(domains)
|
||||
}
|
||||
|
||||
return allDomains.distinct()
|
||||
DomainListUtils.syncLists(this)
|
||||
return DomainListUtils.getActiveDomains(this)
|
||||
}
|
||||
|
||||
private fun loadCmds(): List<String> {
|
||||
@@ -397,10 +373,38 @@ class TestActivity : BaseActivity() {
|
||||
|
||||
return if (userCommands) {
|
||||
val content = prefs.getStringNotNull("byedpi_proxytest_commands", "")
|
||||
content.replace("{sni}", sniValue).lines().map { it.trim() }.filter { it.isNotEmpty() }
|
||||
content.replace("{sni}", "\"${sniValue}\"").lines().map { it.trim() }.filter { it.isNotEmpty() }
|
||||
} else {
|
||||
val content = assets.open("proxytest_strategies.list").bufferedReader().readText()
|
||||
content.replace("{sni}", sniValue).lines().map { it.trim() }.filter { it.isNotEmpty() }
|
||||
content.replace("{sni}", "\"${sniValue}\"").lines().map { it.trim() }.filter { it.isNotEmpty() }
|
||||
}
|
||||
}
|
||||
|
||||
private fun copyLog() {
|
||||
val completeStrategies = strategies.filter { it.isCompleted }
|
||||
|
||||
if (completeStrategies.isEmpty()) {
|
||||
Toast.makeText(this, R.string.toast_copied, Toast.LENGTH_SHORT).show()
|
||||
return
|
||||
}
|
||||
|
||||
val sb = StringBuilder()
|
||||
|
||||
completeStrategies.forEach { strategy ->
|
||||
sb.appendLine("${strategy.command}\n")
|
||||
|
||||
strategy.siteResults.forEach { site ->
|
||||
sb.appendLine("${site.site} - ${site.successCount}/${site.totalCount}")
|
||||
}
|
||||
|
||||
sb.appendLine("\n${strategy.successCount}/${strategy.totalRequests}")
|
||||
sb.appendLine("-------------")
|
||||
}
|
||||
|
||||
val clipboard = getSystemService(CLIPBOARD_SERVICE) as android.content.ClipboardManager
|
||||
val clip = android.content.ClipData.newPlainText("proxy_test_log", sb.toString())
|
||||
clipboard.setPrimaryClip(clip)
|
||||
|
||||
Toast.makeText(this, R.string.toast_copied, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package io.github.romanvht.byedpi.activities
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.MenuItem
|
||||
import io.github.romanvht.byedpi.R
|
||||
import io.github.romanvht.byedpi.fragments.DomainListsFragment
|
||||
import io.github.romanvht.byedpi.fragments.ProxyTestSettingsFragment
|
||||
|
||||
class TestSettingsActivity : BaseActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.activity_test_settings)
|
||||
setupToolbar()
|
||||
|
||||
if (savedInstanceState == null) {
|
||||
val openFragment = intent.getStringExtra("open_fragment")
|
||||
|
||||
when (openFragment) {
|
||||
"domain_lists" -> {
|
||||
supportFragmentManager
|
||||
.beginTransaction()
|
||||
.replace(R.id.test_settings, DomainListsFragment())
|
||||
.commit()
|
||||
}
|
||||
else -> {
|
||||
supportFragmentManager
|
||||
.beginTransaction()
|
||||
.replace(R.id.test_settings, ProxyTestSettingsFragment())
|
||||
.commit()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean = when (item.itemId) {
|
||||
android.R.id.home -> {
|
||||
onBackPressedDispatcher.onBackPressed()
|
||||
true
|
||||
}
|
||||
else -> super.onOptionsItemSelected(item)
|
||||
}
|
||||
}
|
||||
+22
-38
@@ -1,4 +1,4 @@
|
||||
package io.github.dovecoteescapee.byedpi.activities
|
||||
package io.github.romanvht.byedpi.activities
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.SharedPreferences
|
||||
@@ -8,12 +8,13 @@ import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.util.Log
|
||||
import androidx.core.content.edit
|
||||
import io.github.dovecoteescapee.byedpi.data.AppStatus
|
||||
import io.github.dovecoteescapee.byedpi.data.Mode
|
||||
import io.github.dovecoteescapee.byedpi.services.ServiceManager
|
||||
import io.github.dovecoteescapee.byedpi.services.appStatus
|
||||
import io.github.dovecoteescapee.byedpi.utility.getPreferences
|
||||
import io.github.dovecoteescapee.byedpi.utility.mode
|
||||
import io.github.romanvht.byedpi.data.AppStatus
|
||||
import io.github.romanvht.byedpi.data.Mode
|
||||
import io.github.romanvht.byedpi.services.ServiceManager
|
||||
import io.github.romanvht.byedpi.services.appStatus
|
||||
import io.github.romanvht.byedpi.utility.getCmdArgs
|
||||
import io.github.romanvht.byedpi.utility.getPreferences
|
||||
import io.github.romanvht.byedpi.utility.mode
|
||||
|
||||
class ToggleActivity : Activity() {
|
||||
|
||||
@@ -73,6 +74,17 @@ class ToggleActivity : Activity() {
|
||||
Log.i(TAG, "Toggle service start")
|
||||
}
|
||||
|
||||
private fun restartService() {
|
||||
val mode = prefs.mode()
|
||||
|
||||
if (mode == Mode.VPN && VpnService.prepare(this) != null) {
|
||||
return
|
||||
}
|
||||
|
||||
ServiceManager.restart(this, mode)
|
||||
Log.i(TAG, "Toggle service start")
|
||||
}
|
||||
|
||||
private fun stopService() {
|
||||
ServiceManager.stop(this)
|
||||
Log.i(TAG, "Toggle service stop")
|
||||
@@ -86,11 +98,7 @@ class ToggleActivity : Activity() {
|
||||
}
|
||||
AppStatus.Running -> {
|
||||
if (restart) {
|
||||
stopService()
|
||||
waitForServiceStop { success ->
|
||||
Log.i(TAG, "Service stop: $success")
|
||||
startService()
|
||||
}
|
||||
restartService()
|
||||
} else {
|
||||
stopService()
|
||||
}
|
||||
@@ -99,7 +107,7 @@ class ToggleActivity : Activity() {
|
||||
}
|
||||
|
||||
private fun updateStrategy(strategy: String?): Boolean {
|
||||
val current = prefs.getString("byedpi_cmd_args", null)
|
||||
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")
|
||||
@@ -107,28 +115,4 @@ class ToggleActivity : Activity() {
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
private fun waitForServiceStop(onComplete: (Boolean) -> Unit) {
|
||||
val startTime = System.currentTimeMillis()
|
||||
val handler = Handler(Looper.getMainLooper())
|
||||
|
||||
fun check() {
|
||||
val (status) = appStatus
|
||||
val elapsed = System.currentTimeMillis() - startTime
|
||||
|
||||
when {
|
||||
status == AppStatus.Halted -> {
|
||||
Log.i(TAG, "Service stopped")
|
||||
onComplete(true)
|
||||
}
|
||||
elapsed >= 3000L -> {
|
||||
Log.w(TAG, "Timeout waiting for service to stop")
|
||||
onComplete(false)
|
||||
}
|
||||
else -> handler.postDelayed({ check() }, 100L)
|
||||
}
|
||||
}
|
||||
|
||||
check()
|
||||
}
|
||||
}
|
||||
}
|
||||
+64
-12
@@ -1,4 +1,4 @@
|
||||
package io.github.dovecoteescapee.byedpi.adapters
|
||||
package io.github.romanvht.byedpi.adapters
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
@@ -13,8 +13,13 @@ import android.widget.TextView
|
||||
import androidx.core.content.edit
|
||||
import androidx.preference.PreferenceManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import io.github.dovecoteescapee.byedpi.R
|
||||
import io.github.dovecoteescapee.byedpi.data.AppInfo
|
||||
import io.github.romanvht.byedpi.R
|
||||
import io.github.romanvht.byedpi.data.AppInfo
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
class AppSelectionAdapter(
|
||||
context: Context,
|
||||
@@ -22,30 +27,71 @@ class AppSelectionAdapter(
|
||||
) : RecyclerView.Adapter<AppSelectionAdapter.ViewHolder>(), Filterable {
|
||||
|
||||
private val context = context.applicationContext
|
||||
private val pm = context.packageManager
|
||||
private val originalApps: List<AppInfo> = allApps
|
||||
private val filteredApps: MutableList<AppInfo> = allApps.toMutableList()
|
||||
private val adapterScope = CoroutineScope(Dispatchers.Main + Job())
|
||||
|
||||
class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {
|
||||
val appIcon: ImageView = view.findViewById(R.id.appIcon)
|
||||
val appName: TextView = view.findViewById(R.id.appName)
|
||||
val appPackageName: TextView = view.findViewById(R.id.appPackageName)
|
||||
val appCheckBox: CheckBox = view.findViewById(R.id.appCheckBox)
|
||||
var iconLoadJob: Job? = null
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
||||
val view = LayoutInflater.from(parent.context).inflate(R.layout.app_selection_item, parent, false)
|
||||
return ViewHolder(view)
|
||||
val holder = ViewHolder(view)
|
||||
|
||||
holder.itemView.setOnClickListener {
|
||||
val position = holder.bindingAdapterPosition
|
||||
if (position != RecyclerView.NO_POSITION) {
|
||||
val app = filteredApps[position]
|
||||
app.isSelected = !app.isSelected
|
||||
notifyItemChanged(position)
|
||||
updateSelectedApps()
|
||||
}
|
||||
}
|
||||
|
||||
return holder
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
||||
val app = filteredApps[position]
|
||||
holder.appIcon.setImageDrawable(app.icon)
|
||||
holder.appName.text = app.appName
|
||||
|
||||
if (app.appName == app.packageName) {
|
||||
holder.appName.text = app.packageName
|
||||
holder.appPackageName.visibility = View.GONE
|
||||
} else {
|
||||
holder.appName.text = app.appName
|
||||
holder.appPackageName.text = app.packageName
|
||||
holder.appPackageName.visibility = View.VISIBLE
|
||||
}
|
||||
|
||||
holder.appCheckBox.isChecked = app.isSelected
|
||||
|
||||
holder.itemView.setOnClickListener {
|
||||
app.isSelected = !app.isSelected
|
||||
holder.appCheckBox.isChecked = app.isSelected
|
||||
updateSelectedApps()
|
||||
holder.iconLoadJob?.cancel()
|
||||
|
||||
if (app.icon != null) {
|
||||
holder.appIcon.setImageDrawable(app.icon)
|
||||
} else {
|
||||
holder.appIcon.setImageDrawable(pm.defaultActivityIcon)
|
||||
|
||||
holder.iconLoadJob = adapterScope.launch {
|
||||
val icon = withContext(Dispatchers.IO) {
|
||||
try {
|
||||
pm.getApplicationIcon(app.packageName)
|
||||
} catch (_: Exception) {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
if (icon != null) {
|
||||
app.icon = icon
|
||||
holder.appIcon.setImageDrawable(icon)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,6 +99,11 @@ class AppSelectionAdapter(
|
||||
return filteredApps.size
|
||||
}
|
||||
|
||||
override fun onViewRecycled(holder: ViewHolder) {
|
||||
super.onViewRecycled(holder)
|
||||
holder.iconLoadJob?.cancel()
|
||||
}
|
||||
|
||||
override fun getFilter(): Filter {
|
||||
return object : Filter() {
|
||||
override fun performFiltering(constraint: CharSequence?): FilterResults {
|
||||
@@ -61,7 +112,8 @@ class AppSelectionAdapter(
|
||||
|
||||
if (query.isNotEmpty()) {
|
||||
filteredList = originalApps.filter {
|
||||
it.appName.lowercase().contains(query)
|
||||
it.appName.lowercase().contains(query) ||
|
||||
it.packageName.lowercase().contains(query)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -89,4 +141,4 @@ class AppSelectionAdapter(
|
||||
val selectedApps = getAllSelectedPackages()
|
||||
prefs.edit { putStringSet("selected_apps", selectedApps) }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
package io.github.romanvht.byedpi.adapters
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.CheckBox
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.TextView
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.ListAdapter
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import io.github.romanvht.byedpi.R
|
||||
import io.github.romanvht.byedpi.data.DomainList
|
||||
|
||||
class DomainListAdapter(
|
||||
private val onStateChanged: (DomainList) -> Unit,
|
||||
private val onEdit: (DomainList) -> Unit,
|
||||
private val onDelete: (DomainList) -> Unit,
|
||||
private val onCopy: (DomainList) -> Unit
|
||||
) : ListAdapter<DomainList, DomainListAdapter.DomainListViewHolder>(DiffCallback()) {
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): DomainListViewHolder {
|
||||
val view = LayoutInflater.from(parent.context).inflate(R.layout.item_domain_list, parent, false)
|
||||
val holder = DomainListViewHolder(view)
|
||||
|
||||
holder.checkbox.setOnClickListener {
|
||||
val position = holder.bindingAdapterPosition
|
||||
if (position != RecyclerView.NO_POSITION) {
|
||||
onStateChanged(getItem(position))
|
||||
}
|
||||
}
|
||||
|
||||
holder.contentLayout.setOnClickListener {
|
||||
val position = holder.bindingAdapterPosition
|
||||
if (position != RecyclerView.NO_POSITION) {
|
||||
holder.showActionDialog(getItem(position), onEdit, onCopy, onDelete)
|
||||
}
|
||||
}
|
||||
|
||||
return holder
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: DomainListViewHolder, position: Int) {
|
||||
holder.bind(getItem(position))
|
||||
}
|
||||
|
||||
class DomainListViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
|
||||
private val nameText: TextView = itemView.findViewById(R.id.list_name)
|
||||
private val countText: TextView = itemView.findViewById(R.id.list_count)
|
||||
val checkbox: CheckBox = itemView.findViewById(R.id.list_checkbox)
|
||||
val contentLayout: LinearLayout = itemView.findViewById(R.id.list_content)
|
||||
|
||||
fun bind(domainList: DomainList) {
|
||||
nameText.text = domainList.name
|
||||
|
||||
val domains = domainList.domains.take(5).joinToString("\n")
|
||||
val summary = if (domainList.domains.size > 5) "$domains\n..." else domains
|
||||
countText.text = summary
|
||||
|
||||
checkbox.isChecked = domainList.isActive
|
||||
}
|
||||
|
||||
fun showActionDialog(
|
||||
domainList: DomainList,
|
||||
onEdit: (DomainList) -> Unit,
|
||||
onCopy: (DomainList) -> Unit,
|
||||
onDelete: (DomainList) -> Unit
|
||||
) {
|
||||
val context = itemView.context
|
||||
val options = arrayOf(
|
||||
context.getString(R.string.domain_list_edit),
|
||||
context.getString(R.string.cmd_history_copy),
|
||||
context.getString(R.string.domain_list_delete)
|
||||
)
|
||||
|
||||
AlertDialog.Builder(context, R.style.CustomAlertDialog)
|
||||
.setTitle(domainList.name)
|
||||
.setItems(options) { _, which ->
|
||||
when (which) {
|
||||
0 -> onEdit(domainList)
|
||||
1 -> onCopy(domainList)
|
||||
2 -> onDelete(domainList)
|
||||
}
|
||||
}
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.show()
|
||||
}
|
||||
}
|
||||
|
||||
private class DiffCallback : DiffUtil.ItemCallback<DomainList>() {
|
||||
override fun areItemsTheSame(oldItem: DomainList, newItem: DomainList): Boolean {
|
||||
return oldItem.id == newItem.id
|
||||
}
|
||||
|
||||
override fun areContentsTheSame(oldItem: DomainList, newItem: DomainList): Boolean {
|
||||
return oldItem == newItem
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package io.github.romanvht.byedpi.adapters
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.TextView
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import io.github.romanvht.byedpi.R
|
||||
import io.github.romanvht.byedpi.data.SiteResult
|
||||
|
||||
class SiteResultAdapter : RecyclerView.Adapter<SiteResultAdapter.SiteViewHolder>() {
|
||||
|
||||
private val sites = mutableListOf<SiteResult>()
|
||||
|
||||
class SiteViewHolder(view: View) : RecyclerView.ViewHolder(view) {
|
||||
val siteTextView: TextView = view.findViewById(R.id.siteTextView)
|
||||
val resultTextView: TextView = view.findViewById(R.id.siteResultTextView)
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SiteViewHolder {
|
||||
val view = LayoutInflater.from(parent.context).inflate(R.layout.item_site_result, parent, false)
|
||||
return SiteViewHolder(view)
|
||||
}
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
override fun onBindViewHolder(holder: SiteViewHolder, position: Int) {
|
||||
val site = sites[position]
|
||||
holder.siteTextView.text = site.site
|
||||
holder.resultTextView.text = "${site.successCount}/${site.totalCount}"
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int {
|
||||
return sites.size
|
||||
}
|
||||
|
||||
@SuppressLint("NotifyDataSetChanged")
|
||||
fun updateSites(newSites: List<SiteResult>) {
|
||||
sites.clear()
|
||||
sites.addAll(newSites)
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,186 @@
|
||||
package io.github.romanvht.byedpi.adapters
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ImageView
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.ProgressBar
|
||||
import android.widget.TextView
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import io.github.romanvht.byedpi.R
|
||||
import io.github.romanvht.byedpi.data.StrategyResult
|
||||
import androidx.core.view.isGone
|
||||
import androidx.core.view.isVisible
|
||||
import io.github.romanvht.byedpi.utility.ClipboardUtils
|
||||
|
||||
class StrategyResultAdapter(
|
||||
private val context: Context,
|
||||
private val onApply: (String) -> Unit
|
||||
) : RecyclerView.Adapter<StrategyResultAdapter.StrategyViewHolder>() {
|
||||
|
||||
private var isTesting = false
|
||||
private val strategies = mutableListOf<StrategyResult>()
|
||||
|
||||
class StrategyViewHolder(view: View) : RecyclerView.ViewHolder(view) {
|
||||
val commandTextView: TextView = view.findViewById(R.id.commandTextView)
|
||||
val progressLayout: LinearLayout = view.findViewById(R.id.progressLayout)
|
||||
val progressBar: ProgressBar = view.findViewById(R.id.progressBar)
|
||||
val progressTextView: TextView = view.findViewById(R.id.progressTextView)
|
||||
val expandButton: LinearLayout = view.findViewById(R.id.expandButton)
|
||||
val expandTextView: TextView = view.findViewById(R.id.expandTextView)
|
||||
val expandIcon: ImageView = view.findViewById(R.id.expandIcon)
|
||||
val sitesRecyclerView: RecyclerView = view.findViewById(R.id.sitesRecyclerView)
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): StrategyViewHolder {
|
||||
val view = LayoutInflater.from(parent.context).inflate(R.layout.item_strategy_result, parent, false)
|
||||
val holder = StrategyViewHolder(view)
|
||||
|
||||
holder.commandTextView.setOnClickListener {
|
||||
val position = holder.bindingAdapterPosition
|
||||
if (position != RecyclerView.NO_POSITION) {
|
||||
showCommandMenu(strategies[position].command)
|
||||
}
|
||||
}
|
||||
|
||||
holder.expandButton.setOnClickListener {
|
||||
val position = holder.bindingAdapterPosition
|
||||
if (position != RecyclerView.NO_POSITION) {
|
||||
strategies[position].isExpanded = !strategies[position].isExpanded
|
||||
notifyItemChanged(position)
|
||||
}
|
||||
}
|
||||
|
||||
return holder
|
||||
}
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
override fun onBindViewHolder(holder: StrategyViewHolder, position: Int) {
|
||||
val strategy = strategies[position]
|
||||
|
||||
holder.commandTextView.text = strategy.command
|
||||
|
||||
if (strategy.totalRequests > 0) {
|
||||
holder.progressLayout.visibility = View.VISIBLE
|
||||
holder.progressTextView.visibility = View.VISIBLE
|
||||
|
||||
val isProgress = isTesting && !strategy.isCompleted
|
||||
|
||||
if (isProgress) {
|
||||
holder.progressBar.isIndeterminate = true
|
||||
holder.progressTextView.text = "${strategy.successCount}/${strategy.totalRequests}"
|
||||
} else {
|
||||
holder.progressBar.isIndeterminate = false
|
||||
holder.progressBar.max = strategy.totalRequests
|
||||
holder.progressBar.progress = strategy.successCount
|
||||
holder.progressTextView.text = "${strategy.successCount}/${strategy.totalRequests}"
|
||||
}
|
||||
} else {
|
||||
holder.progressLayout.visibility = View.GONE
|
||||
}
|
||||
|
||||
if (strategy.siteResults.isNotEmpty()) {
|
||||
holder.expandButton.visibility = View.VISIBLE
|
||||
|
||||
if (strategy.isExpanded) {
|
||||
holder.expandTextView.text = context.getString(R.string.test_hide_details)
|
||||
holder.expandIcon.rotation = 180f
|
||||
holder.sitesRecyclerView.visibility = View.VISIBLE
|
||||
|
||||
val siteAdapter = SiteResultAdapter()
|
||||
holder.sitesRecyclerView.layoutManager = LinearLayoutManager(context)
|
||||
holder.sitesRecyclerView.adapter = siteAdapter
|
||||
siteAdapter.updateSites(strategy.siteResults)
|
||||
} else {
|
||||
holder.expandTextView.text = context.getString(R.string.test_show_details)
|
||||
holder.expandIcon.rotation = 0f
|
||||
holder.sitesRecyclerView.visibility = View.GONE
|
||||
}
|
||||
} else {
|
||||
holder.expandButton.visibility = View.GONE
|
||||
holder.sitesRecyclerView.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
override fun onBindViewHolder(holder: StrategyViewHolder, position: Int, payloads: MutableList<Any>) {
|
||||
if (payloads.isEmpty()) {
|
||||
super.onBindViewHolder(holder, position, payloads)
|
||||
} else {
|
||||
val strategy = strategies[position]
|
||||
|
||||
if (strategy.totalRequests > 0) {
|
||||
val isProgress = isTesting && !strategy.isCompleted
|
||||
|
||||
if (isProgress) {
|
||||
holder.progressBar.isIndeterminate = true
|
||||
holder.progressTextView.text = "${strategy.successCount}/${strategy.totalRequests}"
|
||||
} else {
|
||||
holder.progressBar.isIndeterminate = false
|
||||
holder.progressBar.max = strategy.totalRequests
|
||||
holder.progressBar.progress = strategy.successCount
|
||||
holder.progressTextView.text = "${strategy.successCount}/${strategy.totalRequests}"
|
||||
}
|
||||
}
|
||||
|
||||
if (strategy.siteResults.isNotEmpty() && holder.expandButton.isGone) {
|
||||
holder.expandButton.visibility = View.VISIBLE
|
||||
}
|
||||
|
||||
if (strategy.isExpanded && holder.sitesRecyclerView.isVisible) {
|
||||
val adapter = holder.sitesRecyclerView.adapter
|
||||
if (adapter is SiteResultAdapter) {
|
||||
adapter.updateSites(strategy.siteResults)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int {
|
||||
return strategies.size
|
||||
}
|
||||
|
||||
@SuppressLint("NotifyDataSetChanged")
|
||||
fun setTestingState(testing: Boolean) {
|
||||
isTesting = testing
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
|
||||
@SuppressLint("NotifyDataSetChanged")
|
||||
fun updateStrategies(newStrategies: List<StrategyResult>, sortByPercentage: Boolean = true) {
|
||||
strategies.clear()
|
||||
|
||||
val sorted = if (sortByPercentage) {
|
||||
newStrategies.sortedWith(compareByDescending<StrategyResult> { it.isCompleted }
|
||||
.thenByDescending { it.successPercentage }
|
||||
.thenByDescending { it.successCount })
|
||||
} else {
|
||||
newStrategies
|
||||
}
|
||||
|
||||
strategies.addAll(sorted)
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
|
||||
private fun showCommandMenu(command: String) {
|
||||
val menuItems = arrayOf(
|
||||
context.getString(R.string.cmd_history_apply),
|
||||
context.getString(R.string.cmd_history_copy)
|
||||
)
|
||||
|
||||
AlertDialog.Builder(context)
|
||||
.setTitle(context.getString(R.string.cmd_history_menu))
|
||||
.setItems(menuItems) { _, which ->
|
||||
when (which) {
|
||||
0 -> onApply(command)
|
||||
1 -> ClipboardUtils.copy(context, command, "command")
|
||||
}
|
||||
}
|
||||
.show()
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package io.github.dovecoteescapee.byedpi.core
|
||||
package io.github.romanvht.byedpi.core
|
||||
|
||||
class ByeDpiProxy {
|
||||
companion object {
|
||||
@@ -0,0 +1,164 @@
|
||||
package io.github.romanvht.byedpi.core
|
||||
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import android.util.Log
|
||||
import io.github.romanvht.byedpi.data.UISettings
|
||||
import io.github.romanvht.byedpi.utility.DomainListUtils
|
||||
import io.github.romanvht.byedpi.utility.checkIpAndPortInCmd
|
||||
import io.github.romanvht.byedpi.utility.getCmdArgs
|
||||
import io.github.romanvht.byedpi.utility.getCmdEnable
|
||||
import io.github.romanvht.byedpi.utility.getStringNotNull
|
||||
import io.github.romanvht.byedpi.utility.shellSplit
|
||||
|
||||
sealed interface ByeDpiProxyPreferences {
|
||||
companion object {
|
||||
fun fromSharedPreferences(preferences: SharedPreferences, context: Context): ByeDpiProxyPreferences =
|
||||
when (preferences.getCmdEnable()) {
|
||||
true -> ByeDpiProxyCmdPreferences(preferences, context)
|
||||
false -> ByeDpiProxyUIPreferences(preferences)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class ByeDpiProxyCmdPreferences(val args: Array<String>) : ByeDpiProxyPreferences {
|
||||
constructor(preferences: SharedPreferences, context: Context) : this(
|
||||
parseCmdToArguments(preferences, context)
|
||||
)
|
||||
|
||||
companion object {
|
||||
private fun parseCmdToArguments(preferences: SharedPreferences, context: Context): Array<String> {
|
||||
val cmd = preferences.getCmdArgs()
|
||||
val preparedCmd = getLists(cmd, context)
|
||||
|
||||
val firstArgIndex = preparedCmd.indexOf("-")
|
||||
val args = (if (firstArgIndex > 0) preparedCmd.substring(firstArgIndex) else preparedCmd).trim()
|
||||
|
||||
Log.d("ProxyPref", "CMD: $args")
|
||||
|
||||
val (cmdIp, cmdPort) = preferences.checkIpAndPortInCmd()
|
||||
val ip = preferences.getStringNotNull("byedpi_proxy_ip", "127.0.0.1")
|
||||
val port = preferences.getStringNotNull("byedpi_proxy_port", "1080")
|
||||
|
||||
val prefix = buildString {
|
||||
if (cmdIp == null) append("--ip $ip ")
|
||||
if (cmdPort == null) append("--port $port ")
|
||||
}
|
||||
|
||||
Log.d("ProxyPref", "Added from settings: $prefix")
|
||||
|
||||
val blacklist = setOf("--help", "--version", "-h", "-v")
|
||||
val splitArgs = shellSplit("$prefix$args").filter { it !in blacklist }
|
||||
|
||||
return arrayOf("ciadpi") + splitArgs
|
||||
}
|
||||
|
||||
private fun getLists(cmd: String, context: Context): String {
|
||||
return Regex("""\{list:([^}]+)\}""").replace(cmd) { matchResult ->
|
||||
val listName = matchResult.groupValues[1].trim()
|
||||
val lists = DomainListUtils.getLists(context)
|
||||
val domainList = lists.find { it.name.equals(listName, ignoreCase = true) }
|
||||
|
||||
if (domainList != null && domainList.domains.isNotEmpty()) {
|
||||
domainList.domains.joinToString(" ")
|
||||
} else {
|
||||
Log.w("ProxyPref", "List '$listName' not found or empty")
|
||||
""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class ByeDpiProxyUIPreferences(val settings: UISettings = UISettings()) : ByeDpiProxyPreferences {
|
||||
|
||||
constructor(preferences: SharedPreferences) : this(
|
||||
UISettings.fromSharedPreferences(preferences)
|
||||
)
|
||||
|
||||
val uiargs: Array<String>
|
||||
get() {
|
||||
val args = mutableListOf("ciadpi")
|
||||
|
||||
if (settings.ip.isNotEmpty()) args.add("-i${settings.ip}")
|
||||
if (settings.port != 0) args.add("-p${settings.port}")
|
||||
if (settings.maxConnections != 0) args.add("-c${settings.maxConnections}")
|
||||
if (settings.bufferSize != 0) args.add("-b${settings.bufferSize}")
|
||||
|
||||
val protocols = buildList {
|
||||
if (settings.desyncHttps) add("t")
|
||||
if (settings.desyncHttp) add("h")
|
||||
}
|
||||
|
||||
if (!settings.hosts.isNullOrBlank()) {
|
||||
val hostStr = ":${settings.hosts.replace("\n", " ")}"
|
||||
when (settings.hostsMode) {
|
||||
UISettings.HostsMode.Blacklist -> {
|
||||
args.add("-H$hostStr")
|
||||
args.add("-An")
|
||||
if (protocols.isNotEmpty()) args.add("-K${protocols.joinToString(",")}")
|
||||
}
|
||||
UISettings.HostsMode.Whitelist -> {
|
||||
if (protocols.isNotEmpty()) args.add("-K${protocols.joinToString(",")}")
|
||||
args.add("-H$hostStr")
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
} else {
|
||||
if (protocols.isNotEmpty()) args.add("-K${protocols.joinToString(",")}")
|
||||
}
|
||||
|
||||
if (settings.defaultTtl != 0) args.add("-g${settings.defaultTtl}")
|
||||
if (settings.noDomain) args.add("-N")
|
||||
|
||||
if (settings.splitPosition != 0) {
|
||||
val posArg = settings.splitPosition.toString() + if (settings.splitAtHost) "+h" else ""
|
||||
val option = when (settings.desyncMethod) {
|
||||
UISettings.DesyncMethod.Split -> "-s"
|
||||
UISettings.DesyncMethod.Disorder -> "-d"
|
||||
UISettings.DesyncMethod.OOB -> "-o"
|
||||
UISettings.DesyncMethod.DISOOB -> "-q"
|
||||
UISettings.DesyncMethod.Fake -> "-f"
|
||||
UISettings.DesyncMethod.None -> ""
|
||||
}
|
||||
if (option.isNotEmpty()) args.add("$option$posArg")
|
||||
}
|
||||
|
||||
if (settings.desyncMethod == UISettings.DesyncMethod.Fake) {
|
||||
if (settings.fakeTtl != 0) args.add("-t${settings.fakeTtl}")
|
||||
if (settings.fakeSni.isNotEmpty()) args.add("-n${settings.fakeSni}")
|
||||
if (settings.fakeOffset != 0) args.add("-O${settings.fakeOffset}")
|
||||
}
|
||||
|
||||
if (settings.desyncMethod == UISettings.DesyncMethod.OOB || settings.desyncMethod == UISettings.DesyncMethod.DISOOB) {
|
||||
args.add("-e${settings.oobChar[0].code.toByte()}")
|
||||
}
|
||||
|
||||
val modHttpFlags = buildList {
|
||||
if (settings.hostMixedCase) add("h")
|
||||
if (settings.domainMixedCase) add("d")
|
||||
if (settings.hostRemoveSpaces) add("r")
|
||||
}
|
||||
|
||||
if (modHttpFlags.isNotEmpty()) args.add("-M${modHttpFlags.joinToString(",")}")
|
||||
|
||||
if (settings.tlsRecordSplit && settings.tlsRecordSplitPosition != 0) {
|
||||
val tlsRecArg = settings.tlsRecordSplitPosition.toString() + if (settings.tlsRecordSplitAtSni) "+s" else ""
|
||||
args.add("-r$tlsRecArg")
|
||||
}
|
||||
|
||||
if (settings.tcpFastOpen) args.add("-F")
|
||||
if (settings.dropSack) args.add("-Y")
|
||||
|
||||
args.add("-An")
|
||||
|
||||
if (settings.desyncUdp) {
|
||||
args.add("-Ku")
|
||||
if (settings.udpFakeCount != 0) args.add("-a${settings.udpFakeCount}")
|
||||
args.add("-An")
|
||||
}
|
||||
|
||||
Log.d("ProxyPref", "UI to cmd: ${args.joinToString(" ")}")
|
||||
return args.toTypedArray()
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package io.github.dovecoteescapee.byedpi.core
|
||||
package io.github.romanvht.byedpi.core
|
||||
|
||||
object TProxyService {
|
||||
init {
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package io.github.dovecoteescapee.byedpi.data
|
||||
package io.github.romanvht.byedpi.data
|
||||
|
||||
const val START_ACTION = "start"
|
||||
const val STOP_ACTION = "stop"
|
||||
+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,
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
package io.github.dovecoteescapee.byedpi.data
|
||||
package io.github.romanvht.byedpi.data
|
||||
|
||||
import io.github.dovecoteescapee.byedpi.BuildConfig
|
||||
import io.github.romanvht.byedpi.BuildConfig
|
||||
|
||||
const val STARTED_BROADCAST = "${BuildConfig.APPLICATION_ID}.STARTED"
|
||||
const val STOPPED_BROADCAST = "${BuildConfig.APPLICATION_ID}.STOPPED"
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package io.github.dovecoteescapee.byedpi.data
|
||||
package io.github.romanvht.byedpi.data
|
||||
|
||||
data class Command(
|
||||
var text: String,
|
||||
@@ -0,0 +1,12 @@
|
||||
package io.github.romanvht.byedpi.data
|
||||
|
||||
data class DomainList(
|
||||
val id: String,
|
||||
val name: String,
|
||||
val domains: List<String>,
|
||||
val isActive: Boolean = true,
|
||||
|
||||
val isBuiltIn: Boolean = false,
|
||||
val isModified: Boolean = false,
|
||||
val isDeleted: Boolean = false
|
||||
)
|
||||
+1
-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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
+17
-16
@@ -1,4 +1,4 @@
|
||||
package io.github.dovecoteescapee.byedpi.fragments
|
||||
package io.github.romanvht.byedpi.fragments
|
||||
|
||||
import android.content.SharedPreferences
|
||||
import android.content.pm.ApplicationInfo
|
||||
@@ -14,10 +14,12 @@ import androidx.lifecycle.lifecycleScope
|
||||
import androidx.preference.PreferenceManager
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import io.github.dovecoteescapee.byedpi.R
|
||||
import io.github.dovecoteescapee.byedpi.adapters.AppSelectionAdapter
|
||||
import io.github.dovecoteescapee.byedpi.data.AppInfo
|
||||
import io.github.romanvht.byedpi.R
|
||||
import io.github.romanvht.byedpi.adapters.AppSelectionAdapter
|
||||
import io.github.romanvht.byedpi.data.AppInfo
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.awaitAll
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
@@ -42,7 +44,6 @@ class AppSelectionFragment : Fragment() {
|
||||
|
||||
setupRecyclerView()
|
||||
setupSearchView()
|
||||
|
||||
loadApps()
|
||||
|
||||
return view
|
||||
@@ -71,10 +72,11 @@ class AppSelectionFragment : Fragment() {
|
||||
|
||||
private fun loadApps() {
|
||||
progressBar.visibility = View.VISIBLE
|
||||
searchView.visibility = View.GONE
|
||||
|
||||
lifecycleScope.launch {
|
||||
val apps = withContext(Dispatchers.IO) {
|
||||
getInstalledApps()
|
||||
getInstalledAppsAsync()
|
||||
}
|
||||
|
||||
adapter = AppSelectionAdapter(requireContext(), apps)
|
||||
@@ -84,14 +86,19 @@ class AppSelectionFragment : Fragment() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun getInstalledApps(): List<AppInfo> {
|
||||
private suspend fun getInstalledAppsAsync(): List<AppInfo> {
|
||||
val pm = requireContext().packageManager
|
||||
val installedApps = pm.getInstalledApplications(0)
|
||||
val selectedApps = prefs.getStringSet("selected_apps", setOf()) ?: setOf()
|
||||
|
||||
return installedApps
|
||||
.filter { it.packageName != requireContext().packageName }
|
||||
.map { createAppInfo(it, pm, selectedApps) }
|
||||
.map { appInfo ->
|
||||
lifecycleScope.async(Dispatchers.IO) {
|
||||
createAppInfo(appInfo, pm, selectedApps)
|
||||
}
|
||||
}
|
||||
.awaitAll()
|
||||
.sortedWith(compareBy({ !it.isSelected }, { it.appName.lowercase() }))
|
||||
}
|
||||
|
||||
@@ -106,17 +113,11 @@ class AppSelectionFragment : Fragment() {
|
||||
appInfo.packageName
|
||||
}
|
||||
|
||||
val appIcon = try {
|
||||
pm.getApplicationIcon(appInfo.packageName)
|
||||
} catch (_: Exception) {
|
||||
pm.defaultActivityIcon
|
||||
}
|
||||
|
||||
return AppInfo(
|
||||
appName,
|
||||
appInfo.packageName,
|
||||
appIcon,
|
||||
selectedApps.contains(appInfo.packageName)
|
||||
selectedApps.contains(appInfo.packageName),
|
||||
icon = null
|
||||
)
|
||||
}
|
||||
}
|
||||
+96
-40
@@ -1,51 +1,53 @@
|
||||
package io.github.dovecoteescapee.byedpi.fragments
|
||||
package io.github.romanvht.byedpi.fragments
|
||||
|
||||
import android.content.ClipData
|
||||
import android.content.ClipboardManager
|
||||
import android.content.Context
|
||||
import android.net.VpnService
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.widget.EditText
|
||||
import android.widget.ImageView
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
import androidx.preference.*
|
||||
import io.github.dovecoteescapee.byedpi.R
|
||||
import io.github.dovecoteescapee.byedpi.utility.findPreferenceNotNull
|
||||
import io.github.romanvht.byedpi.R
|
||||
import io.github.romanvht.byedpi.utility.findPreferenceNotNull
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import io.github.dovecoteescapee.byedpi.data.Command
|
||||
import io.github.dovecoteescapee.byedpi.utility.HistoryUtils
|
||||
import io.github.romanvht.byedpi.data.AppStatus
|
||||
import io.github.romanvht.byedpi.data.Command
|
||||
import io.github.romanvht.byedpi.data.Mode
|
||||
import io.github.romanvht.byedpi.services.ServiceManager
|
||||
import io.github.romanvht.byedpi.services.appStatus
|
||||
import io.github.romanvht.byedpi.utility.ClipboardUtils
|
||||
import io.github.romanvht.byedpi.utility.HistoryUtils
|
||||
import io.github.romanvht.byedpi.utility.getCmdArgs
|
||||
import io.github.romanvht.byedpi.utility.getPreferences
|
||||
import io.github.romanvht.byedpi.utility.mode
|
||||
|
||||
class ByeDpiCommandLineSettingsFragment : PreferenceFragmentCompat() {
|
||||
class ByeDpiCMDSettingsFragment : PreferenceFragmentCompat() {
|
||||
|
||||
private lateinit var cmdHistoryUtils: HistoryUtils
|
||||
private lateinit var editTextPreference: EditTextPreference
|
||||
private lateinit var historyHeader: Preference
|
||||
private lateinit var clearButton: Preference
|
||||
private val historyPreferences = mutableListOf<Preference>()
|
||||
|
||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||
val uiScale = resources.displayMetrics.density
|
||||
val threshold = 3f
|
||||
|
||||
val overlay = if (uiScale > threshold) R.style.Theme_ByeDPI_History_NoPaddingLeft
|
||||
else R.style.Theme_ByeDPI_History_PaddingLeft
|
||||
|
||||
requireActivity().theme.applyStyle(overlay, true)
|
||||
setPreferencesFromResource(R.xml.byedpi_cmd_settings, rootKey)
|
||||
|
||||
cmdHistoryUtils = HistoryUtils(requireContext())
|
||||
|
||||
editTextPreference = findPreferenceNotNull("byedpi_cmd_args")
|
||||
editTextPreference.text = requireContext().getPreferences().getCmdArgs()
|
||||
historyHeader = findPreferenceNotNull("cmd_history_header")
|
||||
clearButton = findPreferenceNotNull("clear_cmd_args")
|
||||
|
||||
editTextPreference.setOnPreferenceChangeListener { _, newValue ->
|
||||
val newCommand = newValue.toString()
|
||||
if (newCommand.isNotBlank()) cmdHistoryUtils.addCommand(newCommand)
|
||||
updateHistoryItems()
|
||||
true
|
||||
}
|
||||
|
||||
clearButton.setOnPreferenceClickListener {
|
||||
editTextPreference.text = ""
|
||||
if (newCommand.isNotBlank()) {
|
||||
cmdHistoryUtils.addCommand(newCommand)
|
||||
restartService()
|
||||
}
|
||||
|
||||
updateHistoryItems()
|
||||
true
|
||||
}
|
||||
|
||||
@@ -57,7 +59,31 @@ class ByeDpiCommandLineSettingsFragment : PreferenceFragmentCompat() {
|
||||
updateHistoryItems()
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
view.post {
|
||||
view.findViewById<View>(R.id.btn_clear)?.setOnClickListener {
|
||||
editTextPreference.text = ""
|
||||
updateHistoryItems()
|
||||
}
|
||||
|
||||
view.findViewById<View>(R.id.btn_paste)?.setOnClickListener {
|
||||
val text = ClipboardUtils.paste(requireContext())
|
||||
if (!text.isNullOrBlank()) {
|
||||
editTextPreference.text = text
|
||||
cmdHistoryUtils.addCommand(text)
|
||||
updateHistoryItems()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateHistoryItems() {
|
||||
if (listView !== null) {
|
||||
listView.itemAnimator = null
|
||||
}
|
||||
|
||||
historyPreferences.forEach { preference ->
|
||||
preferenceScreen.removePreference(preference)
|
||||
}
|
||||
@@ -77,9 +103,34 @@ class ByeDpiCommandLineSettingsFragment : PreferenceFragmentCompat() {
|
||||
}
|
||||
|
||||
private fun createPreference(command: Command) =
|
||||
Preference(requireContext()).apply {
|
||||
object : Preference(requireContext()) {
|
||||
override fun onBindViewHolder(holder: PreferenceViewHolder) {
|
||||
super.onBindViewHolder(holder)
|
||||
|
||||
val nameView = holder.itemView.findViewById<TextView>(R.id.command_name)
|
||||
val summaryView = holder.itemView.findViewById<TextView>(android.R.id.summary)
|
||||
val pinIcon = holder.itemView.findViewById<ImageView>(R.id.pin_icon)
|
||||
val commandName = command.name
|
||||
|
||||
if (!commandName.isNullOrBlank()) {
|
||||
nameView.visibility = View.VISIBLE
|
||||
nameView.text = command.name
|
||||
} else {
|
||||
nameView.visibility = View.GONE
|
||||
}
|
||||
|
||||
val summaryText = buildSummary(command)
|
||||
if (summaryText.isNotBlank()) {
|
||||
summaryView.visibility = View.VISIBLE
|
||||
summaryView.text = summaryText
|
||||
} else {
|
||||
summaryView.visibility = View.GONE
|
||||
}
|
||||
|
||||
pinIcon.visibility = if (command.pinned) View.VISIBLE else View.GONE
|
||||
}
|
||||
}.apply {
|
||||
title = command.text
|
||||
summary = buildSummary(command)
|
||||
layoutResource = R.layout.history_item
|
||||
setOnPreferenceClickListener {
|
||||
showActionDialog(command)
|
||||
@@ -88,15 +139,13 @@ class ByeDpiCommandLineSettingsFragment : PreferenceFragmentCompat() {
|
||||
}
|
||||
|
||||
private fun buildSummary(command: Command): String {
|
||||
val summary = StringBuilder()
|
||||
if (command.name != null) {
|
||||
summary.append(command.name)
|
||||
val parts = mutableListOf<String>()
|
||||
|
||||
if (command.text == editTextPreference.text) {
|
||||
parts.add(getString(R.string.cmd_history_applied))
|
||||
}
|
||||
if (command.pinned) {
|
||||
if (summary.isNotEmpty()) summary.append(" - ")
|
||||
summary.append(context?.getString(R.string.cmd_history_pinned))
|
||||
}
|
||||
return summary.toString()
|
||||
|
||||
return parts.joinToString(" • ")
|
||||
}
|
||||
|
||||
private fun showHistoryClearDialog() {
|
||||
@@ -145,7 +194,7 @@ class ByeDpiCommandLineSettingsFragment : PreferenceFragmentCompat() {
|
||||
1 -> if (command.pinned) unpinCommand(command.text) else pinCommand(command.text)
|
||||
2 -> showRenameDialog(command)
|
||||
3 -> showEditDialog(command)
|
||||
4 -> copyToClipboard(command.text)
|
||||
4 -> ClipboardUtils.copy(requireContext(), command.text, "command")
|
||||
5 -> deleteCommand(command.text)
|
||||
}
|
||||
}
|
||||
@@ -205,6 +254,8 @@ class ByeDpiCommandLineSettingsFragment : PreferenceFragmentCompat() {
|
||||
|
||||
private fun applyCommand(command: String) {
|
||||
editTextPreference.text = command
|
||||
updateHistoryItems()
|
||||
restartService()
|
||||
}
|
||||
|
||||
private fun pinCommand(command: String) {
|
||||
@@ -222,9 +273,14 @@ class ByeDpiCommandLineSettingsFragment : PreferenceFragmentCompat() {
|
||||
updateHistoryItems()
|
||||
}
|
||||
|
||||
private fun copyToClipboard(command: String) {
|
||||
val clipboard = requireContext().getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
||||
val clip = ClipData.newPlainText("Command", command)
|
||||
clipboard.setPrimaryClip(clip)
|
||||
private fun restartService() {
|
||||
if (appStatus.first == AppStatus.Running) {
|
||||
val ctx = context ?: return
|
||||
val mode = ctx.getPreferences().mode()
|
||||
if (mode == Mode.VPN && VpnService.prepare(ctx) != null) return
|
||||
|
||||
ServiceManager.restart(ctx, mode)
|
||||
Toast.makeText(ctx, R.string.service_restart, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+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()
|
||||
}
|
||||
}
|
||||
+106
-103
@@ -1,68 +1,22 @@
|
||||
package io.github.dovecoteescapee.byedpi.fragments
|
||||
package io.github.romanvht.byedpi.fragments
|
||||
|
||||
import android.Manifest
|
||||
import android.content.Intent
|
||||
import android.content.SharedPreferences
|
||||
import android.content.pm.PackageManager
|
||||
import androidx.core.net.toUri
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.os.Environment
|
||||
import android.provider.Settings
|
||||
import android.util.Log
|
||||
import androidx.appcompat.app.AppCompatDelegate
|
||||
import androidx.core.app.ActivityCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.os.LocaleListCompat
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.core.content.edit
|
||||
import androidx.preference.*
|
||||
import io.github.dovecoteescapee.byedpi.BuildConfig
|
||||
import io.github.dovecoteescapee.byedpi.R
|
||||
import io.github.dovecoteescapee.byedpi.activities.TestActivity
|
||||
import io.github.dovecoteescapee.byedpi.data.Mode
|
||||
import io.github.dovecoteescapee.byedpi.utility.*
|
||||
import io.github.romanvht.byedpi.R
|
||||
import io.github.romanvht.byedpi.BuildConfig
|
||||
import io.github.romanvht.byedpi.activities.TestActivity
|
||||
import io.github.romanvht.byedpi.data.Mode
|
||||
import io.github.romanvht.byedpi.utility.*
|
||||
import androidx.core.net.toUri
|
||||
|
||||
class MainSettingsFragment : PreferenceFragmentCompat() {
|
||||
companion object {
|
||||
private val TAG: String = MainSettingsFragment::class.java.simpleName
|
||||
private const val STORAGE_PERMISSION_REQUEST = 1001
|
||||
|
||||
fun setLang(lang: String) {
|
||||
val appLocale = localeByName(lang) ?: throw IllegalStateException("Invalid value for language: $lang")
|
||||
|
||||
if (AppCompatDelegate.getApplicationLocales().toLanguageTags() != appLocale.toLanguageTags()) {
|
||||
AppCompatDelegate.setApplicationLocales(appLocale)
|
||||
}
|
||||
}
|
||||
|
||||
private fun localeByName(lang: String): LocaleListCompat? = when (lang) {
|
||||
"system" -> LocaleListCompat.getEmptyLocaleList()
|
||||
"ru" -> LocaleListCompat.forLanguageTags("ru")
|
||||
"en" -> LocaleListCompat.forLanguageTags("en")
|
||||
"tr" -> LocaleListCompat.forLanguageTags("tr")
|
||||
"kk" -> LocaleListCompat.forLanguageTags("kk")
|
||||
else -> {
|
||||
Log.w(TAG, "Invalid value for language: $lang")
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
fun setTheme(name: String) {
|
||||
val appTheme = themeByName(name) ?: throw IllegalStateException("Invalid value for app_theme: $name")
|
||||
|
||||
if (AppCompatDelegate.getDefaultNightMode() != appTheme) {
|
||||
AppCompatDelegate.setDefaultNightMode(appTheme)
|
||||
}
|
||||
}
|
||||
|
||||
private fun themeByName(name: String): Int? = when (name) {
|
||||
"system" -> AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM
|
||||
"light" -> AppCompatDelegate.MODE_NIGHT_NO
|
||||
"dark" -> AppCompatDelegate.MODE_NIGHT_YES
|
||||
else -> {
|
||||
Log.w(TAG, "Invalid value for app_theme: $name")
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val preferenceListener =
|
||||
@@ -79,13 +33,13 @@ class MainSettingsFragment : PreferenceFragmentCompat() {
|
||||
|
||||
findPreferenceNotNull<ListPreference>("language")
|
||||
.setOnPreferenceChangeListener { _, newValue ->
|
||||
setLang(newValue as String)
|
||||
SettingsUtils.setLang(newValue as String)
|
||||
true
|
||||
}
|
||||
|
||||
findPreferenceNotNull<ListPreference>("app_theme")
|
||||
.setOnPreferenceChangeListener { _, newValue ->
|
||||
setTheme(newValue as String)
|
||||
SettingsUtils.setTheme(newValue as String)
|
||||
true
|
||||
}
|
||||
|
||||
@@ -96,14 +50,32 @@ class MainSettingsFragment : PreferenceFragmentCompat() {
|
||||
true
|
||||
}
|
||||
|
||||
findPreferenceNotNull<Preference>("battery_optimization")
|
||||
.setOnPreferenceClickListener {
|
||||
BatteryUtils.requestBatteryOptimization(requireContext())
|
||||
true
|
||||
}
|
||||
|
||||
findPreferenceNotNull<Preference>("storage_access")
|
||||
.setOnPreferenceClickListener {
|
||||
requestStoragePermission()
|
||||
StorageUtils.requestStoragePermission(this)
|
||||
true
|
||||
}
|
||||
|
||||
findPreferenceNotNull<Preference>("donate")
|
||||
.setOnPreferenceClickListener {
|
||||
showDonateDialog()
|
||||
true
|
||||
}
|
||||
|
||||
findPreferenceNotNull<Preference>("dns_ip_picker")
|
||||
.setOnPreferenceClickListener {
|
||||
showDnsDialog()
|
||||
true
|
||||
}
|
||||
|
||||
findPreferenceNotNull<Preference>("version").summary = BuildConfig.VERSION_NAME
|
||||
findPreferenceNotNull<Preference>("byedpi_version").summary = "0.17.3"
|
||||
findPreferenceNotNull<Preference>("byedpi_version").summary = "0.17.3 (ba53229)"
|
||||
|
||||
updatePreferences()
|
||||
}
|
||||
@@ -119,56 +91,17 @@ class MainSettingsFragment : PreferenceFragmentCompat() {
|
||||
sharedPreferences?.unregisterOnSharedPreferenceChangeListener(preferenceListener)
|
||||
}
|
||||
|
||||
private fun hasStoragePermission(): Boolean {
|
||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
Environment.isExternalStorageManager()
|
||||
} else {
|
||||
val readPermission = ContextCompat.checkSelfPermission(
|
||||
requireContext(),
|
||||
Manifest.permission.READ_EXTERNAL_STORAGE
|
||||
) == PackageManager.PERMISSION_GRANTED
|
||||
|
||||
val writePermission = ContextCompat.checkSelfPermission(
|
||||
requireContext(),
|
||||
Manifest.permission.WRITE_EXTERNAL_STORAGE
|
||||
) == PackageManager.PERMISSION_GRANTED
|
||||
|
||||
readPermission && writePermission
|
||||
}
|
||||
}
|
||||
|
||||
private fun requestStoragePermission() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
try {
|
||||
val intent = Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION).apply {
|
||||
data = "package:${requireContext().packageName}".toUri()
|
||||
}
|
||||
startActivity(intent)
|
||||
} catch (e: Exception) {
|
||||
val intent = Intent(Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION)
|
||||
startActivity(intent)
|
||||
}
|
||||
} else {
|
||||
ActivityCompat.requestPermissions(
|
||||
requireActivity(),
|
||||
arrayOf(
|
||||
Manifest.permission.READ_EXTERNAL_STORAGE,
|
||||
Manifest.permission.WRITE_EXTERNAL_STORAGE
|
||||
),
|
||||
STORAGE_PERMISSION_REQUEST
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun updatePreferences() {
|
||||
val cmdEnable = findPreferenceNotNull<SwitchPreference>("byedpi_enable_cmd_settings").isChecked
|
||||
val mode = findPreferenceNotNull<ListPreference>("byedpi_mode").value.let { Mode.fromString(it) }
|
||||
val dns = findPreferenceNotNull<EditTextPreference>("dns_ip")
|
||||
val dns = findPreferenceNotNull<Preference>("dns_ip_picker")
|
||||
val dnsEdit = findPreferenceNotNull<EditTextPreference>("dns_ip")
|
||||
val ipv6 = findPreferenceNotNull<SwitchPreference>("ipv6_enable")
|
||||
val proxy = findPreferenceNotNull<PreferenceCategory>("byedpi_proxy_category")
|
||||
|
||||
val applistType = findPreferenceNotNull<ListPreference>("applist_type")
|
||||
val selectedApps = findPreferenceNotNull<Preference>("selected_apps")
|
||||
val batteryOptimization = findPreferenceNotNull<Preference>("battery_optimization")
|
||||
val storageAccess = findPreferenceNotNull<Preference>("storage_access")
|
||||
|
||||
val uiSettings = findPreferenceNotNull<Preference>("byedpi_ui_settings")
|
||||
@@ -186,6 +119,9 @@ class MainSettingsFragment : PreferenceFragmentCompat() {
|
||||
cmdSettings.isEnabled = cmdEnable
|
||||
proxyTest.isEnabled = cmdEnable
|
||||
|
||||
dns.summary = dnsSummary()
|
||||
dnsEdit.isVisible = false
|
||||
|
||||
when (mode) {
|
||||
Mode.VPN -> {
|
||||
dns.isVisible = true
|
||||
@@ -216,10 +152,77 @@ class MainSettingsFragment : PreferenceFragmentCompat() {
|
||||
}
|
||||
}
|
||||
|
||||
if (hasStoragePermission()) {
|
||||
if (BatteryUtils.isOptimizationDisabled(requireContext())) {
|
||||
batteryOptimization.summary = getString(R.string.battery_optimization_disabled_summary)
|
||||
} else {
|
||||
batteryOptimization.summary = getString(R.string.battery_optimization_summary)
|
||||
}
|
||||
|
||||
if (StorageUtils.hasStoragePermission(requireContext())) {
|
||||
storageAccess.summary = getString(R.string.storage_access_allowed_summary)
|
||||
} else {
|
||||
storageAccess.summary = getString(R.string.storage_access_summary)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val dnsPresets: List<Pair<String, String>>
|
||||
get() = listOf(
|
||||
"" to getString(R.string.dns_system),
|
||||
"8.8.8.8" to getString(R.string.dns_google),
|
||||
"1.1.1.1" to getString(R.string.dns_cloudflare),
|
||||
"9.9.9.9" to getString(R.string.dns_quad9),
|
||||
)
|
||||
|
||||
private fun dnsSummary(): String {
|
||||
val ip = sharedPreferences?.getStringNotNull("dns_ip", "1.1.1.1") ?: "1.1.1.1"
|
||||
val preset = dnsPresets.firstOrNull { it.first == ip } ?: return ip
|
||||
return if (preset.first.isBlank()) preset.second else "${preset.second} ($ip)"
|
||||
}
|
||||
|
||||
private fun showDnsDialog() {
|
||||
val context = requireContext()
|
||||
val presets = dnsPresets
|
||||
|
||||
val items = (presets.map { it.second } + getString(R.string.dns_custom)).toTypedArray()
|
||||
val dnsPref = findPreferenceNotNull<EditTextPreference>("dns_ip")
|
||||
|
||||
AlertDialog.Builder(context)
|
||||
.setTitle(getString(R.string.dbs_ip_setting))
|
||||
.setItems(items) { _, which ->
|
||||
if (which < presets.size) {
|
||||
sharedPreferences?.edit { putString("dns_ip", presets[which].first) }
|
||||
} else {
|
||||
onDisplayPreferenceDialog(dnsPref)
|
||||
}
|
||||
}
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.show()
|
||||
}
|
||||
|
||||
private fun showDonateDialog() {
|
||||
val context = requireContext()
|
||||
|
||||
val items = arrayOf(
|
||||
getString(R.string.donate_message),
|
||||
getString(R.string.donate_cloudtips),
|
||||
getString(R.string.donate_telegram_bot),
|
||||
getString(R.string.donate_thanks),
|
||||
)
|
||||
|
||||
AlertDialog.Builder(context)
|
||||
.setTitle(getString(R.string.donate))
|
||||
.setItems(items) { _, which ->
|
||||
when (which) {
|
||||
1 -> openUrl("https://pay.cloudtips.ru/p/92c754db")
|
||||
2 -> openUrl("https://t.me/romanvht_donate_bot")
|
||||
}
|
||||
}
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.show()
|
||||
}
|
||||
|
||||
private fun openUrl(url: String) {
|
||||
val intent = Intent(Intent.ACTION_VIEW, url.toUri())
|
||||
startActivity(intent)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
package io.github.romanvht.byedpi.fragments
|
||||
|
||||
import android.content.SharedPreferences
|
||||
import android.os.Bundle
|
||||
import androidx.preference.*
|
||||
import io.github.romanvht.byedpi.R
|
||||
import io.github.romanvht.byedpi.utility.*
|
||||
|
||||
class ProxyTestSettingsFragment : PreferenceFragmentCompat() {
|
||||
|
||||
private val preferenceListener =
|
||||
SharedPreferences.OnSharedPreferenceChangeListener { _, _ ->
|
||||
updatePreferences()
|
||||
}
|
||||
|
||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||
setPreferencesFromResource(R.xml.proxy_test_settings, rootKey)
|
||||
|
||||
setupNumberSummary("byedpi_proxytest_delay", R.string.proxytest_delay_desc)
|
||||
setupNumberSummary("byedpi_proxytest_requests", R.string.proxytest_requests_desc)
|
||||
setupNumberSummary("byedpi_proxytest_limit", R.string.proxytest_limit_desc)
|
||||
setupNumberSummary("byedpi_proxytest_timeout", R.string.proxytest_timeout_desc)
|
||||
|
||||
setEditTestPreferenceListenerInt("byedpi_proxytest_delay", 0, 10)
|
||||
setEditTestPreferenceListenerInt("byedpi_proxytest_requests", 1, 20)
|
||||
setEditTestPreferenceListenerInt("byedpi_proxytest_timeout", 1, 15)
|
||||
setEditTestPreferenceListenerInt("byedpi_proxytest_limit", 1, 50)
|
||||
|
||||
setEditTestPreferenceListenerDomain("byedpi_proxytest_sni")
|
||||
|
||||
updatePreferences()
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
updatePreferences()
|
||||
sharedPreferences?.registerOnSharedPreferenceChangeListener(preferenceListener)
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
sharedPreferences?.unregisterOnSharedPreferenceChangeListener(preferenceListener)
|
||||
}
|
||||
|
||||
private fun updatePreferences() {
|
||||
val switchUserCommands = findPreferenceNotNull<SwitchPreference>("byedpi_proxytest_usercommands")
|
||||
val textUserCommands = findPreferenceNotNull<EditTextPreference>("byedpi_proxytest_commands")
|
||||
val manageDomainLists = findPreferenceNotNull<Preference>("manage_domain_lists")
|
||||
val activeLists = DomainListUtils.getLists(requireContext()).filter { it.isActive }
|
||||
|
||||
manageDomainLists.summary = if (activeLists.isEmpty()) {
|
||||
getString(R.string.domain_lists_summary)
|
||||
} else {
|
||||
activeLists.joinToString(", ") { it.name }
|
||||
}
|
||||
|
||||
textUserCommands.isEnabled = switchUserCommands.isChecked
|
||||
}
|
||||
|
||||
private fun setupNumberSummary(key: String, descriptionResId: Int) {
|
||||
val pref = findPreference<EditTextPreference>(key) ?: return
|
||||
|
||||
pref.summaryProvider = Preference.SummaryProvider<EditTextPreference> { p ->
|
||||
val value = p.text ?: "—"
|
||||
val description = getString(descriptionResId)
|
||||
"$value - $description"
|
||||
}
|
||||
}
|
||||
}
|
||||
+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) {
|
||||
+26
-15
@@ -1,18 +1,18 @@
|
||||
package io.github.dovecoteescapee.byedpi.services
|
||||
package io.github.romanvht.byedpi.services
|
||||
|
||||
import android.app.Notification
|
||||
import android.app.NotificationManager
|
||||
import android.content.Intent
|
||||
import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE
|
||||
import android.content.pm.ServiceInfo
|
||||
import android.os.Build
|
||||
import android.util.Log
|
||||
import androidx.lifecycle.LifecycleService
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import io.github.dovecoteescapee.byedpi.R
|
||||
import io.github.dovecoteescapee.byedpi.core.ByeDpiProxy
|
||||
import io.github.dovecoteescapee.byedpi.core.ByeDpiProxyPreferences
|
||||
import io.github.dovecoteescapee.byedpi.data.*
|
||||
import io.github.dovecoteescapee.byedpi.utility.*
|
||||
import io.github.romanvht.byedpi.R
|
||||
import io.github.romanvht.byedpi.core.ByeDpiProxy
|
||||
import io.github.romanvht.byedpi.core.ByeDpiProxyPreferences
|
||||
import io.github.romanvht.byedpi.data.*
|
||||
import io.github.romanvht.byedpi.utility.*
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.delay
|
||||
@@ -47,6 +47,9 @@ class ByeDpiProxyService : LifecycleService() {
|
||||
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
super.onStartCommand(intent, flags, startId)
|
||||
|
||||
startForeground()
|
||||
|
||||
return when (val action = intent?.action) {
|
||||
START_ACTION -> {
|
||||
lifecycleScope.launch {
|
||||
@@ -92,15 +95,15 @@ class ByeDpiProxyService : LifecycleService() {
|
||||
|
||||
if (status == ServiceStatus.Connected) {
|
||||
Log.w(TAG, "Proxy already connected")
|
||||
updateStatus(ServiceStatus.Connected)
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
startForeground()
|
||||
mutex.withLock {
|
||||
startProxy()
|
||||
updateStatus(ServiceStatus.Connected)
|
||||
}
|
||||
updateStatus(ServiceStatus.Connected)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Failed to start proxy", e)
|
||||
updateStatus(ServiceStatus.Failed)
|
||||
@@ -114,7 +117,7 @@ class ByeDpiProxyService : LifecycleService() {
|
||||
startForeground(
|
||||
FOREGROUND_SERVICE_ID,
|
||||
notification,
|
||||
FOREGROUND_SERVICE_TYPE_SPECIAL_USE,
|
||||
ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC,
|
||||
)
|
||||
} else {
|
||||
startForeground(FOREGROUND_SERVICE_ID, notification)
|
||||
@@ -124,6 +127,12 @@ class ByeDpiProxyService : LifecycleService() {
|
||||
private suspend fun stop() {
|
||||
Log.i(TAG, "Stopping")
|
||||
|
||||
if (status != ServiceStatus.Connected) {
|
||||
Log.w(TAG, "Proxy not connected")
|
||||
updateStatus(ServiceStatus.Disconnected)
|
||||
return
|
||||
}
|
||||
|
||||
mutex.withLock {
|
||||
withContext(Dispatchers.IO) {
|
||||
stopProxy()
|
||||
@@ -147,16 +156,14 @@ class ByeDpiProxyService : LifecycleService() {
|
||||
|
||||
proxyJob = lifecycleScope.launch(Dispatchers.IO) {
|
||||
val code = proxy.startProxy(preferences)
|
||||
|
||||
delay(500)
|
||||
|
||||
if (code != 0) {
|
||||
Log.e(TAG, "Proxy stopped with code $code")
|
||||
updateStatus(ServiceStatus.Failed)
|
||||
} else {
|
||||
updateStatus(ServiceStatus.Disconnected)
|
||||
stopSelf()
|
||||
}
|
||||
|
||||
stopSelf()
|
||||
}
|
||||
|
||||
Log.i(TAG, "Proxy started")
|
||||
@@ -193,7 +200,7 @@ class ByeDpiProxyService : LifecycleService() {
|
||||
}
|
||||
|
||||
private fun getByeDpiPreferences(): ByeDpiProxyPreferences =
|
||||
ByeDpiProxyPreferences.fromSharedPreferences(getPreferences())
|
||||
ByeDpiProxyPreferences.fromSharedPreferences(getPreferences(), this)
|
||||
|
||||
private fun updateStatus(newStatus: ServiceStatus) {
|
||||
Log.d(TAG, "Proxy status changed from $status to $newStatus")
|
||||
@@ -221,6 +228,10 @@ class ByeDpiProxyService : LifecycleService() {
|
||||
)
|
||||
intent.putExtra(SENDER, Sender.Proxy.ordinal)
|
||||
sendBroadcast(intent)
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
QuickTileService.updateTile()
|
||||
}
|
||||
}
|
||||
|
||||
private fun createNotification(): Notification =
|
||||
@@ -0,0 +1,11 @@
|
||||
package io.github.romanvht.byedpi.services
|
||||
|
||||
import io.github.romanvht.byedpi.data.AppStatus
|
||||
import io.github.romanvht.byedpi.data.Mode
|
||||
|
||||
var appStatus = AppStatus.Halted to Mode.VPN
|
||||
private set
|
||||
|
||||
fun setStatus(status: AppStatus, mode: Mode) {
|
||||
appStatus = status to mode
|
||||
}
|
||||
+51
-18
@@ -1,4 +1,4 @@
|
||||
package io.github.dovecoteescapee.byedpi.services
|
||||
package io.github.romanvht.byedpi.services
|
||||
|
||||
import android.app.Notification
|
||||
import android.app.NotificationManager
|
||||
@@ -9,13 +9,13 @@ import android.os.Build
|
||||
import android.os.ParcelFileDescriptor
|
||||
import android.util.Log
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import io.github.dovecoteescapee.byedpi.R
|
||||
import io.github.dovecoteescapee.byedpi.activities.MainActivity
|
||||
import io.github.dovecoteescapee.byedpi.core.ByeDpiProxy
|
||||
import io.github.dovecoteescapee.byedpi.core.ByeDpiProxyPreferences
|
||||
import io.github.dovecoteescapee.byedpi.core.TProxyService
|
||||
import io.github.dovecoteescapee.byedpi.data.*
|
||||
import io.github.dovecoteescapee.byedpi.utility.*
|
||||
import io.github.romanvht.byedpi.R
|
||||
import io.github.romanvht.byedpi.activities.MainActivity
|
||||
import io.github.romanvht.byedpi.core.ByeDpiProxy
|
||||
import io.github.romanvht.byedpi.core.ByeDpiProxyPreferences
|
||||
import io.github.romanvht.byedpi.core.TProxyService
|
||||
import io.github.romanvht.byedpi.data.*
|
||||
import io.github.romanvht.byedpi.utility.*
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.delay
|
||||
@@ -57,6 +57,9 @@ class ByeDpiVpnService : LifecycleVpnService() {
|
||||
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
super.onStartCommand(intent, flags, startId)
|
||||
|
||||
startForeground()
|
||||
|
||||
return when (val action = intent?.action) {
|
||||
START_ACTION -> {
|
||||
lifecycleScope.launch {
|
||||
@@ -89,6 +92,22 @@ class ByeDpiVpnService : LifecycleVpnService() {
|
||||
START_NOT_STICKY
|
||||
}
|
||||
|
||||
SERVICE_INTERFACE -> {
|
||||
Log.i(TAG, "Started by Android")
|
||||
|
||||
if (getPreferences().mode() != Mode.VPN) {
|
||||
Log.w(TAG, "Always-On disabled in proxy mode")
|
||||
stopSelf()
|
||||
return START_NOT_STICKY
|
||||
}
|
||||
|
||||
lifecycleScope.launch {
|
||||
start()
|
||||
}
|
||||
|
||||
START_STICKY
|
||||
}
|
||||
|
||||
else -> {
|
||||
Log.w(TAG, "Unknown action: $action")
|
||||
START_NOT_STICKY
|
||||
@@ -109,16 +128,16 @@ class ByeDpiVpnService : LifecycleVpnService() {
|
||||
|
||||
if (status == ServiceStatus.Connected) {
|
||||
Log.w(TAG, "VPN already connected")
|
||||
updateStatus(ServiceStatus.Connected)
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
startForeground()
|
||||
mutex.withLock {
|
||||
startProxy()
|
||||
startTun2Socks()
|
||||
updateStatus(ServiceStatus.Connected)
|
||||
}
|
||||
updateStatus(ServiceStatus.Connected)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Failed to start VPN", e)
|
||||
updateStatus(ServiceStatus.Failed)
|
||||
@@ -132,7 +151,7 @@ class ByeDpiVpnService : LifecycleVpnService() {
|
||||
startForeground(
|
||||
FOREGROUND_SERVICE_ID,
|
||||
notification,
|
||||
ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE,
|
||||
ServiceInfo.FOREGROUND_SERVICE_TYPE_SYSTEM_EXEMPTED,
|
||||
)
|
||||
} else {
|
||||
startForeground(FOREGROUND_SERVICE_ID, notification)
|
||||
@@ -142,6 +161,12 @@ class ByeDpiVpnService : LifecycleVpnService() {
|
||||
private suspend fun stop() {
|
||||
Log.i(TAG, "Stopping")
|
||||
|
||||
if (status != ServiceStatus.Connected) {
|
||||
Log.w(TAG, "VPN not connected")
|
||||
updateStatus(ServiceStatus.Disconnected)
|
||||
return
|
||||
}
|
||||
|
||||
mutex.withLock {
|
||||
try {
|
||||
withContext(Dispatchers.IO) {
|
||||
@@ -169,17 +194,15 @@ class ByeDpiVpnService : LifecycleVpnService() {
|
||||
|
||||
proxyJob = lifecycleScope.launch(Dispatchers.IO) {
|
||||
val code = byeDpiProxy.startProxy(preferences)
|
||||
|
||||
delay(500)
|
||||
|
||||
if (code != 0) {
|
||||
Log.e(TAG, "Proxy stopped with code $code")
|
||||
updateStatus(ServiceStatus.Failed)
|
||||
} else {
|
||||
updateStatus(ServiceStatus.Disconnected)
|
||||
stopTun2Socks()
|
||||
stopSelf()
|
||||
}
|
||||
|
||||
stopTun2Socks()
|
||||
stopSelf()
|
||||
}
|
||||
|
||||
Log.i(TAG, "Proxy started")
|
||||
@@ -219,13 +242,14 @@ class ByeDpiVpnService : LifecycleVpnService() {
|
||||
Log.i(TAG, "Starting tun2socks")
|
||||
|
||||
if (tunFd != null) {
|
||||
Log.w(TAG, "VPN field not null")
|
||||
throw IllegalStateException("VPN field not null")
|
||||
}
|
||||
|
||||
val sharedPreferences = getPreferences()
|
||||
val (ip, port) = sharedPreferences.getProxyIpAndPort()
|
||||
|
||||
val dns = sharedPreferences.getStringNotNull("dns_ip", "8.8.8.8")
|
||||
val dns = sharedPreferences.getStringNotNull("dns_ip", "1.1.1.1")
|
||||
val ipv6 = sharedPreferences.getBoolean("ipv6_enable", false)
|
||||
|
||||
val tun2socksConfig = buildString {
|
||||
@@ -263,6 +287,11 @@ class ByeDpiVpnService : LifecycleVpnService() {
|
||||
private fun stopTun2Socks() {
|
||||
Log.i(TAG, "Stopping tun2socks")
|
||||
|
||||
if (tunFd == null) {
|
||||
Log.w(TAG, "VPN field is null, skipping")
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
TProxyService.TProxyStopService()
|
||||
} catch (e: Exception) {
|
||||
@@ -287,7 +316,7 @@ class ByeDpiVpnService : LifecycleVpnService() {
|
||||
}
|
||||
|
||||
private fun getByeDpiPreferences(): ByeDpiProxyPreferences =
|
||||
ByeDpiProxyPreferences.fromSharedPreferences(getPreferences())
|
||||
ByeDpiProxyPreferences.fromSharedPreferences(getPreferences(), this)
|
||||
|
||||
private fun updateStatus(newStatus: ServiceStatus) {
|
||||
Log.d(TAG, "VPN status changed from $status to $newStatus")
|
||||
@@ -316,6 +345,10 @@ class ByeDpiVpnService : LifecycleVpnService() {
|
||||
)
|
||||
intent.putExtra(SENDER, Sender.VPN.ordinal)
|
||||
sendBroadcast(intent)
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
QuickTileService.updateTile()
|
||||
}
|
||||
}
|
||||
|
||||
private fun createNotification(): Notification =
|
||||
+1
-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)
|
||||
}
|
||||
}
|
||||
+6
-2
@@ -1,8 +1,8 @@
|
||||
package io.github.dovecoteescapee.byedpi.utility
|
||||
package io.github.romanvht.byedpi.utility
|
||||
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import io.github.dovecoteescapee.byedpi.data.Command
|
||||
import io.github.romanvht.byedpi.data.Command
|
||||
import com.google.gson.Gson
|
||||
import androidx.core.content.edit
|
||||
|
||||
@@ -71,6 +71,10 @@ class HistoryUtils(context: Context) {
|
||||
}
|
||||
}
|
||||
|
||||
fun getPinnedHistory(): List<Command> {
|
||||
return getHistory().filter { it.pinned }
|
||||
}
|
||||
|
||||
fun saveHistory(history: List<Command>) {
|
||||
val historyJson = Gson().toJson(history)
|
||||
sharedPreferences.edit { putString(historyKey, historyJson) }
|
||||
+6
-6
@@ -1,4 +1,4 @@
|
||||
package io.github.dovecoteescapee.byedpi.utility
|
||||
package io.github.romanvht.byedpi.utility
|
||||
|
||||
import android.app.Notification
|
||||
import android.app.NotificationChannel
|
||||
@@ -9,11 +9,11 @@ import android.content.Intent
|
||||
import android.os.Build
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.core.app.NotificationCompat
|
||||
import io.github.dovecoteescapee.byedpi.R
|
||||
import io.github.dovecoteescapee.byedpi.activities.MainActivity
|
||||
import io.github.dovecoteescapee.byedpi.data.PAUSE_ACTION
|
||||
import io.github.dovecoteescapee.byedpi.data.RESUME_ACTION
|
||||
import io.github.dovecoteescapee.byedpi.data.STOP_ACTION
|
||||
import io.github.romanvht.byedpi.R
|
||||
import io.github.romanvht.byedpi.activities.MainActivity
|
||||
import io.github.romanvht.byedpi.data.PAUSE_ACTION
|
||||
import io.github.romanvht.byedpi.data.RESUME_ACTION
|
||||
import io.github.romanvht.byedpi.data.STOP_ACTION
|
||||
|
||||
fun registerNotificationChannel(context: Context, id: String, @StringRes name: Int) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
+14
-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
|
||||
@@ -32,11 +34,17 @@ fun SharedPreferences.getSelectedApps(): List<String> {
|
||||
return getStringSet("selected_apps", emptySet())?.toList() ?: emptyList()
|
||||
}
|
||||
|
||||
fun SharedPreferences.checkIpAndPortInCmd(): Pair<String?, String?> {
|
||||
val cmdEnable = getBoolean("byedpi_enable_cmd_settings", false)
|
||||
if (!cmdEnable) return Pair(null, null)
|
||||
fun SharedPreferences.getCmdEnable(): Boolean {
|
||||
return getBoolean("byedpi_enable_cmd_settings", true)
|
||||
}
|
||||
|
||||
val cmdArgs = getString("byedpi_cmd_args", "")?.let { shellSplit(it) } ?: emptyList()
|
||||
fun SharedPreferences.getCmdArgs(): String {
|
||||
return getStringNotNull("byedpi_cmd_args", DEFAULT_CMD_ARGS)
|
||||
}
|
||||
|
||||
fun SharedPreferences.checkIpAndPortInCmd(): Pair<String?, String?> {
|
||||
if (!getCmdEnable()) return Pair(null, null)
|
||||
val cmdArgs = shellSplit(getCmdArgs())
|
||||
|
||||
fun getArgValue(argsList: List<String>, keys: List<String>): String? {
|
||||
for (i in argsList.indices) {
|
||||
@@ -0,0 +1,256 @@
|
||||
package io.github.romanvht.byedpi.utility
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.util.Log
|
||||
import android.widget.Toast
|
||||
import androidx.appcompat.app.AppCompatDelegate
|
||||
import androidx.core.content.edit
|
||||
import androidx.core.os.LocaleListCompat
|
||||
import com.google.gson.Gson
|
||||
import io.github.romanvht.byedpi.BuildConfig
|
||||
import io.github.romanvht.byedpi.R
|
||||
import io.github.romanvht.byedpi.data.AppSettings
|
||||
|
||||
object SettingsUtils {
|
||||
private const val TAG = "SettingsUtils"
|
||||
private val separatePreferenceKeys = setOf("byedpi_command_history", "selected_apps")
|
||||
|
||||
enum class Section {
|
||||
SETTINGS,
|
||||
HISTORY,
|
||||
APPS,
|
||||
DOMAIN_LISTS
|
||||
}
|
||||
|
||||
fun getCurrentLanguage(context: Context): String {
|
||||
val lang = context.getPreferences().getStringNotNull("language", "system")
|
||||
if (lang != "system") return lang
|
||||
|
||||
val locales = AppCompatDelegate.getApplicationLocales()
|
||||
return locales[0]?.language ?: java.util.Locale.getDefault().language
|
||||
}
|
||||
|
||||
fun setLang(lang: String) {
|
||||
val appLocale = localeByName(lang) ?: LocaleListCompat.getEmptyLocaleList()
|
||||
|
||||
if (AppCompatDelegate.getApplicationLocales().toLanguageTags() != appLocale.toLanguageTags()) {
|
||||
AppCompatDelegate.setApplicationLocales(appLocale)
|
||||
}
|
||||
}
|
||||
|
||||
private fun localeByName(lang: String): LocaleListCompat? = when (lang) {
|
||||
"system" -> LocaleListCompat.getEmptyLocaleList()
|
||||
"ru" -> LocaleListCompat.forLanguageTags("ru")
|
||||
"en" -> LocaleListCompat.forLanguageTags("en")
|
||||
"tr" -> LocaleListCompat.forLanguageTags("tr")
|
||||
"kk" -> LocaleListCompat.forLanguageTags("kk")
|
||||
"vi" -> LocaleListCompat.forLanguageTags("vi")
|
||||
else -> {
|
||||
Log.w(TAG, "Invalid value for language: $lang")
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
fun setTheme(name: String) {
|
||||
val appTheme = themeByName(name) ?: AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM
|
||||
|
||||
if (AppCompatDelegate.getDefaultNightMode() != appTheme) {
|
||||
AppCompatDelegate.setDefaultNightMode(appTheme)
|
||||
}
|
||||
}
|
||||
|
||||
private fun themeByName(name: String): Int? = when (name) {
|
||||
"system" -> AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM
|
||||
"light" -> AppCompatDelegate.MODE_NIGHT_NO
|
||||
"dark" -> AppCompatDelegate.MODE_NIGHT_YES
|
||||
else -> {
|
||||
Log.w(TAG, "Invalid value for app_theme: $name")
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
fun exportSettings(context: Context, uri: Uri, sections: Set<Section>) {
|
||||
try {
|
||||
val prefs = context.getPreferences()
|
||||
|
||||
val export = AppSettings(
|
||||
app = BuildConfig.APPLICATION_ID,
|
||||
version = BuildConfig.VERSION_NAME,
|
||||
history = if (Section.HISTORY in sections) {
|
||||
HistoryUtils(context).getHistory()
|
||||
} else {
|
||||
null
|
||||
},
|
||||
apps = if (Section.APPS in sections) prefs.getSelectedApps() else null,
|
||||
domainLists = if (Section.DOMAIN_LISTS in sections) {
|
||||
DomainListUtils.getAllLists(context)
|
||||
} else {
|
||||
null
|
||||
},
|
||||
settings = if (Section.SETTINGS in sections) {
|
||||
prefs.all.filterKeys { it !in separatePreferenceKeys }
|
||||
} else {
|
||||
null
|
||||
}
|
||||
)
|
||||
|
||||
val json = Gson().toJson(export)
|
||||
|
||||
context.contentResolver.openOutputStream(uri)?.use { outputStream ->
|
||||
outputStream.write(json.toByteArray())
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Failed to export settings", e)
|
||||
Handler(Looper.getMainLooper()).post {
|
||||
Toast.makeText(context, R.string.export_failed, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun readSettings(context: Context, uri: Uri): AppSettings? {
|
||||
return try {
|
||||
val json = context.contentResolver.openInputStream(uri)?.use { inputStream ->
|
||||
inputStream.bufferedReader().readText()
|
||||
} ?: return null
|
||||
|
||||
val settings = Gson().fromJson(json, AppSettings::class.java)
|
||||
if (settings?.app == BuildConfig.APPLICATION_ID) {
|
||||
settings
|
||||
} else {
|
||||
Toast.makeText(context, R.string.import_failed, Toast.LENGTH_LONG).show()
|
||||
null
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Failed to read settings", e)
|
||||
Toast.makeText(context, R.string.import_failed, Toast.LENGTH_SHORT).show()
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
fun getAvailableSections(settings: AppSettings): Set<Section> = buildSet {
|
||||
if (settings.settings != null) add(Section.SETTINGS)
|
||||
if (settings.history != null) add(Section.HISTORY)
|
||||
if (settings.apps != null) add(Section.APPS)
|
||||
if (settings.domainLists != null) add(Section.DOMAIN_LISTS)
|
||||
}
|
||||
|
||||
fun importSettings(
|
||||
context: Context,
|
||||
import: AppSettings,
|
||||
sections: Set<Section>,
|
||||
onRestart: () -> Unit
|
||||
) {
|
||||
try {
|
||||
val prefs = context.getPreferences()
|
||||
|
||||
if (Section.SETTINGS in sections && import.settings != null) {
|
||||
prefs.edit(commit = true) {
|
||||
prefs.all.keys
|
||||
.filter { it !in separatePreferenceKeys }
|
||||
.forEach { remove(it) }
|
||||
|
||||
import.settings
|
||||
.filterKeys { it !in separatePreferenceKeys }
|
||||
.forEach { (key, value) ->
|
||||
when (value) {
|
||||
is Int -> putInt(key, value)
|
||||
is Boolean -> putBoolean(key, value)
|
||||
is String -> putString(key, value)
|
||||
is Float -> putFloat(key, value)
|
||||
is Long -> putLong(key, value)
|
||||
is Double -> {
|
||||
when (value) {
|
||||
value.toInt().toDouble() -> {
|
||||
putInt(key, value.toInt())
|
||||
}
|
||||
value.toLong().toDouble() -> {
|
||||
putLong(key, value.toLong())
|
||||
}
|
||||
else -> {
|
||||
putFloat(key, value.toFloat())
|
||||
}
|
||||
}
|
||||
}
|
||||
is Collection<*> -> {
|
||||
if (value.all { it is String }) {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
putStringSet(key, (value as Collection<String>).toSet())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (Section.APPS in sections && import.apps != null) {
|
||||
prefs.edit(commit = true) { putStringSet("selected_apps", import.apps.toSet()) }
|
||||
}
|
||||
|
||||
if (Section.HISTORY in sections && import.history != null) {
|
||||
HistoryUtils(context).saveHistory(import.history)
|
||||
}
|
||||
|
||||
if (Section.DOMAIN_LISTS in sections && import.domainLists != null) {
|
||||
val normalized = import.domainLists.map {
|
||||
if (it.isBuiltIn) {
|
||||
it.copy(
|
||||
isModified = it.isModified,
|
||||
isDeleted = it.isDeleted
|
||||
)
|
||||
} else {
|
||||
it.copy(
|
||||
isModified = false,
|
||||
isDeleted = false
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
DomainListUtils.saveLists(context, normalized)
|
||||
}
|
||||
|
||||
if (Section.SETTINGS in sections) {
|
||||
val newLang = prefs.getString("language", "system") ?: "system"
|
||||
val newTheme = prefs.getString("app_theme", "system") ?: "system"
|
||||
setLang(newLang)
|
||||
setTheme(newTheme)
|
||||
}
|
||||
|
||||
Handler(Looper.getMainLooper()).post {
|
||||
onRestart()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Failed to import settings", e)
|
||||
Handler(Looper.getMainLooper()).post {
|
||||
Toast.makeText(context, R.string.import_failed, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun resetSettings(context: Context, onRestart: () -> Unit) {
|
||||
try {
|
||||
val prefs = context.getPreferences()
|
||||
|
||||
prefs.edit(commit = true) {
|
||||
clear()
|
||||
}
|
||||
|
||||
HistoryUtils(context).saveHistory(emptyList())
|
||||
DomainListUtils.resetLists(context)
|
||||
|
||||
setLang("system")
|
||||
setTheme("system")
|
||||
|
||||
Handler(Looper.getMainLooper()).post {
|
||||
onRestart()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Failed to reset settings", e)
|
||||
Handler(Looper.getMainLooper()).post {
|
||||
Toast.makeText(context, "Failed to reset settings", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+4
-4
@@ -1,4 +1,4 @@
|
||||
package io.github.dovecoteescapee.byedpi.utility
|
||||
package io.github.romanvht.byedpi.utility
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
@@ -6,8 +6,8 @@ import android.content.pm.ShortcutInfo
|
||||
import android.content.pm.ShortcutManager
|
||||
import android.graphics.drawable.Icon
|
||||
import android.os.Build
|
||||
import io.github.dovecoteescapee.byedpi.R
|
||||
import io.github.dovecoteescapee.byedpi.activities.ToggleActivity
|
||||
import io.github.romanvht.byedpi.R
|
||||
import io.github.romanvht.byedpi.activities.ToggleActivity
|
||||
|
||||
object ShortcutUtils {
|
||||
|
||||
@@ -30,7 +30,7 @@ object ShortcutUtils {
|
||||
|
||||
shortcuts.add(toggleShortcut)
|
||||
|
||||
if (context.getPreferences().getBoolean("byedpi_enable_cmd_settings", false)) {
|
||||
if (context.getPreferences().getCmdEnable()) {
|
||||
val history = HistoryUtils(context)
|
||||
val pinned = history.getHistory().filter { it.pinned }.take(3)
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
package io.github.romanvht.byedpi.utility
|
||||
|
||||
import android.util.Log
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.awaitAll
|
||||
import kotlinx.coroutines.sync.Semaphore
|
||||
import kotlinx.coroutines.sync.withPermit
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.io.IOException
|
||||
import java.net.HttpURLConnection
|
||||
import java.net.InetSocketAddress
|
||||
import java.net.Proxy
|
||||
import java.net.URL
|
||||
|
||||
class SiteCheckUtils(
|
||||
private val proxyIp: String,
|
||||
private val proxyPort: Int
|
||||
) {
|
||||
|
||||
suspend fun checkSitesAsync(
|
||||
sites: List<String>,
|
||||
requestsCount: Int,
|
||||
requestTimeout: Long,
|
||||
concurrentRequests: Int = 20,
|
||||
fullLog: Boolean,
|
||||
onSiteChecked: ((String, Int, Int) -> Unit)? = null
|
||||
): List<Pair<String, Int>> {
|
||||
val semaphore = Semaphore(concurrentRequests)
|
||||
return withContext(Dispatchers.IO) {
|
||||
sites.map { site ->
|
||||
async {
|
||||
semaphore.withPermit {
|
||||
val successCount = checkSiteAccess(site, requestsCount, requestTimeout)
|
||||
if (fullLog) {
|
||||
onSiteChecked?.invoke(site, successCount, requestsCount)
|
||||
}
|
||||
site to successCount
|
||||
}
|
||||
}
|
||||
}.awaitAll()
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun checkSiteAccess(
|
||||
site: String,
|
||||
requestsCount: Int,
|
||||
timeout: Long
|
||||
): Int = withContext(Dispatchers.IO) {
|
||||
var responseCount = 0
|
||||
|
||||
val formattedUrl = if (site.startsWith("http://") || site.startsWith("https://")) site
|
||||
else "https://$site"
|
||||
|
||||
val url = try {
|
||||
URL(formattedUrl)
|
||||
} catch (_: Exception) {
|
||||
Log.e("SiteChecker", "Invalid URL: $formattedUrl")
|
||||
return@withContext 0
|
||||
}
|
||||
|
||||
val proxy = Proxy(Proxy.Type.SOCKS, InetSocketAddress(proxyIp, proxyPort))
|
||||
|
||||
repeat(requestsCount) { attempt ->
|
||||
Log.i("SiteChecker", "Attempt ${attempt + 1}/$requestsCount for $site")
|
||||
|
||||
var connection: HttpURLConnection? = null
|
||||
try {
|
||||
connection = url.openConnection(proxy) as HttpURLConnection
|
||||
connection.connectTimeout = (timeout * 1000).toInt()
|
||||
connection.readTimeout = (timeout * 1000).toInt()
|
||||
connection.instanceFollowRedirects = true
|
||||
connection.setRequestProperty("Connection", "close")
|
||||
|
||||
val responseCode = connection.responseCode
|
||||
val declaredLength = if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.N) {
|
||||
connection.contentLengthLong
|
||||
} else {
|
||||
connection.contentLength.toLong()
|
||||
}
|
||||
|
||||
var actualLength = 0L
|
||||
try {
|
||||
val inputStream = if (responseCode in 200..299) connection.inputStream else connection.errorStream
|
||||
if (inputStream != null) {
|
||||
val buffer = ByteArray(8192)
|
||||
var bytesRead: Int
|
||||
|
||||
val limit = if (declaredLength > 0) declaredLength else 1024L * 1024
|
||||
|
||||
while (actualLength < limit) {
|
||||
val remaining = limit - actualLength
|
||||
val toRead = if (remaining > buffer.size) buffer.size else remaining.toInt()
|
||||
bytesRead = inputStream.read(buffer, 0, toRead)
|
||||
if (bytesRead == -1) break
|
||||
actualLength += bytesRead
|
||||
}
|
||||
}
|
||||
} catch (_: IOException) {
|
||||
// Stream reading failed
|
||||
}
|
||||
|
||||
if (declaredLength <= 0L || actualLength >= declaredLength) {
|
||||
Log.i("SiteChecker", "Response for $site: $responseCode, Declared: $declaredLength, Actual: $actualLength")
|
||||
responseCount++
|
||||
} else {
|
||||
Log.w("SiteChecker", "Block detected for $site, Declared: $declaredLength, Actual: $actualLength")
|
||||
}
|
||||
|
||||
} catch (e: Exception) {
|
||||
Log.e("SiteChecker", "Error accessing $site: ${e.message}")
|
||||
} finally {
|
||||
connection?.disconnect()
|
||||
}
|
||||
}
|
||||
|
||||
responseCount
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
package io.github.romanvht.byedpi.utility
|
||||
|
||||
import android.Manifest
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Build
|
||||
import android.os.Environment
|
||||
import android.provider.Settings
|
||||
import android.util.Log
|
||||
import androidx.core.app.ActivityCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.net.toUri
|
||||
import androidx.fragment.app.Fragment
|
||||
|
||||
object StorageUtils {
|
||||
private const val TAG = "StorageUtils"
|
||||
private const val STORAGE_PERMISSION_REQUEST = 1001
|
||||
|
||||
fun hasStoragePermission(context: Context): Boolean {
|
||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
Environment.isExternalStorageManager()
|
||||
} else {
|
||||
val readPermission = ContextCompat.checkSelfPermission(
|
||||
context,
|
||||
Manifest.permission.READ_EXTERNAL_STORAGE
|
||||
) == PackageManager.PERMISSION_GRANTED
|
||||
|
||||
val writePermission = ContextCompat.checkSelfPermission(
|
||||
context,
|
||||
Manifest.permission.WRITE_EXTERNAL_STORAGE
|
||||
) == PackageManager.PERMISSION_GRANTED
|
||||
|
||||
readPermission && writePermission
|
||||
}
|
||||
}
|
||||
|
||||
fun requestStoragePermission(fragment: Fragment) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
try {
|
||||
val intent = Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION).apply {
|
||||
data = "package:${fragment.requireContext().packageName}".toUri()
|
||||
}
|
||||
fragment.startActivity(intent)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Failed to request storage permission", e)
|
||||
try {
|
||||
val intent = Intent(Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION)
|
||||
fragment.startActivity(intent)
|
||||
} catch (e2: Exception) {
|
||||
Log.e(TAG, "Failed to open storage settings", e2)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Log.d(TAG, "Requesting storage permission for Android 10 and below")
|
||||
ActivityCompat.requestPermissions(
|
||||
fragment.requireActivity(),
|
||||
arrayOf(
|
||||
Manifest.permission.READ_EXTERNAL_STORAGE,
|
||||
Manifest.permission.WRITE_EXTERNAL_STORAGE
|
||||
),
|
||||
STORAGE_PERMISSION_REQUEST
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package io.github.dovecoteescapee.byedpi.utility
|
||||
package io.github.romanvht.byedpi.utility
|
||||
|
||||
import android.net.InetAddresses
|
||||
import android.os.Build
|
||||
@@ -16,6 +16,7 @@
|
||||
APP_OPTIM := release
|
||||
APP_PLATFORM := android-21
|
||||
APP_ABI := armeabi-v7a arm64-v8a x86 x86_64
|
||||
APP_CFLAGS := -O3 -DPKGNAME=io/github/dovecoteescapee/byedpi/core
|
||||
APP_CFLAGS := -O3 -DPKGNAME=io/github/romanvht/byedpi/core
|
||||
APP_CPPFLAGS := -O3 -std=c++11
|
||||
NDK_TOOLCHAIN_VERSION := clang
|
||||
APP_LDFLAGS := -Wl,-z,max-page-size=16384
|
||||
|
||||
Submodule app/src/main/jni/hev-socks5-tunnel updated: 677bb4530c...4d6c334dbf
@@ -0,0 +1,10 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24"
|
||||
android:tint="?attr/colorControlNormal">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M16,1H4C2.9,1 2,1.9 2,3v14h2V3h12V1zm3,4H8C6.9,5 6,5.9 6,7v14c0,1.1 0.9,2 2,2h11c1.1,0 2,-0.9 2,-2V7c0,-1.1 -0.9,-2 -2,-2zm0,16H8V7h11v14z"/>
|
||||
</vector>
|
||||
@@ -2,8 +2,9 @@
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
android:viewportHeight="24"
|
||||
android:tint="?attr/colorControlNormal">
|
||||
<path
|
||||
android:fillColor="?attr/colorControlNormal"
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M19.14,12.94c0.04,-0.3 0.06,-0.61 0.06,-0.94c0,-0.32 -0.02,-0.64 -0.07,-0.94l2.03,-1.58c0.18,-0.14 0.23,-0.41 0.12,-0.61l-1.92,-3.32c-0.12,-0.22 -0.37,-0.29 -0.59,-0.22l-2.39,0.96c-0.5,-0.38 -1.03,-0.7 -1.62,-0.94L14.4,2.81c-0.04,-0.24 -0.24,-0.41 -0.48,-0.41h-3.84c-0.24,0 -0.43,0.17 -0.47,0.41L9.25,5.35C8.66,5.59 8.12,5.92 7.63,6.29L5.24,5.33c-0.22,-0.08 -0.47,0 -0.59,0.22L2.74,8.87C2.62,9.08 2.66,9.34 2.86,9.48l2.03,1.58C4.84,11.36 4.8,11.69 4.8,12s0.02,0.64 0.07,0.94l-2.03,1.58c-0.18,0.14 -0.23,0.41 -0.12,0.61l1.92,3.32c0.12,0.22 0.37,0.29 0.59,0.22l2.39,-0.96c0.5,0.38 1.03,0.7 1.62,0.94l0.36,2.54c0.05,0.24 0.24,0.41 0.48,0.41h3.84c0.24,0 0.44,-0.17 0.47,-0.41l0.36,-2.54c0.59,-0.24 1.13,-0.56 1.62,-0.94l2.39,0.96c0.22,0.08 0.47,0 0.59,-0.22l1.92,-3.32c0.12,-0.22 0.07,-0.47 -0.12,-0.61L19.14,12.94zM12,15.6c-1.98,0 -3.6,-1.62 -3.6,-3.6s1.62,-3.6 3.6,-3.6s3.6,1.62 3.6,3.6S13.98,15.6 12,15.6z"/>
|
||||
</vector>
|
||||
</vector>
|
||||
@@ -0,0 +1,7 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp" android:height="24dp"
|
||||
android:viewportWidth="24" android:viewportHeight="24"
|
||||
android:tint="?attr/colorControlNormal">
|
||||
<path android:fillColor="@android:color/white"
|
||||
android:pathData="M4,8h4L8,4L4,4v4zM10,20h4v-4h-4v4zM4,20h4v-4L4,16v4zM4,14h4v-4L4,10v4zM10,14h4v-4h-4v4zM16,4v4h4L20,4h-4zM10,8h4L14,4h-4v4zM16,14h4v-4h-4v4zM16,20h4v-4h-4v4z"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="?attr/colorControlNormal"
|
||||
android:pathData="M7,10l5,5 5,-5z"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,7 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp" android:height="24dp"
|
||||
android:viewportWidth="24" android:viewportHeight="24"
|
||||
android:tint="?attr/colorControlNormal">
|
||||
<path android:fillColor="@android:color/white"
|
||||
android:pathData="M12,6v3l4,-4 -4,-4v3c-4.42,0 -8,3.58 -8,8 0,1.57 0.46,3.03 1.24,4.26L6.7,14.8c-0.45,-0.83 -0.7,-1.79 -0.7,-2.8 0,-3.31 2.69,-6 6,-6zM18.76,7.74L17.3,9.2c0.44,0.84 0.7,1.79 0.7,2.8 0,3.31 -2.69,6 -6,6v-3l-4,4 4,4v-3c4.42,0 8,-3.58 8,-8 0,-1.57 -0.46,-3.03 -1.24,-4.26z"/>
|
||||
</vector>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user