38 Commits

Author SHA1 Message Date
CherretGit
c5b60b1ee9 use card onClick in settings items 2025-10-18 19:41:51 +07:00
CherretGit
2f349e5108 bump app version 2025-10-18 19:29:27 +07:00
CherretGit
e8a8fa69db fix JNI seg fault 2025-10-18 19:21:51 +07:00
CherretGit
c094a072b8 fix app list mode 2025-10-18 18:46:40 +07:00
white
786321d852 edit strings.xml 2025-10-18 12:57:21 +03:00
white
7a17c93622 change selection screen title font size 2025-10-18 12:55:56 +03:00
white
cf38ef7ac2 some strategy selection interface changes 2025-10-18 12:20:30 +03:00
CherretGit
94979d2b2e fix testDomain 2025-10-18 12:42:06 +07:00
CherretGit
bd1bdf8298 Merge remote-tracking branch 'origin/main' 2025-10-18 01:56:19 +07:00
CherretGit
adc98db3f1 add info in strategy selection 2025-10-18 01:56:11 +07:00
CherretGit
9bbc3b771f filter list/ipset/strategy files to only .txt 2025-10-17 15:37:59 +07:00
egor-white
51356e63eb Update update.json 2025-10-16 16:43:08 +03:00
egor-white
f3f2cf999c Update changelog.md 2025-10-16 16:42:32 +03:00
white
69f395275e change build.gradle version 2025-10-16 16:18:03 +03:00
CherretGit
39dc2baad3 Merge remote-tracking branch 'origin/main' 2025-10-16 19:46:05 +07:00
CherretGit
ad5c556241 fix ipsets loading 2025-10-16 19:45:57 +07:00
white
48e7497e79 add no hosts dialog, optimize imports 2025-10-16 15:45:19 +03:00
white
2954cef0a6 small fixes, added vpn permission request before selection 2025-10-14 20:31:05 +03:00
CherretGit
c49cce1de4 fix JNI 2025-10-14 23:05:49 +07:00
white
4786ef8faf optimise sum imports, fix two active strategies 2025-10-12 19:48:44 +03:00
white
0b69d2bf4d add automatic strategy selection, remove phantom switch in settings, add ipset repository url setting, made some refactoring, disable analytics and autoupdate in debug releases by default, etc 2025-10-12 19:01:22 +03:00
CherretGit
9208885de4 Merge remote-tracking branch 'origin/main' 2025-10-10 16:51:45 +07:00
CherretGit
44fbb5fa98 useModule to view model 2025-10-10 16:51:37 +07:00
CherretGit
7c2fba684d Update workflow.yml 2025-10-09 23:19:20 +07:00
CherretGit
3ac0ca73e1 Update workflow.yml 2025-10-09 23:07:12 +07:00
CherretGit
abb3a8e4e4 Update workflow.yml 2025-10-09 22:57:43 +07:00
CherretGit
73cbd89390 Update workflow.yml 2025-10-09 22:49:55 +07:00
CherretGit
8cfe6f4389 Update workflow.yml 2025-10-09 22:39:42 +07:00
CherretGit
9a89fcccae Update workflow.yml 2025-10-09 22:38:50 +07:00
CherretGit
cd44a09cfc git submodule fix 2025-10-09 01:05:33 +07:00
CherretGit
14d6ea6caa rewrite JNI to Rust 2025-10-09 00:31:05 +07:00
white
6caff9b611 change repo source to ipsets, add ipset parsing to byedpi 2025-09-30 19:24:15 +03:00
white
5eba2461dd add ipsets screen 2025-09-30 19:00:55 +03:00
CherretGit
5a6bf9780b ipset repo and autostart 2025-09-30 20:35:46 +07:00
CherretGit
ce6e1f07b8 Merge remote-tracking branch 'origin/main' 2025-09-30 18:40:25 +07:00
CherretGit
1e985807d9 micro bug fix 2025-09-30 18:40:12 +07:00
CherretGit
80d885167e Update update.json 2025-08-31 22:38:42 +07:00
CherretGit
01f4333fee Update changelog.md 2025-08-31 22:38:21 +07:00
43 changed files with 1794 additions and 948 deletions

View File

@@ -37,6 +37,19 @@ jobs:
- name: Set up Android SDK - name: Set up Android SDK
uses: android-actions/setup-android@v2 uses: android-actions/setup-android@v2
- name: Setup NDK
run: sdkmanager "ndk;27.0.12077973" "cmake;3.22.1"
- name: Install toolchain
run: |
rustup default stable
rustup update stable
cargo install cargo-ndk
rustup target add armv7-linux-androideabi
rustup target install aarch64-linux-android
rustup target add i686-linux-android
rustup target add x86_64-linux-android
- name: Build APK - name: Build APK
run: ./gradlew assembleRelease run: ./gradlew assembleRelease

4
.gitmodules vendored
View File

@@ -1,5 +1,5 @@
[submodule "app/src/main/cpp/byedpi"] [submodule "rust/byedpi"]
path = app/src/main/cpp/byedpi path = rust/byedpi
url = https://github.com/hufrea/byedpi url = https://github.com/hufrea/byedpi
[submodule "app/src/main/jni/hev-socks5-tunnel"] [submodule "app/src/main/jni/hev-socks5-tunnel"]
path = app/src/main/jni/hev-socks5-tunnel path = app/src/main/jni/hev-socks5-tunnel

View File

@@ -4,10 +4,10 @@
<selectionStates> <selectionStates>
<SelectionState runConfigName="app"> <SelectionState runConfigName="app">
<option name="selectionMode" value="DROPDOWN" /> <option name="selectionMode" value="DROPDOWN" />
<DropdownSelection timestamp="2025-07-09T09:45:05.074315845Z"> <DropdownSelection timestamp="2025-10-03T07:21:08.712998131Z">
<Target type="DEFAULT_BOOT"> <Target type="DEFAULT_BOOT">
<handle> <handle>
<DeviceId pluginId="LocalEmulator" identifier="path=/home/white/.android/avd/Pixel_8.avd" /> <DeviceId pluginId="LocalEmulator" identifier="path=/home/dimap/.android/avd/Medium_Phone.avd" />
</handle> </handle>
</Target> </Target>
</DropdownSelection> </DropdownSelection>

13
.idea/deviceManager.xml generated Normal file
View File

@@ -0,0 +1,13 @@
<?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>

2
.idea/vcs.xml generated
View File

@@ -2,11 +2,11 @@
<project version="4"> <project version="4">
<component name="VcsDirectoryMappings"> <component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" /> <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" 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/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/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/lwip" vcs="Git" />
<mapping directory="$PROJECT_DIR$/app/src/main/jni/hev-socks5-tunnel/third-part/yaml" vcs="Git" /> <mapping directory="$PROJECT_DIR$/app/src/main/jni/hev-socks5-tunnel/third-part/yaml" vcs="Git" />
<mapping directory="$PROJECT_DIR$/rust/byedpi" vcs="Git" />
</component> </component>
</project> </project>

View File

@@ -5,6 +5,7 @@ plugins {
id("com.google.gms.google-services") id("com.google.gms.google-services")
id("com.google.firebase.crashlytics") id("com.google.firebase.crashlytics")
kotlin("plugin.serialization") version "2.1.20" kotlin("plugin.serialization") version "2.1.20"
id("org.mozilla.rust-android-gradle.rust-android") version "0.9.6"
} }
android { android {
@@ -15,19 +16,10 @@ android {
applicationId = "com.cherret.zaprett" applicationId = "com.cherret.zaprett"
minSdk = 29 minSdk = 29
targetSdk = 35 targetSdk = 35
versionCode = 21 versionCode = 23
versionName = "2.9" versionName = "2.11"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
ndk {
abiFilters += listOf("armeabi-v7a", "arm64-v8a", "x86", "x86_64")
}
}
externalNativeBuild {
cmake {
path = file("src/main/cpp/CMakeLists.txt")
version = "3.22.1"
}
} }
buildTypes { buildTypes {
release { release {
@@ -37,6 +29,12 @@ android {
getDefaultProguardFile("proguard-android-optimize.txt"), getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro" "proguard-rules.pro"
) )
buildConfigField("boolean", "send_firebase_analytics", "true")
buildConfigField("boolean", "auto_update", "true")
}
debug {
buildConfigField("boolean", "send_firebase_analytics", "false")
buildConfigField("boolean", "auto_update", "false")
} }
} }
compileOptions { compileOptions {
@@ -75,6 +73,27 @@ tasks.preBuild {
dependsOn("runNdkBuild") dependsOn("runNdkBuild")
} }
cargo {
module = "../rust"
libname = "byedpi"
targets = listOf("arm", "arm64", "x86", "x86_64")
profile = "release"
}
tasks.preBuild {
dependsOn("cargoBuild")
}
tasks.register<Exec>("cargoClean") {
workingDir = file("../rust")
commandLine("cargo", "clean")
group = "build"
}
tasks.named("clean") {
dependsOn("cargoClean")
}
dependencies { dependencies {
implementation(libs.compose.material3) implementation(libs.compose.material3)
implementation(libs.compose.material3.window.size) implementation(libs.compose.material3.window.size)

View File

@@ -1,83 +0,0 @@
#define VERSION "17.1"
union sockaddr_u;
int get_default_ttl(void);
int get_addr(const char *str, union sockaddr_u *addr);
void *add(void **root, int *n, size_t ss);
void clear_params(void);
char *ftob(const char *str, ssize_t *sl);
char *data_from_str(const char *str, ssize_t *size);
size_t parse_cform(char *buffer, size_t blen, const char *str, size_t slen);
struct mphdr *parse_hosts(char *buffer, size_t size);
struct mphdr *parse_ipset(char *buffer, size_t size);
int parse_offset(struct part *part, const char *str);
static const char help_text[] = {
" -i, --ip, <ip> Listening IP, default 0.0.0.0\n"
" -p, --port <num> Listening port, default 1080\n"
#ifdef DAEMON
" -D, --daemon Daemonize\n"
" -w, --pidfile <filename> Write PID to file\n"
#endif
#ifdef __linux__
" -E, --transparent Transparent proxy mode\n"
#endif
" -c, --max-conn <count> Connection count limit, default 512\n"
" -N, --no-domain Deny domain resolving\n"
" -U, --no-udp Deny UDP association\n"
" -I --conn-ip <ip> Connection binded IP, default ::\n"
" -b, --buf-size <size> Buffer size, default 16384\n"
" -x, --debug <level> Print logs, 0, 1 or 2\n"
" -g, --def-ttl <num> TTL for all outgoing connections\n"
// desync options
#ifdef TCP_FASTOPEN_CONNECT
" -F, --tfo Enable TCP Fast Open\n"
#endif
" -A, --auto <t,r,s,n> Try desync params after this option\n"
" Detect: torst,redirect,ssl_err,none\n"
" -L, --auto-mode <0-3> Mode: 1 - post_resp, 2 - sort, 3 - 1+2\n"
" -u, --cache-ttl <sec> Lifetime of cached desync params for IP\n"
" -y, --cache-dump <file|-> Dump cache to file or stdout\n"
#ifdef TIMEOUT_SUPPORT
" -T, --timeout <sec> Timeout waiting for response, after which trigger auto\n"
#endif
" -K, --proto <t,h,u,i> Protocol whitelist: tls,http,udp,ipv4\n"
" -H, --hosts <file|:str> Hosts whitelist, filename or :string\n"
" -j, --ipset <file|:str> IP whitelist\n"
" -V, --pf <port[-portr]> Ports range whitelist\n"
" -R, --round <num[-numr]> Number of request to which desync will be applied\n"
" -s, --split <pos_t> Position format: offset[:repeats:skip][+flag1[flag2]]\n"
" Flags: +s - SNI offset, +h - HTTP host offset, +n - null\n"
" Additional flags: +e - end, +m - middle\n"
" -d, --disorder <pos_t> Split and send reverse order\n"
" -o, --oob <pos_t> Split and send as OOB data\n"
" -q, --disoob <pos_t> Split and send reverse order as OOB data\n"
#ifdef FAKE_SUPPORT
" -f, --fake <pos_t> Split and send fake packet\n"
#ifdef __linux__
" -S, --md5sig Add MD5 Signature option for fake packets\n"
#endif
" -n, --fake-sni <str> Change SNI in fake\n"
" Replaced: ? - rand let, # - rand num, * - rand let/num\n"
#endif
" -t, --ttl <num> TTL of fake packets, default 8\n"
" -O, --fake-offset <pos_t> Fake data start offset\n"
" -l, --fake-data <f|:str> Set custom fake packet\n"
" -Q, --fake-tls-mod <r,o> Modify fake TLS CH: rand,orig\n"
" -e, --oob-data <char> Set custom OOB data\n"
" -M, --mod-http <h,d,r> Modify HTTP: hcsmix,dcsmix,rmspace\n"
" -r, --tlsrec <pos_t> Make TLS record at position\n"
" -a, --udp-fake <count> UDP fakes count, default 0\n"
#ifdef __linux__
" -Y, --drop-sack Drop packets with SACK extension\n"
#endif
};

View File

@@ -1,100 +0,0 @@
#include <string.h>
#include <jni.h>
#include <android/log.h>
#include <malloc.h>
#include "byedpi/error.h"
#include "byedpi/proxy.h"
#include "utils.h"
static int g_proxy_fd = -1;
JNIEXPORT jint JNI_OnLoad(
__attribute__((unused)) JavaVM *vm,
__attribute__((unused)) void *reserved) {
default_params = params;
return JNI_VERSION_1_6;
}
JNIEXPORT jint JNICALL
Java_com_cherret_zaprett_byedpi_NativeBridge_jniCreateSocket(
JNIEnv *env,
__attribute__((unused)) jobject thiz,
jobjectArray args) {
if (g_proxy_fd != -1) {
LOG(LOG_S, "proxy already running, fd: %d", g_proxy_fd);
return -1;
}
int argc = (*env)->GetArrayLength(env, args);
char *argv[argc];
for (int i = 0; i < argc; i++) {
jstring arg = (jstring) (*env)->GetObjectArrayElement(env, args, i);
const char *arg_str = (*env)->GetStringUTFChars(env, arg, 0);
argv[i] = strdup(arg_str);
(*env)->ReleaseStringUTFChars(env, arg, arg_str);
}
int res = parse_args(argc, argv);
if (res < 0) {
uniperror("parse_args");
return -1;
}
int fd = listen_socket((union sockaddr_u *)&params.laddr);
for (int i = 0; i < argc; i++) {
free(argv[i]);
}
if (fd < 0) {
uniperror("listen_socket");
return -1;
}
g_proxy_fd = fd;
LOG(LOG_S, "listen_socket, fd: %d", fd);
return fd;
}
JNIEXPORT jint JNICALL
Java_com_cherret_zaprett_byedpi_NativeBridge_jniStartProxy(
__attribute__((unused)) JNIEnv *env,
__attribute__((unused)) jobject thiz) {
LOG(LOG_S, "start_proxy, fd: %d", g_proxy_fd);
if (start_event_loop(g_proxy_fd) < 0) {
uniperror("event_loop");
return get_e();
}
return 0;
}
JNIEXPORT jint JNICALL
Java_com_cherret_zaprett_byedpi_NativeBridge_jniStopProxy(
__attribute__((unused)) JNIEnv *env,
__attribute__((unused)) jobject thiz) {
LOG(LOG_S, "stop_proxy, fd: %d", g_proxy_fd);
if (g_proxy_fd < 0) {
LOG(LOG_S, "proxy is not running, fd: %d", g_proxy_fd);
return 0;
}
reset_params();
int res = shutdown(g_proxy_fd, SHUT_RDWR);
g_proxy_fd = -1;
if (res < 0) {
uniperror("shutdown");
return get_e();
}
return 0;
}

View File

@@ -1,594 +0,0 @@
#include <getopt.h>
#include <stdlib.h>
#include <string.h>
#include "byedpi/params.h"
#include "error.h"
#include "main.h"
#include "packets.h"
#include "utils.h"
struct params default_params;
extern const struct option options[46];
void reset_params(void) {
clear_params();
params = default_params;
}
static struct desync_params *add_group(struct desync_params *prev)
{
struct desync_params *dp = calloc(1, sizeof(*prev));
if (!dp) {
return 0;
}
if (prev) {
dp->prev = prev;
prev->next = dp;
}
params.dp_n++;
return dp;
}
int parse_args(int argc, char **argv)
{
int optc = sizeof(options)/sizeof(*options);
for (int i = 0, e = optc; i < e; i++)
optc += options[i].has_arg;
char opt[optc + 1];
opt[optc] = 0;
for (int i = 0, o = 0; o < optc; i++, o++) {
opt[o] = options[i].val;
for (int c = options[i].has_arg; c; c--) {
o++;
opt[o] = ':';
}
}
//
params.laddr.in.sin_port = htons(1080);
if (!ipv6_support()) {
params.baddr.sa.sa_family = AF_INET;
}
const char *pid_file = 0;
bool daemonize = 0;
int rez;
int invalid = 0;
long val = 0;
char *end = 0;
bool all_limited = 1;
int curr_optind = 1;
struct desync_params *dp = add_group(0);
if (!dp) {
reset_params();
return -1;
}
params.dp = dp;
while (!invalid && (rez = getopt_long(
argc, argv, opt, options, 0)) != -1) {
switch (rez) {
case 'N':
params.resolve = 0;
break;
case 'X':
params.ipv6 = 0;
break;
case 'U':
params.udp = 0;
break;
case 'G':
params.http_connect = 1;
break;
#ifdef __linux__
case 'E':
params.transparent = 1;
break;
#endif
#ifdef DAEMON
case 'D':
daemonize = 1;
break;
case 'w':
pid_file = optarg;
break;
#endif
case 'h':
printf("%s", help_text);
reset_params();
return 0;
case 'v':
printf("%s\n", VERSION);
reset_params();
return 0;
case 'i':
if (get_addr(optarg, &params.laddr) < 0)
invalid = 1;
break;
case 'p':
val = strtol(optarg, &end, 0);
if (val <= 0 || val > 0xffff || *end)
invalid = 1;
else
params.laddr.in.sin_port = htons(val);
break;
case 'I':
if (get_addr(optarg, &params.baddr) < 0)
invalid = 1;
break;
case 'b':
val = strtol(optarg, &end, 0);
if (val <= 0 || val > INT_MAX/4 || *end)
invalid = 1;
else
params.bfsize = val;
break;
case 'c':
val = strtol(optarg, &end, 0);
if (val <= 0 || val >= (0xffff/2) || *end)
invalid = 1;
else
params.max_open = val;
break;
case 'x': //
params.debug = strtol(optarg, 0, 0);
if (params.debug < 0)
invalid = 1;
break;
case 'y': //
params.cache_file = optarg;
break;
// desync options
case 'F':
params.tfo = 1;
break;
case 'L':
end = optarg;
while (end && !invalid) {
switch (*end) {
case '0':
break;
case '1':
case 'p':
params.auto_level |= AUTO_POST;
break;
case '2':
case 's':
params.auto_level |= AUTO_SORT;
break;
case 'r':
params.auto_level = 0;
break;
case '3':
params.auto_level |= (AUTO_POST | AUTO_SORT);
break;
default:
invalid = 1;
continue;
}
end = strchr(end, ',');
if (end) end++;
}
break;
case 'A':
if (optind < curr_optind) {
optind = curr_optind;
continue;
}
if (!(dp->hosts || dp->proto || dp->pf[0] || dp->detect || dp->ipset)) {
all_limited = 0;
}
dp = add_group(dp);
if (!dp) {
reset_params();
return -1;
}
end = optarg;
while (end && !invalid) {
switch (*end) {
case 't':
dp->detect |= DETECT_TORST;
break;
case 'r':
dp->detect |= DETECT_HTTP_LOCAT;
break;
case 'a':
case 's':
dp->detect |= DETECT_TLS_ERR;
break;
case 'n':
break;
default:
invalid = 1;
continue;
}
end = strchr(end, ',');
if (end) end++;
}
if (dp->detect) {
params.auto_level |= AUTO_RECONN;
}
dp->_optind = optind;
break;
case 'B':
if (optind < curr_optind) {
continue;
}
if (*optarg == 'i') {
dp->pf[0] = htons(1);
continue;
}
val = strtol(optarg, &end, 0);
struct desync_params *itdp = params.dp;
while (itdp && itdp->id != val - 1) {
itdp = itdp->next;
}
if (!itdp)
invalid = 1;
else {
curr_optind = optind;
optind = itdp->_optind;
}
break;
case 'u':
val = strtol(optarg, &end, 0);
if (val <= 0 || *end)
invalid = 1;
else
params.cache_ttl = val;
break;
case 'T':;
#ifdef __linux__
float f = strtof(optarg, &end);
val = (long)(f * 1000);
#else
val = strtol(optarg, &end, 0);
#endif
if (val <= 0 || (unsigned long)val > UINT_MAX || *end)
invalid = 1;
else
params.timeout = val;
break;
case 'K':
end = optarg;
while (end && !invalid) {
switch (*end) {
case 't':
dp->proto |= IS_TCP | IS_HTTPS;
break;
case 'h':
dp->proto |= IS_TCP | IS_HTTP;
break;
case 'u':
dp->proto |= IS_UDP;
break;
case 'i':
dp->proto |= IS_IPV4;
break;
default:
invalid = 1;
continue;
}
end = strchr(end, ',');
if (end) end++;
}
break;
case 'H':;
if (dp->file_ptr) {
continue;
}
dp->file_ptr = ftob(optarg, &dp->file_size);
if (!dp->file_ptr) {
uniperror("read/parse");
invalid = 1;
continue;
}
dp->hosts = parse_hosts(dp->file_ptr, dp->file_size);
if (!dp->hosts) {
uniperror("parse_hosts");
reset_params();
return -1;
}
break;
case 'j':;
if (dp->ipset) {
continue;
}
ssize_t size;
char *data = ftob(optarg, &size);
if (!data) {
uniperror("read/parse");
invalid = 1;
continue;
}
dp->ipset = parse_ipset(data, size);
if (!dp->ipset) {
uniperror("parse_ipset");
invalid = 1;
}
free(data);
break;
case 's':
case 'd':
case 'o':
case 'q':
case 'f':
;
struct part *part = add((void *)&dp->parts,
&dp->parts_n, sizeof(struct part));
if (!part) {
reset_params();
return -1;
}
if (parse_offset(part, optarg)) {
invalid = 1;
break;
}
switch (rez) {
case 's': part->m = DESYNC_SPLIT;
break;
case 'd': part->m = DESYNC_DISORDER;
break;
case 'o': part->m = DESYNC_OOB;
break;
case 'q': part->m = DESYNC_DISOOB;
break;
case 'f': part->m = DESYNC_FAKE;
}
break;
case 't':
val = strtol(optarg, &end, 0);
if (val <= 0 || val > 255 || *end)
invalid = 1;
else
dp->ttl = val;
break;
case 'S':
dp->md5sig = 1;
break;
case 'O':
if (parse_offset(&dp->fake_offset, optarg)) {
invalid = 1;
break;
} else dp->fake_offset.m = 1;
break;
case 'Q':
end = optarg;
while (end && !invalid) {
switch (*end) {
case 'r':
dp->fake_mod |= FM_RAND;
break;
case 'o':
dp->fake_mod |= FM_ORIG;
break;
default:
invalid = 1;
continue;
}
end = strchr(end, ',');
if (end) end++;
}
break;
case 'n':;
const char **p = add((void *)&dp->fake_sni_list,
&dp->fake_sni_count, sizeof(optarg));
if (!p) {
invalid = 1;
continue;
}
*p = optarg;
break;
case 'l':
if (dp->fake_data.data) {
continue;
}
dp->fake_data.data = ftob(optarg, &dp->fake_data.size);
if (!dp->fake_data.data) {
uniperror("read/parse");
invalid = 1;
}
break;
case 'e':
val = parse_cform(dp->oob_char, 1, optarg, strlen(optarg));
if (val != 1) {
invalid = 1;
}
else dp->oob_char[1] = 1;
break;
case 'M':
end = optarg;
while (end && !invalid) {
switch (*end) {
case 'r':
dp->mod_http |= MH_SPACE;
break;
case 'h':
dp->mod_http |= MH_HMIX;
break;
case 'd':
dp->mod_http |= MH_DMIX;
break;
default:
invalid = 1;
continue;
}
end = strchr(end, ',');
if (end) end++;
}
break;
case 'r':
part = add((void *)&dp->tlsrec,
&dp->tlsrec_n, sizeof(struct part));
if (!part) {
reset_params();
return -1;
}
if (parse_offset(part, optarg)
|| part->pos > 0xffff) {
invalid = 1;
break;
}
break;
case 'a':
val = strtol(optarg, &end, 0);
if (val < 0 || val > INT_MAX || *end)
invalid = 1;
else
dp->udp_fake_count = val;
break;
case 'V':
val = strtol(optarg, &end, 0);
if (val <= 0 || val > USHRT_MAX)
invalid = 1;
else {
dp->pf[0] = htons(val);
if (*end == '-') {
val = strtol(end + 1, &end, 0);
if (val <= 0 || val > USHRT_MAX)
invalid = 1;
}
if (*end)
invalid = 1;
else
dp->pf[1] = htons(val);
}
break;
case 'R':
val = strtol(optarg, &end, 0);
if (val <= 0 || val > INT_MAX)
invalid = 1;
else {
dp->rounds[0] = val;
if (*end == '-') {
val = strtol(end + 1, &end, 0);
if (val <= 0 || val > INT_MAX)
invalid = 1;
}
if (*end)
invalid = 1;
else
dp->rounds[1] = val;
}
break;
case 'g':
val = strtol(optarg, &end, 0);
if (val <= 0 || val > 255 || *end)
invalid = 1;
else {
params.def_ttl = val;
params.custom_ttl = 1;
}
break;
case 'Y':
dp->drop_sack = 1;
break;
case 'Z':
params.wait_send = 1;
break;
case 'W':
params.await_int = atoi(optarg);
break;
case 'C':
if (get_addr(optarg, &dp->custom_dst_addr) < 0)
invalid = 1;
else
dp->custom_dst = 1;
break;
#ifdef __linux__
case 'P':
params.protect_path = optarg;
break;
#endif
case 0:
break;
case '?':
reset_params();
return -1;
default:
printf("?: %c\n", rez);
reset_params();
return -1;
}
}
if (invalid) {
fprintf(stderr, "invalid value: -%c %s\n", rez, optarg);
reset_params();
return -1;
}
if (all_limited) {
dp = add_group(dp);
if (!dp) {
reset_params();
return -1;
}
}
if ((size_t )params.dp_n > sizeof(dp->bit) * 8) {
LOG(LOG_E, "too many groups!\n");
}
if (params.baddr.sa.sa_family != AF_INET6) {
params.ipv6 = 0;
}
if (!params.def_ttl) {
if ((params.def_ttl = get_default_ttl()) < 1) {
reset_params();
return -1;
}
}
params.mempool = mem_pool(MF_EXTRA, CMP_BYTES);
if (!params.mempool) {
uniperror("mem_pool");
reset_params();
return -1;
}
srand((unsigned int)time(0));
return 0;
}

View File

@@ -1,5 +0,0 @@
extern struct params default_params;
void reset_params(void);
int parse_args(int argc, char **argv);
bool ipv6_support(void);

View File

@@ -22,6 +22,7 @@ import androidx.compose.material.icons.filled.Home
import androidx.compose.material.icons.filled.Lan import androidx.compose.material.icons.filled.Lan
import androidx.compose.material.icons.filled.MultipleStop import androidx.compose.material.icons.filled.MultipleStop
import androidx.compose.material.icons.filled.Settings import androidx.compose.material.icons.filled.Settings
import androidx.compose.material.icons.filled.SettingsInputComposite
import androidx.compose.material3.AlertDialog import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.NavigationBar import androidx.compose.material3.NavigationBar
@@ -49,12 +50,15 @@ import androidx.navigation.navArgument
import com.cherret.zaprett.ui.screen.DebugScreen import com.cherret.zaprett.ui.screen.DebugScreen
import com.cherret.zaprett.ui.screen.HomeScreen import com.cherret.zaprett.ui.screen.HomeScreen
import com.cherret.zaprett.ui.screen.HostsScreen import com.cherret.zaprett.ui.screen.HostsScreen
import com.cherret.zaprett.ui.screen.IpsetsScreen
import com.cherret.zaprett.ui.screen.RepoScreen import com.cherret.zaprett.ui.screen.RepoScreen
import com.cherret.zaprett.ui.screen.SettingsScreen import com.cherret.zaprett.ui.screen.SettingsScreen
import com.cherret.zaprett.ui.screen.StrategyScreen import com.cherret.zaprett.ui.screen.StrategyScreen
import com.cherret.zaprett.ui.screen.StrategySelectionScreen
import com.cherret.zaprett.ui.theme.ZaprettTheme import com.cherret.zaprett.ui.theme.ZaprettTheme
import com.cherret.zaprett.ui.viewmodel.HomeViewModel import com.cherret.zaprett.ui.viewmodel.HomeViewModel
import com.cherret.zaprett.ui.viewmodel.HostRepoViewModel import com.cherret.zaprett.ui.viewmodel.HostRepoViewModel
import com.cherret.zaprett.ui.viewmodel.IpsetRepoViewModel
import com.cherret.zaprett.ui.viewmodel.StrategyRepoViewModel import com.cherret.zaprett.ui.viewmodel.StrategyRepoViewModel
import com.cherret.zaprett.utils.checkModuleInstallation import com.cherret.zaprett.utils.checkModuleInstallation
import com.google.firebase.Firebase import com.google.firebase.Firebase
@@ -65,10 +69,11 @@ sealed class Screen(val route: String, @StringRes val nameResId: Int, val icon:
object home : Screen("home", R.string.title_home, Icons.Default.Home) object home : Screen("home", R.string.title_home, Icons.Default.Home)
object hosts : Screen("hosts", R.string.title_hosts, Icons.Default.Lan) object hosts : Screen("hosts", R.string.title_hosts, Icons.Default.Lan)
object strategies : Screen("strategies", R.string.title_strategies, Icons.Default.MultipleStop) object strategies : Screen("strategies", R.string.title_strategies, Icons.Default.MultipleStop)
object ipsets : Screen("ipsets", R.string.title_ipset, Icons.Default.SettingsInputComposite)
object settings : Screen("settings", R.string.title_settings, Icons.Default.Settings) object settings : Screen("settings", R.string.title_settings, Icons.Default.Settings)
} }
val topLevelRoutes = listOf(Screen.home, Screen.hosts, Screen.strategies, Screen.settings) val topLevelRoutes = listOf(Screen.home, Screen.hosts, Screen.strategies, Screen.ipsets, Screen.settings)
val hideNavBar = listOf("repo?source={source}", "debugScreen") val hideNavBar = listOf("repo?source={source}", "debugScreen", "selectionScreen")
class MainActivity : ComponentActivity() { class MainActivity : ComponentActivity() {
private val viewModel: HomeViewModel by viewModels() private val viewModel: HomeViewModel by viewModels()
private lateinit var notificationPermissionLauncher: ActivityResultLauncher<String> private lateinit var notificationPermissionLauncher: ActivityResultLauncher<String>
@@ -123,7 +128,7 @@ class MainActivity : ComponentActivity() {
) )
} }
var showWelcomeDialog by remember { mutableStateOf(sharedPreferences.getBoolean("welcome_dialog", true)) } var showWelcomeDialog by remember { mutableStateOf(sharedPreferences.getBoolean("welcome_dialog", true)) }
firebaseAnalytics.setAnalyticsCollectionEnabled(sharedPreferences.getBoolean("send_firebase_analytics", true)) firebaseAnalytics.setAnalyticsCollectionEnabled(sharedPreferences.getBoolean("send_firebase_analytics", BuildConfig.send_firebase_analytics))
BottomBar() BottomBar()
if (showStoragePermissionDialog) { if (showStoragePermissionDialog) {
PermissionDialog( PermissionDialog(
@@ -215,6 +220,7 @@ class MainActivity : ComponentActivity() {
composable(Screen.home.route) { HomeScreen(viewModel = viewModel, vpnPermissionLauncher) } composable(Screen.home.route) { HomeScreen(viewModel = viewModel, vpnPermissionLauncher) }
composable(Screen.hosts.route) { HostsScreen(navController) } composable(Screen.hosts.route) { HostsScreen(navController) }
composable(Screen.strategies.route) { StrategyScreen(navController) } composable(Screen.strategies.route) { StrategyScreen(navController) }
composable(Screen.ipsets.route) { IpsetsScreen(navController) }
composable(Screen.settings.route) { SettingsScreen(navController) } composable(Screen.settings.route) { SettingsScreen(navController) }
composable(route = "repo?source={source}",arguments = listOf(navArgument("source") {})) { backStackEntry -> composable(route = "repo?source={source}",arguments = listOf(navArgument("source") {})) { backStackEntry ->
val source = backStackEntry.arguments?.getString("source") val source = backStackEntry.arguments?.getString("source")
@@ -223,6 +229,10 @@ class MainActivity : ComponentActivity() {
val viewModel: HostRepoViewModel = androidx.lifecycle.viewmodel.compose.viewModel() val viewModel: HostRepoViewModel = androidx.lifecycle.viewmodel.compose.viewModel()
RepoScreen(navController, viewModel) RepoScreen(navController, viewModel)
} }
"ipsets" -> {
val viewModel: IpsetRepoViewModel = androidx.lifecycle.viewmodel.compose.viewModel()
RepoScreen(navController, viewModel)
}
"strategies" -> { "strategies" -> {
val viewModel: StrategyRepoViewModel = androidx.lifecycle.viewmodel.compose.viewModel() val viewModel: StrategyRepoViewModel = androidx.lifecycle.viewmodel.compose.viewModel()
RepoScreen(navController, viewModel) RepoScreen(navController, viewModel)
@@ -230,6 +240,7 @@ class MainActivity : ComponentActivity() {
} }
} }
composable("debugScreen") { DebugScreen(navController) } composable("debugScreen") { DebugScreen(navController) }
composable("selectionScreen") { StrategySelectionScreen(navController, vpnPermissionLauncher) }
} }
} }
} }

View File

@@ -14,7 +14,11 @@ import androidx.core.app.NotificationCompat
import com.cherret.zaprett.MainActivity import com.cherret.zaprett.MainActivity
import com.cherret.zaprett.R import com.cherret.zaprett.R
import com.cherret.zaprett.data.ServiceStatus import com.cherret.zaprett.data.ServiceStatus
import com.cherret.zaprett.utils.disableList
import com.cherret.zaprett.utils.getActiveByeDPIStrategyContent
import com.cherret.zaprett.utils.getActiveExcludeIpsets
import com.cherret.zaprett.utils.getActiveExcludeLists import com.cherret.zaprett.utils.getActiveExcludeLists
import com.cherret.zaprett.utils.getActiveIpsets
import com.cherret.zaprett.utils.getActiveLists import com.cherret.zaprett.utils.getActiveLists
import com.cherret.zaprett.utils.getActiveStrategy import com.cherret.zaprett.utils.getActiveStrategy
import com.cherret.zaprett.utils.getAppsListMode import com.cherret.zaprett.utils.getAppsListMode
@@ -174,7 +178,7 @@ class ByeDpiVpnService : VpnService() {
try { try {
vpnInterface?.close() vpnInterface?.close()
vpnInterface = null vpnInterface = null
NativeBridge().stopProxy() NativeBridge().jniStopProxy()
TProxyService.TProxyStopService() TProxyService.TProxyStopService()
status = ServiceStatus.Disconnected status = ServiceStatus.Disconnected
} catch (e: Exception) { } catch (e: Exception) {
@@ -186,9 +190,16 @@ class ByeDpiVpnService : VpnService() {
val socksIp = sharedPreferences.getString("ip", "127.0.0.1")?: "127.0.0.1" val socksIp = sharedPreferences.getString("ip", "127.0.0.1")?: "127.0.0.1"
val socksPort = sharedPreferences.getString("port", "1080")?: "1080" val socksPort = sharedPreferences.getString("port", "1080")?: "1080"
val listSet = if (getHostListMode(sharedPreferences) == "whitelist") getActiveLists(sharedPreferences) else getActiveExcludeLists(sharedPreferences) val listSet = if (getHostListMode(sharedPreferences) == "whitelist") getActiveLists(sharedPreferences) else getActiveExcludeLists(sharedPreferences)
val ipsetSet = if (getHostListMode(sharedPreferences) == "whitelist") getActiveIpsets(sharedPreferences) else getActiveExcludeIpsets(sharedPreferences)
CoroutineScope(Dispatchers.IO).launch { CoroutineScope(Dispatchers.IO).launch {
val args = parseArgs(socksIp, socksPort, getActiveStrategy(sharedPreferences), prepareList(listSet), sharedPreferences) val args = parseArgs(
val result = NativeBridge().startProxy(args) socksIp,
socksPort,
getActiveByeDPIStrategyContent(sharedPreferences),
prepareList(listSet),
prepareIpset(ipsetSet),
sharedPreferences)
val result = NativeBridge().jniStartProxy(args)
if (result < 0) { if (result < 0) {
Log.d("proxy","Failed to start byedpi proxy") Log.d("proxy","Failed to start byedpi proxy")
} else { } else {
@@ -205,10 +216,14 @@ class ByeDpiVpnService : VpnService() {
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
hostlist.printWriter().use { out -> hostlist.printWriter().use { out ->
lists.forEach { lists.forEach {
it.bufferedReader().useLines { if (it.exists()) {
it.forEach { it.bufferedReader().useLines {
out.println(it) it.forEach {
out.println(it)
}
} }
} else {
disableList(it.name, sharedPreferences)
} }
} }
} }
@@ -218,7 +233,33 @@ class ByeDpiVpnService : VpnService() {
return "" return ""
} }
private fun parseArgs(ip: String, port: String, rawArgs: List<String>, list : String, sharedPreferences: SharedPreferences): Array<String> { private suspend fun prepareIpset(actsets: Array<String>): String {
if (actsets.isNotEmpty()) {
val lists: Array<File> = actsets.map { File(it) }.toTypedArray()
val hostlist = withContext(Dispatchers.IO) {
File.createTempFile("ipset", ".txt", cacheDir)
}.apply { deleteOnExit() }
withContext(Dispatchers.IO) {
hostlist.printWriter().use { out ->
lists.forEach {
if (it.exists()) {
it.bufferedReader().useLines {
it.forEach {
out.println(it)
}
}
} else {
disableList(it.name, sharedPreferences)
}
}
}
}
return hostlist.absolutePath
}
return ""
}
private fun parseArgs(ip: String, port: String, rawArgs: List<String>, list : String, ipset : String, sharedPreferences: SharedPreferences): Array<String> {
val regex = Regex("""--?\S+(?:=(?:[^"'\s]+|"[^"]*"|'[^']*'))?|[^\s]+""") val regex = Regex("""--?\S+(?:=(?:[^"'\s]+|"[^"]*"|'[^']*'))?|[^\s]+""")
val parsedArgs = rawArgs val parsedArgs = rawArgs
.flatMap { args -> regex.findAll(args).map { it.value } } .flatMap { args -> regex.findAll(args).map { it.value } }
@@ -227,6 +268,8 @@ class ByeDpiVpnService : VpnService() {
when { when {
arg == "\$hostlist" && list.isNotEmpty() -> listOf("-H", list) arg == "\$hostlist" && list.isNotEmpty() -> listOf("-H", list)
arg == "\$hostlist" && list.isEmpty() -> emptyList() arg == "\$hostlist" && list.isEmpty() -> emptyList()
arg == "\$ipset" && list.isNotEmpty() -> listOf("-H", list)
arg == "\$ipset" && list.isEmpty() -> emptyList()
else -> listOf(arg) else -> listOf(arg)
} }
} else { } else {
@@ -235,6 +278,13 @@ class ByeDpiVpnService : VpnService() {
} else { } else {
listOf("-An", arg).filter { it != "\$hostlist" } listOf("-An", arg).filter { it != "\$hostlist" }
} }
if (ipset.isEmpty()) {
listOf("-H", list, "-An", arg).filter { it != "\$ipset" }
}
else {
listOf("-An", arg).filter { it != "\$ipset" }
}
} }
} }
.toMutableList() .toMutableList()

View File

@@ -6,16 +6,7 @@ class NativeBridge {
System.loadLibrary("byedpi") System.loadLibrary("byedpi")
} }
} }
fun startProxy(args: Array<String>): Int {
jniCreateSocket(args)
return jniStartProxy()
}
fun stopProxy(): Int { external fun jniStartProxy(args: Array<String>): Int
return jniStopProxy() external fun jniStopProxy(): Int
}
private external fun jniCreateSocket(args: Array<String>): Int
private external fun jniStartProxy(): Int
private external fun jniStopProxy(): Int
} }

View File

@@ -1,5 +1,5 @@
package com.cherret.zaprett.data package com.cherret.zaprett.data
enum class ItemType { enum class ItemType {
byedpi, nfqws, list, list_exclude byedpi, nfqws, list, list_exclude, ipset, ipset_exclude
} }

View File

@@ -0,0 +1,8 @@
package com.cherret.zaprett.data
data class StrategyCheckResult (
val path : String,
val progress : Float,
var domains: List<String>,
val status : StrategyTestingStatus,
)

View File

@@ -0,0 +1,7 @@
package com.cherret.zaprett.data
import com.cherret.zaprett.R
enum class StrategyTestingStatus(val resId: Int) {
Waiting(R.string.strategy_status_waiting), Testing(R.string.strategy_status_testing), Completed(R.string.strategy_status_tested)
}

View File

@@ -1,32 +1,58 @@
package com.cherret.zaprett.ui.component package com.cherret.zaprett.ui.component
import android.content.Context
import android.content.SharedPreferences
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.expandVertically
import androidx.compose.animation.shrinkVertically
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Check
import androidx.compose.material.icons.filled.Delete import androidx.compose.material.icons.filled.Delete
import androidx.compose.material.icons.filled.InstallMobile import androidx.compose.material.icons.filled.InstallMobile
import androidx.compose.material.icons.filled.Update import androidx.compose.material.icons.filled.Update
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults import androidx.compose.material3.CardDefaults
import androidx.compose.material3.ElevatedCard import androidx.compose.material3.ElevatedCard
import androidx.compose.material3.FilledTonalButton import androidx.compose.material3.FilledTonalButton
import androidx.compose.material3.FilledTonalIconButton
import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.LinearProgressIndicator
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ProgressIndicatorDefaults
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Switch import androidx.compose.material3.Switch
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.cherret.zaprett.R import com.cherret.zaprett.R
import com.cherret.zaprett.data.StrategyCheckResult
import com.cherret.zaprett.data.StrategyTestingStatus
import com.cherret.zaprett.ui.viewmodel.BaseRepoViewModel import com.cherret.zaprett.ui.viewmodel.BaseRepoViewModel
import com.cherret.zaprett.utils.RepoItemInfo import com.cherret.zaprett.utils.RepoItemInfo
import com.cherret.zaprett.utils.disableStrategy
import com.cherret.zaprett.utils.enableStrategy
import com.cherret.zaprett.utils.getActiveStrategy
import kotlinx.coroutines.launch
@Composable @Composable
fun ListSwitchItem(item: String, isChecked: Boolean, onCheckedChange: (Boolean) -> Unit, onDeleteClick: () -> Unit) { fun ListSwitchItem(item: String, isChecked: Boolean, onCheckedChange: (Boolean) -> Unit, onDeleteClick: () -> Unit) {
@@ -164,3 +190,119 @@ fun RepoItem(
} }
} }
} }
@Composable
fun StrategySelectionItem(strategy : StrategyCheckResult, prefs : SharedPreferences, context : Context, snackbarHostState : SnackbarHostState) {
val scope = rememberCoroutineScope()
var expanded by remember { mutableStateOf(false) }
ElevatedCard (
elevation = CardDefaults.cardElevation(defaultElevation = 6.dp),
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceContainer),
onClick = {
if (strategy.status == StrategyTestingStatus.Completed && strategy.domains.isNotEmpty()) {
expanded = !expanded
}
},
modifier = Modifier
.fillMaxWidth()
.padding(start = 10.dp, end = 10.dp, top = 25.dp, bottom = 0.dp)
) {
Column (
Modifier
.fillMaxWidth()
.padding(16.dp)
)
{
Row {
Text(
text = strategy.path,
modifier = Modifier
.weight(1f)
)
FilledTonalIconButton(
onClick = {
if (getActiveStrategy(prefs).isNotEmpty()) disableStrategy(getActiveStrategy(prefs)[0], prefs)
enableStrategy(strategy.path, prefs)
scope.launch {
snackbarHostState.showSnackbar(
message = context.getString(R.string.strategy_applied)
)
}
},
enabled = strategy.status == StrategyTestingStatus.Completed
) {
Icon(
imageVector = Icons.Default.Check,
contentDescription = "apply"
)
}
}
Row {
Text(
text = stringResource(strategy.status.resId),
modifier = Modifier
.weight(1f),
fontSize = 12.sp,
)
}
Row (
modifier = Modifier
.fillMaxWidth()
.padding(top = 8.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.End
) {
LinearProgressIndicator(
modifier = Modifier
.weight(1f),
progress = {
strategy.progress
},
color = ProgressIndicatorDefaults.linearColor,
trackColor = ProgressIndicatorDefaults.linearTrackColor,
strokeCap = ProgressIndicatorDefaults.LinearStrokeCap,
)
Text(
text = "${(strategy.progress*100).toInt()}%",
modifier = Modifier
.padding(start = 16.dp),
)
}
}
AnimatedVisibility(
visible = expanded,
enter = expandVertically(),
exit = shrinkVertically()
) {
Card (
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceContainer),
modifier = Modifier
.padding(horizontal = 8.dp)
.fillMaxWidth()
) {
Text(
text = stringResource(R.string.selection_available_domains)
)
LazyColumn(modifier = Modifier.heightIn(max = 300.dp)) {
items(strategy.domains) { item ->
Card(
elevation = CardDefaults.cardElevation(4.dp),
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 4.dp)
) {
Text(
text = item,
modifier = Modifier.padding(16.dp),
style = MaterialTheme.typography.bodyLarge
)
}
}
}
}
}
}
}

View File

@@ -35,9 +35,9 @@ fun SettingsItem(title: String, checked: Boolean, onToggle: (Boolean) -> Unit, o
ElevatedCard( ElevatedCard(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.height(80.dp) .height(80.dp),
.clickable { onToggle(!checked) },
elevation = CardDefaults.cardElevation(defaultElevation = 4.dp), elevation = CardDefaults.cardElevation(defaultElevation = 4.dp),
onClick = { onToggle(!checked) },
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceVariant) colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceVariant)
) { ) {
Row( Row(
@@ -63,9 +63,9 @@ fun SettingsActionItem(title: String, onClick: () -> Unit) {
ElevatedCard( ElevatedCard(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.height(80.dp) .height(80.dp),
.clickable { onClick() },
elevation = CardDefaults.cardElevation(defaultElevation = 4.dp), elevation = CardDefaults.cardElevation(defaultElevation = 4.dp),
onClick = { onClick() },
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceVariant) colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceVariant)
) { ) {
Row( Row(

View File

@@ -196,9 +196,7 @@ private fun UpdateCard(updateAvailable: MutableState<Boolean>, onClick: () -> Un
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceVariant), colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceVariant),
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(start = 10.dp, top = 10.dp, end = 10.dp) .padding(start = 10.dp, top = 10.dp, end = 10.dp),
.width(140.dp)
.height(70.dp),
onClick = onClick onClick = onClick
) { ) {
Text( Text(

View File

@@ -0,0 +1,223 @@
package com.cherret.zaprett.ui.screen
import android.content.Context
import android.content.SharedPreferences
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.Download
import androidx.compose.material.icons.filled.UploadFile
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.FloatingActionButton
import androidx.compose.material3.Icon
import androidx.compose.material3.Scaffold
import androidx.compose.material3.SegmentedButton
import androidx.compose.material3.SegmentedButtonDefaults
import androidx.compose.material3.SingleChoiceSegmentedButtonRow
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.pulltorefresh.PullToRefreshBox
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.Font
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.NavController
import com.cherret.zaprett.R
import com.cherret.zaprett.ui.component.ListSwitchItem
import com.cherret.zaprett.ui.viewmodel.HostsViewModel
import com.cherret.zaprett.ui.viewmodel.IpsetViewModel
import com.cherret.zaprett.utils.getHostListMode
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun IpsetsScreen(navController: NavController, viewModel: IpsetViewModel = viewModel()) {
val context = LocalContext.current
val prefs = context.getSharedPreferences("settings", Context.MODE_PRIVATE)
val scope = rememberCoroutineScope()
val snackbarHostState = remember { SnackbarHostState() }
val allLists = viewModel.allItems
val checked = viewModel.checked
val isRefreshing = viewModel.isRefreshing
val filePickerLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.OpenDocument()
) { uri ->
uri?.let {
if (getHostListMode(prefs) == "whitelist") viewModel.copySelectedFile(context, "/ipset/include", it)
else viewModel.copySelectedFile(context, "/ipset/exclude", it) }
}
LaunchedEffect(Unit) {
viewModel.refresh()
}
Scaffold(
topBar = {
TopAppBar(
title = {
Text(
text = stringResource(R.string.title_ipset),
fontSize = 40.sp,
fontFamily = FontFamily(Font(R.font.unbounded, FontWeight.Normal))
)
},
windowInsets = WindowInsets(0)
)
},
content = { paddingValues ->
PullToRefreshBox(
isRefreshing = isRefreshing,
onRefresh = {
viewModel.refresh()
},
modifier = Modifier.fillMaxSize()
) {
LazyColumn(
contentPadding = PaddingValues(
top = paddingValues.calculateTopPadding(),
bottom = paddingValues.calculateBottomPadding() + 80.dp
),
modifier = Modifier.navigationBarsPadding().fillMaxSize()
) {
item {
IpsetTypeChoose(viewModel, prefs)
}
when {
allLists.isEmpty() -> {
item {
Box(
modifier = Modifier.fillParentMaxSize(),
contentAlignment = Alignment.Center
) {
Text(
stringResource(R.string.empty_list),
textAlign = TextAlign.Center
)
}
}
}
else -> {
items(allLists) { item ->
ListSwitchItem (
item = item,
isChecked = checked[item] == true,
onCheckedChange = { isChecked ->
viewModel.onCheckedChange(item, isChecked, snackbarHostState, scope)
},
onDeleteClick = {
viewModel.deleteItem(item, snackbarHostState, scope)
}
)
}
}
}
}
}
},
floatingActionButton = {
FloatingMenu(navController, filePickerLauncher)
},
snackbarHost = { SnackbarHost(hostState = snackbarHostState) }
)
}
@Composable
private fun FloatingMenu(navController: NavController, launcher: ActivityResultLauncher<Array<String>>) {
var expanded by remember { mutableStateOf(false) }
FloatingActionButton(
modifier = Modifier.size(80.dp),
onClick = { expanded = !expanded }
) {
Icon(Icons.Default.Add, contentDescription = stringResource(R.string.btn_add_host))
}
DropdownMenu(
expanded = expanded,
onDismissRequest = { expanded = false }
) {
DropdownMenuItem(
text = { Text(stringResource(R.string.btn_download_host)) },
onClick = {
expanded = false
navController.navigate("repo?source=ipsets") { launchSingleTop = true }
},
leadingIcon = {
Icon(Icons.Default.Download, contentDescription = stringResource(R.string.btn_download_host))
}
)
DropdownMenuItem(
text = { Text(stringResource(R.string.btn_add_host)) },
onClick = {
expanded = false
addHost(launcher)
},
leadingIcon = {
Icon(Icons.Default.UploadFile, contentDescription = stringResource(R.string.btn_add_host))
}
)
}
}
@Composable
fun IpsetTypeChoose(viewModel: IpsetViewModel, prefs : SharedPreferences) {
val listType = remember { mutableStateOf(getHostListMode(prefs))}
val options = listOf(stringResource(R.string.title_whitelist), stringResource(R.string.title_blacklist))
val selectedIndex = if (listType.value == "whitelist") 0 else 1
SingleChoiceSegmentedButtonRow (
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp)
) {
options.forEachIndexed { index, label ->
SegmentedButton(
shape = SegmentedButtonDefaults.itemShape(
index = index,
count = options.size
),
onClick = {
listType.value = if (index == 0) "whitelist" else "blacklist"
viewModel.setListType(listType.value)
},
selected = index == selectedIndex,
label = {
Text(
label
)
}
)
}
}
}
private fun addHost(launcher: ActivityResultLauncher<Array<String>>) {
launcher.launch(arrayOf("text/plain"))
}

View File

@@ -97,16 +97,17 @@ fun SettingsScreen(navController: NavController, viewModel : SettingsViewModel =
val context = LocalContext.current val context = LocalContext.current
val sharedPreferences = remember { context.getSharedPreferences("settings", Context.MODE_PRIVATE) } val sharedPreferences = remember { context.getSharedPreferences("settings", Context.MODE_PRIVATE) }
val editor = remember { sharedPreferences.edit() } val editor = remember { sharedPreferences.edit() }
val useModule = remember { mutableStateOf(sharedPreferences.getBoolean("use_module", false)) } val useModule = viewModel.useModule.collectAsState()
val updateOnBoot = remember { mutableStateOf(sharedPreferences.getBoolean("update_on_boot", true)) } val updateOnBoot = remember { mutableStateOf(sharedPreferences.getBoolean("update_on_boot", true)) }
val autoRestart = remember { mutableStateOf(getStartOnBoot()) } val autoRestart = viewModel.autoRestart.collectAsState()
val autoUpdate = remember { mutableStateOf(sharedPreferences.getBoolean("auto_update", true)) } val autoUpdate = remember { mutableStateOf(sharedPreferences.getBoolean("auto_update", BuildConfig.auto_update)) }
val sendFirebaseAnalytics = remember { mutableStateOf(sharedPreferences.getBoolean("send_firebase_analytics", true)) } val sendFirebaseAnalytics = remember { mutableStateOf(sharedPreferences.getBoolean("send_firebase_analytics", BuildConfig.send_firebase_analytics)) }
val ipv6 = remember { mutableStateOf(sharedPreferences.getBoolean("ipv6",false)) } val ipv6 = remember { mutableStateOf(sharedPreferences.getBoolean("ipv6",false)) }
val openNoRootDialog = remember { mutableStateOf(false) } val openNoRootDialog = remember { mutableStateOf(false) }
val openNoModuleDialog = remember { mutableStateOf(false) } val openNoModuleDialog = remember { mutableStateOf(false) }
val showAboutDialog = remember { mutableStateOf(false) } val showAboutDialog = remember { mutableStateOf(false) }
val showHostsRepoUrlDialog = remember { mutableStateOf(false) } val showHostsRepoUrlDialog = remember { mutableStateOf(false) }
val showIpsetRepoUrlDialog = remember { mutableStateOf(false) }
val showStrategyRepoUrlDialog = remember { mutableStateOf(false) } val showStrategyRepoUrlDialog = remember { mutableStateOf(false) }
val showIPDialog = remember { mutableStateOf(false) } val showIPDialog = remember { mutableStateOf(false) }
val showPortDialog = remember { mutableStateOf(false) } val showPortDialog = remember { mutableStateOf(false) }
@@ -116,6 +117,7 @@ fun SettingsScreen(navController: NavController, viewModel : SettingsViewModel =
val showBlackDialog = remember { mutableStateOf(false) } val showBlackDialog = remember { mutableStateOf(false) }
val showAppsListsSheet = remember { mutableStateOf(false) } val showAppsListsSheet = remember { mutableStateOf(false) }
val showSystemApps = remember { mutableStateOf(sharedPreferences.getBoolean("show_system_apps", false)) } val showSystemApps = remember { mutableStateOf(sharedPreferences.getBoolean("show_system_apps", false)) }
val showChangeProbeTimeout = remember { mutableStateOf(false) }
val settingsList = listOf( val settingsList = listOf(
Setting.Section(stringResource(R.string.general_section)), Setting.Section(stringResource(R.string.general_section)),
@@ -123,17 +125,11 @@ fun SettingsScreen(navController: NavController, viewModel : SettingsViewModel =
title = stringResource(R.string.btn_use_root), title = stringResource(R.string.btn_use_root),
checked = useModule.value, checked = useModule.value,
onToggle = { isChecked -> onToggle = { isChecked ->
useModule( viewModel.useModule(
context = context, context = context,
checked = isChecked, checked = isChecked,
openNoRootDialog = openNoRootDialog, openNoRootDialog = openNoRootDialog,
openNoModuleDialog = openNoModuleDialog openNoModuleDialog = openNoModuleDialog)
) { success ->
if (success) {
useModule.value = isChecked
if (!isChecked) stopService { }
}
}
} }
), ),
Setting.Toggle( Setting.Toggle(
@@ -167,6 +163,13 @@ fun SettingsScreen(navController: NavController, viewModel : SettingsViewModel =
showHostsRepoUrlDialog.value = true showHostsRepoUrlDialog.value = true
} }
), ),
Setting.Action(
title = stringResource(R.string.ipset_repo_url),
onClick = {
textDialogValue.value = sharedPreferences.getString("ipset_repo_url", "https://raw.githubusercontent.com/CherretGit/zaprett-repo/refs/heads/main/ipsets.json") ?: "https://raw.githubusercontent.com/CherretGit/zaprett-repo/refs/heads/main/ipsets.json"
showIpsetRepoUrlDialog.value = true
}
),
Setting.Action( Setting.Action(
title = stringResource(R.string.btn_repository_url_strategies), title = stringResource(R.string.btn_repository_url_strategies),
onClick = { onClick = {
@@ -174,9 +177,7 @@ fun SettingsScreen(navController: NavController, viewModel : SettingsViewModel =
showStrategyRepoUrlDialog.value = true showStrategyRepoUrlDialog.value = true
} }
), ),
Setting.Section( Setting.Section(title = stringResource(R.string.shared_section)),
title = stringResource(R.string.shared_section)
),
Setting.Action( Setting.Action(
title = stringResource(R.string.btn_applist), title = stringResource(R.string.btn_applist),
onClick = { onClick = {
@@ -195,6 +196,20 @@ fun SettingsScreen(navController: NavController, viewModel : SettingsViewModel =
showBlackDialog.value = true showBlackDialog.value = true
} }
), ),
Setting.Section(stringResource(R.string.title_selection)),
Setting.Action(
title = stringResource(R.string.begin_selection),
onClick = {
navController.navigate("selectionScreen")
}
),
Setting.Action(
title = stringResource(R.string.change_probe_timeout),
onClick = {
textDialogValue.value = sharedPreferences.getLong("probe_timeout", 1000L).toString()
showChangeProbeTimeout.value = true
}
),
Setting.Section(stringResource(R.string.byedpi_section)), Setting.Section(stringResource(R.string.byedpi_section)),
Setting.Toggle( Setting.Toggle(
title = stringResource(R.string.btn_ipv6), title = stringResource(R.string.btn_ipv6),
@@ -225,14 +240,12 @@ fun SettingsScreen(navController: NavController, viewModel : SettingsViewModel =
showDNSDialog.value = true showDNSDialog.value = true
} }
), ),
Setting.Section( Setting.Section(title = stringResource(R.string.zapret_section)),
title = stringResource(R.string.zapret_section)
),
Setting.Toggle( Setting.Toggle(
title = stringResource(R.string.btn_autorestart), title = stringResource(R.string.btn_autorestart),
checked = autoRestart.value, checked = autoRestart.value,
onToggle = { onToggle = {
if (handleAutoRestart(context, it)) autoRestart.value = it viewModel.handleAutoRestart(context)
} }
) )
) )
@@ -262,6 +275,11 @@ fun SettingsScreen(navController: NavController, viewModel : SettingsViewModel =
editor.putString("hosts_repo_url", it).apply() editor.putString("hosts_repo_url", it).apply()
}, onDismiss = { showHostsRepoUrlDialog.value = false }) }, onDismiss = { showHostsRepoUrlDialog.value = false })
} }
if (showIpsetRepoUrlDialog.value) {
TextDialog(stringResource(R.string.btn_repository_url_ipsets), stringResource(R.string.hint_enter_repository_url_ipsets), textDialogValue.value, onConfirm = {
editor.putString("ipsets_repo_url", it).apply()
}, onDismiss = { showIpsetRepoUrlDialog.value = false })
}
if (showStrategyRepoUrlDialog.value) { if (showStrategyRepoUrlDialog.value) {
TextDialog(stringResource(R.string.btn_repository_url_strategies), stringResource(R.string.hint_enter_repository_url_strategies), textDialogValue.value, onConfirm = { TextDialog(stringResource(R.string.btn_repository_url_strategies), stringResource(R.string.hint_enter_repository_url_strategies), textDialogValue.value, onConfirm = {
@@ -323,6 +341,12 @@ fun SettingsScreen(navController: NavController, viewModel : SettingsViewModel =
) )
} }
if (showChangeProbeTimeout.value) {
TextDialog(stringResource(R.string.probe_timeout), stringResource(R.string.hint_enter_probe_timeout), textDialogValue.value, onConfirm = {
editor.putLong("probe_timeout", it.toLong()).apply()
}, onDismiss = { showChangeProbeTimeout.value = false })
}
Scaffold( Scaffold(
topBar = { topBar = {
TopAppBar( TopAppBar(
@@ -470,54 +494,6 @@ private fun ListBottomSheet(
} }
} }
private fun useModule(context: Context, checked: Boolean, openNoRootDialog: MutableState<Boolean>, openNoModuleDialog: MutableState<Boolean>, callback: (Boolean) -> Unit) {
val sharedPreferences = context.getSharedPreferences("settings", Context.MODE_PRIVATE)
val editor = sharedPreferences.edit()
if (checked) {
checkRoot { hasRoot ->
if (hasRoot) {
checkModuleInstallation { hasModule ->
if (hasModule) {
editor.putBoolean("use_module", true)
.putBoolean("update_on_boot", true)
.apply()
if (ByeDpiVpnService.status == ServiceStatus.Connected) {
context.startService(Intent(context, ByeDpiVpnService::class.java).apply {
action = "STOP_VPN"
})
}
editor.remove("lists").apply()
editor.remove("active_strategy").apply()
editor.remove("applist").apply()
editor.remove("whitelist").apply()
editor.remove("blacklist").apply()
callback(true)
} else {
openNoModuleDialog.value = true
}
}
} else {
openNoRootDialog.value = true
}
}
} else {
editor.putBoolean("use_module", false)
.putBoolean("update_on_boot", false)
.apply()
callback(true)
}
}
private fun handleAutoRestart(context: Context, checked: Boolean): Boolean {
val sharedPreferences = context.getSharedPreferences("settings", Context.MODE_PRIVATE)
return if (sharedPreferences.getBoolean("use_module", false)) {
setStartOnBoot(checked)
true
} else {
false
}
}
@Composable @Composable

View File

@@ -0,0 +1,196 @@
package com.cherret.zaprett.ui.screen
import android.content.Context.MODE_PRIVATE
import android.content.Intent
import android.net.VpnService
import androidx.activity.result.ActivityResultLauncher
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Scaffold
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.Font
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.sp
import androidx.compose.foundation.lazy.items
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.filled.Info
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.FilledTonalButton
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.TextButton
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.platform.LocalContext
import androidx.lifecycle.viewModelScope
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.NavController
import com.cherret.zaprett.R
import com.cherret.zaprett.ui.component.StrategySelectionItem
import com.cherret.zaprett.ui.viewmodel.StrategySelectionViewModel
import kotlinx.coroutines.launch
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun StrategySelectionScreen(navController: NavController, vpnLauncher: ActivityResultLauncher<Intent>, viewModel : StrategySelectionViewModel = viewModel()){
val snackbarHostState = remember { SnackbarHostState() }
val strategyStates = viewModel.strategyStates
val context = LocalContext.current
val prefs = context.getSharedPreferences("settings", MODE_PRIVATE)
var showDialog = remember { mutableStateOf(false) }
val requestVpnPermission by viewModel.requestVpnPermission.collectAsState()
if (showDialog.value) {
InfoAlert { showDialog.value = false }
}
LaunchedEffect(requestVpnPermission) {
if (requestVpnPermission) {
val intent = VpnService.prepare(context)
if (intent != null) {
vpnLauncher.launch(intent)
} else {
viewModel.startVpn()
viewModel.clearVpnPermissionRequest()
}
}
}
Scaffold(
topBar = {
TopAppBar(
title = {
Text(
text = stringResource(R.string.title_selection),
fontSize = 30.sp,
fontFamily = FontFamily(Font(R.font.unbounded, FontWeight.Normal))
)
},
navigationIcon = {
IconButton(onClick = { navController.popBackStack() }) {
Icon(
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
contentDescription = stringResource(R.string.btn_back)
)
}
},
actions = {
IconButton(
onClick = {
showDialog.value = true
}
) {
Icon(
imageVector = Icons.Default.Info,
contentDescription = "info"
)
}
},
windowInsets = WindowInsets(0)
)
},
snackbarHost = { SnackbarHost(snackbarHostState) },
content = { paddingValues ->
LazyColumn (
modifier = Modifier
.padding(paddingValues)
) {
item {
Row (
modifier = Modifier
.fillMaxWidth(),
horizontalArrangement = Arrangement.Center
)
{
NoHostsCard(viewModel.noHostsCard)
FilledTonalButton(
onClick = {
viewModel.viewModelScope.launch {
snackbarHostState.showSnackbar(
context.getString(R.string.begin_selection_snack)
)
viewModel.performTest()
}
}
) {
Text(stringResource(R.string.begin_selection))
}
}
}
when {
strategyStates.isEmpty() -> {
item {
Box(
modifier = Modifier.fillParentMaxSize(),
contentAlignment = Alignment.Center
) {
Text(
stringResource(R.string.empty_list),
textAlign = TextAlign.Center
)
}
}
}
else -> {
items(strategyStates, key = { it.path }) { item ->
StrategySelectionItem(item, prefs, context, snackbarHostState)
}
}
}
}
}
)
}
@Composable
fun InfoAlert(onDismiss: () -> Unit) {
AlertDialog(
title = { Text(text = stringResource(R.string.strategy_selection_info_title)) },
text = { Text(text = stringResource(R.string.strategy_selection_info_msg)) },
onDismissRequest = onDismiss,
confirmButton = {
TextButton(onClick = onDismiss) {
Text(stringResource(R.string.btn_continue))
}
}
)
}
@Composable
private fun NoHostsCard(noHostsCard: MutableState<Boolean>) {
if (noHostsCard.value) {
AlertDialog(
title = { Text(text = stringResource(R.string.selection_no_hosts_title)) },
text = { Text(text = stringResource(R.string.selection_no_hosts_message)) },
onDismissRequest = {
noHostsCard.value = false
},
confirmButton = {
TextButton(onClick = { noHostsCard.value = false }) {
Text(stringResource(R.string.btn_continue))
}
}
)
}
}

View File

@@ -1,5 +1,6 @@
package com.cherret.zaprett.ui.viewmodel package com.cherret.zaprett.ui.viewmodel
import android.annotation.SuppressLint
import android.app.Application import android.app.Application
import android.content.Context import android.content.Context
import android.content.SharedPreferences import android.content.SharedPreferences
@@ -25,7 +26,8 @@ import kotlinx.coroutines.launch
import java.io.File import java.io.File
abstract class BaseRepoViewModel(application: Application) : AndroidViewModel(application) { abstract class BaseRepoViewModel(application: Application) : AndroidViewModel(application) {
val context = application.applicationContext @SuppressLint("StaticFieldLeak")
val context: Context = application.applicationContext
val sharedPreferences: SharedPreferences = context.getSharedPreferences("settings", Context.MODE_PRIVATE) val sharedPreferences: SharedPreferences = context.getSharedPreferences("settings", Context.MODE_PRIVATE)
private val _errorFlow = MutableStateFlow<Throwable?>(null) private val _errorFlow = MutableStateFlow<Throwable?>(null)
val errorFlow: StateFlow<Throwable?> = _errorFlow val errorFlow: StateFlow<Throwable?> = _errorFlow
@@ -59,6 +61,8 @@ abstract class BaseRepoViewModel(application: Application) : AndroidViewModel(ap
when (item.type) { when (item.type) {
ItemType.list -> listType == "whitelist" ItemType.list -> listType == "whitelist"
ItemType.list_exclude -> listType == "blacklist" ItemType.list_exclude -> listType == "blacklist"
ItemType.ipset -> listType == "whitelist"
ItemType.ipset_exclude -> listType == "blacklist"
ItemType.nfqws -> useModule ItemType.nfqws -> useModule
ItemType.byedpi -> !useModule ItemType.byedpi -> !useModule
} }
@@ -101,6 +105,8 @@ abstract class BaseRepoViewModel(application: Application) : AndroidViewModel(ap
ItemType.nfqws -> File(getZaprettPath(), "strategies/nfqws") ItemType.nfqws -> File(getZaprettPath(), "strategies/nfqws")
ItemType.list -> File(getZaprettPath(), "lists/include") ItemType.list -> File(getZaprettPath(), "lists/include")
ItemType.list_exclude -> File(getZaprettPath(), "lists/exclude") ItemType.list_exclude -> File(getZaprettPath(), "lists/exclude")
ItemType.ipset -> File(getZaprettPath(), "ipset/include")
ItemType.ipset_exclude -> File(getZaprettPath(), "ipset/exclude")
} }
val targetFile = File(targetDir, uri.lastPathSegment!!) val targetFile = File(targetDir, uri.lastPathSegment!!)
sourceFile.copyTo(targetFile, overwrite = true) sourceFile.copyTo(targetFile, overwrite = true)
@@ -131,6 +137,8 @@ abstract class BaseRepoViewModel(application: Application) : AndroidViewModel(ap
ItemType.nfqws -> File(getZaprettPath(), "strategies/nfqws") ItemType.nfqws -> File(getZaprettPath(), "strategies/nfqws")
ItemType.list -> File(getZaprettPath(), "lists/include") ItemType.list -> File(getZaprettPath(), "lists/include")
ItemType.list_exclude -> File(getZaprettPath(), "lists/exclude") ItemType.list_exclude -> File(getZaprettPath(), "lists/exclude")
ItemType.ipset -> File(getZaprettPath(), "ipset/include")
ItemType.ipset_exclude -> File(getZaprettPath(), "ipset/exclude")
} }
val targetFile = File(targetDir, uri.lastPathSegment!!) val targetFile = File(targetDir, uri.lastPathSegment!!)
sourceFile.copyTo(targetFile, overwrite = true) sourceFile.copyTo(targetFile, overwrite = true)

View File

@@ -14,6 +14,7 @@ import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.AndroidViewModel
import com.cherret.zaprett.BuildConfig
import com.cherret.zaprett.R import com.cherret.zaprett.R
import com.cherret.zaprett.byedpi.ByeDpiVpnService import com.cherret.zaprett.byedpi.ByeDpiVpnService
import com.cherret.zaprett.data.ServiceStatus import com.cherret.zaprett.data.ServiceStatus
@@ -50,7 +51,7 @@ class HomeViewModel(application: Application) : AndroidViewModel(application) {
var nfqwsVer = mutableStateOf(context.getString(R.string.unknown_text)) var nfqwsVer = mutableStateOf(context.getString(R.string.unknown_text))
private set private set
var byedpiVer = mutableStateOf("0.17.2") var byedpiVer = mutableStateOf("0.17.3")
private set private set
var serviceMode = mutableIntStateOf(R.string.service_mode_ciadpi) var serviceMode = mutableIntStateOf(R.string.service_mode_ciadpi)
@@ -71,7 +72,7 @@ class HomeViewModel(application: Application) : AndroidViewModel(application) {
var showUpdateDialog = mutableStateOf(false) var showUpdateDialog = mutableStateOf(false)
fun checkForUpdate() { fun checkForUpdate() {
if (prefs.getBoolean("auto_update", true)) { if (prefs.getBoolean("auto_update", BuildConfig.auto_update)) {
getUpdate(prefs) { getUpdate(prefs) {
if (it != null) { if (it != null) {
downloadUrl.value = it.downloadUrl.toString() downloadUrl.value = it.downloadUrl.toString()

View File

@@ -0,0 +1,14 @@
package com.cherret.zaprett.ui.viewmodel
import android.app.Application
import com.cherret.zaprett.utils.RepoItemInfo
import com.cherret.zaprett.utils.getAllIpsets
import com.cherret.zaprett.utils.getAllLists
import com.cherret.zaprett.utils.getHostList
import com.cherret.zaprett.utils.getIpsetList
class IpsetRepoViewModel(application: Application): BaseRepoViewModel(application) {
override fun getInstalledLists(): Array<String> = getAllIpsets()
override fun getRepoList(callback: (Result<List<RepoItemInfo>>) -> Unit) =
getIpsetList(sharedPreferences, callback)
}

View File

@@ -0,0 +1,65 @@
package com.cherret.zaprett.ui.viewmodel
import android.app.Application
import android.content.Context
import androidx.compose.material3.SnackbarHostState
import com.cherret.zaprett.utils.disableIpset
import com.cherret.zaprett.utils.disableList
import com.cherret.zaprett.utils.enableIpset
import com.cherret.zaprett.utils.enableList
import com.cherret.zaprett.utils.getActiveExcludeIpsets
import com.cherret.zaprett.utils.getActiveExcludeLists
import com.cherret.zaprett.utils.getActiveIpsets
import com.cherret.zaprett.utils.getAllExcludeIpsets
import com.cherret.zaprett.utils.getAllIpsets
import com.cherret.zaprett.utils.getHostListMode
import com.cherret.zaprett.utils.getStatus
import com.cherret.zaprett.utils.setHostListMode
import kotlinx.coroutines.CoroutineScope
import java.io.File
class IpsetViewModel(application: Application): BaseListsViewModel(application) {
private val sharedPreferences = application.getSharedPreferences("settings", Context.MODE_PRIVATE)
override fun loadAllItems(): Array<String> =
if (getHostListMode(sharedPreferences) == "whitelist") getAllIpsets()
else getAllExcludeIpsets()
override fun loadActiveItems(): Array<String> =
if (getHostListMode(sharedPreferences) == "whitelist") getActiveIpsets(sharedPreferences)
else getActiveExcludeIpsets(sharedPreferences)
override fun deleteItem(item: String, snackbarHostState: SnackbarHostState, scope: CoroutineScope) {
val wasChecked = checked[item] == true
disableIpset(item, sharedPreferences)
val success = File(item).delete()
if (success) refresh()
if (sharedPreferences.getBoolean("use_module", false)) {
getStatus { isEnabled ->
if (isEnabled && wasChecked) {
snackbarHostState.currentSnackbarData?.dismiss()
showRestartSnackbar(context, snackbarHostState, scope)
}
}
}
}
override fun onCheckedChange(item: String, isChecked: Boolean, snackbarHostState: SnackbarHostState, scope: CoroutineScope) {
checked[item] = isChecked
if (isChecked) enableIpset(item, sharedPreferences) else disableIpset(item, sharedPreferences)
if (sharedPreferences.getBoolean("use_module", false)) {
getStatus { isEnabled ->
if (isEnabled) {
snackbarHostState.currentSnackbarData?.dismiss()
showRestartSnackbar(
context,
snackbarHostState,
scope
)
}
}
}
}
fun setListType(type : String) {
setHostListMode(sharedPreferences, type)
refresh()
}
}

View File

@@ -1,15 +1,24 @@
package com.cherret.zaprett.ui.viewmodel package com.cherret.zaprett.ui.viewmodel
import android.app.Application import android.app.Application
import android.content.Context
import android.content.Context.MODE_PRIVATE import android.content.Context.MODE_PRIVATE
import android.content.Intent
import android.content.pm.ApplicationInfo import android.content.pm.ApplicationInfo
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.graphics.drawable.Drawable import android.graphics.drawable.Drawable
import androidx.compose.runtime.MutableState
import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.AndroidViewModel
import com.cherret.zaprett.byedpi.ByeDpiVpnService
import com.cherret.zaprett.data.AppListType import com.cherret.zaprett.data.AppListType
import com.cherret.zaprett.data.ServiceStatus
import com.cherret.zaprett.utils.addPackageToList import com.cherret.zaprett.utils.addPackageToList
import com.cherret.zaprett.utils.checkModuleInstallation
import com.cherret.zaprett.utils.checkRoot
import com.cherret.zaprett.utils.getAppList import com.cherret.zaprett.utils.getAppList
import com.cherret.zaprett.utils.getStartOnBoot
import com.cherret.zaprett.utils.removePackageFromList import com.cherret.zaprett.utils.removePackageFromList
import com.cherret.zaprett.utils.setStartOnBoot
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
@@ -24,9 +33,18 @@ class SettingsViewModel(application: Application) : AndroidViewModel(application
private val _selectedPackages = MutableStateFlow<Set<String>>(emptySet()) private val _selectedPackages = MutableStateFlow<Set<String>>(emptySet())
val selectedPackages: StateFlow<Set<String>> = _selectedPackages.asStateFlow() val selectedPackages: StateFlow<Set<String>> = _selectedPackages.asStateFlow()
private val _currentListType = MutableStateFlow(AppListType.Whitelist) private val _currentListType = MutableStateFlow(AppListType.Whitelist)
private val _useModule = MutableStateFlow(false)
val useModule: StateFlow<Boolean> = _useModule
private val _autoRestart = MutableStateFlow(false)
val autoRestart: StateFlow<Boolean> = _autoRestart
init { init {
refreshApplications() refreshApplications()
_useModule.value = context.getSharedPreferences("settings", MODE_PRIVATE).getBoolean("use_module", false)
getStartOnBoot(prefs) { value ->
_autoRestart.value = value
}
} }
suspend fun getAppIconBitmap(packageName: String): Drawable? = withContext(Dispatchers.IO) { suspend fun getAppIconBitmap(packageName: String): Drawable? = withContext(Dispatchers.IO) {
@@ -100,4 +118,46 @@ class SettingsViewModel(application: Application) : AndroidViewModel(application
refreshApplications() refreshApplications()
} }
fun useModule(context: Context, checked: Boolean, openNoRootDialog: MutableState<Boolean>, openNoModuleDialog: MutableState<Boolean>) {
val sharedPreferences = context.getSharedPreferences("settings", Context.MODE_PRIVATE)
val editor = sharedPreferences.edit()
if (checked) {
checkRoot { hasRoot ->
if (hasRoot) {
checkModuleInstallation { hasModule ->
if (hasModule) {
editor.putBoolean("use_module", true).apply()
if (ByeDpiVpnService.status == ServiceStatus.Connected) {
context.startService(Intent(context, ByeDpiVpnService::class.java).apply {
action = "STOP_VPN"
})
}
editor.remove("lists").apply()
editor.remove("active_strategy").apply()
editor.remove("applist").apply()
editor.remove("whitelist").apply()
editor.remove("blacklist").apply()
_useModule.value = true
} else {
openNoModuleDialog.value = true
}
}
} else {
openNoRootDialog.value = true
}
}
} else {
editor.putBoolean("use_module", false).apply()
_useModule.value = false
}
}
fun handleAutoRestart(context: Context) {
val sharedPreferences = context.getSharedPreferences("settings", MODE_PRIVATE)
if (sharedPreferences.getBoolean("use_module", false)) {
setStartOnBoot(prefs) { value ->
_autoRestart.value = value
}
}
}
} }

View File

@@ -0,0 +1,192 @@
package com.cherret.zaprett.ui.viewmodel
import android.app.Application
import android.content.Context.MODE_PRIVATE
import android.content.Intent
import android.util.Log
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateOf
import androidx.core.content.ContextCompat
import androidx.lifecycle.AndroidViewModel
import com.cherret.zaprett.R
import com.cherret.zaprett.byedpi.ByeDpiVpnService
import com.cherret.zaprett.data.ServiceStatus
import com.cherret.zaprett.data.StrategyCheckResult
import com.cherret.zaprett.data.StrategyTestingStatus
import com.cherret.zaprett.utils.disableStrategy
import com.cherret.zaprett.utils.enableStrategy
import com.cherret.zaprett.utils.getActiveLists
import com.cherret.zaprett.utils.getActiveStrategy
import com.cherret.zaprett.utils.getAllStrategies
import com.cherret.zaprett.utils.getStatus
import com.cherret.zaprett.utils.startService
import com.cherret.zaprett.utils.stopService
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.withContext
import kotlinx.coroutines.withTimeoutOrNull
import okhttp3.OkHttpClient
import okhttp3.Request
import java.io.File
import java.net.InetSocketAddress
import java.net.Proxy
import java.util.concurrent.TimeUnit
class StrategySelectionViewModel(application: Application) : AndroidViewModel(application) {
val prefs = application.getSharedPreferences("settings", MODE_PRIVATE)
val context = application
private val _requestVpnPermission = MutableStateFlow(false)
val requestVpnPermission = _requestVpnPermission.asStateFlow()
val strategyStates = mutableStateListOf<StrategyCheckResult>()
var noHostsCard = mutableStateOf(false)
private set
init {
loadStrategies()
checkHosts()
}
fun buildHttpClient(): OkHttpClient {
val builder = OkHttpClient.Builder()
.callTimeout(prefs.getLong("probe_timeout", 1000L), TimeUnit.MILLISECONDS)
.followRedirects(true)
.followSslRedirects(true)
if (!prefs.getBoolean("use_module", false)) {
val ip = prefs.getString("ip", "127.0.0.1") ?: "127.0.0.1"
val port = prefs.getString("port", "1080")?.toIntOrNull() ?: 1080
val proxy = Proxy(Proxy.Type.SOCKS, InetSocketAddress(ip, port))
builder.proxy(proxy)
}
return builder.build()
}
fun loadStrategies() {
val strategyList = getAllStrategies(prefs)
strategyStates.clear()
strategyList.forEach { name ->
strategyStates += StrategyCheckResult(
path = name,
status = StrategyTestingStatus.Waiting,
progress = 0f,
domains = emptyList()
)
}
}
suspend fun testDomain(domain : String) : Boolean = withContext(Dispatchers.IO) {
val request = Request.Builder()
.url("https://${domain}")
.build()
try {
buildHttpClient().newCall(request).execute().use { response ->
val body = response.body.byteStream().readBytes()
val contentLength = response.body.contentLength()
contentLength <= 0 || body.size.toLong() >= contentLength
}
} catch (e: Exception) {
false
}
}
suspend fun countReachable(index: Int, urls: List<String>): Float = coroutineScope {
if (urls.isEmpty()) return@coroutineScope 0f
val results: List<String> = urls.map { url ->
async { if (testDomain(url)) url else null }
}.awaitAll().filterNotNull()
strategyStates[index].domains = results
(results.size.toFloat() / urls.size.toFloat()).coerceIn(0f, 1f)
}
suspend fun readActiveListsLines(): List<String> = withContext(Dispatchers.IO) {
val result = mutableListOf<String>()
getActiveLists(prefs).forEach { path ->
runCatching {
File(path).useLines { lines ->
lines.forEach { line ->
result += line
}
}
}.onFailure {
Log.e("Error", "Occured error when creating list for check")
}
}
result
}
suspend fun performTest() {
val targets = readActiveListsLines()
for (index in strategyStates.indices) {
val current = strategyStates[index]
strategyStates[index] = current.copy(status = StrategyTestingStatus.Testing)
enableStrategy(current.path, prefs)
if (prefs.getBoolean("use_module", false)) {
getStatus { if (it) stopService {} }
startService {}
try {
val progress = countReachable(index, targets)
val old = strategyStates[index]
strategyStates[index] = old.copy(
progress = progress,
status = StrategyTestingStatus.Completed
)
} finally {
stopService {}
disableStrategy(current.path, prefs)
}
}
else {
if (ByeDpiVpnService.status == ServiceStatus.Connected) {
context.startService(Intent(context, ByeDpiVpnService::class.java).apply {
action = "STOP_VPN"
})
delay(300L)
}
_requestVpnPermission.value = true
/*context.startService(Intent(context, ByeDpiVpnService::class.java).apply {
action = "START_VPN"
})*/
val connected = withTimeoutOrNull(10_000L) {
while (ByeDpiVpnService.status != ServiceStatus.Connected) {
delay(100L)
}
true
} ?: false
if (connected) delay(150L)
try {
val progress = countReachable(index,targets)
val old = strategyStates[index]
strategyStates[index] = old.copy(
progress = progress,
status = StrategyTestingStatus.Completed
)
} finally {
context.startService(Intent(context, ByeDpiVpnService::class.java).apply {
action = "STOP_VPN"
})
delay(200L)
disableStrategy(current.path, prefs)
}
}
}
val sorted = strategyStates.sortedByDescending { it.progress }
strategyStates.clear()
strategyStates.addAll(sorted)
}
fun checkHosts() {
if (getActiveLists(prefs).isEmpty()) noHostsCard.value = true
Log.d("getActiveLists.isEmpty", getActiveLists(prefs).isEmpty().toString())
}
fun startVpn() {
ContextCompat.startForegroundService(context, Intent(context, ByeDpiVpnService::class.java).apply { action = "START_VPN" })
}
fun clearVpnPermissionRequest() {
_requestVpnPermission.value = false
}
}

View File

@@ -8,8 +8,8 @@ import com.cherret.zaprett.byedpi.ByeDpiVpnService
import com.cherret.zaprett.data.ServiceStatus import com.cherret.zaprett.data.ServiceStatus
import com.cherret.zaprett.utils.disableStrategy import com.cherret.zaprett.utils.disableStrategy
import com.cherret.zaprett.utils.enableStrategy import com.cherret.zaprett.utils.enableStrategy
import com.cherret.zaprett.utils.getActiveByeDPIStrategies import com.cherret.zaprett.utils.getActiveByeDPIStrategy
import com.cherret.zaprett.utils.getActiveNfqwsStrategies import com.cherret.zaprett.utils.getActiveNfqwsStrategy
import com.cherret.zaprett.utils.getAllByeDPIStrategies import com.cherret.zaprett.utils.getAllByeDPIStrategies
import com.cherret.zaprett.utils.getAllNfqwsStrategies import com.cherret.zaprett.utils.getAllNfqwsStrategies
import com.cherret.zaprett.utils.getStatus import com.cherret.zaprett.utils.getStatus
@@ -84,10 +84,10 @@ interface StrategyProvider {
class NfqwsStrategyProvider : StrategyProvider { class NfqwsStrategyProvider : StrategyProvider {
override fun getAll() = getAllNfqwsStrategies() override fun getAll() = getAllNfqwsStrategies()
override fun getActive() = getActiveNfqwsStrategies() override fun getActive() = getActiveNfqwsStrategy()
} }
class ByeDPIStrategyProvider(private val sharedPreferences: SharedPreferences) : StrategyProvider { class ByeDPIStrategyProvider(private val sharedPreferences: SharedPreferences) : StrategyProvider {
override fun getAll() = getAllByeDPIStrategies() override fun getAll() = getAllByeDPIStrategies()
override fun getActive() = getActiveByeDPIStrategies(sharedPreferences) override fun getActive() = getActiveByeDPIStrategy(sharedPreferences)
} }

View File

@@ -36,6 +36,16 @@ fun getHostList(sharedPreferences: SharedPreferences, callback: (Result<List<Rep
) )
} }
fun getIpsetList(sharedPreferences: SharedPreferences, callback: (Result<List<RepoItemInfo>>) -> Unit) {
getRepo(
sharedPreferences.getString(
"ipset_repo_url",
"https://raw.githubusercontent.com/CherretGit/zaprett-repo/refs/heads/main/ipsets.json"
) ?: "https://raw.githubusercontent.com/CherretGit/zaprett-repo/refs/heads/main/ipsets.json",
callback
)
}
fun getStrategiesList(sharedPreferences: SharedPreferences, callback: (Result<List<RepoItemInfo>>) -> Unit) { fun getStrategiesList(sharedPreferences: SharedPreferences, callback: (Result<List<RepoItemInfo>>) -> Unit) {
getRepo( getRepo(
sharedPreferences.getString( sharedPreferences.getString(

View File

@@ -64,39 +64,20 @@ fun getConfigFile(): File {
return File(Environment.getExternalStorageDirectory(), "zaprett/config") return File(Environment.getExternalStorageDirectory(), "zaprett/config")
} }
fun setStartOnBoot(startOnBoot: Boolean) { fun setStartOnBoot(prefs : SharedPreferences, callback: (Boolean) -> Unit) {
val configFile = getConfigFile() if (prefs.getBoolean("use_module", false)) {
if (configFile.exists()) { Shell.cmd("zaprett autostart").submit { result ->
val props = Properties() if (result.out.isNotEmpty() && result.out.toString().contains("true")) callback(true) else callback(false)
try {
FileInputStream(configFile).use { input ->
props.load(input)
}
props.setProperty("start_on_boot", startOnBoot.toString())
FileOutputStream(configFile).use { output ->
props.store(output, "Don't place '/' in end of directory! Example: /sdcard")
}
} catch (e: IOException) {
throw RuntimeException(e)
} }
} }
} }
fun getStartOnBoot(): Boolean { fun getStartOnBoot(prefs : SharedPreferences, callback: (Boolean) -> Unit) {
val configFile = getConfigFile() if (prefs.getBoolean("use_module", false)) {
val props = Properties() Shell.cmd("zaprett get-autostart").submit { result ->
return try { if (result.out.isNotEmpty() && result.out.toString().contains("true")) callback(true) else callback(false)
if (configFile.exists()) {
FileInputStream(configFile).use { input ->
props.load(input)
}
props.getProperty("start_on_boot", "false").toBoolean()
} else {
false
} }
} catch (_: IOException) { } else { callback(false) }
false
}
} }
fun getZaprettPath(): String { fun getZaprettPath(): String {
@@ -117,7 +98,15 @@ fun getZaprettPath(): String {
fun getAllLists(): Array<String> { fun getAllLists(): Array<String> {
val listsDir = File("${getZaprettPath()}/lists/include") val listsDir = File("${getZaprettPath()}/lists/include")
return listsDir.listFiles { file -> file.isFile } return listsDir.listFiles { file -> file.isFile && file.extension.lowercase() == "txt" }
?.map { it.absolutePath }
?.toTypedArray()
?: emptyArray()
}
fun getAllIpsets(): Array<String> {
val listsDir = File("${getZaprettPath()}/ipset/include")
return listsDir.listFiles { file -> file.isFile && file.extension.lowercase() == "txt" }
?.map { it.absolutePath } ?.map { it.absolutePath }
?.toTypedArray() ?.toTypedArray()
?: emptyArray() ?: emptyArray()
@@ -125,7 +114,15 @@ fun getAllLists(): Array<String> {
fun getAllExcludeLists(): Array<String> { fun getAllExcludeLists(): Array<String> {
val listsDir = File("${getZaprettPath()}/lists/exclude/") val listsDir = File("${getZaprettPath()}/lists/exclude/")
return listsDir.listFiles { file -> file.isFile } return listsDir.listFiles { file -> file.isFile && file.extension.lowercase() == "txt" }
?.map { it.absolutePath }
?.toTypedArray()
?: emptyArray()
}
fun getAllExcludeIpsets(): Array<String> {
val listsDir = File("${getZaprettPath()}/ipset/exclude/")
return listsDir.listFiles { file -> file.isFile && file.extension.lowercase() == "txt" }
?.map { it.absolutePath } ?.map { it.absolutePath }
?.toTypedArray() ?.toTypedArray()
?: emptyArray() ?: emptyArray()
@@ -133,7 +130,7 @@ fun getAllExcludeLists(): Array<String> {
fun getAllNfqwsStrategies(): Array<String> { fun getAllNfqwsStrategies(): Array<String> {
val listsDir = File("${getZaprettPath()}/strategies/nfqws") val listsDir = File("${getZaprettPath()}/strategies/nfqws")
return listsDir.listFiles { file -> file.isFile } return listsDir.listFiles { file -> file.isFile && file.extension.lowercase() == "txt" }
?.map { it.absolutePath } ?.map { it.absolutePath }
?.toTypedArray() ?.toTypedArray()
?: emptyArray() ?: emptyArray()
@@ -141,12 +138,17 @@ fun getAllNfqwsStrategies(): Array<String> {
fun getAllByeDPIStrategies(): Array<String> { fun getAllByeDPIStrategies(): Array<String> {
val listsDir = File("${getZaprettPath()}/strategies/byedpi") val listsDir = File("${getZaprettPath()}/strategies/byedpi")
return listsDir.listFiles { file -> file.isFile } return listsDir.listFiles { file -> file.isFile && file.extension.lowercase() == "txt" }
?.map { it.absolutePath } ?.map { it.absolutePath }
?.toTypedArray() ?.toTypedArray()
?: emptyArray() ?: emptyArray()
} }
fun getAllStrategies(sharedPreferences : SharedPreferences) : Array<String> {
return if (sharedPreferences.getBoolean("use_module", false)) getAllNfqwsStrategies()
else getAllByeDPIStrategies()
}
fun getActiveLists(sharedPreferences: SharedPreferences): Array<String> { fun getActiveLists(sharedPreferences: SharedPreferences): Array<String> {
if (sharedPreferences.getBoolean("use_module", false)) { if (sharedPreferences.getBoolean("use_module", false)) {
@@ -171,6 +173,27 @@ fun getActiveLists(sharedPreferences: SharedPreferences): Array<String> {
return sharedPreferences.getStringSet("lists", emptySet())?.toTypedArray() ?: emptyArray() return sharedPreferences.getStringSet("lists", emptySet())?.toTypedArray() ?: emptyArray()
} }
} }
fun getActiveIpsets(sharedPreferences: SharedPreferences): Array<String> {
if (sharedPreferences.getBoolean("use_module", false)) {
val configFile = getConfigFile()
if (configFile.exists()) {
val props = Properties()
return try {
FileInputStream(configFile).use { input ->
props.load(input)
}
val activeLists = props.getProperty("active_ipsets", "")
Log.d("Active ipsets", activeLists)
if (activeLists.isNotEmpty()) activeLists.split(",")
.toTypedArray() else emptyArray()
} catch (e: IOException) {
throw RuntimeException(e)
}
}
return emptyArray()
}
else return sharedPreferences.getStringSet("ipsets", emptySet())?.toTypedArray() ?: emptyArray()
}
fun getActiveExcludeLists(sharedPreferences: SharedPreferences): Array<String> { fun getActiveExcludeLists(sharedPreferences: SharedPreferences): Array<String> {
if (sharedPreferences.getBoolean("use_module", false)) { if (sharedPreferences.getBoolean("use_module", false)) {
val configFile = getConfigFile() val configFile = getConfigFile()
@@ -194,7 +217,29 @@ fun getActiveExcludeLists(sharedPreferences: SharedPreferences): Array<String> {
} }
} }
fun getActiveNfqwsStrategies(): Array<String> { fun getActiveExcludeIpsets(sharedPreferences: SharedPreferences): Array<String> {
if (sharedPreferences.getBoolean("use_module", false)) {
val configFile = getConfigFile()
if (configFile.exists()) {
val props = Properties()
return try {
FileInputStream(configFile).use { input ->
props.load(input)
}
val activeLists = props.getProperty("active_exclude_ipsets", "")
Log.d("Active ipsets", activeLists)
if (activeLists.isNotEmpty()) activeLists.split(",")
.toTypedArray() else emptyArray()
} catch (e: IOException) {
throw RuntimeException(e)
}
}
return emptyArray()
}
else return sharedPreferences.getStringSet("exclude_ipsets", emptySet())?.toTypedArray() ?: emptyArray()
}
fun getActiveNfqwsStrategy(): Array<String> {
val configFile = File("${getZaprettPath()}/config") val configFile = File("${getZaprettPath()}/config")
if (configFile.exists()) { if (configFile.exists()) {
val props = Properties() val props = Properties()
@@ -212,7 +257,7 @@ fun getActiveNfqwsStrategies(): Array<String> {
return emptyArray() return emptyArray()
} }
fun getActiveByeDPIStrategies(sharedPreferences: SharedPreferences): Array<String> { fun getActiveByeDPIStrategy(sharedPreferences: SharedPreferences): Array<String> {
val path = sharedPreferences.getString("active_strategy", "") val path = sharedPreferences.getString("active_strategy", "")
if (!path.isNullOrBlank() && File(path).exists()) { if (!path.isNullOrBlank() && File(path).exists()) {
return arrayOf(path) return arrayOf(path)
@@ -220,7 +265,7 @@ fun getActiveByeDPIStrategies(sharedPreferences: SharedPreferences): Array<Strin
return emptyArray() return emptyArray()
} }
fun getActiveStrategy(sharedPreferences: SharedPreferences): List<String> { fun getActiveByeDPIStrategyContent(sharedPreferences: SharedPreferences): List<String> {
val path = sharedPreferences.getString("active_strategy", "") val path = sharedPreferences.getString("active_strategy", "")
if (!path.isNullOrBlank() && File(path).exists()) { if (!path.isNullOrBlank() && File(path).exists()) {
return File(path).readLines() return File(path).readLines()
@@ -228,6 +273,12 @@ fun getActiveStrategy(sharedPreferences: SharedPreferences): List<String> {
return emptyList() return emptyList()
} }
fun getActiveStrategy(sharedPreferences: SharedPreferences): Array<String> {
return if (sharedPreferences.getBoolean("use_module", false)) getActiveNfqwsStrategy()
else getActiveByeDPIStrategy(sharedPreferences)
}
fun enableList(path: String, sharedPreferences: SharedPreferences) { fun enableList(path: String, sharedPreferences: SharedPreferences) {
if (sharedPreferences.getBoolean("use_module", false)) { if (sharedPreferences.getBoolean("use_module", false)) {
val configFile = getConfigFile() val configFile = getConfigFile()
@@ -272,7 +323,50 @@ fun enableList(path: String, sharedPreferences: SharedPreferences) {
} }
} }
} }
fun enableIpset(path: String, sharedPreferences: SharedPreferences) {
if (sharedPreferences.getBoolean("use_module", false)) {
val configFile = getConfigFile()
try {
val props = Properties()
if (configFile.exists()) {
FileInputStream(configFile).use { input ->
props.load(input)
}
}
val activeLists = props.getProperty(
if (getHostListMode(sharedPreferences) == "whitelist") "active_ipsets"
else "active_exclude_ipsets",
"")
.split(",")
.filter { it.isNotBlank() }
.toMutableList()
if (path !in activeLists) {
activeLists.add(path)
}
props.setProperty(
if (getHostListMode(sharedPreferences) == "whitelist") "active_ipsets"
else "active_exclude_ipsets",
activeLists.joinToString(",")
)
FileOutputStream(configFile).use { output ->
props.store(output, "Don't place '/' in end of directory! Example: /sdcard")
}
} catch (e: IOException) {
throw RuntimeException(e)
}
}
else {
val currentSet = sharedPreferences.getStringSet(
if (getHostListMode(sharedPreferences) == "whitelist") "ipsets"
else "exclude_ipsets", emptySet())?.toMutableSet() ?: mutableSetOf()
if (path !in currentSet) {
currentSet.add(path)
sharedPreferences.edit { putStringSet(
if (getHostListMode(sharedPreferences) == "whitelist") "ipsets"
else "exclude_ipsets", currentSet) }
}
}
}
fun enableStrategy(path: String, sharedPreferences: SharedPreferences) { fun enableStrategy(path: String, sharedPreferences: SharedPreferences) {
if (sharedPreferences.getBoolean("use_module", false)) { if (sharedPreferences.getBoolean("use_module", false)) {
val props = Properties() val props = Properties()
@@ -354,6 +448,57 @@ fun disableList(path: String, sharedPreferences: SharedPreferences) {
} }
} }
fun disableIpset(path: String, sharedPreferences: SharedPreferences) {
if (sharedPreferences.getBoolean("use_module", false)) {
val props = Properties()
val configFile = getConfigFile()
try {
if (configFile.exists()) {
FileInputStream(configFile).use { input ->
props.load(input)
}
}
val activeLists = props.getProperty(
if (getHostListMode(sharedPreferences) == "whitelist") "active_ipsets"
else "active_exclude_ipsets",
"")
.split(",")
.filter { it.isNotBlank() }
.toMutableList()
if (path in activeLists) {
activeLists.remove(path)
}
props.setProperty(
if (getHostListMode(sharedPreferences) == "whitelist") "active_ipsets"
else "active_exclude_ipsets",
activeLists.joinToString(",")
)
FileOutputStream(configFile).use { output ->
props.store(output, "Don't place '/' in end of directory! Example: /sdcard")
}
} catch (e: IOException) {
throw RuntimeException(e)
}
}
else {
val currentSet = sharedPreferences.getStringSet(
if (getHostListMode(sharedPreferences) == "whitelist") "ipsets"
else "exclude_ipsets", emptySet())?.toMutableSet() ?: mutableSetOf()
if (path in currentSet) {
currentSet.remove(path)
sharedPreferences.edit { putStringSet(
if (getHostListMode(sharedPreferences) == "whitelist") "ipsets"
else "exclude_ipsets", currentSet) }
}
if (currentSet.isEmpty()) {
sharedPreferences.edit { remove(
if (getHostListMode(sharedPreferences) == "whitelist") "ipsets"
else "exclude_ipsets"
) }
}
}
}
fun disableStrategy(path: String, sharedPreferences: SharedPreferences) { fun disableStrategy(path: String, sharedPreferences: SharedPreferences) {
if (sharedPreferences.getBoolean("use_module", false)) { if (sharedPreferences.getBoolean("use_module", false)) {
val props = Properties() val props = Properties()
@@ -580,7 +725,7 @@ fun getAppsListMode(prefs : SharedPreferences) : String {
} }
} }
else { else {
return prefs.getString("applist", "")!! return prefs.getString("app_list", "none")!!
} }
return "none" return "none"
} }
@@ -608,7 +753,7 @@ fun setAppsListMode(prefs: SharedPreferences, mode: String) {
} }
} }
else { else {
prefs.edit { putString("app-list", mode) } prefs.edit { putString("app_list", mode) }
} }
Log.d("App List", "Changed to $mode") Log.d("App List", "Changed to $mode")
} }

View File

@@ -99,4 +99,22 @@
<string name="btn_copy_log">Скопировать лог</string> <string name="btn_copy_log">Скопировать лог</string>
<string name="log_copied">Лог скопирован</string> <string name="log_copied">Лог скопирован</string>
<string name="download_error">Произошла ошибка скачивания, пожалуйста, сообщите о ней разработчикам</string> <string name="download_error">Произошла ошибка скачивания, пожалуйста, сообщите о ней разработчикам</string>
<string name="ipset_repo_url">URL репозитория ipset</string>
<string name="title_selection">Подбор стратегии</string>
<string name="btn_repository_url_ipsets">URL репозитория ipset</string>
<string name="hint_enter_repository_url_ipsets">Введите URL репозитория ipset</string>
<string name="strategy_status_waiting">ожидание...</string>
<string name="strategy_status_testing">проверка...</string>
<string name="strategy_status_tested">проверка завершена</string>
<string name="strategy_applied">Стратегия применена</string>
<string name="begin_selection">Начать подбор</string>
<string name="begin_selection_snack">Начат подбор стратегии. Не закрывайте эту вкладку</string>
<string name="change_probe_timeout">Изменить таймаут пробы</string>
<string name="probe_timeout">Таймаут пробы</string>
<string name="hint_enter_probe_timeout">Введите таймаут пробы</string>
<string name="strategy_selection_info_title">Информация</string>
<string name="strategy_selection_info_msg">"В этом разделе настроек приложения представлен перебор стратегий\n Подбор проходит среди скачанных стратегий, поэтому заранее скачайте из репозитория или добавьте из файловой системы интересующие вас стратегии для сравнения. \n Перед началом так же выберете один или несколько листов доменов на вкладке \"Листы\", затем нажмите на \"Начать подбор\". Не используйте для перебора списки с большим количеством доменов."</string>
<string name="selection_no_hosts_title">Нет активных листов</string>
<string name="selection_no_hosts_message">Не обнаружено активных списков хостов, включите один или несколько, иначе подбор не сработает</string>
<string name="selection_available_domains">Доступные домены</string>
</resources> </resources>

View File

@@ -103,4 +103,23 @@
<string name="btn_copy_log">Copy log</string> <string name="btn_copy_log">Copy log</string>
<string name="log_copied">Log copied</string> <string name="log_copied">Log copied</string>
<string name="download_error">Occurred a file download error, please report to developers</string> <string name="download_error">Occurred a file download error, please report to developers</string>
<string name="ipset_repo_url">Ipset repository URL</string>
<string name="title_ipset" translatable="false">Ipset</string>
<string name="title_selection">Strategy selection</string>
<string name="btn_repository_url_ipsets">Ipsets repository url</string>
<string name="hint_enter_repository_url_ipsets">Enter ipsets repository URL</string>
<string name="strategy_status_waiting">waiting...</string>
<string name="strategy_status_testing">testing...</string>
<string name="strategy_status_tested">testing ended</string>
<string name="strategy_applied">Strategy applied</string>
<string name="begin_selection">Begin selection</string>
<string name="begin_selection_snack">Strategy selection has begun. Please keep this tab open.</string>
<string name="change_probe_timeout">Change probe timeout</string>
<string name="probe_timeout">Probe timeout</string>
<string name="hint_enter_probe_timeout">Enter probe timeout</string>
<string name="strategy_selection_info_title">Tip</string>
<string name="strategy_selection_info_msg">This section of the application settings allows you to iterate through strategies.\n The selection is based on downloaded strategies, so download the strategies you\'re interested in from the repository or add them from the file system for comparison.\n Before starting, select one or more domain lists in the \"Lists\" tab, then click \"Start selection\". Avoid using lists with a large number of domains.</string>
<string name="selection_no_hosts_title">No active hosts</string>
<string name="selection_no_hosts_message">No active host lists found, please enable one or more, otherwise the selection will not work</string>
<string name="selection_available_domains">Available domains</string>
</resources> </resources>

View File

@@ -1,3 +1,4 @@
1. Обработка ошибок связанных с репозиторием(приложение теперь не будет вылетать) 1) Добавлена поддержка ipset
2. Исправление ошибок интерфейса 2) JNI **полностью переписан** на Rust
3. Прочие мелкие исправления 3) Добавлен подбор стратегии
4) Исправлены баги

View File

@@ -1,5 +1,5 @@
[versions] [versions]
agp = "8.12.2" agp = "8.13.0"
kotlin = "2.2.10" kotlin = "2.2.10"
coreKtx = "1.17.0" coreKtx = "1.17.0"
junit = "4.13.2" junit = "4.13.2"

View File

@@ -5,7 +5,7 @@ project(byedpi_native)
file(GLOB BYE_DPI_SRC byedpi/*.c) file(GLOB BYE_DPI_SRC byedpi/*.c)
list(REMOVE_ITEM BYE_DPI_SRC ${CMAKE_CURRENT_SOURCE_DIR}/byedpi/win_service.c) list(REMOVE_ITEM BYE_DPI_SRC ${CMAKE_CURRENT_SOURCE_DIR}/byedpi/win_service.c)
add_library(byedpi SHARED ${BYE_DPI_SRC} native-lib.c utils.c) add_library(byedpi STATIC ${BYE_DPI_SRC})
target_include_directories(byedpi PRIVATE byedpi) target_include_directories(byedpi PRIVATE byedpi)
target_compile_options(byedpi PRIVATE -std=c99 -O2 -Wall -Wno-unused -Wextra -Wno-unused-parameter -pedantic) target_compile_options(byedpi PRIVATE -std=c99 -O2 -Wall -Wno-unused -Wextra -Wno-unused-parameter -pedantic)

349
rust/Cargo.lock generated Normal file
View File

@@ -0,0 +1,349 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "aho-corasick"
version = "1.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916"
dependencies = [
"memchr",
]
[[package]]
name = "android_log-sys"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "84521a3cf562bc62942e294181d9eef17eb38ceb8c68677bc49f144e4c3d4f8d"
[[package]]
name = "android_logger"
version = "0.15.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dbb4e440d04be07da1f1bf44fb4495ebd58669372fe0cffa6e48595ac5bd88a3"
dependencies = [
"android_log-sys",
"env_filter",
"log",
]
[[package]]
name = "byedpi"
version = "0.1.0"
dependencies = [
"android_logger",
"cmake",
"jni",
"libc",
"log",
"once_cell",
]
[[package]]
name = "bytes"
version = "1.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a"
[[package]]
name = "cc"
version = "1.2.40"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e1d05d92f4b1fd76aad469d46cdd858ca761576082cd37df81416691e50199fb"
dependencies = [
"find-msvc-tools",
"shlex",
]
[[package]]
name = "cesu8"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c"
[[package]]
name = "cfg-if"
version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9"
[[package]]
name = "cmake"
version = "0.1.54"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e7caa3f9de89ddbe2c607f4101924c5abec803763ae9534e4f4d7d8f84aa81f0"
dependencies = [
"cc",
]
[[package]]
name = "combine"
version = "4.6.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd"
dependencies = [
"bytes",
"memchr",
]
[[package]]
name = "env_filter"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "186e05a59d4c50738528153b83b0b0194d3a29507dfec16eccd4b342903397d0"
dependencies = [
"log",
"regex",
]
[[package]]
name = "find-msvc-tools"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0399f9d26e5191ce32c498bebd31e7a3ceabc2745f0ac54af3f335126c3f24b3"
[[package]]
name = "jni"
version = "0.21.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97"
dependencies = [
"cesu8",
"cfg-if",
"combine",
"jni-sys",
"log",
"thiserror",
"walkdir",
"windows-sys 0.45.0",
]
[[package]]
name = "jni-sys"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130"
[[package]]
name = "libc"
version = "0.2.176"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "58f929b4d672ea937a23a1ab494143d968337a5f47e56d0815df1e0890ddf174"
[[package]]
name = "log"
version = "0.4.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432"
[[package]]
name = "memchr"
version = "2.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273"
[[package]]
name = "once_cell"
version = "1.21.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
[[package]]
name = "proc-macro2"
version = "1.0.101"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.41"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ce25767e7b499d1b604768e7cde645d14cc8584231ea6b295e9c9eb22c02e1d1"
dependencies = [
"proc-macro2",
]
[[package]]
name = "regex"
version = "1.11.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b5288124840bee7b386bc413c487869b360b2b4ec421ea56425128692f2a82c"
dependencies = [
"aho-corasick",
"memchr",
"regex-automata",
"regex-syntax",
]
[[package]]
name = "regex-automata"
version = "0.4.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "833eb9ce86d40ef33cb1306d8accf7bc8ec2bfea4355cbdebb3df68b40925cad"
dependencies = [
"aho-corasick",
"memchr",
"regex-syntax",
]
[[package]]
name = "regex-syntax"
version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "caf4aa5b0f434c91fe5c7f1ecb6a5ece2130b02ad2a590589dda5146df959001"
[[package]]
name = "same-file"
version = "1.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
dependencies = [
"winapi-util",
]
[[package]]
name = "shlex"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
[[package]]
name = "syn"
version = "2.0.106"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "thiserror"
version = "1.0.69"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "1.0.69"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "unicode-ident"
version = "1.0.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f63a545481291138910575129486daeaf8ac54aee4387fe7906919f7830c7d9d"
[[package]]
name = "walkdir"
version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b"
dependencies = [
"same-file",
"winapi-util",
]
[[package]]
name = "winapi-util"
version = "0.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
dependencies = [
"windows-sys 0.61.1",
]
[[package]]
name = "windows-link"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "45e46c0661abb7180e7b9c281db115305d49ca1709ab8242adf09666d2173c65"
[[package]]
name = "windows-sys"
version = "0.45.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0"
dependencies = [
"windows-targets",
]
[[package]]
name = "windows-sys"
version = "0.61.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6f109e41dd4a3c848907eb83d5a42ea98b3769495597450cf6d153507b166f0f"
dependencies = [
"windows-link",
]
[[package]]
name = "windows-targets"
version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071"
dependencies = [
"windows_aarch64_gnullvm",
"windows_aarch64_msvc",
"windows_i686_gnu",
"windows_i686_msvc",
"windows_x86_64_gnu",
"windows_x86_64_gnullvm",
"windows_x86_64_msvc",
]
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8"
[[package]]
name = "windows_aarch64_msvc"
version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43"
[[package]]
name = "windows_i686_gnu"
version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f"
[[package]]
name = "windows_i686_msvc"
version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060"
[[package]]
name = "windows_x86_64_gnu"
version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3"
[[package]]
name = "windows_x86_64_msvc"
version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0"

17
rust/Cargo.toml Normal file
View File

@@ -0,0 +1,17 @@
[package]
name = "byedpi"
version = "0.1.0"
edition = "2024"
[dependencies]
jni = "0.21.1"
libc = "0.2.0"
android_logger = "0.15.1"
log = "0.4"
once_cell = "1.21.3"
[lib]
crate-type = ["cdylib"]
[build-dependencies]
cmake = "0.1.49"

8
rust/build.rs Normal file
View File

@@ -0,0 +1,8 @@
use cmake::Config;
fn main() {
let dst = Config::new(".").build_target("byedpi").build();
let lib_path = dst.join("build");
println!("cargo:rustc-link-search=native={}", lib_path.display());
println!("cargo:rustc-link-lib=static=byedpi");
}

1
rust/byedpi Submodule

Submodule rust/byedpi added at 7efde1b129

74
rust/src/lib.rs Normal file
View File

@@ -0,0 +1,74 @@
use android_logger::Config;
use jni::JNIEnv;
use jni::objects::{JClass, JObjectArray, JString};
use jni::sys::jint;
use libc::{SHUT_RDWR, shutdown};
use log::{LevelFilter, info};
use std::ffi::CString;
use std::os::raw::c_char;
use std::sync::atomic::{AtomicBool, Ordering};
static PROXY_RUNNING: AtomicBool = AtomicBool::new(false);
#[link(name = "byedpi", kind = "static")]
unsafe extern "C" {
static mut server_fd: i32;
static mut optind: i32;
static mut optreset: i32;
fn main(argc: libc::c_int, argv: *const *const c_char) -> libc::c_int;
fn clear_params();
}
fn init_logger() {
android_logger::init_once(Config::default().with_max_level(LevelFilter::Info));
}
#[unsafe(no_mangle)]
pub unsafe extern "system" fn Java_com_cherret_zaprett_byedpi_NativeBridge_jniStartProxy(
mut env: JNIEnv,
_class: JClass,
args: JObjectArray,
) -> jint {
init_logger();
if PROXY_RUNNING.swap(true, Ordering::SeqCst) {
info!("proxy already running");
return -1;
}
let argc = env.get_array_length(&args).unwrap_or(0) as usize;
let mut cstrings: Vec<CString> = Vec::with_capacity(argc);
for i in 0..argc {
let jstr: JString = env
.get_object_array_element(&args, i as i32)
.unwrap()
.into();
let rust_str: String = env.get_string(&jstr).unwrap().into();
cstrings.push(CString::new(rust_str).unwrap());
}
let mut argv: Vec<*const c_char> = cstrings.iter().map(|s| s.as_ptr()).collect();
argv.push(std::ptr::null());
info!("starting proxy");
unsafe {
optind = 1;
optreset = 1;
clear_params();
}
PROXY_RUNNING.store(true, Ordering::SeqCst);
let ret = unsafe { main(argc as i32, argv.as_ptr()) };
PROXY_RUNNING.store(false, Ordering::SeqCst);
ret as jint
}
#[unsafe(no_mangle)]
pub unsafe extern "system" fn Java_com_cherret_zaprett_byedpi_NativeBridge_jniStopProxy(
_env: JNIEnv,
_class: JClass,
) -> jint {
init_logger();
if !PROXY_RUNNING.load(Ordering::SeqCst) {
info!("failed to stop proxy");
return -1;
}
info!("stopping proxy");
let ret = unsafe { shutdown(server_fd, SHUT_RDWR) };
ret as jint
}

View File

@@ -1,6 +1,6 @@
{ {
"version": "2.8", "version": "2.10",
"versionCode": 20, "versionCode": 22,
"downloadUrl": "https://github.com/CherretGit/zaprett-app/releases/download/2.8.0/app-release.apk", "downloadUrl": "https://github.com/CherretGit/zaprett-app/releases/download/2.10.0/app-release.apk",
"changelogUrl": "https://raw.githubusercontent.com/CherretGit/zaprett-app/refs/heads/main/changelog.md" "changelogUrl": "https://raw.githubusercontent.com/CherretGit/zaprett-app/refs/heads/main/changelog.md"
} }