mirror of
https://github.com/CherretGit/zaprett-app.git
synced 2025-12-10 13:39:41 +05:00
Compare commits
91 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
80abf2380a | ||
|
|
35b0068b8c | ||
|
|
cdcd8b85b5 | ||
|
|
fd69451adc | ||
|
|
611e8ae39c | ||
|
|
21e80d0463 | ||
|
|
98f2af3853 | ||
|
|
2dd181494a | ||
|
|
77823252f2 | ||
|
|
87a9740aad | ||
|
|
efb60d0c93 | ||
|
|
f0c3baa3bd | ||
|
|
d00677270a | ||
|
|
57217bbe1c | ||
|
|
6f9bd86d70 | ||
|
|
80f5934060 | ||
|
|
0065b8a92b | ||
|
|
b5e3d256a6 | ||
|
|
77bbd41609 | ||
|
|
4e1568b405 | ||
|
|
b8844c8cca | ||
|
|
1d6eb32473 | ||
|
|
ded89165dc | ||
|
|
ed7452b7cb | ||
|
|
717368b4e9 | ||
|
|
36885a8410 | ||
|
|
ad614b3b4a | ||
|
|
5c8425573a | ||
|
|
59e30206f7 | ||
|
|
c4dcf934aa | ||
|
|
1bbeb4e987 | ||
|
|
003a4c57a3 | ||
|
|
432873409c | ||
|
|
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 |
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 add 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
|
||||
|
||||
21
.idea/appInsightsSettings.xml
generated
21
.idea/appInsightsSettings.xml
generated
@@ -2,5 +2,26 @@
|
||||
<project version="4">
|
||||
<component name="AppInsightsSettings">
|
||||
<option name="selectedTabId" value="Android Vitals" />
|
||||
<option name="tabSettings">
|
||||
<map>
|
||||
<entry key="Firebase Crashlytics">
|
||||
<value>
|
||||
<InsightsFilterSettings>
|
||||
<option name="connection">
|
||||
<ConnectionSetting>
|
||||
<option name="appId" value="com.cherret.zaprett" />
|
||||
<option name="mobileSdkAppId" value="1:1005804036856:android:e7db5546b8bb4daf91510d" />
|
||||
<option name="projectId" value="zaprett-app" />
|
||||
<option name="projectNumber" value="1005804036856" />
|
||||
</ConnectionSetting>
|
||||
</option>
|
||||
<option name="signal" value="SIGNAL_UNSPECIFIED" />
|
||||
<option name="timeIntervalDays" value="THIRTY_DAYS" />
|
||||
<option name="visibilityType" value="ALL" />
|
||||
</InsightsFilterSettings>
|
||||
</value>
|
||||
</entry>
|
||||
</map>
|
||||
</option>
|
||||
</component>
|
||||
</project>
|
||||
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>
|
||||
33
README.md
33
README.md
@@ -1,19 +1,42 @@
|
||||
# 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,6 +5,7 @@ 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 {
|
||||
@@ -15,19 +16,10 @@ android {
|
||||
applicationId = "com.cherret.zaprett"
|
||||
minSdk = 29
|
||||
targetSdk = 35
|
||||
versionCode = 19
|
||||
versionName = "2.7"
|
||||
versionCode = 25
|
||||
versionName = "2.13"
|
||||
|
||||
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,6 +73,27 @@ 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(libs.compose.material3)
|
||||
implementation(libs.compose.material3.window.size)
|
||||
@@ -89,6 +108,7 @@ dependencies {
|
||||
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);
|
||||
@@ -1,3 +1,4 @@
|
||||
@file:OptIn(ExperimentalMaterial3ExpressiveApi::class)
|
||||
package com.cherret.zaprett
|
||||
|
||||
import android.Manifest
|
||||
@@ -22,7 +23,9 @@ 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.ExperimentalMaterial3ExpressiveApi
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.NavigationBar
|
||||
import androidx.compose.material3.NavigationBarItem
|
||||
@@ -46,16 +49,21 @@ 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.cherret.zaprett.utils.checkStoragePermission
|
||||
import com.google.firebase.Firebase
|
||||
import com.google.firebase.analytics.FirebaseAnalytics
|
||||
import com.google.firebase.analytics.analytics
|
||||
@@ -64,10 +72,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>
|
||||
@@ -104,16 +113,7 @@ class MainActivity : ComponentActivity() {
|
||||
}
|
||||
}
|
||||
var showStoragePermissionDialog by remember {
|
||||
mutableStateOf(
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
!Environment.isExternalStorageManager()
|
||||
} else {
|
||||
ContextCompat.checkSelfPermission(
|
||||
this,
|
||||
Manifest.permission.WRITE_EXTERNAL_STORAGE
|
||||
) != PackageManager.PERMISSION_GRANTED
|
||||
}
|
||||
)
|
||||
mutableStateOf(!checkStoragePermission(this))
|
||||
}
|
||||
var showNotificationPermissionDialog by remember {
|
||||
mutableStateOf(
|
||||
@@ -122,7 +122,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 +189,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 +214,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 +223,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) }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -259,5 +266,4 @@ class MainActivity : ComponentActivity() {
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -14,7 +14,11 @@ 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
|
||||
@@ -174,7 +178,7 @@ class ByeDpiVpnService : VpnService() {
|
||||
try {
|
||||
vpnInterface?.close()
|
||||
vpnInterface = null
|
||||
NativeBridge().stopProxy()
|
||||
NativeBridge().jniStopProxy()
|
||||
TProxyService.TProxyStopService()
|
||||
status = ServiceStatus.Disconnected
|
||||
} catch (e: Exception) {
|
||||
@@ -186,9 +190,16 @@ class ByeDpiVpnService : VpnService() {
|
||||
val socksIp = sharedPreferences.getString("ip", "127.0.0.1")?: "127.0.0.1"
|
||||
val socksPort = sharedPreferences.getString("port", "1080")?: "1080"
|
||||
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), sharedPreferences)
|
||||
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 {
|
||||
@@ -205,11 +216,15 @@ class ByeDpiVpnService : VpnService() {
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -218,20 +233,52 @@ class ByeDpiVpnService : VpnService() {
|
||||
return ""
|
||||
}
|
||||
|
||||
private fun parseArgs(ip: String, port: String, rawArgs: List<String>, list : String, sharedPreferences: SharedPreferences): Array<String> {
|
||||
private suspend fun prepareIpset(actsets: Array<String>): String {
|
||||
if (actsets.isNotEmpty()) {
|
||||
val lists: Array<File> = actsets.map { File(it) }.toTypedArray()
|
||||
val hostlist = withContext(Dispatchers.IO) {
|
||||
File.createTempFile("ipset", ".txt", cacheDir)
|
||||
}.apply { deleteOnExit() }
|
||||
withContext(Dispatchers.IO) {
|
||||
hostlist.printWriter().use { out ->
|
||||
lists.forEach {
|
||||
if (it.exists()) {
|
||||
it.bufferedReader().useLines {
|
||||
it.forEach {
|
||||
out.println(it)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
disableList(it.name, sharedPreferences)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return hostlist.absolutePath
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
private fun parseArgs(ip: String, port: String, rawArgs: List<String>, list : String, ipset : String, sharedPreferences: SharedPreferences): Array<String> {
|
||||
val regex = Regex("""--?\S+(?:=(?:[^"'\s]+|"[^"]*"|'[^']*'))?|[^\s]+""")
|
||||
val parsedArgs = rawArgs
|
||||
.flatMap { args -> regex.findAll(args).map { it.value } }
|
||||
.flatMap { arg ->
|
||||
if (getHostListMode(sharedPreferences) == "whitelist") {
|
||||
when {
|
||||
arg == "\$hostlist" && list.isNotEmpty() -> listOf("-H", list)
|
||||
arg == "\$hostlist" && list.isNotEmpty() -> listOf("--hosts", list)
|
||||
arg == "\$hostlist" && list.isEmpty() -> emptyList()
|
||||
arg == "\$ipset" && list.isNotEmpty() -> listOf("--ipset", list)
|
||||
arg == "\$ipset" && list.isEmpty() -> emptyList()
|
||||
else -> listOf(arg)
|
||||
}
|
||||
} else {
|
||||
when {
|
||||
arg == "\$hostlist" && list.isNotEmpty() -> listOf("--hosts", list, "-An", list)
|
||||
arg == "\$ipset" && ipset.isNotEmpty() -> listOf("--ipset", ipset, "-An", ipset)
|
||||
arg == "\$hostlist" || arg == "\$ipset" -> emptyList()
|
||||
else -> listOf(arg)
|
||||
}
|
||||
else {
|
||||
listOf("-H", list, "-An", arg)
|
||||
}
|
||||
}
|
||||
.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
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
package com.cherret.zaprett.data
|
||||
|
||||
enum class ItemType {
|
||||
byedpi, nfqws, list, list_exclude
|
||||
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)
|
||||
}
|
||||
26
app/src/main/java/com/cherret/zaprett/data/ZaprettConfig.kt
Normal file
26
app/src/main/java/com/cherret/zaprett/data/ZaprettConfig.kt
Normal file
@@ -0,0 +1,26 @@
|
||||
package com.cherret.zaprett.data
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class ZaprettConfig(
|
||||
@SerialName("active_lists")
|
||||
val activeLists: List<String> = emptyList(),
|
||||
@SerialName("active_ipsets")
|
||||
val activeIpsets: List<String> = emptyList(),
|
||||
@SerialName("active_exclude_lists")
|
||||
val activeExcludeLists: List<String> = emptyList(),
|
||||
@SerialName("active_exclude_ipsets")
|
||||
val activeExcludeIpsets: List<String> = emptyList(),
|
||||
@SerialName("list_type")
|
||||
val listType: String = "whitelist",
|
||||
@SerialName("strategy")
|
||||
val strategy: String = "",
|
||||
@SerialName("app_list")
|
||||
val appList: String = "none",
|
||||
@SerialName("whitelist")
|
||||
val whitelist: List<String> = emptyList(),
|
||||
@SerialName("blacklist")
|
||||
val blacklist: List<String> = emptyList()
|
||||
)
|
||||
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))
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
157
app/src/main/java/com/cherret/zaprett/ui/screen/DebugScreen.kt
Normal file
157
app/src/main/java/com/cherret/zaprett/ui/screen/DebugScreen.kt
Normal file
@@ -0,0 +1,157 @@
|
||||
@file:OptIn(ExperimentalMaterial3ExpressiveApi::class)
|
||||
package com.cherret.zaprett.ui.screen
|
||||
|
||||
import android.app.Activity
|
||||
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.AlertDialog
|
||||
import androidx.compose.material3.DropdownMenu
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
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 showResetSettingsDialog = remember { mutableStateOf(false) }
|
||||
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
|
||||
}
|
||||
),
|
||||
Setting.Action(
|
||||
title = stringResource(R.string.reset_settings_title),
|
||||
onClick = {
|
||||
showResetSettingsDialog.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 })
|
||||
}
|
||||
|
||||
if (showResetSettingsDialog.value) {
|
||||
AlertDialog(title = { Text(text = stringResource(R.string.reset_settings_title)) },
|
||||
text = { Text(text = stringResource(R.string.reset_settings_message)) },
|
||||
onDismissRequest = {
|
||||
showResetSettingsDialog.value = false
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = {
|
||||
showResetSettingsDialog.value = false
|
||||
}) {
|
||||
Text(stringResource(R.string.btn_dismiss))
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(onClick = {
|
||||
context.deleteSharedPreferences("settings")
|
||||
showResetSettingsDialog.value = false
|
||||
val activity = context as Activity
|
||||
val intent = activity.intent
|
||||
activity.finish()
|
||||
activity.startActivity(intent)
|
||||
}) {
|
||||
Text(text = stringResource(R.string.btn_continue))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,14 @@
|
||||
@file:OptIn(ExperimentalMaterial3ExpressiveApi::class)
|
||||
package com.cherret.zaprett.ui.screen
|
||||
|
||||
import android.content.ClipData
|
||||
import android.content.ClipboardManager
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.SharedPreferences
|
||||
import android.net.VpnService
|
||||
import android.os.Build
|
||||
import android.widget.Toast
|
||||
import androidx.activity.result.ActivityResultLauncher
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.expandVertically
|
||||
@@ -35,6 +40,7 @@ import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.CardDefaults
|
||||
import androidx.compose.material3.ElevatedCard
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
|
||||
import androidx.compose.material3.FilledTonalButton
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.Icon
|
||||
@@ -65,10 +71,15 @@ 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.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
|
||||
import kotlinx.serialization.SerializationException
|
||||
import java.io.IOException
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
@@ -78,8 +89,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
|
||||
@@ -88,6 +98,7 @@ fun HomeScreen(viewModel: HomeViewModel = viewModel(), vpnLauncher: ActivityResu
|
||||
val nfqwsVer = viewModel.nfqwsVer;
|
||||
val byedpiVer = viewModel.byedpiVer;
|
||||
val serviceMode = viewModel.serviceMode
|
||||
val error by viewModel.errorFlow.collectAsState()
|
||||
LaunchedEffect(Unit) {
|
||||
viewModel.checkForUpdate()
|
||||
viewModel.checkServiceStatus()
|
||||
@@ -106,6 +117,37 @@ fun HomeScreen(viewModel: HomeViewModel = viewModel(), vpnLauncher: ActivityResu
|
||||
}
|
||||
}
|
||||
|
||||
if (error.isNotEmpty()) {
|
||||
AlertDialog(
|
||||
onDismissRequest = {
|
||||
viewModel.clearError()
|
||||
},
|
||||
title = { Text(stringResource(R.string.error_text)) },
|
||||
text = {
|
||||
Text(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)
|
||||
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()
|
||||
}) {
|
||||
Text(stringResource(R.string.btn_continue))
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
@@ -124,7 +166,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 +184,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 +204,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 +237,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 +361,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,54 +1,54 @@
|
||||
@file:OptIn(ExperimentalMaterial3ExpressiveApi::class)
|
||||
package com.cherret.zaprett.ui.screen
|
||||
|
||||
import android.content.ClipData
|
||||
import android.content.ClipboardManager
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import android.os.Build
|
||||
import android.widget.Toast
|
||||
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.height
|
||||
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.AlertDialog
|
||||
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.ExperimentalMaterial3ExpressiveApi
|
||||
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.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.MutableState
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
@@ -58,12 +58,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.HostsViewModel
|
||||
import com.cherret.zaprett.utils.getHostListMode
|
||||
|
||||
@@ -77,6 +77,7 @@ fun HostsScreen(navController: NavController, viewModel: HostsViewModel = viewMo
|
||||
val allLists = viewModel.allItems
|
||||
val checked = viewModel.checked
|
||||
val isRefreshing = viewModel.isRefreshing
|
||||
val showPermissionDialog by viewModel.showNoPermissionDialog.collectAsState()
|
||||
val filePickerLauncher = rememberLauncherForActivityResult(
|
||||
contract = ActivityResultContracts.OpenDocument()
|
||||
) { uri ->
|
||||
@@ -84,11 +85,43 @@ fun HostsScreen(navController: NavController, viewModel: HostsViewModel = viewMo
|
||||
if (getHostListMode(prefs) == "whitelist") viewModel.copySelectedFile(context, "/lists/include", it)
|
||||
else viewModel.copySelectedFile(context, "/lists/exclude", it) }
|
||||
}
|
||||
val error by viewModel.errorFlow.collectAsState()
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
viewModel.refresh()
|
||||
}
|
||||
|
||||
if (error.isNotEmpty()) {
|
||||
AlertDialog(
|
||||
onDismissRequest = {
|
||||
viewModel.clearError()
|
||||
},
|
||||
title = { Text(stringResource(R.string.error_text)) },
|
||||
text = {
|
||||
Text(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)
|
||||
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()
|
||||
}) {
|
||||
Text(stringResource(R.string.btn_continue))
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
@@ -111,8 +144,11 @@ 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)
|
||||
@@ -133,7 +169,7 @@ fun HostsScreen(navController: NavController, viewModel: HostsViewModel = viewMo
|
||||
}
|
||||
else -> {
|
||||
items(allLists) { item ->
|
||||
HostItem (
|
||||
ListSwitchItem (
|
||||
item = item,
|
||||
isChecked = checked[item] == true,
|
||||
onCheckedChange = { isChecked ->
|
||||
@@ -154,43 +190,23 @@ fun HostsScreen(navController: NavController, viewModel: HostsViewModel = viewMo
|
||||
},
|
||||
snackbarHost = { SnackbarHost(hostState = snackbarHostState) }
|
||||
)
|
||||
if (showPermissionDialog) {
|
||||
AlertDialog(
|
||||
title = { Text(text = stringResource(R.string.error_no_storage_title)) },
|
||||
text = { Text(text = stringResource(R.string.no_storage_permission_message)) },
|
||||
onDismissRequest = {
|
||||
viewModel.hideNoPermissionDialog()
|
||||
navController.popBackStack()
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(onClick = {
|
||||
viewModel.hideNoPermissionDialog()
|
||||
navController.popBackStack()
|
||||
}) {
|
||||
Text(stringResource(R.string.btn_continue))
|
||||
}
|
||||
|
||||
@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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
281
app/src/main/java/com/cherret/zaprett/ui/screen/IpsetsScreen.kt
Normal file
281
app/src/main/java/com/cherret/zaprett/ui/screen/IpsetsScreen.kt
Normal file
@@ -0,0 +1,281 @@
|
||||
@file:OptIn(ExperimentalMaterial3ExpressiveApi::class)
|
||||
package com.cherret.zaprett.ui.screen
|
||||
|
||||
import android.content.ClipData
|
||||
import android.content.ClipboardManager
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import android.os.Build
|
||||
import android.widget.Toast
|
||||
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.AlertDialog
|
||||
import androidx.compose.material3.DropdownMenu
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
|
||||
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.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.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 showPermissionDialog by viewModel.showNoPermissionDialog.collectAsState()
|
||||
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) }
|
||||
}
|
||||
val error by viewModel.errorFlow.collectAsState()
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
viewModel.refresh()
|
||||
}
|
||||
|
||||
if (error.isNotEmpty()) {
|
||||
AlertDialog(
|
||||
onDismissRequest = {
|
||||
viewModel.clearError()
|
||||
},
|
||||
title = { Text(stringResource(R.string.error_text)) },
|
||||
text = {
|
||||
Text(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)
|
||||
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()
|
||||
}) {
|
||||
Text(stringResource(R.string.btn_continue))
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
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) }
|
||||
)
|
||||
if (showPermissionDialog) {
|
||||
AlertDialog(
|
||||
title = { Text(text = stringResource(R.string.error_no_storage_title)) },
|
||||
text = { Text(text = stringResource(R.string.no_storage_permission_message)) },
|
||||
onDismissRequest = {
|
||||
viewModel.hideNoPermissionDialog()
|
||||
navController.popBackStack()
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(onClick = {
|
||||
viewModel.hideNoPermissionDialog()
|
||||
navController.popBackStack()
|
||||
}) {
|
||||
Text(stringResource(R.string.btn_continue))
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@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,35 @@
|
||||
@file:OptIn(ExperimentalMaterial3ExpressiveApi::class)
|
||||
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.ExperimentalMaterial3ExpressiveApi
|
||||
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 +39,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 +57,102 @@ 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()
|
||||
val showPermissionDialog by viewModel.showPermissionDialog.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))
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
if (showPermissionDialog) {
|
||||
AlertDialog(
|
||||
title = { Text(text = stringResource(R.string.error_no_storage_title)) },
|
||||
text = { Text(text = stringResource(R.string.no_storage_permission_message)) },
|
||||
onDismissRequest = {
|
||||
viewModel.hideNoPermissionDialog()
|
||||
navController.popBackStack()
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(onClick = {
|
||||
viewModel.hideNoPermissionDialog()
|
||||
navController.popBackStack()
|
||||
}) {
|
||||
Text(stringResource(R.string.btn_continue))
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
@@ -91,7 +182,7 @@ fun RepoScreen(navController: NavController, viewModel: BaseRepoViewModel) {
|
||||
) {
|
||||
LazyColumn(
|
||||
contentPadding = paddingValues,
|
||||
modifier = Modifier.fillMaxSize()
|
||||
modifier = Modifier.navigationBarsPadding().fillMaxSize()
|
||||
) {
|
||||
if (hostLists.isEmpty()) {
|
||||
item {
|
||||
@@ -107,95 +198,14 @@ 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)
|
||||
RepoItem(
|
||||
item = item,
|
||||
viewModel = viewModel,
|
||||
isInstalling = isInstalling,
|
||||
isUpdateInstalling = isUpdateInstalling,
|
||||
isUpdate = isUpdate
|
||||
)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,23 +1,59 @@
|
||||
@file:OptIn(ExperimentalMaterial3ExpressiveApi::class)
|
||||
package com.cherret.zaprett.ui.screen
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.SharedPreferences
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.net.Uri
|
||||
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.ExperimentalMaterial3ExpressiveApi
|
||||
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
|
||||
@@ -32,14 +68,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
|
||||
@@ -48,25 +92,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
|
||||
import androidx.core.net.toUri
|
||||
|
||||
@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) }
|
||||
@@ -76,6 +119,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)),
|
||||
@@ -83,18 +127,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(
|
||||
@@ -128,6 +165,13 @@ fun SettingsScreen(viewModel : SettingsViewModel = viewModel()) {
|
||||
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 = {
|
||||
@@ -135,9 +179,7 @@ fun SettingsScreen(viewModel : SettingsViewModel = viewModel()) {
|
||||
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 = {
|
||||
@@ -156,6 +198,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),
|
||||
@@ -186,14 +242,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)
|
||||
}
|
||||
)
|
||||
)
|
||||
@@ -215,7 +269,7 @@ fun SettingsScreen(viewModel : SettingsViewModel = viewModel()) {
|
||||
}
|
||||
|
||||
if (showAboutDialog.value) {
|
||||
AboutDialog(onDismiss = { showAboutDialog.value = false })
|
||||
AboutDialog(navController, onDismiss = { showAboutDialog.value = false })
|
||||
}
|
||||
|
||||
if (showHostsRepoUrlDialog.value) {
|
||||
@@ -223,10 +277,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 })
|
||||
}
|
||||
|
||||
@@ -284,6 +343,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(
|
||||
@@ -334,7 +399,7 @@ fun SettingsScreen(viewModel : SettingsViewModel = viewModel()) {
|
||||
)
|
||||
}
|
||||
is Setting.Action -> {
|
||||
SettingsTextItem(
|
||||
SettingsActionItem(
|
||||
title = setting.title,
|
||||
setting.onClick
|
||||
)
|
||||
@@ -349,59 +414,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(
|
||||
@@ -484,121 +496,13 @@ 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
|
||||
@@ -607,7 +511,13 @@ private fun AboutDialog(onDismiss: () -> Unit) {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Text(text = stringResource(R.string.about_text, BuildConfig.VERSION_NAME))
|
||||
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,
|
||||
@@ -801,9 +711,3 @@ private fun AppItem(viewModel: SettingsViewModel, packageName : String, enabled
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
@@ -1,43 +1,43 @@
|
||||
@file:OptIn(ExperimentalMaterial3ExpressiveApi::class)
|
||||
package com.cherret.zaprett.ui.screen
|
||||
|
||||
import android.content.ClipData
|
||||
import android.content.ClipboardManager
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import android.widget.Toast
|
||||
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.AlertDialog
|
||||
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.ExperimentalMaterial3ExpressiveApi
|
||||
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.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.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
@@ -51,12 +51,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)
|
||||
@@ -69,6 +69,7 @@ fun StrategyScreen(navController: NavController, viewModel: StrategyViewModel =
|
||||
val allLists = viewModel.allItems
|
||||
val checked = viewModel.checked
|
||||
val isRefreshing = viewModel.isRefreshing
|
||||
val showPermissionDialog by viewModel.showNoPermissionDialog.collectAsState()
|
||||
val filePickerLauncher = rememberLauncherForActivityResult(
|
||||
contract = ActivityResultContracts.OpenDocument()
|
||||
) { uri ->
|
||||
@@ -78,11 +79,43 @@ fun StrategyScreen(navController: NavController, viewModel: StrategyViewModel =
|
||||
it
|
||||
) }
|
||||
}
|
||||
val error by viewModel.errorFlow.collectAsState()
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
viewModel.refresh()
|
||||
}
|
||||
|
||||
if (error.isNotEmpty()) {
|
||||
AlertDialog(
|
||||
onDismissRequest = {
|
||||
viewModel.clearError()
|
||||
},
|
||||
title = { Text(stringResource(R.string.error_text)) },
|
||||
text = {
|
||||
Text(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)
|
||||
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()
|
||||
}) {
|
||||
Text(stringResource(R.string.btn_continue))
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
@@ -105,8 +138,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 +160,7 @@ fun StrategyScreen(navController: NavController, viewModel: StrategyViewModel =
|
||||
}
|
||||
else -> {
|
||||
items(allLists) { item ->
|
||||
StrategyItem(
|
||||
ListSwitchItem(
|
||||
item = item,
|
||||
isChecked = checked[item] == true,
|
||||
onCheckedChange = { isChecked ->
|
||||
@@ -145,43 +181,23 @@ fun StrategyScreen(navController: NavController, viewModel: StrategyViewModel =
|
||||
},
|
||||
snackbarHost = { SnackbarHost(hostState = snackbarHostState) }
|
||||
)
|
||||
if (showPermissionDialog) {
|
||||
AlertDialog(
|
||||
title = { Text(text = stringResource(R.string.error_no_storage_title)) },
|
||||
text = { Text(text = stringResource(R.string.no_storage_permission_message)) },
|
||||
onDismissRequest = {
|
||||
viewModel.hideNoPermissionDialog()
|
||||
navController.popBackStack()
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(onClick = {
|
||||
viewModel.hideNoPermissionDialog()
|
||||
navController.popBackStack()
|
||||
}) {
|
||||
Text(stringResource(R.string.btn_continue))
|
||||
}
|
||||
|
||||
@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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,239 @@
|
||||
@file:OptIn(ExperimentalMaterial3ExpressiveApi::class)
|
||||
package com.cherret.zaprett.ui.screen
|
||||
|
||||
import android.content.ClipData
|
||||
import android.content.ClipboardManager
|
||||
import android.content.Context
|
||||
import android.content.Context.MODE_PRIVATE
|
||||
import android.content.Intent
|
||||
import android.net.VpnService
|
||||
import android.os.Build
|
||||
import android.widget.Toast
|
||||
import androidx.activity.result.ActivityResultLauncher
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
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.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.dp
|
||||
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.ExperimentalMaterial3ExpressiveApi
|
||||
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()
|
||||
val error by viewModel.errorFlow.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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (error.isNotEmpty()) {
|
||||
AlertDialog(
|
||||
onDismissRequest = {
|
||||
viewModel.clearError()
|
||||
},
|
||||
title = { Text(stringResource(R.string.error_text)) },
|
||||
text = {
|
||||
Text(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)
|
||||
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()
|
||||
}) {
|
||||
Text(stringResource(R.string.btn_continue))
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
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 (
|
||||
contentPadding = PaddingValues(
|
||||
top = paddingValues.calculateTopPadding(),
|
||||
bottom = paddingValues.calculateBottomPadding() + 40.dp
|
||||
),
|
||||
modifier = Modifier.fillMaxSize()
|
||||
) {
|
||||
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))
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,10 @@ package com.cherret.zaprett.ui.theme
|
||||
|
||||
import android.os.Build
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
|
||||
import androidx.compose.material3.MaterialExpressiveTheme
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.MotionScheme
|
||||
import androidx.compose.material3.darkColorScheme
|
||||
import androidx.compose.material3.dynamicDarkColorScheme
|
||||
import androidx.compose.material3.dynamicLightColorScheme
|
||||
@@ -32,6 +35,7 @@ private val LightColorScheme = lightColorScheme(
|
||||
*/
|
||||
)
|
||||
|
||||
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
|
||||
@Composable
|
||||
fun ZaprettTheme(
|
||||
darkTheme: Boolean = isSystemInDarkTheme(),
|
||||
@@ -49,9 +53,10 @@ fun ZaprettTheme(
|
||||
else -> LightColorScheme
|
||||
}
|
||||
|
||||
MaterialTheme(
|
||||
MaterialExpressiveTheme(
|
||||
colorScheme = colorScheme,
|
||||
typography = Typography,
|
||||
motionScheme = MotionScheme.expressive(),
|
||||
content = content
|
||||
)
|
||||
}
|
||||
@@ -16,9 +16,13 @@ import androidx.compose.runtime.setValue
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.lifecycle.AndroidViewModel
|
||||
import com.cherret.zaprett.R
|
||||
import com.cherret.zaprett.utils.checkStoragePermission
|
||||
import com.cherret.zaprett.utils.getZaprettPath
|
||||
import com.cherret.zaprett.utils.restartService
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
@@ -34,12 +38,20 @@ abstract class BaseListsViewModel(application: Application) : AndroidViewModel(a
|
||||
var isRefreshing by mutableStateOf(false)
|
||||
private set
|
||||
|
||||
private val _errorFlow = MutableStateFlow("")
|
||||
val errorFlow = _errorFlow.asStateFlow()
|
||||
|
||||
private var _showNoPermissionDialog = MutableStateFlow(false)
|
||||
val showNoPermissionDialog: StateFlow<Boolean> = _showNoPermissionDialog
|
||||
|
||||
abstract fun loadAllItems(): Array<String>
|
||||
abstract fun loadActiveItems(): Array<String>
|
||||
abstract fun onCheckedChange(item: String, isChecked: Boolean, snackbarHostState: SnackbarHostState, scope: CoroutineScope)
|
||||
abstract fun deleteItem(item: String, snackbarHostState: SnackbarHostState, scope: CoroutineScope)
|
||||
|
||||
fun refresh() {
|
||||
when (checkStoragePermission(context)) {
|
||||
true -> {
|
||||
isRefreshing = true
|
||||
allItems = loadAllItems().toList()
|
||||
activeItems = loadActiveItems().toList()
|
||||
@@ -49,6 +61,13 @@ abstract class BaseListsViewModel(application: Application) : AndroidViewModel(a
|
||||
}
|
||||
isRefreshing = false
|
||||
}
|
||||
false -> _showNoPermissionDialog.value = true
|
||||
}
|
||||
}
|
||||
|
||||
fun hideNoPermissionDialog() {
|
||||
_showNoPermissionDialog.value = false
|
||||
}
|
||||
|
||||
fun showRestartSnackbar(context: Context, snackbarHostState: SnackbarHostState, scope: CoroutineScope) {
|
||||
scope.launch {
|
||||
@@ -57,12 +76,18 @@ abstract class BaseListsViewModel(application: Application) : AndroidViewModel(a
|
||||
actionLabel = context.getString(R.string.btn_restart_service)
|
||||
)
|
||||
if (result == SnackbarResult.ActionPerformed) {
|
||||
restartService {}
|
||||
restartService { error ->
|
||||
_errorFlow.value = error
|
||||
}
|
||||
snackbarHostState.showSnackbar(context.getString(R.string.snack_reload))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun clearError() {
|
||||
_errorFlow.value = ""
|
||||
}
|
||||
|
||||
fun copySelectedFile(context: Context, path: String, uri: Uri) {
|
||||
//if (!Environment.isExternalStorageManager()) return
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R){
|
||||
|
||||
@@ -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
|
||||
@@ -12,19 +13,32 @@ 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.checkStoragePermission
|
||||
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
|
||||
|
||||
private var _showPermissionDialog = MutableStateFlow(false)
|
||||
val showPermissionDialog: StateFlow<Boolean> = _showPermissionDialog
|
||||
|
||||
var hostLists = mutableStateOf<List<RepoItemInfo>>(emptyList())
|
||||
protected set
|
||||
@@ -37,19 +51,22 @@ 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()
|
||||
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
|
||||
}
|
||||
@@ -60,9 +77,21 @@ abstract class BaseRepoViewModel(application: Application) : AndroidViewModel(ap
|
||||
for (item in filteredList) {
|
||||
isUpdate[item.name] = item.hash !in existingHashes
|
||||
}
|
||||
}
|
||||
.onFailure { e ->
|
||||
_errorFlow.value = e
|
||||
}
|
||||
}
|
||||
isRefreshing.value = false
|
||||
}
|
||||
}
|
||||
|
||||
fun clearError() {
|
||||
_errorFlow.value = null
|
||||
}
|
||||
|
||||
fun clearDownloadError() {
|
||||
_downloadErrorFlow.value = null
|
||||
}
|
||||
|
||||
fun isItemInstalled(item: RepoItemInfo): Boolean {
|
||||
@@ -70,9 +99,11 @@ abstract class BaseRepoViewModel(application: Application) : AndroidViewModel(ap
|
||||
}
|
||||
|
||||
fun install(item: RepoItemInfo) {
|
||||
when (checkStoragePermission(context)) {
|
||||
true -> {
|
||||
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) {
|
||||
@@ -80,6 +111,8 @@ abstract class BaseRepoViewModel(application: Application) : AndroidViewModel(ap
|
||||
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)
|
||||
@@ -88,12 +121,28 @@ 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
|
||||
})
|
||||
}
|
||||
false -> _showPermissionDialog.value = true
|
||||
}
|
||||
}
|
||||
|
||||
fun hideNoPermissionDialog() {
|
||||
_showPermissionDialog.value = false
|
||||
}
|
||||
|
||||
fun update(item: RepoItemInfo) {
|
||||
isUpdateInstalling[item.name] = true
|
||||
val downloadId = download(context, item.url)
|
||||
registerDownloadListenerHost(context, downloadId) { uri ->
|
||||
registerDownloadListener(
|
||||
context,
|
||||
downloadId,
|
||||
onDownloaded = { uri ->
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
val sourceFile = File(uri.path!!)
|
||||
val targetDir = when (item.type) {
|
||||
@@ -101,6 +150,8 @@ abstract class BaseRepoViewModel(application: Application) : AndroidViewModel(ap
|
||||
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)
|
||||
@@ -109,7 +160,14 @@ abstract class BaseRepoViewModel(application: Application) : AndroidViewModel(ap
|
||||
isUpdate[item.name] = false
|
||||
refresh()
|
||||
}
|
||||
},
|
||||
onError = { error ->
|
||||
isUpdateInstalling[item.name] = false
|
||||
isUpdate[item.name] = false
|
||||
refresh()
|
||||
_downloadErrorFlow.value = error
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
fun showRestartSnackbar(snackbarHostState: SnackbarHostState) {
|
||||
|
||||
@@ -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,11 @@ 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()
|
||||
|
||||
private val _errorFlow = MutableStateFlow("")
|
||||
val errorFlow = _errorFlow.asStateFlow()
|
||||
|
||||
var moduleVer = mutableStateOf(context.getString(R.string.unknown_text))
|
||||
private set
|
||||
@@ -51,7 +54,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 +75,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,53 +87,35 @@ 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 checkServiceStatus() {
|
||||
val updateOnBoot = prefs.getBoolean("update_on_boot", true)
|
||||
if (updateOnBoot) {
|
||||
val useModule = prefs.getBoolean("use_module", false)
|
||||
updateServiceStatus(useModule)
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
val useModule = prefs.getBoolean("use_module", false)
|
||||
updateServiceStatus(useModule)
|
||||
}
|
||||
|
||||
fun startVpn() {
|
||||
@@ -147,7 +132,10 @@ class HomeViewModel(application: Application) : AndroidViewModel(application) {
|
||||
)
|
||||
)
|
||||
}
|
||||
if (!isEnabled) startService {}
|
||||
if (!isEnabled) startService { error ->
|
||||
_errorFlow.value = error
|
||||
onCardClick()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (ByeDpiVpnService.status == ServiceStatus.Disconnected || ByeDpiVpnService.status == ServiceStatus.Failed) {
|
||||
@@ -187,7 +175,10 @@ class HomeViewModel(application: Application) : AndroidViewModel(application) {
|
||||
)
|
||||
)
|
||||
}
|
||||
if (isEnabled) stopService {}
|
||||
if (isEnabled) stopService { error ->
|
||||
_errorFlow.value = error
|
||||
onCardClick()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (ByeDpiVpnService.status == ServiceStatus.Connected) {
|
||||
@@ -210,7 +201,10 @@ class HomeViewModel(application: Application) : AndroidViewModel(application) {
|
||||
|
||||
fun onBtnRestart(snackbarHostState: SnackbarHostState, scope: CoroutineScope) {
|
||||
if (prefs.getBoolean("use_module", false)) {
|
||||
restartService {}
|
||||
restartService { error ->
|
||||
_errorFlow.value = error
|
||||
onCardClick()
|
||||
}
|
||||
scope.launch {
|
||||
snackbarHostState.showSnackbar(context.getString(R.string.snack_reload))
|
||||
}
|
||||
@@ -245,9 +239,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)
|
||||
@@ -257,6 +254,7 @@ class HomeViewModel(application: Application) : AndroidViewModel(application) {
|
||||
}
|
||||
}
|
||||
|
||||
// unused?
|
||||
fun parseArgs(ip: String, port: String, lines: List<String>): Array<String> {
|
||||
val regex = Regex("""--?\S+(?:=(?:[^"'\s]+|"[^"]*"|'[^']*'))?|[^\s]+""")
|
||||
val parsedArgs = lines
|
||||
@@ -264,4 +262,7 @@ class HomeViewModel(application: Application) : AndroidViewModel(application) {
|
||||
return arrayOf("ciadpi", "--ip", ip, "--port", port) + parsedArgs
|
||||
}
|
||||
|
||||
fun clearError() {
|
||||
_errorFlow.value = ""
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -32,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)
|
||||
}
|
||||
}
|
||||
@@ -44,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,
|
||||
|
||||
@@ -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,15 +1,24 @@
|
||||
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.drawable.Drawable
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.lifecycle.AndroidViewModel
|
||||
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
|
||||
@@ -24,9 +33,18 @@ 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
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getAppIconBitmap(packageName: String): Drawable? = withContext(Dispatchers.IO) {
|
||||
@@ -100,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,208 @@
|
||||
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()
|
||||
private val _errorFlow = MutableStateFlow("")
|
||||
val errorFlow = _errorFlow.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()
|
||||
var stopTest : Boolean = false;
|
||||
for (index in strategyStates.indices) {
|
||||
val current = strategyStates[index]
|
||||
if (stopTest) break
|
||||
strategyStates[index] = current.copy(status = StrategyTestingStatus.Testing)
|
||||
enableStrategy(current.path, prefs)
|
||||
if (prefs.getBoolean("use_module", false)) {
|
||||
getStatus { if (it) stopService { error ->
|
||||
_errorFlow.value = error
|
||||
if (error.isNotEmpty()) stopTest = true
|
||||
} }
|
||||
startService { error ->
|
||||
_errorFlow.value = error
|
||||
if (error.isNotEmpty()) stopTest = true
|
||||
}
|
||||
try {
|
||||
val progress = countReachable(index, targets)
|
||||
val old = strategyStates[index]
|
||||
strategyStates[index] = old.copy(
|
||||
progress = progress,
|
||||
status = StrategyTestingStatus.Completed
|
||||
)
|
||||
} finally {
|
||||
stopService { error ->
|
||||
_errorFlow.value = error
|
||||
if (error.isNotEmpty()) stopTest = true
|
||||
}
|
||||
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() || getAllStrategies(prefs).isEmpty()) noHostsCard.value = true
|
||||
Log.d("getActiveLists.isEmpty || getAllStrategies.isEmpty", getActiveLists(prefs).isEmpty().toString())
|
||||
}
|
||||
fun startVpn() {
|
||||
ContextCompat.startForegroundService(context, Intent(context, ByeDpiVpnService::class.java).apply { action = "START_VPN" })
|
||||
}
|
||||
fun clearVpnPermissionRequest() {
|
||||
_requestVpnPermission.value = false
|
||||
}
|
||||
fun clearError() {
|
||||
_errorFlow.value = ""
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -18,8 +18,8 @@ import java.io.File
|
||||
|
||||
class StrategyViewModel(application: Application): BaseListsViewModel(application) {
|
||||
private val sharedPreferences = application.getSharedPreferences("settings", Context.MODE_PRIVATE)
|
||||
private val useModule = sharedPreferences.getBoolean("use_module", false)
|
||||
private val strategyProvider: StrategyProvider = if (useModule) {
|
||||
private val strategyProvider: StrategyProvider
|
||||
get() = if (sharedPreferences.getBoolean("use_module", false)) {
|
||||
NfqwsStrategyProvider()
|
||||
} else {
|
||||
ByeDPIStrategyProvider(sharedPreferences)
|
||||
@@ -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)
|
||||
}
|
||||
@@ -1,664 +0,0 @@
|
||||
package com.cherret.zaprett.utils
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Context.MODE_PRIVATE
|
||||
import android.content.SharedPreferences
|
||||
import android.os.Environment
|
||||
import android.util.Log
|
||||
import com.topjohnwu.superuser.Shell
|
||||
import java.io.File
|
||||
import java.io.FileInputStream
|
||||
import java.io.FileOutputStream
|
||||
import java.io.IOException
|
||||
import java.util.Properties
|
||||
import androidx.core.content.edit
|
||||
import com.cherret.zaprett.data.AppListType
|
||||
|
||||
fun checkRoot(callback: (Boolean) -> Unit) {
|
||||
Shell.getShell().isRoot.let { callback(it) }
|
||||
}
|
||||
|
||||
fun checkModuleInstallation(callback: (Boolean) -> Unit) {
|
||||
Shell.cmd("zaprett").submit { result ->
|
||||
callback(result.out.toString().contains("zaprett"))
|
||||
}
|
||||
}
|
||||
|
||||
fun getStatus(callback: (Boolean) -> Unit) {
|
||||
Shell.cmd("zaprett status").submit { result ->
|
||||
callback(result.out.toString().contains("working"))
|
||||
}
|
||||
}
|
||||
|
||||
fun startService(callback: (Boolean) -> Unit) {
|
||||
Shell.cmd("zaprett start").submit { result ->
|
||||
callback(result.isSuccess)
|
||||
}
|
||||
}
|
||||
|
||||
fun stopService(callback: (Boolean) -> Unit) {
|
||||
Shell.cmd("zaprett stop").submit { result ->
|
||||
callback(result.isSuccess)
|
||||
}
|
||||
}
|
||||
|
||||
fun restartService(callback: (Boolean) -> Unit) {
|
||||
Shell.cmd("zaprett restart").submit { result ->
|
||||
callback(result.isSuccess)
|
||||
}
|
||||
}
|
||||
|
||||
fun getModuleVersion(callback: (String) -> Unit) {
|
||||
Shell.cmd("zaprett module-ver").submit { result ->
|
||||
if (result.out.isNotEmpty()) callback(result.out.first()) else "undefined"
|
||||
}
|
||||
}
|
||||
|
||||
fun getBinVersion(callback: (String) -> Unit) {
|
||||
Shell.cmd("zaprett bin-ver").submit { result ->
|
||||
if (result.out.isNotEmpty()) callback(result.out.first()) else "undefined"
|
||||
}
|
||||
}
|
||||
|
||||
fun getConfigFile(): File {
|
||||
return File(Environment.getExternalStorageDirectory(), "zaprett/config")
|
||||
}
|
||||
|
||||
fun setStartOnBoot(startOnBoot: Boolean) {
|
||||
val configFile = getConfigFile()
|
||||
if (configFile.exists()) {
|
||||
val props = Properties()
|
||||
try {
|
||||
FileInputStream(configFile).use { input ->
|
||||
props.load(input)
|
||||
}
|
||||
props.setProperty("start_on_boot", startOnBoot.toString())
|
||||
FileOutputStream(configFile).use { output ->
|
||||
props.store(output, "Don't place '/' in end of directory! Example: /sdcard")
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
throw RuntimeException(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun getStartOnBoot(): Boolean {
|
||||
val configFile = getConfigFile()
|
||||
val props = Properties()
|
||||
return try {
|
||||
if (configFile.exists()) {
|
||||
FileInputStream(configFile).use { input ->
|
||||
props.load(input)
|
||||
}
|
||||
props.getProperty("start_on_boot", "false").toBoolean()
|
||||
} else {
|
||||
false
|
||||
}
|
||||
} catch (_: IOException) {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
fun getZaprettPath(): String {
|
||||
val props = Properties()
|
||||
val configFile = getConfigFile()
|
||||
if (configFile.exists()) {
|
||||
return try {
|
||||
FileInputStream(configFile).use { input ->
|
||||
props.load(input)
|
||||
}
|
||||
props.getProperty("zaprettdir", Environment.getExternalStorageDirectory().path + "/zaprett")
|
||||
} catch (e: IOException) {
|
||||
throw RuntimeException(e)
|
||||
}
|
||||
}
|
||||
return Environment.getExternalStorageDirectory().path + "/zaprett"
|
||||
}
|
||||
|
||||
fun getAllLists(): Array<String> {
|
||||
val listsDir = File("${getZaprettPath()}/lists/include")
|
||||
return listsDir.listFiles { file -> file.isFile }
|
||||
?.map { it.absolutePath }
|
||||
?.toTypedArray()
|
||||
?: emptyArray()
|
||||
}
|
||||
|
||||
fun getAllExcludeLists(): Array<String> {
|
||||
val listsDir = File("${getZaprettPath()}/lists/exclude/")
|
||||
return listsDir.listFiles { file -> file.isFile }
|
||||
?.map { it.absolutePath }
|
||||
?.toTypedArray()
|
||||
?: emptyArray()
|
||||
}
|
||||
|
||||
fun getAllNfqwsStrategies(): Array<String> {
|
||||
val listsDir = File("${getZaprettPath()}/strategies/nfqws")
|
||||
return listsDir.listFiles { file -> file.isFile }
|
||||
?.map { it.absolutePath }
|
||||
?.toTypedArray()
|
||||
?: emptyArray()
|
||||
}
|
||||
|
||||
fun getAllByeDPIStrategies(): Array<String> {
|
||||
val listsDir = File("${getZaprettPath()}/strategies/byedpi")
|
||||
return listsDir.listFiles { file -> file.isFile }
|
||||
?.map { it.absolutePath }
|
||||
?.toTypedArray()
|
||||
?: emptyArray()
|
||||
}
|
||||
|
||||
|
||||
fun getActiveLists(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_lists", "")
|
||||
Log.d("Active lists", activeLists)
|
||||
if (activeLists.isNotEmpty()) activeLists.split(",")
|
||||
.toTypedArray() else emptyArray()
|
||||
} catch (e: IOException) {
|
||||
throw RuntimeException(e)
|
||||
}
|
||||
}
|
||||
return emptyArray()
|
||||
}
|
||||
else {
|
||||
return sharedPreferences.getStringSet("lists", emptySet())?.toTypedArray() ?: emptyArray()
|
||||
}
|
||||
}
|
||||
fun 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> {
|
||||
val configFile = File("${getZaprettPath()}/config")
|
||||
if (configFile.exists()) {
|
||||
val props = Properties()
|
||||
return try {
|
||||
FileInputStream(configFile).use { input ->
|
||||
props.load(input)
|
||||
}
|
||||
val activeStrategies = props.getProperty("strategy", "")
|
||||
Log.d("Active strategies", activeStrategies)
|
||||
if (activeStrategies.isNotEmpty()) activeStrategies.split(",").toTypedArray() else emptyArray()
|
||||
} catch (e: IOException) {
|
||||
throw RuntimeException(e)
|
||||
}
|
||||
}
|
||||
return emptyArray()
|
||||
}
|
||||
|
||||
fun getActiveByeDPIStrategies(sharedPreferences: SharedPreferences): Array<String> {
|
||||
val path = sharedPreferences.getString("active_strategy", "")
|
||||
if (!path.isNullOrBlank() && File(path).exists()) {
|
||||
return arrayOf(path)
|
||||
}
|
||||
return emptyArray()
|
||||
}
|
||||
|
||||
fun getActiveStrategy(sharedPreferences: SharedPreferences): List<String> {
|
||||
val path = sharedPreferences.getString("active_strategy", "")
|
||||
if (!path.isNullOrBlank() && File(path).exists()) {
|
||||
return File(path).readLines()
|
||||
}
|
||||
return emptyList()
|
||||
}
|
||||
|
||||
fun enableList(path: String, sharedPreferences: SharedPreferences) {
|
||||
if (sharedPreferences.getBoolean("use_module", false)) {
|
||||
val 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_lists"
|
||||
else "active_exclude_lists",
|
||||
"")
|
||||
.split(",")
|
||||
.filter { it.isNotBlank() }
|
||||
.toMutableList()
|
||||
if (path !in activeLists) {
|
||||
activeLists.add(path)
|
||||
}
|
||||
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")
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
throw RuntimeException(e)
|
||||
}
|
||||
}
|
||||
else {
|
||||
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(
|
||||
if (getHostListMode(sharedPreferences) == "whitelist") "lists"
|
||||
else "exclude_lists", currentSet) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun enableStrategy(path: String, sharedPreferences: SharedPreferences) {
|
||||
if (sharedPreferences.getBoolean("use_module", false)) {
|
||||
val props = Properties()
|
||||
val configFile = getConfigFile()
|
||||
try {
|
||||
if (configFile.exists()) {
|
||||
FileInputStream(configFile).use { input ->
|
||||
props.load(input)
|
||||
}
|
||||
}
|
||||
val activeStrategies = props.getProperty("strategy", "")
|
||||
.split(",")
|
||||
.filter { it.isNotBlank() }
|
||||
.toMutableList()
|
||||
if (path !in activeStrategies) {
|
||||
activeStrategies.add(path)
|
||||
}
|
||||
props.setProperty("strategy", activeStrategies.joinToString(","))
|
||||
FileOutputStream(configFile).use { output ->
|
||||
props.store(output, "Don't place '/' in end of directory! Example: /sdcard")
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
throw RuntimeException(e)
|
||||
}
|
||||
}
|
||||
else {
|
||||
sharedPreferences.edit { putString("active_strategy", path) }
|
||||
}
|
||||
}
|
||||
|
||||
fun disableList(path: String, sharedPreferences: SharedPreferences) {
|
||||
if (sharedPreferences.getBoolean("use_module", false)) {
|
||||
val props = Properties()
|
||||
val configFile = getConfigFile()
|
||||
try {
|
||||
if (configFile.exists()) {
|
||||
FileInputStream(configFile).use { input ->
|
||||
props.load(input)
|
||||
}
|
||||
}
|
||||
val activeLists = props.getProperty(
|
||||
if (getHostListMode(sharedPreferences) == "whitelist") "active_lists"
|
||||
else "active_exclude_lists",
|
||||
"")
|
||||
.split(",")
|
||||
.filter { it.isNotBlank() }
|
||||
.toMutableList()
|
||||
if (path in activeLists) {
|
||||
activeLists.remove(path)
|
||||
}
|
||||
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")
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
throw RuntimeException(e)
|
||||
}
|
||||
}
|
||||
else {
|
||||
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(
|
||||
if (getHostListMode(sharedPreferences) == "whitelist") "lists"
|
||||
else "exclude_lists", currentSet) }
|
||||
}
|
||||
if (currentSet.isEmpty()) {
|
||||
sharedPreferences.edit { remove(
|
||||
if (getHostListMode(sharedPreferences) == "whitelist") "lists"
|
||||
else "exclude_lists"
|
||||
) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun disableStrategy(path: String, sharedPreferences: SharedPreferences) {
|
||||
if (sharedPreferences.getBoolean("use_module", false)) {
|
||||
val props = Properties()
|
||||
val configFile = getConfigFile()
|
||||
try {
|
||||
if (configFile.exists()) {
|
||||
FileInputStream(configFile).use { input ->
|
||||
props.load(input)
|
||||
}
|
||||
}
|
||||
val activeStrategies = props.getProperty("strategy", "")
|
||||
.split(",")
|
||||
.filter { it.isNotBlank() }
|
||||
.toMutableList()
|
||||
if (path in activeStrategies) {
|
||||
activeStrategies.remove(path)
|
||||
}
|
||||
props.setProperty("strategy", activeStrategies.joinToString(","))
|
||||
FileOutputStream(configFile).use { output ->
|
||||
props.store(output, "Don't place '/' in end of directory! Example: /sdcard")
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
throw RuntimeException(e)
|
||||
}
|
||||
}
|
||||
else {
|
||||
sharedPreferences.edit { remove("active_strategy") }
|
||||
}
|
||||
}
|
||||
|
||||
fun addPackageToList(listType: AppListType, packageName: String, prefs : SharedPreferences, context : Context) {
|
||||
if (prefs.getBoolean("use_module", false)){
|
||||
val configFile = getConfigFile()
|
||||
try {
|
||||
val props = Properties()
|
||||
if (configFile.exists()) {
|
||||
FileInputStream(configFile).use { input ->
|
||||
props.load(input)
|
||||
}
|
||||
}
|
||||
if (listType == AppListType.Whitelist) {
|
||||
val whitelist = props.getProperty("whitelist", "")
|
||||
.split(",")
|
||||
.filter { it.isNotBlank() }
|
||||
.toMutableList()
|
||||
if (packageName !in whitelist) {
|
||||
whitelist.add(packageName)
|
||||
}
|
||||
props.setProperty("whitelist", whitelist.joinToString(","))
|
||||
}
|
||||
if (listType == AppListType.Blacklist) {
|
||||
val blacklist = props.getProperty("blacklist", "")
|
||||
.split(",")
|
||||
.filter { it.isNotBlank() }
|
||||
.toMutableList()
|
||||
if (packageName !in blacklist) {
|
||||
blacklist.add(packageName)
|
||||
}
|
||||
props.setProperty("blacklist", blacklist.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 prefs = context.getSharedPreferences("settings", MODE_PRIVATE)
|
||||
if (listType == AppListType.Whitelist){
|
||||
val set = prefs.getStringSet("whitelist", emptySet())?.toMutableSet() ?: mutableSetOf()
|
||||
set.add(packageName)
|
||||
prefs.edit().putStringSet("whitelist", set).apply()
|
||||
}
|
||||
if (listType == AppListType.Blacklist){
|
||||
val set = prefs.getStringSet("blacklist", emptySet())?.toMutableSet() ?: mutableSetOf()
|
||||
set.add(packageName)
|
||||
prefs.edit().putStringSet("blacklist", set).apply()
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
fun removePackageFromList(listType: AppListType, packageName: String, prefs: SharedPreferences, context: Context) {
|
||||
if (prefs.getBoolean("use_module", false)){
|
||||
val props = Properties()
|
||||
val configFile = getConfigFile()
|
||||
try {
|
||||
if (configFile.exists()) {
|
||||
FileInputStream(configFile).use { input ->
|
||||
props.load(input)
|
||||
}
|
||||
}
|
||||
if (listType == AppListType.Whitelist){
|
||||
val whitelist = props.getProperty("whitelist", "")
|
||||
.split(",")
|
||||
.filter { it.isNotBlank() }
|
||||
.toMutableList()
|
||||
if (packageName in whitelist) {
|
||||
whitelist.remove(packageName)
|
||||
}
|
||||
props.setProperty("whitelist", whitelist.joinToString(","))
|
||||
}
|
||||
if (listType == AppListType.Blacklist) {
|
||||
val blacklist = props.getProperty("blacklist", "")
|
||||
.split(",")
|
||||
.filter { it.isNotBlank() }
|
||||
.toMutableList()
|
||||
if (packageName in blacklist) {
|
||||
blacklist.remove(packageName)
|
||||
}
|
||||
props.setProperty("blacklist", blacklist.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 prefs = context.getSharedPreferences("settings", MODE_PRIVATE)
|
||||
if (listType == AppListType.Whitelist) {
|
||||
val set = prefs.getStringSet("whitelist", emptySet())?.toMutableSet() ?: mutableSetOf()
|
||||
set.remove(packageName)
|
||||
prefs.edit().putStringSet("whitelist", set).apply()
|
||||
}
|
||||
if (listType == AppListType.Blacklist) {
|
||||
val set = prefs.getStringSet("blacklist", emptySet())?.toMutableSet() ?: mutableSetOf()
|
||||
set.remove(packageName)
|
||||
prefs.edit().putStringSet("blacklist", set).apply()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun isInList(listType: AppListType, packageName: String, prefs: SharedPreferences, context: Context) : Boolean {
|
||||
if (prefs.getBoolean("use_module", false)) {
|
||||
val configFile = getConfigFile()
|
||||
if (configFile.exists()) {
|
||||
val props = Properties()
|
||||
try {
|
||||
FileInputStream(configFile).use { input ->
|
||||
props.load(input)
|
||||
}
|
||||
if (listType == AppListType.Whitelist) {
|
||||
val whitelist = props.getProperty("whitelist", "")
|
||||
return if (whitelist.isNotEmpty()) whitelist.split(",")
|
||||
.toTypedArray().contains(packageName) else false
|
||||
}
|
||||
if (listType == AppListType.Blacklist) {
|
||||
val blacklist = props.getProperty("blacklist", "")
|
||||
return if (blacklist.isNotEmpty()) blacklist.split(",")
|
||||
.toTypedArray().contains(packageName) else false
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
throw RuntimeException(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
val prefs = context.getSharedPreferences("settings", MODE_PRIVATE)
|
||||
if(listType == AppListType.Whitelist){
|
||||
val whitelist = prefs.getStringSet("whitelist", emptySet()) ?: emptySet()
|
||||
return packageName in whitelist
|
||||
}
|
||||
else {
|
||||
val blacklist = prefs.getStringSet("blacklist", emptySet()) ?: emptySet()
|
||||
return packageName in blacklist
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
fun getAppList(listType: AppListType, sharedPreferences : SharedPreferences, context : Context) : Set<String> {
|
||||
if (sharedPreferences.getBoolean("use_module", false)) {
|
||||
val configFile = File("${getZaprettPath()}/config")
|
||||
if (configFile.exists()) {
|
||||
val props = Properties()
|
||||
try {
|
||||
FileInputStream(configFile).use { input ->
|
||||
props.load(input)
|
||||
}
|
||||
if (listType == AppListType.Whitelist) {
|
||||
val whitelist = props.getProperty("whitelist", "")
|
||||
return if (whitelist.isNotEmpty()) whitelist.split(",")
|
||||
.toSet() else emptySet()
|
||||
}
|
||||
if (listType == AppListType.Blacklist) {
|
||||
val blacklist = props.getProperty("blacklist", "")
|
||||
return if (blacklist.isNotEmpty()) blacklist.split(",")
|
||||
.toSet() else emptySet()
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
throw RuntimeException(e)
|
||||
}
|
||||
}
|
||||
return emptySet()
|
||||
}
|
||||
else {
|
||||
return if (listType == AppListType.Whitelist) context.getSharedPreferences("settings", MODE_PRIVATE)
|
||||
.getStringSet("whitelist", emptySet()) ?: emptySet()
|
||||
else context.getSharedPreferences("settings", MODE_PRIVATE)
|
||||
.getStringSet("blacklist", emptySet()) ?: emptySet()
|
||||
}
|
||||
}
|
||||
|
||||
fun getAppsListMode(prefs : SharedPreferences) : String {
|
||||
if(prefs.getBoolean("use_module", false)) {
|
||||
val configFile = getConfigFile()
|
||||
if (configFile.exists()) {
|
||||
val props = Properties()
|
||||
try {
|
||||
FileInputStream(configFile).use { input ->
|
||||
props.load(input)
|
||||
}
|
||||
val applist = props.getProperty("app_list", "")!!
|
||||
Log.d("App list", "Equals to $applist")
|
||||
return if (applist == "whitelist" || applist == "blacklist" || applist == "none") applist
|
||||
else "none"
|
||||
} catch (e: IOException) {
|
||||
throw RuntimeException(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
return prefs.getString("applist", "")!!
|
||||
}
|
||||
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()
|
||||
try {
|
||||
FileInputStream(configFile).use { input ->
|
||||
props.load(input)
|
||||
}
|
||||
val hostlist = props.getProperty("list_type", "whitelist")!!
|
||||
return if (hostlist == "whitelist" || hostlist == "blacklist") hostlist
|
||||
else "whitelist"
|
||||
} catch (e: IOException) {
|
||||
throw RuntimeException(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
return prefs.getString("list_type", "whitelist")!!
|
||||
}
|
||||
return "whitelist"
|
||||
}
|
||||
@@ -26,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-repo/refs/heads/main/hosts.json")?: "https://raw.githubusercontent.com/CherretGit/zaprett-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-repo/refs/heads/main/strategies.json")?: "https://raw.githubusercontent.com/CherretGit/zaprett-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?) {
|
||||
@@ -77,7 +90,9 @@ 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 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()
|
||||
@@ -85,6 +100,24 @@ fun registerDownloadListenerHost(context: Context, downloadId: Long, onDownloade
|
||||
onDownloaded(uri)
|
||||
}
|
||||
}
|
||||
|
||||
DownloadManager.STATUS_FAILED -> {
|
||||
context.unregisterReceiver(this)
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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?,
|
||||
|
||||
504
app/src/main/java/com/cherret/zaprett/utils/ZaprettManager.kt
Normal file
504
app/src/main/java/com/cherret/zaprett/utils/ZaprettManager.kt
Normal file
@@ -0,0 +1,504 @@
|
||||
package com.cherret.zaprett.utils
|
||||
|
||||
import android.Manifest
|
||||
import android.content.Context
|
||||
import android.content.Context.MODE_PRIVATE
|
||||
import android.content.SharedPreferences
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Build
|
||||
import android.os.Environment
|
||||
import android.util.Log
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.content.edit
|
||||
import com.cherret.zaprett.data.AppListType
|
||||
import com.cherret.zaprett.data.ZaprettConfig
|
||||
import com.topjohnwu.superuser.Shell
|
||||
import kotlinx.serialization.json.Json
|
||||
import java.io.File
|
||||
|
||||
private val json = Json {
|
||||
prettyPrint = true
|
||||
ignoreUnknownKeys = true
|
||||
coerceInputValues = true
|
||||
encodeDefaults = true
|
||||
}
|
||||
|
||||
private fun readConfig(): ZaprettConfig {
|
||||
val configFile = getConfigFile()
|
||||
if (!configFile.exists()) {
|
||||
return ZaprettConfig()
|
||||
}
|
||||
return try {
|
||||
val content = configFile.readText()
|
||||
if (content.isBlank()) ZaprettConfig() else json.decodeFromString<ZaprettConfig>(content)
|
||||
} catch (e: Exception) {
|
||||
Log.e("ZaprettManager", "Error reading config, returning defaults", e)
|
||||
ZaprettConfig()
|
||||
}
|
||||
}
|
||||
|
||||
private fun writeConfig(config: ZaprettConfig) {
|
||||
val configFile = getConfigFile()
|
||||
try {
|
||||
configFile.parentFile?.mkdirs()
|
||||
val content = json.encodeToString(config)
|
||||
configFile.writeText(content)
|
||||
} catch (e: Exception) {
|
||||
Log.e("ZaprettManager", "Error writing config", e)
|
||||
}
|
||||
}
|
||||
|
||||
fun checkStoragePermission(context: Context): Boolean {
|
||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
Environment.isExternalStorageManager()
|
||||
} else {
|
||||
ContextCompat.checkSelfPermission(
|
||||
context,
|
||||
Manifest.permission.WRITE_EXTERNAL_STORAGE
|
||||
) == PackageManager.PERMISSION_GRANTED
|
||||
}
|
||||
}
|
||||
|
||||
fun checkRoot(callback: (Boolean) -> Unit) {
|
||||
Shell.getShell().isRoot.let { callback(it) }
|
||||
}
|
||||
|
||||
fun checkModuleInstallation(callback: (Boolean) -> Unit) {
|
||||
Shell.cmd("zaprett").submit { result ->
|
||||
callback(result.out.toString().contains("zaprett"))
|
||||
}
|
||||
}
|
||||
|
||||
fun getStatus(callback: (Boolean) -> Unit) {
|
||||
Shell.cmd("zaprett status").submit { result ->
|
||||
callback(result.out.toString().contains("working"))
|
||||
}
|
||||
}
|
||||
|
||||
fun startService(callback: (String) -> Unit) {
|
||||
Shell.cmd("zaprett start 2>&1").submit { result ->
|
||||
callback(
|
||||
if (result.isSuccess) ""
|
||||
else result.out.joinToString("\n")
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fun stopService(callback: (String) -> Unit) {
|
||||
Shell.cmd("zaprett stop 2>&1").submit { result ->
|
||||
callback(
|
||||
if (result.isSuccess) ""
|
||||
else result.out.joinToString("\n")
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun restartService(callback: (String) -> Unit) {
|
||||
Shell.cmd("zaprett restart 2>&1").submit { result ->
|
||||
callback(
|
||||
if (result.isSuccess) ""
|
||||
else result.out.joinToString("\n")
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun getModuleVersion(callback: (String) -> Unit) {
|
||||
Shell.cmd("zaprett module-version").submit { result ->
|
||||
if (result.out.isNotEmpty()) callback(result.out.first()) else "undefined"
|
||||
}
|
||||
}
|
||||
|
||||
fun getBinVersion(callback: (String) -> Unit) {
|
||||
Shell.cmd("zaprett binary-version").submit { result ->
|
||||
if (result.out.isNotEmpty()) callback(result.out.first()) else "undefined"
|
||||
}
|
||||
}
|
||||
|
||||
fun getConfigFile(): File {
|
||||
return File(Environment.getExternalStorageDirectory(), "zaprett/config.json")
|
||||
}
|
||||
|
||||
fun setStartOnBoot(prefs: SharedPreferences, callback: (Boolean) -> Unit) {
|
||||
if (prefs.getBoolean("use_module", false)) {
|
||||
Shell.cmd("zaprett set-autostart").submit { result ->
|
||||
if (result.out.isNotEmpty() && result.out.toString().contains("true")) callback(true) else callback(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)
|
||||
}
|
||||
} else {
|
||||
callback(false)
|
||||
}
|
||||
}
|
||||
|
||||
fun getZaprettPath(): String {
|
||||
return Environment.getExternalStorageDirectory().path + "/zaprett"
|
||||
}
|
||||
|
||||
fun getAllLists(): Array<String> {
|
||||
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")
|
||||
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")
|
||||
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)) {
|
||||
return readConfig().activeLists.toTypedArray()
|
||||
} else {
|
||||
return sharedPreferences.getStringSet("lists", emptySet())?.toTypedArray() ?: emptyArray()
|
||||
}
|
||||
}
|
||||
|
||||
fun getActiveIpsets(sharedPreferences: SharedPreferences): Array<String> {
|
||||
if (sharedPreferences.getBoolean("use_module", false)) {
|
||||
return readConfig().activeIpsets.toTypedArray()
|
||||
} else return sharedPreferences.getStringSet("ipsets", emptySet())?.toTypedArray() ?: emptyArray()
|
||||
}
|
||||
|
||||
fun getActiveExcludeLists(sharedPreferences: SharedPreferences): Array<String> {
|
||||
if (sharedPreferences.getBoolean("use_module", false)) {
|
||||
return readConfig().activeExcludeLists.toTypedArray()
|
||||
} else {
|
||||
return sharedPreferences.getStringSet("exclude_lists", emptySet())?.toTypedArray() ?: emptyArray()
|
||||
}
|
||||
}
|
||||
|
||||
fun getActiveExcludeIpsets(sharedPreferences: SharedPreferences): Array<String> {
|
||||
if (sharedPreferences.getBoolean("use_module", false)) {
|
||||
return readConfig().activeExcludeIpsets.toTypedArray()
|
||||
} else return sharedPreferences.getStringSet("exclude_ipsets", emptySet())?.toTypedArray() ?: emptyArray()
|
||||
}
|
||||
|
||||
fun getActiveNfqwsStrategy(): Array<String> {
|
||||
val strategy = readConfig().strategy
|
||||
return if (strategy.isNotBlank()) arrayOf(strategy) else emptyArray()
|
||||
}
|
||||
|
||||
fun getActiveByeDPIStrategy(sharedPreferences: SharedPreferences): Array<String> {
|
||||
val path = sharedPreferences.getString("active_strategy", "")
|
||||
if (!path.isNullOrBlank() && File(path).exists()) {
|
||||
return arrayOf(path)
|
||||
}
|
||||
return emptyArray()
|
||||
}
|
||||
|
||||
fun getActiveByeDPIStrategyContent(sharedPreferences: SharedPreferences): List<String> {
|
||||
val path = sharedPreferences.getString("active_strategy", "")
|
||||
if (!path.isNullOrBlank() && File(path).exists()) {
|
||||
return File(path).readLines()
|
||||
}
|
||||
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 config = readConfig()
|
||||
val isWhitelist = getHostListMode(sharedPreferences) == "whitelist"
|
||||
val currentLists = if (isWhitelist) config.activeLists else config.activeExcludeLists
|
||||
if (path !in currentLists) {
|
||||
val updatedLists = currentLists + path
|
||||
val newConfig = if (isWhitelist) {
|
||||
config.copy(activeLists = updatedLists)
|
||||
} else {
|
||||
config.copy(activeExcludeLists = updatedLists)
|
||||
}
|
||||
writeConfig(newConfig)
|
||||
}
|
||||
} else {
|
||||
val key = if (getHostListMode(sharedPreferences) == "whitelist") "lists" else "exclude_lists"
|
||||
val currentSet = sharedPreferences.getStringSet(key, emptySet())?.toMutableSet() ?: mutableSetOf()
|
||||
if (path !in currentSet) {
|
||||
currentSet.add(path)
|
||||
sharedPreferences.edit { putStringSet(key, currentSet) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun enableIpset(path: String, sharedPreferences: SharedPreferences) {
|
||||
if (sharedPreferences.getBoolean("use_module", false)) {
|
||||
val config = readConfig()
|
||||
val isWhitelist = getHostListMode(sharedPreferences) == "whitelist"
|
||||
val currentIpsets = if (isWhitelist) config.activeIpsets else config.activeExcludeIpsets
|
||||
if (path !in currentIpsets) {
|
||||
val updatedIpsets = currentIpsets + path
|
||||
val newConfig = if (isWhitelist) {
|
||||
config.copy(activeIpsets = updatedIpsets)
|
||||
} else {
|
||||
config.copy(activeExcludeIpsets = updatedIpsets)
|
||||
}
|
||||
writeConfig(newConfig)
|
||||
}
|
||||
} else {
|
||||
val key = if (getHostListMode(sharedPreferences) == "whitelist") "ipsets" else "exclude_ipsets"
|
||||
val currentSet = sharedPreferences.getStringSet(key, emptySet())?.toMutableSet() ?: mutableSetOf()
|
||||
if (path !in currentSet) {
|
||||
currentSet.add(path)
|
||||
sharedPreferences.edit { putStringSet(key, currentSet) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun enableStrategy(path: String, sharedPreferences: SharedPreferences) {
|
||||
if (sharedPreferences.getBoolean("use_module", false)) {
|
||||
val config = readConfig()
|
||||
if (config.strategy != path) {
|
||||
writeConfig(config.copy(strategy = path))
|
||||
}
|
||||
} else {
|
||||
sharedPreferences.edit { putString("active_strategy", path) }
|
||||
}
|
||||
}
|
||||
|
||||
fun disableList(path: String, sharedPreferences: SharedPreferences) {
|
||||
if (sharedPreferences.getBoolean("use_module", false)) {
|
||||
val config = readConfig()
|
||||
val isWhitelist = getHostListMode(sharedPreferences) == "whitelist"
|
||||
val currentLists = if (isWhitelist) config.activeLists else config.activeExcludeLists
|
||||
if (path in currentLists) {
|
||||
val updatedLists = currentLists.filter { it != path }
|
||||
val newConfig = if (isWhitelist) {
|
||||
config.copy(activeLists = updatedLists)
|
||||
} else {
|
||||
config.copy(activeExcludeLists = updatedLists)
|
||||
}
|
||||
writeConfig(newConfig)
|
||||
}
|
||||
} else {
|
||||
val key = if (getHostListMode(sharedPreferences) == "whitelist") "lists" else "exclude_lists"
|
||||
val currentSet = sharedPreferences.getStringSet(key, emptySet())?.toMutableSet() ?: mutableSetOf()
|
||||
if (path in currentSet) {
|
||||
currentSet.remove(path)
|
||||
sharedPreferences.edit { putStringSet(key, currentSet) }
|
||||
}
|
||||
if (currentSet.isEmpty()) {
|
||||
sharedPreferences.edit { remove(key) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun disableIpset(path: String, sharedPreferences: SharedPreferences) {
|
||||
if (sharedPreferences.getBoolean("use_module", false)) {
|
||||
val config = readConfig()
|
||||
val isWhitelist = getHostListMode(sharedPreferences) == "whitelist"
|
||||
val currentIpsets = if (isWhitelist) config.activeIpsets else config.activeExcludeIpsets
|
||||
if (path in currentIpsets) {
|
||||
val updatedIpsets = currentIpsets.filter { it != path }
|
||||
val newConfig = if (isWhitelist) {
|
||||
config.copy(activeIpsets = updatedIpsets)
|
||||
} else {
|
||||
config.copy(activeExcludeIpsets = updatedIpsets)
|
||||
}
|
||||
writeConfig(newConfig)
|
||||
}
|
||||
} else {
|
||||
val key = if (getHostListMode(sharedPreferences) == "whitelist") "ipsets" else "exclude_ipsets"
|
||||
val currentSet = sharedPreferences.getStringSet(key, emptySet())?.toMutableSet() ?: mutableSetOf()
|
||||
if (path in currentSet) {
|
||||
currentSet.remove(path)
|
||||
sharedPreferences.edit { putStringSet(key, currentSet) }
|
||||
}
|
||||
if (currentSet.isEmpty()) {
|
||||
sharedPreferences.edit { remove(key) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun disableStrategy(path: String, sharedPreferences: SharedPreferences) {
|
||||
if (sharedPreferences.getBoolean("use_module", false)) {
|
||||
val config = readConfig()
|
||||
if (config.strategy == path) {
|
||||
writeConfig(config.copy(strategy = ""))
|
||||
}
|
||||
} else {
|
||||
sharedPreferences.edit { remove("active_strategy") }
|
||||
}
|
||||
}
|
||||
|
||||
fun addPackageToList(listType: AppListType, packageName: String, prefs: SharedPreferences, context: Context) {
|
||||
if (prefs.getBoolean("use_module", false)) {
|
||||
val config = readConfig()
|
||||
if (listType == AppListType.Whitelist) {
|
||||
if (packageName !in config.whitelist) {
|
||||
writeConfig(config.copy(whitelist = config.whitelist + packageName))
|
||||
}
|
||||
} else if (listType == AppListType.Blacklist) {
|
||||
if (packageName !in config.blacklist) {
|
||||
writeConfig(config.copy(blacklist = config.blacklist + packageName))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
val prefs = context.getSharedPreferences("settings", MODE_PRIVATE)
|
||||
if (listType == AppListType.Whitelist) {
|
||||
val set = prefs.getStringSet("whitelist", emptySet())?.toMutableSet() ?: mutableSetOf()
|
||||
set.add(packageName)
|
||||
prefs.edit().putStringSet("whitelist", set).apply()
|
||||
}
|
||||
if (listType == AppListType.Blacklist) {
|
||||
val set = prefs.getStringSet("blacklist", emptySet())?.toMutableSet() ?: mutableSetOf()
|
||||
set.add(packageName)
|
||||
prefs.edit().putStringSet("blacklist", set).apply()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun removePackageFromList(listType: AppListType, packageName: String, prefs: SharedPreferences, context: Context) {
|
||||
if (prefs.getBoolean("use_module", false)) {
|
||||
val config = readConfig()
|
||||
if (listType == AppListType.Whitelist) {
|
||||
if (packageName in config.whitelist) {
|
||||
writeConfig(config.copy(whitelist = config.whitelist.filter { it != packageName }))
|
||||
}
|
||||
} else if (listType == AppListType.Blacklist) {
|
||||
if (packageName in config.blacklist) {
|
||||
writeConfig(config.copy(blacklist = config.blacklist.filter { it != packageName }))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
val prefs = context.getSharedPreferences("settings", MODE_PRIVATE)
|
||||
if (listType == AppListType.Whitelist) {
|
||||
val set = prefs.getStringSet("whitelist", emptySet())?.toMutableSet() ?: mutableSetOf()
|
||||
set.remove(packageName)
|
||||
prefs.edit().putStringSet("whitelist", set).apply()
|
||||
}
|
||||
if (listType == AppListType.Blacklist) {
|
||||
val set = prefs.getStringSet("blacklist", emptySet())?.toMutableSet() ?: mutableSetOf()
|
||||
set.remove(packageName)
|
||||
prefs.edit().putStringSet("blacklist", set).apply()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun isInList(listType: AppListType, packageName: String, prefs: SharedPreferences, context: Context): Boolean {
|
||||
if (prefs.getBoolean("use_module", false)) {
|
||||
val config = readConfig()
|
||||
return if (listType == AppListType.Whitelist) {
|
||||
packageName in config.whitelist
|
||||
} else {
|
||||
packageName in config.blacklist
|
||||
}
|
||||
} else {
|
||||
val prefs = context.getSharedPreferences("settings", MODE_PRIVATE)
|
||||
if (listType == AppListType.Whitelist) {
|
||||
val whitelist = prefs.getStringSet("whitelist", emptySet()) ?: emptySet()
|
||||
return packageName in whitelist
|
||||
} else {
|
||||
val blacklist = prefs.getStringSet("blacklist", emptySet()) ?: emptySet()
|
||||
return packageName in blacklist
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun getAppList(listType: AppListType, sharedPreferences: SharedPreferences, context: Context): Set<String> {
|
||||
if (sharedPreferences.getBoolean("use_module", false)) {
|
||||
val config = readConfig()
|
||||
return if (listType == AppListType.Whitelist) {
|
||||
config.whitelist.toSet()
|
||||
} else {
|
||||
config.blacklist.toSet()
|
||||
}
|
||||
} else {
|
||||
return if (listType == AppListType.Whitelist) context.getSharedPreferences("settings", MODE_PRIVATE)
|
||||
.getStringSet("whitelist", emptySet()) ?: emptySet()
|
||||
else context.getSharedPreferences("settings", MODE_PRIVATE)
|
||||
.getStringSet("blacklist", emptySet()) ?: emptySet()
|
||||
}
|
||||
}
|
||||
|
||||
fun getAppsListMode(prefs: SharedPreferences): String {
|
||||
if (prefs.getBoolean("use_module", false)) {
|
||||
val applist = readConfig().appList
|
||||
Log.d("App list", "Equals to $applist")
|
||||
return if (applist == "whitelist" || applist == "blacklist" || applist == "none") applist
|
||||
else "none"
|
||||
} else {
|
||||
return prefs.getString("app_list", "none")!!
|
||||
}
|
||||
}
|
||||
|
||||
fun setAppsListMode(prefs: SharedPreferences, mode: String) {
|
||||
if (prefs.getBoolean("use_module", false)) {
|
||||
val config = readConfig()
|
||||
writeConfig(config.copy(appList = mode))
|
||||
} 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 config = readConfig()
|
||||
writeConfig(config.copy(listType = mode))
|
||||
} 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 hostlist = readConfig().listType
|
||||
return if (hostlist == "whitelist" || hostlist == "blacklist") hostlist
|
||||
else "whitelist"
|
||||
} else {
|
||||
return prefs.getString("list_type", "whitelist")!!
|
||||
}
|
||||
}
|
||||
@@ -5,12 +5,13 @@
|
||||
<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>
|
||||
<string name="btn_update">Обновить</string>
|
||||
<string name="btn_dismiss">Отмена</string>
|
||||
<string name="text_welcome">Привет в zaprett! Это приложение предназначено для обхода цензуры и иных блокировок. Для полноценной работоспособности необходимо установить Magisk модуль.</string>
|
||||
<string name="text_welcome">Добро пожаловать в zaprett! Это приложение предназначено для обхода цензуры и иных блокировок. Для полноценной работоспособности необходимо установить Magisk модуль.</string>
|
||||
<string name="btn_use_root">Использовать модуль</string>
|
||||
<string name="error_root_title">Root не получен</string>
|
||||
<string name="error_root_message">Не получилось получить root доступ. Дайте права root для использования модуля Magisk</string>
|
||||
@@ -23,7 +24,7 @@
|
||||
<string name="status_enabled">Сервис zaprett работает. Нажми для обновления состояния</string>
|
||||
<string name="status_disabled">Сервис zaprett не работает. Нажми для обновления состояния</string>
|
||||
<string name="status_crashed">Сервис zaprett вероятно крашнулся. Нажмите кнопку перезапуска ниже</string>
|
||||
<string name="update_available">Доступно новое обновление!</string>
|
||||
<string name="update_available">Доступно обновление</string>
|
||||
<string name="btn_update_on_boot">Обновлять статус при запуске Главной страницы</string>
|
||||
<string name="btn_start_service">Запустить сервис</string>
|
||||
<string name="btn_stop_service">Остановить сервис</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>
|
||||
@@ -89,4 +92,32 @@
|
||||
<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>
|
||||
<string name="no_storage_permission_message">Нет разрешения на доступ к файлам</string>
|
||||
<string name="reset_settings_title">Сброс настроек</string>
|
||||
<string name="reset_settings_message">Вы действительно хотите сбросить настройки?</string>
|
||||
</resources>
|
||||
@@ -5,12 +5,13 @@
|
||||
<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>
|
||||
<string name="btn_update">Update</string>
|
||||
<string name="btn_dismiss">Dismiss</string>
|
||||
<string name="text_welcome">Hello to zaprett! This application is designed to bypass censorship and other blockages. For full functionality you need to install Magisk module.</string>
|
||||
<string name="text_welcome">Welcome to zaprett! This application is designed to bypass censorship and other blockages. For full functionality you need to install Magisk module.</string>
|
||||
<string name="btn_use_root">Use module</string>
|
||||
<string name="error_root_title">Can\'t get root</string>
|
||||
<string name="error_root_message">Couldn\'t get root access. Give root access to use the Magisk module</string>
|
||||
@@ -23,7 +24,7 @@
|
||||
<string name="status_enabled">zaprett service is working. Tap to update</string>
|
||||
<string name="status_disabled">zaprett service disabled.</string>
|
||||
<string name="status_crashed">zaprett service crashed. Tap restart button below</string>
|
||||
<string name="update_available">New update available!</string>
|
||||
<string name="update_available">Update available</string>
|
||||
<string name="btn_update_on_boot">Update the status when the Home page is launched</string>
|
||||
<string name="btn_start_service">Start service</string>
|
||||
<string name="btn_stop_service">Stop service</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,33 @@
|
||||
<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/strategies</string>
|
||||
<string name="selection_no_hosts_message">No active host lists or available strategies were found. Please enable one or more, or download strategies for testing, otherwise the selection will not work.</string>
|
||||
<string name="selection_available_domains">Available domains</string>
|
||||
<string name="no_storage_permission_message">Missing permission to access files</string>
|
||||
<string name="reset_settings_title">Reset settings</string>
|
||||
<string name="reset_settings_message">Are you sure you want to reset the settings?</string>
|
||||
</resources>
|
||||
@@ -1,4 +1,3 @@
|
||||
Добавление черных списков
|
||||
Исправление ошибок
|
||||
|
||||
Внимание! Эта версия приложения совместима с модулем версии 5.0+
|
||||
1. Изменения под новую версию модуля
|
||||
2. Исправление работы ipset с byedpi
|
||||
3. Исправление вылетов
|
||||
|
||||
@@ -1,23 +1,24 @@
|
||||
[versions]
|
||||
agp = "8.12.1"
|
||||
kotlin = "2.2.10"
|
||||
agp = "8.13.1"
|
||||
kotlin = "2.2.21"
|
||||
coreKtx = "1.17.0"
|
||||
junit = "4.13.2"
|
||||
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"
|
||||
lifecycleRuntimeKtx = "2.10.0"
|
||||
activityCompose = "1.12.1"
|
||||
composeBom = "2025.12.00"
|
||||
compose-material3 = "1.5.0-alpha10"
|
||||
compose-material3-adaptive = "1.5.0-alpha10"
|
||||
navigation = "2.9.6"
|
||||
compose-icons = "1.7.8"
|
||||
libsu = "6.0.0"
|
||||
okhttp = "5.1.0"
|
||||
okhttp = "5.3.2"
|
||||
serialization = "1.9.0"
|
||||
firebase-bom = "34.1.0"
|
||||
firebase-bom = "34.6.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" }
|
||||
@@ -47,6 +48,7 @@ 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]
|
||||
|
||||
@@ -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"
|
||||
23
rust/Cargo.toml
Normal file
23
rust/Cargo.toml
Normal file
@@ -0,0 +1,23 @@
|
||||
[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"
|
||||
|
||||
[profile.release]
|
||||
opt-level = "z"
|
||||
lto = true
|
||||
strip = true
|
||||
codegen-units = 1
|
||||
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.7",
|
||||
"versionCode": 19,
|
||||
"downloadUrl": "https://github.com/CherretGit/zaprett-app/releases/download/2_7_0/app-release.apk",
|
||||
"version": "2.12",
|
||||
"versionCode": 24,
|
||||
"downloadUrl": "https://github.com/CherretGit/zaprett-app/releases/download/2.12.0/app-release.apk",
|
||||
"changelogUrl": "https://raw.githubusercontent.com/CherretGit/zaprett-app/refs/heads/main/changelog.md"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user