28 Commits
1_8_0 ... 2_0

Author SHA1 Message Date
CherretGit
26dfba1dda ciadpi version 2025-06-30 01:01:14 +07:00
CherretGit
e59a4ad75c Merge remote-tracking branch 'origin/main' 2025-06-30 00:48:41 +07:00
CherretGit
df38f29b55 remove unused imports 2025-06-30 00:47:10 +07:00
CherretGit
693f93e297 non root and etc 2025-06-30 00:42:04 +07:00
egor-white
7dff7ac36a update readmme 2025-06-21 20:25:01 +00:00
CherretGit
45a6e10fb5 Update README.md 2025-06-20 01:31:01 +07:00
CherretGit
353bb169f3 Merge remote-tracking branch 'origin/main' 2025-06-03 21:07:46 +07:00
CherretGit
aba5d650d8 Refactor: migrate to MVVM architecture 2025-06-03 21:05:18 +07:00
CherretGit
6b0e86a5cf Update README.md 2025-05-10 21:08:35 +07:00
CherretGit
67e14b32be Add files via upload 2025-05-10 21:06:31 +07:00
CherretGit
f1f3963137 Update update.json 2025-05-10 21:04:32 +07:00
CherretGit
c54ee5c22d Update changelog.md 2025-05-10 21:04:27 +07:00
Cherret
87ff9c5164 Merge remote-tracking branch 'origin/main' 2025-05-10 20:46:12 +07:00
Cherret
c61937a4b0 Update version 2025-05-10 20:45:56 +07:00
Cherret
edf54851ae Add strategies 2025-05-10 20:43:35 +07:00
CherretGit
b766deafa5 Update update.json 2025-05-05 23:40:11 +07:00
CherretGit
7974066105 Update changelog.md 2025-05-05 23:40:06 +07:00
Cherret
dc096ae3ed Merge remote-tracking branch 'origin/main' 2025-05-05 23:21:34 +07:00
Cherret
23172b1cd8 enable shrink 2025-05-05 23:21:24 +07:00
CherretGit
a9ed6a5e67 Update README.md 2025-05-05 23:01:45 +07:00
CherretGit
00b5334273 Update README.md 2025-05-05 14:25:50 +07:00
CherretGit
f11071fd4d Update README.md 2025-05-05 14:10:43 +07:00
CherretGit
82e28c1620 Update changelog.md 2025-05-01 21:12:47 +07:00
CherretGit
9ea170a2b2 Update update.json 2025-05-01 21:12:42 +07:00
Cherret
2d82aec1eb Merge remote-tracking branch 'origin/main' 2025-05-01 21:04:08 +07:00
Cherret
4ee72db907 disable shrink 2025-05-01 21:03:45 +07:00
CherretGit
96a1455ebc Update README.md 2025-05-01 20:43:42 +07:00
CherretGit
e5ed91e90d Add files via upload 2025-05-01 20:38:14 +07:00
50 changed files with 3218 additions and 880 deletions

6
.idea/appInsightsSettings.xml generated Normal file
View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="AppInsightsSettings">
<option name="selectedTabId" value="Android Vitals" />
</component>
</project>

View File

@@ -1,5 +1,40 @@
<component name="ProjectCodeStyleConfiguration">
<code_scheme name="Project" version="173">
<JavaCodeStyleSettings>
<option name="IMPORT_LAYOUT_TABLE">
<value>
<package name="" withSubpackages="true" static="false" module="true" />
<package name="android" withSubpackages="true" static="true" />
<package name="androidx" withSubpackages="true" static="true" />
<package name="com" withSubpackages="true" static="true" />
<package name="junit" withSubpackages="true" static="true" />
<package name="net" withSubpackages="true" static="true" />
<package name="org" withSubpackages="true" static="true" />
<package name="java" withSubpackages="true" static="true" />
<package name="javax" withSubpackages="true" static="true" />
<package name="" withSubpackages="true" static="true" />
<emptyLine />
<package name="android" withSubpackages="true" static="false" />
<emptyLine />
<package name="androidx" withSubpackages="true" static="false" />
<emptyLine />
<package name="com" withSubpackages="true" static="false" />
<emptyLine />
<package name="junit" withSubpackages="true" static="false" />
<emptyLine />
<package name="net" withSubpackages="true" static="false" />
<emptyLine />
<package name="org" withSubpackages="true" static="false" />
<emptyLine />
<package name="java" withSubpackages="true" static="false" />
<emptyLine />
<package name="javax" withSubpackages="true" static="false" />
<emptyLine />
<package name="" withSubpackages="true" static="false" />
<emptyLine />
</value>
</option>
</JavaCodeStyleSettings>
<JetCodeStyleSettings>
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
</JetCodeStyleSettings>

6
.idea/vcs.xml generated
View File

@@ -2,5 +2,11 @@
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
<mapping directory="$PROJECT_DIR$/app/src/main/cpp/byedpi" vcs="Git" />
<mapping directory="$PROJECT_DIR$/app/src/main/jni/hev-socks5-tunnel" vcs="Git" />
<mapping directory="$PROJECT_DIR$/app/src/main/jni/hev-socks5-tunnel/src/core" vcs="Git" />
<mapping directory="$PROJECT_DIR$/app/src/main/jni/hev-socks5-tunnel/third-part/hev-task-system" vcs="Git" />
<mapping directory="$PROJECT_DIR$/app/src/main/jni/hev-socks5-tunnel/third-part/lwip" vcs="Git" />
<mapping directory="$PROJECT_DIR$/app/src/main/jni/hev-socks5-tunnel/third-part/yaml" vcs="Git" />
</component>
</project>

View File

@@ -2,12 +2,17 @@
## О приложении
Приложение разработано для работы с модулем [zaprett](https://github.com/egor-white/zaprett)
### [Официальный Telegram-канал приложения](https://t.me/zaprett_module)
### Данное приложение является ремейком [приложения](https://github.com/egor-white/zaprett-app) от [egor-white](https://github.com/egor-white)
## ВНИМАНИЕ приложение работает только с root правами
На данный момент приложение умеет:
* Включать, выключать и перезапускать модуль
* Работа с листами (добавление, включение и выключение)
* Запускать, останавливать и перезапускать модуль
* Работа с листами (добавление, включение и выключение, загрузка из репозитория)
* Работа с стратегиями (добавление, выбор, загрузка из репозитория)
* Авто обновление приложения
## [Репозиторий с хостами и стратегиями](https://github.com/CherretGit/zaprett-hosts-repo)
#### Данное приложение является ремейком [приложения](https://github.com/egor-white/zaprett-app) от [egor-white](https://github.com/egor-white), разработка которого была прекращена в пользу этого приложения
## Скриншоты:
<img src="images/1.png" width="300"><img src="images/2.png" width="300"><img src="images/3.png" width="300">
<img src="images/1.png" width="300"><img src="images/2.png" width="300"><img src="images/3.png" width="300"><img src="images/4.png" width="300"><img src="images/5.png" width="300">
<img src="images/6.png" width="300">

View File

@@ -4,6 +4,7 @@ plugins {
alias(libs.plugins.kotlin.compose)
id("com.google.gms.google-services")
id("com.google.firebase.crashlytics")
kotlin("plugin.serialization") version "2.1.20"
}
android {
@@ -14,12 +15,20 @@ android {
applicationId = "com.cherret.zaprett"
minSdk = 30
targetSdk = 35
versionCode = 9
versionName = "1.8"
versionCode = 12
versionName = "2.0"
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 {
release {
isMinifyEnabled = true
@@ -43,6 +52,29 @@ android {
}
}
tasks.register<Exec>("runNdkBuild") {
group = "build"
val ndkDir = android.ndkDirectory
executable = if (System.getProperty("os.name").startsWith("Windows", ignoreCase = true)) {
"$ndkDir\\ndk-build.cmd"
} else {
"$ndkDir/ndk-build"
}
setArgs(listOf(
"NDK_PROJECT_PATH=build/intermediates/ndkBuild",
"NDK_LIBS_OUT=src/main/jniLibs",
"APP_BUILD_SCRIPT=src/main/jni/Android.mk",
"NDK_APPLICATION_MK=src/main/jni/Application.mk"
))
println("Command: $commandLine")
}
tasks.preBuild {
dependsOn("runNdkBuild")
}
dependencies {
implementation("androidx.compose.material3:material3:1.3.1")
implementation("androidx.compose.material3:material3-window-size-class:1.3.1")
@@ -51,10 +83,11 @@ dependencies {
implementation("androidx.compose.material:material-icons-extended:1.7.8")
implementation ("com.github.topjohnwu.libsu:core:6.0.0")
implementation("com.squareup.okhttp3:okhttp:5.0.0-alpha.14")
implementation("com.squareup.moshi:moshi-kotlin:1.15.2")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.8.0")
implementation(platform("com.google.firebase:firebase-bom:33.12.0"))
implementation("com.google.firebase:firebase-analytics")
implementation("com.google.firebase:firebase-crashlytics")
implementation("androidx.fragment:fragment-compose:1.8.8")
implementation(libs.androidx.core.ktx)
implementation(libs.androidx.lifecycle.runtime.ktx)
implementation(libs.androidx.activity.compose)

View File

@@ -18,4 +18,10 @@
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
#-renamesourcefileattribute SourceFile
-keep class com.cherret.zaprett.TProxyService {
native long[] TProxyGetStats();
native void TProxyStartService(java.lang.String, int);
native void TProxyStopService();
}

View File

@@ -10,6 +10,9 @@
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES"/>
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" />
<application
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
@@ -51,6 +54,15 @@
<action android:name="android.service.quicksettings.action.QS_TILE" />
</intent-filter>
</service>
<service
android:name=".ByeDpiVpnService"
android:exported="true"
android:permission="android.permission.BIND_VPN_SERVICE"
android:foregroundServiceType="specialUse">
<intent-filter>
<action android:name="android.net.VpnService" />
</intent-filter>
</service>
</application>
</manifest>

View File

@@ -0,0 +1,14 @@
cmake_minimum_required(VERSION 3.22.1)
project(byedpi_native)
file(GLOB BYE_DPI_SRC byedpi/*.c)
list(REMOVE_ITEM BYE_DPI_SRC ${CMAKE_CURRENT_SOURCE_DIR}/byedpi/win_service.c)
add_library(byedpi SHARED ${BYE_DPI_SRC} native-lib.c utils.c)
target_include_directories(byedpi PRIVATE byedpi)
target_compile_options(byedpi PRIVATE -std=c99 -O2 -Wall -Wno-unused -Wextra -Wno-unused-parameter -pedantic)
target_compile_definitions(byedpi PRIVATE ANDROID_APP)
target_link_libraries(byedpi PRIVATE android log)

Submodule app/src/main/cpp/byedpi added at ed76be1dad

83
app/src/main/cpp/main.h Normal file
View File

@@ -0,0 +1,83 @@
#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

@@ -0,0 +1,100 @@
#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_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_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_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;
}

594
app/src/main/cpp/utils.c Normal file
View File

@@ -0,0 +1,594 @@
#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;
}

5
app/src/main/cpp/utils.h Normal file
View File

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

View File

@@ -0,0 +1,194 @@
package com.cherret.zaprett
import android.annotation.SuppressLint
import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.content.SharedPreferences
import android.net.VpnService
import android.os.ParcelFileDescriptor
import android.util.Log
import android.widget.Toast
import androidx.core.app.NotificationCompat
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import java.io.File
class ByeDpiVpnService : VpnService() {
private var vpnInterface: ParcelFileDescriptor? = null
private lateinit var sharedPreferences: SharedPreferences
companion object {
private const val CHANNEL_ID = "zaprett_vpn_channel"
private const val NOTIFICATION_ID = 1
var status: ServiceStatus = ServiceStatus.Disconnected
}
@SuppressLint("ForegroundServiceType")
override fun onCreate() {
super.onCreate()
sharedPreferences = getSharedPreferences("settings", Context.MODE_PRIVATE)
createNotificationChannel()
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
super.onStartCommand(intent, flags, startId)
return when (intent?.action) {
"START_VPN" -> {
setupProxy()
START_STICKY
}
"STOP_VPN" -> {
stopProxy()
stopSelf()
START_NOT_STICKY
}
else -> {
START_NOT_STICKY
}
}
}
override fun onDestroy() {
super.onDestroy()
vpnInterface?.close()
}
private fun createNotificationChannel() {
val channel = NotificationChannel(
CHANNEL_ID,
getString(R.string.notification_zaprett_proxy),
NotificationManager.IMPORTANCE_LOW
).apply {
description = getString(R.string.notification_zaprett_description)
}
val manager = getSystemService(NotificationManager::class.java)
manager?.createNotificationChannel(channel)
}
private fun createNotification(): Notification {
val notificationIntent = Intent(this, MainActivity::class.java)
val pendingIntent = PendingIntent.getActivity(
this,
0,
notificationIntent,
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
)
return NotificationCompat.Builder(this, CHANNEL_ID)
.setContentTitle(getString(R.string.notification_zaprett_proxy))
.setContentText(getString(R.string.notification_zaprett_description))
.setSmallIcon(R.drawable.ic_launcher_monochrome)
.setContentIntent(pendingIntent)
.addAction(0, getString(R.string.btn_stop_service),
PendingIntent.getService(
this,
0,
Intent(this, ByeDpiVpnService::class.java).setAction("STOP_VPN"),
PendingIntent.FLAG_IMMUTABLE,
)
)
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
.setOngoing(true)
.build()
}
private fun setupProxy() {
if (getActiveStrategy(sharedPreferences).isNotEmpty()) {
startForeground(NOTIFICATION_ID, createNotification())
try {
startSocksProxy()
startByeDpi()
status = ServiceStatus.Connected
} catch (e: Exception) {
Log.e("proxy", "Failed to start")
status = ServiceStatus.Failed
}
}
else {
Toast.makeText(
this@ByeDpiVpnService,
getString(R.string.toast_no_strategy_selected),
Toast.LENGTH_SHORT
).show()
}
}
private fun startSocksProxy() {
val dns = sharedPreferences.getString("dns", "8.8.8.8")?: "8.8.8.8"
val ipv6 = sharedPreferences.getBoolean("ipv6", false)
val socksIp = sharedPreferences.getString("ip", "127.0.0.1")?: "127.0.0.1"
val socksPort = sharedPreferences.getString("port", "1080")?: "1080"
val builder = Builder()
builder.setSession(getString(R.string.notification_zaprett_proxy))
.setMtu(1500)
.addAddress("10.10.10.10", 32)
.addDnsServer(dns)
.addRoute("0.0.0.0", 0)
.setMetered(false)
.addDisallowedApplication(applicationContext.packageName)
if (ipv6) {
builder.addAddress("fd00::1", 128)
.addRoute("::", 0)
}
vpnInterface = builder.establish()
val tun2socksConfig = """
| misc:
| task-stack-size: 81920
| socks5:
| mtu: 8500
| address: $socksIp
| port: $socksPort
| udp: udp
""".trimMargin("| ")
val configPath = File.createTempFile("config", "tmp", cacheDir).apply {
writeText(tun2socksConfig)
deleteOnExit()
}
vpnInterface?.fd?.let { fd ->
TProxyService.TProxyStartService(configPath.absolutePath, fd)
}
}
private fun stopProxy() {
try {
vpnInterface?.close()
NativeBridge().stopProxy()
TProxyService.TProxyStopService()
status = ServiceStatus.Disconnected
} catch (e: Exception) {
Log.e("proxy", "error stop proxy")
}
}
private fun startByeDpi() {
val socksIp = sharedPreferences.getString("ip", "127.0.0.1")?: "127.0.0.1"
val socksPort = sharedPreferences.getString("port", "1080")?: "1080"
val listSet = sharedPreferences.getStringSet("lists", emptySet())?: emptySet()
CoroutineScope(Dispatchers.IO).launch {
val args = parseArgs(socksIp, socksPort, getActiveStrategy(sharedPreferences), listSet)
val result = NativeBridge().startProxy(args)
if (result < 0) {
println("Failed to start byedpi proxy")
} else {
println("Byedpi proxy started successfully")
}
}
}
fun parseArgs(ip: String, port: String, rawArgs: List<String>, listSet: Set<String>): Array<String> {
val regex = Regex("""--?\S+(?:=(?:[^"'\s]+|"[^"]*"|'[^']*'))?|[^\s]+""")
val parsedArgs = rawArgs
.flatMap { args -> regex.findAll(args).map { it.value } }
.toMutableList()
if (listSet.isNotEmpty()) {
for (path in listSet) {
parsedArgs.add("--hosts")
parsedArgs.add(path)
}
}
return arrayOf("ciadpi", "--ip", ip, "--port", port) + parsedArgs
}
}

View File

@@ -1,18 +1,25 @@
package com.cherret.zaprett
import android.Manifest
import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.os.Environment
import android.provider.Settings
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels
import androidx.annotation.StringRes
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Dashboard
import androidx.compose.material.icons.filled.Dns
import androidx.compose.material.icons.filled.Home
import androidx.compose.material.icons.filled.Settings
import androidx.compose.material3.AlertDialog
@@ -28,49 +35,97 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.core.content.ContextCompat
import androidx.core.content.edit
import androidx.navigation.NavGraph.Companion.findStartDestination
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.currentBackStackEntryAsState
import androidx.navigation.compose.rememberNavController
import com.cherret.zaprett.ui.screens.HomeScreen
import com.cherret.zaprett.ui.screens.HostsRepoScreen
import com.cherret.zaprett.ui.screens.HostsScreen
import com.cherret.zaprett.ui.screens.SettingsScreen
import androidx.navigation.navArgument
import com.cherret.zaprett.ui.screen.HomeScreen
import com.cherret.zaprett.ui.screen.RepoScreen
import com.cherret.zaprett.ui.screen.HostsScreen
import com.cherret.zaprett.ui.screen.SettingsScreen
import com.cherret.zaprett.ui.screen.StrategyScreen
import com.cherret.zaprett.ui.theme.ZaprettTheme
import com.cherret.zaprett.ui.viewmodel.HomeViewModel
import com.cherret.zaprett.ui.viewmodel.HostRepoViewModel
import com.cherret.zaprett.ui.viewmodel.StrategyRepoViewModel
import com.google.firebase.Firebase
import com.google.firebase.analytics.FirebaseAnalytics
import com.google.firebase.analytics.analytics
sealed class Screen(val route: String, @StringRes val nameResId: Int, val icon: androidx.compose.ui.graphics.vector.ImageVector) {
sealed class Screen(val route: String, @StringRes val nameResId: Int, val icon: ImageVector) {
object home : Screen("home", R.string.title_home, Icons.Default.Home)
object hosts : Screen("hosts", R.string.title_hosts, Icons.Default.Dashboard)
object strategies : Screen("strategies", R.string.title_strategies, Icons.Default.Dns)
object settings : Screen("settings", R.string.title_settings, Icons.Default.Settings)
}
val topLevelRoutes = listOf(Screen.home, Screen.hosts, Screen.settings)
val hideNavBar = listOf("hosts_repo")
val topLevelRoutes = listOf(Screen.home, Screen.hosts, Screen.strategies, Screen.settings)
val hideNavBar = listOf("repo?source={source}")
class MainActivity : ComponentActivity() {
private val viewModel: HomeViewModel by viewModels()
private lateinit var notificationPermissionLauncher: ActivityResultLauncher<String>
private lateinit var firebaseAnalytics: FirebaseAnalytics
private lateinit var vpnPermissionLauncher: ActivityResultLauncher<Intent>
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
vpnPermissionLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
if (result.resultCode == RESULT_OK) {
viewModel.startVpn()
viewModel.clearVpnPermissionRequest()
}
}
notificationPermissionLauncher = registerForActivityResult(ActivityResultContracts.RequestPermission()) { granted -> }
firebaseAnalytics = Firebase.analytics
enableEdgeToEdge()
setContent {
ZaprettTheme {
val sharedPreferences = remember { getSharedPreferences("settings", MODE_PRIVATE) }
var showPermissionDialog by remember { mutableStateOf(!Environment.isExternalStorageManager()) }
var showStoragePermissionDialog by remember { mutableStateOf(!Environment.isExternalStorageManager()) }
var showNotificationPermissionDialog by remember {
mutableStateOf(
Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU &&
ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED
)
}
var showWelcomeDialog by remember { mutableStateOf(sharedPreferences.getBoolean("welcome_dialog", true)) }
firebaseAnalytics.setAnalyticsCollectionEnabled(sharedPreferences.getBoolean("send_firebase_analytics", true))
BottomBar()
if (showPermissionDialog) {
PermissionDialog { showPermissionDialog = false }
if (showStoragePermissionDialog) {
PermissionDialog(
title = stringResource(R.string.error_no_storage_title),
message = stringResource(R.string.error_no_storage_message),
onConfirm = {
val intent = Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION)
val uri = Uri.fromParts("package", packageName, null)
intent.data = uri
startActivity(intent)
showStoragePermissionDialog = false
},
onDismiss = { showStoragePermissionDialog = false }
)
}
if (showNotificationPermissionDialog) {
PermissionDialog(
title = stringResource(R.string.notification_permission_title),
message = stringResource(R.string.notification_permission_message),
onConfirm = {
notificationPermissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS)
showNotificationPermissionDialog = false
},
onDismiss = { showNotificationPermissionDialog = false }
)
}
if (showWelcomeDialog) {
WelcomeDialog {
sharedPreferences.edit() { putBoolean("welcome_dialog", false) }
sharedPreferences.edit { putBoolean("welcome_dialog", false) }
showWelcomeDialog = false
}
}
@@ -117,10 +172,23 @@ class MainActivity : ComponentActivity() {
startDestination = Screen.home.route,
Modifier.padding(innerPadding)
) {
composable(Screen.home.route) { HomeScreen() }
composable(Screen.home.route) { HomeScreen(viewModel = viewModel, vpnPermissionLauncher) }
composable(Screen.hosts.route) { HostsScreen(navController) }
composable(Screen.strategies.route) { StrategyScreen(navController) }
composable(Screen.settings.route) { SettingsScreen() }
composable("hosts_repo") { HostsRepoScreen(navController) }
composable(route = "repo?source={source}",arguments = listOf(navArgument("source") {})) { backStackEntry ->
val source = backStackEntry.arguments?.getString("source")
when (source) {
"hosts" -> {
val viewModel: HostRepoViewModel = androidx.lifecycle.viewmodel.compose.viewModel()
RepoScreen(navController, viewModel)
}
"strategies" -> {
val viewModel: StrategyRepoViewModel = androidx.lifecycle.viewmodel.compose.viewModel()
RepoScreen(navController, viewModel)
}
}
}
}
}
}
@@ -140,25 +208,17 @@ class MainActivity : ComponentActivity() {
}
@Composable
fun PermissionDialog(onDismiss: () -> Unit) {
val context = LocalContext.current
fun PermissionDialog(title: String, message: String, onConfirm: () -> Unit, onDismiss: () -> Unit) {
AlertDialog(
title = { Text(text = stringResource(R.string.error_no_storage_title)) },
text = { Text(text = stringResource(R.string.error_no_storage_message)) },
title = { Text(title) },
text = { Text(message) },
onDismissRequest = onDismiss,
confirmButton = {
TextButton(
onClick = {
val intent = Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION)
val uri = Uri.fromParts("package", context.packageName, null)
intent.data = uri
context.startActivity(intent)
onDismiss()
}
) {
TextButton(onClick = onConfirm) {
Text(stringResource(R.string.btn_continue))
}
}
)
}
}

View File

@@ -1,5 +1,6 @@
package com.cherret.zaprett
import android.content.SharedPreferences
import android.os.Environment
import android.util.Log
import com.topjohnwu.superuser.Shell
@@ -8,6 +9,7 @@ import java.io.FileInputStream
import java.io.FileOutputStream
import java.io.IOException
import java.util.Properties
import androidx.core.content.edit
fun checkRoot(callback: (Boolean) -> Unit) {
Shell.cmd("ls /").submit { result ->
@@ -45,6 +47,18 @@ fun restartService(callback: (Boolean) -> Unit) {
}
}
fun getModuleVersion(callback: (String) -> Unit) {
Shell.cmd("zaprett module-ver").submit { result ->
if (result.out.isNotEmpty()) callback(result.out.first()) else "undefined"
}
}
fun getBinVersion(callback: (String) -> Unit) {
Shell.cmd("zaprett bin-ver").submit { result ->
if (result.out.isNotEmpty()) callback(result.out.first()) else "undefined"
}
}
fun getConfigFile(): File {
return File(Environment.getExternalStorageDirectory(), "zaprett/config")
}
@@ -109,7 +123,49 @@ fun getAllLists(): Array<String> {
return emptyArray()
}
fun getActiveLists(): Array<String> {
fun getAllNfqwsStrategies(): Array<String> {
val listsDir = File("${getZaprettPath()}/strategies/nfqws")
if (listsDir.exists() && listsDir.isDirectory) {
val onlyNames = listsDir.list() ?: return emptyArray()
return onlyNames.map { "$listsDir/$it" }.toTypedArray()
}
return emptyArray()
}
fun getAllByeDPIStrategies(): Array<String> {
val listsDir = File("${getZaprettPath()}/strategies/byedpi")
if (listsDir.exists() && listsDir.isDirectory) {
val onlyNames = listsDir.list() ?: return emptyArray()
return onlyNames.map { "$listsDir/$it" }.toTypedArray()
}
return emptyArray()
}
fun getActiveLists(sharedPreferences: SharedPreferences): Array<String> {
if (sharedPreferences.getBoolean("use_module", false)) {
val configFile = File("${getZaprettPath()}/config")
if (configFile.exists()) {
val props = Properties()
return try {
FileInputStream(configFile).use { input ->
props.load(input)
}
val activeLists = props.getProperty("activelists", "")
Log.d("Active lists", activeLists)
if (activeLists.isNotEmpty()) activeLists.split(",")
.toTypedArray() else emptyArray()
} catch (e: IOException) {
throw RuntimeException(e)
}
}
return emptyArray()
}
else {
return sharedPreferences.getStringSet("lists", emptySet())?.toTypedArray() ?: emptyArray()
}
}
fun getActiveNfqwsStrategies(): Array<String> {
val configFile = File("${getZaprettPath()}/config")
if (configFile.exists()) {
val props = Properties()
@@ -117,9 +173,9 @@ fun getActiveLists(): Array<String> {
FileInputStream(configFile).use { input ->
props.load(input)
}
val activeLists = props.getProperty("activelists", "")
Log.d("Active lists", activeLists)
if (activeLists.isNotEmpty()) activeLists.split(",").toTypedArray() else emptyArray()
val activeStrategies = props.getProperty("strategy", "")
Log.d("Active strategies", activeStrategies)
if (activeStrategies.isNotEmpty()) activeStrategies.split(",").toTypedArray() else emptyArray()
} catch (e: IOException) {
throw RuntimeException(e)
}
@@ -127,46 +183,149 @@ fun getActiveLists(): Array<String> {
return emptyArray()
}
fun enableList(path: String) {
val props = Properties()
val configFile = getConfigFile()
try {
if (configFile.exists()) {
FileInputStream(configFile).use { input ->
props.load(input)
fun getActiveByeDPIStrategies(sharedPreferences: SharedPreferences): Array<String> {
val path = sharedPreferences.getString("active_strategy", "")
if (!path.isNullOrBlank() && File(path).exists()) {
return arrayOf(path)
}
return emptyArray()
}
fun getActiveStrategy(sharedPreferences: SharedPreferences): List<String> {
val path = sharedPreferences.getString("active_strategy", "")
if (!path.isNullOrBlank() && File(path).exists()) {
return File(path).readLines()
}
return emptyList()
}
fun enableList(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("activelists", "")
.split(",")
.filter { it.isNotBlank() }
.toMutableList()
if (path !in activeLists) {
activeLists.add(path)
}
props.setProperty("activelists", activeLists.joinToString(","))
FileOutputStream(configFile).use { output ->
props.store(output, "Don't place '/' in end of directory! Example: /sdcard")
}
} catch (e: IOException) {
throw RuntimeException(e)
}
val activeLists = props.getProperty("activelists", "").split(",").toMutableList()
if (path !in activeLists) {
activeLists.add(path)
}
else {
val currentSet = sharedPreferences.getStringSet("lists", emptySet())?.toMutableSet() ?: mutableSetOf()
if (path !in currentSet) {
currentSet.add(path)
sharedPreferences.edit { putStringSet("lists", currentSet) }
}
props.setProperty("activelists", activeLists.joinToString(","))
FileOutputStream(configFile).use { output ->
props.store(output, "Don't place '/' in end of directory! Example: /sdcard")
}
} catch (e: IOException) {
throw RuntimeException(e)
}
}
fun disableList(path: String) {
val props = Properties()
val configFile = getConfigFile()
try {
if (configFile.exists()) {
FileInputStream(configFile).use { input ->
props.load(input)
fun enableStrategy(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 activeStrategies = props.getProperty("strategy", "")
.split(",")
.filter { it.isNotBlank() }
.toMutableList()
if (path !in activeStrategies) {
activeStrategies.add(path)
}
props.setProperty("strategy", activeStrategies.joinToString(","))
FileOutputStream(configFile).use { output ->
props.store(output, "Don't place '/' in end of directory! Example: /sdcard")
}
} catch (e: IOException) {
throw RuntimeException(e)
}
val activeLists = props.getProperty("activelists", "").split(",").toMutableList()
if (path in activeLists) {
activeLists.remove(path)
}
props.setProperty("activelists", 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 {
sharedPreferences.edit { putString("active_strategy", path) }
}
}
fun disableList(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("activelists", "")
.split(",")
.filter { it.isNotBlank() }
.toMutableList()
if (path in activeLists) {
activeLists.remove(path)
}
props.setProperty("activelists", 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("lists", emptySet())?.toMutableSet() ?: mutableSetOf()
if (path in currentSet) {
currentSet.remove(path)
sharedPreferences.edit { putStringSet("lists", currentSet) }
}
if (currentSet.isEmpty()) {
sharedPreferences.edit { remove("lists") }
}
}
}
fun disableStrategy(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 activeStrategies = props.getProperty("strategy", "")
.split(",")
.filter { it.isNotBlank() }
.toMutableList()
if (path in activeStrategies) {
activeStrategies.remove(path)
}
props.setProperty("strategy", activeStrategies.joinToString(","))
FileOutputStream(configFile).use { output ->
props.store(output, "Don't place '/' in end of directory! Example: /sdcard")
}
} catch (e: IOException) {
throw RuntimeException(e)
}
}
else {
sharedPreferences.edit { remove("active_strategy") }
}
}

View File

@@ -0,0 +1,21 @@
package com.cherret.zaprett
class NativeBridge {
companion object {
init {
System.loadLibrary("byedpi")
}
}
fun startProxy(args: Array<String>): Int {
jniCreateSocket(args)
return jniStartProxy()
}
fun stopProxy(): Int {
return jniStopProxy()
}
private external fun jniCreateSocket(args: Array<String>): Int
private external fun jniStartProxy(): Int
private external fun jniStopProxy(): Int
}

View File

@@ -31,11 +31,7 @@ class QSTileService: TileService() {
updateStatus()
}
override fun onTileRemoved() {
super.onTileRemoved()
}
fun updateStatus() {
private fun updateStatus() {
if (getSharedPreferences("settings", MODE_PRIVATE).getBoolean("use_module", false)) {
getStatus {
if (it) {

View File

@@ -6,13 +6,13 @@ import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.content.SharedPreferences
import android.net.Uri
import android.os.Build
import androidx.core.content.ContextCompat
import androidx.core.net.toUri
import com.squareup.moshi.Moshi
import com.squareup.moshi.Types
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import okhttp3.Call
import okhttp3.Callback
import okhttp3.OkHttpClient
@@ -23,12 +23,10 @@ import java.io.File
import java.security.MessageDigest
private val client = OkHttpClient()
private val json = Json { ignoreUnknownKeys = true }
fun getHostList(callback: (List<HostsInfo>?) -> Unit) {
val request = Request.Builder().url("https://raw.githubusercontent.com/CherretGit/zaprett-hosts-repo/refs/heads/main/hosts.json").build()
val moshi = Moshi.Builder().add(KotlinJsonAdapterFactory()).build()
val type = Types.newParameterizedType(List::class.java, HostsInfo::class.java)
val jsonAdapter = moshi.adapter<List<HostsInfo>>(type)
fun getHostList(sharedPreferences: SharedPreferences, callback: (List<RepoItemInfo>?) -> Unit) {
val request = Request.Builder().url(sharedPreferences.getString("hosts_repo_url","https://raw.githubusercontent.com/CherretGit/zaprett-hosts-repo/refs/heads/main/hosts.json")?: "https://raw.githubusercontent.com/CherretGit/zaprett-hosts-repo/refs/heads/main/hosts.json").build()
client.newCall(request).enqueue(object : Callback {
override fun onFailure(call: Call, e: IOException) {
e.printStackTrace()
@@ -37,14 +35,31 @@ fun getHostList(callback: (List<HostsInfo>?) -> Unit) {
override fun onResponse(call: Call, response: Response) {
response.use {
if (!response.isSuccessful) {
throw IOException()
callback(null)
}
val jsonString = response.body.string()
val updateInfo = jsonAdapter.fromJson(jsonString)
if (updateInfo != null) {
callback(updateInfo)
val hostsInfo = json.decodeFromString<List<RepoItemInfo>>(jsonString)
callback(hostsInfo)
}
}
})
}
fun getStrategiesList(sharedPreferences: SharedPreferences, callback: (List<RepoItemInfo>?) -> Unit) {
val request = Request.Builder().url(sharedPreferences.getString("strategies_repo_url", "https://raw.githubusercontent.com/CherretGit/zaprett-hosts-repo/refs/heads/main/strategies.json")?: "https://raw.githubusercontent.com/CherretGit/zaprett-hosts-repo/refs/heads/main/strategies.json").build()
client.newCall(request).enqueue(object : Callback {
override fun onFailure(call: Call, e: IOException) {
e.printStackTrace()
callback(null)
}
override fun onResponse(call: Call, response: Response) {
response.use {
if (!response.isSuccessful) {
callback(null)
}
val jsonString = response.body.string()
val strategiesInfo = json.decodeFromString<List<RepoItemInfo>>(jsonString)
callback(strategiesInfo)
}
}
})
@@ -93,9 +108,12 @@ fun getFileSha256(file: File): String {
return digest.digest().joinToString("") { "%02x".format(it) }
}
data class HostsInfo(
@Serializable
data class RepoItemInfo(
val name: String,
val author: String,
val description: String,
val type: String? = null,
val hash: String,
val url: String
)

View File

@@ -0,0 +1,7 @@
package com.cherret.zaprett
enum class ServiceStatus {
Disconnected,
Connected,
Failed,
}

View File

@@ -0,0 +1,11 @@
package com.cherret.zaprett
object TProxyService {
init {
System.loadLibrary("hev-socks5-tunnel")
}
external fun TProxyStartService(config: String, fd: Int)
external fun TProxyGetStats(): LongArray
external fun TProxyStopService()
}

View File

@@ -13,8 +13,8 @@ import android.provider.Settings
import androidx.core.content.ContextCompat
import androidx.core.content.FileProvider
import androidx.core.net.toUri
import com.squareup.moshi.Moshi
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import okhttp3.Call
import okhttp3.Callback
import okhttp3.OkHttpClient
@@ -24,11 +24,10 @@ import okio.IOException
import java.io.File
private val client = OkHttpClient()
private val json = Json { ignoreUnknownKeys = true }
fun getUpdate(callback: (UpdateInfo?) -> Unit) {
val request = Request.Builder().url("https://raw.githubusercontent.com/CherretGit/zaprett-app/refs/heads/main/update.json").build()
val moshi = Moshi.Builder().add(KotlinJsonAdapterFactory()).build()
val jsonAdapter = moshi.adapter(UpdateInfo::class.java)
client.newCall(request).enqueue(object : Callback {
override fun onFailure(call: Call, e: IOException) {
e.printStackTrace()
@@ -41,7 +40,7 @@ fun getUpdate(callback: (UpdateInfo?) -> Unit) {
callback(null)
}
val jsonString = response.body.string()
val updateInfo = jsonAdapter.fromJson(jsonString)
val updateInfo = json.decodeFromString<UpdateInfo>(jsonString)
updateInfo?.versionCode?.let { versionCode ->
if (versionCode > BuildConfig.VERSION_CODE)
callback(updateInfo)
@@ -74,15 +73,9 @@ fun getChangelog(changelogUrl: String, callback: (String?) -> Unit) {
fun download(context: Context, url: String): Long {
val fileName = url.substringAfterLast("/")
val downloadManager = context.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
if (Environment.isExternalStorageManager()) {
val file = File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS), fileName)
if (file.exists()) {
file.delete()
}
}
val request = DownloadManager.Request(url.toUri()).apply {
setTitle(fileName)
setDescription("Загрузка $fileName")
setDescription(fileName)
setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED)
setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, fileName)
setAllowedNetworkTypes(DownloadManager.Request.NETWORK_WIFI or DownloadManager.Request.NETWORK_MOBILE)
@@ -90,7 +83,6 @@ fun download(context: Context, url: String): Long {
return downloadManager.enqueue(request)
}
fun installApk(context: Context, uri: Uri) {
val file = File(uri.path!!)
val apkUri = FileProvider.getUriForFile(context, "${context.packageName}.provider", file)
@@ -131,6 +123,7 @@ fun registerDownloadListener(context: Context, downloadId: Long, onDownloaded: (
}
}
@Serializable
data class UpdateInfo(
val version: String?,
val versionCode: Int?,

View File

@@ -1,26 +1,37 @@
package com.cherret.zaprett.ui.screens
package com.cherret.zaprett.ui.screen
import android.content.Context
import android.content.Context.MODE_PRIVATE
import android.content.Intent
import android.content.SharedPreferences
import android.net.VpnService
import androidx.activity.result.ActivityResultLauncher
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.expandVertically
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.shrinkVertically
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Build
import androidx.compose.material.icons.filled.Dangerous
import androidx.compose.material.icons.filled.Extension
import androidx.compose.material.icons.filled.PlayArrow
import androidx.compose.material.icons.filled.RestartAlt
import androidx.compose.material.icons.filled.Stop
import androidx.compose.material.icons.filled.WavingHand
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.ElevatedCard
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.FilledTonalButton
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
@@ -32,13 +43,13 @@ import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.graphics.vector.rememberVectorPainter
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.Font
@@ -47,48 +58,43 @@ 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 com.cherret.zaprett.BuildConfig
import com.cherret.zaprett.R
import com.cherret.zaprett.download
import com.cherret.zaprett.getChangelog
import com.cherret.zaprett.getStatus
import com.cherret.zaprett.getUpdate
import com.cherret.zaprett.installApk
import com.cherret.zaprett.registerDownloadListener
import com.cherret.zaprett.restartService
import com.cherret.zaprett.startService
import com.cherret.zaprett.stopService
import com.cherret.zaprett.ui.viewmodel.HomeViewModel
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun HomeScreen() {
fun HomeScreen(viewModel: HomeViewModel = viewModel(), vpnLauncher: ActivityResultLauncher<Intent>) {
val context = LocalContext.current
val sharedPreferences = remember { context.getSharedPreferences("settings", MODE_PRIVATE) }
val cardText = remember { mutableIntStateOf(R.string.status_not_availible) }
val changeLog = remember { mutableStateOf<String?>(null) }
val newVersion = remember { mutableStateOf<String?>(null) }
val updateAvailable = remember { mutableStateOf(false) }
val downloadUrl = remember { mutableStateOf<String?>(null) }
var showUpdateDialog by remember { mutableStateOf(false) }
val sharedPreferences: SharedPreferences = remember { context.getSharedPreferences("settings", Context.MODE_PRIVATE) }
val requestVpnPermission by viewModel.requestVpnPermission.collectAsState()
val scope = rememberCoroutineScope()
val snackbarHostState = remember { SnackbarHostState() }
val cardText = viewModel.cardText
val changeLog = viewModel.changeLog
val newVersion = viewModel.newVersion
val updateAvailable = viewModel.updateAvailable
val showUpdateDialog = viewModel.showUpdateDialog.value
val moduleVer = viewModel.moduleVer;
val nfqwsVer = viewModel.nfqwsVer;
val byedpiVer = viewModel.byedpiVer;
val serviceMode = viewModel.serviceMode
LaunchedEffect(Unit) {
if (sharedPreferences.getBoolean("auto_update", true)) {
getUpdate() {
if (it != null) {
downloadUrl.value = it.downloadUrl.toString()
getChangelog(it.changelogUrl.toString()) { log -> changeLog.value = log }
newVersion.value = it.version?.toString()
updateAvailable.value = true
}
}
}
if (sharedPreferences.getBoolean("use_module", false) && sharedPreferences.getBoolean("update_on_boot", false)) {
getStatus { isEnabled ->
cardText.intValue = if (isEnabled) R.string.status_enabled else R.string.status_disabled
viewModel.checkForUpdate()
viewModel.checkServiceStatus()
viewModel.checkModuleInfo()
}
LaunchedEffect(requestVpnPermission) {
if (requestVpnPermission) {
val intent = VpnService.prepare(context)
if (intent != null) {
vpnLauncher.launch(intent)
} else {
viewModel.startVpn()
viewModel.clearVpnPermissionRequest()
}
}
}
@@ -109,27 +115,34 @@ fun HomeScreen() {
snackbarHost = { SnackbarHost(snackbarHostState) },
content = { paddingValues ->
Column(modifier = Modifier.padding(paddingValues)) {
ServiceStatusCard(context, cardText, snackbarHostState, scope)
UpdateCard(updateAvailable) { showUpdateDialog = true }
ServiceStatusCard(viewModel, cardText, snackbarHostState, scope)
UpdateCard(updateAvailable) { viewModel.showUpdateDialog() }
if (showUpdateDialog) {
UpdateDialog(context, downloadUrl.value.orEmpty(), changeLog.value.orEmpty(), newVersion) { showUpdateDialog = false }
UpdateDialog(viewModel, changeLog.value.orEmpty(), newVersion) { viewModel.dismissUpdateDialog() }
}
ServiceControlButtons(context, snackbarHostState, scope)
ServiceControlButtons(
viewModel,
sharedPreferences,
snackbarHostState,
scope
)
ModuleInfoCard(moduleVer, nfqwsVer, byedpiVer, serviceMode)
}
}
)
}
@Composable
private fun ServiceStatusCard(context: Context, cardText: MutableState<Int>, snackbarHostState: SnackbarHostState, scope: CoroutineScope) {
private fun ServiceStatusCard(viewModel: HomeViewModel, cardText: MutableState<Int>, snackbarHostState: SnackbarHostState, scope: CoroutineScope) {
ElevatedCard(
elevation = CardDefaults.cardElevation(6.dp),
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.primary),
modifier = Modifier
.fillMaxWidth()
.padding(start = 10.dp, top = 25.dp, end = 10.dp)
.size(width = 240.dp, height = 150.dp),
onClick = { onCardClick(context, cardText, snackbarHostState, scope) }
.width(240.dp)
.height(150.dp),
onClick = { viewModel.onCardClick() }
) {
Text(
text = stringResource(cardText.value),
@@ -155,7 +168,8 @@ private fun UpdateCard(updateAvailable: MutableState<Boolean>, onClick: () -> Un
modifier = Modifier
.fillMaxWidth()
.padding(start = 10.dp, top = 10.dp, end = 10.dp)
.size(width = 140.dp, height = 70.dp),
.width(140.dp)
.height(70.dp),
onClick = onClick
) {
Text(
@@ -171,9 +185,9 @@ private fun UpdateCard(updateAvailable: MutableState<Boolean>, onClick: () -> Un
}
@Composable
private fun ServiceControlButtons(context: Context, snackbarHostState: SnackbarHostState, scope: CoroutineScope) {
private fun ServiceControlButtons(viewModel: HomeViewModel, sharedPreferences: SharedPreferences, snackbarHostState: SnackbarHostState, scope: CoroutineScope) {
FilledTonalButton(
onClick = { onBtnStartService(context, snackbarHostState, scope) },
onClick = { viewModel.onBtnStartService(snackbarHostState, scope) },
modifier = Modifier
.padding(horizontal = 5.dp, vertical = 8.dp)
.fillMaxWidth()
@@ -186,7 +200,7 @@ private fun ServiceControlButtons(context: Context, snackbarHostState: SnackbarH
Text(stringResource(R.string.btn_start_service))
}
FilledTonalButton(
onClick = { onBtnStopService(context, snackbarHostState, scope) },
onClick = { viewModel.onBtnStopService(snackbarHostState, scope) },
modifier = Modifier
.padding(horizontal = 5.dp, vertical = 8.dp)
.fillMaxWidth()
@@ -198,90 +212,84 @@ private fun ServiceControlButtons(context: Context, snackbarHostState: SnackbarH
)
Text(stringResource(R.string.btn_stop_service))
}
FilledTonalButton(
onClick = { onBtnRestart(context, snackbarHostState, scope) },
modifier = Modifier
.padding(horizontal = 5.dp, vertical = 8.dp)
.fillMaxWidth()
) {
Icon(
imageVector = Icons.Default.RestartAlt,
contentDescription = stringResource(R.string.btn_restart_service),
modifier = Modifier.size(20.dp)
)
Text(stringResource(R.string.btn_restart_service))
}
}
fun onCardClick(context: Context, cardText: MutableState<Int>, snackbarHostState: SnackbarHostState, scope: CoroutineScope) {
val sharedPreferences = context.getSharedPreferences("settings", MODE_PRIVATE)
if (sharedPreferences.getBoolean("use_module", false)) {
getStatus { isEnabled ->
cardText.value = if (isEnabled) R.string.status_enabled else R.string.status_disabled
}
} else {
scope.launch {
snackbarHostState.showSnackbar(context.getString(R.string.snack_module_disabled))
}
}
}
fun onBtnStartService(context: Context, snackbarHostState: SnackbarHostState, scope: CoroutineScope) {
val sharedPreferences = context.getSharedPreferences("settings", MODE_PRIVATE)
if (sharedPreferences.getBoolean("use_module", false)) {
getStatus { isEnabled ->
scope.launch {
snackbarHostState.showSnackbar(
context.getString(
if (isEnabled) R.string.snack_already_started else R.string.snack_starting_service
)
)
}
if (!isEnabled) startService {}
}
} else {
scope.launch {
snackbarHostState.showSnackbar(context.getString(R.string.snack_module_disabled))
}
}
}
fun onBtnStopService(context: Context, snackbarHostState: SnackbarHostState, scope: CoroutineScope) {
val sharedPreferences = context.getSharedPreferences("settings", MODE_PRIVATE)
if (sharedPreferences.getBoolean("use_module", false)) {
getStatus { isEnabled ->
scope.launch {
snackbarHostState.showSnackbar(
context.getString(
if (isEnabled) R.string.snack_stopping_service else R.string.snack_no_service
)
)
}
if (isEnabled) stopService {}
}
} else {
scope.launch {
snackbarHostState.showSnackbar(context.getString(R.string.snack_module_disabled))
}
}
}
fun onBtnRestart(context: Context, snackbarHostState: SnackbarHostState, scope: CoroutineScope) {
val sharedPreferences = context.getSharedPreferences("settings", MODE_PRIVATE)
if (sharedPreferences.getBoolean("use_module", false)) {
restartService {}
scope.launch {
snackbarHostState.showSnackbar(context.getString(R.string.snack_reload))
}
} else {
scope.launch {
snackbarHostState.showSnackbar(context.getString(R.string.snack_module_disabled))
FilledTonalButton(
onClick = { viewModel.onBtnRestart(snackbarHostState, scope) },
modifier = Modifier
.padding(horizontal = 5.dp, vertical = 8.dp)
.fillMaxWidth()
) {
Icon(
imageVector = Icons.Default.RestartAlt,
contentDescription = stringResource(R.string.btn_restart_service),
modifier = Modifier.size(20.dp)
)
Text(stringResource(R.string.btn_restart_service))
}
}
}
@Composable
fun UpdateDialog(context: Context, downloadUrl: String, changeLog: String, newVersion: MutableState<String?>, onDismiss: () -> Unit) {
private fun ModuleInfoCard(
moduleVer: MutableState<String>,
nfqwsVer: MutableState<String>,
byedpiVer: MutableState<String>,
serviceMode: MutableState<Int>
) {
ElevatedCard(
elevation = CardDefaults.cardElevation(6.dp),
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceContainer),
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 10.dp, vertical = 10.dp)
) {
ModuleInfoItem(Icons.Default.Build, stringResource(R.string.service_mode), stringResource(serviceMode.value))
HorizontalDivider()
ModuleInfoItem(Icons.Default.Extension, stringResource(R.string.module_version), moduleVer.value)
HorizontalDivider()
ModuleInfoItem(Icons.Default.Dangerous, stringResource(R.string.nfqws_version), nfqwsVer.value)
HorizontalDivider()
ModuleInfoItem(Icons.Default.WavingHand, stringResource(R.string.ciadpi_version), byedpiVer.value)
}
}
@Composable
private fun ModuleInfoItem(
icon: ImageVector, header : String, value : String
) {
Row (
modifier = Modifier
.fillMaxWidth()
.padding(8.dp)
) {
Icon(
painter = rememberVectorPainter(icon),
modifier = Modifier
.size(50.dp)
.padding(16.dp),
contentDescription = "icon"
)
Column (
modifier = Modifier
.padding(horizontal = 16.dp)
){
Text(
text = header,
modifier = Modifier
.fillMaxWidth(),
fontSize = 18.sp,
textAlign = TextAlign.Justify,
)
Text(
text = value
)
}
}
}
@Composable
fun UpdateDialog(viewModel: HomeViewModel, changeLog: String, newVersion: MutableState<String?>, onDismiss: () -> Unit) {
AlertDialog(
title = { Text(text = stringResource(R.string.update_available)) },
text = {
@@ -293,10 +301,7 @@ fun UpdateDialog(context: Context, downloadUrl: String, changeLog: String, newVe
confirmButton = {
TextButton(onClick = {
onDismiss()
val downloadId = download(context, downloadUrl)
registerDownloadListener(context, downloadId) { uri ->
installApk(context, uri)
}
viewModel.onUpdateConfirm()
}) {
Text(stringResource(R.string.btn_update))
}
@@ -307,4 +312,4 @@ fun UpdateDialog(context: Context, downloadUrl: String, changeLog: String, newVe
}
}
)
}
}

View File

@@ -1,13 +1,16 @@
package com.cherret.zaprett.ui.screens
package com.cherret.zaprett.ui.screen
import android.content.Context
import android.net.Uri
import android.os.Environment
import android.provider.OpenableColumns
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.layout.*
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.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
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
@@ -15,9 +18,30 @@ import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.Delete
import androidx.compose.material.icons.filled.Download
import androidx.compose.material.icons.filled.UploadFile
import androidx.compose.material3.*
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.ElevatedCard
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.FilledTonalButton
import androidx.compose.material3.FloatingActionButton
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Switch
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.pulltorefresh.PullToRefreshBox
import androidx.compose.runtime.*
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
@@ -29,33 +53,28 @@ import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.Dp
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.*
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import java.io.File
import java.io.FileOutputStream
import java.io.IOException
import com.cherret.zaprett.ui.viewmodel.HostsViewModel
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun HostsScreen(navController: NavController) {
fun HostsScreen(navController: NavController, viewModel: HostsViewModel = viewModel()) {
val context = LocalContext.current
var allLists by remember { mutableStateOf(getAllLists()) }
var activeLists by remember { mutableStateOf(getActiveLists()) }
val scope = rememberCoroutineScope()
val snackbarHostState = remember { SnackbarHostState() }
var isRefreshing by remember { mutableStateOf(false) }
val checked = remember {
mutableStateMapOf<String, Boolean>().apply {
allLists.forEach { list -> this[list] = activeLists.contains(list) }
}
}
val allLists = viewModel.allItems
val checked = viewModel.checked
val isRefreshing = viewModel.isRefreshing
val filePickerLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.OpenDocument()
) { uri ->
uri?.let { copySelectedFile(context, it, snackbarHostState, scope) }
uri?.let { viewModel.copySelectedFile(context, "/lists", it) }
}
LaunchedEffect(Unit) {
viewModel.refresh()
}
Scaffold(
@@ -75,14 +94,7 @@ fun HostsScreen(navController: NavController) {
PullToRefreshBox(
isRefreshing = isRefreshing,
onRefresh = {
isRefreshing = true
allLists = getAllLists()
activeLists = getActiveLists()
checked.clear()
allLists.forEach { list ->
checked[list] = activeLists.contains(list)
}
isRefreshing = false
viewModel.refresh()
},
modifier = Modifier.fillMaxSize()
) {
@@ -91,14 +103,14 @@ fun HostsScreen(navController: NavController) {
modifier = Modifier.fillMaxSize()
) {
when {
allLists.isEmpty() != false -> {
allLists.isEmpty() -> {
item {
Box(
modifier = Modifier.fillParentMaxSize(),
contentAlignment = Alignment.Center
) {
Text(
stringResource(R.string.btn_no_hosts),
stringResource(R.string.empty_list),
textAlign = TextAlign.Center
)
}
@@ -106,24 +118,14 @@ fun HostsScreen(navController: NavController) {
}
else -> {
items(allLists) { item ->
HostItem(
HostItem (
item = item,
isChecked = checked[item] == true,
onCheckedChange = { isChecked ->
checked[item] = isChecked
if (isChecked) enableList(item) else disableList(item)
showRestartSnackbar(context, snackbarHostState, scope)
viewModel.onCheckedChange(item, isChecked, snackbarHostState, scope)
},
onDeleteClick = {
if (deleteHost(item)) {
allLists = getAllLists()
activeLists = getActiveLists()
checked.clear()
allLists.forEach { list ->
checked[list] = activeLists.contains(list)
}
}
showRestartSnackbar(context, snackbarHostState, scope)
viewModel.deleteItem(item, snackbarHostState, scope)
}
)
}
@@ -194,7 +196,7 @@ private fun FloatingMenu(navController: NavController, launcher: ActivityResultL
text = { Text(stringResource(R.string.btn_download_host)) },
onClick = {
expanded = false
navController.navigate("hosts_repo") { launchSingleTop = true }
navController.navigate("repo?source=hosts") { launchSingleTop = true }
},
leadingIcon = {
Icon(Icons.Default.Download, contentDescription = stringResource(R.string.btn_download_host))
@@ -215,50 +217,4 @@ private fun FloatingMenu(navController: NavController, launcher: ActivityResultL
private fun addHost(launcher: ActivityResultLauncher<Array<String>>) {
launcher.launch(arrayOf("text/plain"))
}
private fun copySelectedFile(context: Context, uri: Uri, snackbarHostState: SnackbarHostState, scope: CoroutineScope) {
if (!Environment.isExternalStorageManager()) return
val contentResolver = context.contentResolver
val fileName = contentResolver.query(uri, null, null, null, null)?.use { cursor ->
val nameIndex = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME)
if (cursor.moveToFirst() && nameIndex != -1) cursor.getString(nameIndex) else "copied_file"
} ?: "copied_file"
val outputFile = File(getZaprettPath() + "/lists", fileName)
try {
contentResolver.openInputStream(uri)?.use { inputStream ->
FileOutputStream(outputFile).use { outputStream ->
inputStream.copyTo(outputStream)
}
}
showRestartSnackbar(context, snackbarHostState, scope)
} catch (e: IOException) {
e.printStackTrace()
}
}
private fun deleteHost(item: String): Boolean {
val hostFile = File(item)
return if (hostFile.exists()) {
hostFile.delete()
true
} else {
false
}
}
private fun showRestartSnackbar(context: Context, snackbarHostState: SnackbarHostState, scope: CoroutineScope) {
scope.launch {
val result = snackbarHostState.showSnackbar(
context.getString(R.string.pls_restart_snack),
actionLabel = context.getString(R.string.btn_restart_service)
)
if (result == SnackbarResult.ActionPerformed) {
restartService {}
snackbarHostState.showSnackbar(context.getString(R.string.snack_reload))
}
}
}
}

View File

@@ -0,0 +1,205 @@
package com.cherret.zaprett.ui.screen
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
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.automirrored.filled.ArrowBack
import androidx.compose.material.icons.filled.InstallMobile
import androidx.compose.material.icons.filled.Update
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.ElevatedCard
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.FilledTonalButton
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
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.material3.pulltorefresh.PullToRefreshBox
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember
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.dp
import androidx.compose.ui.unit.sp
import androidx.navigation.NavController
import com.cherret.zaprett.R
import com.cherret.zaprett.ui.viewmodel.BaseRepoViewModel
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun RepoScreen(navController: NavController, viewModel: BaseRepoViewModel) {
val context = LocalContext.current
val hostLists = viewModel.hostLists.value
val isUpdate = viewModel.isUpdate
val isInstalling = viewModel.isInstalling
val isUpdateInstalling = viewModel.isUpdateInstalling
val isRefreshing = viewModel.isRefreshing.value
val snackbarHostState = remember { SnackbarHostState() }
LaunchedEffect(Unit) {
viewModel.refresh()
}
Scaffold(
topBar = {
TopAppBar(
title = {
Text(
text = stringResource(R.string.title_repo),
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)
)
}
},
windowInsets = WindowInsets(0)
)
},
content = { paddingValues ->
PullToRefreshBox(
isRefreshing = isRefreshing,
onRefresh = { viewModel.refresh() },
modifier = Modifier.fillMaxSize()
) {
LazyColumn(
contentPadding = paddingValues,
modifier = Modifier.fillMaxSize()
) {
if (hostLists.isEmpty()) {
item {
Box(
modifier = Modifier.fillParentMaxSize(),
contentAlignment = Alignment.Center
) {
Text(
stringResource(R.string.empty_list),
textAlign = TextAlign.Center
)
}
}
} else {
items(hostLists) { item ->
val isInstalled = viewModel.isItemInstalled(item)
val installing = isInstalling[item.name] == true
val updating = isUpdateInstalling[item.name] == true
ElevatedCard(
elevation = CardDefaults.cardElevation(defaultElevation = 6.dp),
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceContainer),
modifier = Modifier
.fillMaxWidth()
.padding(start = 10.dp, top = 25.dp, end = 10.dp)
) {
Column(Modifier.fillMaxWidth()) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
) {
Text(text = item.name, modifier = Modifier.weight(1f))
}
Row(
modifier = Modifier
.fillMaxWidth()
.padding(start = 16.dp)
) {
Text(
text = stringResource(R.string.title_author, item.author),
modifier = Modifier.weight(1f)
)
}
Row(
modifier = Modifier
.fillMaxWidth()
.padding(start = 16.dp)
) {
Text(
text = item.description,
modifier = Modifier.weight(1f)
)
}
HorizontalDivider(thickness = Dp.Hairline)
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.End
) {
if (isUpdate[item.name] == true && isInstalled) {
FilledTonalButton(
onClick = { viewModel.update(item) },
enabled = !updating,
modifier = Modifier.padding(horizontal = 5.dp)
) {
Icon(
imageVector = Icons.Default.Update,
contentDescription = stringResource(R.string.btn_remove_host),
modifier = Modifier.size(20.dp)
)
Text(
if (updating) stringResource(R.string.btn_updating_host)
else stringResource(R.string.btn_update_host)
)
}
}
FilledTonalButton(
onClick = { viewModel.install(item) },
enabled = !installing && !isInstalled,
modifier = Modifier.padding(horizontal = 5.dp)
) {
Icon(
imageVector = Icons.Default.InstallMobile,
contentDescription = stringResource(R.string.btn_remove_host),
modifier = Modifier.size(20.dp)
)
Text(
when {
installing -> stringResource(R.string.btn_installing_host)
isInstalled -> stringResource(R.string.btn_installed_host)
else -> stringResource(R.string.btn_install_host)
}
)
}
}
}
}
}
}
}
}
},
snackbarHost = { SnackbarHost(hostState = snackbarHostState) }
)
}

View File

@@ -0,0 +1,443 @@
package com.cherret.zaprett.ui.screen
import android.content.Context
import android.content.Intent
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowForward
import androidx.compose.material.icons.filled.MoreVert
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
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.unit.dp
import androidx.compose.ui.unit.sp
import com.cherret.zaprett.BuildConfig
import com.cherret.zaprett.ByeDpiVpnService
import com.cherret.zaprett.R
import com.cherret.zaprett.ServiceStatus
import com.cherret.zaprett.checkModuleInstallation
import com.cherret.zaprett.checkRoot
import com.cherret.zaprett.getStartOnBoot
import com.cherret.zaprett.setStartOnBoot
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun SettingsScreen() {
val context = LocalContext.current
val sharedPreferences = remember { context.getSharedPreferences("settings", Context.MODE_PRIVATE) }
val editor = remember { sharedPreferences.edit() }
val useModule = remember { mutableStateOf(sharedPreferences.getBoolean("use_module", false)) }
val updateOnBoot = remember { mutableStateOf(sharedPreferences.getBoolean("update_on_boot", false)) }
val autoRestart = remember { mutableStateOf(getStartOnBoot()) }
val autoUpdate = remember { mutableStateOf(sharedPreferences.getBoolean("auto_update", true)) }
val sendFirebaseAnalytics = remember { mutableStateOf(sharedPreferences.getBoolean("send_firebase_analytics", true)) }
val ipv6 = remember { mutableStateOf(sharedPreferences.getBoolean("ipv6",false)) }
val openNoRootDialog = remember { mutableStateOf(false) }
val openNoModuleDialog = remember { mutableStateOf(false) }
val showAboutDialog = remember { mutableStateOf(false) }
val showHostsRepoUrlDialog = remember { mutableStateOf(false) }
val showStrategyRepoUrlDialog = remember { mutableStateOf(false) }
val showIPDialog = remember { mutableStateOf(false) }
val showPortDialog = remember { mutableStateOf(false) }
val showDNSDialog = remember { mutableStateOf(false) }
val textDialogValue = remember { mutableStateOf("") }
val settingsList = listOf(
Setting.Section(stringResource(R.string.general_section)),
Setting.Toggle(
title = stringResource(R.string.btn_use_root),
checked = useModule.value,
onToggle = { isChecked ->
useModule(
context = context,
checked = isChecked,
updateOnBoot = updateOnBoot,
openNoRootDialog = openNoRootDialog,
openNoModuleDialog = openNoModuleDialog
) { success ->
if (success) useModule.value = isChecked
}
}
),
Setting.Toggle(
title = stringResource(R.string.btn_update_on_boot),
checked = updateOnBoot.value,
onToggle = {
updateOnBoot.value = it
editor.putBoolean("update_on_boot", it).apply()
}
),
Setting.Toggle(
title = stringResource(R.string.btn_autorestart),
checked = autoRestart.value,
onToggle = {
if (handleAutoRestart(context, it)) autoRestart.value = it
}
),
Setting.Toggle(
title = stringResource(R.string.btn_autoupdate),
checked = autoUpdate.value,
onToggle = {
autoUpdate.value = it
editor.putBoolean("auto_update", it).apply()
}
),
Setting.Toggle(
title = stringResource(R.string.btn_send_firebase_analytics),
checked = sendFirebaseAnalytics.value,
onToggle = {
sendFirebaseAnalytics.value = it
editor.putBoolean("send_firebase_analytics", it).apply()
}
),
Setting.Action(
title = stringResource(R.string.btn_repository_url_lists),
onClick = {
textDialogValue.value = sharedPreferences.getString("hosts_repo_url", "https://raw.githubusercontent.com/CherretGit/zaprett-hosts-repo/refs/heads/main/hosts.json") ?: "https://raw.githubusercontent.com/CherretGit/zaprett-hosts-repo/refs/heads/main/hosts.json"
showHostsRepoUrlDialog.value = true
}
),
Setting.Action(
title = stringResource(R.string.btn_repository_url_strategies),
onClick = {
textDialogValue.value = sharedPreferences.getString("strategy_repo_url", "https://raw.githubusercontent.com/CherretGit/zaprett-hosts-repo/refs/heads/main/strategies.json") ?: "https://raw.githubusercontent.com/CherretGit/zaprett-hosts-repo/refs/heads/main/strategies.json"
showStrategyRepoUrlDialog.value = true
}
),
Setting.Section(stringResource(R.string.byedpi_section)),
Setting.Toggle(
title = stringResource(R.string.btn_ipv6),
checked = ipv6.value,
onToggle = {
ipv6.value = it
editor.putBoolean("ipv6", it).apply()
}
),
Setting.Action(
title = stringResource(R.string.btn_ip),
onClick = {
textDialogValue.value = sharedPreferences.getString("ip", "127.0.0.1") ?: "127.0.0.1"
showIPDialog.value = true
}
),
Setting.Action(
title = stringResource(R.string.btn_port),
onClick = {
textDialogValue.value = sharedPreferences.getString("port", "1080") ?: "1080"
showPortDialog.value = true
}
),
Setting.Action(
title = stringResource(R.string.btn_dns),
onClick = {
textDialogValue.value = sharedPreferences.getString("dns", "8.8.8.8") ?: "8.8.8.8"
showDNSDialog.value = true
}
)
)
if (openNoRootDialog.value) {
InfoDialog(
title = stringResource(R.string.error_root_title),
message = stringResource(R.string.error_root_message),
onDismiss = { openNoRootDialog.value = false }
)
}
if (openNoModuleDialog.value) {
InfoDialog(
title = stringResource(R.string.error_no_module_title),
message = stringResource(R.string.error_no_module_message),
onDismiss = { openNoModuleDialog.value = false }
)
}
if (showAboutDialog.value) {
AboutDialog(onDismiss = { showAboutDialog.value = false })
}
if (showHostsRepoUrlDialog.value) {
TextDialog(stringResource(R.string.btn_repository_url_lists), stringResource(R.string.hint_enter_repository_url_lists), textDialogValue.value, onConfirm = {
editor.putString("hosts_repo_url", it).apply()
}, onDismiss = { showHostsRepoUrlDialog.value = false })
}
if (showStrategyRepoUrlDialog.value) {
TextDialog(stringResource(R.string.btn_repository_url_strategies), stringResource(R.string.hint_enter_repository_url_strategies), textDialogValue.value, onConfirm = {
editor.putString("strategy_repo_url", it).apply()
}, onDismiss = { showStrategyRepoUrlDialog.value = false })
}
if (showIPDialog.value) {
TextDialog(stringResource(R.string.btn_ip), stringResource(R.string.hint_ip), textDialogValue.value, onConfirm = {
editor.putString("ip", it).apply()
}, onDismiss = { showIPDialog.value = false })
}
if (showPortDialog.value) {
TextDialog(stringResource(R.string.btn_port), stringResource(R.string.hint_port), textDialogValue.value, onConfirm = {
editor.putString("port", it).apply()
}, onDismiss = { showPortDialog.value = false })
}
if (showDNSDialog.value) {
TextDialog(stringResource(R.string.btn_dns), stringResource(R.string.hint_dns), textDialogValue.value, onConfirm = {
editor.putString("dns", it).apply()
}, onDismiss = { showDNSDialog.value = false })
}
Scaffold(
topBar = {
TopAppBar(
title = {
Text(
text = stringResource(R.string.title_settings),
fontSize = 40.sp,
fontFamily = FontFamily(Font(R.font.unbounded, FontWeight.Normal))
)
},
actions = {
var expanded by remember { mutableStateOf(false) }
IconButton(onClick = { expanded = !expanded }) {
Icon(Icons.Default.MoreVert, contentDescription = null)
}
DropdownMenu(
expanded = expanded,
onDismissRequest = { expanded = false }
) {
DropdownMenuItem(
text = { Text(stringResource(R.string.about_title)) },
onClick = {
expanded = false
showAboutDialog.value = true
}
)
}
},
windowInsets = WindowInsets(0)
)
},
content = { paddingValues ->
LazyColumn(
modifier = Modifier
.padding(paddingValues)
.fillMaxSize(),
contentPadding = PaddingValues(horizontal = 10.dp, vertical = 25.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
items(settingsList) { setting ->
when (setting) {
is Setting.Toggle -> {
SettingsItem(
title = setting.title,
onToggle = setting.onToggle,
checked = setting.checked,
onCheckedChange = setting.onToggle
)
}
is Setting.Action -> {
SettingsTextItem(
title = setting.title,
setting.onClick
)
}
is Setting.Section -> {
SettingsSection(setting.title)
}
}
}
}
}
)
}
@Composable
private fun SettingsItem(title: String, checked: Boolean, onToggle: (Boolean) -> Unit, onCheckedChange: (Boolean) -> Unit) {
ElevatedCard(
modifier = Modifier
.fillMaxWidth()
.height(80.dp)
.clickable { onToggle(!checked) },
elevation = CardDefaults.cardElevation(defaultElevation = 4.dp),
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceVariant)
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
) {
Text(
text = title,
modifier = Modifier.weight(1f)
)
Switch(
checked = checked,
onCheckedChange = onCheckedChange
)
}
}
}
@Composable
private fun SettingsTextItem(title: String, onClick: () -> Unit) {
ElevatedCard(
modifier = Modifier
.fillMaxWidth()
.height(80.dp)
.clickable { onClick() },
elevation = CardDefaults.cardElevation(defaultElevation = 4.dp),
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceVariant)
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.fillMaxSize()
.padding(16.dp)
) {
Text(
text = title,
modifier = Modifier.weight(1f)
)
Icon(imageVector = Icons.AutoMirrored.Default.ArrowForward, contentDescription = "test")
}
}
}
@Composable
private fun SettingsSection(title: String) {
Text(
text = title,
fontSize = 20.sp,
fontWeight = FontWeight.Bold,
modifier = Modifier.padding(vertical = 8.dp, horizontal = 4.dp)
)
}
private fun useModule(context: Context, checked: Boolean, updateOnBoot: MutableState<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()
updateOnBoot.value = true
callback(true)
} else {
openNoModuleDialog.value = true
}
}
} else {
openNoRootDialog.value = true
}
}
} else {
editor.putBoolean("use_module", false)
.putBoolean("update_on_boot", false)
.apply()
updateOnBoot.value = false
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
private fun InfoDialog(title: String, message: String, onDismiss: () -> Unit) {
AlertDialog(
title = { Text(text = title) },
text = { Text(text = message) },
onDismissRequest = onDismiss,
confirmButton = {
TextButton(onClick = onDismiss) {
Text(stringResource(R.string.btn_continue))
}
}
)
}
@Composable
private fun TextDialog(title: String, message: String, initialText: String, onConfirm: (String) -> Unit, onDismiss: () -> Unit) {
var inputText by remember { mutableStateOf(initialText) }
AlertDialog(
title = { Text(text = title) },
text = {
TextField(
value = inputText,
onValueChange = { inputText = it },
placeholder = { Text(message) },
singleLine = true,
modifier = Modifier.fillMaxWidth()
)},
onDismissRequest = onDismiss,
confirmButton = {
TextButton(onClick = {
if (inputText.isNotEmpty()) {
onConfirm(inputText)
onDismiss()
}
else {
onDismiss()
}
}
) {
Text(stringResource(R.string.btn_continue))
}
},
dismissButton = {
TextButton(
onClick = { onDismiss() }
) {
Text(stringResource(R.string.btn_dismiss))
}
}
)
}
@Composable
private fun AboutDialog(onDismiss: () -> Unit) {
AlertDialog(
title = { Text(text = stringResource(R.string.about_title)) },
icon = {Icon(painterResource(R.drawable.ic_launcher_monochrome), contentDescription = stringResource(R.string.app_name), modifier = Modifier
.size(64.dp))},
text = { Text(text = stringResource(R.string.about_text, BuildConfig.VERSION_NAME)) },
onDismissRequest = onDismiss,
confirmButton = { }
)
}
sealed class Setting {
data class Toggle(val title: String, val checked: Boolean, val onToggle: (Boolean) -> Unit) : Setting()
data class Action(val title: String, val onClick: () -> Unit) : Setting()
data class Section(val title: String) : Setting()
}

View File

@@ -0,0 +1,226 @@
package com.cherret.zaprett.ui.screen
import android.content.Context
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
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.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
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.Delete
import androidx.compose.material.icons.filled.Download
import androidx.compose.material.icons.filled.UploadFile
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.ElevatedCard
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.FilledTonalButton
import androidx.compose.material3.FloatingActionButton
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Switch
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.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.viewmodel.StrategyViewModel
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun StrategyScreen(navController: NavController, viewModel: StrategyViewModel = viewModel()) {
val context = LocalContext.current
val sharedPreferences = 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 { viewModel.copySelectedFile(
context,
if (sharedPreferences.getBoolean("use_module", false)) "/strategies/nfqws" else "/strategies/byedpi",
it
) }
}
LaunchedEffect(Unit) {
viewModel.refresh()
}
Scaffold(
topBar = {
TopAppBar(
title = {
Text(
text = stringResource(R.string.title_strategies),
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,
modifier = Modifier.fillMaxSize()
) {
when {
allLists.isEmpty() -> {
item {
Box(
modifier = Modifier.fillParentMaxSize(),
contentAlignment = Alignment.Center
) {
Text(
stringResource(R.string.empty_list),
textAlign = TextAlign.Center
)
}
}
}
else -> {
items(allLists) { item ->
StrategyItem(
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 StrategyItem(item: String, isChecked: Boolean, onCheckedChange: (Boolean) -> Unit, onDeleteClick: () -> Unit) {
ElevatedCard(
elevation = CardDefaults.cardElevation(defaultElevation = 6.dp),
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceContainer),
modifier = Modifier
.fillMaxWidth()
.padding(start = 10.dp, top = 25.dp, end = 10.dp)
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
) {
Text(text = item, modifier = Modifier.weight(1f))
Switch(checked = isChecked, onCheckedChange = onCheckedChange)
}
HorizontalDivider(thickness = Dp.Hairline)
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.End
) {
FilledTonalButton(
onClick = onDeleteClick,
modifier = Modifier.padding(horizontal = 5.dp)
) {
Icon(
imageVector = Icons.Default.Delete,
contentDescription = stringResource(R.string.btn_remove_host),
modifier = Modifier.size(20.dp)
)
Text(stringResource(R.string.btn_remove_host))
}
}
}
}
@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=strategies") { 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
addStrategy(launcher)
},
leadingIcon = {
Icon(Icons.Default.UploadFile, contentDescription = stringResource(R.string.btn_add_host))
}
)
}
}
private fun addStrategy(launcher: ActivityResultLauncher<Array<String>>) {
launcher.launch(arrayOf("text/plain"))
}

View File

@@ -1,268 +0,0 @@
package com.cherret.zaprett.ui.screens
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.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
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.automirrored.filled.ArrowBack
import androidx.compose.material.icons.filled.InstallMobile
import androidx.compose.material.icons.filled.Update
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.ElevatedCard
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.FilledTonalButton
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
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.material3.pulltorefresh.PullToRefreshBox
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateMapOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
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.dp
import androidx.compose.ui.unit.sp
import androidx.navigation.NavController
import com.cherret.zaprett.HostsInfo
import com.cherret.zaprett.R
import com.cherret.zaprett.download
import com.cherret.zaprett.getAllLists
import com.cherret.zaprett.getFileSha256
import com.cherret.zaprett.getHostList
import com.cherret.zaprett.getZaprettPath
import com.cherret.zaprett.registerDownloadListenerHost
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.io.File
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun HostsRepoScreen(navController: NavController) {
val context = LocalContext.current
var allLists by remember { mutableStateOf(getAllLists()) }
var hostLists by remember { mutableStateOf<List<HostsInfo>?>(null) }
val isUpdate = remember { mutableStateMapOf<String, Boolean>() }
val snackbarHostState = remember { SnackbarHostState() }
var isRefreshing by remember { mutableStateOf(false) }
LaunchedEffect(Unit) {
getHostList {
hostLists = it
}
}
LaunchedEffect(hostLists) {
if (hostLists != null) {
withContext(Dispatchers.IO) {
hostLists!!.forEach { item ->
isUpdate[item.name] = !allLists.any { getFileSha256(File(it)) == item.hash }
}
}
}
}
Scaffold(
topBar = {
TopAppBar(
title = {
Text(
text = stringResource(R.string.title_hosts_repo),
fontSize = 40.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)
)
}
},
windowInsets = WindowInsets(0)
)
},
content = { paddingValues ->
PullToRefreshBox(
isRefreshing = isRefreshing,
onRefresh = {
isRefreshing = true
getHostList {
hostLists = it
isRefreshing = false
}
},
modifier = Modifier.fillMaxSize()
) {
LazyColumn(
contentPadding = paddingValues,
modifier = Modifier.fillMaxSize()
) {
when {
hostLists?.isEmpty() != false -> {
item {
Box(
modifier = Modifier.fillParentMaxSize(),
contentAlignment = Alignment.Center
) {
Text(
stringResource(R.string.btn_no_hosts),
textAlign = TextAlign.Center
)
}
}
}
else -> {
items(hostLists.orEmpty()) { item ->
var isButtonEnabled by remember { mutableStateOf(!allLists.any { File(it).name == item.name }) }
var isInstalling by remember { mutableStateOf(false) }
var isButtonUpdateEnabled by remember { mutableStateOf(true) }
var isUpdateInstalling by remember { mutableStateOf(false) }
ElevatedCard(
elevation = CardDefaults.cardElevation(
defaultElevation = 6.dp
),
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.surfaceContainer,
),
modifier = Modifier
.fillMaxWidth()
.padding(start = 10.dp, top = 25.dp, end = 10.dp),
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
) {
Text(
text = item.name,
modifier = Modifier.weight(1f)
)
}
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.fillMaxWidth()
.padding(start = 16.dp)
) {
Text(
text = item.description,
modifier = Modifier.weight(1f)
)
}
HorizontalDivider(thickness = Dp.Hairline)
Row(
modifier = Modifier
.fillMaxWidth(),
horizontalArrangement = Arrangement.End
) {
if (isUpdate[item.name] == true && allLists.any { File(it).name == item.name }) {
FilledTonalButton(
onClick = {
isUpdateInstalling = true
isButtonUpdateEnabled = false
val downloadId = download(context, item.url)
registerDownloadListenerHost(
context,
downloadId
) { uri ->
val sourceFile = File(uri.path!!)
val targetFile = File(
getZaprettPath() + "/lists",
uri.lastPathSegment!!
)
sourceFile.copyTo(targetFile, overwrite = true)
sourceFile.delete()
isUpdateInstalling = false
getHostList {
hostLists = it
}
isUpdate[item.name] = false
}
},
enabled = isButtonUpdateEnabled,
modifier = Modifier
.padding(start = 5.dp, end = 5.dp),
) {
Icon(
imageVector = Icons.Default.Update,
contentDescription = stringResource(R.string.btn_remove_host),
modifier = Modifier.size(20.dp)
)
Text(
if (!isUpdateInstalling) stringResource(R.string.btn_update_host) else stringResource(
R.string.btn_updating_host
)
)
}
}
FilledTonalButton(
onClick = {
isInstalling = true
isButtonEnabled = false
val downloadId = download(context, item.url)
registerDownloadListenerHost(
context,
downloadId
) { uri ->
val sourceFile = File(uri.path!!)
val targetFile = File(
getZaprettPath() + "/lists",
uri.lastPathSegment!!
)
sourceFile.copyTo(targetFile, overwrite = true)
sourceFile.delete()
isInstalling = false
getHostList {
hostLists = it
}
}
},
enabled = isButtonEnabled,
modifier = Modifier
.padding(start = 5.dp, end = 5.dp),
) {
Icon(
imageVector = Icons.Default.InstallMobile,
contentDescription = stringResource(R.string.btn_remove_host),
modifier = Modifier.size(20.dp)
)
Text(
if (isButtonEnabled) stringResource(R.string.btn_install_host) else if (isInstalling) stringResource(
R.string.btn_installing_host
) else stringResource(R.string.btn_installed_host)
)
}
}
}
}
}
}
}
}
},
snackbarHost = {SnackbarHost(hostState = snackbarHostState)}
)
}

View File

@@ -1,265 +0,0 @@
package com.cherret.zaprett.ui.screens
import android.content.Context
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.MoreVert
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
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.unit.dp
import androidx.compose.ui.unit.sp
import com.cherret.zaprett.BuildConfig
import com.cherret.zaprett.R
import com.cherret.zaprett.checkModuleInstallation
import com.cherret.zaprett.checkRoot
import com.cherret.zaprett.getStartOnBoot
import com.cherret.zaprett.setStartOnBoot
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun SettingsScreen() {
val context = LocalContext.current
val sharedPreferences = remember { context.getSharedPreferences("settings", Context.MODE_PRIVATE) }
val editor = remember { sharedPreferences.edit() }
val useModule = remember { mutableStateOf(sharedPreferences.getBoolean("use_module", false)) }
val updateOnBoot = remember { mutableStateOf(sharedPreferences.getBoolean("update_on_boot", false)) }
val autoRestart = remember { mutableStateOf(getStartOnBoot()) }
val autoUpdate = remember { mutableStateOf(sharedPreferences.getBoolean("auto_update", true)) }
val sendFirebaseAnalytics = remember { mutableStateOf(sharedPreferences.getBoolean("send_firebase_analytics", true)) }
val openNoRootDialog = remember { mutableStateOf(false) }
val openNoModuleDialog = remember { mutableStateOf(false) }
var showAboutDialog = remember { mutableStateOf(false) }
val settingsList = listOf(
SettingItem(
title = stringResource(R.string.btn_use_root),
checked = useModule.value,
onToggle = { isChecked ->
useModule(
context = context,
checked = isChecked,
updateOnBoot = updateOnBoot,
openNoRootDialog = openNoRootDialog,
openNoModuleDialog = openNoModuleDialog
) { success ->
if (success) useModule.value = isChecked
}
}
),
SettingItem(
title = stringResource(R.string.btn_update_on_boot),
checked = updateOnBoot.value,
onToggle = {
updateOnBoot.value = it
editor.putBoolean("update_on_boot", it).apply()
}
),
SettingItem(
title = stringResource(R.string.btn_autorestart),
checked = autoRestart.value,
onToggle = {
if (handleAutoRestart(context, it)) autoRestart.value = it
}
),
SettingItem(
title = stringResource(R.string.btn_autoupdate),
checked = autoUpdate.value,
onToggle = {
autoUpdate.value = it
editor.putBoolean("auto_update", it).apply()
}
),
SettingItem(
title = stringResource(R.string.btn_send_firebase_analytics),
checked = sendFirebaseAnalytics.value,
onToggle = {
sendFirebaseAnalytics.value = it
editor.putBoolean("send_firebase_analytics", it).apply()
}
)
)
if (openNoRootDialog.value) {
InfoDialog(
title = stringResource(R.string.error_root_title),
message = stringResource(R.string.error_root_message),
onDismiss = { openNoRootDialog.value = false }
)
}
if (openNoModuleDialog.value) {
InfoDialog(
title = stringResource(R.string.error_no_module_title),
message = stringResource(R.string.error_no_module_message),
onDismiss = { openNoModuleDialog.value = false }
)
}
if (showAboutDialog.value) {
AboutDialog(onDismiss = { showAboutDialog.value = false })
}
Scaffold(
topBar = {
TopAppBar(
title = {
Text(
text = stringResource(R.string.title_settings),
fontSize = 40.sp,
fontFamily = FontFamily(Font(R.font.unbounded, FontWeight.Normal))
)
},
actions = {
var expanded by remember { mutableStateOf(false) }
IconButton(onClick = { expanded = !expanded }) {
Icon(Icons.Default.MoreVert, contentDescription = null)
}
DropdownMenu(
expanded = expanded,
onDismissRequest = { expanded = false }
) {
DropdownMenuItem(
text = { Text(stringResource(R.string.about_title)) },
onClick = {
expanded = false
showAboutDialog.value = true
}
)
}
},
windowInsets = WindowInsets(0)
)
},
content = { paddingValues ->
LazyColumn(
modifier = Modifier
.padding(paddingValues)
.fillMaxSize(),
contentPadding = PaddingValues(horizontal = 10.dp, vertical = 25.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
items(settingsList) { setting ->
ElevatedCard(
modifier = Modifier.fillMaxWidth(),
elevation = CardDefaults.cardElevation(defaultElevation = 4.dp),
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceVariant)
) {
Row(modifier = Modifier.clickable {
setting.onToggle(!setting.checked)
}) {
SettingsItem(
title = setting.title,
checked = setting.checked,
onCheckedChange = setting.onToggle
)
}
}
}
}
}
)
}
@Composable
private fun SettingsItem(title: String, checked: Boolean, onCheckedChange: (Boolean) -> Unit) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
) {
Text(
text = title,
modifier = Modifier.weight(1f)
)
Switch(
checked = checked,
onCheckedChange = onCheckedChange
)
}
}
private fun useModule(context: Context, checked: Boolean, updateOnBoot: MutableState<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()
updateOnBoot.value = true
callback(true)
} else {
openNoModuleDialog.value = true
}
}
} else {
openNoRootDialog.value = true
}
}
} else {
editor.putBoolean("use_module", false)
.putBoolean("update_on_boot", false)
.apply()
updateOnBoot.value = false
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
private fun InfoDialog(title: String, message: String, onDismiss: () -> Unit) {
AlertDialog(
title = { Text(text = title) },
text = { Text(text = message) },
onDismissRequest = onDismiss,
confirmButton = {
TextButton(onClick = onDismiss) {
Text(stringResource(R.string.btn_continue))
}
}
)
}
@Composable
private fun AboutDialog(onDismiss: () -> Unit) {
AlertDialog(
title = { Text(text = stringResource(R.string.about_title)) },
icon = {Icon(painterResource(R.drawable.ic_launcher_monochrome), contentDescription = stringResource(R.string.app_name), modifier = Modifier
.size(64.dp))},
text = { Text(text = stringResource(R.string.about_text, BuildConfig.VERSION_NAME)) },
onDismissRequest = onDismiss,
confirmButton = { }
)
}
data class SettingItem(
val title: String,
val checked: Boolean,
val onToggle: (Boolean) -> Unit
)

View File

@@ -0,0 +1,88 @@
package com.cherret.zaprett.ui.viewmodel
import android.app.Application
import android.content.Context
import android.net.Uri
import android.os.Environment
import android.provider.OpenableColumns
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.SnackbarResult
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateMapOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.AndroidViewModel
import com.cherret.zaprett.R
import com.cherret.zaprett.getZaprettPath
import com.cherret.zaprett.restartService
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import java.io.File
import java.io.FileOutputStream
import java.io.IOException
abstract class BaseListsViewModel(application: Application) : AndroidViewModel(application) {
val context = application
var allItems by mutableStateOf<List<String>>(emptyList())
private set
var activeItems by mutableStateOf<List<String>>(emptyList())
private set
val checked = mutableStateMapOf<String, Boolean>()
var isRefreshing by mutableStateOf(false)
private set
abstract fun loadAllItems(): Array<String>
abstract fun loadActiveItems(): Array<String>
abstract fun onCheckedChange(item: String, isChecked: Boolean, snackbarHostState: SnackbarHostState, scope: CoroutineScope)
abstract fun deleteItem(item: String, snackbarHostState: SnackbarHostState, scope: CoroutineScope)
fun refresh() {
isRefreshing = true
allItems = loadAllItems().toList()
activeItems = loadActiveItems().toList()
checked.clear()
allItems.forEach { list ->
checked[list] = activeItems.contains(list)
}
isRefreshing = false
}
fun showRestartSnackbar(context: Context, snackbarHostState: SnackbarHostState, scope: CoroutineScope) {
scope.launch {
val result = snackbarHostState.showSnackbar(
context.getString(R.string.pls_restart_snack),
actionLabel = context.getString(R.string.btn_restart_service)
)
if (result == SnackbarResult.ActionPerformed) {
restartService {}
snackbarHostState.showSnackbar(context.getString(R.string.snack_reload))
}
}
}
fun copySelectedFile(context: Context, path: String, uri: Uri) {
if (!Environment.isExternalStorageManager()) return
val contentResolver = context.contentResolver
val fileName = contentResolver.query(uri, null, null, null, null)?.use { cursor ->
val nameIndex = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME)
if (cursor.moveToFirst() && nameIndex != -1) cursor.getString(nameIndex) else "copied_file"
} ?: "copied_file"
val directory = File(getZaprettPath() + path)
if (!directory.exists()) {
directory.mkdirs()
}
val outputFile = File(getZaprettPath() + path, fileName)
try {
contentResolver.openInputStream(uri)?.use { inputStream ->
FileOutputStream(outputFile).use { outputStream ->
inputStream.copyTo(outputStream)
}
}
refresh()
} catch (e: IOException) {
e.printStackTrace()
}
}
}

View File

@@ -0,0 +1,123 @@
package com.cherret.zaprett.ui.viewmodel
import android.app.Application
import android.content.Context
import android.content.SharedPreferences
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.SnackbarResult
import androidx.compose.runtime.mutableStateMapOf
import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.viewModelScope
import com.cherret.zaprett.RepoItemInfo
import com.cherret.zaprett.R
import com.cherret.zaprett.download
import com.cherret.zaprett.getFileSha256
import com.cherret.zaprett.getZaprettPath
import com.cherret.zaprett.registerDownloadListenerHost
import com.cherret.zaprett.restartService
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import java.io.File
abstract class BaseRepoViewModel(application: Application) : AndroidViewModel(application) {
val context = application.applicationContext
val sharedPreferences: SharedPreferences = context.getSharedPreferences("settings", Context.MODE_PRIVATE)
var hostLists = mutableStateOf<List<RepoItemInfo>>(emptyList())
protected set
var isRefreshing = mutableStateOf(false)
protected set
val isUpdate = mutableStateMapOf<String, Boolean>()
val isInstalling = mutableStateMapOf<String, Boolean>()
val isUpdateInstalling = mutableStateMapOf<String, Boolean>()
abstract fun getInstalledLists(): Array<String>
abstract fun getRepoList(callback: (List<RepoItemInfo>?) -> Unit)
fun refresh() {
isRefreshing.value = true
getRepoList { list ->
viewModelScope.launch(Dispatchers.IO) {
val safeList = list ?: emptyList()
val useModule = sharedPreferences.getBoolean("use_module", false)
val filteredList = safeList.filter { item ->
when (item.type) {
"list" -> true
"nfqws" -> useModule
"byedpi" -> !useModule
else -> false
}
}
hostLists.value = filteredList
isUpdate.clear()
val existingHashes = getInstalledLists().map { getFileSha256(File(it)) }
for (item in filteredList) {
isUpdate[item.name] = item.hash !in existingHashes
}
isRefreshing.value = false
}
}
}
fun isItemInstalled(item: RepoItemInfo): Boolean {
return getInstalledLists().any { File(it).name == item.name }
}
fun install(item: RepoItemInfo) {
isInstalling[item.name] = true
val downloadId = download(context, item.url)
registerDownloadListenerHost(context, downloadId) { uri ->
viewModelScope.launch(Dispatchers.IO) {
val sourceFile = File(uri.path!!)
val targetDir = when (item.type) {
"byedpi" -> File(getZaprettPath(), "strategies/byedpi")
"nfqws" -> File(getZaprettPath(), "strategies/nfqws")
else -> File(getZaprettPath(), "lists")
}
val targetFile = File(targetDir, uri.lastPathSegment!!)
sourceFile.copyTo(targetFile, overwrite = true)
sourceFile.delete()
isInstalling[item.name] = false
isUpdate[item.name] = false
refresh()
}
}
}
fun update(item: RepoItemInfo) {
isUpdateInstalling[item.name] = true
val downloadId = download(context, item.url)
registerDownloadListenerHost(context, downloadId) { uri ->
viewModelScope.launch(Dispatchers.IO) {
val sourceFile = File(uri.path!!)
val targetDir = when (item.type) {
"byedpi" -> File(getZaprettPath(), "strategies/byedpi")
"nfqws" -> File(getZaprettPath(), "strategies/nfqws")
else -> File(getZaprettPath(), "lists")
}
val targetFile = File(targetDir, uri.lastPathSegment!!)
sourceFile.copyTo(targetFile, overwrite = true)
sourceFile.delete()
isUpdateInstalling[item.name] = false
isUpdate[item.name] = false
refresh()
}
}
}
fun showRestartSnackbar(snackbarHostState: SnackbarHostState) {
viewModelScope.launch {
val result = snackbarHostState.showSnackbar(
context.getString(R.string.pls_restart_snack),
actionLabel = context.getString(R.string.btn_restart_service)
)
if (result == SnackbarResult.ActionPerformed) {
restartService {}
snackbarHostState.showSnackbar(context.getString(R.string.snack_reload))
}
}
}
}

View File

@@ -0,0 +1,204 @@
package com.cherret.zaprett.ui.viewmodel
import android.app.Application
import android.content.Context
import android.content.Intent
import androidx.compose.material3.SnackbarHostState
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.core.content.ContextCompat
import androidx.lifecycle.AndroidViewModel
import com.cherret.zaprett.ByeDpiVpnService
import com.cherret.zaprett.R
import com.cherret.zaprett.ServiceStatus
import com.cherret.zaprett.download
import com.cherret.zaprett.getBinVersion
import com.cherret.zaprett.getChangelog
import com.cherret.zaprett.getModuleVersion
import com.cherret.zaprett.getStatus
import com.cherret.zaprett.getUpdate
import com.cherret.zaprett.installApk
import com.cherret.zaprett.registerDownloadListener
import com.cherret.zaprett.restartService
import com.cherret.zaprett.startService
import com.cherret.zaprett.stopService
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
class HomeViewModel(application: Application) : AndroidViewModel(application) {
private val context = application
private val prefs = context.getSharedPreferences("settings", Context.MODE_PRIVATE)
private val _requestVpnPermission = MutableStateFlow(false)
val requestVpnPermission = _requestVpnPermission.asStateFlow()
var cardText = mutableIntStateOf(R.string.status_not_availible) // MVP temporarily(maybe)
private set
var moduleVer = mutableStateOf(context.getString(R.string.unknown_text))
private set
var nfqwsVer = mutableStateOf(context.getString(R.string.unknown_text))
private set
var byedpiVer = mutableStateOf(context.getString(R.string.unknown_text))
private set
var serviceMode = mutableIntStateOf(R.string.service_mode_ciadpi)
private set
var changeLog = mutableStateOf<String?>(null)
private set
var newVersion = mutableStateOf<String?>(null)
private set
var updateAvailable = mutableStateOf(false)
private set
var downloadUrl = mutableStateOf<String?>(null)
private set
var showUpdateDialog = mutableStateOf(false)
fun checkForUpdate() {
if (prefs.getBoolean("auto_update", true)) {
getUpdate {
if (it != null) {
downloadUrl.value = it.downloadUrl.toString()
getChangelog(it.changelogUrl.toString()) { log -> changeLog.value = log }
newVersion.value = it.version
updateAvailable.value = true
}
}
}
}
fun checkServiceStatus() {
if (prefs.getBoolean("use_module", false) && prefs.getBoolean("update_on_boot", false)) {
getStatus { isEnabled ->
cardText.intValue = if (isEnabled) R.string.status_enabled else R.string.status_disabled
}
}
else {
cardText.value = if (ByeDpiVpnService.status == ServiceStatus.Connected) R.string.status_enabled else R.string.status_disabled
}
}
fun onCardClick() {
if (prefs.getBoolean("use_module", false)) {
getStatus { isEnabled ->
cardText.value = if (isEnabled) R.string.status_enabled else R.string.status_disabled
}
} else {
cardText.value = if (ByeDpiVpnService.status == ServiceStatus.Connected) R.string.status_enabled else R.string.status_disabled
}
}
fun startVpn() {
ContextCompat.startForegroundService(context, Intent(context, ByeDpiVpnService::class.java).apply { action = "START_VPN" })
}
fun onBtnStartService(snackbarHostState: SnackbarHostState, scope: CoroutineScope) {
if (prefs.getBoolean("use_module", false)) {
getStatus { isEnabled ->
scope.launch {
snackbarHostState.showSnackbar(
context.getString(
if (isEnabled) R.string.snack_already_started else R.string.snack_starting_service
)
)
}
if (!isEnabled) startService {}
}
} else {
if (ByeDpiVpnService.status == ServiceStatus.Disconnected || ByeDpiVpnService.status == ServiceStatus.Failed) {
scope.launch {
snackbarHostState.showSnackbar(context.getString(R.string.snack_starting_service))
}
_requestVpnPermission.value = true
}
else {
scope.launch {
snackbarHostState.showSnackbar(context.getString(R.string.snack_already_started))
}
}
}
}
fun clearVpnPermissionRequest() {
_requestVpnPermission.value = false
}
fun onBtnStopService(snackbarHostState: SnackbarHostState, scope: CoroutineScope) {
if (prefs.getBoolean("use_module", false)) {
getStatus { isEnabled ->
scope.launch {
snackbarHostState.showSnackbar(
context.getString(
if (isEnabled) R.string.snack_stopping_service else R.string.snack_no_service
)
)
}
if (isEnabled) stopService {}
}
} else {
if (ByeDpiVpnService.status == ServiceStatus.Connected) {
scope.launch {
snackbarHostState.showSnackbar(
context.getString(R.string.snack_stopping_service)
)
}
context.startService(Intent(context, ByeDpiVpnService::class.java).apply {
action = "STOP_VPN"
})
}
else {
scope.launch {
snackbarHostState.showSnackbar(context.getString(R.string.snack_no_service))
}
}
}
}
fun onBtnRestart(snackbarHostState: SnackbarHostState, scope: CoroutineScope) {
if (prefs.getBoolean("use_module", false)) {
restartService {}
scope.launch {
snackbarHostState.showSnackbar(context.getString(R.string.snack_reload))
}
} else {
scope.launch {
snackbarHostState.showSnackbar(context.getString(R.string.snack_module_disabled))
}
}
}
fun checkModuleInfo() {
if (prefs.getBoolean("use_module", false)) {
getModuleVersion { value ->
moduleVer.value = value
}
getBinVersion { value ->
nfqwsVer.value = value
}
serviceMode.intValue = R.string.service_mode_nfqws;
}
}
fun showUpdateDialog() {
showUpdateDialog.value = true
}
fun dismissUpdateDialog() {
showUpdateDialog.value = false
}
fun onUpdateConfirm() {
showUpdateDialog.value = false
val id = download(context, downloadUrl.value.orEmpty())
registerDownloadListener(context, id) { uri ->
installApk(context, uri)
}
}
}

View File

@@ -0,0 +1,18 @@
package com.cherret.zaprett.ui.viewmodel
import android.app.Application
import androidx.lifecycle.viewModelScope
import com.cherret.zaprett.RepoItemInfo
import com.cherret.zaprett.download
import com.cherret.zaprett.getAllLists
import com.cherret.zaprett.getHostList
import com.cherret.zaprett.getZaprettPath
import com.cherret.zaprett.registerDownloadListenerHost
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import java.io.File
class HostRepoViewModel(application: Application): BaseRepoViewModel(application) {
override fun getInstalledLists(): Array<String> = getAllLists()
override fun getRepoList(callback: (List<RepoItemInfo>?) -> Unit) = getHostList(sharedPreferences, callback)
}

View File

@@ -0,0 +1,48 @@
package com.cherret.zaprett.ui.viewmodel
import android.app.Application
import android.content.Context
import androidx.compose.material3.SnackbarHostState
import com.cherret.zaprett.disableList
import com.cherret.zaprett.enableList
import com.cherret.zaprett.getActiveLists
import com.cherret.zaprett.getAllLists
import com.cherret.zaprett.getStatus
import kotlinx.coroutines.CoroutineScope
import java.io.File
class HostsViewModel(application: Application): BaseListsViewModel(application) {
private val sharedPreferences = application.getSharedPreferences("settings", Context.MODE_PRIVATE)
override fun loadAllItems(): Array<String> = getAllLists()
override fun loadActiveItems(): Array<String> = getActiveLists(sharedPreferences)
override fun deleteItem(item: String, snackbarHostState: SnackbarHostState, scope: CoroutineScope) {
val wasChecked = checked[item] == true
disableList(item, sharedPreferences)
val success = File(item).delete()
if (success) refresh()
if (sharedPreferences.getBoolean("use_module", false)) {
getStatus { isEnabled ->
if (isEnabled && wasChecked) {
showRestartSnackbar(context, snackbarHostState, scope)
}
}
}
}
override fun onCheckedChange(item: String, isChecked: Boolean, snackbarHostState: SnackbarHostState, scope: CoroutineScope) {
checked[item] = isChecked
if (isChecked) enableList(item, sharedPreferences) else disableList(item, sharedPreferences)
if (sharedPreferences.getBoolean("use_module", false)) {
getStatus { isEnabled ->
if (isEnabled) {
showRestartSnackbar(
context,
snackbarHostState,
scope
)
}
}
}
}
}

View File

@@ -0,0 +1,17 @@
package com.cherret.zaprett.ui.viewmodel
import android.app.Application
import com.cherret.zaprett.RepoItemInfo
import com.cherret.zaprett.getAllByeDPIStrategies
import com.cherret.zaprett.getAllNfqwsStrategies
import com.cherret.zaprett.getStrategiesList
class StrategyRepoViewModel(application: Application): BaseRepoViewModel(application) {
override fun getInstalledLists(): Array<String> =
if (sharedPreferences.getBoolean("use_module", false)) {
getAllNfqwsStrategies()
} else {
getAllByeDPIStrategies()
}
override fun getRepoList(callback: (List<RepoItemInfo>?) -> Unit) = getStrategiesList(sharedPreferences, callback)
}

View File

@@ -0,0 +1,91 @@
package com.cherret.zaprett.ui.viewmodel
import android.app.Application
import android.content.Context
import android.content.SharedPreferences
import androidx.compose.material3.SnackbarHostState
import com.cherret.zaprett.ByeDpiVpnService
import com.cherret.zaprett.ServiceStatus
import com.cherret.zaprett.disableStrategy
import com.cherret.zaprett.enableStrategy
import com.cherret.zaprett.getActiveByeDPIStrategies
import com.cherret.zaprett.getActiveNfqwsStrategies
import com.cherret.zaprett.getAllByeDPIStrategies
import com.cherret.zaprett.getAllNfqwsStrategies
import com.cherret.zaprett.getStatus
import kotlinx.coroutines.CoroutineScope
import java.io.File
class StrategyViewModel(application: Application): BaseListsViewModel(application) {
private val sharedPreferences = application.getSharedPreferences("settings", Context.MODE_PRIVATE)
private val useModule = sharedPreferences.getBoolean("use_module", false)
private val strategyProvider: StrategyProvider = if (useModule) {
NfqwsStrategyProvider()
} else {
ByeDPIStrategyProvider(sharedPreferences)
}
override fun loadAllItems(): Array<String> = strategyProvider.getAll()
override fun loadActiveItems(): Array<String> = strategyProvider.getActive()
override fun deleteItem(item: String, snackbarHostState: SnackbarHostState, scope: CoroutineScope) {
val wasChecked = checked[item] == true
disableStrategy(item, sharedPreferences)
val success = File(item).delete()
if (success) refresh()
if (sharedPreferences.getBoolean("use_module", false)) {
getStatus { isEnabled ->
if (isEnabled && wasChecked) {
showRestartSnackbar(context, snackbarHostState, scope)
}
}
}
else {
if (ByeDpiVpnService.status == ServiceStatus.Connected && wasChecked) {
showRestartSnackbar(context, snackbarHostState, scope)
}
}
}
override fun onCheckedChange(item: String, isChecked: Boolean, snackbarHostState: SnackbarHostState, scope: CoroutineScope) {
checked[item] = isChecked
if (isChecked) {
checked.keys.forEach { key ->
checked[key] = false
disableStrategy(key, sharedPreferences)
}
checked[item] = true
enableStrategy(item, sharedPreferences)
}
else {
checked[item] = false
disableStrategy(item, sharedPreferences)
}
if (sharedPreferences.getBoolean("use_module", false)) {
getStatus { isEnabled ->
if (isEnabled) {
showRestartSnackbar(
context,
snackbarHostState,
scope
)
}
}
}
}
}
interface StrategyProvider {
fun getAll(): Array<String>
fun getActive(): Array<String>
}
class NfqwsStrategyProvider : StrategyProvider {
override fun getAll() = getAllNfqwsStrategies()
override fun getActive() = getActiveNfqwsStrategies()
}
class ByeDPIStrategyProvider(private val sharedPreferences: SharedPreferences) : StrategyProvider {
override fun getAll() = getAllByeDPIStrategies()
override fun getActive() = getActiveByeDPIStrategies(sharedPreferences)
}

View File

@@ -0,0 +1,16 @@
# Copyright (C) 2023 The Android Open Source Project
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
include $(call all-subdir-makefiles)

View File

@@ -0,0 +1,21 @@
# Copyright (C) 2023 The Android Open Source Project
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
APP_OPTIM := release
APP_PLATFORM := android-21
APP_ABI := armeabi-v7a arm64-v8a x86 x86_64
APP_CFLAGS := -O3 -DPKGNAME=com/cherret/zaprett
APP_CPPFLAGS := -O3 -std=c++11
NDK_TOOLCHAIN_VERSION := clang

View File

@@ -2,8 +2,11 @@
<resources>
<string name="title_home">Главная</string>
<string name="title_hosts">Хосты</string>
<string name="title_hosts_repo">Репозиторий</string>
<string name="title_strategies">Стратегии</string>
<string name="title_repo">Репозиторий</string>
<string name="title_settings">Настройки</string>
<string name="general_section">Основные настройки</string>
<string name="byedpi_section">Настройки ByeDPI</string>
<string name="btn_continue">Продолжить</string>
<string name="btn_update">Обновить</string>
<string name="btn_dismiss">Отмена</string>
@@ -25,11 +28,14 @@
<string name="btn_start_service">Запустить сервис</string>
<string name="btn_stop_service">Остановить сервис</string>
<string name="btn_restart_service">Перезапустить сервис</string>
<string name="notification_zaprett_description">zaprett proxy запущен</string>
<string name="toast_no_strategy_selected">Стратегия не выбрана</string>
<string name="btn_remove_host">Удалить</string>
<string name="btn_download_host">Скачать</string>
<string name="btn_add_host">Добавить</string>
<string name="btn_back">Назад</string>
<string name="btn_no_hosts">Нет хостов</string>
<string name="empty_list">Пусто :(</string>
<string name="title_author">Автор: %1$s</string>
<string name="btn_install_host">Установить</string>
<string name="btn_update_host">Обновить</string>
<string name="btn_installing_host">Установка</string>
@@ -38,12 +44,25 @@
<string name="btn_autorestart">Переодически перезапускать сервис</string>
<string name="btn_autoupdate">Авто обновление</string>
<string name="btn_send_firebase_analytics">Отправлять аналитику Firebase</string>
<string name="alert_version">Version: %1$s → %2$s\nСписок изменений:\n%3$s</string>
<string name="btn_repository_url_lists">URL репозитория хостов</string>
<string name="hint_enter_repository_url_lists">Введите URL репозитория хостов</string>
<string name="btn_repository_url_strategies">URL репозитория стратегий</string>
<string name="hint_enter_repository_url_strategies">Введите URL репозитория стратегий</string>
<string name="btn_ipv6">IPv6</string>
<string name="btn_ip">IP-адрес</string>
<string name="hint_ip">Введите IP-адрес</string>
<string name="btn_port">Порт</string>
<string name="hint_port">Введите порт</string>
<string name="btn_dns">DNS</string>
<string name="hint_dns">Введите DNS</string>
<string name="alert_version">Версия: %1$s → %2$s\nСписок изменений:\n%3$s</string>
<string name="snack_already_started">Сервис уже запущен.</string>
<string name="snack_starting_service">Запускаем сервис…</string>
<string name="snack_no_service">Сервис не запущен</string>
<string name="snack_stopping_service">Останавливаем сервис…</string>
<string name="error_no_storage_title">Нет разрешения</string>
<string name="notification_permission_title">Разрешение на уведомления</string>
<string name="notification_permission_message">Приложению нужно разрешение на отправку уведомлений</string>
<string name="error_no_storage_message">Для правильной работы приложения необходимо разрешение на доступ к хранилищу</string>
<string name="btn_show_full_path">Показывать полный путь к листу</string>
<string name="pls_reboot_snack">Перезагрузите устройство для вступления изменений в силу</string>
@@ -51,9 +70,14 @@
<string name="qs_name">Zaprett</string>
<string name="qs_starting">Запуск…</string>
<string name="qs_stopping">Остановка…</string>
<string name="module_version">Версия модуля</string>
<string name="nfqws_version">Версия nfqws</string>
<string name="ciadpi_version">Версия ciadpi 0.17.1</string>
<string name="unknown_text">неизвестно</string>
<string name="service_mode">Режим работы</string>
<string name="qs_not_available">Не доступно</string>
<string name="qs_working">Работает</string>
<string name="qs_not_working">Не работает</string>
<string name="about_title">О приложении</string>
<string name="about_text">Zaprett app от Cherret\nВерсия: %1$s</string>
<string name="about_text">Zaprett app от Cherret, egor-white\nВерсия: %1$s</string>
</resources>

View File

@@ -2,8 +2,11 @@
<string name="app_name" translatable="false">zaprett</string>
<string name="title_home">Home</string>
<string name="title_hosts">Hosts</string>
<string name="title_hosts_repo">Repository</string>
<string name="title_strategies">Strategies</string>
<string name="title_repo">Repository</string>
<string name="title_settings">Settings</string>
<string name="general_section">General settings</string>
<string name="byedpi_section">ByeDPI settings</string>
<string name="btn_continue">Continue</string>
<string name="btn_update">Update</string>
<string name="btn_dismiss">Dismiss</string>
@@ -25,35 +28,59 @@
<string name="btn_start_service">Start service</string>
<string name="btn_stop_service">Stop service</string>
<string name="btn_restart_service">Restart service</string>
<string name="notification_zaprett_proxy" translatable="false">zaprett proxy</string>
<string name="notification_zaprett_description">zaprett proxy is running</string>
<string name="toast_no_strategy_selected">No strategy selected</string>
<string name="btn_remove_host">Remove</string>
<string name="btn_download_host">Download</string>
<string name="btn_add_host">Add</string>
<string name="btn_back">Back</string>
<string name="btn_no_hosts">No Hosts</string>
<string name="empty_list">Empty :(</string>
<string name="title_author">Author: %1$s</string>
<string name="btn_install_host">Install</string>
<string name="btn_update_host">Update</string>
<string name="btn_installing_host">Installing</string>
<string name="btn_updating_host">Updating</string>
<string name="btn_installed_host">Installed</string>
<string name="btn_autorestart">Restart service periodicaly</string>
<string name="btn_autorestart">Restart service periodically</string>
<string name="btn_autoupdate">Autoupdate</string>
<string name="btn_send_firebase_analytics">Send Firebase Analytics</string>
<string name="btn_repository_url_lists">Hosts repository URL</string>
<string name="hint_enter_repository_url_lists">Enter hosts repository URL</string>
<string name="btn_repository_url_strategies">Strategies repository URL</string>
<string name="hint_enter_repository_url_strategies">Enter strategies repository URL</string>
<string name="btn_ipv6">IPv6</string>
<string name="btn_ip">IP address</string>
<string name="hint_ip">Enter IP address</string>
<string name="btn_port">Port</string>
<string name="hint_port">Enter port</string>
<string name="btn_dns">DNS</string>
<string name="hint_dns">Enter DNS</string>
<string name="alert_version">Version: %1$s → %2$s\nChangelog:\n%3$s</string>
<string name="snack_already_started">Service already started.</string>
<string name="snack_starting_service">Starting service…</string>
<string name="snack_no_service">Service is not launched</string>
<string name="snack_stopping_service">Stopping service…</string>
<string name="error_no_storage_title">No permission</string>
<string name="error_no_storage_message">The application requires permission to acess the storage to work properly</string>
<string name="error_no_storage_message">The application requires permission to access the storage to work properly</string>
<string name="notification_permission_title">Notification Permission</string>
<string name="notification_permission_message">This app needs permission to send notifications</string>
<string name="btn_show_full_path">Show full list\'s path</string>
<string name="pls_reboot_snack">Reboot your device for the changes to take effect</string>
<string name="pls_restart_snack">Restart zaprett service for the changes to take effects</string>
<string name="qs_name">Zaprett</string>
<string name="qs_starting">Starting…</string>
<string name="qs_stopping">Stopping…</string>
<string name="module_version">Module version</string>
<string name="nfqws_version">nfqws version</string>
<string name="ciadpi_version">ciadpi version 0.17.1</string>
<string name="unknown_text">unknown</string>
<string name="service_mode">Service mode</string>
<string name="service_mode_nfqws" translatable="false">nfqws</string>
<string name="service_mode_ciadpi" translatable="false">ciadpi</string>
<string name="qs_not_available">Not available</string>
<string name="qs_working">Working</string>
<string name="qs_not_working">Not working</string>
<string name="about_title">About app</string>
<string name="about_text">Zaprett App by Cherret\nVersion: %1$s</string>
<string name="about_text">Zaprett App by Cherret, egor-white\nVersion: %1$s</string>
</resources>

View File

@@ -1,5 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="Theme.Zaprett" parent="android:Theme.Material.Light.NoActionBar" />
</resources>

View File

@@ -1,2 +1 @@
Добавлена возможность загружать хосты из репозитория
Рефакторинг кода
Добавлена вкладка "Стратегии"

View File

@@ -1,5 +1,5 @@
[versions]
agp = "8.9.0"
agp = "8.10.1"
kotlin = "2.0.21"
coreKtx = "1.10.1"
junit = "4.13.2"
@@ -8,6 +8,7 @@ espressoCore = "3.5.1"
lifecycleRuntimeKtx = "2.6.1"
activityCompose = "1.8.0"
composeBom = "2024.09.00"
lifecycleService = "2.9.1"
[libraries]
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
@@ -24,6 +25,7 @@ androidx-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-toolin
androidx-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" }
androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" }
androidx-material3 = { group = "androidx.compose.material3", name = "material3" }
androidx-lifecycle-service = { group = "androidx.lifecycle", name = "lifecycle-service", version.ref = "lifecycleService" }
[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 79 KiB

After

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 80 KiB

After

Width:  |  Height:  |  Size: 142 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 79 KiB

After

Width:  |  Height:  |  Size: 134 KiB

BIN
images/4.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

BIN
images/5.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 104 KiB

BIN
images/6.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 103 KiB

View File

@@ -1,6 +1,6 @@
{
"version": "1.7",
"versionCode": 8,
"downloadUrl": "https://github.com/CherretGit/zaprett-app/releases/download/1_7_0/app-release.apk",
"version": "1.10",
"versionCode": 11,
"downloadUrl": "https://github.com/CherretGit/zaprett-app/releases/download/1_10_0/app-release.apk",
"changelogUrl": "https://raw.githubusercontent.com/CherretGit/zaprett-app/refs/heads/main/changelog.md"
}