42 Commits
2.10.0 ... main

Author SHA1 Message Date
cheesedroid
774948ae87 Update update.json 2025-12-08 19:45:16 +03:00
cheesedroid
297ce0cdea Update changelog.md 2025-12-08 19:44:53 +03:00
CherretGit
35b0068b8c optimize libbyedpi 2025-12-08 23:11:49 +07:00
cheesedroid
cdcd8b85b5 Update build.gradle.kts 2025-12-08 18:11:45 +03:00
CherretGit
2dd181494a remove unnecessary modifier 2025-12-08 16:19:47 +07:00
white
77823252f2 add autostop on strategy testing error 2025-12-08 12:07:38 +03:00
CherretGit
87a9740aad remove unused import 2025-12-08 16:04:52 +07:00
CherretGit
efb60d0c93 fix shadow 2025-12-08 16:02:44 +07:00
white
f0c3baa3bd change NoHostsCard and it's logic, added getAllStrategies(prefs).isEmpty() testing 2025-12-08 11:44:15 +03:00
white
57217bbe1c add module error handling (and showing) in HostsScreen.kt, IpsetsScreen.kt, StrategyScreen.kt, StrategySelectionScreen.kt 2025-12-04 17:43:04 +03:00
white
6f9bd86d70 update agp 2025-12-04 16:45:32 +03:00
white
80f5934060 remove unwanted popBackStack() in error dialog 2025-12-04 16:25:56 +03:00
white
0065b8a92b added module errors handling on service start, stop, restart, added automated service status updating on start, stop, restart 2025-12-04 16:08:05 +03:00
CherretGit
b5e3d256a6 Merge remote-tracking branch 'origin/main' 2025-11-29 17:33:31 +07:00
CherretGit
77bbd41609 fix strings 2025-11-29 17:33:22 +07:00
CherretGit
4e1568b405 Update workflow.yml 2025-11-26 19:26:13 +07:00
CherretGit
b8844c8cca add reset settings option 2025-11-22 17:53:14 +07:00
CherretGit
1d6eb32473 Update changelog.md 2025-11-04 02:46:33 +07:00
CherretGit
ded89165dc Update changelog.md 2025-11-04 02:46:16 +07:00
CherretGit
ed7452b7cb Update update.json 2025-11-04 02:43:59 +07:00
CherretGit
717368b4e9 bump version 2025-11-03 18:03:43 +07:00
CherretGit
36885a8410 fix some crashed from firebase crashlytics 2025-11-03 17:59:27 +07:00
CherretGit
ad614b3b4a fix ByeDpiVpnService 2025-11-03 02:19:06 +07:00
CherretGit
5c8425573a change commands in ZaprettManager 2025-11-03 02:05:14 +07:00
CherretGit
59e30206f7 Merge remote-tracking branch 'origin/main' 2025-10-30 21:42:50 +07:00
CherretGit
c4dcf934aa Config to json 2025-10-30 21:42:43 +07:00
egor-white
1bbeb4e987 Update README.md 2025-10-27 16:49:10 +03:00
egor-white
003a4c57a3 update changelog 2025-10-18 15:57:54 +03:00
egor-white
432873409c Update update.json 2025-10-18 15:57:20 +03:00
CherretGit
c5b60b1ee9 use card onClick in settings items 2025-10-18 19:41:51 +07:00
CherretGit
2f349e5108 bump app version 2025-10-18 19:29:27 +07:00
CherretGit
e8a8fa69db fix JNI seg fault 2025-10-18 19:21:51 +07:00
CherretGit
c094a072b8 fix app list mode 2025-10-18 18:46:40 +07:00
white
786321d852 edit strings.xml 2025-10-18 12:57:21 +03:00
white
7a17c93622 change selection screen title font size 2025-10-18 12:55:56 +03:00
white
cf38ef7ac2 some strategy selection interface changes 2025-10-18 12:20:30 +03:00
CherretGit
94979d2b2e fix testDomain 2025-10-18 12:42:06 +07:00
CherretGit
bd1bdf8298 Merge remote-tracking branch 'origin/main' 2025-10-18 01:56:19 +07:00
CherretGit
adc98db3f1 add info in strategy selection 2025-10-18 01:56:11 +07:00
CherretGit
9bbc3b771f filter list/ipset/strategy files to only .txt 2025-10-17 15:37:59 +07:00
egor-white
51356e63eb Update update.json 2025-10-16 16:43:08 +03:00
egor-white
f3f2cf999c Update changelog.md 2025-10-16 16:42:32 +03:00
31 changed files with 879 additions and 682 deletions

View File

@@ -46,7 +46,7 @@ jobs:
rustup update stable rustup update stable
cargo install cargo-ndk cargo install cargo-ndk
rustup target add armv7-linux-androideabi rustup target add armv7-linux-androideabi
rustup target install aarch64-linux-android rustup target add aarch64-linux-android
rustup target add i686-linux-android rustup target add i686-linux-android
rustup target add x86_64-linux-android rustup target add x86_64-linux-android

View File

@@ -2,5 +2,26 @@
<project version="4"> <project version="4">
<component name="AppInsightsSettings"> <component name="AppInsightsSettings">
<option name="selectedTabId" value="Android Vitals" /> <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> </component>
</project> </project>

View File

@@ -1,5 +1,10 @@
# zaprett # zaprett
## О приложении ## О приложении
![GitHub Downloads (all assets, all releases)](https://img.shields.io/github/downloads/CherretGit/zaprett-app/total)
![GitHub Downloads (all assets, latest release)](https://img.shields.io/github/downloads/CherretGit/zaprett-app/latest/total)
![GitHub Actions Workflow Status](https://img.shields.io/github/actions/workflow/status/CherretGit/zaprett-app/workflow.yml)
Приложение разработано для работы с модулем [zaprett](https://github.com/egor-white/zaprett) Приложение разработано для работы с модулем [zaprett](https://github.com/egor-white/zaprett)
> [!IMPORTANT] > [!IMPORTANT]
> 📢 [Официальный Telegram-канал приложения](https://t.me/zaprett_module) > 📢 [Официальный Telegram-канал приложения](https://t.me/zaprett_module)

View File

@@ -16,8 +16,8 @@ android {
applicationId = "com.cherret.zaprett" applicationId = "com.cherret.zaprett"
minSdk = 29 minSdk = 29
targetSdk = 35 targetSdk = 35
versionCode = 22 versionCode = 25
versionName = "2.10" versionName = "2.13"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
} }
@@ -124,4 +124,4 @@ dependencies {
androidTestImplementation(libs.androidx.ui.test.junit4) androidTestImplementation(libs.androidx.ui.test.junit4)
debugImplementation(libs.androidx.ui.tooling) debugImplementation(libs.androidx.ui.tooling)
debugImplementation(libs.androidx.ui.test.manifest) debugImplementation(libs.androidx.ui.test.manifest)
} }

View File

@@ -61,6 +61,7 @@ import com.cherret.zaprett.ui.viewmodel.HostRepoViewModel
import com.cherret.zaprett.ui.viewmodel.IpsetRepoViewModel import com.cherret.zaprett.ui.viewmodel.IpsetRepoViewModel
import com.cherret.zaprett.ui.viewmodel.StrategyRepoViewModel import com.cherret.zaprett.ui.viewmodel.StrategyRepoViewModel
import com.cherret.zaprett.utils.checkModuleInstallation import com.cherret.zaprett.utils.checkModuleInstallation
import com.cherret.zaprett.utils.checkStoragePermission
import com.google.firebase.Firebase import com.google.firebase.Firebase
import com.google.firebase.analytics.FirebaseAnalytics import com.google.firebase.analytics.FirebaseAnalytics
import com.google.firebase.analytics.analytics import com.google.firebase.analytics.analytics
@@ -110,16 +111,7 @@ class MainActivity : ComponentActivity() {
} }
} }
var showStoragePermissionDialog by remember { var showStoragePermissionDialog by remember {
mutableStateOf( mutableStateOf(!checkStoragePermission(this))
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
!Environment.isExternalStorageManager()
} else {
ContextCompat.checkSelfPermission(
this,
Manifest.permission.WRITE_EXTERNAL_STORAGE
) != PackageManager.PERMISSION_GRANTED
}
)
} }
var showNotificationPermissionDialog by remember { var showNotificationPermissionDialog by remember {
mutableStateOf( mutableStateOf(
@@ -217,7 +209,7 @@ class MainActivity : ComponentActivity() {
startDestination = Screen.home.route, startDestination = Screen.home.route,
Modifier.padding(innerPadding) Modifier.padding(innerPadding)
) { ) {
composable(Screen.home.route) { HomeScreen(viewModel = viewModel, vpnPermissionLauncher) } composable(Screen.home.route) { HomeScreen(viewModel = viewModel,vpnPermissionLauncher) }
composable(Screen.hosts.route) { HostsScreen(navController) } composable(Screen.hosts.route) { HostsScreen(navController) }
composable(Screen.strategies.route) { StrategyScreen(navController) } composable(Screen.strategies.route) { StrategyScreen(navController) }
composable(Screen.ipsets.route) { IpsetsScreen(navController) } composable(Screen.ipsets.route) { IpsetsScreen(navController) }
@@ -272,5 +264,4 @@ class MainActivity : ComponentActivity() {
} }
) )
} }
} }

View File

@@ -266,24 +266,18 @@ class ByeDpiVpnService : VpnService() {
.flatMap { arg -> .flatMap { arg ->
if (getHostListMode(sharedPreferences) == "whitelist") { if (getHostListMode(sharedPreferences) == "whitelist") {
when { when {
arg == "\$hostlist" && list.isNotEmpty() -> listOf("-H", list) arg == "\$hostlist" && list.isNotEmpty() -> listOf("--hosts", list)
arg == "\$hostlist" && list.isEmpty() -> emptyList() arg == "\$hostlist" && list.isEmpty() -> emptyList()
arg == "\$ipset" && list.isNotEmpty() -> listOf("-H", list) arg == "\$ipset" && list.isNotEmpty() -> listOf("--ipset", list)
arg == "\$ipset" && list.isEmpty() -> emptyList() arg == "\$ipset" && list.isEmpty() -> emptyList()
else -> listOf(arg) else -> listOf(arg)
} }
} else { } else {
if (list.isNotEmpty()) { when {
listOf("-H", list, "-An", arg).filter { it != "\$hostlist" } arg == "\$hostlist" && list.isNotEmpty() -> listOf("--hosts", list, "-An", list)
} else { arg == "\$ipset" && ipset.isNotEmpty() -> listOf("--ipset", ipset, "-An", ipset)
listOf("-An", arg).filter { it != "\$hostlist" } arg == "\$hostlist" || arg == "\$ipset" -> emptyList()
} else -> listOf(arg)
if (ipset.isEmpty()) {
listOf("-H", list, "-An", arg).filter { it != "\$ipset" }
}
else {
listOf("-An", arg).filter { it != "\$ipset" }
} }
} }
} }

View File

@@ -3,5 +3,6 @@ package com.cherret.zaprett.data
data class StrategyCheckResult ( data class StrategyCheckResult (
val path : String, val path : String,
val progress : Float, val progress : Float,
val status : Int var domains: List<String>,
val status : StrategyTestingStatus,
) )

View File

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

View File

@@ -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()
)

View File

@@ -2,17 +2,24 @@ package com.cherret.zaprett.ui.component
import android.content.Context import android.content.Context
import android.content.SharedPreferences import android.content.SharedPreferences
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.expandVertically
import androidx.compose.animation.shrinkVertically
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Check import androidx.compose.material.icons.filled.Check
import androidx.compose.material.icons.filled.Delete import androidx.compose.material.icons.filled.Delete
import androidx.compose.material.icons.filled.InstallMobile import androidx.compose.material.icons.filled.InstallMobile
import androidx.compose.material.icons.filled.Update import androidx.compose.material.icons.filled.Update
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults import androidx.compose.material3.CardDefaults
import androidx.compose.material3.ElevatedCard import androidx.compose.material3.ElevatedCard
import androidx.compose.material3.FilledTonalButton import androidx.compose.material3.FilledTonalButton
@@ -26,8 +33,11 @@ import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Switch import androidx.compose.material3.Switch
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
@@ -36,6 +46,7 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import com.cherret.zaprett.R import com.cherret.zaprett.R
import com.cherret.zaprett.data.StrategyCheckResult import com.cherret.zaprett.data.StrategyCheckResult
import com.cherret.zaprett.data.StrategyTestingStatus
import com.cherret.zaprett.ui.viewmodel.BaseRepoViewModel import com.cherret.zaprett.ui.viewmodel.BaseRepoViewModel
import com.cherret.zaprett.utils.RepoItemInfo import com.cherret.zaprett.utils.RepoItemInfo
import com.cherret.zaprett.utils.disableStrategy import com.cherret.zaprett.utils.disableStrategy
@@ -184,9 +195,15 @@ fun RepoItem(
@Composable @Composable
fun StrategySelectionItem(strategy : StrategyCheckResult, prefs : SharedPreferences, context : Context, snackbarHostState : SnackbarHostState) { fun StrategySelectionItem(strategy : StrategyCheckResult, prefs : SharedPreferences, context : Context, snackbarHostState : SnackbarHostState) {
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
var expanded by remember { mutableStateOf(false) }
ElevatedCard ( ElevatedCard (
elevation = CardDefaults.cardElevation(defaultElevation = 6.dp), elevation = CardDefaults.cardElevation(defaultElevation = 6.dp),
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceContainer), colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceContainer),
onClick = {
if (strategy.status == StrategyTestingStatus.Completed && strategy.domains.isNotEmpty()) {
expanded = !expanded
}
},
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(start = 10.dp, end = 10.dp, top = 25.dp, bottom = 0.dp) .padding(start = 10.dp, end = 10.dp, top = 25.dp, bottom = 0.dp)
@@ -213,7 +230,7 @@ fun StrategySelectionItem(strategy : StrategyCheckResult, prefs : SharedPreferen
) )
} }
}, },
enabled = strategy.status == R.string.strategy_status_tested enabled = strategy.status == StrategyTestingStatus.Completed
) { ) {
Icon( Icon(
imageVector = Icons.Default.Check, imageVector = Icons.Default.Check,
@@ -223,7 +240,7 @@ fun StrategySelectionItem(strategy : StrategyCheckResult, prefs : SharedPreferen
} }
Row { Row {
Text( Text(
text = stringResource(strategy.status), text = stringResource(strategy.status.resId),
modifier = Modifier modifier = Modifier
.weight(1f), .weight(1f),
fontSize = 12.sp, fontSize = 12.sp,
@@ -255,5 +272,37 @@ fun StrategySelectionItem(strategy : StrategyCheckResult, prefs : SharedPreferen
) )
} }
} }
AnimatedVisibility(
visible = expanded,
enter = expandVertically(),
exit = shrinkVertically()
) {
Card (
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceContainer),
modifier = Modifier
.padding(horizontal = 8.dp)
.fillMaxWidth()
) {
Text(
text = stringResource(R.string.selection_available_domains)
)
LazyColumn(modifier = Modifier.heightIn(max = 300.dp)) {
items(strategy.domains) { item ->
Card(
elevation = CardDefaults.cardElevation(4.dp),
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 4.dp)
) {
Text(
text = item,
modifier = Modifier.padding(16.dp),
style = MaterialTheme.typography.bodyLarge
)
}
}
}
}
}
} }
} }

View File

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

View File

@@ -1,5 +1,6 @@
package com.cherret.zaprett.ui.screen package com.cherret.zaprett.ui.screen
import android.app.Activity
import android.content.Context import android.content.Context
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
@@ -11,6 +12,7 @@ import androidx.compose.foundation.lazy.items
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.filled.MoreVert import androidx.compose.material.icons.filled.MoreVert
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.DropdownMenu import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
@@ -18,6 +20,7 @@ import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
@@ -48,6 +51,7 @@ fun DebugScreen(navController: NavController) {
val editor = remember { sharedPreferences.edit() } val editor = remember { sharedPreferences.edit() }
val showUpdateUrlDialog = remember { mutableStateOf(false) } val showUpdateUrlDialog = remember { mutableStateOf(false) }
val textDialogValue = remember { mutableStateOf("") } val textDialogValue = remember { mutableStateOf("") }
val showResetSettingsDialog = remember { mutableStateOf(false) }
val settingsList = listOf( val settingsList = listOf(
Setting.Action( Setting.Action(
title = stringResource(R.string.btn_update_repository_url), title = stringResource(R.string.btn_update_repository_url),
@@ -56,6 +60,12 @@ fun DebugScreen(navController: NavController) {
showUpdateUrlDialog.value = true showUpdateUrlDialog.value = true
} }
), ),
Setting.Action(
title = stringResource(R.string.reset_settings_title),
onClick = {
showResetSettingsDialog.value = true
}
)
) )
Scaffold( Scaffold(
topBar = { topBar = {
@@ -115,4 +125,31 @@ fun DebugScreen(navController: NavController) {
editor.putString("update_repo_url", it).apply() editor.putString("update_repo_url", it).apply()
}, onDismiss = { showUpdateUrlDialog.value = false }) }, 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))
}
})
}
} }

View File

@@ -1,9 +1,13 @@
package com.cherret.zaprett.ui.screen package com.cherret.zaprett.ui.screen
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.SharedPreferences import android.content.SharedPreferences
import android.net.VpnService import android.net.VpnService
import android.os.Build
import android.widget.Toast
import androidx.activity.result.ActivityResultLauncher import androidx.activity.result.ActivityResultLauncher
import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.expandVertically import androidx.compose.animation.expandVertically
@@ -65,12 +69,15 @@ 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.compose.ui.unit.sp
import androidx.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.NavController
import com.cherret.zaprett.BuildConfig import com.cherret.zaprett.BuildConfig
import com.cherret.zaprett.R import com.cherret.zaprett.R
import com.cherret.zaprett.data.ServiceStatusUI import com.cherret.zaprett.data.ServiceStatusUI
import com.cherret.zaprett.ui.viewmodel.HomeViewModel import com.cherret.zaprett.ui.viewmodel.HomeViewModel
import dev.jeziellago.compose.markdowntext.MarkdownText import dev.jeziellago.compose.markdowntext.MarkdownText
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.serialization.SerializationException
import java.io.IOException
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
@@ -89,6 +96,7 @@ fun HomeScreen(viewModel: HomeViewModel = viewModel(), vpnLauncher: ActivityResu
val nfqwsVer = viewModel.nfqwsVer; val nfqwsVer = viewModel.nfqwsVer;
val byedpiVer = viewModel.byedpiVer; val byedpiVer = viewModel.byedpiVer;
val serviceMode = viewModel.serviceMode val serviceMode = viewModel.serviceMode
val error by viewModel.errorFlow.collectAsState()
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
viewModel.checkForUpdate() viewModel.checkForUpdate()
viewModel.checkServiceStatus() viewModel.checkServiceStatus()
@@ -107,6 +115,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( Scaffold(
topBar = { topBar = {
TopAppBar( TopAppBar(

View File

@@ -1,7 +1,11 @@
package com.cherret.zaprett.ui.screen package com.cherret.zaprett.ui.screen
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context import android.content.Context
import android.content.SharedPreferences import android.content.SharedPreferences
import android.os.Build
import android.widget.Toast
import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.ActivityResultLauncher import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
@@ -19,6 +23,7 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.Download import androidx.compose.material.icons.filled.Download
import androidx.compose.material.icons.filled.UploadFile import androidx.compose.material.icons.filled.UploadFile
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.DropdownMenu import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
@@ -31,10 +36,13 @@ import androidx.compose.material3.SingleChoiceSegmentedButtonRow
import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBar
import androidx.compose.material3.pulltorefresh.PullToRefreshBox import androidx.compose.material3.pulltorefresh.PullToRefreshBox
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
@@ -67,6 +75,7 @@ fun HostsScreen(navController: NavController, viewModel: HostsViewModel = viewMo
val allLists = viewModel.allItems val allLists = viewModel.allItems
val checked = viewModel.checked val checked = viewModel.checked
val isRefreshing = viewModel.isRefreshing val isRefreshing = viewModel.isRefreshing
val showPermissionDialog by viewModel.showNoPermissionDialog.collectAsState()
val filePickerLauncher = rememberLauncherForActivityResult( val filePickerLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.OpenDocument() contract = ActivityResultContracts.OpenDocument()
) { uri -> ) { uri ->
@@ -74,11 +83,43 @@ fun HostsScreen(navController: NavController, viewModel: HostsViewModel = viewMo
if (getHostListMode(prefs) == "whitelist") viewModel.copySelectedFile(context, "/lists/include", it) if (getHostListMode(prefs) == "whitelist") viewModel.copySelectedFile(context, "/lists/include", it)
else viewModel.copySelectedFile(context, "/lists/exclude", it) } else viewModel.copySelectedFile(context, "/lists/exclude", it) }
} }
val error by viewModel.errorFlow.collectAsState()
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
viewModel.refresh() 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( Scaffold(
topBar = { topBar = {
TopAppBar( TopAppBar(
@@ -147,10 +188,26 @@ fun HostsScreen(navController: NavController, viewModel: HostsViewModel = viewMo
}, },
snackbarHost = { SnackbarHost(hostState = snackbarHostState) } 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 @Composable
private fun FloatingMenu(navController: NavController, launcher: ActivityResultLauncher<Array<String>>) { private fun FloatingMenu(navController: NavController, launcher: ActivityResultLauncher<Array<String>>) {
var expanded by remember { mutableStateOf(false) } var expanded by remember { mutableStateOf(false) }

View File

@@ -1,7 +1,11 @@
package com.cherret.zaprett.ui.screen package com.cherret.zaprett.ui.screen
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context import android.content.Context
import android.content.SharedPreferences import android.content.SharedPreferences
import android.os.Build
import android.widget.Toast
import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.ActivityResultLauncher import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
@@ -19,6 +23,7 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.Download import androidx.compose.material.icons.filled.Download
import androidx.compose.material.icons.filled.UploadFile import androidx.compose.material.icons.filled.UploadFile
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.DropdownMenu import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
@@ -31,10 +36,12 @@ import androidx.compose.material3.SingleChoiceSegmentedButtonRow
import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBar
import androidx.compose.material3.pulltorefresh.PullToRefreshBox import androidx.compose.material3.pulltorefresh.PullToRefreshBox
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
@@ -68,6 +75,7 @@ fun IpsetsScreen(navController: NavController, viewModel: IpsetViewModel = viewM
val allLists = viewModel.allItems val allLists = viewModel.allItems
val checked = viewModel.checked val checked = viewModel.checked
val isRefreshing = viewModel.isRefreshing val isRefreshing = viewModel.isRefreshing
val showPermissionDialog by viewModel.showNoPermissionDialog.collectAsState()
val filePickerLauncher = rememberLauncherForActivityResult( val filePickerLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.OpenDocument() contract = ActivityResultContracts.OpenDocument()
) { uri -> ) { uri ->
@@ -75,11 +83,43 @@ fun IpsetsScreen(navController: NavController, viewModel: IpsetViewModel = viewM
if (getHostListMode(prefs) == "whitelist") viewModel.copySelectedFile(context, "/ipset/include", it) if (getHostListMode(prefs) == "whitelist") viewModel.copySelectedFile(context, "/ipset/include", it)
else viewModel.copySelectedFile(context, "/ipset/exclude", it) } else viewModel.copySelectedFile(context, "/ipset/exclude", it) }
} }
val error by viewModel.errorFlow.collectAsState()
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
viewModel.refresh() 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( Scaffold(
topBar = { topBar = {
TopAppBar( TopAppBar(
@@ -148,10 +188,26 @@ fun IpsetsScreen(navController: NavController, viewModel: IpsetViewModel = viewM
}, },
snackbarHost = { SnackbarHost(hostState = snackbarHostState) } 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 @Composable
private fun FloatingMenu(navController: NavController, launcher: ActivityResultLauncher<Array<String>>) { private fun FloatingMenu(navController: NavController, launcher: ActivityResultLauncher<Array<String>>) {
var expanded by remember { mutableStateOf(false) } var expanded by remember { mutableStateOf(false) }

View File

@@ -57,6 +57,7 @@ fun RepoScreen(navController: NavController, viewModel: BaseRepoViewModel) {
val snackbarHostState = remember { SnackbarHostState() } val snackbarHostState = remember { SnackbarHostState() }
val error by viewModel.errorFlow.collectAsState() val error by viewModel.errorFlow.collectAsState()
val downloadError by viewModel.downloadErrorFlow.collectAsState() val downloadError by viewModel.downloadErrorFlow.collectAsState()
val showPermissionDialog by viewModel.showPermissionDialog.collectAsState()
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
viewModel.refresh() viewModel.refresh()
} }
@@ -131,6 +132,24 @@ fun RepoScreen(navController: NavController, viewModel: BaseRepoViewModel) {
) )
} }
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( Scaffold(
topBar = { topBar = {

View File

@@ -1,6 +1,10 @@
package com.cherret.zaprett.ui.screen package com.cherret.zaprett.ui.screen
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context import android.content.Context
import android.os.Build
import android.widget.Toast
import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.ActivityResultLauncher import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
@@ -16,6 +20,7 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.Download import androidx.compose.material.icons.filled.Download
import androidx.compose.material.icons.filled.UploadFile import androidx.compose.material.icons.filled.UploadFile
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.DropdownMenu import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
@@ -25,10 +30,12 @@ import androidx.compose.material3.Scaffold
import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBar
import androidx.compose.material3.pulltorefresh.PullToRefreshBox import androidx.compose.material3.pulltorefresh.PullToRefreshBox
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
@@ -60,6 +67,7 @@ fun StrategyScreen(navController: NavController, viewModel: StrategyViewModel =
val allLists = viewModel.allItems val allLists = viewModel.allItems
val checked = viewModel.checked val checked = viewModel.checked
val isRefreshing = viewModel.isRefreshing val isRefreshing = viewModel.isRefreshing
val showPermissionDialog by viewModel.showNoPermissionDialog.collectAsState()
val filePickerLauncher = rememberLauncherForActivityResult( val filePickerLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.OpenDocument() contract = ActivityResultContracts.OpenDocument()
) { uri -> ) { uri ->
@@ -69,11 +77,43 @@ fun StrategyScreen(navController: NavController, viewModel: StrategyViewModel =
it it
) } ) }
} }
val error by viewModel.errorFlow.collectAsState()
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
viewModel.refresh() 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( Scaffold(
topBar = { topBar = {
TopAppBar( TopAppBar(
@@ -139,6 +179,24 @@ fun StrategyScreen(navController: NavController, viewModel: StrategyViewModel =
}, },
snackbarHost = { SnackbarHost(hostState = snackbarHostState) } 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 @Composable

View File

@@ -1,47 +1,55 @@
package com.cherret.zaprett.ui.screen 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.Context.MODE_PRIVATE
import android.content.Intent import android.content.Intent
import android.net.VpnService import android.net.VpnService
import android.os.Build
import android.widget.Toast
import androidx.activity.result.ActivityResultLauncher import androidx.activity.result.ActivityResultLauncher
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Scaffold
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.Font
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.sp
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.filled.Info import androidx.compose.material.icons.filled.Info
import androidx.compose.material3.AlertDialog import androidx.compose.material3.AlertDialog
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.FilledTonalButton import androidx.compose.material3.FilledTonalButton
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
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.TextButton
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState import androidx.compose.runtime.MutableState
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.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.viewModelScope import androidx.lifecycle.viewModelScope
import androidx.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.NavController import androidx.navigation.NavController
@@ -59,6 +67,7 @@ fun StrategySelectionScreen(navController: NavController, vpnLauncher: ActivityR
val prefs = context.getSharedPreferences("settings", MODE_PRIVATE) val prefs = context.getSharedPreferences("settings", MODE_PRIVATE)
var showDialog = remember { mutableStateOf(false) } var showDialog = remember { mutableStateOf(false) }
val requestVpnPermission by viewModel.requestVpnPermission.collectAsState() val requestVpnPermission by viewModel.requestVpnPermission.collectAsState()
val error by viewModel.errorFlow.collectAsState()
if (showDialog.value) { if (showDialog.value) {
InfoAlert { showDialog.value = false } InfoAlert { showDialog.value = false }
@@ -76,13 +85,44 @@ fun StrategySelectionScreen(navController: NavController, vpnLauncher: ActivityR
} }
} }
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( Scaffold(
topBar = { topBar = {
TopAppBar( TopAppBar(
title = { title = {
Text( Text(
text = stringResource(R.string.title_selection), text = stringResource(R.string.title_selection),
fontSize = 40.sp, fontSize = 30.sp,
fontFamily = FontFamily(Font(R.font.unbounded, FontWeight.Normal)) fontFamily = FontFamily(Font(R.font.unbounded, FontWeight.Normal))
) )
}, },
@@ -112,8 +152,11 @@ fun StrategySelectionScreen(navController: NavController, vpnLauncher: ActivityR
snackbarHost = { SnackbarHost(snackbarHostState) }, snackbarHost = { SnackbarHost(snackbarHostState) },
content = { paddingValues -> content = { paddingValues ->
LazyColumn ( LazyColumn (
modifier = Modifier contentPadding = PaddingValues(
.padding(paddingValues) top = paddingValues.calculateTopPadding(),
bottom = paddingValues.calculateBottomPadding() + 40.dp
),
modifier = Modifier.fillMaxSize()
) { ) {
item { item {
Row ( Row (
@@ -193,4 +236,3 @@ private fun NoHostsCard(noHostsCard: MutableState<Boolean>) {
) )
} }
} }

View File

@@ -16,9 +16,13 @@ import androidx.compose.runtime.setValue
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.AndroidViewModel
import com.cherret.zaprett.R import com.cherret.zaprett.R
import com.cherret.zaprett.utils.checkStoragePermission
import com.cherret.zaprett.utils.getZaprettPath import com.cherret.zaprett.utils.getZaprettPath
import com.cherret.zaprett.utils.restartService import com.cherret.zaprett.utils.restartService
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.io.File import java.io.File
import java.io.FileOutputStream import java.io.FileOutputStream
@@ -34,20 +38,35 @@ abstract class BaseListsViewModel(application: Application) : AndroidViewModel(a
var isRefreshing by mutableStateOf(false) var isRefreshing by mutableStateOf(false)
private set 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 loadAllItems(): Array<String>
abstract fun loadActiveItems(): Array<String> abstract fun loadActiveItems(): Array<String>
abstract fun onCheckedChange(item: String, isChecked: Boolean, snackbarHostState: SnackbarHostState, scope: CoroutineScope) abstract fun onCheckedChange(item: String, isChecked: Boolean, snackbarHostState: SnackbarHostState, scope: CoroutineScope)
abstract fun deleteItem(item: String, snackbarHostState: SnackbarHostState, scope: CoroutineScope) abstract fun deleteItem(item: String, snackbarHostState: SnackbarHostState, scope: CoroutineScope)
fun refresh() { fun refresh() {
isRefreshing = true when (checkStoragePermission(context)) {
allItems = loadAllItems().toList() true -> {
activeItems = loadActiveItems().toList() isRefreshing = true
checked.clear() allItems = loadAllItems().toList()
allItems.forEach { list -> activeItems = loadActiveItems().toList()
checked[list] = activeItems.contains(list) checked.clear()
allItems.forEach { list ->
checked[list] = activeItems.contains(list)
}
isRefreshing = false
}
false -> _showNoPermissionDialog.value = true
} }
isRefreshing = false }
fun hideNoPermissionDialog() {
_showNoPermissionDialog.value = false
} }
fun showRestartSnackbar(context: Context, snackbarHostState: SnackbarHostState, scope: CoroutineScope) { fun showRestartSnackbar(context: Context, snackbarHostState: SnackbarHostState, scope: CoroutineScope) {
@@ -57,12 +76,18 @@ abstract class BaseListsViewModel(application: Application) : AndroidViewModel(a
actionLabel = context.getString(R.string.btn_restart_service) actionLabel = context.getString(R.string.btn_restart_service)
) )
if (result == SnackbarResult.ActionPerformed) { if (result == SnackbarResult.ActionPerformed) {
restartService {} restartService { error ->
_errorFlow.value = error
}
snackbarHostState.showSnackbar(context.getString(R.string.snack_reload)) snackbarHostState.showSnackbar(context.getString(R.string.snack_reload))
} }
} }
} }
fun clearError() {
_errorFlow.value = ""
}
fun copySelectedFile(context: Context, path: String, uri: Uri) { fun copySelectedFile(context: Context, path: String, uri: Uri) {
//if (!Environment.isExternalStorageManager()) return //if (!Environment.isExternalStorageManager()) return
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R){ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R){

View File

@@ -13,6 +13,7 @@ import androidx.lifecycle.viewModelScope
import com.cherret.zaprett.utils.RepoItemInfo import com.cherret.zaprett.utils.RepoItemInfo
import com.cherret.zaprett.R import com.cherret.zaprett.R
import com.cherret.zaprett.data.ItemType import com.cherret.zaprett.data.ItemType
import com.cherret.zaprett.utils.checkStoragePermission
import com.cherret.zaprett.utils.download import com.cherret.zaprett.utils.download
import com.cherret.zaprett.utils.getFileSha256 import com.cherret.zaprett.utils.getFileSha256
import com.cherret.zaprett.utils.getHostListMode import com.cherret.zaprett.utils.getHostListMode
@@ -36,6 +37,9 @@ abstract class BaseRepoViewModel(application: Application) : AndroidViewModel(ap
val downloadErrorFlow: StateFlow<String?> = _downloadErrorFlow val downloadErrorFlow: StateFlow<String?> = _downloadErrorFlow
private var _showPermissionDialog = MutableStateFlow(false)
val showPermissionDialog: StateFlow<Boolean> = _showPermissionDialog
var hostLists = mutableStateOf<List<RepoItemInfo>>(emptyList()) var hostLists = mutableStateOf<List<RepoItemInfo>>(emptyList())
protected set protected set
@@ -95,32 +99,41 @@ abstract class BaseRepoViewModel(application: Application) : AndroidViewModel(ap
} }
fun install(item: RepoItemInfo) { fun install(item: RepoItemInfo) {
isInstalling[item.name] = true when (checkStoragePermission(context)) {
val downloadId = download(context, item.url) true -> {
registerDownloadListener(context, downloadId, { uri -> isInstalling[item.name] = true
viewModelScope.launch(Dispatchers.IO) { val downloadId = download(context, item.url)
val sourceFile = File(uri.path!!) registerDownloadListener(context, downloadId, { uri ->
val targetDir = when (item.type) { viewModelScope.launch(Dispatchers.IO) {
ItemType.byedpi -> File(getZaprettPath(), "strategies/byedpi") val sourceFile = File(uri.path!!)
ItemType.nfqws -> File(getZaprettPath(), "strategies/nfqws") val targetDir = when (item.type) {
ItemType.list -> File(getZaprettPath(), "lists/include") ItemType.byedpi -> File(getZaprettPath(), "strategies/byedpi")
ItemType.list_exclude -> File(getZaprettPath(), "lists/exclude") ItemType.nfqws -> File(getZaprettPath(), "strategies/nfqws")
ItemType.ipset -> File(getZaprettPath(), "ipset/include") ItemType.list -> File(getZaprettPath(), "lists/include")
ItemType.ipset_exclude -> File(getZaprettPath(), "ipset/exclude") ItemType.list_exclude -> File(getZaprettPath(), "lists/exclude")
} ItemType.ipset -> File(getZaprettPath(), "ipset/include")
val targetFile = File(targetDir, uri.lastPathSegment!!) ItemType.ipset_exclude -> File(getZaprettPath(), "ipset/exclude")
sourceFile.copyTo(targetFile, overwrite = true) }
sourceFile.delete() val targetFile = File(targetDir, uri.lastPathSegment!!)
isInstalling[item.name] = false sourceFile.copyTo(targetFile, overwrite = true)
isUpdate[item.name] = false sourceFile.delete()
refresh() isInstalling[item.name] = false
isUpdate[item.name] = false
refresh()
}
}, onError = {
isInstalling[item.name] = false
isUpdate[item.name] = false
refresh()
_downloadErrorFlow.value = it
})
} }
}, onError = { false -> _showPermissionDialog.value = true
isInstalling[item.name] = false }
isUpdate[item.name] = false }
refresh()
_downloadErrorFlow.value = it fun hideNoPermissionDialog() {
}) _showPermissionDialog.value = false
} }
fun update(item: RepoItemInfo) { fun update(item: RepoItemInfo) {
@@ -157,7 +170,6 @@ abstract class BaseRepoViewModel(application: Application) : AndroidViewModel(ap
) )
} }
fun showRestartSnackbar(snackbarHostState: SnackbarHostState) { fun showRestartSnackbar(snackbarHostState: SnackbarHostState) {
viewModelScope.launch { viewModelScope.launch {
val result = snackbarHostState.showSnackbar( val result = snackbarHostState.showSnackbar(

View File

@@ -45,6 +45,9 @@ class HomeViewModel(application: Application) : AndroidViewModel(application) {
private val _serviceStatus = MutableStateFlow(ServiceStatusUI()) private val _serviceStatus = MutableStateFlow(ServiceStatusUI())
val serviceStatus: StateFlow<ServiceStatusUI> = _serviceStatus.asStateFlow() val serviceStatus: StateFlow<ServiceStatusUI> = _serviceStatus.asStateFlow()
private val _errorFlow = MutableStateFlow("")
val errorFlow = _errorFlow.asStateFlow()
var moduleVer = mutableStateOf(context.getString(R.string.unknown_text)) var moduleVer = mutableStateOf(context.getString(R.string.unknown_text))
private set private set
@@ -103,7 +106,7 @@ class HomeViewModel(application: Application) : AndroidViewModel(application) {
} }
fun checkServiceStatus() { fun checkServiceStatus() {
val updateOnBoot = prefs.getBoolean("update_on_boot", false) val updateOnBoot = prefs.getBoolean("update_on_boot", true)
if (updateOnBoot) { if (updateOnBoot) {
val useModule = prefs.getBoolean("use_module", false) val useModule = prefs.getBoolean("use_module", false)
updateServiceStatus(useModule) updateServiceStatus(useModule)
@@ -129,7 +132,10 @@ class HomeViewModel(application: Application) : AndroidViewModel(application) {
) )
) )
} }
if (!isEnabled) startService {} if (!isEnabled) startService { error ->
_errorFlow.value = error
onCardClick()
}
} }
} else { } else {
if (ByeDpiVpnService.status == ServiceStatus.Disconnected || ByeDpiVpnService.status == ServiceStatus.Failed) { if (ByeDpiVpnService.status == ServiceStatus.Disconnected || ByeDpiVpnService.status == ServiceStatus.Failed) {
@@ -169,7 +175,10 @@ class HomeViewModel(application: Application) : AndroidViewModel(application) {
) )
) )
} }
if (isEnabled) stopService {} if (isEnabled) stopService { error ->
_errorFlow.value = error
onCardClick()
}
} }
} else { } else {
if (ByeDpiVpnService.status == ServiceStatus.Connected) { if (ByeDpiVpnService.status == ServiceStatus.Connected) {
@@ -192,7 +201,10 @@ class HomeViewModel(application: Application) : AndroidViewModel(application) {
fun onBtnRestart(snackbarHostState: SnackbarHostState, scope: CoroutineScope) { fun onBtnRestart(snackbarHostState: SnackbarHostState, scope: CoroutineScope) {
if (prefs.getBoolean("use_module", false)) { if (prefs.getBoolean("use_module", false)) {
restartService {} restartService { error ->
_errorFlow.value = error
onCardClick()
}
scope.launch { scope.launch {
snackbarHostState.showSnackbar(context.getString(R.string.snack_reload)) snackbarHostState.showSnackbar(context.getString(R.string.snack_reload))
} }
@@ -242,6 +254,7 @@ class HomeViewModel(application: Application) : AndroidViewModel(application) {
} }
} }
// unused?
fun parseArgs(ip: String, port: String, lines: List<String>): Array<String> { fun parseArgs(ip: String, port: String, lines: List<String>): Array<String> {
val regex = Regex("""--?\S+(?:=(?:[^"'\s]+|"[^"]*"|'[^']*'))?|[^\s]+""") val regex = Regex("""--?\S+(?:=(?:[^"'\s]+|"[^"]*"|'[^']*'))?|[^\s]+""")
val parsedArgs = lines val parsedArgs = lines
@@ -249,4 +262,7 @@ class HomeViewModel(application: Application) : AndroidViewModel(application) {
return arrayOf("ciadpi", "--ip", ip, "--port", port) + parsedArgs return arrayOf("ciadpi", "--ip", ip, "--port", port) + parsedArgs
} }
fun clearError() {
_errorFlow.value = ""
}
} }

View File

@@ -12,6 +12,7 @@ import com.cherret.zaprett.R
import com.cherret.zaprett.byedpi.ByeDpiVpnService import com.cherret.zaprett.byedpi.ByeDpiVpnService
import com.cherret.zaprett.data.ServiceStatus import com.cherret.zaprett.data.ServiceStatus
import com.cherret.zaprett.data.StrategyCheckResult import com.cherret.zaprett.data.StrategyCheckResult
import com.cherret.zaprett.data.StrategyTestingStatus
import com.cherret.zaprett.utils.disableStrategy import com.cherret.zaprett.utils.disableStrategy
import com.cherret.zaprett.utils.enableStrategy import com.cherret.zaprett.utils.enableStrategy
import com.cherret.zaprett.utils.getActiveLists import com.cherret.zaprett.utils.getActiveLists
@@ -26,22 +27,24 @@ import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import kotlinx.coroutines.withTimeoutOrNull import kotlinx.coroutines.withTimeoutOrNull
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.Request import okhttp3.Request
import java.io.File import java.io.File
import java.net.InetSocketAddress
import java.net.Proxy
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
class StrategySelectionViewModel(application: Application) : AndroidViewModel(application) { class StrategySelectionViewModel(application: Application) : AndroidViewModel(application) {
val prefs = application.getSharedPreferences("settings", MODE_PRIVATE) val prefs = application.getSharedPreferences("settings", MODE_PRIVATE)
val client = OkHttpClient.Builder()
.callTimeout(prefs.getLong("probe_timeout", 1000L), TimeUnit.MILLISECONDS)
.build()
val context = application val context = application
private val _requestVpnPermission = MutableStateFlow(false) private val _requestVpnPermission = MutableStateFlow(false)
val requestVpnPermission = _requestVpnPermission.asStateFlow() val requestVpnPermission = _requestVpnPermission.asStateFlow()
private val _errorFlow = MutableStateFlow("")
val errorFlow = _errorFlow.asStateFlow()
val strategyStates = mutableStateListOf<StrategyCheckResult>() val strategyStates = mutableStateListOf<StrategyCheckResult>()
var noHostsCard = mutableStateOf(false) var noHostsCard = mutableStateOf(false)
private set private set
@@ -51,14 +54,29 @@ class StrategySelectionViewModel(application: Application) : AndroidViewModel(ap
checkHosts() 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() { fun loadStrategies() {
val strategyList = getAllStrategies(prefs) val strategyList = getAllStrategies(prefs)
strategyStates.clear() strategyStates.clear()
strategyList.forEach { name -> strategyList.forEach { name ->
strategyStates += StrategyCheckResult( strategyStates += StrategyCheckResult(
path = name, path = name,
status = R.string.strategy_status_waiting, status = StrategyTestingStatus.Waiting,
progress = 0f progress = 0f,
domains = emptyList()
) )
} }
} }
@@ -68,21 +86,23 @@ class StrategySelectionViewModel(application: Application) : AndroidViewModel(ap
.url("https://${domain}") .url("https://${domain}")
.build() .build()
try { try {
client.newCall(request).execute().use { response -> buildHttpClient().newCall(request).execute().use { response ->
response.isSuccessful || (response.code in 300..399) val body = response.body.byteStream().readBytes()
val contentLength = response.body.contentLength()
contentLength <= 0 || body.size.toLong() >= contentLength
} }
} catch (e: Exception) { } catch (e: Exception) {
false false
} }
} }
suspend fun countReachable(urls: List<String>): Float = coroutineScope { suspend fun countReachable(index: Int, urls: List<String>): Float = coroutineScope {
if (urls.isEmpty()) return@coroutineScope 0f if (urls.isEmpty()) return@coroutineScope 0f
val results: List<Boolean> = urls.map { url -> val results: List<String> = urls.map { url ->
async { testDomain(url) } async { if (testDomain(url)) url else null }
}.awaitAll() }.awaitAll().filterNotNull()
val successCount = results.count { it } strategyStates[index].domains = results
(successCount.toFloat() / urls.size.toFloat()).coerceIn(0f, 1f) (results.size.toFloat() / urls.size.toFloat()).coerceIn(0f, 1f)
} }
suspend fun readActiveListsLines(): List<String> = withContext(Dispatchers.IO) { suspend fun readActiveListsLines(): List<String> = withContext(Dispatchers.IO) {
@@ -102,22 +122,33 @@ class StrategySelectionViewModel(application: Application) : AndroidViewModel(ap
} }
suspend fun performTest() { suspend fun performTest() {
val targets = readActiveListsLines() val targets = readActiveListsLines()
var stopTest : Boolean = false;
for (index in strategyStates.indices) { for (index in strategyStates.indices) {
val current = strategyStates[index] val current = strategyStates[index]
strategyStates[index] = current.copy(status = R.string.strategy_status_testing) if (stopTest) break
strategyStates[index] = current.copy(status = StrategyTestingStatus.Testing)
enableStrategy(current.path, prefs) enableStrategy(current.path, prefs)
if (prefs.getBoolean("use_module", false)) { if (prefs.getBoolean("use_module", false)) {
getStatus { if (it) stopService {} } getStatus { if (it) stopService { error ->
startService {} _errorFlow.value = error
if (error.isNotEmpty()) stopTest = true
} }
startService { error ->
_errorFlow.value = error
if (error.isNotEmpty()) stopTest = true
}
try { try {
val progress = countReachable(targets) val progress = countReachable(index, targets)
val old = strategyStates[index] val old = strategyStates[index]
strategyStates[index] = old.copy( strategyStates[index] = old.copy(
progress = progress, progress = progress,
status = R.string.strategy_status_tested status = StrategyTestingStatus.Completed
) )
} finally { } finally {
stopService {} stopService { error ->
_errorFlow.value = error
if (error.isNotEmpty()) stopTest = true
}
disableStrategy(current.path, prefs) disableStrategy(current.path, prefs)
} }
} }
@@ -140,12 +171,12 @@ class StrategySelectionViewModel(application: Application) : AndroidViewModel(ap
} ?: false } ?: false
if (connected) delay(150L) if (connected) delay(150L)
try { try {
val progress = countReachable(targets) val progress = countReachable(index,targets)
val old = strategyStates[index] val old = strategyStates[index]
strategyStates[index] = old.copy( strategyStates[index] = old.copy(
progress = progress, progress = progress,
status = R.string.strategy_status_tested status = StrategyTestingStatus.Completed
) )
} finally { } finally {
context.startService(Intent(context, ByeDpiVpnService::class.java).apply { context.startService(Intent(context, ByeDpiVpnService::class.java).apply {
@@ -162,8 +193,8 @@ class StrategySelectionViewModel(application: Application) : AndroidViewModel(ap
} }
fun checkHosts() { fun checkHosts() {
if (getActiveLists(prefs).isEmpty()) noHostsCard.value = true if (getActiveLists(prefs).isEmpty() || getAllStrategies(prefs).isEmpty()) noHostsCard.value = true
Log.d("getActiveLists.isEmpty", getActiveLists(prefs).isEmpty().toString()) Log.d("getActiveLists.isEmpty || getAllStrategies.isEmpty", getActiveLists(prefs).isEmpty().toString())
} }
fun startVpn() { fun startVpn() {
ContextCompat.startForegroundService(context, Intent(context, ByeDpiVpnService::class.java).apply { action = "START_VPN" }) ContextCompat.startForegroundService(context, Intent(context, ByeDpiVpnService::class.java).apply { action = "START_VPN" })
@@ -171,4 +202,7 @@ class StrategySelectionViewModel(application: Application) : AndroidViewModel(ap
fun clearVpnPermissionRequest() { fun clearVpnPermissionRequest() {
_requestVpnPermission.value = false _requestVpnPermission.value = false
} }
fun clearError() {
_errorFlow.value = ""
}
} }

View File

@@ -18,12 +18,12 @@ import java.io.File
class StrategyViewModel(application: Application): BaseListsViewModel(application) { class StrategyViewModel(application: Application): BaseListsViewModel(application) {
private val sharedPreferences = application.getSharedPreferences("settings", Context.MODE_PRIVATE) private val sharedPreferences = application.getSharedPreferences("settings", Context.MODE_PRIVATE)
private val useModule = sharedPreferences.getBoolean("use_module", false) private val strategyProvider: StrategyProvider
private val strategyProvider: StrategyProvider = if (useModule) { get() = if (sharedPreferences.getBoolean("use_module", false)) {
NfqwsStrategyProvider() NfqwsStrategyProvider()
} else { } else {
ByeDPIStrategyProvider(sharedPreferences) ByeDPIStrategyProvider(sharedPreferences)
} }
override fun loadAllItems(): Array<String> = strategyProvider.getAll() override fun loadAllItems(): Array<String> = strategyProvider.getAll()
override fun loadActiveItems(): Array<String> = strategyProvider.getActive() override fun loadActiveItems(): Array<String> = strategyProvider.getActive()

View File

@@ -1,18 +1,63 @@
package com.cherret.zaprett.utils package com.cherret.zaprett.utils
import android.Manifest
import android.content.Context import android.content.Context
import android.content.Context.MODE_PRIVATE import android.content.Context.MODE_PRIVATE
import android.content.SharedPreferences import android.content.SharedPreferences
import android.content.pm.PackageManager
import android.os.Build
import android.os.Environment import android.os.Environment
import android.util.Log import android.util.Log
import com.topjohnwu.superuser.Shell import androidx.core.content.ContextCompat
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 androidx.core.content.edit
import com.cherret.zaprett.data.AppListType 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) { fun checkRoot(callback: (Boolean) -> Unit) {
Shell.getShell().isRoot.let { callback(it) } Shell.getShell().isRoot.let { callback(it) }
@@ -30,75 +75,75 @@ fun getStatus(callback: (Boolean) -> Unit) {
} }
} }
fun startService(callback: (Boolean) -> Unit) { fun startService(callback: (String) -> Unit) {
Shell.cmd("zaprett start").submit { result -> Shell.cmd("zaprett start 2>&1").submit { result ->
callback(result.isSuccess) callback(
if (result.isSuccess) ""
else result.out.joinToString("\n")
)
} }
} }
fun stopService(callback: (Boolean) -> Unit) {
Shell.cmd("zaprett stop").submit { result -> fun stopService(callback: (String) -> Unit) {
callback(result.isSuccess) Shell.cmd("zaprett stop 2>&1").submit { result ->
callback(
if (result.isSuccess) ""
else result.out.joinToString("\n")
)
} }
} }
fun restartService(callback: (Boolean) -> Unit) { fun restartService(callback: (String) -> Unit) {
Shell.cmd("zaprett restart").submit { result -> Shell.cmd("zaprett restart 2>&1").submit { result ->
callback(result.isSuccess) callback(
if (result.isSuccess) ""
else result.out.joinToString("\n")
)
} }
} }
fun getModuleVersion(callback: (String) -> Unit) { fun getModuleVersion(callback: (String) -> Unit) {
Shell.cmd("zaprett module-ver").submit { result -> Shell.cmd("zaprett module-version").submit { result ->
if (result.out.isNotEmpty()) callback(result.out.first()) else "undefined" if (result.out.isNotEmpty()) callback(result.out.first()) else "undefined"
} }
} }
fun getBinVersion(callback: (String) -> Unit) { fun getBinVersion(callback: (String) -> Unit) {
Shell.cmd("zaprett bin-ver").submit { result -> Shell.cmd("zaprett binary-version").submit { result ->
if (result.out.isNotEmpty()) callback(result.out.first()) else "undefined" if (result.out.isNotEmpty()) callback(result.out.first()) else "undefined"
} }
} }
fun getConfigFile(): File { fun getConfigFile(): File {
return File(Environment.getExternalStorageDirectory(), "zaprett/config") return File(Environment.getExternalStorageDirectory(), "zaprett/config.json")
} }
fun setStartOnBoot(prefs : SharedPreferences, callback: (Boolean) -> Unit) { fun setStartOnBoot(prefs: SharedPreferences, callback: (Boolean) -> Unit) {
if (prefs.getBoolean("use_module", false)) { if (prefs.getBoolean("use_module", false)) {
Shell.cmd("zaprett autostart").submit { result -> Shell.cmd("zaprett set-autostart").submit { result ->
if (result.out.isNotEmpty() && result.out.toString().contains("true")) callback(true) else callback(false) if (result.out.isNotEmpty() && result.out.toString().contains("true")) callback(true) else callback(false)
} }
} }
} }
fun getStartOnBoot(prefs : SharedPreferences, callback: (Boolean) -> Unit) { fun getStartOnBoot(prefs: SharedPreferences, callback: (Boolean) -> Unit) {
if (prefs.getBoolean("use_module", false)) { if (prefs.getBoolean("use_module", false)) {
Shell.cmd("zaprett get-autostart").submit { result -> Shell.cmd("zaprett get-autostart").submit { result ->
if (result.out.isNotEmpty() && result.out.toString().contains("true")) callback(true) else callback(false) if (result.out.isNotEmpty() && result.out.toString().contains("true")) callback(true) else callback(false)
} }
} else { callback(false) } } else {
callback(false)
}
} }
fun getZaprettPath(): String { 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" return Environment.getExternalStorageDirectory().path + "/zaprett"
} }
fun getAllLists(): Array<String> { fun getAllLists(): Array<String> {
val listsDir = File("${getZaprettPath()}/lists/include") val listsDir = File("${getZaprettPath()}/lists/include")
return listsDir.listFiles { file -> file.isFile } return listsDir.listFiles { file -> file.isFile && file.extension.lowercase() == "txt" }
?.map { it.absolutePath } ?.map { it.absolutePath }
?.toTypedArray() ?.toTypedArray()
?: emptyArray() ?: emptyArray()
@@ -106,7 +151,7 @@ fun getAllLists(): Array<String> {
fun getAllIpsets(): Array<String> { fun getAllIpsets(): Array<String> {
val listsDir = File("${getZaprettPath()}/ipset/include") val listsDir = File("${getZaprettPath()}/ipset/include")
return listsDir.listFiles { file -> file.isFile } return listsDir.listFiles { file -> file.isFile && file.extension.lowercase() == "txt" }
?.map { it.absolutePath } ?.map { it.absolutePath }
?.toTypedArray() ?.toTypedArray()
?: emptyArray() ?: emptyArray()
@@ -114,7 +159,7 @@ fun getAllIpsets(): Array<String> {
fun getAllExcludeLists(): Array<String> { fun getAllExcludeLists(): Array<String> {
val listsDir = File("${getZaprettPath()}/lists/exclude/") val listsDir = File("${getZaprettPath()}/lists/exclude/")
return listsDir.listFiles { file -> file.isFile } return listsDir.listFiles { file -> file.isFile && file.extension.lowercase() == "txt" }
?.map { it.absolutePath } ?.map { it.absolutePath }
?.toTypedArray() ?.toTypedArray()
?: emptyArray() ?: emptyArray()
@@ -122,7 +167,7 @@ fun getAllExcludeLists(): Array<String> {
fun getAllExcludeIpsets(): Array<String> { fun getAllExcludeIpsets(): Array<String> {
val listsDir = File("${getZaprettPath()}/ipset/exclude/") val listsDir = File("${getZaprettPath()}/ipset/exclude/")
return listsDir.listFiles { file -> file.isFile } return listsDir.listFiles { file -> file.isFile && file.extension.lowercase() == "txt" }
?.map { it.absolutePath } ?.map { it.absolutePath }
?.toTypedArray() ?.toTypedArray()
?: emptyArray() ?: emptyArray()
@@ -130,7 +175,7 @@ fun getAllExcludeIpsets(): Array<String> {
fun getAllNfqwsStrategies(): Array<String> { fun getAllNfqwsStrategies(): Array<String> {
val listsDir = File("${getZaprettPath()}/strategies/nfqws") val listsDir = File("${getZaprettPath()}/strategies/nfqws")
return listsDir.listFiles { file -> file.isFile } return listsDir.listFiles { file -> file.isFile && file.extension.lowercase() == "txt" }
?.map { it.absolutePath } ?.map { it.absolutePath }
?.toTypedArray() ?.toTypedArray()
?: emptyArray() ?: emptyArray()
@@ -138,123 +183,48 @@ fun getAllNfqwsStrategies(): Array<String> {
fun getAllByeDPIStrategies(): Array<String> { fun getAllByeDPIStrategies(): Array<String> {
val listsDir = File("${getZaprettPath()}/strategies/byedpi") val listsDir = File("${getZaprettPath()}/strategies/byedpi")
return listsDir.listFiles { file -> file.isFile } return listsDir.listFiles { file -> file.isFile && file.extension.lowercase() == "txt" }
?.map { it.absolutePath } ?.map { it.absolutePath }
?.toTypedArray() ?.toTypedArray()
?: emptyArray() ?: emptyArray()
} }
fun getAllStrategies(sharedPreferences : SharedPreferences) : Array<String> { fun getAllStrategies(sharedPreferences: SharedPreferences): Array<String> {
return if (sharedPreferences.getBoolean("use_module", false)) getAllNfqwsStrategies() return if (sharedPreferences.getBoolean("use_module", false)) getAllNfqwsStrategies()
else getAllByeDPIStrategies() else getAllByeDPIStrategies()
} }
fun getActiveLists(sharedPreferences: SharedPreferences): Array<String> { fun getActiveLists(sharedPreferences: SharedPreferences): Array<String> {
if (sharedPreferences.getBoolean("use_module", false)) { if (sharedPreferences.getBoolean("use_module", false)) {
val configFile = getConfigFile() return readConfig().activeLists.toTypedArray()
if (configFile.exists()) { } else {
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() return sharedPreferences.getStringSet("lists", emptySet())?.toTypedArray() ?: emptyArray()
} }
} }
fun getActiveIpsets(sharedPreferences: SharedPreferences): Array<String> { fun getActiveIpsets(sharedPreferences: SharedPreferences): Array<String> {
if (sharedPreferences.getBoolean("use_module", false)) { if (sharedPreferences.getBoolean("use_module", false)) {
val configFile = getConfigFile() return readConfig().activeIpsets.toTypedArray()
if (configFile.exists()) { } else return sharedPreferences.getStringSet("ipsets", emptySet())?.toTypedArray() ?: emptyArray()
val props = Properties()
return try {
FileInputStream(configFile).use { input ->
props.load(input)
}
val activeLists = props.getProperty("active_ipsets", "")
Log.d("Active ipsets", activeLists)
if (activeLists.isNotEmpty()) activeLists.split(",")
.toTypedArray() else emptyArray()
} catch (e: IOException) {
throw RuntimeException(e)
}
}
return emptyArray()
}
else return sharedPreferences.getStringSet("ipsets", emptySet())?.toTypedArray() ?: emptyArray()
} }
fun getActiveExcludeLists(sharedPreferences: SharedPreferences): Array<String> { fun getActiveExcludeLists(sharedPreferences: SharedPreferences): Array<String> {
if (sharedPreferences.getBoolean("use_module", false)) { if (sharedPreferences.getBoolean("use_module", false)) {
val configFile = getConfigFile() return readConfig().activeExcludeLists.toTypedArray()
if (configFile.exists()) { } else {
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() return sharedPreferences.getStringSet("exclude_lists", emptySet())?.toTypedArray() ?: emptyArray()
} }
} }
fun getActiveExcludeIpsets(sharedPreferences: SharedPreferences): Array<String> { fun getActiveExcludeIpsets(sharedPreferences: SharedPreferences): Array<String> {
if (sharedPreferences.getBoolean("use_module", false)) { if (sharedPreferences.getBoolean("use_module", false)) {
val configFile = getConfigFile() return readConfig().activeExcludeIpsets.toTypedArray()
if (configFile.exists()) { } else return sharedPreferences.getStringSet("exclude_ipsets", emptySet())?.toTypedArray() ?: emptyArray()
val props = Properties()
return try {
FileInputStream(configFile).use { input ->
props.load(input)
}
val activeLists = props.getProperty("active_exclude_ipsets", "")
Log.d("Active ipsets", activeLists)
if (activeLists.isNotEmpty()) activeLists.split(",")
.toTypedArray() else emptyArray()
} catch (e: IOException) {
throw RuntimeException(e)
}
}
return emptyArray()
}
else return sharedPreferences.getStringSet("exclude_ipsets", emptySet())?.toTypedArray() ?: emptyArray()
} }
fun getActiveNfqwsStrategy(): Array<String> { fun getActiveNfqwsStrategy(): Array<String> {
val configFile = File("${getZaprettPath()}/config") val strategy = readConfig().strategy
if (configFile.exists()) { return if (strategy.isNotBlank()) arrayOf(strategy) else emptyArray()
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 getActiveByeDPIStrategy(sharedPreferences: SharedPreferences): Array<String> { fun getActiveByeDPIStrategy(sharedPreferences: SharedPreferences): Array<String> {
@@ -275,352 +245,173 @@ fun getActiveByeDPIStrategyContent(sharedPreferences: SharedPreferences): List<S
fun getActiveStrategy(sharedPreferences: SharedPreferences): Array<String> { fun getActiveStrategy(sharedPreferences: SharedPreferences): Array<String> {
return if (sharedPreferences.getBoolean("use_module", false)) getActiveNfqwsStrategy() return if (sharedPreferences.getBoolean("use_module", false)) getActiveNfqwsStrategy()
else getActiveByeDPIStrategy(sharedPreferences) else getActiveByeDPIStrategy(sharedPreferences)
} }
fun enableList(path: String, sharedPreferences: SharedPreferences) { fun enableList(path: String, sharedPreferences: SharedPreferences) {
if (sharedPreferences.getBoolean("use_module", false)) { if (sharedPreferences.getBoolean("use_module", false)) {
val configFile = getConfigFile() val config = readConfig()
try { val isWhitelist = getHostListMode(sharedPreferences) == "whitelist"
val props = Properties() val currentLists = if (isWhitelist) config.activeLists else config.activeExcludeLists
if (configFile.exists()) { if (path !in currentLists) {
FileInputStream(configFile).use { input -> val updatedLists = currentLists + path
props.load(input) val newConfig = if (isWhitelist) {
} config.copy(activeLists = updatedLists)
} else {
config.copy(activeExcludeLists = updatedLists)
} }
val activeLists = props.getProperty( writeConfig(newConfig)
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 {
else { val key = if (getHostListMode(sharedPreferences) == "whitelist") "lists" else "exclude_lists"
val currentSet = sharedPreferences.getStringSet( val currentSet = sharedPreferences.getStringSet(key, emptySet())?.toMutableSet() ?: mutableSetOf()
if (getHostListMode(sharedPreferences) == "whitelist") "lists"
else "exclude_lists", emptySet())?.toMutableSet() ?: mutableSetOf()
if (path !in currentSet) { if (path !in currentSet) {
currentSet.add(path) currentSet.add(path)
sharedPreferences.edit { putStringSet( sharedPreferences.edit { putStringSet(key, currentSet) }
if (getHostListMode(sharedPreferences) == "whitelist") "lists"
else "exclude_lists", currentSet) }
} }
} }
} }
fun enableIpset(path: String, sharedPreferences: SharedPreferences) { fun enableIpset(path: String, sharedPreferences: SharedPreferences) {
if (sharedPreferences.getBoolean("use_module", false)) { if (sharedPreferences.getBoolean("use_module", false)) {
val configFile = getConfigFile() val config = readConfig()
try { val isWhitelist = getHostListMode(sharedPreferences) == "whitelist"
val props = Properties() val currentIpsets = if (isWhitelist) config.activeIpsets else config.activeExcludeIpsets
if (configFile.exists()) { if (path !in currentIpsets) {
FileInputStream(configFile).use { input -> val updatedIpsets = currentIpsets + path
props.load(input) val newConfig = if (isWhitelist) {
} config.copy(activeIpsets = updatedIpsets)
} else {
config.copy(activeExcludeIpsets = updatedIpsets)
} }
val activeLists = props.getProperty( writeConfig(newConfig)
if (getHostListMode(sharedPreferences) == "whitelist") "active_ipsets"
else "active_exclude_ipsets",
"")
.split(",")
.filter { it.isNotBlank() }
.toMutableList()
if (path !in activeLists) {
activeLists.add(path)
}
props.setProperty(
if (getHostListMode(sharedPreferences) == "whitelist") "active_ipsets"
else "active_exclude_ipsets",
activeLists.joinToString(",")
)
FileOutputStream(configFile).use { output ->
props.store(output, "Don't place '/' in end of directory! Example: /sdcard")
}
} catch (e: IOException) {
throw RuntimeException(e)
} }
} } else {
else { val key = if (getHostListMode(sharedPreferences) == "whitelist") "ipsets" else "exclude_ipsets"
val currentSet = sharedPreferences.getStringSet( val currentSet = sharedPreferences.getStringSet(key, emptySet())?.toMutableSet() ?: mutableSetOf()
if (getHostListMode(sharedPreferences) == "whitelist") "ipsets"
else "exclude_ipsets", emptySet())?.toMutableSet() ?: mutableSetOf()
if (path !in currentSet) { if (path !in currentSet) {
currentSet.add(path) currentSet.add(path)
sharedPreferences.edit { putStringSet( sharedPreferences.edit { putStringSet(key, currentSet) }
if (getHostListMode(sharedPreferences) == "whitelist") "ipsets"
else "exclude_ipsets", currentSet) }
} }
} }
} }
fun enableStrategy(path: String, sharedPreferences: SharedPreferences) { fun enableStrategy(path: String, sharedPreferences: SharedPreferences) {
if (sharedPreferences.getBoolean("use_module", false)) { if (sharedPreferences.getBoolean("use_module", false)) {
val props = Properties() val config = readConfig()
val configFile = getConfigFile() if (config.strategy != path) {
try { writeConfig(config.copy(strategy = path))
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 {
else {
sharedPreferences.edit { putString("active_strategy", path) } sharedPreferences.edit { putString("active_strategy", path) }
} }
} }
fun disableList(path: String, sharedPreferences: SharedPreferences) { fun disableList(path: String, sharedPreferences: SharedPreferences) {
if (sharedPreferences.getBoolean("use_module", false)) { if (sharedPreferences.getBoolean("use_module", false)) {
val props = Properties() val config = readConfig()
val configFile = getConfigFile() val isWhitelist = getHostListMode(sharedPreferences) == "whitelist"
try { val currentLists = if (isWhitelist) config.activeLists else config.activeExcludeLists
if (configFile.exists()) { if (path in currentLists) {
FileInputStream(configFile).use { input -> val updatedLists = currentLists.filter { it != path }
props.load(input) val newConfig = if (isWhitelist) {
} config.copy(activeLists = updatedLists)
} else {
config.copy(activeExcludeLists = updatedLists)
} }
val activeLists = props.getProperty( writeConfig(newConfig)
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 {
else { val key = if (getHostListMode(sharedPreferences) == "whitelist") "lists" else "exclude_lists"
val currentSet = sharedPreferences.getStringSet( val currentSet = sharedPreferences.getStringSet(key, emptySet())?.toMutableSet() ?: mutableSetOf()
if (getHostListMode(sharedPreferences) == "whitelist") "lists"
else "exclude_lists", emptySet())?.toMutableSet() ?: mutableSetOf()
if (path in currentSet) { if (path in currentSet) {
currentSet.remove(path) currentSet.remove(path)
sharedPreferences.edit { putStringSet( sharedPreferences.edit { putStringSet(key, currentSet) }
if (getHostListMode(sharedPreferences) == "whitelist") "lists"
else "exclude_lists", currentSet) }
} }
if (currentSet.isEmpty()) { if (currentSet.isEmpty()) {
sharedPreferences.edit { remove( sharedPreferences.edit { remove(key) }
if (getHostListMode(sharedPreferences) == "whitelist") "lists"
else "exclude_lists"
) }
} }
} }
} }
fun disableIpset(path: String, sharedPreferences: SharedPreferences) { fun disableIpset(path: String, sharedPreferences: SharedPreferences) {
if (sharedPreferences.getBoolean("use_module", false)) { if (sharedPreferences.getBoolean("use_module", false)) {
val props = Properties() val config = readConfig()
val configFile = getConfigFile() val isWhitelist = getHostListMode(sharedPreferences) == "whitelist"
try { val currentIpsets = if (isWhitelist) config.activeIpsets else config.activeExcludeIpsets
if (configFile.exists()) { if (path in currentIpsets) {
FileInputStream(configFile).use { input -> val updatedIpsets = currentIpsets.filter { it != path }
props.load(input) val newConfig = if (isWhitelist) {
} config.copy(activeIpsets = updatedIpsets)
} else {
config.copy(activeExcludeIpsets = updatedIpsets)
} }
val activeLists = props.getProperty( writeConfig(newConfig)
if (getHostListMode(sharedPreferences) == "whitelist") "active_ipsets"
else "active_exclude_ipsets",
"")
.split(",")
.filter { it.isNotBlank() }
.toMutableList()
if (path in activeLists) {
activeLists.remove(path)
}
props.setProperty(
if (getHostListMode(sharedPreferences) == "whitelist") "active_ipsets"
else "active_exclude_ipsets",
activeLists.joinToString(",")
)
FileOutputStream(configFile).use { output ->
props.store(output, "Don't place '/' in end of directory! Example: /sdcard")
}
} catch (e: IOException) {
throw RuntimeException(e)
} }
} } else {
else { val key = if (getHostListMode(sharedPreferences) == "whitelist") "ipsets" else "exclude_ipsets"
val currentSet = sharedPreferences.getStringSet( val currentSet = sharedPreferences.getStringSet(key, emptySet())?.toMutableSet() ?: mutableSetOf()
if (getHostListMode(sharedPreferences) == "whitelist") "ipsets"
else "exclude_ipsets", emptySet())?.toMutableSet() ?: mutableSetOf()
if (path in currentSet) { if (path in currentSet) {
currentSet.remove(path) currentSet.remove(path)
sharedPreferences.edit { putStringSet( sharedPreferences.edit { putStringSet(key, currentSet) }
if (getHostListMode(sharedPreferences) == "whitelist") "ipsets"
else "exclude_ipsets", currentSet) }
} }
if (currentSet.isEmpty()) { if (currentSet.isEmpty()) {
sharedPreferences.edit { remove( sharedPreferences.edit { remove(key) }
if (getHostListMode(sharedPreferences) == "whitelist") "ipsets"
else "exclude_ipsets"
) }
} }
} }
} }
fun disableStrategy(path: String, sharedPreferences: SharedPreferences) { fun disableStrategy(path: String, sharedPreferences: SharedPreferences) {
if (sharedPreferences.getBoolean("use_module", false)) { if (sharedPreferences.getBoolean("use_module", false)) {
val props = Properties() val config = readConfig()
val configFile = getConfigFile() if (config.strategy == path) {
try { writeConfig(config.copy(strategy = ""))
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 {
else {
sharedPreferences.edit { remove("active_strategy") } sharedPreferences.edit { remove("active_strategy") }
} }
} }
fun addPackageToList(listType: AppListType, packageName: String, prefs : SharedPreferences, context : Context) { fun addPackageToList(listType: AppListType, packageName: String, prefs: SharedPreferences, context: Context) {
if (prefs.getBoolean("use_module", false)){ if (prefs.getBoolean("use_module", false)) {
val configFile = getConfigFile() val config = readConfig()
try { if (listType == AppListType.Whitelist) {
val props = Properties() if (packageName !in config.whitelist) {
if (configFile.exists()) { writeConfig(config.copy(whitelist = config.whitelist + packageName))
FileInputStream(configFile).use { input ->
props.load(input)
}
} }
if (listType == AppListType.Whitelist) { } else if (listType == AppListType.Blacklist) {
val whitelist = props.getProperty("whitelist", "") if (packageName !in config.blacklist) {
.split(",") writeConfig(config.copy(blacklist = config.blacklist + packageName))
.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 {
else {
val prefs = context.getSharedPreferences("settings", MODE_PRIVATE) val prefs = context.getSharedPreferences("settings", MODE_PRIVATE)
if (listType == AppListType.Whitelist){ if (listType == AppListType.Whitelist) {
val set = prefs.getStringSet("whitelist", emptySet())?.toMutableSet() ?: mutableSetOf() val set = prefs.getStringSet("whitelist", emptySet())?.toMutableSet() ?: mutableSetOf()
set.add(packageName) set.add(packageName)
prefs.edit().putStringSet("whitelist", set).apply() prefs.edit().putStringSet("whitelist", set).apply()
} }
if (listType == AppListType.Blacklist){ if (listType == AppListType.Blacklist) {
val set = prefs.getStringSet("blacklist", emptySet())?.toMutableSet() ?: mutableSetOf() val set = prefs.getStringSet("blacklist", emptySet())?.toMutableSet() ?: mutableSetOf()
set.add(packageName) set.add(packageName)
prefs.edit().putStringSet("blacklist", set).apply() prefs.edit().putStringSet("blacklist", set).apply()
} }
} }
} }
fun removePackageFromList(listType: AppListType, packageName: String, prefs: SharedPreferences, context: Context) { fun removePackageFromList(listType: AppListType, packageName: String, prefs: SharedPreferences, context: Context) {
if (prefs.getBoolean("use_module", false)){ if (prefs.getBoolean("use_module", false)) {
val props = Properties() val config = readConfig()
val configFile = getConfigFile() if (listType == AppListType.Whitelist) {
try { if (packageName in config.whitelist) {
if (configFile.exists()) { writeConfig(config.copy(whitelist = config.whitelist.filter { it != packageName }))
FileInputStream(configFile).use { input ->
props.load(input)
}
} }
if (listType == AppListType.Whitelist){ } else if (listType == AppListType.Blacklist) {
val whitelist = props.getProperty("whitelist", "") if (packageName in config.blacklist) {
.split(",") writeConfig(config.copy(blacklist = config.blacklist.filter { it != packageName }))
.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 {
else {
val prefs = context.getSharedPreferences("settings", MODE_PRIVATE) val prefs = context.getSharedPreferences("settings", MODE_PRIVATE)
if (listType == AppListType.Whitelist) { if (listType == AppListType.Whitelist) {
val set = prefs.getStringSet("whitelist", emptySet())?.toMutableSet() ?: mutableSetOf() val set = prefs.getStringSet("whitelist", emptySet())?.toMutableSet() ?: mutableSetOf()
@@ -635,175 +426,79 @@ fun removePackageFromList(listType: AppListType, packageName: String, prefs: Sha
} }
} }
fun isInList(listType: AppListType, packageName: String, prefs: SharedPreferences, context: Context) : Boolean { fun isInList(listType: AppListType, packageName: String, prefs: SharedPreferences, context: Context): Boolean {
if (prefs.getBoolean("use_module", false)) { if (prefs.getBoolean("use_module", false)) {
val configFile = getConfigFile() val config = readConfig()
if (configFile.exists()) { return if (listType == AppListType.Whitelist) {
val props = Properties() packageName in config.whitelist
try { } else {
FileInputStream(configFile).use { input -> packageName in config.blacklist
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 {
else {
val prefs = context.getSharedPreferences("settings", MODE_PRIVATE) val prefs = context.getSharedPreferences("settings", MODE_PRIVATE)
if(listType == AppListType.Whitelist){ if (listType == AppListType.Whitelist) {
val whitelist = prefs.getStringSet("whitelist", emptySet()) ?: emptySet() val whitelist = prefs.getStringSet("whitelist", emptySet()) ?: emptySet()
return packageName in whitelist return packageName in whitelist
} } else {
else {
val blacklist = prefs.getStringSet("blacklist", emptySet()) ?: emptySet() val blacklist = prefs.getStringSet("blacklist", emptySet()) ?: emptySet()
return packageName in blacklist return packageName in blacklist
} }
} }
return false
} }
fun getAppList(listType: AppListType, sharedPreferences : SharedPreferences, context : Context) : Set<String> { fun getAppList(listType: AppListType, sharedPreferences: SharedPreferences, context: Context): Set<String> {
if (sharedPreferences.getBoolean("use_module", false)) { if (sharedPreferences.getBoolean("use_module", false)) {
val configFile = File("${getZaprettPath()}/config") val config = readConfig()
if (configFile.exists()) { return if (listType == AppListType.Whitelist) {
val props = Properties() config.whitelist.toSet()
try { } else {
FileInputStream(configFile).use { input -> config.blacklist.toSet()
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 {
}
else {
return if (listType == AppListType.Whitelist) context.getSharedPreferences("settings", MODE_PRIVATE) return if (listType == AppListType.Whitelist) context.getSharedPreferences("settings", MODE_PRIVATE)
.getStringSet("whitelist", emptySet()) ?: emptySet() .getStringSet("whitelist", emptySet()) ?: emptySet()
else context.getSharedPreferences("settings", MODE_PRIVATE) else context.getSharedPreferences("settings", MODE_PRIVATE)
.getStringSet("blacklist", emptySet()) ?: emptySet() .getStringSet("blacklist", emptySet()) ?: emptySet()
} }
} }
fun getAppsListMode(prefs : SharedPreferences) : String { fun getAppsListMode(prefs: SharedPreferences): String {
if(prefs.getBoolean("use_module", false)) { if (prefs.getBoolean("use_module", false)) {
val configFile = getConfigFile() val applist = readConfig().appList
if (configFile.exists()) { Log.d("App list", "Equals to $applist")
val props = Properties() return if (applist == "whitelist" || applist == "blacklist" || applist == "none") applist
try { else "none"
FileInputStream(configFile).use { input -> } else {
props.load(input) return prefs.getString("app_list", "none")!!
}
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) { fun setAppsListMode(prefs: SharedPreferences, mode: String) {
if (prefs.getBoolean("use_module", false)) { if (prefs.getBoolean("use_module", false)) {
val configFile = getConfigFile() val config = readConfig()
val props = Properties() writeConfig(config.copy(appList = mode))
if (configFile.exists()) { } else {
try { prefs.edit { putString("app_list", mode) }
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") Log.d("App List", "Changed to $mode")
} }
fun setHostListMode(prefs: SharedPreferences, mode: String) { fun setHostListMode(prefs: SharedPreferences, mode: String) {
if (prefs.getBoolean("use_module", false)) { if (prefs.getBoolean("use_module", false)) {
val configFile = getConfigFile() val config = readConfig()
val props = Properties() writeConfig(config.copy(listType = mode))
if (configFile.exists()) { } else {
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) } prefs.edit { putString("list_type", mode) }
} }
Log.d("App List", "Changed to $mode") Log.d("App List", "Changed to $mode")
} }
fun getHostListMode(prefs : SharedPreferences) : String { fun getHostListMode(prefs: SharedPreferences): String {
if(prefs.getBoolean("use_module", false)) { if (prefs.getBoolean("use_module", false)) {
val configFile = getConfigFile() val hostlist = readConfig().listType
if (configFile.exists()) { return if (hostlist == "whitelist" || hostlist == "blacklist") hostlist
val props = Properties() else "whitelist"
try { } else {
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 prefs.getString("list_type", "whitelist")!!
} }
return "whitelist"
} }

View File

@@ -11,7 +11,7 @@
<string name="btn_continue">Продолжить</string> <string name="btn_continue">Продолжить</string>
<string name="btn_update">Обновить</string> <string name="btn_update">Обновить</string>
<string name="btn_dismiss">Отмена</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="btn_use_root">Использовать модуль</string>
<string name="error_root_title">Root не получен</string> <string name="error_root_title">Root не получен</string>
<string name="error_root_message">Не получилось получить root доступ. Дайте права root для использования модуля Magisk</string> <string name="error_root_message">Не получилось получить root доступ. Дайте права root для использования модуля Magisk</string>
@@ -24,7 +24,7 @@
<string name="status_enabled">Сервис zaprett работает. Нажми для обновления состояния</string> <string name="status_enabled">Сервис zaprett работает. Нажми для обновления состояния</string>
<string name="status_disabled">Сервис zaprett не работает. Нажми для обновления состояния</string> <string name="status_disabled">Сервис zaprett не работает. Нажми для обновления состояния</string>
<string name="status_crashed">Сервис 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_update_on_boot">Обновлять статус при запуске Главной страницы</string>
<string name="btn_start_service">Запустить сервис</string> <string name="btn_start_service">Запустить сервис</string>
<string name="btn_stop_service">Остановить сервис</string> <string name="btn_stop_service">Остановить сервис</string>
@@ -114,6 +114,10 @@
<string name="hint_enter_probe_timeout">Введите таймаут пробы</string> <string name="hint_enter_probe_timeout">Введите таймаут пробы</string>
<string name="strategy_selection_info_title">Информация</string> <string name="strategy_selection_info_title">Информация</string>
<string name="strategy_selection_info_msg">"В этом разделе настроек приложения представлен перебор стратегий\n Подбор проходит среди скачанных стратегий, поэтому заранее скачайте из репозитория или добавьте из файловой системы интересующие вас стратегии для сравнения. \n Перед началом так же выберете один или несколько листов доменов на вкладке \"Листы\", затем нажмите на \"Начать подбор\". Не используйте для перебора списки с большим количеством доменов."</string> <string name="strategy_selection_info_msg">"В этом разделе настроек приложения представлен перебор стратегий\n Подбор проходит среди скачанных стратегий, поэтому заранее скачайте из репозитория или добавьте из файловой системы интересующие вас стратегии для сравнения. \n Перед началом так же выберете один или несколько листов доменов на вкладке \"Листы\", затем нажмите на \"Начать подбор\". Не используйте для перебора списки с большим количеством доменов."</string>
<string name="selection_no_hosts_title">Нет активных листов</string> <string name="selection_no_hosts_title">Нет включенных листов/стратегий</string>
<string name="selection_no_hosts_message">Не обнаружено активных списков хостов, включите один или несколько, иначе подбор не сработает</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> </resources>

View File

@@ -11,7 +11,7 @@
<string name="btn_continue">Continue</string> <string name="btn_continue">Continue</string>
<string name="btn_update">Update</string> <string name="btn_update">Update</string>
<string name="btn_dismiss">Dismiss</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="btn_use_root">Use module</string>
<string name="error_root_title">Can\'t get root</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> <string name="error_root_message">Couldn\'t get root access. Give root access to use the Magisk module</string>
@@ -24,7 +24,7 @@
<string name="status_enabled">zaprett service is working. Tap to update</string> <string name="status_enabled">zaprett service is working. Tap to update</string>
<string name="status_disabled">zaprett service disabled.</string> <string name="status_disabled">zaprett service disabled.</string>
<string name="status_crashed">zaprett service crashed. Tap restart button below</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_update_on_boot">Update the status when the Home page is launched</string>
<string name="btn_start_service">Start service</string> <string name="btn_start_service">Start service</string>
<string name="btn_stop_service">Stop service</string> <string name="btn_stop_service">Stop service</string>
@@ -119,6 +119,10 @@
<string name="hint_enter_probe_timeout">Enter 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_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="strategy_selection_info_msg">This section of the application settings allows you to iterate through strategies.\n The selection is based on downloaded strategies, so download the strategies you\'re interested in from the repository or add them from the file system for comparison.\n Before starting, select one or more domain lists in the \"Lists\" tab, then click \"Start selection\". Avoid using lists with a large number of domains.</string>
<string name="selection_no_hosts_title">No active hosts</string> <string name="selection_no_hosts_title">No active hosts/strategies</string>
<string name="selection_no_hosts_message">No active host lists found, please enable one or more, otherwise the selection will not work</string> <string name="selection_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> </resources>

View File

@@ -1,4 +1,3 @@
Это последнее летнее обновление 😭 1. Добавлено отображение ошибок при запуске сервиса
2. Оптимизация библиотеки byedpi
1) Добавление поддержки markdown в окне обновления 3. Исправление мелких ошибок
2) Обработка ошибок загрузки и получения обновления

View File

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

View File

@@ -15,3 +15,9 @@ crate-type = ["cdylib"]
[build-dependencies] [build-dependencies]
cmake = "0.1.49" cmake = "0.1.49"
[profile.release]
opt-level = "z"
lto = true
strip = true
codegen-units = 1

View File

@@ -47,6 +47,11 @@ pub unsafe extern "system" fn Java_com_cherret_zaprett_byedpi_NativeBridge_jniSt
let mut argv: Vec<*const c_char> = cstrings.iter().map(|s| s.as_ptr()).collect(); let mut argv: Vec<*const c_char> = cstrings.iter().map(|s| s.as_ptr()).collect();
argv.push(std::ptr::null()); argv.push(std::ptr::null());
info!("starting proxy"); info!("starting proxy");
unsafe {
optind = 1;
optreset = 1;
clear_params();
}
PROXY_RUNNING.store(true, Ordering::SeqCst); PROXY_RUNNING.store(true, Ordering::SeqCst);
let ret = unsafe { main(argc as i32, argv.as_ptr()) }; let ret = unsafe { main(argc as i32, argv.as_ptr()) };
PROXY_RUNNING.store(false, Ordering::SeqCst); PROXY_RUNNING.store(false, Ordering::SeqCst);
@@ -65,10 +70,5 @@ pub unsafe extern "system" fn Java_com_cherret_zaprett_byedpi_NativeBridge_jniSt
} }
info!("stopping proxy"); info!("stopping proxy");
let ret = unsafe { shutdown(server_fd, SHUT_RDWR) }; let ret = unsafe { shutdown(server_fd, SHUT_RDWR) };
unsafe {
optreset = 1;
optind = 1;
}
unsafe { clear_params() };
ret as jint ret as jint
} }

View File

@@ -1,6 +1,6 @@
{ {
"version": "2.9", "version": "2.13",
"versionCode": 21, "versionCode": 25,
"downloadUrl": "https://github.com/CherretGit/zaprett-app/releases/download/2.9.0/app-release.apk", "downloadUrl": "https://github.com/CherretGit/zaprett-app/releases/download/2.13.0/app-release.apk",
"changelogUrl": "https://raw.githubusercontent.com/CherretGit/zaprett-app/refs/heads/main/changelog.md" "changelogUrl": "https://raw.githubusercontent.com/CherretGit/zaprett-app/refs/heads/main/changelog.md"
} }