mirror of
https://github.com/CherretGit/zaprett-app.git
synced 2026-01-19 01:49:38 +05:00
Compare commits
83 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c5b60b1ee9 | ||
|
|
2f349e5108 | ||
|
|
e8a8fa69db | ||
|
|
c094a072b8 | ||
|
|
786321d852 | ||
|
|
7a17c93622 | ||
|
|
cf38ef7ac2 | ||
|
|
94979d2b2e | ||
|
|
bd1bdf8298 | ||
|
|
adc98db3f1 | ||
|
|
9bbc3b771f | ||
|
|
51356e63eb | ||
|
|
f3f2cf999c | ||
|
|
69f395275e | ||
|
|
39dc2baad3 | ||
|
|
ad5c556241 | ||
|
|
48e7497e79 | ||
|
|
2954cef0a6 | ||
|
|
c49cce1de4 | ||
|
|
4786ef8faf | ||
|
|
0b69d2bf4d | ||
|
|
9208885de4 | ||
|
|
44fbb5fa98 | ||
|
|
7c2fba684d | ||
|
|
3ac0ca73e1 | ||
|
|
abb3a8e4e4 | ||
|
|
73cbd89390 | ||
|
|
8cfe6f4389 | ||
|
|
9a89fcccae | ||
|
|
cd44a09cfc | ||
|
|
14d6ea6caa | ||
|
|
6caff9b611 | ||
|
|
5eba2461dd | ||
|
|
5a6bf9780b | ||
|
|
ce6e1f07b8 | ||
|
|
1e985807d9 | ||
|
|
80d885167e | ||
|
|
01f4333fee | ||
|
|
c67846a7e4 | ||
|
|
c4da9d1c29 | ||
|
|
0386fbbccd | ||
|
|
109042d5e7 | ||
|
|
34595512d0 | ||
|
|
7d2b969337 | ||
|
|
da66743014 | ||
|
|
6146504125 | ||
|
|
0d8deb2e9f | ||
|
|
fd06b6c75b | ||
|
|
ce91b578b8 | ||
|
|
68b25d8efa | ||
|
|
b8d26a1a57 | ||
|
|
e6ffe78eb1 | ||
|
|
d754abf794 | ||
|
|
2bdc69b1ba | ||
|
|
902b2939cd | ||
|
|
c70752f8c5 | ||
|
|
a385e498ea | ||
|
|
3f6086c11f | ||
|
|
f7f8d3e3b9 | ||
|
|
6095a3d485 | ||
|
|
8771c8fd1b | ||
|
|
51d774b6aa | ||
|
|
879bf98ed1 | ||
|
|
cb283b7d62 | ||
|
|
e988bfdd2e | ||
|
|
49f61892da | ||
|
|
2aa831baa5 | ||
|
|
ef04471a05 | ||
|
|
baec0f3838 | ||
|
|
8101c44dc1 | ||
|
|
aef5b962b5 | ||
|
|
28dca0716a | ||
|
|
0807eebeb2 | ||
|
|
b17a0dc4cb | ||
|
|
1ada6304a4 | ||
|
|
e16e8e3bbe | ||
|
|
1e0368e467 | ||
|
|
4dcf90dcfc | ||
|
|
94ff869bd5 | ||
|
|
435f548698 | ||
|
|
660c3b509a | ||
|
|
6e8960a707 | ||
|
|
c5f836d65c |
13
.github/workflows/workflow.yml
vendored
13
.github/workflows/workflow.yml
vendored
@@ -37,6 +37,19 @@ jobs:
|
||||
- name: Set up Android SDK
|
||||
uses: android-actions/setup-android@v2
|
||||
|
||||
- name: Setup NDK
|
||||
run: sdkmanager "ndk;27.0.12077973" "cmake;3.22.1"
|
||||
|
||||
- name: Install toolchain
|
||||
run: |
|
||||
rustup default stable
|
||||
rustup update stable
|
||||
cargo install cargo-ndk
|
||||
rustup target add armv7-linux-androideabi
|
||||
rustup target install aarch64-linux-android
|
||||
rustup target add i686-linux-android
|
||||
rustup target add x86_64-linux-android
|
||||
|
||||
- name: Build APK
|
||||
run: ./gradlew assembleRelease
|
||||
|
||||
|
||||
4
.gitmodules
vendored
4
.gitmodules
vendored
@@ -1,5 +1,5 @@
|
||||
[submodule "app/src/main/cpp/byedpi"]
|
||||
path = app/src/main/cpp/byedpi
|
||||
[submodule "rust/byedpi"]
|
||||
path = rust/byedpi
|
||||
url = https://github.com/hufrea/byedpi
|
||||
[submodule "app/src/main/jni/hev-socks5-tunnel"]
|
||||
path = app/src/main/jni/hev-socks5-tunnel
|
||||
|
||||
4
.idea/deploymentTargetSelector.xml
generated
4
.idea/deploymentTargetSelector.xml
generated
@@ -4,10 +4,10 @@
|
||||
<selectionStates>
|
||||
<SelectionState runConfigName="app">
|
||||
<option name="selectionMode" value="DROPDOWN" />
|
||||
<DropdownSelection timestamp="2025-07-09T09:45:05.074315845Z">
|
||||
<DropdownSelection timestamp="2025-10-03T07:21:08.712998131Z">
|
||||
<Target type="DEFAULT_BOOT">
|
||||
<handle>
|
||||
<DeviceId pluginId="LocalEmulator" identifier="path=/home/white/.android/avd/Pixel_8.avd" />
|
||||
<DeviceId pluginId="LocalEmulator" identifier="path=/home/dimap/.android/avd/Medium_Phone.avd" />
|
||||
</handle>
|
||||
</Target>
|
||||
</DropdownSelection>
|
||||
|
||||
13
.idea/deviceManager.xml
generated
Normal file
13
.idea/deviceManager.xml
generated
Normal file
@@ -0,0 +1,13 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="DeviceTable">
|
||||
<option name="columnSorters">
|
||||
<list>
|
||||
<ColumnSorterState>
|
||||
<option name="column" value="Name" />
|
||||
<option name="order" value="ASCENDING" />
|
||||
</ColumnSorterState>
|
||||
</list>
|
||||
</option>
|
||||
</component>
|
||||
</project>
|
||||
2
.idea/vcs.xml
generated
2
.idea/vcs.xml
generated
@@ -2,11 +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" />
|
||||
<mapping directory="$PROJECT_DIR$/rust/byedpi" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
||||
28
README.md
28
README.md
@@ -1,19 +1,37 @@
|
||||
# zaprett
|
||||
## О приложении
|
||||
Приложение разработано для работы с модулем [zaprett](https://github.com/egor-white/zaprett)
|
||||
### [Официальный Telegram-канал приложения](https://t.me/zaprett_module)
|
||||
## ВНИМАНИЕ приложению желательно наличие root прав на устройстве, но есть режим работы без них на основе byedpi
|
||||
> [!IMPORTANT]
|
||||
> 📢 [Официальный Telegram-канал приложения](https://t.me/zaprett_module)
|
||||
|
||||
> [!CAUTION]
|
||||
> ⚠️ Для корректной работы приложения **желательны root-права**, однако предусмотрен режим без root на основе **byedpi**
|
||||
|
||||
---
|
||||
|
||||
На данный момент приложение умеет:
|
||||
* Запускать, останавливать и перезапускать сервис
|
||||
* Работа с листами (добавление, включение и выключение, загрузка из репозитория)
|
||||
* Работа с стратегиями (добавление, выбор, загрузка из репозитория)
|
||||
* Авто обновление приложения
|
||||
|
||||
---
|
||||
|
||||
## [Репозиторий с хостами и стратегиями](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/4.png" width="300"><img src="images/5.png" width="300">
|
||||
<img src="images/6.png" width="300">
|
||||
<img src="images/7.png" width="300">
|
||||
<p align="center">
|
||||
<img src="images/1.png" width="180">
|
||||
<img src="images/2.png" width="180">
|
||||
<img src="images/3.png" width="180"><br>
|
||||
<img src="images/4.png" width="180">
|
||||
<img src="images/5.png" width="180">
|
||||
<img src="images/6.png" width="180"><br>
|
||||
<img src="images/7.png" width="180">
|
||||
</p>
|
||||
|
||||
@@ -5,29 +5,21 @@ plugins {
|
||||
id("com.google.gms.google-services")
|
||||
id("com.google.firebase.crashlytics")
|
||||
kotlin("plugin.serialization") version "2.1.20"
|
||||
id("org.mozilla.rust-android-gradle.rust-android") version "0.9.6"
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "com.cherret.zaprett"
|
||||
compileSdk = 35
|
||||
compileSdk = 36
|
||||
|
||||
defaultConfig {
|
||||
applicationId = "com.cherret.zaprett"
|
||||
minSdk = 29
|
||||
targetSdk = 35
|
||||
versionCode = 18
|
||||
versionName = "2.6"
|
||||
versionCode = 23
|
||||
versionName = "2.11"
|
||||
|
||||
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 {
|
||||
@@ -37,6 +29,12 @@ android {
|
||||
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||
"proguard-rules.pro"
|
||||
)
|
||||
buildConfigField("boolean", "send_firebase_analytics", "true")
|
||||
buildConfigField("boolean", "auto_update", "true")
|
||||
}
|
||||
debug {
|
||||
buildConfigField("boolean", "send_firebase_analytics", "false")
|
||||
buildConfigField("boolean", "auto_update", "false")
|
||||
}
|
||||
}
|
||||
compileOptions {
|
||||
@@ -75,20 +73,42 @@ tasks.preBuild {
|
||||
dependsOn("runNdkBuild")
|
||||
}
|
||||
|
||||
cargo {
|
||||
module = "../rust"
|
||||
libname = "byedpi"
|
||||
targets = listOf("arm", "arm64", "x86", "x86_64")
|
||||
profile = "release"
|
||||
}
|
||||
|
||||
tasks.preBuild {
|
||||
dependsOn("cargoBuild")
|
||||
}
|
||||
|
||||
tasks.register<Exec>("cargoClean") {
|
||||
workingDir = file("../rust")
|
||||
commandLine("cargo", "clean")
|
||||
group = "build"
|
||||
}
|
||||
|
||||
tasks.named("clean") {
|
||||
dependsOn("cargoClean")
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation("androidx.compose.material3:material3:1.3.1")
|
||||
implementation("androidx.compose.material3:material3-window-size-class:1.3.1")
|
||||
implementation("androidx.compose.material3:material3-adaptive-navigation-suite:1.4.0-alpha10")
|
||||
implementation("androidx.navigation:navigation-compose:2.8.9")
|
||||
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("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("io.coil-kt.coil3:coil-compose:3.2.0")
|
||||
implementation(libs.compose.material3)
|
||||
implementation(libs.compose.material3.window.size)
|
||||
implementation(libs.compose.material3.adaptive.nav)
|
||||
implementation(libs.navigation.compose)
|
||||
implementation(libs.compose.icons)
|
||||
implementation(libs.libsu.core)
|
||||
implementation(libs.okhttp)
|
||||
implementation(libs.serialization.json)
|
||||
implementation(platform(libs.firebase.bom))
|
||||
implementation(libs.firebase.analytics)
|
||||
implementation(libs.firebase.crashlytics)
|
||||
implementation(libs.fragment.compose)
|
||||
implementation(libs.coil.compose)
|
||||
implementation(libs.compose.markdown)
|
||||
implementation(libs.androidx.core.ktx)
|
||||
implementation(libs.androidx.lifecycle.runtime.ktx)
|
||||
implementation(libs.androidx.activity.compose)
|
||||
|
||||
Submodule app/src/main/cpp/byedpi deleted from 22ae84f73d
@@ -1,83 +0,0 @@
|
||||
#define VERSION "17.1"
|
||||
union sockaddr_u;
|
||||
|
||||
int get_default_ttl(void);
|
||||
|
||||
int get_addr(const char *str, union sockaddr_u *addr);
|
||||
|
||||
void *add(void **root, int *n, size_t ss);
|
||||
|
||||
void clear_params(void);
|
||||
|
||||
char *ftob(const char *str, ssize_t *sl);
|
||||
|
||||
char *data_from_str(const char *str, ssize_t *size);
|
||||
|
||||
size_t parse_cform(char *buffer, size_t blen, const char *str, size_t slen);
|
||||
|
||||
struct mphdr *parse_hosts(char *buffer, size_t size);
|
||||
|
||||
struct mphdr *parse_ipset(char *buffer, size_t size);
|
||||
|
||||
int parse_offset(struct part *part, const char *str);
|
||||
|
||||
static const char help_text[] = {
|
||||
" -i, --ip, <ip> Listening IP, default 0.0.0.0\n"
|
||||
" -p, --port <num> Listening port, default 1080\n"
|
||||
#ifdef DAEMON
|
||||
" -D, --daemon Daemonize\n"
|
||||
" -w, --pidfile <filename> Write PID to file\n"
|
||||
#endif
|
||||
#ifdef __linux__
|
||||
" -E, --transparent Transparent proxy mode\n"
|
||||
#endif
|
||||
" -c, --max-conn <count> Connection count limit, default 512\n"
|
||||
" -N, --no-domain Deny domain resolving\n"
|
||||
" -U, --no-udp Deny UDP association\n"
|
||||
" -I --conn-ip <ip> Connection binded IP, default ::\n"
|
||||
" -b, --buf-size <size> Buffer size, default 16384\n"
|
||||
" -x, --debug <level> Print logs, 0, 1 or 2\n"
|
||||
" -g, --def-ttl <num> TTL for all outgoing connections\n"
|
||||
// desync options
|
||||
#ifdef TCP_FASTOPEN_CONNECT
|
||||
" -F, --tfo Enable TCP Fast Open\n"
|
||||
#endif
|
||||
" -A, --auto <t,r,s,n> Try desync params after this option\n"
|
||||
" Detect: torst,redirect,ssl_err,none\n"
|
||||
" -L, --auto-mode <0-3> Mode: 1 - post_resp, 2 - sort, 3 - 1+2\n"
|
||||
" -u, --cache-ttl <sec> Lifetime of cached desync params for IP\n"
|
||||
" -y, --cache-dump <file|-> Dump cache to file or stdout\n"
|
||||
#ifdef TIMEOUT_SUPPORT
|
||||
" -T, --timeout <sec> Timeout waiting for response, after which trigger auto\n"
|
||||
#endif
|
||||
" -K, --proto <t,h,u,i> Protocol whitelist: tls,http,udp,ipv4\n"
|
||||
" -H, --hosts <file|:str> Hosts whitelist, filename or :string\n"
|
||||
" -j, --ipset <file|:str> IP whitelist\n"
|
||||
" -V, --pf <port[-portr]> Ports range whitelist\n"
|
||||
" -R, --round <num[-numr]> Number of request to which desync will be applied\n"
|
||||
" -s, --split <pos_t> Position format: offset[:repeats:skip][+flag1[flag2]]\n"
|
||||
" Flags: +s - SNI offset, +h - HTTP host offset, +n - null\n"
|
||||
" Additional flags: +e - end, +m - middle\n"
|
||||
" -d, --disorder <pos_t> Split and send reverse order\n"
|
||||
" -o, --oob <pos_t> Split and send as OOB data\n"
|
||||
" -q, --disoob <pos_t> Split and send reverse order as OOB data\n"
|
||||
#ifdef FAKE_SUPPORT
|
||||
" -f, --fake <pos_t> Split and send fake packet\n"
|
||||
#ifdef __linux__
|
||||
" -S, --md5sig Add MD5 Signature option for fake packets\n"
|
||||
#endif
|
||||
" -n, --fake-sni <str> Change SNI in fake\n"
|
||||
" Replaced: ? - rand let, # - rand num, * - rand let/num\n"
|
||||
#endif
|
||||
" -t, --ttl <num> TTL of fake packets, default 8\n"
|
||||
" -O, --fake-offset <pos_t> Fake data start offset\n"
|
||||
" -l, --fake-data <f|:str> Set custom fake packet\n"
|
||||
" -Q, --fake-tls-mod <r,o> Modify fake TLS CH: rand,orig\n"
|
||||
" -e, --oob-data <char> Set custom OOB data\n"
|
||||
" -M, --mod-http <h,d,r> Modify HTTP: hcsmix,dcsmix,rmspace\n"
|
||||
" -r, --tlsrec <pos_t> Make TLS record at position\n"
|
||||
" -a, --udp-fake <count> UDP fakes count, default 0\n"
|
||||
#ifdef __linux__
|
||||
" -Y, --drop-sack Drop packets with SACK extension\n"
|
||||
#endif
|
||||
};
|
||||
@@ -1,100 +0,0 @@
|
||||
#include <string.h>
|
||||
|
||||
#include <jni.h>
|
||||
#include <android/log.h>
|
||||
#include <malloc.h>
|
||||
|
||||
#include "byedpi/error.h"
|
||||
#include "byedpi/proxy.h"
|
||||
#include "utils.h"
|
||||
|
||||
static int g_proxy_fd = -1;
|
||||
|
||||
JNIEXPORT jint JNI_OnLoad(
|
||||
__attribute__((unused)) JavaVM *vm,
|
||||
__attribute__((unused)) void *reserved) {
|
||||
default_params = params;
|
||||
return JNI_VERSION_1_6;
|
||||
}
|
||||
|
||||
JNIEXPORT jint JNICALL
|
||||
Java_com_cherret_zaprett_byedpi_NativeBridge_jniCreateSocket(
|
||||
JNIEnv *env,
|
||||
__attribute__((unused)) jobject thiz,
|
||||
jobjectArray args) {
|
||||
|
||||
if (g_proxy_fd != -1) {
|
||||
LOG(LOG_S, "proxy already running, fd: %d", g_proxy_fd);
|
||||
return -1;
|
||||
}
|
||||
|
||||
int argc = (*env)->GetArrayLength(env, args);
|
||||
char *argv[argc];
|
||||
for (int i = 0; i < argc; i++) {
|
||||
jstring arg = (jstring) (*env)->GetObjectArrayElement(env, args, i);
|
||||
const char *arg_str = (*env)->GetStringUTFChars(env, arg, 0);
|
||||
argv[i] = strdup(arg_str);
|
||||
(*env)->ReleaseStringUTFChars(env, arg, arg_str);
|
||||
}
|
||||
|
||||
int res = parse_args(argc, argv);
|
||||
|
||||
if (res < 0) {
|
||||
uniperror("parse_args");
|
||||
return -1;
|
||||
}
|
||||
|
||||
int fd = listen_socket((union sockaddr_u *)¶ms.laddr);
|
||||
|
||||
for (int i = 0; i < argc; i++) {
|
||||
free(argv[i]);
|
||||
}
|
||||
|
||||
if (fd < 0) {
|
||||
uniperror("listen_socket");
|
||||
return -1;
|
||||
}
|
||||
|
||||
g_proxy_fd = fd;
|
||||
LOG(LOG_S, "listen_socket, fd: %d", fd);
|
||||
return fd;
|
||||
}
|
||||
|
||||
JNIEXPORT jint JNICALL
|
||||
Java_com_cherret_zaprett_byedpi_NativeBridge_jniStartProxy(
|
||||
__attribute__((unused)) JNIEnv *env,
|
||||
__attribute__((unused)) jobject thiz) {
|
||||
|
||||
LOG(LOG_S, "start_proxy, fd: %d", g_proxy_fd);
|
||||
|
||||
if (start_event_loop(g_proxy_fd) < 0) {
|
||||
uniperror("event_loop");
|
||||
return get_e();
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
JNIEXPORT jint JNICALL
|
||||
Java_com_cherret_zaprett_byedpi_NativeBridge_jniStopProxy(
|
||||
__attribute__((unused)) JNIEnv *env,
|
||||
__attribute__((unused)) jobject thiz) {
|
||||
|
||||
LOG(LOG_S, "stop_proxy, fd: %d", g_proxy_fd);
|
||||
|
||||
if (g_proxy_fd < 0) {
|
||||
LOG(LOG_S, "proxy is not running, fd: %d", g_proxy_fd);
|
||||
return 0;
|
||||
}
|
||||
|
||||
reset_params();
|
||||
int res = shutdown(g_proxy_fd, SHUT_RDWR);
|
||||
g_proxy_fd = -1;
|
||||
|
||||
if (res < 0) {
|
||||
uniperror("shutdown");
|
||||
return get_e();
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
@@ -1,594 +0,0 @@
|
||||
#include <getopt.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "byedpi/params.h"
|
||||
#include "error.h"
|
||||
#include "main.h"
|
||||
#include "packets.h"
|
||||
#include "utils.h"
|
||||
|
||||
struct params default_params;
|
||||
extern const struct option options[46];
|
||||
|
||||
void reset_params(void) {
|
||||
clear_params();
|
||||
params = default_params;
|
||||
}
|
||||
|
||||
static struct desync_params *add_group(struct desync_params *prev)
|
||||
{
|
||||
struct desync_params *dp = calloc(1, sizeof(*prev));
|
||||
if (!dp) {
|
||||
return 0;
|
||||
}
|
||||
if (prev) {
|
||||
dp->prev = prev;
|
||||
prev->next = dp;
|
||||
}
|
||||
params.dp_n++;
|
||||
return dp;
|
||||
}
|
||||
|
||||
int parse_args(int argc, char **argv)
|
||||
{
|
||||
int optc = sizeof(options)/sizeof(*options);
|
||||
for (int i = 0, e = optc; i < e; i++)
|
||||
optc += options[i].has_arg;
|
||||
|
||||
char opt[optc + 1];
|
||||
opt[optc] = 0;
|
||||
|
||||
for (int i = 0, o = 0; o < optc; i++, o++) {
|
||||
opt[o] = options[i].val;
|
||||
for (int c = options[i].has_arg; c; c--) {
|
||||
o++;
|
||||
opt[o] = ':';
|
||||
}
|
||||
}
|
||||
//
|
||||
params.laddr.in.sin_port = htons(1080);
|
||||
if (!ipv6_support()) {
|
||||
params.baddr.sa.sa_family = AF_INET;
|
||||
}
|
||||
|
||||
const char *pid_file = 0;
|
||||
bool daemonize = 0;
|
||||
|
||||
int rez;
|
||||
int invalid = 0;
|
||||
|
||||
long val = 0;
|
||||
char *end = 0;
|
||||
bool all_limited = 1;
|
||||
|
||||
int curr_optind = 1;
|
||||
|
||||
struct desync_params *dp = add_group(0);
|
||||
if (!dp) {
|
||||
reset_params();
|
||||
return -1;
|
||||
}
|
||||
params.dp = dp;
|
||||
|
||||
while (!invalid && (rez = getopt_long(
|
||||
argc, argv, opt, options, 0)) != -1) {
|
||||
switch (rez) {
|
||||
|
||||
case 'N':
|
||||
params.resolve = 0;
|
||||
break;
|
||||
case 'X':
|
||||
params.ipv6 = 0;
|
||||
break;
|
||||
case 'U':
|
||||
params.udp = 0;
|
||||
break;
|
||||
case 'G':
|
||||
params.http_connect = 1;
|
||||
break;
|
||||
#ifdef __linux__
|
||||
case 'E':
|
||||
params.transparent = 1;
|
||||
break;
|
||||
#endif
|
||||
|
||||
#ifdef DAEMON
|
||||
case 'D':
|
||||
daemonize = 1;
|
||||
break;
|
||||
|
||||
case 'w':
|
||||
pid_file = optarg;
|
||||
break;
|
||||
#endif
|
||||
case 'h':
|
||||
printf("%s", help_text);
|
||||
reset_params();
|
||||
return 0;
|
||||
case 'v':
|
||||
printf("%s\n", VERSION);
|
||||
reset_params();
|
||||
return 0;
|
||||
|
||||
case 'i':
|
||||
if (get_addr(optarg, ¶ms.laddr) < 0)
|
||||
invalid = 1;
|
||||
break;
|
||||
|
||||
case 'p':
|
||||
val = strtol(optarg, &end, 0);
|
||||
if (val <= 0 || val > 0xffff || *end)
|
||||
invalid = 1;
|
||||
else
|
||||
params.laddr.in.sin_port = htons(val);
|
||||
break;
|
||||
|
||||
case 'I':
|
||||
if (get_addr(optarg, ¶ms.baddr) < 0)
|
||||
invalid = 1;
|
||||
break;
|
||||
|
||||
case 'b':
|
||||
val = strtol(optarg, &end, 0);
|
||||
if (val <= 0 || val > INT_MAX/4 || *end)
|
||||
invalid = 1;
|
||||
else
|
||||
params.bfsize = val;
|
||||
break;
|
||||
|
||||
case 'c':
|
||||
val = strtol(optarg, &end, 0);
|
||||
if (val <= 0 || val >= (0xffff/2) || *end)
|
||||
invalid = 1;
|
||||
else
|
||||
params.max_open = val;
|
||||
break;
|
||||
|
||||
case 'x': //
|
||||
params.debug = strtol(optarg, 0, 0);
|
||||
if (params.debug < 0)
|
||||
invalid = 1;
|
||||
break;
|
||||
|
||||
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;
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
extern struct params default_params;
|
||||
|
||||
void reset_params(void);
|
||||
int parse_args(int argc, char **argv);
|
||||
bool ipv6_support(void);
|
||||
@@ -22,6 +22,7 @@ import androidx.compose.material.icons.filled.Home
|
||||
import androidx.compose.material.icons.filled.Lan
|
||||
import androidx.compose.material.icons.filled.MultipleStop
|
||||
import androidx.compose.material.icons.filled.Settings
|
||||
import androidx.compose.material.icons.filled.SettingsInputComposite
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.NavigationBar
|
||||
@@ -46,14 +47,18 @@ import androidx.navigation.compose.composable
|
||||
import androidx.navigation.compose.currentBackStackEntryAsState
|
||||
import androidx.navigation.compose.rememberNavController
|
||||
import androidx.navigation.navArgument
|
||||
import com.cherret.zaprett.ui.screen.DebugScreen
|
||||
import com.cherret.zaprett.ui.screen.HomeScreen
|
||||
import com.cherret.zaprett.ui.screen.HostsScreen
|
||||
import com.cherret.zaprett.ui.screen.IpsetsScreen
|
||||
import com.cherret.zaprett.ui.screen.RepoScreen
|
||||
import com.cherret.zaprett.ui.screen.SettingsScreen
|
||||
import com.cherret.zaprett.ui.screen.StrategyScreen
|
||||
import com.cherret.zaprett.ui.screen.StrategySelectionScreen
|
||||
import com.cherret.zaprett.ui.theme.ZaprettTheme
|
||||
import com.cherret.zaprett.ui.viewmodel.HomeViewModel
|
||||
import com.cherret.zaprett.ui.viewmodel.HostRepoViewModel
|
||||
import com.cherret.zaprett.ui.viewmodel.IpsetRepoViewModel
|
||||
import com.cherret.zaprett.ui.viewmodel.StrategyRepoViewModel
|
||||
import com.cherret.zaprett.utils.checkModuleInstallation
|
||||
import com.google.firebase.Firebase
|
||||
@@ -64,10 +69,11 @@ sealed class Screen(val route: String, @StringRes val nameResId: Int, val icon:
|
||||
object home : Screen("home", R.string.title_home, Icons.Default.Home)
|
||||
object hosts : Screen("hosts", R.string.title_hosts, Icons.Default.Lan)
|
||||
object strategies : Screen("strategies", R.string.title_strategies, Icons.Default.MultipleStop)
|
||||
object ipsets : Screen("ipsets", R.string.title_ipset, Icons.Default.SettingsInputComposite)
|
||||
object settings : Screen("settings", R.string.title_settings, Icons.Default.Settings)
|
||||
}
|
||||
val topLevelRoutes = listOf(Screen.home, Screen.hosts, Screen.strategies, Screen.settings)
|
||||
val hideNavBar = listOf("repo?source={source}")
|
||||
val topLevelRoutes = listOf(Screen.home, Screen.hosts, Screen.strategies, Screen.ipsets, Screen.settings)
|
||||
val hideNavBar = listOf("repo?source={source}", "debugScreen", "selectionScreen")
|
||||
class MainActivity : ComponentActivity() {
|
||||
private val viewModel: HomeViewModel by viewModels()
|
||||
private lateinit var notificationPermissionLauncher: ActivityResultLauncher<String>
|
||||
@@ -122,7 +128,7 @@ class MainActivity : ComponentActivity() {
|
||||
)
|
||||
}
|
||||
var showWelcomeDialog by remember { mutableStateOf(sharedPreferences.getBoolean("welcome_dialog", true)) }
|
||||
firebaseAnalytics.setAnalyticsCollectionEnabled(sharedPreferences.getBoolean("send_firebase_analytics", true))
|
||||
firebaseAnalytics.setAnalyticsCollectionEnabled(sharedPreferences.getBoolean("send_firebase_analytics", BuildConfig.send_firebase_analytics))
|
||||
BottomBar()
|
||||
if (showStoragePermissionDialog) {
|
||||
PermissionDialog(
|
||||
@@ -189,7 +195,7 @@ class MainActivity : ComponentActivity() {
|
||||
contentDescription = stringResource(id = topLevelRoute.nameResId)
|
||||
)
|
||||
},
|
||||
label = { Text(text = stringResource(id = topLevelRoute.nameResId)) },
|
||||
label = { Text(text = stringResource(id = topLevelRoute.nameResId)) }, alwaysShowLabel = false,
|
||||
selected = currentDestination?.route == topLevelRoute.route,
|
||||
onClick = {
|
||||
navController.navigate(topLevelRoute.route) {
|
||||
@@ -214,7 +220,8 @@ class MainActivity : ComponentActivity() {
|
||||
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(Screen.ipsets.route) { IpsetsScreen(navController) }
|
||||
composable(Screen.settings.route) { SettingsScreen(navController) }
|
||||
composable(route = "repo?source={source}",arguments = listOf(navArgument("source") {})) { backStackEntry ->
|
||||
val source = backStackEntry.arguments?.getString("source")
|
||||
when (source) {
|
||||
@@ -222,12 +229,18 @@ class MainActivity : ComponentActivity() {
|
||||
val viewModel: HostRepoViewModel = androidx.lifecycle.viewmodel.compose.viewModel()
|
||||
RepoScreen(navController, viewModel)
|
||||
}
|
||||
"ipsets" -> {
|
||||
val viewModel: IpsetRepoViewModel = androidx.lifecycle.viewmodel.compose.viewModel()
|
||||
RepoScreen(navController, viewModel)
|
||||
}
|
||||
"strategies" -> {
|
||||
val viewModel: StrategyRepoViewModel = androidx.lifecycle.viewmodel.compose.viewModel()
|
||||
RepoScreen(navController, viewModel)
|
||||
}
|
||||
}
|
||||
}
|
||||
composable("debugScreen") { DebugScreen(navController) }
|
||||
composable("selectionScreen") { StrategySelectionScreen(navController, vpnPermissionLauncher) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,8 +14,15 @@ import androidx.core.app.NotificationCompat
|
||||
import com.cherret.zaprett.MainActivity
|
||||
import com.cherret.zaprett.R
|
||||
import com.cherret.zaprett.data.ServiceStatus
|
||||
import com.cherret.zaprett.utils.disableList
|
||||
import com.cherret.zaprett.utils.getActiveByeDPIStrategyContent
|
||||
import com.cherret.zaprett.utils.getActiveExcludeIpsets
|
||||
import com.cherret.zaprett.utils.getActiveExcludeLists
|
||||
import com.cherret.zaprett.utils.getActiveIpsets
|
||||
import com.cherret.zaprett.utils.getActiveLists
|
||||
import com.cherret.zaprett.utils.getActiveStrategy
|
||||
import com.cherret.zaprett.utils.getAppsListMode
|
||||
import com.cherret.zaprett.utils.getHostListMode
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
@@ -171,7 +178,7 @@ class ByeDpiVpnService : VpnService() {
|
||||
try {
|
||||
vpnInterface?.close()
|
||||
vpnInterface = null
|
||||
NativeBridge().stopProxy()
|
||||
NativeBridge().jniStopProxy()
|
||||
TProxyService.TProxyStopService()
|
||||
status = ServiceStatus.Disconnected
|
||||
} catch (e: Exception) {
|
||||
@@ -182,10 +189,17 @@ class ByeDpiVpnService : VpnService() {
|
||||
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()
|
||||
val listSet = if (getHostListMode(sharedPreferences) == "whitelist") getActiveLists(sharedPreferences) else getActiveExcludeLists(sharedPreferences)
|
||||
val ipsetSet = if (getHostListMode(sharedPreferences) == "whitelist") getActiveIpsets(sharedPreferences) else getActiveExcludeIpsets(sharedPreferences)
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
val args = parseArgs(socksIp, socksPort, getActiveStrategy(sharedPreferences), prepareList(listSet))
|
||||
val result = NativeBridge().startProxy(args)
|
||||
val args = parseArgs(
|
||||
socksIp,
|
||||
socksPort,
|
||||
getActiveByeDPIStrategyContent(sharedPreferences),
|
||||
prepareList(listSet),
|
||||
prepareIpset(ipsetSet),
|
||||
sharedPreferences)
|
||||
val result = NativeBridge().jniStartProxy(args)
|
||||
if (result < 0) {
|
||||
Log.d("proxy","Failed to start byedpi proxy")
|
||||
} else {
|
||||
@@ -193,7 +207,7 @@ class ByeDpiVpnService : VpnService() {
|
||||
}
|
||||
}
|
||||
}
|
||||
private suspend fun prepareList(actlists: Set<String>): String {
|
||||
private suspend fun prepareList(actlists: Array<String>): String {
|
||||
if (actlists.isNotEmpty()) {
|
||||
val lists: Array<File> = actlists.map { File(it) }.toTypedArray()
|
||||
val hostlist = withContext(Dispatchers.IO) {
|
||||
@@ -202,10 +216,14 @@ class ByeDpiVpnService : VpnService() {
|
||||
withContext(Dispatchers.IO) {
|
||||
hostlist.printWriter().use { out ->
|
||||
lists.forEach {
|
||||
it.bufferedReader().useLines {
|
||||
it.forEach {
|
||||
out.println(it)
|
||||
if (it.exists()) {
|
||||
it.bufferedReader().useLines {
|
||||
it.forEach {
|
||||
out.println(it)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
disableList(it.name, sharedPreferences)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -215,15 +233,58 @@ class ByeDpiVpnService : VpnService() {
|
||||
return ""
|
||||
}
|
||||
|
||||
private fun parseArgs(ip: String, port: String, rawArgs: List<String>, list : String): Array<String> {
|
||||
private suspend fun prepareIpset(actsets: Array<String>): String {
|
||||
if (actsets.isNotEmpty()) {
|
||||
val lists: Array<File> = actsets.map { File(it) }.toTypedArray()
|
||||
val hostlist = withContext(Dispatchers.IO) {
|
||||
File.createTempFile("ipset", ".txt", cacheDir)
|
||||
}.apply { deleteOnExit() }
|
||||
withContext(Dispatchers.IO) {
|
||||
hostlist.printWriter().use { out ->
|
||||
lists.forEach {
|
||||
if (it.exists()) {
|
||||
it.bufferedReader().useLines {
|
||||
it.forEach {
|
||||
out.println(it)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
disableList(it.name, sharedPreferences)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return hostlist.absolutePath
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
private fun parseArgs(ip: String, port: String, rawArgs: List<String>, list : String, ipset : String, sharedPreferences: SharedPreferences): Array<String> {
|
||||
val regex = Regex("""--?\S+(?:=(?:[^"'\s]+|"[^"]*"|'[^']*'))?|[^\s]+""")
|
||||
val parsedArgs = rawArgs
|
||||
.flatMap { args -> regex.findAll(args).map { it.value } }
|
||||
.flatMap { arg ->
|
||||
when {
|
||||
arg == "\$hostlist" && list.isNotEmpty() -> listOf("-H", list)
|
||||
arg == "\$hostlist" && list.isEmpty() -> emptyList()
|
||||
else -> listOf(arg)
|
||||
if (getHostListMode(sharedPreferences) == "whitelist") {
|
||||
when {
|
||||
arg == "\$hostlist" && list.isNotEmpty() -> listOf("-H", list)
|
||||
arg == "\$hostlist" && list.isEmpty() -> emptyList()
|
||||
arg == "\$ipset" && list.isNotEmpty() -> listOf("-H", list)
|
||||
arg == "\$ipset" && list.isEmpty() -> emptyList()
|
||||
else -> listOf(arg)
|
||||
}
|
||||
} else {
|
||||
if (list.isNotEmpty()) {
|
||||
listOf("-H", list, "-An", arg).filter { it != "\$hostlist" }
|
||||
} else {
|
||||
listOf("-An", arg).filter { it != "\$hostlist" }
|
||||
}
|
||||
if (ipset.isEmpty()) {
|
||||
listOf("-H", list, "-An", arg).filter { it != "\$ipset" }
|
||||
|
||||
}
|
||||
else {
|
||||
listOf("-An", arg).filter { it != "\$ipset" }
|
||||
}
|
||||
}
|
||||
}
|
||||
.toMutableList()
|
||||
|
||||
@@ -6,16 +6,7 @@ class NativeBridge {
|
||||
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
|
||||
external fun jniStartProxy(args: Array<String>): Int
|
||||
external fun jniStopProxy(): Int
|
||||
}
|
||||
5
app/src/main/java/com/cherret/zaprett/data/ItemType.kt
Normal file
5
app/src/main/java/com/cherret/zaprett/data/ItemType.kt
Normal file
@@ -0,0 +1,5 @@
|
||||
package com.cherret.zaprett.data
|
||||
|
||||
enum class ItemType {
|
||||
byedpi, nfqws, list, list_exclude, ipset, ipset_exclude
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package com.cherret.zaprett.data
|
||||
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.Help
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import com.cherret.zaprett.R
|
||||
|
||||
data class ServiceStatusUI(
|
||||
val textRes: Int = R.string.status_not_availible,
|
||||
val icon: ImageVector = Icons.AutoMirrored.Filled.Help
|
||||
)
|
||||
7
app/src/main/java/com/cherret/zaprett/data/Setting.kt
Normal file
7
app/src/main/java/com/cherret/zaprett/data/Setting.kt
Normal file
@@ -0,0 +1,7 @@
|
||||
package com.cherret.zaprett.data
|
||||
|
||||
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()
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package com.cherret.zaprett.data
|
||||
|
||||
data class StrategyCheckResult (
|
||||
val path : String,
|
||||
val progress : Float,
|
||||
var domains: List<String>,
|
||||
val status : StrategyTestingStatus,
|
||||
)
|
||||
@@ -0,0 +1,7 @@
|
||||
package com.cherret.zaprett.data
|
||||
|
||||
import com.cherret.zaprett.R
|
||||
|
||||
enum class StrategyTestingStatus(val resId: Int) {
|
||||
Waiting(R.string.strategy_status_waiting), Testing(R.string.strategy_status_testing), Completed(R.string.strategy_status_tested)
|
||||
}
|
||||
308
app/src/main/java/com/cherret/zaprett/ui/component/ListItems.kt
Normal file
308
app/src/main/java/com/cherret/zaprett/ui/component/ListItems.kt
Normal file
@@ -0,0 +1,308 @@
|
||||
package com.cherret.zaprett.ui.component
|
||||
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.expandVertically
|
||||
import androidx.compose.animation.shrinkVertically
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.heightIn
|
||||
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.Check
|
||||
import androidx.compose.material.icons.filled.Delete
|
||||
import androidx.compose.material.icons.filled.InstallMobile
|
||||
import androidx.compose.material.icons.filled.Update
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.CardDefaults
|
||||
import androidx.compose.material3.ElevatedCard
|
||||
import androidx.compose.material3.FilledTonalButton
|
||||
import androidx.compose.material3.FilledTonalIconButton
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.LinearProgressIndicator
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.ProgressIndicatorDefaults
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.material3.Switch
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.cherret.zaprett.R
|
||||
import com.cherret.zaprett.data.StrategyCheckResult
|
||||
import com.cherret.zaprett.data.StrategyTestingStatus
|
||||
import com.cherret.zaprett.ui.viewmodel.BaseRepoViewModel
|
||||
import com.cherret.zaprett.utils.RepoItemInfo
|
||||
import com.cherret.zaprett.utils.disableStrategy
|
||||
import com.cherret.zaprett.utils.enableStrategy
|
||||
import com.cherret.zaprett.utils.getActiveStrategy
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@Composable
|
||||
fun ListSwitchItem(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
|
||||
fun RepoItem(
|
||||
item: RepoItemInfo,
|
||||
viewModel: BaseRepoViewModel,
|
||||
isInstalling: Map<String, Boolean>,
|
||||
isUpdateInstalling: Map<String, Boolean>,
|
||||
isUpdate: Map<String, Boolean>,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
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)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Composable
|
||||
fun StrategySelectionItem(strategy : StrategyCheckResult, prefs : SharedPreferences, context : Context, snackbarHostState : SnackbarHostState) {
|
||||
val scope = rememberCoroutineScope()
|
||||
var expanded by remember { mutableStateOf(false) }
|
||||
ElevatedCard (
|
||||
elevation = CardDefaults.cardElevation(defaultElevation = 6.dp),
|
||||
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceContainer),
|
||||
onClick = {
|
||||
if (strategy.status == StrategyTestingStatus.Completed && strategy.domains.isNotEmpty()) {
|
||||
expanded = !expanded
|
||||
}
|
||||
},
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(start = 10.dp, end = 10.dp, top = 25.dp, bottom = 0.dp)
|
||||
) {
|
||||
Column (
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp)
|
||||
)
|
||||
{
|
||||
Row {
|
||||
Text(
|
||||
text = strategy.path,
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
)
|
||||
FilledTonalIconButton(
|
||||
onClick = {
|
||||
if (getActiveStrategy(prefs).isNotEmpty()) disableStrategy(getActiveStrategy(prefs)[0], prefs)
|
||||
enableStrategy(strategy.path, prefs)
|
||||
scope.launch {
|
||||
snackbarHostState.showSnackbar(
|
||||
message = context.getString(R.string.strategy_applied)
|
||||
)
|
||||
}
|
||||
},
|
||||
enabled = strategy.status == StrategyTestingStatus.Completed
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Check,
|
||||
contentDescription = "apply"
|
||||
)
|
||||
}
|
||||
}
|
||||
Row {
|
||||
Text(
|
||||
text = stringResource(strategy.status.resId),
|
||||
modifier = Modifier
|
||||
.weight(1f),
|
||||
fontSize = 12.sp,
|
||||
)
|
||||
}
|
||||
Row (
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(top = 8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.End
|
||||
) {
|
||||
LinearProgressIndicator(
|
||||
modifier = Modifier
|
||||
.weight(1f),
|
||||
|
||||
progress = {
|
||||
strategy.progress
|
||||
},
|
||||
color = ProgressIndicatorDefaults.linearColor,
|
||||
trackColor = ProgressIndicatorDefaults.linearTrackColor,
|
||||
strokeCap = ProgressIndicatorDefaults.LinearStrokeCap,
|
||||
)
|
||||
Text(
|
||||
text = "${(strategy.progress*100).toInt()}%",
|
||||
modifier = Modifier
|
||||
.padding(start = 16.dp),
|
||||
|
||||
)
|
||||
}
|
||||
}
|
||||
AnimatedVisibility(
|
||||
visible = expanded,
|
||||
enter = expandVertically(),
|
||||
exit = shrinkVertically()
|
||||
) {
|
||||
Card (
|
||||
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceContainer),
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 8.dp)
|
||||
.fillMaxWidth()
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.selection_available_domains)
|
||||
)
|
||||
LazyColumn(modifier = Modifier.heightIn(max = 300.dp)) {
|
||||
items(strategy.domains) { item ->
|
||||
Card(
|
||||
elevation = CardDefaults.cardElevation(4.dp),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 4.dp)
|
||||
) {
|
||||
Text(
|
||||
text = item,
|
||||
modifier = Modifier.padding(16.dp),
|
||||
style = MaterialTheme.typography.bodyLarge
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
package com.cherret.zaprett.ui.component
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowForward
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.CardDefaults
|
||||
import androidx.compose.material3.ElevatedCard
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Switch
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.material3.TextField
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
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.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.cherret.zaprett.R
|
||||
|
||||
@Composable
|
||||
fun SettingsItem(title: String, checked: Boolean, onToggle: (Boolean) -> Unit, onCheckedChange: (Boolean) -> Unit) {
|
||||
ElevatedCard(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(80.dp),
|
||||
elevation = CardDefaults.cardElevation(defaultElevation = 4.dp),
|
||||
onClick = { onToggle(!checked) },
|
||||
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
|
||||
fun SettingsActionItem(title: String, onClick: () -> Unit) {
|
||||
ElevatedCard(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(80.dp),
|
||||
elevation = CardDefaults.cardElevation(defaultElevation = 4.dp),
|
||||
onClick = { onClick() },
|
||||
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 = null)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SettingsSection(title: String) {
|
||||
Text(
|
||||
text = title,
|
||||
fontSize = 20.sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
modifier = Modifier.padding(vertical = 8.dp, horizontal = 4.dp)
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
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
|
||||
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))
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
118
app/src/main/java/com/cherret/zaprett/ui/screen/DebugScreen.kt
Normal file
118
app/src/main/java/com/cherret/zaprett/ui/screen/DebugScreen.kt
Normal file
@@ -0,0 +1,118 @@
|
||||
package com.cherret.zaprett.ui.screen
|
||||
|
||||
import android.content.Context
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
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.MoreVert
|
||||
import androidx.compose.material3.DropdownMenu
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
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.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.navigation.NavController
|
||||
import com.cherret.zaprett.R
|
||||
import com.cherret.zaprett.data.Setting
|
||||
import com.cherret.zaprett.ui.component.SettingsActionItem
|
||||
import com.cherret.zaprett.ui.component.SettingsItem
|
||||
import com.cherret.zaprett.ui.component.SettingsSection
|
||||
import com.cherret.zaprett.ui.component.TextDialog
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun DebugScreen(navController: NavController) {
|
||||
val context = LocalContext.current
|
||||
val sharedPreferences = remember { context.getSharedPreferences("settings", Context.MODE_PRIVATE) }
|
||||
val editor = remember { sharedPreferences.edit() }
|
||||
val showUpdateUrlDialog = remember { mutableStateOf(false) }
|
||||
val textDialogValue = remember { mutableStateOf("") }
|
||||
val settingsList = listOf(
|
||||
Setting.Action(
|
||||
title = stringResource(R.string.btn_update_repository_url),
|
||||
onClick = {
|
||||
textDialogValue.value = sharedPreferences.getString("update_repo_url", "https://raw.githubusercontent.com/CherretGit/zaprett-app/refs/heads/main/update.json")?: "https://raw.githubusercontent.com/CherretGit/zaprett-app/refs/heads/main/update.json"
|
||||
showUpdateUrlDialog.value = true
|
||||
}
|
||||
),
|
||||
)
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = {
|
||||
Text(
|
||||
text = stringResource(R.string.title_debug),
|
||||
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 ->
|
||||
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 -> {
|
||||
SettingsActionItem(
|
||||
title = setting.title,
|
||||
setting.onClick
|
||||
)
|
||||
}
|
||||
is Setting.Section -> {
|
||||
SettingsSection(setting.title)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
if (showUpdateUrlDialog.value) {
|
||||
TextDialog(stringResource(R.string.btn_update_repository_url), stringResource(R.string.hint_enter_update_repository_url), textDialogValue.value, onConfirm = {
|
||||
editor.putString("update_repo_url", it).apply()
|
||||
}, onDismiss = { showUpdateUrlDialog.value = false })
|
||||
}
|
||||
}
|
||||
@@ -67,7 +67,9 @@ 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.data.ServiceStatusUI
|
||||
import com.cherret.zaprett.ui.viewmodel.HomeViewModel
|
||||
import dev.jeziellago.compose.markdowntext.MarkdownText
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@@ -78,8 +80,7 @@ fun HomeScreen(viewModel: HomeViewModel = viewModel(), vpnLauncher: ActivityResu
|
||||
val requestVpnPermission by viewModel.requestVpnPermission.collectAsState()
|
||||
val scope = rememberCoroutineScope()
|
||||
val snackbarHostState = remember { SnackbarHostState() }
|
||||
val cardText = viewModel.cardText
|
||||
val cardIcon = viewModel.cardIcon;
|
||||
val status by viewModel.serviceStatus.collectAsState()
|
||||
val changeLog = viewModel.changeLog
|
||||
val newVersion = viewModel.newVersion
|
||||
val updateAvailable = viewModel.updateAvailable
|
||||
@@ -124,7 +125,7 @@ fun HomeScreen(viewModel: HomeViewModel = viewModel(), vpnLauncher: ActivityResu
|
||||
Column(modifier = Modifier
|
||||
.padding(paddingValues)
|
||||
.verticalScroll(rememberScrollState())) {
|
||||
ServiceStatusCard(viewModel, cardText, cardIcon, snackbarHostState, scope)
|
||||
ServiceStatusCard(viewModel, status, snackbarHostState, scope)
|
||||
UpdateCard(updateAvailable) { viewModel.showUpdateDialog() }
|
||||
if (showUpdateDialog) {
|
||||
UpdateDialog(viewModel, changeLog.value.orEmpty(), newVersion) { viewModel.dismissUpdateDialog() }
|
||||
@@ -142,7 +143,7 @@ fun HomeScreen(viewModel: HomeViewModel = viewModel(), vpnLauncher: ActivityResu
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ServiceStatusCard(viewModel: HomeViewModel, cardText: MutableState<Int>, cardIcon : MutableState<ImageVector>, snackbarHostState: SnackbarHostState, scope: CoroutineScope) {
|
||||
private fun ServiceStatusCard(viewModel: HomeViewModel, status: ServiceStatusUI, snackbarHostState: SnackbarHostState, scope: CoroutineScope) {
|
||||
ElevatedCard(
|
||||
elevation = CardDefaults.cardElevation(6.dp),
|
||||
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.primary),
|
||||
@@ -162,14 +163,14 @@ private fun ServiceStatusCard(viewModel: HomeViewModel, cardText: MutableState<I
|
||||
)
|
||||
{
|
||||
Icon(
|
||||
painter = rememberVectorPainter(cardIcon.value),
|
||||
painter = rememberVectorPainter(status.icon),
|
||||
modifier = Modifier
|
||||
.width(60.dp)
|
||||
.height(60.dp),
|
||||
contentDescription = "icon"
|
||||
)
|
||||
Text(
|
||||
text = stringResource(cardText.value),
|
||||
text = stringResource(status.textRes),
|
||||
fontFamily = FontFamily(Font(R.font.unbounded, FontWeight.Normal)),
|
||||
fontSize = 16.sp,
|
||||
//maxLines = 3,
|
||||
@@ -195,9 +196,7 @@ private fun UpdateCard(updateAvailable: MutableState<Boolean>, onClick: () -> Un
|
||||
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceVariant),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(start = 10.dp, top = 10.dp, end = 10.dp)
|
||||
.width(140.dp)
|
||||
.height(70.dp),
|
||||
.padding(start = 10.dp, top = 10.dp, end = 10.dp),
|
||||
onClick = onClick
|
||||
) {
|
||||
Text(
|
||||
@@ -321,8 +320,8 @@ fun UpdateDialog(viewModel: HomeViewModel, changeLog: String, newVersion: Mutabl
|
||||
AlertDialog(
|
||||
title = { Text(text = stringResource(R.string.update_available)) },
|
||||
text = {
|
||||
Text(
|
||||
text = stringResource(R.string.alert_version, BuildConfig.VERSION_NAME, newVersion.value.toString(), changeLog)
|
||||
MarkdownText(
|
||||
markdown = stringResource(R.string.alert_version, BuildConfig.VERSION_NAME, newVersion.value.toString(), changeLog)
|
||||
)
|
||||
},
|
||||
onDismissRequest = onDismiss,
|
||||
|
||||
@@ -1,37 +1,35 @@
|
||||
package com.cherret.zaprett.ui.screen
|
||||
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.ActivityResultLauncher
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.navigationBarsPadding
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Add
|
||||
import androidx.compose.material.icons.filled.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.SegmentedButton
|
||||
import androidx.compose.material3.SegmentedButtonDefaults
|
||||
import androidx.compose.material3.SingleChoiceSegmentedButtonRow
|
||||
import androidx.compose.material3.SnackbarHost
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.material3.Switch
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.material3.pulltorefresh.PullToRefreshBox
|
||||
@@ -50,18 +48,20 @@ 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.component.ListSwitchItem
|
||||
import com.cherret.zaprett.ui.viewmodel.HostsViewModel
|
||||
import com.cherret.zaprett.utils.getHostListMode
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun HostsScreen(navController: NavController, viewModel: HostsViewModel = viewModel()) {
|
||||
val context = LocalContext.current
|
||||
val prefs = context.getSharedPreferences("settings", Context.MODE_PRIVATE)
|
||||
val scope = rememberCoroutineScope()
|
||||
val snackbarHostState = remember { SnackbarHostState() }
|
||||
val allLists = viewModel.allItems
|
||||
@@ -70,7 +70,9 @@ fun HostsScreen(navController: NavController, viewModel: HostsViewModel = viewMo
|
||||
val filePickerLauncher = rememberLauncherForActivityResult(
|
||||
contract = ActivityResultContracts.OpenDocument()
|
||||
) { uri ->
|
||||
uri?.let { viewModel.copySelectedFile(context, "/lists", it) }
|
||||
uri?.let {
|
||||
if (getHostListMode(prefs) == "whitelist") viewModel.copySelectedFile(context, "/lists/include", it)
|
||||
else viewModel.copySelectedFile(context, "/lists/exclude", it) }
|
||||
}
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
@@ -99,9 +101,15 @@ fun HostsScreen(navController: NavController, viewModel: HostsViewModel = viewMo
|
||||
modifier = Modifier.fillMaxSize()
|
||||
) {
|
||||
LazyColumn(
|
||||
contentPadding = paddingValues,
|
||||
modifier = Modifier.fillMaxSize()
|
||||
contentPadding = PaddingValues(
|
||||
top = paddingValues.calculateTopPadding(),
|
||||
bottom = paddingValues.calculateBottomPadding() + 80.dp
|
||||
),
|
||||
modifier = Modifier.navigationBarsPadding().fillMaxSize()
|
||||
) {
|
||||
item {
|
||||
ListTypeChoose(viewModel, prefs)
|
||||
}
|
||||
when {
|
||||
allLists.isEmpty() -> {
|
||||
item {
|
||||
@@ -118,7 +126,7 @@ fun HostsScreen(navController: NavController, viewModel: HostsViewModel = viewMo
|
||||
}
|
||||
else -> {
|
||||
items(allLists) { item ->
|
||||
HostItem (
|
||||
ListSwitchItem (
|
||||
item = item,
|
||||
isChecked = checked[item] == true,
|
||||
onCheckedChange = { isChecked ->
|
||||
@@ -141,43 +149,7 @@ fun HostsScreen(navController: NavController, viewModel: HostsViewModel = viewMo
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun HostItem(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>>) {
|
||||
@@ -215,6 +187,38 @@ private fun FloatingMenu(navController: NavController, launcher: ActivityResultL
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Composable
|
||||
fun ListTypeChoose(viewModel: HostsViewModel, prefs : SharedPreferences) {
|
||||
val listType = remember { mutableStateOf(getHostListMode(prefs))}
|
||||
val options = listOf(stringResource(R.string.title_whitelist), stringResource(R.string.title_blacklist))
|
||||
val selectedIndex = if (listType.value == "whitelist") 0 else 1
|
||||
|
||||
SingleChoiceSegmentedButtonRow (
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp)
|
||||
) {
|
||||
options.forEachIndexed { index, label ->
|
||||
SegmentedButton(
|
||||
shape = SegmentedButtonDefaults.itemShape(
|
||||
index = index,
|
||||
count = options.size
|
||||
),
|
||||
onClick = {
|
||||
listType.value = if (index == 0) "whitelist" else "blacklist"
|
||||
viewModel.setListType(listType.value)
|
||||
},
|
||||
selected = index == selectedIndex,
|
||||
label = {
|
||||
Text(
|
||||
label
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
private fun addHost(launcher: ActivityResultLauncher<Array<String>>) {
|
||||
launcher.launch(arrayOf("text/plain"))
|
||||
}
|
||||
223
app/src/main/java/com/cherret/zaprett/ui/screen/IpsetsScreen.kt
Normal file
223
app/src/main/java/com/cherret/zaprett/ui/screen/IpsetsScreen.kt
Normal file
@@ -0,0 +1,223 @@
|
||||
package com.cherret.zaprett.ui.screen
|
||||
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.ActivityResultLauncher
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.navigationBarsPadding
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Add
|
||||
import androidx.compose.material.icons.filled.Download
|
||||
import androidx.compose.material.icons.filled.UploadFile
|
||||
import androidx.compose.material3.DropdownMenu
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.FloatingActionButton
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.SegmentedButton
|
||||
import androidx.compose.material3.SegmentedButtonDefaults
|
||||
import androidx.compose.material3.SingleChoiceSegmentedButtonRow
|
||||
import androidx.compose.material3.SnackbarHost
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.material3.pulltorefresh.PullToRefreshBox
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.Font
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import androidx.navigation.NavController
|
||||
import com.cherret.zaprett.R
|
||||
import com.cherret.zaprett.ui.component.ListSwitchItem
|
||||
import com.cherret.zaprett.ui.viewmodel.HostsViewModel
|
||||
import com.cherret.zaprett.ui.viewmodel.IpsetViewModel
|
||||
import com.cherret.zaprett.utils.getHostListMode
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun IpsetsScreen(navController: NavController, viewModel: IpsetViewModel = viewModel()) {
|
||||
val context = LocalContext.current
|
||||
val prefs = context.getSharedPreferences("settings", Context.MODE_PRIVATE)
|
||||
val scope = rememberCoroutineScope()
|
||||
val snackbarHostState = remember { SnackbarHostState() }
|
||||
val allLists = viewModel.allItems
|
||||
val checked = viewModel.checked
|
||||
val isRefreshing = viewModel.isRefreshing
|
||||
val filePickerLauncher = rememberLauncherForActivityResult(
|
||||
contract = ActivityResultContracts.OpenDocument()
|
||||
) { uri ->
|
||||
uri?.let {
|
||||
if (getHostListMode(prefs) == "whitelist") viewModel.copySelectedFile(context, "/ipset/include", it)
|
||||
else viewModel.copySelectedFile(context, "/ipset/exclude", it) }
|
||||
}
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
viewModel.refresh()
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = {
|
||||
Text(
|
||||
text = stringResource(R.string.title_ipset),
|
||||
fontSize = 40.sp,
|
||||
fontFamily = FontFamily(Font(R.font.unbounded, FontWeight.Normal))
|
||||
)
|
||||
},
|
||||
windowInsets = WindowInsets(0)
|
||||
)
|
||||
},
|
||||
content = { paddingValues ->
|
||||
PullToRefreshBox(
|
||||
isRefreshing = isRefreshing,
|
||||
onRefresh = {
|
||||
viewModel.refresh()
|
||||
},
|
||||
modifier = Modifier.fillMaxSize()
|
||||
) {
|
||||
LazyColumn(
|
||||
contentPadding = PaddingValues(
|
||||
top = paddingValues.calculateTopPadding(),
|
||||
bottom = paddingValues.calculateBottomPadding() + 80.dp
|
||||
),
|
||||
modifier = Modifier.navigationBarsPadding().fillMaxSize()
|
||||
) {
|
||||
item {
|
||||
IpsetTypeChoose(viewModel, prefs)
|
||||
}
|
||||
when {
|
||||
allLists.isEmpty() -> {
|
||||
item {
|
||||
Box(
|
||||
modifier = Modifier.fillParentMaxSize(),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text(
|
||||
stringResource(R.string.empty_list),
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
items(allLists) { item ->
|
||||
ListSwitchItem (
|
||||
item = item,
|
||||
isChecked = checked[item] == true,
|
||||
onCheckedChange = { isChecked ->
|
||||
viewModel.onCheckedChange(item, isChecked, snackbarHostState, scope)
|
||||
},
|
||||
onDeleteClick = {
|
||||
viewModel.deleteItem(item, snackbarHostState, scope)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
floatingActionButton = {
|
||||
FloatingMenu(navController, filePickerLauncher)
|
||||
},
|
||||
snackbarHost = { SnackbarHost(hostState = snackbarHostState) }
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
|
||||
@Composable
|
||||
private fun FloatingMenu(navController: NavController, launcher: ActivityResultLauncher<Array<String>>) {
|
||||
var expanded by remember { mutableStateOf(false) }
|
||||
FloatingActionButton(
|
||||
modifier = Modifier.size(80.dp),
|
||||
onClick = { expanded = !expanded }
|
||||
) {
|
||||
Icon(Icons.Default.Add, contentDescription = stringResource(R.string.btn_add_host))
|
||||
}
|
||||
DropdownMenu(
|
||||
expanded = expanded,
|
||||
onDismissRequest = { expanded = false }
|
||||
) {
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringResource(R.string.btn_download_host)) },
|
||||
onClick = {
|
||||
expanded = false
|
||||
navController.navigate("repo?source=ipsets") { launchSingleTop = true }
|
||||
},
|
||||
leadingIcon = {
|
||||
Icon(Icons.Default.Download, contentDescription = stringResource(R.string.btn_download_host))
|
||||
}
|
||||
)
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringResource(R.string.btn_add_host)) },
|
||||
onClick = {
|
||||
expanded = false
|
||||
addHost(launcher)
|
||||
},
|
||||
leadingIcon = {
|
||||
Icon(Icons.Default.UploadFile, contentDescription = stringResource(R.string.btn_add_host))
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
@Composable
|
||||
fun IpsetTypeChoose(viewModel: IpsetViewModel, prefs : SharedPreferences) {
|
||||
val listType = remember { mutableStateOf(getHostListMode(prefs))}
|
||||
val options = listOf(stringResource(R.string.title_whitelist), stringResource(R.string.title_blacklist))
|
||||
val selectedIndex = if (listType.value == "whitelist") 0 else 1
|
||||
|
||||
SingleChoiceSegmentedButtonRow (
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp)
|
||||
) {
|
||||
options.forEachIndexed { index, label ->
|
||||
SegmentedButton(
|
||||
shape = SegmentedButtonDefaults.itemShape(
|
||||
index = index,
|
||||
count = options.size
|
||||
),
|
||||
onClick = {
|
||||
listType.value = if (index == 0) "whitelist" else "blacklist"
|
||||
viewModel.setListType(listType.value)
|
||||
},
|
||||
selected = index == selectedIndex,
|
||||
label = {
|
||||
Text(
|
||||
label
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
private fun addHost(launcher: ActivityResultLauncher<Array<String>>) {
|
||||
launcher.launch(arrayOf("text/plain"))
|
||||
}
|
||||
@@ -1,36 +1,33 @@
|
||||
package com.cherret.zaprett.ui.screen
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import android.content.ClipData
|
||||
import android.content.ClipboardManager
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import android.widget.Toast
|
||||
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.layout.navigationBarsPadding
|
||||
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.AlertDialog
|
||||
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.TextButton
|
||||
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.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
@@ -40,12 +37,13 @@ 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.component.RepoItem
|
||||
import com.cherret.zaprett.ui.viewmodel.BaseRepoViewModel
|
||||
import kotlinx.serialization.SerializationException
|
||||
import java.io.IOException
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
@@ -57,11 +55,83 @@ fun RepoScreen(navController: NavController, viewModel: BaseRepoViewModel) {
|
||||
val isUpdateInstalling = viewModel.isUpdateInstalling
|
||||
val isRefreshing = viewModel.isRefreshing.value
|
||||
val snackbarHostState = remember { SnackbarHostState() }
|
||||
|
||||
val error by viewModel.errorFlow.collectAsState()
|
||||
val downloadError by viewModel.downloadErrorFlow.collectAsState()
|
||||
LaunchedEffect(Unit) {
|
||||
viewModel.refresh()
|
||||
}
|
||||
|
||||
if (error != null) {
|
||||
AlertDialog(
|
||||
onDismissRequest = {
|
||||
viewModel.clearError()
|
||||
navController.popBackStack()
|
||||
},
|
||||
title = { Text(stringResource(R.string.error_text)) },
|
||||
text = {
|
||||
Text(
|
||||
when (error) {
|
||||
is IOException -> stringResource(R.string.error_server_data)
|
||||
is SerializationException -> stringResource(R.string.error_processing_data)
|
||||
else -> stringResource(R.string.error_unknown)
|
||||
}
|
||||
)
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = {
|
||||
val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
||||
val clip: ClipData = ClipData.newPlainText("Error log", error?.message)
|
||||
clipboard.setPrimaryClip(clip)
|
||||
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.S) {
|
||||
Toast.makeText(context, context.getString(R.string.log_copied), Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}) {
|
||||
Text(stringResource(R.string.btn_copy_log))
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(onClick = {
|
||||
viewModel.clearError()
|
||||
navController.popBackStack()
|
||||
}) {
|
||||
Text(stringResource(R.string.btn_continue))
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
if (downloadError != null) {
|
||||
AlertDialog(
|
||||
onDismissRequest = {
|
||||
viewModel.clearDownloadError()
|
||||
},
|
||||
title = { Text(stringResource(R.string.error_text)) },
|
||||
text = {
|
||||
Text(stringResource(R.string.download_error))
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = {
|
||||
val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
||||
val clip: ClipData = ClipData.newPlainText("Error log", downloadError)
|
||||
clipboard.setPrimaryClip(clip)
|
||||
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.S) {
|
||||
Toast.makeText(context, context.getString(R.string.log_copied), Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}) {
|
||||
Text(stringResource(R.string.btn_copy_log))
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(onClick = {
|
||||
viewModel.clearDownloadError()
|
||||
}) {
|
||||
Text(stringResource(R.string.btn_continue))
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
@@ -91,7 +161,7 @@ fun RepoScreen(navController: NavController, viewModel: BaseRepoViewModel) {
|
||||
) {
|
||||
LazyColumn(
|
||||
contentPadding = paddingValues,
|
||||
modifier = Modifier.fillMaxSize()
|
||||
modifier = Modifier.navigationBarsPadding().fillMaxSize()
|
||||
) {
|
||||
if (hostLists.isEmpty()) {
|
||||
item {
|
||||
@@ -107,94 +177,13 @@ fun RepoScreen(navController: NavController, viewModel: BaseRepoViewModel) {
|
||||
}
|
||||
} 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)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
RepoItem(
|
||||
item = item,
|
||||
viewModel = viewModel,
|
||||
isInstalling = isInstalling,
|
||||
isUpdateInstalling = isUpdateInstalling,
|
||||
isUpdate = isUpdate
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,18 +3,55 @@ package com.cherret.zaprett.ui.screen
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.SharedPreferences
|
||||
import android.graphics.drawable.Drawable
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
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.height
|
||||
import androidx.compose.foundation.layout.heightIn
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.wrapContentHeight
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.selection.selectable
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowForward
|
||||
import androidx.compose.material.icons.filled.AttachMoney
|
||||
import androidx.compose.material.icons.filled.MoreVert
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.Checkbox
|
||||
import androidx.compose.material3.DropdownMenu
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.ModalBottomSheet
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.RadioButton
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Switch
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.material3.TextField
|
||||
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.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
@@ -29,14 +66,22 @@ import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.compose.ui.window.Dialog
|
||||
import androidx.core.content.edit
|
||||
import androidx.core.net.toUri
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import androidx.navigation.NavController
|
||||
import coil3.compose.AsyncImage
|
||||
import coil3.request.ImageRequest
|
||||
import com.cherret.zaprett.BuildConfig
|
||||
import com.cherret.zaprett.byedpi.ByeDpiVpnService
|
||||
import com.cherret.zaprett.R
|
||||
import com.cherret.zaprett.data.ServiceStatus
|
||||
import com.cherret.zaprett.byedpi.ByeDpiVpnService
|
||||
import com.cherret.zaprett.ui.component.SettingsActionItem
|
||||
import com.cherret.zaprett.ui.component.SettingsItem
|
||||
import com.cherret.zaprett.ui.component.SettingsSection
|
||||
import com.cherret.zaprett.data.AppListType
|
||||
import com.cherret.zaprett.data.ServiceStatus
|
||||
import com.cherret.zaprett.ui.component.InfoDialog
|
||||
import com.cherret.zaprett.data.Setting
|
||||
import com.cherret.zaprett.ui.component.TextDialog
|
||||
import com.cherret.zaprett.ui.viewmodel.SettingsViewModel
|
||||
import com.cherret.zaprett.utils.checkModuleInstallation
|
||||
import com.cherret.zaprett.utils.checkRoot
|
||||
@@ -45,24 +90,24 @@ import com.cherret.zaprett.utils.getStartOnBoot
|
||||
import com.cherret.zaprett.utils.setAppsListMode
|
||||
import com.cherret.zaprett.utils.setStartOnBoot
|
||||
import com.cherret.zaprett.utils.stopService
|
||||
import androidx.core.content.edit
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun SettingsScreen(viewModel : SettingsViewModel = viewModel()) {
|
||||
fun SettingsScreen(navController: NavController, viewModel : SettingsViewModel = viewModel()) {
|
||||
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 useModule = viewModel.useModule.collectAsState()
|
||||
val updateOnBoot = remember { mutableStateOf(sharedPreferences.getBoolean("update_on_boot", true)) }
|
||||
val autoRestart = viewModel.autoRestart.collectAsState()
|
||||
val autoUpdate = remember { mutableStateOf(sharedPreferences.getBoolean("auto_update", BuildConfig.auto_update)) }
|
||||
val sendFirebaseAnalytics = remember { mutableStateOf(sharedPreferences.getBoolean("send_firebase_analytics", BuildConfig.send_firebase_analytics)) }
|
||||
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 showIpsetRepoUrlDialog = remember { mutableStateOf(false) }
|
||||
val showStrategyRepoUrlDialog = remember { mutableStateOf(false) }
|
||||
val showIPDialog = remember { mutableStateOf(false) }
|
||||
val showPortDialog = remember { mutableStateOf(false) }
|
||||
@@ -72,6 +117,7 @@ fun SettingsScreen(viewModel : SettingsViewModel = viewModel()) {
|
||||
val showBlackDialog = remember { mutableStateOf(false) }
|
||||
val showAppsListsSheet = remember { mutableStateOf(false) }
|
||||
val showSystemApps = remember { mutableStateOf(sharedPreferences.getBoolean("show_system_apps", false)) }
|
||||
val showChangeProbeTimeout = remember { mutableStateOf(false) }
|
||||
|
||||
val settingsList = listOf(
|
||||
Setting.Section(stringResource(R.string.general_section)),
|
||||
@@ -79,18 +125,11 @@ fun SettingsScreen(viewModel : SettingsViewModel = viewModel()) {
|
||||
title = stringResource(R.string.btn_use_root),
|
||||
checked = useModule.value,
|
||||
onToggle = { isChecked ->
|
||||
useModule(
|
||||
viewModel.useModule(
|
||||
context = context,
|
||||
checked = isChecked,
|
||||
updateOnBoot = updateOnBoot,
|
||||
openNoRootDialog = openNoRootDialog,
|
||||
openNoModuleDialog = openNoModuleDialog
|
||||
) { success ->
|
||||
if (success) {
|
||||
useModule.value = isChecked
|
||||
if (!isChecked) stopService { }
|
||||
}
|
||||
}
|
||||
openNoModuleDialog = openNoModuleDialog)
|
||||
}
|
||||
),
|
||||
Setting.Toggle(
|
||||
@@ -120,20 +159,25 @@ fun SettingsScreen(viewModel : SettingsViewModel = viewModel()) {
|
||||
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"
|
||||
textDialogValue.value = sharedPreferences.getString("hosts_repo_url", "https://raw.githubusercontent.com/CherretGit/zaprett-repo/refs/heads/main/hosts.json") ?: "https://raw.githubusercontent.com/CherretGit/zaprett-repo/refs/heads/main/hosts.json"
|
||||
showHostsRepoUrlDialog.value = true
|
||||
}
|
||||
),
|
||||
Setting.Action(
|
||||
title = stringResource(R.string.ipset_repo_url),
|
||||
onClick = {
|
||||
textDialogValue.value = sharedPreferences.getString("ipset_repo_url", "https://raw.githubusercontent.com/CherretGit/zaprett-repo/refs/heads/main/ipsets.json") ?: "https://raw.githubusercontent.com/CherretGit/zaprett-repo/refs/heads/main/ipsets.json"
|
||||
showIpsetRepoUrlDialog.value = true
|
||||
}
|
||||
),
|
||||
Setting.Action(
|
||||
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"
|
||||
textDialogValue.value = sharedPreferences.getString("strategy_repo_url", "https://raw.githubusercontent.com/CherretGit/zaprett-repo/refs/heads/main/strategies.json") ?: "https://raw.githubusercontent.com/CherretGit/zaprett-repo/refs/heads/main/strategies.json"
|
||||
showStrategyRepoUrlDialog.value = true
|
||||
}
|
||||
),
|
||||
Setting.Section(
|
||||
title = stringResource(R.string.shared_section)
|
||||
),
|
||||
Setting.Section(title = stringResource(R.string.shared_section)),
|
||||
Setting.Action(
|
||||
title = stringResource(R.string.btn_applist),
|
||||
onClick = {
|
||||
@@ -152,6 +196,20 @@ fun SettingsScreen(viewModel : SettingsViewModel = viewModel()) {
|
||||
showBlackDialog.value = true
|
||||
}
|
||||
),
|
||||
Setting.Section(stringResource(R.string.title_selection)),
|
||||
Setting.Action(
|
||||
title = stringResource(R.string.begin_selection),
|
||||
onClick = {
|
||||
navController.navigate("selectionScreen")
|
||||
}
|
||||
),
|
||||
Setting.Action(
|
||||
title = stringResource(R.string.change_probe_timeout),
|
||||
onClick = {
|
||||
textDialogValue.value = sharedPreferences.getLong("probe_timeout", 1000L).toString()
|
||||
showChangeProbeTimeout.value = true
|
||||
}
|
||||
),
|
||||
Setting.Section(stringResource(R.string.byedpi_section)),
|
||||
Setting.Toggle(
|
||||
title = stringResource(R.string.btn_ipv6),
|
||||
@@ -182,14 +240,12 @@ fun SettingsScreen(viewModel : SettingsViewModel = viewModel()) {
|
||||
showDNSDialog.value = true
|
||||
}
|
||||
),
|
||||
Setting.Section(
|
||||
title = stringResource(R.string.zapret_section)
|
||||
),
|
||||
Setting.Section(title = stringResource(R.string.zapret_section)),
|
||||
Setting.Toggle(
|
||||
title = stringResource(R.string.btn_autorestart),
|
||||
checked = autoRestart.value,
|
||||
onToggle = {
|
||||
if (handleAutoRestart(context, it)) autoRestart.value = it
|
||||
viewModel.handleAutoRestart(context)
|
||||
}
|
||||
)
|
||||
)
|
||||
@@ -211,7 +267,7 @@ fun SettingsScreen(viewModel : SettingsViewModel = viewModel()) {
|
||||
}
|
||||
|
||||
if (showAboutDialog.value) {
|
||||
AboutDialog(onDismiss = { showAboutDialog.value = false })
|
||||
AboutDialog(navController, onDismiss = { showAboutDialog.value = false })
|
||||
}
|
||||
|
||||
if (showHostsRepoUrlDialog.value) {
|
||||
@@ -219,10 +275,15 @@ fun SettingsScreen(viewModel : SettingsViewModel = viewModel()) {
|
||||
editor.putString("hosts_repo_url", it).apply()
|
||||
}, onDismiss = { showHostsRepoUrlDialog.value = false })
|
||||
}
|
||||
if (showIpsetRepoUrlDialog.value) {
|
||||
TextDialog(stringResource(R.string.btn_repository_url_ipsets), stringResource(R.string.hint_enter_repository_url_ipsets), textDialogValue.value, onConfirm = {
|
||||
editor.putString("ipsets_repo_url", it).apply()
|
||||
}, onDismiss = { showIpsetRepoUrlDialog.value = false })
|
||||
}
|
||||
|
||||
if (showStrategyRepoUrlDialog.value) {
|
||||
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()
|
||||
editor.putString("strategies_repo_url", it).apply()
|
||||
}, onDismiss = { showStrategyRepoUrlDialog.value = false })
|
||||
}
|
||||
|
||||
@@ -280,6 +341,12 @@ fun SettingsScreen(viewModel : SettingsViewModel = viewModel()) {
|
||||
)
|
||||
}
|
||||
|
||||
if (showChangeProbeTimeout.value) {
|
||||
TextDialog(stringResource(R.string.probe_timeout), stringResource(R.string.hint_enter_probe_timeout), textDialogValue.value, onConfirm = {
|
||||
editor.putLong("probe_timeout", it.toLong()).apply()
|
||||
}, onDismiss = { showChangeProbeTimeout.value = false })
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
@@ -330,7 +397,7 @@ fun SettingsScreen(viewModel : SettingsViewModel = viewModel()) {
|
||||
)
|
||||
}
|
||||
is Setting.Action -> {
|
||||
SettingsTextItem(
|
||||
SettingsActionItem(
|
||||
title = setting.title,
|
||||
setting.onClick
|
||||
)
|
||||
@@ -345,59 +412,6 @@ fun SettingsScreen(viewModel : SettingsViewModel = viewModel()) {
|
||||
)
|
||||
}
|
||||
|
||||
@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 = null)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
private fun ListBottomSheet(
|
||||
@@ -480,125 +494,64 @@ private fun ListBottomSheet(
|
||||
}
|
||||
}
|
||||
|
||||
@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()
|
||||
editor.remove("applist").apply()
|
||||
editor.remove("whitelist").apply()
|
||||
editor.remove("blacklist").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) {
|
||||
private fun AboutDialog(navController: NavController, onDismiss: () -> Unit) {
|
||||
val context = LocalContext.current
|
||||
val interactionSource = remember { MutableInteractionSource() }
|
||||
var clickCount by remember { mutableIntStateOf(0) }
|
||||
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)) },
|
||||
text = {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Text(text = stringResource(R.string.about_text, BuildConfig.VERSION_NAME), modifier = Modifier.clickable(interactionSource = interactionSource, null) {
|
||||
clickCount++
|
||||
if (clickCount == 7) {
|
||||
onDismiss()
|
||||
navController.navigate("debugScreen") { launchSingleTop = true }
|
||||
}
|
||||
})
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.SpaceEvenly,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
IconButton(onClick = {
|
||||
val intent = Intent(Intent.ACTION_VIEW,
|
||||
"https://github.com/CherretGit/zaprett-app".toUri())
|
||||
context.startActivity(intent)
|
||||
}) {
|
||||
Icon(painterResource(R.drawable.github), "GitHub")
|
||||
}
|
||||
IconButton(onClick = {
|
||||
val intent = Intent(Intent.ACTION_VIEW,
|
||||
"https://t.me/zaprett_module".toUri())
|
||||
context.startActivity(intent)
|
||||
}) {
|
||||
Icon(painterResource(R.drawable.telegram), "Telegram")
|
||||
}
|
||||
IconButton(onClick = {
|
||||
val intent = Intent(Intent.ACTION_VIEW,
|
||||
"https://matrix.to/#/#zaprett-group:matrix.cherret.ru".toUri())
|
||||
context.startActivity(intent)
|
||||
}) {
|
||||
Icon(painterResource(R.drawable.matrix), "Matrix")
|
||||
}
|
||||
IconButton(onClick = {
|
||||
val intent = Intent(Intent.ACTION_VIEW,
|
||||
"https://pay.cloudtips.ru/p/672192fd".toUri())
|
||||
context.startActivity(intent)
|
||||
}) {
|
||||
Icon(Icons.Default.AttachMoney, "Donate")
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
onDismissRequest = onDismiss,
|
||||
confirmButton = { }
|
||||
)
|
||||
@@ -724,7 +677,11 @@ private fun ChooseAppsDialog(
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun AppItem(viewModel: SettingsViewModel, packageName : String, enabled : Boolean, onCheckedChange: (Boolean) -> Unit){
|
||||
private fun AppItem(viewModel: SettingsViewModel, packageName : String, enabled : Boolean, onCheckedChange: (Boolean) -> Unit) {
|
||||
var bitmap by remember { mutableStateOf<Drawable?>(null) }
|
||||
LaunchedEffect(packageName) {
|
||||
bitmap = viewModel.getAppIconBitmap(packageName)
|
||||
}
|
||||
Row (
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
@@ -733,7 +690,7 @@ private fun AppItem(viewModel: SettingsViewModel, packageName : String, enabled
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
AsyncImage(
|
||||
model = ImageRequest.Builder(LocalContext.current).data(viewModel.getAppIconBitmap(packageName)).build(),
|
||||
model = bitmap,
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.padding(4.dp)
|
||||
@@ -751,10 +708,4 @@ private fun AppItem(viewModel: SettingsViewModel, packageName : String, enabled
|
||||
onCheckedChange = onCheckedChange
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
@@ -4,35 +4,26 @@ 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.PaddingValues
|
||||
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.navigationBarsPadding
|
||||
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
|
||||
@@ -51,12 +42,12 @@ 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.component.ListSwitchItem
|
||||
import com.cherret.zaprett.ui.viewmodel.StrategyViewModel
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@@ -105,8 +96,11 @@ fun StrategyScreen(navController: NavController, viewModel: StrategyViewModel =
|
||||
modifier = Modifier.fillMaxSize()
|
||||
) {
|
||||
LazyColumn(
|
||||
contentPadding = paddingValues,
|
||||
modifier = Modifier.fillMaxSize()
|
||||
contentPadding = PaddingValues(
|
||||
top = paddingValues.calculateTopPadding(),
|
||||
bottom = paddingValues.calculateBottomPadding() + 80.dp
|
||||
),
|
||||
modifier = Modifier.navigationBarsPadding().fillMaxSize()
|
||||
) {
|
||||
when {
|
||||
allLists.isEmpty() -> {
|
||||
@@ -124,7 +118,7 @@ fun StrategyScreen(navController: NavController, viewModel: StrategyViewModel =
|
||||
}
|
||||
else -> {
|
||||
items(allLists) { item ->
|
||||
StrategyItem(
|
||||
ListSwitchItem(
|
||||
item = item,
|
||||
isChecked = checked[item] == true,
|
||||
onCheckedChange = { isChecked ->
|
||||
@@ -147,44 +141,6 @@ fun StrategyScreen(navController: NavController, viewModel: StrategyViewModel =
|
||||
)
|
||||
}
|
||||
|
||||
@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) }
|
||||
|
||||
@@ -0,0 +1,196 @@
|
||||
package com.cherret.zaprett.ui.screen
|
||||
|
||||
import android.content.Context.MODE_PRIVATE
|
||||
import android.content.Intent
|
||||
import android.net.VpnService
|
||||
import androidx.activity.result.ActivityResultLauncher
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.SnackbarHost
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.Font
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material.icons.filled.Info
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.FilledTonalButton
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import androidx.navigation.NavController
|
||||
import com.cherret.zaprett.R
|
||||
import com.cherret.zaprett.ui.component.StrategySelectionItem
|
||||
import com.cherret.zaprett.ui.viewmodel.StrategySelectionViewModel
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun StrategySelectionScreen(navController: NavController, vpnLauncher: ActivityResultLauncher<Intent>, viewModel : StrategySelectionViewModel = viewModel()){
|
||||
val snackbarHostState = remember { SnackbarHostState() }
|
||||
val strategyStates = viewModel.strategyStates
|
||||
val context = LocalContext.current
|
||||
val prefs = context.getSharedPreferences("settings", MODE_PRIVATE)
|
||||
var showDialog = remember { mutableStateOf(false) }
|
||||
val requestVpnPermission by viewModel.requestVpnPermission.collectAsState()
|
||||
|
||||
if (showDialog.value) {
|
||||
InfoAlert { showDialog.value = false }
|
||||
}
|
||||
|
||||
LaunchedEffect(requestVpnPermission) {
|
||||
if (requestVpnPermission) {
|
||||
val intent = VpnService.prepare(context)
|
||||
if (intent != null) {
|
||||
vpnLauncher.launch(intent)
|
||||
} else {
|
||||
viewModel.startVpn()
|
||||
viewModel.clearVpnPermissionRequest()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = {
|
||||
Text(
|
||||
text = stringResource(R.string.title_selection),
|
||||
fontSize = 30.sp,
|
||||
fontFamily = FontFamily(Font(R.font.unbounded, FontWeight.Normal))
|
||||
)
|
||||
},
|
||||
navigationIcon = {
|
||||
IconButton(onClick = { navController.popBackStack() }) {
|
||||
Icon(
|
||||
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
|
||||
contentDescription = stringResource(R.string.btn_back)
|
||||
)
|
||||
}
|
||||
},
|
||||
actions = {
|
||||
IconButton(
|
||||
onClick = {
|
||||
showDialog.value = true
|
||||
}
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Info,
|
||||
contentDescription = "info"
|
||||
)
|
||||
}
|
||||
},
|
||||
windowInsets = WindowInsets(0)
|
||||
)
|
||||
},
|
||||
snackbarHost = { SnackbarHost(snackbarHostState) },
|
||||
content = { paddingValues ->
|
||||
LazyColumn (
|
||||
modifier = Modifier
|
||||
.padding(paddingValues)
|
||||
) {
|
||||
item {
|
||||
Row (
|
||||
modifier = Modifier
|
||||
.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.Center
|
||||
)
|
||||
{
|
||||
NoHostsCard(viewModel.noHostsCard)
|
||||
FilledTonalButton(
|
||||
onClick = {
|
||||
viewModel.viewModelScope.launch {
|
||||
snackbarHostState.showSnackbar(
|
||||
context.getString(R.string.begin_selection_snack)
|
||||
)
|
||||
viewModel.performTest()
|
||||
}
|
||||
}
|
||||
) {
|
||||
Text(stringResource(R.string.begin_selection))
|
||||
}
|
||||
}
|
||||
}
|
||||
when {
|
||||
strategyStates.isEmpty() -> {
|
||||
item {
|
||||
Box(
|
||||
modifier = Modifier.fillParentMaxSize(),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text(
|
||||
stringResource(R.string.empty_list),
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
items(strategyStates, key = { it.path }) { item ->
|
||||
StrategySelectionItem(item, prefs, context, snackbarHostState)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun InfoAlert(onDismiss: () -> Unit) {
|
||||
AlertDialog(
|
||||
title = { Text(text = stringResource(R.string.strategy_selection_info_title)) },
|
||||
text = { Text(text = stringResource(R.string.strategy_selection_info_msg)) },
|
||||
onDismissRequest = onDismiss,
|
||||
confirmButton = {
|
||||
TextButton(onClick = onDismiss) {
|
||||
Text(stringResource(R.string.btn_continue))
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun NoHostsCard(noHostsCard: MutableState<Boolean>) {
|
||||
if (noHostsCard.value) {
|
||||
AlertDialog(
|
||||
title = { Text(text = stringResource(R.string.selection_no_hosts_title)) },
|
||||
text = { Text(text = stringResource(R.string.selection_no_hosts_message)) },
|
||||
onDismissRequest = {
|
||||
noHostsCard.value = false
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(onClick = { noHostsCard.value = false }) {
|
||||
Text(stringResource(R.string.btn_continue))
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.cherret.zaprett.ui.viewmodel
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Application
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
@@ -11,18 +12,29 @@ import androidx.lifecycle.AndroidViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.cherret.zaprett.utils.RepoItemInfo
|
||||
import com.cherret.zaprett.R
|
||||
import com.cherret.zaprett.data.ItemType
|
||||
import com.cherret.zaprett.utils.download
|
||||
import com.cherret.zaprett.utils.getFileSha256
|
||||
import com.cherret.zaprett.utils.getHostListMode
|
||||
import com.cherret.zaprett.utils.getZaprettPath
|
||||
import com.cherret.zaprett.utils.registerDownloadListenerHost
|
||||
import com.cherret.zaprett.utils.registerDownloadListener
|
||||
import com.cherret.zaprett.utils.restartService
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import java.io.File
|
||||
|
||||
abstract class BaseRepoViewModel(application: Application) : AndroidViewModel(application) {
|
||||
val context = application.applicationContext
|
||||
@SuppressLint("StaticFieldLeak")
|
||||
val context: Context = application.applicationContext
|
||||
val sharedPreferences: SharedPreferences = context.getSharedPreferences("settings", Context.MODE_PRIVATE)
|
||||
private val _errorFlow = MutableStateFlow<Throwable?>(null)
|
||||
val errorFlow: StateFlow<Throwable?> = _errorFlow
|
||||
|
||||
private val _downloadErrorFlow = MutableStateFlow<String?>(null)
|
||||
|
||||
val downloadErrorFlow: StateFlow<String?> = _downloadErrorFlow
|
||||
|
||||
var hostLists = mutableStateOf<List<RepoItemInfo>>(emptyList())
|
||||
protected set
|
||||
@@ -35,33 +47,49 @@ abstract class BaseRepoViewModel(application: Application) : AndroidViewModel(ap
|
||||
val isUpdateInstalling = mutableStateMapOf<String, Boolean>()
|
||||
|
||||
abstract fun getInstalledLists(): Array<String>
|
||||
abstract fun getRepoList(callback: (List<RepoItemInfo>?) -> Unit)
|
||||
abstract fun getRepoList(callback: (Result<List<RepoItemInfo>>) -> Unit)
|
||||
|
||||
fun refresh() {
|
||||
isRefreshing.value = true
|
||||
getRepoList { list ->
|
||||
getRepoList { result ->
|
||||
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
|
||||
result
|
||||
.onSuccess { safeList ->
|
||||
val useModule = sharedPreferences.getBoolean("use_module", false)
|
||||
val listType = getHostListMode(sharedPreferences)
|
||||
val filteredList = safeList.filter { item ->
|
||||
when (item.type) {
|
||||
ItemType.list -> listType == "whitelist"
|
||||
ItemType.list_exclude -> listType == "blacklist"
|
||||
ItemType.ipset -> listType == "whitelist"
|
||||
ItemType.ipset_exclude -> listType == "blacklist"
|
||||
ItemType.nfqws -> useModule
|
||||
ItemType.byedpi -> !useModule
|
||||
}
|
||||
}
|
||||
hostLists.value = filteredList
|
||||
isUpdate.clear()
|
||||
val existingHashes = getInstalledLists().map { getFileSha256(File(it)) }
|
||||
for (item in filteredList) {
|
||||
isUpdate[item.name] = item.hash !in existingHashes
|
||||
}
|
||||
}
|
||||
.onFailure { e ->
|
||||
_errorFlow.value = e
|
||||
}
|
||||
}
|
||||
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
|
||||
}
|
||||
isRefreshing.value = false
|
||||
}
|
||||
}
|
||||
|
||||
fun clearError() {
|
||||
_errorFlow.value = null
|
||||
}
|
||||
|
||||
fun clearDownloadError() {
|
||||
_downloadErrorFlow.value = null
|
||||
}
|
||||
|
||||
fun isItemInstalled(item: RepoItemInfo): Boolean {
|
||||
return getInstalledLists().any { File(it).name == item.name }
|
||||
}
|
||||
@@ -69,13 +97,16 @@ abstract class BaseRepoViewModel(application: Application) : AndroidViewModel(ap
|
||||
fun install(item: RepoItemInfo) {
|
||||
isInstalling[item.name] = true
|
||||
val downloadId = download(context, item.url)
|
||||
registerDownloadListenerHost(context, downloadId) { uri ->
|
||||
registerDownloadListener(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")
|
||||
ItemType.byedpi -> File(getZaprettPath(), "strategies/byedpi")
|
||||
ItemType.nfqws -> File(getZaprettPath(), "strategies/nfqws")
|
||||
ItemType.list -> File(getZaprettPath(), "lists/include")
|
||||
ItemType.list_exclude -> File(getZaprettPath(), "lists/exclude")
|
||||
ItemType.ipset -> File(getZaprettPath(), "ipset/include")
|
||||
ItemType.ipset_exclude -> File(getZaprettPath(), "ipset/exclude")
|
||||
}
|
||||
val targetFile = File(targetDir, uri.lastPathSegment!!)
|
||||
sourceFile.copyTo(targetFile, overwrite = true)
|
||||
@@ -84,29 +115,49 @@ abstract class BaseRepoViewModel(application: Application) : AndroidViewModel(ap
|
||||
isUpdate[item.name] = false
|
||||
refresh()
|
||||
}
|
||||
}
|
||||
}, onError = {
|
||||
isInstalling[item.name] = false
|
||||
isUpdate[item.name] = false
|
||||
refresh()
|
||||
_downloadErrorFlow.value = it
|
||||
})
|
||||
}
|
||||
|
||||
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")
|
||||
registerDownloadListener(
|
||||
context,
|
||||
downloadId,
|
||||
onDownloaded = { uri ->
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
val sourceFile = File(uri.path!!)
|
||||
val targetDir = when (item.type) {
|
||||
ItemType.byedpi -> File(getZaprettPath(), "strategies/byedpi")
|
||||
ItemType.nfqws -> File(getZaprettPath(), "strategies/nfqws")
|
||||
ItemType.list -> File(getZaprettPath(), "lists/include")
|
||||
ItemType.list_exclude -> File(getZaprettPath(), "lists/exclude")
|
||||
ItemType.ipset -> File(getZaprettPath(), "ipset/include")
|
||||
ItemType.ipset_exclude -> File(getZaprettPath(), "ipset/exclude")
|
||||
}
|
||||
val targetFile = File(targetDir, uri.lastPathSegment!!)
|
||||
sourceFile.copyTo(targetFile, overwrite = true)
|
||||
sourceFile.delete()
|
||||
isUpdateInstalling[item.name] = false
|
||||
isUpdate[item.name] = false
|
||||
refresh()
|
||||
}
|
||||
val targetFile = File(targetDir, uri.lastPathSegment!!)
|
||||
sourceFile.copyTo(targetFile, overwrite = true)
|
||||
sourceFile.delete()
|
||||
},
|
||||
onError = { error ->
|
||||
isUpdateInstalling[item.name] = false
|
||||
isUpdate[item.name] = false
|
||||
refresh()
|
||||
_downloadErrorFlow.value = error
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
fun showRestartSnackbar(snackbarHostState: SnackbarHostState) {
|
||||
viewModelScope.launch {
|
||||
val result = snackbarHostState.showSnackbar(
|
||||
|
||||
@@ -7,7 +7,6 @@ import android.net.Uri
|
||||
import android.provider.Settings
|
||||
import android.widget.Toast
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.Help
|
||||
import androidx.compose.material.icons.filled.Cancel
|
||||
import androidx.compose.material.icons.filled.CheckCircle
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
@@ -15,9 +14,11 @@ import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.lifecycle.AndroidViewModel
|
||||
import com.cherret.zaprett.BuildConfig
|
||||
import com.cherret.zaprett.R
|
||||
import com.cherret.zaprett.byedpi.ByeDpiVpnService
|
||||
import com.cherret.zaprett.data.ServiceStatus
|
||||
import com.cherret.zaprett.data.ServiceStatusUI
|
||||
import com.cherret.zaprett.utils.download
|
||||
import com.cherret.zaprett.utils.getActiveStrategy
|
||||
import com.cherret.zaprett.utils.getBinVersion
|
||||
@@ -32,6 +33,7 @@ import com.cherret.zaprett.utils.startService
|
||||
import com.cherret.zaprett.utils.stopService
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@@ -40,10 +42,8 @@ class HomeViewModel(application: Application) : AndroidViewModel(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 cardIcon = mutableStateOf(Icons.AutoMirrored.Filled.Help)
|
||||
private set
|
||||
private val _serviceStatus = MutableStateFlow(ServiceStatusUI())
|
||||
val serviceStatus: StateFlow<ServiceStatusUI> = _serviceStatus.asStateFlow()
|
||||
|
||||
var moduleVer = mutableStateOf(context.getString(R.string.unknown_text))
|
||||
private set
|
||||
@@ -51,7 +51,7 @@ class HomeViewModel(application: Application) : AndroidViewModel(application) {
|
||||
var nfqwsVer = mutableStateOf(context.getString(R.string.unknown_text))
|
||||
private set
|
||||
|
||||
var byedpiVer = mutableStateOf("0.17.2")
|
||||
var byedpiVer = mutableStateOf("0.17.3")
|
||||
private set
|
||||
|
||||
var serviceMode = mutableIntStateOf(R.string.service_mode_ciadpi)
|
||||
@@ -72,8 +72,8 @@ class HomeViewModel(application: Application) : AndroidViewModel(application) {
|
||||
var showUpdateDialog = mutableStateOf(false)
|
||||
|
||||
fun checkForUpdate() {
|
||||
if (prefs.getBoolean("auto_update", true)) {
|
||||
getUpdate {
|
||||
if (prefs.getBoolean("auto_update", BuildConfig.auto_update)) {
|
||||
getUpdate(prefs) {
|
||||
if (it != null) {
|
||||
downloadUrl.value = it.downloadUrl.toString()
|
||||
getChangelog(it.changelogUrl.toString()) { log -> changeLog.value = log }
|
||||
@@ -84,55 +84,37 @@ class HomeViewModel(application: Application) : AndroidViewModel(application) {
|
||||
}
|
||||
}
|
||||
|
||||
fun checkServiceStatus() {
|
||||
if (prefs.getBoolean("use_module", false) && prefs.getBoolean("update_on_boot", false)) {
|
||||
private fun updateServiceStatus(useModule: Boolean) {
|
||||
if (useModule) {
|
||||
getStatus { isEnabled ->
|
||||
if (isEnabled){
|
||||
cardText.intValue = R.string.status_enabled
|
||||
cardIcon.value = Icons.Filled.CheckCircle
|
||||
}
|
||||
else {
|
||||
cardText.intValue = R.string.status_disabled
|
||||
cardIcon.value = Icons.Filled.Cancel
|
||||
_serviceStatus.value = if (isEnabled) {
|
||||
ServiceStatusUI(R.string.status_enabled, Icons.Filled.CheckCircle)
|
||||
} else {
|
||||
ServiceStatusUI(R.string.status_disabled, Icons.Filled.Cancel)
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (ByeDpiVpnService.status == ServiceStatus.Connected){
|
||||
cardText.intValue = R.string.status_enabled
|
||||
cardIcon.value = Icons.Filled.CheckCircle
|
||||
}
|
||||
else {
|
||||
cardText.intValue = R.string.status_disabled
|
||||
cardIcon.value = Icons.Filled.Cancel
|
||||
} else {
|
||||
_serviceStatus.value = if (ByeDpiVpnService.status == ServiceStatus.Connected) {
|
||||
ServiceStatusUI(R.string.status_enabled, Icons.Filled.CheckCircle)
|
||||
} else {
|
||||
ServiceStatusUI(R.string.status_disabled, Icons.Filled.Cancel)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun onCardClick() {
|
||||
if (prefs.getBoolean("use_module", false)) {
|
||||
getStatus { isEnabled ->
|
||||
if (isEnabled){
|
||||
cardText.intValue = R.string.status_enabled
|
||||
cardIcon.value = Icons.Filled.CheckCircle
|
||||
}
|
||||
else {
|
||||
cardText.intValue = R.string.status_disabled
|
||||
cardIcon.value = Icons.Filled.Cancel
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (ByeDpiVpnService.status == ServiceStatus.Connected){
|
||||
cardText.intValue = R.string.status_enabled
|
||||
cardIcon.value = Icons.Filled.CheckCircle
|
||||
}
|
||||
else {
|
||||
cardText.intValue = R.string.status_disabled
|
||||
cardIcon.value = Icons.Filled.Cancel
|
||||
}
|
||||
fun checkServiceStatus() {
|
||||
val updateOnBoot = prefs.getBoolean("update_on_boot", false)
|
||||
if (updateOnBoot) {
|
||||
val useModule = prefs.getBoolean("use_module", false)
|
||||
updateServiceStatus(useModule)
|
||||
}
|
||||
}
|
||||
|
||||
fun onCardClick() {
|
||||
val useModule = prefs.getBoolean("use_module", false)
|
||||
updateServiceStatus(useModule)
|
||||
}
|
||||
|
||||
fun startVpn() {
|
||||
ContextCompat.startForegroundService(context, Intent(context, ByeDpiVpnService::class.java).apply { action = "START_VPN" })
|
||||
}
|
||||
@@ -245,9 +227,12 @@ class HomeViewModel(application: Application) : AndroidViewModel(application) {
|
||||
showUpdateDialog.value = false
|
||||
if (context.packageManager.canRequestPackageInstalls()){
|
||||
val id = download(context, downloadUrl.value.orEmpty())
|
||||
registerDownloadListener(context, id) { uri ->
|
||||
registerDownloadListener(context, id, { uri ->
|
||||
installApk(context, uri)
|
||||
}
|
||||
},
|
||||
onError = {
|
||||
|
||||
})
|
||||
}
|
||||
else {
|
||||
val packageUri = Uri.fromParts("package", context.packageName, null)
|
||||
|
||||
@@ -7,5 +7,5 @@ import com.cherret.zaprett.utils.getHostList
|
||||
|
||||
class HostRepoViewModel(application: Application): BaseRepoViewModel(application) {
|
||||
override fun getInstalledLists(): Array<String> = getAllLists()
|
||||
override fun getRepoList(callback: (List<RepoItemInfo>?) -> Unit) = getHostList(sharedPreferences, callback)
|
||||
override fun getRepoList(callback: (Result<List<RepoItemInfo>>) -> Unit) = getHostList(sharedPreferences, callback)
|
||||
}
|
||||
@@ -5,16 +5,24 @@ import android.content.Context
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import com.cherret.zaprett.utils.disableList
|
||||
import com.cherret.zaprett.utils.enableList
|
||||
import com.cherret.zaprett.utils.getActiveExcludeLists
|
||||
import com.cherret.zaprett.utils.getActiveLists
|
||||
import com.cherret.zaprett.utils.getAllExcludeLists
|
||||
import com.cherret.zaprett.utils.getAllLists
|
||||
import com.cherret.zaprett.utils.getHostListMode
|
||||
import com.cherret.zaprett.utils.getStatus
|
||||
import com.cherret.zaprett.utils.setHostListMode
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import java.io.File
|
||||
|
||||
class 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 loadAllItems(): Array<String> =
|
||||
if (getHostListMode(sharedPreferences) == "whitelist") getAllLists()
|
||||
else getAllExcludeLists()
|
||||
override fun loadActiveItems(): Array<String> =
|
||||
if (getHostListMode(sharedPreferences) == "whitelist") getActiveLists(sharedPreferences)
|
||||
else getActiveExcludeLists(sharedPreferences)
|
||||
|
||||
override fun deleteItem(item: String, snackbarHostState: SnackbarHostState, scope: CoroutineScope) {
|
||||
val wasChecked = checked[item] == true
|
||||
@@ -24,6 +32,7 @@ class HostsViewModel(application: Application): BaseListsViewModel(application)
|
||||
if (sharedPreferences.getBoolean("use_module", false)) {
|
||||
getStatus { isEnabled ->
|
||||
if (isEnabled && wasChecked) {
|
||||
snackbarHostState.currentSnackbarData?.dismiss()
|
||||
showRestartSnackbar(context, snackbarHostState, scope)
|
||||
}
|
||||
}
|
||||
@@ -36,6 +45,7 @@ class HostsViewModel(application: Application): BaseListsViewModel(application)
|
||||
if (sharedPreferences.getBoolean("use_module", false)) {
|
||||
getStatus { isEnabled ->
|
||||
if (isEnabled) {
|
||||
snackbarHostState.currentSnackbarData?.dismiss()
|
||||
showRestartSnackbar(
|
||||
context,
|
||||
snackbarHostState,
|
||||
@@ -45,4 +55,9 @@ class HostsViewModel(application: Application): BaseListsViewModel(application)
|
||||
}
|
||||
}
|
||||
}
|
||||
fun setListType(type : String) {
|
||||
setHostListMode(sharedPreferences, type)
|
||||
refresh()
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package com.cherret.zaprett.ui.viewmodel
|
||||
|
||||
import android.app.Application
|
||||
import com.cherret.zaprett.utils.RepoItemInfo
|
||||
import com.cherret.zaprett.utils.getAllIpsets
|
||||
import com.cherret.zaprett.utils.getAllLists
|
||||
import com.cherret.zaprett.utils.getHostList
|
||||
import com.cherret.zaprett.utils.getIpsetList
|
||||
|
||||
class IpsetRepoViewModel(application: Application): BaseRepoViewModel(application) {
|
||||
override fun getInstalledLists(): Array<String> = getAllIpsets()
|
||||
override fun getRepoList(callback: (Result<List<RepoItemInfo>>) -> Unit) =
|
||||
getIpsetList(sharedPreferences, callback)
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
package com.cherret.zaprett.ui.viewmodel
|
||||
|
||||
import android.app.Application
|
||||
import android.content.Context
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import com.cherret.zaprett.utils.disableIpset
|
||||
import com.cherret.zaprett.utils.disableList
|
||||
import com.cherret.zaprett.utils.enableIpset
|
||||
import com.cherret.zaprett.utils.enableList
|
||||
import com.cherret.zaprett.utils.getActiveExcludeIpsets
|
||||
import com.cherret.zaprett.utils.getActiveExcludeLists
|
||||
import com.cherret.zaprett.utils.getActiveIpsets
|
||||
import com.cherret.zaprett.utils.getAllExcludeIpsets
|
||||
import com.cherret.zaprett.utils.getAllIpsets
|
||||
import com.cherret.zaprett.utils.getHostListMode
|
||||
import com.cherret.zaprett.utils.getStatus
|
||||
import com.cherret.zaprett.utils.setHostListMode
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import java.io.File
|
||||
|
||||
class IpsetViewModel(application: Application): BaseListsViewModel(application) {
|
||||
private val sharedPreferences = application.getSharedPreferences("settings", Context.MODE_PRIVATE)
|
||||
override fun loadAllItems(): Array<String> =
|
||||
if (getHostListMode(sharedPreferences) == "whitelist") getAllIpsets()
|
||||
else getAllExcludeIpsets()
|
||||
override fun loadActiveItems(): Array<String> =
|
||||
if (getHostListMode(sharedPreferences) == "whitelist") getActiveIpsets(sharedPreferences)
|
||||
else getActiveExcludeIpsets(sharedPreferences)
|
||||
|
||||
override fun deleteItem(item: String, snackbarHostState: SnackbarHostState, scope: CoroutineScope) {
|
||||
val wasChecked = checked[item] == true
|
||||
disableIpset(item, sharedPreferences)
|
||||
val success = File(item).delete()
|
||||
if (success) refresh()
|
||||
if (sharedPreferences.getBoolean("use_module", false)) {
|
||||
getStatus { isEnabled ->
|
||||
if (isEnabled && wasChecked) {
|
||||
snackbarHostState.currentSnackbarData?.dismiss()
|
||||
showRestartSnackbar(context, snackbarHostState, scope)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCheckedChange(item: String, isChecked: Boolean, snackbarHostState: SnackbarHostState, scope: CoroutineScope) {
|
||||
checked[item] = isChecked
|
||||
if (isChecked) enableIpset(item, sharedPreferences) else disableIpset(item, sharedPreferences)
|
||||
if (sharedPreferences.getBoolean("use_module", false)) {
|
||||
getStatus { isEnabled ->
|
||||
if (isEnabled) {
|
||||
snackbarHostState.currentSnackbarData?.dismiss()
|
||||
showRestartSnackbar(
|
||||
context,
|
||||
snackbarHostState,
|
||||
scope
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
fun setListType(type : String) {
|
||||
setHostListMode(sharedPreferences, type)
|
||||
refresh()
|
||||
}
|
||||
}
|
||||
@@ -1,22 +1,29 @@
|
||||
package com.cherret.zaprett.ui.viewmodel
|
||||
|
||||
import android.app.Application
|
||||
import android.content.Context
|
||||
import android.content.Context.MODE_PRIVATE
|
||||
import android.content.Intent
|
||||
import android.content.pm.ApplicationInfo
|
||||
import android.content.pm.PackageManager
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.drawable.BitmapDrawable
|
||||
import android.graphics.drawable.Drawable
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.lifecycle.AndroidViewModel
|
||||
import androidx.core.graphics.createBitmap
|
||||
import com.cherret.zaprett.byedpi.ByeDpiVpnService
|
||||
import com.cherret.zaprett.data.AppListType
|
||||
import com.cherret.zaprett.data.ServiceStatus
|
||||
import com.cherret.zaprett.utils.addPackageToList
|
||||
import com.cherret.zaprett.utils.checkModuleInstallation
|
||||
import com.cherret.zaprett.utils.checkRoot
|
||||
import com.cherret.zaprett.utils.getAppList
|
||||
import com.cherret.zaprett.utils.getStartOnBoot
|
||||
import com.cherret.zaprett.utils.removePackageFromList
|
||||
import com.cherret.zaprett.utils.setStartOnBoot
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
class SettingsViewModel(application: Application) : AndroidViewModel(application) {
|
||||
private val context = application
|
||||
@@ -26,34 +33,28 @@ class SettingsViewModel(application: Application) : AndroidViewModel(application
|
||||
private val _selectedPackages = MutableStateFlow<Set<String>>(emptySet())
|
||||
val selectedPackages: StateFlow<Set<String>> = _selectedPackages.asStateFlow()
|
||||
private val _currentListType = MutableStateFlow(AppListType.Whitelist)
|
||||
private val _useModule = MutableStateFlow(false)
|
||||
val useModule: StateFlow<Boolean> = _useModule
|
||||
|
||||
private val _autoRestart = MutableStateFlow(false)
|
||||
val autoRestart: StateFlow<Boolean> = _autoRestart
|
||||
|
||||
init {
|
||||
refreshApplications()
|
||||
_useModule.value = context.getSharedPreferences("settings", MODE_PRIVATE).getBoolean("use_module", false)
|
||||
getStartOnBoot(prefs) { value ->
|
||||
_autoRestart.value = value
|
||||
}
|
||||
}
|
||||
|
||||
fun getAppIconBitmap(packageName: String): Bitmap? {
|
||||
val pm: PackageManager = getApplication<Application>().packageManager
|
||||
suspend fun getAppIconBitmap(packageName: String): Drawable? = withContext(Dispatchers.IO) {
|
||||
val pm: PackageManager = context.packageManager
|
||||
val drawable: Drawable = try {
|
||||
pm.getApplicationIcon(packageName)
|
||||
|
||||
} catch (e: PackageManager.NameNotFoundException) {
|
||||
return null
|
||||
return@withContext null
|
||||
}
|
||||
return drawableToBitmap(drawable)
|
||||
}
|
||||
|
||||
private fun drawableToBitmap(drawable: Drawable): Bitmap {
|
||||
if (drawable is BitmapDrawable) {
|
||||
return drawable.bitmap
|
||||
}
|
||||
val bitmap = createBitmap(
|
||||
drawable.intrinsicWidth.coerceAtLeast(1),
|
||||
drawable.intrinsicHeight.coerceAtLeast(1)
|
||||
)
|
||||
val canvas = Canvas(bitmap)
|
||||
drawable.setBounds(0, 0, canvas.width, canvas.height)
|
||||
drawable.draw(canvas)
|
||||
return bitmap
|
||||
return@withContext drawable
|
||||
}
|
||||
|
||||
fun getApplicationName(packageName: String) : String? {
|
||||
@@ -77,7 +78,6 @@ class SettingsViewModel(application: Application) : AndroidViewModel(application
|
||||
}
|
||||
|
||||
fun refreshApplications() {
|
||||
val context = getApplication<Application>()
|
||||
val packages = if (prefs.getBoolean("show_system_apps", false)){
|
||||
context.packageManager.getInstalledPackages(PackageManager.GET_META_DATA)
|
||||
}
|
||||
@@ -118,4 +118,46 @@ class SettingsViewModel(application: Application) : AndroidViewModel(application
|
||||
refreshApplications()
|
||||
}
|
||||
|
||||
fun useModule(context: Context, checked: Boolean, openNoRootDialog: MutableState<Boolean>, openNoModuleDialog: MutableState<Boolean>) {
|
||||
val sharedPreferences = context.getSharedPreferences("settings", Context.MODE_PRIVATE)
|
||||
val editor = sharedPreferences.edit()
|
||||
if (checked) {
|
||||
checkRoot { hasRoot ->
|
||||
if (hasRoot) {
|
||||
checkModuleInstallation { hasModule ->
|
||||
if (hasModule) {
|
||||
editor.putBoolean("use_module", true).apply()
|
||||
if (ByeDpiVpnService.status == ServiceStatus.Connected) {
|
||||
context.startService(Intent(context, ByeDpiVpnService::class.java).apply {
|
||||
action = "STOP_VPN"
|
||||
})
|
||||
}
|
||||
editor.remove("lists").apply()
|
||||
editor.remove("active_strategy").apply()
|
||||
editor.remove("applist").apply()
|
||||
editor.remove("whitelist").apply()
|
||||
editor.remove("blacklist").apply()
|
||||
_useModule.value = true
|
||||
} else {
|
||||
openNoModuleDialog.value = true
|
||||
}
|
||||
}
|
||||
} else {
|
||||
openNoRootDialog.value = true
|
||||
}
|
||||
}
|
||||
} else {
|
||||
editor.putBoolean("use_module", false).apply()
|
||||
_useModule.value = false
|
||||
}
|
||||
}
|
||||
|
||||
fun handleAutoRestart(context: Context) {
|
||||
val sharedPreferences = context.getSharedPreferences("settings", MODE_PRIVATE)
|
||||
if (sharedPreferences.getBoolean("use_module", false)) {
|
||||
setStartOnBoot(prefs) { value ->
|
||||
_autoRestart.value = value
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -13,5 +13,5 @@ class StrategyRepoViewModel(application: Application): BaseRepoViewModel(applica
|
||||
} else {
|
||||
getAllByeDPIStrategies()
|
||||
}
|
||||
override fun getRepoList(callback: (List<RepoItemInfo>?) -> Unit) = getStrategiesList(sharedPreferences, callback)
|
||||
override fun getRepoList(callback: (Result<List<RepoItemInfo>>) -> Unit) = getStrategiesList(sharedPreferences, callback)
|
||||
}
|
||||
@@ -0,0 +1,192 @@
|
||||
package com.cherret.zaprett.ui.viewmodel
|
||||
|
||||
import android.app.Application
|
||||
import android.content.Context.MODE_PRIVATE
|
||||
import android.content.Intent
|
||||
import android.util.Log
|
||||
import androidx.compose.runtime.mutableStateListOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.lifecycle.AndroidViewModel
|
||||
import com.cherret.zaprett.R
|
||||
import com.cherret.zaprett.byedpi.ByeDpiVpnService
|
||||
import com.cherret.zaprett.data.ServiceStatus
|
||||
import com.cherret.zaprett.data.StrategyCheckResult
|
||||
import com.cherret.zaprett.data.StrategyTestingStatus
|
||||
import com.cherret.zaprett.utils.disableStrategy
|
||||
import com.cherret.zaprett.utils.enableStrategy
|
||||
import com.cherret.zaprett.utils.getActiveLists
|
||||
import com.cherret.zaprett.utils.getActiveStrategy
|
||||
import com.cherret.zaprett.utils.getAllStrategies
|
||||
import com.cherret.zaprett.utils.getStatus
|
||||
import com.cherret.zaprett.utils.startService
|
||||
import com.cherret.zaprett.utils.stopService
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.awaitAll
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.coroutines.withTimeoutOrNull
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import java.io.File
|
||||
import java.net.InetSocketAddress
|
||||
import java.net.Proxy
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
class StrategySelectionViewModel(application: Application) : AndroidViewModel(application) {
|
||||
val prefs = application.getSharedPreferences("settings", MODE_PRIVATE)
|
||||
val context = application
|
||||
private val _requestVpnPermission = MutableStateFlow(false)
|
||||
val requestVpnPermission = _requestVpnPermission.asStateFlow()
|
||||
val strategyStates = mutableStateListOf<StrategyCheckResult>()
|
||||
var noHostsCard = mutableStateOf(false)
|
||||
private set
|
||||
|
||||
init {
|
||||
loadStrategies()
|
||||
checkHosts()
|
||||
}
|
||||
|
||||
fun buildHttpClient(): OkHttpClient {
|
||||
val builder = OkHttpClient.Builder()
|
||||
.callTimeout(prefs.getLong("probe_timeout", 1000L), TimeUnit.MILLISECONDS)
|
||||
.followRedirects(true)
|
||||
.followSslRedirects(true)
|
||||
if (!prefs.getBoolean("use_module", false)) {
|
||||
val ip = prefs.getString("ip", "127.0.0.1") ?: "127.0.0.1"
|
||||
val port = prefs.getString("port", "1080")?.toIntOrNull() ?: 1080
|
||||
val proxy = Proxy(Proxy.Type.SOCKS, InetSocketAddress(ip, port))
|
||||
builder.proxy(proxy)
|
||||
}
|
||||
return builder.build()
|
||||
}
|
||||
|
||||
fun loadStrategies() {
|
||||
val strategyList = getAllStrategies(prefs)
|
||||
strategyStates.clear()
|
||||
strategyList.forEach { name ->
|
||||
strategyStates += StrategyCheckResult(
|
||||
path = name,
|
||||
status = StrategyTestingStatus.Waiting,
|
||||
progress = 0f,
|
||||
domains = emptyList()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun testDomain(domain : String) : Boolean = withContext(Dispatchers.IO) {
|
||||
val request = Request.Builder()
|
||||
.url("https://${domain}")
|
||||
.build()
|
||||
try {
|
||||
buildHttpClient().newCall(request).execute().use { response ->
|
||||
val body = response.body.byteStream().readBytes()
|
||||
val contentLength = response.body.contentLength()
|
||||
contentLength <= 0 || body.size.toLong() >= contentLength
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun countReachable(index: Int, urls: List<String>): Float = coroutineScope {
|
||||
if (urls.isEmpty()) return@coroutineScope 0f
|
||||
val results: List<String> = urls.map { url ->
|
||||
async { if (testDomain(url)) url else null }
|
||||
}.awaitAll().filterNotNull()
|
||||
strategyStates[index].domains = results
|
||||
(results.size.toFloat() / urls.size.toFloat()).coerceIn(0f, 1f)
|
||||
}
|
||||
|
||||
suspend fun readActiveListsLines(): List<String> = withContext(Dispatchers.IO) {
|
||||
val result = mutableListOf<String>()
|
||||
getActiveLists(prefs).forEach { path ->
|
||||
runCatching {
|
||||
File(path).useLines { lines ->
|
||||
lines.forEach { line ->
|
||||
result += line
|
||||
}
|
||||
}
|
||||
}.onFailure {
|
||||
Log.e("Error", "Occured error when creating list for check")
|
||||
}
|
||||
}
|
||||
result
|
||||
}
|
||||
suspend fun performTest() {
|
||||
val targets = readActiveListsLines()
|
||||
for (index in strategyStates.indices) {
|
||||
val current = strategyStates[index]
|
||||
strategyStates[index] = current.copy(status = StrategyTestingStatus.Testing)
|
||||
enableStrategy(current.path, prefs)
|
||||
if (prefs.getBoolean("use_module", false)) {
|
||||
getStatus { if (it) stopService {} }
|
||||
startService {}
|
||||
try {
|
||||
val progress = countReachable(index, targets)
|
||||
val old = strategyStates[index]
|
||||
strategyStates[index] = old.copy(
|
||||
progress = progress,
|
||||
status = StrategyTestingStatus.Completed
|
||||
)
|
||||
} finally {
|
||||
stopService {}
|
||||
disableStrategy(current.path, prefs)
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (ByeDpiVpnService.status == ServiceStatus.Connected) {
|
||||
context.startService(Intent(context, ByeDpiVpnService::class.java).apply {
|
||||
action = "STOP_VPN"
|
||||
})
|
||||
delay(300L)
|
||||
}
|
||||
_requestVpnPermission.value = true
|
||||
/*context.startService(Intent(context, ByeDpiVpnService::class.java).apply {
|
||||
action = "START_VPN"
|
||||
})*/
|
||||
val connected = withTimeoutOrNull(10_000L) {
|
||||
while (ByeDpiVpnService.status != ServiceStatus.Connected) {
|
||||
delay(100L)
|
||||
}
|
||||
true
|
||||
} ?: false
|
||||
if (connected) delay(150L)
|
||||
try {
|
||||
val progress = countReachable(index,targets)
|
||||
val old = strategyStates[index]
|
||||
|
||||
strategyStates[index] = old.copy(
|
||||
progress = progress,
|
||||
status = StrategyTestingStatus.Completed
|
||||
)
|
||||
} finally {
|
||||
context.startService(Intent(context, ByeDpiVpnService::class.java).apply {
|
||||
action = "STOP_VPN"
|
||||
})
|
||||
delay(200L)
|
||||
disableStrategy(current.path, prefs)
|
||||
}
|
||||
}
|
||||
}
|
||||
val sorted = strategyStates.sortedByDescending { it.progress }
|
||||
strategyStates.clear()
|
||||
strategyStates.addAll(sorted)
|
||||
}
|
||||
|
||||
fun checkHosts() {
|
||||
if (getActiveLists(prefs).isEmpty()) noHostsCard.value = true
|
||||
Log.d("getActiveLists.isEmpty", getActiveLists(prefs).isEmpty().toString())
|
||||
}
|
||||
fun startVpn() {
|
||||
ContextCompat.startForegroundService(context, Intent(context, ByeDpiVpnService::class.java).apply { action = "START_VPN" })
|
||||
}
|
||||
fun clearVpnPermissionRequest() {
|
||||
_requestVpnPermission.value = false
|
||||
}
|
||||
}
|
||||
@@ -8,8 +8,8 @@ import com.cherret.zaprett.byedpi.ByeDpiVpnService
|
||||
import com.cherret.zaprett.data.ServiceStatus
|
||||
import com.cherret.zaprett.utils.disableStrategy
|
||||
import com.cherret.zaprett.utils.enableStrategy
|
||||
import com.cherret.zaprett.utils.getActiveByeDPIStrategies
|
||||
import com.cherret.zaprett.utils.getActiveNfqwsStrategies
|
||||
import com.cherret.zaprett.utils.getActiveByeDPIStrategy
|
||||
import com.cherret.zaprett.utils.getActiveNfqwsStrategy
|
||||
import com.cherret.zaprett.utils.getAllByeDPIStrategies
|
||||
import com.cherret.zaprett.utils.getAllNfqwsStrategies
|
||||
import com.cherret.zaprett.utils.getStatus
|
||||
@@ -36,6 +36,7 @@ class StrategyViewModel(application: Application): BaseListsViewModel(applicatio
|
||||
if (sharedPreferences.getBoolean("use_module", false)) {
|
||||
getStatus { isEnabled ->
|
||||
if (isEnabled && wasChecked) {
|
||||
snackbarHostState.currentSnackbarData?.dismiss()
|
||||
showRestartSnackbar(context, snackbarHostState, scope)
|
||||
}
|
||||
}
|
||||
@@ -64,6 +65,7 @@ class StrategyViewModel(application: Application): BaseListsViewModel(applicatio
|
||||
if (sharedPreferences.getBoolean("use_module", false)) {
|
||||
getStatus { isEnabled ->
|
||||
if (isEnabled) {
|
||||
snackbarHostState.currentSnackbarData?.dismiss()
|
||||
showRestartSnackbar(
|
||||
context,
|
||||
snackbarHostState,
|
||||
@@ -82,10 +84,10 @@ interface StrategyProvider {
|
||||
|
||||
class NfqwsStrategyProvider : StrategyProvider {
|
||||
override fun getAll() = getAllNfqwsStrategies()
|
||||
override fun getActive() = getActiveNfqwsStrategies()
|
||||
override fun getActive() = getActiveNfqwsStrategy()
|
||||
}
|
||||
|
||||
class ByeDPIStrategyProvider(private val sharedPreferences: SharedPreferences) : StrategyProvider {
|
||||
override fun getAll() = getAllByeDPIStrategies()
|
||||
override fun getActive() = getActiveByeDPIStrategies(sharedPreferences)
|
||||
override fun getActive() = getActiveByeDPIStrategy(sharedPreferences)
|
||||
}
|
||||
@@ -11,6 +11,7 @@ import android.net.Uri
|
||||
import android.os.Build
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.net.toUri
|
||||
import com.cherret.zaprett.data.ItemType
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.json.Json
|
||||
import okhttp3.Call
|
||||
@@ -25,47 +26,60 @@ import java.security.MessageDigest
|
||||
private val client = OkHttpClient()
|
||||
private val json = Json { ignoreUnknownKeys = true }
|
||||
|
||||
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()
|
||||
fun getHostList(sharedPreferences: SharedPreferences, callback: (Result<List<RepoItemInfo>>) -> Unit) {
|
||||
getRepo(
|
||||
sharedPreferences.getString(
|
||||
"hosts_repo_url",
|
||||
"https://raw.githubusercontent.com/CherretGit/zaprett-repo/refs/heads/main/hosts.json"
|
||||
) ?: "https://raw.githubusercontent.com/CherretGit/zaprett-repo/refs/heads/main/hosts.json",
|
||||
callback
|
||||
)
|
||||
}
|
||||
|
||||
fun getIpsetList(sharedPreferences: SharedPreferences, callback: (Result<List<RepoItemInfo>>) -> Unit) {
|
||||
getRepo(
|
||||
sharedPreferences.getString(
|
||||
"ipset_repo_url",
|
||||
"https://raw.githubusercontent.com/CherretGit/zaprett-repo/refs/heads/main/ipsets.json"
|
||||
) ?: "https://raw.githubusercontent.com/CherretGit/zaprett-repo/refs/heads/main/ipsets.json",
|
||||
callback
|
||||
)
|
||||
}
|
||||
|
||||
fun getStrategiesList(sharedPreferences: SharedPreferences, callback: (Result<List<RepoItemInfo>>) -> Unit) {
|
||||
getRepo(
|
||||
sharedPreferences.getString(
|
||||
"strategies_repo_url",
|
||||
"https://raw.githubusercontent.com/CherretGit/zaprett-repo/refs/heads/main/strategies.json"
|
||||
) ?: "https://raw.githubusercontent.com/CherretGit/zaprett-repo/refs/heads/main/strategies.json",
|
||||
callback
|
||||
)
|
||||
}
|
||||
|
||||
fun getRepo(url: String, callback: (Result<List<RepoItemInfo>>) -> Unit) {
|
||||
val request = Request.Builder().url(url).build()
|
||||
client.newCall(request).enqueue(object : Callback {
|
||||
override fun onFailure(call: Call, e: IOException) {
|
||||
e.printStackTrace()
|
||||
callback(null)
|
||||
callback(Result.failure(e))
|
||||
}
|
||||
override fun onResponse(call: Call, response: Response) {
|
||||
response.use {
|
||||
if (!response.isSuccessful) {
|
||||
callback(null)
|
||||
callback(Result.failure(IOException("Unexpected HTTP code ${response.code}")))
|
||||
return
|
||||
}
|
||||
val jsonString = response.body.string()
|
||||
val hostsInfo = json.decodeFromString<List<RepoItemInfo>>(jsonString)
|
||||
callback(hostsInfo)
|
||||
val result = runCatching {
|
||||
json.decodeFromString<List<RepoItemInfo>>(jsonString)
|
||||
}
|
||||
callback(result)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fun registerDownloadListenerHost(context: Context, downloadId: Long, onDownloaded: (Uri) -> Unit) {// AI Generated
|
||||
fun registerDownloadListener(context: Context, downloadId: Long, onDownloaded: (Uri) -> Unit, onError: (String) -> Unit) {// AI Generated
|
||||
val receiver = object : BroadcastReceiver() {
|
||||
@SuppressLint("Range")
|
||||
override fun onReceive(context: Context?, intent: Intent?) {
|
||||
@@ -76,12 +90,32 @@ fun registerDownloadListenerHost(context: Context, downloadId: Long, onDownloade
|
||||
dm.query(query)?.use { cursor ->
|
||||
if (cursor.moveToFirst()) {
|
||||
val status = cursor.getInt(cursor.getColumnIndex(DownloadManager.COLUMN_STATUS))
|
||||
if (status == DownloadManager.STATUS_SUCCESSFUL) {
|
||||
val uriString = cursor.getString(cursor.getColumnIndex(DownloadManager.COLUMN_LOCAL_URI))
|
||||
if (uriString != null) {
|
||||
val uri = uriString.toUri()
|
||||
val reason = cursor.getInt(cursor.getColumnIndex(DownloadManager.COLUMN_REASON))
|
||||
when (status) {
|
||||
DownloadManager.STATUS_SUCCESSFUL -> {
|
||||
val uriString = cursor.getString(cursor.getColumnIndex(DownloadManager.COLUMN_LOCAL_URI))
|
||||
if (uriString != null) {
|
||||
val uri = uriString.toUri()
|
||||
context.unregisterReceiver(this)
|
||||
onDownloaded(uri)
|
||||
}
|
||||
}
|
||||
|
||||
DownloadManager.STATUS_FAILED -> {
|
||||
context.unregisterReceiver(this)
|
||||
onDownloaded(uri)
|
||||
val errorMessage = when (reason) {
|
||||
DownloadManager.ERROR_CANNOT_RESUME -> "Cannot resume download"
|
||||
DownloadManager.ERROR_DEVICE_NOT_FOUND -> "Device not found"
|
||||
DownloadManager.ERROR_FILE_ALREADY_EXISTS -> "File already exists"
|
||||
DownloadManager.ERROR_FILE_ERROR -> "File error"
|
||||
DownloadManager.ERROR_HTTP_DATA_ERROR -> "HTTP data error"
|
||||
DownloadManager.ERROR_INSUFFICIENT_SPACE -> "Insufficient space"
|
||||
DownloadManager.ERROR_TOO_MANY_REDIRECTS -> "Too many redirects"
|
||||
DownloadManager.ERROR_UNHANDLED_HTTP_CODE -> "Unhandled HTTP code"
|
||||
DownloadManager.ERROR_UNKNOWN -> "Unknown error"
|
||||
else -> "Download failed: reason=$reason"
|
||||
}
|
||||
onError(errorMessage)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -113,7 +147,7 @@ data class RepoItemInfo(
|
||||
val name: String,
|
||||
val author: String,
|
||||
val description: String,
|
||||
val type: String? = null,
|
||||
val type: ItemType,
|
||||
val hash: String,
|
||||
val url: String
|
||||
)
|
||||
@@ -6,6 +6,7 @@ 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 android.os.Environment
|
||||
@@ -25,9 +26,9 @@ 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()
|
||||
// PLS EGOR-WHITE REFACTOR THIS
|
||||
fun getUpdate(sharedPreferences: SharedPreferences, callback: (UpdateInfo?) -> Unit) {
|
||||
val request = Request.Builder().url(sharedPreferences.getString("update_repo_url", "https://raw.githubusercontent.com/CherretGit/zaprett-app/refs/heads/main/update.json")?: "https://raw.githubusercontent.com/CherretGit/zaprett-app/refs/heads/main/update.json").build()
|
||||
client.newCall(request).enqueue(object : Callback {
|
||||
override fun onFailure(call: Call, e: IOException) {
|
||||
e.printStackTrace()
|
||||
@@ -94,30 +95,6 @@ fun installApk(context: Context, uri: Uri) {
|
||||
context.startActivity(intent)
|
||||
}
|
||||
}
|
||||
|
||||
fun registerDownloadListener(context: Context, downloadId: Long, onDownloaded: (Uri) -> Unit) {// AI Generated
|
||||
val receiver = object : BroadcastReceiver() {
|
||||
@SuppressLint("Range")
|
||||
override fun onReceive(context: Context?, intent: Intent?) {
|
||||
if (intent?.action != DownloadManager.ACTION_DOWNLOAD_COMPLETE) return
|
||||
if (intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, -1) != downloadId) return
|
||||
val downloadManager = context?.getSystemService(Context.DOWNLOAD_SERVICE) as? DownloadManager ?: return
|
||||
downloadManager.query(DownloadManager.Query().setFilterById(downloadId)).use { cursor ->
|
||||
if (cursor.moveToFirst() && cursor.getInt(cursor.getColumnIndex(DownloadManager.COLUMN_STATUS)) == DownloadManager.STATUS_SUCCESSFUL) {
|
||||
context.unregisterReceiver(this)
|
||||
onDownloaded(cursor.getString(cursor.getColumnIndex(DownloadManager.COLUMN_LOCAL_URI)).toUri())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
val intentFilter = IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
context.registerReceiver(receiver, intentFilter, Context.RECEIVER_EXPORTED)
|
||||
} else {
|
||||
ContextCompat.registerReceiver(context, receiver, IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE), ContextCompat.RECEIVER_EXPORTED)
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class UpdateInfo(
|
||||
val version: String?,
|
||||
|
||||
@@ -64,39 +64,20 @@ fun getConfigFile(): File {
|
||||
return File(Environment.getExternalStorageDirectory(), "zaprett/config")
|
||||
}
|
||||
|
||||
fun setStartOnBoot(startOnBoot: Boolean) {
|
||||
val configFile = getConfigFile()
|
||||
if (configFile.exists()) {
|
||||
val props = Properties()
|
||||
try {
|
||||
FileInputStream(configFile).use { input ->
|
||||
props.load(input)
|
||||
}
|
||||
props.setProperty("autorestart", startOnBoot.toString())
|
||||
FileOutputStream(configFile).use { output ->
|
||||
props.store(output, "Don't place '/' in end of directory! Example: /sdcard")
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
throw RuntimeException(e)
|
||||
fun setStartOnBoot(prefs : SharedPreferences, callback: (Boolean) -> Unit) {
|
||||
if (prefs.getBoolean("use_module", false)) {
|
||||
Shell.cmd("zaprett autostart").submit { result ->
|
||||
if (result.out.isNotEmpty() && result.out.toString().contains("true")) callback(true) else callback(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun getStartOnBoot(): Boolean {
|
||||
val configFile = getConfigFile()
|
||||
val props = Properties()
|
||||
return try {
|
||||
if (configFile.exists()) {
|
||||
FileInputStream(configFile).use { input ->
|
||||
props.load(input)
|
||||
}
|
||||
props.getProperty("autorestart", "false").toBoolean()
|
||||
} else {
|
||||
false
|
||||
fun getStartOnBoot(prefs : SharedPreferences, callback: (Boolean) -> Unit) {
|
||||
if (prefs.getBoolean("use_module", false)) {
|
||||
Shell.cmd("zaprett get-autostart").submit { result ->
|
||||
if (result.out.isNotEmpty() && result.out.toString().contains("true")) callback(true) else callback(false)
|
||||
}
|
||||
} catch (_: IOException) {
|
||||
false
|
||||
}
|
||||
} else { callback(false) }
|
||||
}
|
||||
|
||||
fun getZaprettPath(): String {
|
||||
@@ -116,32 +97,59 @@ fun getZaprettPath(): String {
|
||||
}
|
||||
|
||||
fun getAllLists(): Array<String> {
|
||||
val listsDir = File("${getZaprettPath()}/lists/")
|
||||
if (listsDir.exists() && listsDir.isDirectory) {
|
||||
val onlyNames = listsDir.list() ?: return emptyArray()
|
||||
return onlyNames.map { "$listsDir/$it" }.toTypedArray()
|
||||
}
|
||||
return emptyArray()
|
||||
val listsDir = File("${getZaprettPath()}/lists/include")
|
||||
return listsDir.listFiles { file -> file.isFile && file.extension.lowercase() == "txt" }
|
||||
?.map { it.absolutePath }
|
||||
?.toTypedArray()
|
||||
?: emptyArray()
|
||||
}
|
||||
|
||||
fun getAllIpsets(): Array<String> {
|
||||
val listsDir = File("${getZaprettPath()}/ipset/include")
|
||||
return listsDir.listFiles { file -> file.isFile && file.extension.lowercase() == "txt" }
|
||||
?.map { it.absolutePath }
|
||||
?.toTypedArray()
|
||||
?: emptyArray()
|
||||
}
|
||||
|
||||
fun getAllExcludeLists(): Array<String> {
|
||||
val listsDir = File("${getZaprettPath()}/lists/exclude/")
|
||||
return listsDir.listFiles { file -> file.isFile && file.extension.lowercase() == "txt" }
|
||||
?.map { it.absolutePath }
|
||||
?.toTypedArray()
|
||||
?: emptyArray()
|
||||
}
|
||||
|
||||
fun getAllExcludeIpsets(): Array<String> {
|
||||
val listsDir = File("${getZaprettPath()}/ipset/exclude/")
|
||||
return listsDir.listFiles { file -> file.isFile && file.extension.lowercase() == "txt" }
|
||||
?.map { it.absolutePath }
|
||||
?.toTypedArray()
|
||||
?: emptyArray()
|
||||
}
|
||||
|
||||
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()
|
||||
return listsDir.listFiles { file -> file.isFile && file.extension.lowercase() == "txt" }
|
||||
?.map { it.absolutePath }
|
||||
?.toTypedArray()
|
||||
?: 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()
|
||||
return listsDir.listFiles { file -> file.isFile && file.extension.lowercase() == "txt" }
|
||||
?.map { it.absolutePath }
|
||||
?.toTypedArray()
|
||||
?: emptyArray()
|
||||
}
|
||||
|
||||
fun getAllStrategies(sharedPreferences : SharedPreferences) : Array<String> {
|
||||
return if (sharedPreferences.getBoolean("use_module", false)) getAllNfqwsStrategies()
|
||||
else getAllByeDPIStrategies()
|
||||
}
|
||||
|
||||
|
||||
fun getActiveLists(sharedPreferences: SharedPreferences): Array<String> {
|
||||
if (sharedPreferences.getBoolean("use_module", false)) {
|
||||
val configFile = getConfigFile()
|
||||
@@ -151,7 +159,7 @@ fun getActiveLists(sharedPreferences: SharedPreferences): Array<String> {
|
||||
FileInputStream(configFile).use { input ->
|
||||
props.load(input)
|
||||
}
|
||||
val activeLists = props.getProperty("activelists", "")
|
||||
val activeLists = props.getProperty("active_lists", "")
|
||||
Log.d("Active lists", activeLists)
|
||||
if (activeLists.isNotEmpty()) activeLists.split(",")
|
||||
.toTypedArray() else emptyArray()
|
||||
@@ -165,8 +173,73 @@ fun getActiveLists(sharedPreferences: SharedPreferences): Array<String> {
|
||||
return sharedPreferences.getStringSet("lists", emptySet())?.toTypedArray() ?: emptyArray()
|
||||
}
|
||||
}
|
||||
fun getActiveIpsets(sharedPreferences: SharedPreferences): Array<String> {
|
||||
if (sharedPreferences.getBoolean("use_module", false)) {
|
||||
val configFile = getConfigFile()
|
||||
if (configFile.exists()) {
|
||||
val props = Properties()
|
||||
return try {
|
||||
FileInputStream(configFile).use { input ->
|
||||
props.load(input)
|
||||
}
|
||||
val activeLists = props.getProperty("active_ipsets", "")
|
||||
Log.d("Active ipsets", activeLists)
|
||||
if (activeLists.isNotEmpty()) activeLists.split(",")
|
||||
.toTypedArray() else emptyArray()
|
||||
} catch (e: IOException) {
|
||||
throw RuntimeException(e)
|
||||
}
|
||||
}
|
||||
return emptyArray()
|
||||
}
|
||||
else return sharedPreferences.getStringSet("ipsets", emptySet())?.toTypedArray() ?: emptyArray()
|
||||
}
|
||||
fun getActiveExcludeLists(sharedPreferences: SharedPreferences): Array<String> {
|
||||
if (sharedPreferences.getBoolean("use_module", false)) {
|
||||
val configFile = getConfigFile()
|
||||
if (configFile.exists()) {
|
||||
val props = Properties()
|
||||
return try {
|
||||
FileInputStream(configFile).use { input ->
|
||||
props.load(input)
|
||||
}
|
||||
val activeLists = props.getProperty("active_exclude_lists", "")
|
||||
if (activeLists.isNotEmpty()) activeLists.split(",")
|
||||
.toTypedArray() else emptyArray()
|
||||
} catch (e: IOException) {
|
||||
throw RuntimeException(e)
|
||||
}
|
||||
}
|
||||
return emptyArray()
|
||||
}
|
||||
else {
|
||||
return sharedPreferences.getStringSet("exclude_lists", emptySet())?.toTypedArray() ?: emptyArray()
|
||||
}
|
||||
}
|
||||
|
||||
fun getActiveNfqwsStrategies(): Array<String> {
|
||||
fun getActiveExcludeIpsets(sharedPreferences: SharedPreferences): Array<String> {
|
||||
if (sharedPreferences.getBoolean("use_module", false)) {
|
||||
val configFile = getConfigFile()
|
||||
if (configFile.exists()) {
|
||||
val props = Properties()
|
||||
return try {
|
||||
FileInputStream(configFile).use { input ->
|
||||
props.load(input)
|
||||
}
|
||||
val activeLists = props.getProperty("active_exclude_ipsets", "")
|
||||
Log.d("Active ipsets", activeLists)
|
||||
if (activeLists.isNotEmpty()) activeLists.split(",")
|
||||
.toTypedArray() else emptyArray()
|
||||
} catch (e: IOException) {
|
||||
throw RuntimeException(e)
|
||||
}
|
||||
}
|
||||
return emptyArray()
|
||||
}
|
||||
else return sharedPreferences.getStringSet("exclude_ipsets", emptySet())?.toTypedArray() ?: emptyArray()
|
||||
}
|
||||
|
||||
fun getActiveNfqwsStrategy(): Array<String> {
|
||||
val configFile = File("${getZaprettPath()}/config")
|
||||
if (configFile.exists()) {
|
||||
val props = Properties()
|
||||
@@ -184,7 +257,7 @@ fun getActiveNfqwsStrategies(): Array<String> {
|
||||
return emptyArray()
|
||||
}
|
||||
|
||||
fun getActiveByeDPIStrategies(sharedPreferences: SharedPreferences): Array<String> {
|
||||
fun getActiveByeDPIStrategy(sharedPreferences: SharedPreferences): Array<String> {
|
||||
val path = sharedPreferences.getString("active_strategy", "")
|
||||
if (!path.isNullOrBlank() && File(path).exists()) {
|
||||
return arrayOf(path)
|
||||
@@ -192,7 +265,7 @@ fun getActiveByeDPIStrategies(sharedPreferences: SharedPreferences): Array<Strin
|
||||
return emptyArray()
|
||||
}
|
||||
|
||||
fun getActiveStrategy(sharedPreferences: SharedPreferences): List<String> {
|
||||
fun getActiveByeDPIStrategyContent(sharedPreferences: SharedPreferences): List<String> {
|
||||
val path = sharedPreferences.getString("active_strategy", "")
|
||||
if (!path.isNullOrBlank() && File(path).exists()) {
|
||||
return File(path).readLines()
|
||||
@@ -200,6 +273,12 @@ fun getActiveStrategy(sharedPreferences: SharedPreferences): List<String> {
|
||||
return emptyList()
|
||||
}
|
||||
|
||||
fun getActiveStrategy(sharedPreferences: SharedPreferences): Array<String> {
|
||||
return if (sharedPreferences.getBoolean("use_module", false)) getActiveNfqwsStrategy()
|
||||
else getActiveByeDPIStrategy(sharedPreferences)
|
||||
|
||||
}
|
||||
|
||||
fun enableList(path: String, sharedPreferences: SharedPreferences) {
|
||||
if (sharedPreferences.getBoolean("use_module", false)) {
|
||||
val configFile = getConfigFile()
|
||||
@@ -210,14 +289,21 @@ fun enableList(path: String, sharedPreferences: SharedPreferences) {
|
||||
props.load(input)
|
||||
}
|
||||
}
|
||||
val activeLists = props.getProperty("activelists", "")
|
||||
.split(",")
|
||||
.filter { it.isNotBlank() }
|
||||
.toMutableList()
|
||||
val activeLists = props.getProperty(
|
||||
if (getHostListMode(sharedPreferences) == "whitelist") "active_lists"
|
||||
else "active_exclude_lists",
|
||||
"")
|
||||
.split(",")
|
||||
.filter { it.isNotBlank() }
|
||||
.toMutableList()
|
||||
if (path !in activeLists) {
|
||||
activeLists.add(path)
|
||||
}
|
||||
props.setProperty("activelists", activeLists.joinToString(","))
|
||||
props.setProperty(
|
||||
if (getHostListMode(sharedPreferences) == "whitelist") "active_lists"
|
||||
else "active_exclude_lists",
|
||||
activeLists.joinToString(",")
|
||||
)
|
||||
FileOutputStream(configFile).use { output ->
|
||||
props.store(output, "Don't place '/' in end of directory! Example: /sdcard")
|
||||
}
|
||||
@@ -226,14 +312,61 @@ fun enableList(path: String, sharedPreferences: SharedPreferences) {
|
||||
}
|
||||
}
|
||||
else {
|
||||
val currentSet = sharedPreferences.getStringSet("lists", emptySet())?.toMutableSet() ?: mutableSetOf()
|
||||
val currentSet = sharedPreferences.getStringSet(
|
||||
if (getHostListMode(sharedPreferences) == "whitelist") "lists"
|
||||
else "exclude_lists", emptySet())?.toMutableSet() ?: mutableSetOf()
|
||||
if (path !in currentSet) {
|
||||
currentSet.add(path)
|
||||
sharedPreferences.edit { putStringSet("lists", currentSet) }
|
||||
sharedPreferences.edit { putStringSet(
|
||||
if (getHostListMode(sharedPreferences) == "whitelist") "lists"
|
||||
else "exclude_lists", currentSet) }
|
||||
}
|
||||
}
|
||||
}
|
||||
fun enableIpset(path: String, sharedPreferences: SharedPreferences) {
|
||||
if (sharedPreferences.getBoolean("use_module", false)) {
|
||||
val configFile = getConfigFile()
|
||||
try {
|
||||
val props = Properties()
|
||||
if (configFile.exists()) {
|
||||
FileInputStream(configFile).use { input ->
|
||||
props.load(input)
|
||||
}
|
||||
}
|
||||
val activeLists = props.getProperty(
|
||||
if (getHostListMode(sharedPreferences) == "whitelist") "active_ipsets"
|
||||
else "active_exclude_ipsets",
|
||||
"")
|
||||
.split(",")
|
||||
.filter { it.isNotBlank() }
|
||||
.toMutableList()
|
||||
if (path !in activeLists) {
|
||||
activeLists.add(path)
|
||||
}
|
||||
props.setProperty(
|
||||
if (getHostListMode(sharedPreferences) == "whitelist") "active_ipsets"
|
||||
else "active_exclude_ipsets",
|
||||
activeLists.joinToString(",")
|
||||
)
|
||||
FileOutputStream(configFile).use { output ->
|
||||
props.store(output, "Don't place '/' in end of directory! Example: /sdcard")
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
throw RuntimeException(e)
|
||||
}
|
||||
}
|
||||
else {
|
||||
val currentSet = sharedPreferences.getStringSet(
|
||||
if (getHostListMode(sharedPreferences) == "whitelist") "ipsets"
|
||||
else "exclude_ipsets", emptySet())?.toMutableSet() ?: mutableSetOf()
|
||||
if (path !in currentSet) {
|
||||
currentSet.add(path)
|
||||
sharedPreferences.edit { putStringSet(
|
||||
if (getHostListMode(sharedPreferences) == "whitelist") "ipsets"
|
||||
else "exclude_ipsets", currentSet) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun enableStrategy(path: String, sharedPreferences: SharedPreferences) {
|
||||
if (sharedPreferences.getBoolean("use_module", false)) {
|
||||
val props = Properties()
|
||||
@@ -274,14 +407,21 @@ fun disableList(path: String, sharedPreferences: SharedPreferences) {
|
||||
props.load(input)
|
||||
}
|
||||
}
|
||||
val activeLists = props.getProperty("activelists", "")
|
||||
.split(",")
|
||||
.filter { it.isNotBlank() }
|
||||
.toMutableList()
|
||||
val activeLists = props.getProperty(
|
||||
if (getHostListMode(sharedPreferences) == "whitelist") "active_lists"
|
||||
else "active_exclude_lists",
|
||||
"")
|
||||
.split(",")
|
||||
.filter { it.isNotBlank() }
|
||||
.toMutableList()
|
||||
if (path in activeLists) {
|
||||
activeLists.remove(path)
|
||||
}
|
||||
props.setProperty("activelists", activeLists.joinToString(","))
|
||||
props.setProperty(
|
||||
if (getHostListMode(sharedPreferences) == "whitelist") "active_lists"
|
||||
else "active_exclude_lists",
|
||||
activeLists.joinToString(",")
|
||||
)
|
||||
FileOutputStream(configFile).use { output ->
|
||||
props.store(output, "Don't place '/' in end of directory! Example: /sdcard")
|
||||
}
|
||||
@@ -290,13 +430,71 @@ fun disableList(path: String, sharedPreferences: SharedPreferences) {
|
||||
}
|
||||
}
|
||||
else {
|
||||
val currentSet = sharedPreferences.getStringSet("lists", emptySet())?.toMutableSet() ?: mutableSetOf()
|
||||
val currentSet = sharedPreferences.getStringSet(
|
||||
if (getHostListMode(sharedPreferences) == "whitelist") "lists"
|
||||
else "exclude_lists", emptySet())?.toMutableSet() ?: mutableSetOf()
|
||||
if (path in currentSet) {
|
||||
currentSet.remove(path)
|
||||
sharedPreferences.edit { putStringSet("lists", currentSet) }
|
||||
sharedPreferences.edit { putStringSet(
|
||||
if (getHostListMode(sharedPreferences) == "whitelist") "lists"
|
||||
else "exclude_lists", currentSet) }
|
||||
}
|
||||
if (currentSet.isEmpty()) {
|
||||
sharedPreferences.edit { remove("lists") }
|
||||
sharedPreferences.edit { remove(
|
||||
if (getHostListMode(sharedPreferences) == "whitelist") "lists"
|
||||
else "exclude_lists"
|
||||
) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun disableIpset(path: String, sharedPreferences: SharedPreferences) {
|
||||
if (sharedPreferences.getBoolean("use_module", false)) {
|
||||
val props = Properties()
|
||||
val configFile = getConfigFile()
|
||||
try {
|
||||
if (configFile.exists()) {
|
||||
FileInputStream(configFile).use { input ->
|
||||
props.load(input)
|
||||
}
|
||||
}
|
||||
val activeLists = props.getProperty(
|
||||
if (getHostListMode(sharedPreferences) == "whitelist") "active_ipsets"
|
||||
else "active_exclude_ipsets",
|
||||
"")
|
||||
.split(",")
|
||||
.filter { it.isNotBlank() }
|
||||
.toMutableList()
|
||||
if (path in activeLists) {
|
||||
activeLists.remove(path)
|
||||
}
|
||||
props.setProperty(
|
||||
if (getHostListMode(sharedPreferences) == "whitelist") "active_ipsets"
|
||||
else "active_exclude_ipsets",
|
||||
activeLists.joinToString(",")
|
||||
)
|
||||
FileOutputStream(configFile).use { output ->
|
||||
props.store(output, "Don't place '/' in end of directory! Example: /sdcard")
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
throw RuntimeException(e)
|
||||
}
|
||||
}
|
||||
else {
|
||||
val currentSet = sharedPreferences.getStringSet(
|
||||
if (getHostListMode(sharedPreferences) == "whitelist") "ipsets"
|
||||
else "exclude_ipsets", emptySet())?.toMutableSet() ?: mutableSetOf()
|
||||
if (path in currentSet) {
|
||||
currentSet.remove(path)
|
||||
sharedPreferences.edit { putStringSet(
|
||||
if (getHostListMode(sharedPreferences) == "whitelist") "ipsets"
|
||||
else "exclude_ipsets", currentSet) }
|
||||
}
|
||||
if (currentSet.isEmpty()) {
|
||||
sharedPreferences.edit { remove(
|
||||
if (getHostListMode(sharedPreferences) == "whitelist") "ipsets"
|
||||
else "exclude_ipsets"
|
||||
) }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -517,7 +715,7 @@ fun getAppsListMode(prefs : SharedPreferences) : String {
|
||||
FileInputStream(configFile).use { input ->
|
||||
props.load(input)
|
||||
}
|
||||
val applist = props.getProperty("applist", "")!!
|
||||
val applist = props.getProperty("app_list", "")!!
|
||||
Log.d("App list", "Equals to $applist")
|
||||
return if (applist == "whitelist" || applist == "blacklist" || applist == "none") applist
|
||||
else "none"
|
||||
@@ -527,13 +725,68 @@ fun getAppsListMode(prefs : SharedPreferences) : String {
|
||||
}
|
||||
}
|
||||
else {
|
||||
return prefs.getString("applist", "")!!
|
||||
return prefs.getString("app_list", "none")!!
|
||||
}
|
||||
return "none"
|
||||
}
|
||||
|
||||
fun setAppsListMode(prefs: SharedPreferences, mode: String) {
|
||||
if (prefs.getBoolean("use_module", false)) {
|
||||
val configFile = getConfigFile()
|
||||
val props = Properties()
|
||||
if (configFile.exists()) {
|
||||
try {
|
||||
FileInputStream(configFile).use { input ->
|
||||
props.load(input)
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
throw RuntimeException(e)
|
||||
}
|
||||
}
|
||||
props.setProperty("app_list", mode)
|
||||
try {
|
||||
FileOutputStream(configFile).use { output ->
|
||||
props.store(output, "Don't place '/' in end of directory! Example: /sdcard")
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
throw RuntimeException(e)
|
||||
}
|
||||
}
|
||||
else {
|
||||
prefs.edit { putString("app_list", mode) }
|
||||
}
|
||||
Log.d("App List", "Changed to $mode")
|
||||
}
|
||||
fun setHostListMode(prefs: SharedPreferences, mode: String) {
|
||||
if (prefs.getBoolean("use_module", false)) {
|
||||
val configFile = getConfigFile()
|
||||
val props = Properties()
|
||||
if (configFile.exists()) {
|
||||
try {
|
||||
FileInputStream(configFile).use { input ->
|
||||
props.load(input)
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
throw RuntimeException(e)
|
||||
}
|
||||
}
|
||||
props.setProperty("list_type", mode)
|
||||
try {
|
||||
FileOutputStream(configFile).use { output ->
|
||||
props.store(output, "Don't place '/' in end of directory! Example: /sdcard")
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
throw RuntimeException(e)
|
||||
}
|
||||
}
|
||||
else {
|
||||
prefs.edit { putString("list_type", mode) }
|
||||
}
|
||||
Log.d("App List", "Changed to $mode")
|
||||
}
|
||||
|
||||
fun getHostListMode(prefs : SharedPreferences) : String {
|
||||
if(prefs.getBoolean("use_module", false)) {
|
||||
val configFile = getConfigFile()
|
||||
if (configFile.exists()) {
|
||||
val props = Properties()
|
||||
@@ -541,17 +794,16 @@ fun setAppsListMode(prefs: SharedPreferences, mode: String) {
|
||||
FileInputStream(configFile).use { input ->
|
||||
props.load(input)
|
||||
}
|
||||
props.setProperty("applist", mode)
|
||||
FileOutputStream(configFile).use { output ->
|
||||
props.store(output, "Don't place '/' in end of directory! Example: /sdcard")
|
||||
}
|
||||
val hostlist = props.getProperty("list_type", "whitelist")!!
|
||||
return if (hostlist == "whitelist" || hostlist == "blacklist") hostlist
|
||||
else "whitelist"
|
||||
} catch (e: IOException) {
|
||||
throw RuntimeException(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
prefs.edit { putString("applist", mode) }
|
||||
return prefs.getString("list_type", "whitelist")!!
|
||||
}
|
||||
Log.d("App List", "Changed to $mode")
|
||||
return "whitelist"
|
||||
}
|
||||
9
app/src/main/res/drawable/github.xml
Normal file
9
app/src/main/res/drawable/github.xml
Normal file
@@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M12,0.297c-6.63,0 -12,5.373 -12,12 0,5.303 3.438,9.8 8.205,11.385 0.6,0.113 0.82,-0.258 0.82,-0.577 0,-0.285 -0.01,-1.04 -0.015,-2.04 -3.338,0.724 -4.042,-1.61 -4.042,-1.61C4.422,18.07 3.633,17.7 3.633,17.7c-1.087,-0.744 0.084,-0.729 0.084,-0.729 1.205,0.084 1.838,1.236 1.838,1.236 1.07,1.835 2.809,1.305 3.495,0.998 0.108,-0.776 0.417,-1.305 0.76,-1.605 -2.665,-0.3 -5.466,-1.332 -5.466,-5.93 0,-1.31 0.465,-2.38 1.235,-3.22 -0.135,-0.303 -0.54,-1.523 0.105,-3.176 0,0 1.005,-0.322 3.3,1.23 0.96,-0.267 1.98,-0.399 3,-0.405 1.02,0.006 2.04,0.138 3,0.405 2.28,-1.552 3.285,-1.23 3.285,-1.23 0.645,1.653 0.24,2.873 0.12,3.176 0.765,0.84 1.23,1.91 1.23,3.22 0,4.61 -2.805,5.625 -5.475,5.92 0.42,0.36 0.81,1.096 0.81,2.22 0,1.606 -0.015,2.896 -0.015,3.286 0,0.315 0.21,0.69 0.825,0.57C20.565,22.092 24,17.592 24,12.297c0,-6.627 -5.373,-12 -12,-12"/>
|
||||
</vector>
|
||||
9
app/src/main/res/drawable/matrix.xml
Normal file
9
app/src/main/res/drawable/matrix.xml
Normal file
@@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M0.632,0.55v22.9L2.28,23.45L2.28,24L0,24L0,0h2.28v0.55zM7.675,7.81v1.157h0.033c0.309,-0.443 0.683,-0.784 1.117,-1.024 0.433,-0.245 0.936,-0.365 1.5,-0.365 0.54,0 1.033,0.107 1.481,0.314 0.448,0.208 0.785,0.582 1.02,1.108 0.254,-0.374 0.6,-0.706 1.034,-0.992 0.434,-0.287 0.95,-0.43 1.546,-0.43 0.453,0 0.872,0.056 1.26,0.167 0.388,0.11 0.716,0.286 0.993,0.53 0.276,0.245 0.489,0.559 0.646,0.951 0.152,0.392 0.23,0.863 0.23,1.417v5.728h-2.349L16.186,11.52c0,-0.286 -0.01,-0.559 -0.032,-0.812a1.755,1.755 0,0 0,-0.18 -0.66,1.106 1.106,0 0,0 -0.438,-0.448c-0.194,-0.11 -0.457,-0.166 -0.785,-0.166 -0.332,0 -0.6,0.064 -0.803,0.189a1.38,1.38 0,0 0,-0.48 0.499,1.946 1.946,0 0,0 -0.231,0.696 5.56,5.56 0,0 0,-0.06 0.785v4.768h-2.35v-4.8c0,-0.254 -0.004,-0.503 -0.018,-0.752a2.074,2.074 0,0 0,-0.143 -0.688,1.052 1.052,0 0,0 -0.415,-0.503c-0.194,-0.125 -0.476,-0.19 -0.854,-0.19 -0.111,0 -0.259,0.024 -0.439,0.074 -0.18,0.051 -0.36,0.143 -0.53,0.282 -0.171,0.138 -0.319,0.337 -0.439,0.595 -0.12,0.259 -0.18,0.6 -0.18,1.02v4.966L5.46,16.375L5.46,7.81zM23.368,23.45L23.368,0.55L21.72,0.55L21.72,0L24,0v24h-2.28v-0.55z"/>
|
||||
</vector>
|
||||
9
app/src/main/res/drawable/telegram.xml
Normal file
9
app/src/main/res/drawable/telegram.xml
Normal file
@@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M11.944,0A12,12 0,0 0,0 12a12,12 0,0 0,12 12,12 12,0 0,0 12,-12A12,12 0,0 0,12 0a12,12 0,0 0,-0.056 0zM16.906,7.224c0.1,-0.002 0.321,0.023 0.465,0.14a0.506,0.506 0,0 1,0.171 0.325c0.016,0.093 0.036,0.306 0.02,0.472 -0.18,1.898 -0.962,6.502 -1.36,8.627 -0.168,0.9 -0.499,1.201 -0.82,1.23 -0.696,0.065 -1.225,-0.46 -1.9,-0.902 -1.056,-0.693 -1.653,-1.124 -2.678,-1.8 -1.185,-0.78 -0.417,-1.21 0.258,-1.91 0.177,-0.184 3.247,-2.977 3.307,-3.23 0.007,-0.032 0.014,-0.15 -0.056,-0.212s-0.174,-0.041 -0.249,-0.024c-0.106,0.024 -1.793,1.14 -5.061,3.345 -0.48,0.33 -0.913,0.49 -1.302,0.48 -0.428,-0.008 -1.252,-0.241 -1.865,-0.44 -0.752,-0.245 -1.349,-0.374 -1.297,-0.789 0.027,-0.216 0.325,-0.437 0.893,-0.663 3.498,-1.524 5.83,-2.529 6.998,-3.014 3.332,-1.386 4.025,-1.627 4.476,-1.635z"/>
|
||||
</vector>
|
||||
@@ -5,6 +5,7 @@
|
||||
<string name="title_strategies">Стратегии</string>
|
||||
<string name="title_repo">Репозиторий</string>
|
||||
<string name="title_settings">Настройки</string>
|
||||
<string name="title_debug">Отладка</string>
|
||||
<string name="general_section">Основные настройки</string>
|
||||
<string name="byedpi_section">Настройки ByeDPI</string>
|
||||
<string name="btn_continue">Продолжить</string>
|
||||
@@ -48,6 +49,8 @@
|
||||
<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_update_repository_url">URL репозитория обновлений</string>
|
||||
<string name="hint_enter_update_repository_url">Введите URL репозитория обновлений</string>
|
||||
<string name="btn_ipv6">IPv6</string>
|
||||
<string name="btn_ip">IP-адрес</string>
|
||||
<string name="hint_ip">Введите IP-адрес</string>
|
||||
@@ -78,7 +81,7 @@
|
||||
<string name="qs_working">Работает</string>
|
||||
<string name="qs_not_working">Не работает</string>
|
||||
<string name="about_title">О приложении</string>
|
||||
<string name="about_text">Zaprett app от Cherret, egor-white\nВерсия: %1$s</string>
|
||||
<string name="about_text">zaprett app от Cherret, egor-white\nВерсия: %1$s</string>
|
||||
<string name="btn_whitelist">Белый список приложений</string>
|
||||
<string name="btn_blacklist">Чёрный список приложений</string>
|
||||
<string name="shared_section">Общие настройки</string>
|
||||
@@ -89,4 +92,29 @@
|
||||
<string name="radio_disabed">Выключено</string>
|
||||
<string name="btn_show_system_apps">Показывать системные приложения</string>
|
||||
<string name="search_field">Поиск…</string>
|
||||
<string name="error_text">Ошибка</string>
|
||||
<string name="error_server_data">Ошибка получения данных с сервера, пожалуйста проверьте интернет соединение</string>
|
||||
<string name="error_processing_data">Ошибка при обработке данных</string>
|
||||
<string name="error_unknown">Неизвестная ошибка</string>
|
||||
<string name="btn_copy_log">Скопировать лог</string>
|
||||
<string name="log_copied">Лог скопирован</string>
|
||||
<string name="download_error">Произошла ошибка скачивания, пожалуйста, сообщите о ней разработчикам</string>
|
||||
<string name="ipset_repo_url">URL репозитория ipset</string>
|
||||
<string name="title_selection">Подбор стратегии</string>
|
||||
<string name="btn_repository_url_ipsets">URL репозитория ipset</string>
|
||||
<string name="hint_enter_repository_url_ipsets">Введите URL репозитория ipset</string>
|
||||
<string name="strategy_status_waiting">ожидание...</string>
|
||||
<string name="strategy_status_testing">проверка...</string>
|
||||
<string name="strategy_status_tested">проверка завершена</string>
|
||||
<string name="strategy_applied">Стратегия применена</string>
|
||||
<string name="begin_selection">Начать подбор</string>
|
||||
<string name="begin_selection_snack">Начат подбор стратегии. Не закрывайте эту вкладку</string>
|
||||
<string name="change_probe_timeout">Изменить таймаут пробы</string>
|
||||
<string name="probe_timeout">Таймаут пробы</string>
|
||||
<string name="hint_enter_probe_timeout">Введите таймаут пробы</string>
|
||||
<string name="strategy_selection_info_title">Информация</string>
|
||||
<string name="strategy_selection_info_msg">"В этом разделе настроек приложения представлен перебор стратегий\n Подбор проходит среди скачанных стратегий, поэтому заранее скачайте из репозитория или добавьте из файловой системы интересующие вас стратегии для сравнения. \n Перед началом так же выберете один или несколько листов доменов на вкладке \"Листы\", затем нажмите на \"Начать подбор\". Не используйте для перебора списки с большим количеством доменов."</string>
|
||||
<string name="selection_no_hosts_title">Нет активных листов</string>
|
||||
<string name="selection_no_hosts_message">Не обнаружено активных списков хостов, включите один или несколько, иначе подбор не сработает</string>
|
||||
<string name="selection_available_domains">Доступные домены</string>
|
||||
</resources>
|
||||
@@ -5,6 +5,7 @@
|
||||
<string name="title_strategies">Strategies</string>
|
||||
<string name="title_repo">Repository</string>
|
||||
<string name="title_settings">Settings</string>
|
||||
<string name="title_debug">Debug</string>
|
||||
<string name="general_section">General settings</string>
|
||||
<string name="byedpi_section">ByeDPI settings</string>
|
||||
<string name="btn_continue">Continue</string>
|
||||
@@ -49,6 +50,8 @@
|
||||
<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_update_repository_url">Update repository URL</string>
|
||||
<string name="hint_enter_update_repository_url">Enter update repository URL</string>
|
||||
<string name="btn_ipv6">IPv6</string>
|
||||
<string name="btn_ip">IP address</string>
|
||||
<string name="hint_ip">Enter IP address</string>
|
||||
@@ -93,4 +96,30 @@
|
||||
<string name="radio_disabed">Disabled</string>
|
||||
<string name="btn_show_system_apps">Show system apps</string>
|
||||
<string name="search_field">Search…</string>
|
||||
<string name="error_text">Error</string>
|
||||
<string name="error_server_data">Error retrieving data from the server, please check your internet connection</string>
|
||||
<string name="error_processing_data">Error processing data</string>
|
||||
<string name="error_unknown">Unknown error</string>
|
||||
<string name="btn_copy_log">Copy log</string>
|
||||
<string name="log_copied">Log copied</string>
|
||||
<string name="download_error">Occurred a file download error, please report to developers</string>
|
||||
<string name="ipset_repo_url">Ipset repository URL</string>
|
||||
<string name="title_ipset" translatable="false">Ipset</string>
|
||||
<string name="title_selection">Strategy selection</string>
|
||||
<string name="btn_repository_url_ipsets">Ipsets repository url</string>
|
||||
<string name="hint_enter_repository_url_ipsets">Enter ipsets repository URL</string>
|
||||
<string name="strategy_status_waiting">waiting...</string>
|
||||
<string name="strategy_status_testing">testing...</string>
|
||||
<string name="strategy_status_tested">testing ended</string>
|
||||
<string name="strategy_applied">Strategy applied</string>
|
||||
<string name="begin_selection">Begin selection</string>
|
||||
<string name="begin_selection_snack">Strategy selection has begun. Please keep this tab open.</string>
|
||||
<string name="change_probe_timeout">Change probe timeout</string>
|
||||
<string name="probe_timeout">Probe timeout</string>
|
||||
<string name="hint_enter_probe_timeout">Enter probe timeout</string>
|
||||
<string name="strategy_selection_info_title">Tip</string>
|
||||
<string name="strategy_selection_info_msg">This section of the application settings allows you to iterate through strategies.\n The selection is based on downloaded strategies, so download the strategies you\'re interested in from the repository or add them from the file system for comparison.\n Before starting, select one or more domain lists in the \"Lists\" tab, then click \"Start selection\". Avoid using lists with a large number of domains.</string>
|
||||
<string name="selection_no_hosts_title">No active hosts</string>
|
||||
<string name="selection_no_hosts_message">No active host lists found, please enable one or more, otherwise the selection will not work</string>
|
||||
<string name="selection_available_domains">Available domains</string>
|
||||
</resources>
|
||||
@@ -1,4 +1,4 @@
|
||||
Все изменения здесь относятся к недавно добавленной настройке
|
||||
Добавление поля поиск
|
||||
Добавление возможности скрытия системных приложений из списка
|
||||
Исправление ошибок интерфейса
|
||||
1) Добавлена поддержка ipset
|
||||
2) JNI **полностью переписан** на Rust
|
||||
3) Добавлен подбор стратегии
|
||||
4) Исправлены баги
|
||||
|
||||
@@ -1,14 +1,24 @@
|
||||
[versions]
|
||||
agp = "8.10.1"
|
||||
kotlin = "2.0.21"
|
||||
coreKtx = "1.10.1"
|
||||
agp = "8.13.0"
|
||||
kotlin = "2.2.10"
|
||||
coreKtx = "1.17.0"
|
||||
junit = "4.13.2"
|
||||
junitVersion = "1.1.5"
|
||||
espressoCore = "3.5.1"
|
||||
lifecycleRuntimeKtx = "2.6.1"
|
||||
activityCompose = "1.8.0"
|
||||
composeBom = "2024.09.00"
|
||||
lifecycleService = "2.9.1"
|
||||
junitVersion = "1.3.0"
|
||||
espressoCore = "3.7.0"
|
||||
lifecycleRuntimeKtx = "2.9.2"
|
||||
activityCompose = "1.10.1"
|
||||
composeBom = "2025.08.00"
|
||||
compose-material3 = "1.3.2"
|
||||
compose-material3-adaptive = "1.4.0-beta02"
|
||||
navigation = "2.9.3"
|
||||
compose-icons = "1.7.8"
|
||||
libsu = "6.0.0"
|
||||
okhttp = "5.1.0"
|
||||
serialization = "1.9.0"
|
||||
firebase-bom = "34.1.0"
|
||||
fragment-compose = "1.8.9"
|
||||
coil3 = "3.3.0"
|
||||
compose-markdown = "0.5.7"
|
||||
|
||||
[libraries]
|
||||
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
|
||||
@@ -25,6 +35,21 @@ 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" }
|
||||
compose-material3 = { group = "androidx.compose.material3", name = "material3", version.ref = "compose-material3" }
|
||||
compose-material3-window-size = { group = "androidx.compose.material3", name = "material3-window-size-class", version.ref = "compose-material3" }
|
||||
compose-material3-adaptive-nav = { group = "androidx.compose.material3", name = "material3-adaptive-navigation-suite", version.ref = "compose-material3-adaptive" }
|
||||
navigation-compose = { group = "androidx.navigation", name = "navigation-compose", version.ref = "navigation" }
|
||||
compose-icons = { group = "androidx.compose.material", name = "material-icons-extended", version.ref = "compose-icons" }
|
||||
libsu-core = { group = "com.github.topjohnwu.libsu", name = "core", version.ref = "libsu" }
|
||||
okhttp = { group = "com.squareup.okhttp3", name = "okhttp", version.ref = "okhttp" }
|
||||
serialization-json = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-json", version.ref = "serialization" }
|
||||
firebase-bom = { group = "com.google.firebase", name = "firebase-bom", version.ref = "firebase-bom" }
|
||||
firebase-analytics = { group = "com.google.firebase", name = "firebase-analytics" }
|
||||
firebase-crashlytics = { group = "com.google.firebase", name = "firebase-crashlytics" }
|
||||
fragment-compose = { group = "androidx.fragment", name = "fragment-compose", version.ref = "fragment-compose" }
|
||||
coil-compose = { group = "io.coil-kt.coil3", name = "coil-compose", version.ref = "coil3" }
|
||||
compose-markdown = { module = "com.github.jeziellago:compose-markdown", version.ref = "compose-markdown" }
|
||||
|
||||
|
||||
[plugins]
|
||||
android-application = { id = "com.android.application", version.ref = "agp" }
|
||||
|
||||
2
gradle/wrapper/gradle-wrapper.properties
vendored
2
gradle/wrapper/gradle-wrapper.properties
vendored
@@ -1,6 +1,6 @@
|
||||
#Sat Mar 22 14:00:49 GMT+07:00 2025
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
|
||||
@@ -5,7 +5,7 @@ project(byedpi_native)
|
||||
file(GLOB BYE_DPI_SRC byedpi/*.c)
|
||||
list(REMOVE_ITEM BYE_DPI_SRC ${CMAKE_CURRENT_SOURCE_DIR}/byedpi/win_service.c)
|
||||
|
||||
add_library(byedpi SHARED ${BYE_DPI_SRC} native-lib.c utils.c)
|
||||
add_library(byedpi STATIC ${BYE_DPI_SRC})
|
||||
target_include_directories(byedpi PRIVATE byedpi)
|
||||
|
||||
target_compile_options(byedpi PRIVATE -std=c99 -O2 -Wall -Wno-unused -Wextra -Wno-unused-parameter -pedantic)
|
||||
349
rust/Cargo.lock
generated
Normal file
349
rust/Cargo.lock
generated
Normal file
@@ -0,0 +1,349 @@
|
||||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "aho-corasick"
|
||||
version = "1.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "android_log-sys"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "84521a3cf562bc62942e294181d9eef17eb38ceb8c68677bc49f144e4c3d4f8d"
|
||||
|
||||
[[package]]
|
||||
name = "android_logger"
|
||||
version = "0.15.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dbb4e440d04be07da1f1bf44fb4495ebd58669372fe0cffa6e48595ac5bd88a3"
|
||||
dependencies = [
|
||||
"android_log-sys",
|
||||
"env_filter",
|
||||
"log",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "byedpi"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"android_logger",
|
||||
"cmake",
|
||||
"jni",
|
||||
"libc",
|
||||
"log",
|
||||
"once_cell",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bytes"
|
||||
version = "1.10.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a"
|
||||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.2.40"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e1d05d92f4b1fd76aad469d46cdd858ca761576082cd37df81416691e50199fb"
|
||||
dependencies = [
|
||||
"find-msvc-tools",
|
||||
"shlex",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cesu8"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c"
|
||||
|
||||
[[package]]
|
||||
name = "cfg-if"
|
||||
version = "1.0.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9"
|
||||
|
||||
[[package]]
|
||||
name = "cmake"
|
||||
version = "0.1.54"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e7caa3f9de89ddbe2c607f4101924c5abec803763ae9534e4f4d7d8f84aa81f0"
|
||||
dependencies = [
|
||||
"cc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "combine"
|
||||
version = "4.6.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "env_filter"
|
||||
version = "0.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "186e05a59d4c50738528153b83b0b0194d3a29507dfec16eccd4b342903397d0"
|
||||
dependencies = [
|
||||
"log",
|
||||
"regex",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "find-msvc-tools"
|
||||
version = "0.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0399f9d26e5191ce32c498bebd31e7a3ceabc2745f0ac54af3f335126c3f24b3"
|
||||
|
||||
[[package]]
|
||||
name = "jni"
|
||||
version = "0.21.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97"
|
||||
dependencies = [
|
||||
"cesu8",
|
||||
"cfg-if",
|
||||
"combine",
|
||||
"jni-sys",
|
||||
"log",
|
||||
"thiserror",
|
||||
"walkdir",
|
||||
"windows-sys 0.45.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jni-sys"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130"
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.176"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "58f929b4d672ea937a23a1ab494143d968337a5f47e56d0815df1e0890ddf174"
|
||||
|
||||
[[package]]
|
||||
name = "log"
|
||||
version = "0.4.28"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432"
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.7.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273"
|
||||
|
||||
[[package]]
|
||||
name = "once_cell"
|
||||
version = "1.21.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.101"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.41"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ce25767e7b499d1b604768e7cde645d14cc8584231ea6b295e9c9eb22c02e1d1"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex"
|
||||
version = "1.11.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8b5288124840bee7b386bc413c487869b360b2b4ec421ea56425128692f2a82c"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
"regex-automata",
|
||||
"regex-syntax",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex-automata"
|
||||
version = "0.4.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "833eb9ce86d40ef33cb1306d8accf7bc8ec2bfea4355cbdebb3df68b40925cad"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
"regex-syntax",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex-syntax"
|
||||
version = "0.8.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "caf4aa5b0f434c91fe5c7f1ecb6a5ece2130b02ad2a590589dda5146df959001"
|
||||
|
||||
[[package]]
|
||||
name = "same-file"
|
||||
version = "1.0.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
|
||||
dependencies = [
|
||||
"winapi-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "shlex"
|
||||
version = "1.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.106"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "1.0.69"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
|
||||
dependencies = [
|
||||
"thiserror-impl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror-impl"
|
||||
version = "1.0.69"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f63a545481291138910575129486daeaf8ac54aee4387fe7906919f7830c7d9d"
|
||||
|
||||
[[package]]
|
||||
name = "walkdir"
|
||||
version = "2.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b"
|
||||
dependencies = [
|
||||
"same-file",
|
||||
"winapi-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winapi-util"
|
||||
version = "0.1.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
|
||||
dependencies = [
|
||||
"windows-sys 0.61.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-link"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "45e46c0661abb7180e7b9c281db115305d49ca1709ab8242adf09666d2173c65"
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.45.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0"
|
||||
dependencies = [
|
||||
"windows-targets",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.61.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6f109e41dd4a3c848907eb83d5a42ea98b3769495597450cf6d153507b166f0f"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-targets"
|
||||
version = "0.42.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071"
|
||||
dependencies = [
|
||||
"windows_aarch64_gnullvm",
|
||||
"windows_aarch64_msvc",
|
||||
"windows_i686_gnu",
|
||||
"windows_i686_msvc",
|
||||
"windows_x86_64_gnu",
|
||||
"windows_x86_64_gnullvm",
|
||||
"windows_x86_64_msvc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_gnullvm"
|
||||
version = "0.42.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8"
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_msvc"
|
||||
version = "0.42.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnu"
|
||||
version = "0.42.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_msvc"
|
||||
version = "0.42.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnu"
|
||||
version = "0.42.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnullvm"
|
||||
version = "0.42.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_msvc"
|
||||
version = "0.42.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0"
|
||||
17
rust/Cargo.toml
Normal file
17
rust/Cargo.toml
Normal file
@@ -0,0 +1,17 @@
|
||||
[package]
|
||||
name = "byedpi"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
jni = "0.21.1"
|
||||
libc = "0.2.0"
|
||||
android_logger = "0.15.1"
|
||||
log = "0.4"
|
||||
once_cell = "1.21.3"
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib"]
|
||||
|
||||
[build-dependencies]
|
||||
cmake = "0.1.49"
|
||||
8
rust/build.rs
Normal file
8
rust/build.rs
Normal file
@@ -0,0 +1,8 @@
|
||||
use cmake::Config;
|
||||
|
||||
fn main() {
|
||||
let dst = Config::new(".").build_target("byedpi").build();
|
||||
let lib_path = dst.join("build");
|
||||
println!("cargo:rustc-link-search=native={}", lib_path.display());
|
||||
println!("cargo:rustc-link-lib=static=byedpi");
|
||||
}
|
||||
1
rust/byedpi
Submodule
1
rust/byedpi
Submodule
Submodule rust/byedpi added at 7efde1b129
74
rust/src/lib.rs
Normal file
74
rust/src/lib.rs
Normal file
@@ -0,0 +1,74 @@
|
||||
use android_logger::Config;
|
||||
use jni::JNIEnv;
|
||||
use jni::objects::{JClass, JObjectArray, JString};
|
||||
use jni::sys::jint;
|
||||
use libc::{SHUT_RDWR, shutdown};
|
||||
use log::{LevelFilter, info};
|
||||
use std::ffi::CString;
|
||||
use std::os::raw::c_char;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
|
||||
static PROXY_RUNNING: AtomicBool = AtomicBool::new(false);
|
||||
|
||||
#[link(name = "byedpi", kind = "static")]
|
||||
unsafe extern "C" {
|
||||
static mut server_fd: i32;
|
||||
static mut optind: i32;
|
||||
static mut optreset: i32;
|
||||
fn main(argc: libc::c_int, argv: *const *const c_char) -> libc::c_int;
|
||||
fn clear_params();
|
||||
}
|
||||
|
||||
fn init_logger() {
|
||||
android_logger::init_once(Config::default().with_max_level(LevelFilter::Info));
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "system" fn Java_com_cherret_zaprett_byedpi_NativeBridge_jniStartProxy(
|
||||
mut env: JNIEnv,
|
||||
_class: JClass,
|
||||
args: JObjectArray,
|
||||
) -> jint {
|
||||
init_logger();
|
||||
if PROXY_RUNNING.swap(true, Ordering::SeqCst) {
|
||||
info!("proxy already running");
|
||||
return -1;
|
||||
}
|
||||
let argc = env.get_array_length(&args).unwrap_or(0) as usize;
|
||||
let mut cstrings: Vec<CString> = Vec::with_capacity(argc);
|
||||
for i in 0..argc {
|
||||
let jstr: JString = env
|
||||
.get_object_array_element(&args, i as i32)
|
||||
.unwrap()
|
||||
.into();
|
||||
let rust_str: String = env.get_string(&jstr).unwrap().into();
|
||||
cstrings.push(CString::new(rust_str).unwrap());
|
||||
}
|
||||
let mut argv: Vec<*const c_char> = cstrings.iter().map(|s| s.as_ptr()).collect();
|
||||
argv.push(std::ptr::null());
|
||||
info!("starting proxy");
|
||||
unsafe {
|
||||
optind = 1;
|
||||
optreset = 1;
|
||||
clear_params();
|
||||
}
|
||||
PROXY_RUNNING.store(true, Ordering::SeqCst);
|
||||
let ret = unsafe { main(argc as i32, argv.as_ptr()) };
|
||||
PROXY_RUNNING.store(false, Ordering::SeqCst);
|
||||
ret as jint
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "system" fn Java_com_cherret_zaprett_byedpi_NativeBridge_jniStopProxy(
|
||||
_env: JNIEnv,
|
||||
_class: JClass,
|
||||
) -> jint {
|
||||
init_logger();
|
||||
if !PROXY_RUNNING.load(Ordering::SeqCst) {
|
||||
info!("failed to stop proxy");
|
||||
return -1;
|
||||
}
|
||||
info!("stopping proxy");
|
||||
let ret = unsafe { shutdown(server_fd, SHUT_RDWR) };
|
||||
ret as jint
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"version": "2.5",
|
||||
"versionCode": 17,
|
||||
"downloadUrl": "https://github.com/CherretGit/zaprett-app/releases/download/2_5/app-release.apk",
|
||||
"version": "2.10",
|
||||
"versionCode": 22,
|
||||
"downloadUrl": "https://github.com/CherretGit/zaprett-app/releases/download/2.10.0/app-release.apk",
|
||||
"changelogUrl": "https://raw.githubusercontent.com/CherretGit/zaprett-app/refs/heads/main/changelog.md"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user