40 Commits

Author SHA1 Message Date
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
cargo install cargo-ndk
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 x86_64-linux-android

View File

@@ -2,5 +2,26 @@
<project version="4">
<component name="AppInsightsSettings">
<option name="selectedTabId" value="Android Vitals" />
<option name="tabSettings">
<map>
<entry key="Firebase Crashlytics">
<value>
<InsightsFilterSettings>
<option name="connection">
<ConnectionSetting>
<option name="appId" value="com.cherret.zaprett" />
<option name="mobileSdkAppId" value="1:1005804036856:android:e7db5546b8bb4daf91510d" />
<option name="projectId" value="zaprett-app" />
<option name="projectNumber" value="1005804036856" />
</ConnectionSetting>
</option>
<option name="signal" value="SIGNAL_UNSPECIFIED" />
<option name="timeIntervalDays" value="THIRTY_DAYS" />
<option name="visibilityType" value="ALL" />
</InsightsFilterSettings>
</value>
</entry>
</map>
</option>
</component>
</project>

View File

@@ -1,5 +1,10 @@
# 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)
> [!IMPORTANT]
> 📢 [Официальный Telegram-канал приложения](https://t.me/zaprett_module)

View File

@@ -16,8 +16,8 @@ android {
applicationId = "com.cherret.zaprett"
minSdk = 29
targetSdk = 35
versionCode = 22
versionName = "2.10"
versionCode = 25
versionName = "2.13"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
@@ -124,4 +124,4 @@ dependencies {
androidTestImplementation(libs.androidx.ui.test.junit4)
debugImplementation(libs.androidx.ui.tooling)
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.StrategyRepoViewModel
import com.cherret.zaprett.utils.checkModuleInstallation
import com.cherret.zaprett.utils.checkStoragePermission
import com.google.firebase.Firebase
import com.google.firebase.analytics.FirebaseAnalytics
import com.google.firebase.analytics.analytics
@@ -110,16 +111,7 @@ class MainActivity : ComponentActivity() {
}
}
var showStoragePermissionDialog by remember {
mutableStateOf(
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
!Environment.isExternalStorageManager()
} else {
ContextCompat.checkSelfPermission(
this,
Manifest.permission.WRITE_EXTERNAL_STORAGE
) != PackageManager.PERMISSION_GRANTED
}
)
mutableStateOf(!checkStoragePermission(this))
}
var showNotificationPermissionDialog by remember {
mutableStateOf(
@@ -217,7 +209,7 @@ class MainActivity : ComponentActivity() {
startDestination = Screen.home.route,
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.strategies.route) { StrategyScreen(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 ->
if (getHostListMode(sharedPreferences) == "whitelist") {
when {
arg == "\$hostlist" && list.isNotEmpty() -> listOf("-H", list)
arg == "\$hostlist" && list.isNotEmpty() -> listOf("--hosts", list)
arg == "\$hostlist" && list.isEmpty() -> emptyList()
arg == "\$ipset" && list.isNotEmpty() -> listOf("-H", list)
arg == "\$ipset" && list.isNotEmpty() -> listOf("--ipset", list)
arg == "\$ipset" && list.isEmpty() -> emptyList()
else -> listOf(arg)
}
} else {
if (list.isNotEmpty()) {
listOf("-H", list, "-An", arg).filter { it != "\$hostlist" }
} else {
listOf("-An", arg).filter { it != "\$hostlist" }
}
if (ipset.isEmpty()) {
listOf("-H", list, "-An", arg).filter { it != "\$ipset" }
}
else {
listOf("-An", arg).filter { it != "\$ipset" }
when {
arg == "\$hostlist" && list.isNotEmpty() -> listOf("--hosts", list, "-An", list)
arg == "\$ipset" && ipset.isNotEmpty() -> listOf("--ipset", ipset, "-An", ipset)
arg == "\$hostlist" || arg == "\$ipset" -> emptyList()
else -> listOf(arg)
}
}
}

View File

@@ -3,5 +3,6 @@ package com.cherret.zaprett.data
data class StrategyCheckResult (
val path : String,
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.SharedPreferences
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.expandVertically
import androidx.compose.animation.shrinkVertically
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Check
import androidx.compose.material.icons.filled.Delete
import androidx.compose.material.icons.filled.InstallMobile
import androidx.compose.material.icons.filled.Update
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.ElevatedCard
import androidx.compose.material3.FilledTonalButton
@@ -26,8 +33,11 @@ import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Switch
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
@@ -36,6 +46,7 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.cherret.zaprett.R
import com.cherret.zaprett.data.StrategyCheckResult
import com.cherret.zaprett.data.StrategyTestingStatus
import com.cherret.zaprett.ui.viewmodel.BaseRepoViewModel
import com.cherret.zaprett.utils.RepoItemInfo
import com.cherret.zaprett.utils.disableStrategy
@@ -184,9 +195,15 @@ fun RepoItem(
@Composable
fun StrategySelectionItem(strategy : StrategyCheckResult, prefs : SharedPreferences, context : Context, snackbarHostState : SnackbarHostState) {
val scope = rememberCoroutineScope()
var expanded by remember { mutableStateOf(false) }
ElevatedCard (
elevation = CardDefaults.cardElevation(defaultElevation = 6.dp),
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceContainer),
onClick = {
if (strategy.status == StrategyTestingStatus.Completed && strategy.domains.isNotEmpty()) {
expanded = !expanded
}
},
modifier = Modifier
.fillMaxWidth()
.padding(start = 10.dp, end = 10.dp, top = 25.dp, bottom = 0.dp)
@@ -213,7 +230,7 @@ fun StrategySelectionItem(strategy : StrategyCheckResult, prefs : SharedPreferen
)
}
},
enabled = strategy.status == R.string.strategy_status_tested
enabled = strategy.status == StrategyTestingStatus.Completed
) {
Icon(
imageVector = Icons.Default.Check,
@@ -223,7 +240,7 @@ fun StrategySelectionItem(strategy : StrategyCheckResult, prefs : SharedPreferen
}
Row {
Text(
text = stringResource(strategy.status),
text = stringResource(strategy.status.resId),
modifier = Modifier
.weight(1f),
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(
modifier = Modifier
.fillMaxWidth()
.height(80.dp)
.clickable { onToggle(!checked) },
.height(80.dp),
elevation = CardDefaults.cardElevation(defaultElevation = 4.dp),
onClick = { onToggle(!checked) },
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceVariant)
) {
Row(
@@ -63,9 +63,9 @@ fun SettingsActionItem(title: String, onClick: () -> Unit) {
ElevatedCard(
modifier = Modifier
.fillMaxWidth()
.height(80.dp)
.clickable { onClick() },
.height(80.dp),
elevation = CardDefaults.cardElevation(defaultElevation = 4.dp),
onClick = { onClick() },
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceVariant)
) {
Row(

View File

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

View File

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

View File

@@ -1,7 +1,11 @@
package com.cherret.zaprett.ui.screen
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
import android.content.SharedPreferences
import android.os.Build
import android.widget.Toast
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
@@ -19,6 +23,7 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.Download
import androidx.compose.material.icons.filled.UploadFile
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.ExperimentalMaterial3Api
@@ -31,10 +36,13 @@ import androidx.compose.material3.SingleChoiceSegmentedButtonRow
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.pulltorefresh.PullToRefreshBox
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
@@ -67,6 +75,7 @@ fun HostsScreen(navController: NavController, viewModel: HostsViewModel = viewMo
val allLists = viewModel.allItems
val checked = viewModel.checked
val isRefreshing = viewModel.isRefreshing
val showPermissionDialog by viewModel.showNoPermissionDialog.collectAsState()
val filePickerLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.OpenDocument()
) { uri ->
@@ -74,11 +83,43 @@ fun HostsScreen(navController: NavController, viewModel: HostsViewModel = viewMo
if (getHostListMode(prefs) == "whitelist") viewModel.copySelectedFile(context, "/lists/include", it)
else viewModel.copySelectedFile(context, "/lists/exclude", it) }
}
val error by viewModel.errorFlow.collectAsState()
LaunchedEffect(Unit) {
viewModel.refresh()
}
if (error.isNotEmpty()) {
AlertDialog(
onDismissRequest = {
viewModel.clearError()
},
title = { Text(stringResource(R.string.error_text)) },
text = {
Text(stringResource(R.string.error_unknown))
},
dismissButton = {
TextButton(onClick = {
val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
val clip: ClipData = ClipData.newPlainText("Error log", error)
clipboard.setPrimaryClip(clip)
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.S) {
Toast.makeText(context, context.getString(R.string.log_copied), Toast.LENGTH_SHORT).show()
}
}) {
Text(stringResource(R.string.btn_copy_log))
}
},
confirmButton = {
TextButton(onClick = {
viewModel.clearError()
}) {
Text(stringResource(R.string.btn_continue))
}
}
)
}
Scaffold(
topBar = {
TopAppBar(
@@ -147,10 +188,26 @@ fun HostsScreen(navController: NavController, viewModel: HostsViewModel = viewMo
},
snackbarHost = { SnackbarHost(hostState = snackbarHostState) }
)
if (showPermissionDialog) {
AlertDialog(
title = { Text(text = stringResource(R.string.error_no_storage_title)) },
text = { Text(text = stringResource(R.string.no_storage_permission_message)) },
onDismissRequest = {
viewModel.hideNoPermissionDialog()
navController.popBackStack()
},
confirmButton = {
TextButton(onClick = {
viewModel.hideNoPermissionDialog()
navController.popBackStack()
}) {
Text(stringResource(R.string.btn_continue))
}
}
)
}
}
@Composable
private fun FloatingMenu(navController: NavController, launcher: ActivityResultLauncher<Array<String>>) {
var expanded by remember { mutableStateOf(false) }

View File

@@ -1,7 +1,11 @@
package com.cherret.zaprett.ui.screen
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
import android.content.SharedPreferences
import android.os.Build
import android.widget.Toast
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
@@ -19,6 +23,7 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.Download
import androidx.compose.material.icons.filled.UploadFile
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.ExperimentalMaterial3Api
@@ -31,10 +36,12 @@ import androidx.compose.material3.SingleChoiceSegmentedButtonRow
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.pulltorefresh.PullToRefreshBox
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
@@ -68,6 +75,7 @@ fun IpsetsScreen(navController: NavController, viewModel: IpsetViewModel = viewM
val allLists = viewModel.allItems
val checked = viewModel.checked
val isRefreshing = viewModel.isRefreshing
val showPermissionDialog by viewModel.showNoPermissionDialog.collectAsState()
val filePickerLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.OpenDocument()
) { uri ->
@@ -75,11 +83,43 @@ fun IpsetsScreen(navController: NavController, viewModel: IpsetViewModel = viewM
if (getHostListMode(prefs) == "whitelist") viewModel.copySelectedFile(context, "/ipset/include", it)
else viewModel.copySelectedFile(context, "/ipset/exclude", it) }
}
val error by viewModel.errorFlow.collectAsState()
LaunchedEffect(Unit) {
viewModel.refresh()
}
if (error.isNotEmpty()) {
AlertDialog(
onDismissRequest = {
viewModel.clearError()
},
title = { Text(stringResource(R.string.error_text)) },
text = {
Text(stringResource(R.string.error_unknown))
},
dismissButton = {
TextButton(onClick = {
val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
val clip: ClipData = ClipData.newPlainText("Error log", error)
clipboard.setPrimaryClip(clip)
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.S) {
Toast.makeText(context, context.getString(R.string.log_copied), Toast.LENGTH_SHORT).show()
}
}) {
Text(stringResource(R.string.btn_copy_log))
}
},
confirmButton = {
TextButton(onClick = {
viewModel.clearError()
}) {
Text(stringResource(R.string.btn_continue))
}
}
)
}
Scaffold(
topBar = {
TopAppBar(
@@ -148,10 +188,26 @@ fun IpsetsScreen(navController: NavController, viewModel: IpsetViewModel = viewM
},
snackbarHost = { SnackbarHost(hostState = snackbarHostState) }
)
if (showPermissionDialog) {
AlertDialog(
title = { Text(text = stringResource(R.string.error_no_storage_title)) },
text = { Text(text = stringResource(R.string.no_storage_permission_message)) },
onDismissRequest = {
viewModel.hideNoPermissionDialog()
navController.popBackStack()
},
confirmButton = {
TextButton(onClick = {
viewModel.hideNoPermissionDialog()
navController.popBackStack()
}) {
Text(stringResource(R.string.btn_continue))
}
}
)
}
}
@Composable
private fun FloatingMenu(navController: NavController, launcher: ActivityResultLauncher<Array<String>>) {
var expanded by remember { mutableStateOf(false) }

View File

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

View File

@@ -1,6 +1,10 @@
package com.cherret.zaprett.ui.screen
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
import android.os.Build
import android.widget.Toast
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
@@ -16,6 +20,7 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.Download
import androidx.compose.material.icons.filled.UploadFile
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.ExperimentalMaterial3Api
@@ -25,10 +30,12 @@ import androidx.compose.material3.Scaffold
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.pulltorefresh.PullToRefreshBox
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
@@ -60,6 +67,7 @@ fun StrategyScreen(navController: NavController, viewModel: StrategyViewModel =
val allLists = viewModel.allItems
val checked = viewModel.checked
val isRefreshing = viewModel.isRefreshing
val showPermissionDialog by viewModel.showNoPermissionDialog.collectAsState()
val filePickerLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.OpenDocument()
) { uri ->
@@ -69,11 +77,43 @@ fun StrategyScreen(navController: NavController, viewModel: StrategyViewModel =
it
) }
}
val error by viewModel.errorFlow.collectAsState()
LaunchedEffect(Unit) {
viewModel.refresh()
}
if (error.isNotEmpty()) {
AlertDialog(
onDismissRequest = {
viewModel.clearError()
},
title = { Text(stringResource(R.string.error_text)) },
text = {
Text(stringResource(R.string.error_unknown))
},
dismissButton = {
TextButton(onClick = {
val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
val clip: ClipData = ClipData.newPlainText("Error log", error)
clipboard.setPrimaryClip(clip)
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.S) {
Toast.makeText(context, context.getString(R.string.log_copied), Toast.LENGTH_SHORT).show()
}
}) {
Text(stringResource(R.string.btn_copy_log))
}
},
confirmButton = {
TextButton(onClick = {
viewModel.clearError()
}) {
Text(stringResource(R.string.btn_continue))
}
}
)
}
Scaffold(
topBar = {
TopAppBar(
@@ -139,6 +179,24 @@ fun StrategyScreen(navController: NavController, viewModel: StrategyViewModel =
},
snackbarHost = { SnackbarHost(hostState = snackbarHostState) }
)
if (showPermissionDialog) {
AlertDialog(
title = { Text(text = stringResource(R.string.error_no_storage_title)) },
text = { Text(text = stringResource(R.string.no_storage_permission_message)) },
onDismissRequest = {
viewModel.hideNoPermissionDialog()
navController.popBackStack()
},
confirmButton = {
TextButton(onClick = {
viewModel.hideNoPermissionDialog()
navController.popBackStack()
}) {
Text(stringResource(R.string.btn_continue))
}
}
)
}
}
@Composable

View File

@@ -1,47 +1,55 @@
package com.cherret.zaprett.ui.screen
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
import android.content.Context.MODE_PRIVATE
import android.content.Intent
import android.net.VpnService
import android.os.Build
import android.widget.Toast
import androidx.activity.result.ActivityResultLauncher
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Scaffold
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.Font
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.sp
import androidx.compose.foundation.lazy.items
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.filled.Info
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.FilledTonalButton
import androidx.compose.material3.Icon
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.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.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.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.viewmodel.compose.viewModel
import androidx.navigation.NavController
@@ -59,6 +67,7 @@ fun StrategySelectionScreen(navController: NavController, vpnLauncher: ActivityR
val prefs = context.getSharedPreferences("settings", MODE_PRIVATE)
var showDialog = remember { mutableStateOf(false) }
val requestVpnPermission by viewModel.requestVpnPermission.collectAsState()
val error by viewModel.errorFlow.collectAsState()
if (showDialog.value) {
InfoAlert { showDialog.value = false }
@@ -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(
topBar = {
TopAppBar(
title = {
Text(
text = stringResource(R.string.title_selection),
fontSize = 40.sp,
fontSize = 30.sp,
fontFamily = FontFamily(Font(R.font.unbounded, FontWeight.Normal))
)
},
@@ -112,8 +152,11 @@ fun StrategySelectionScreen(navController: NavController, vpnLauncher: ActivityR
snackbarHost = { SnackbarHost(snackbarHostState) },
content = { paddingValues ->
LazyColumn (
modifier = Modifier
.padding(paddingValues)
contentPadding = PaddingValues(
top = paddingValues.calculateTopPadding(),
bottom = paddingValues.calculateBottomPadding() + 40.dp
),
modifier = Modifier.fillMaxSize()
) {
item {
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.lifecycle.AndroidViewModel
import com.cherret.zaprett.R
import com.cherret.zaprett.utils.checkStoragePermission
import com.cherret.zaprett.utils.getZaprettPath
import com.cherret.zaprett.utils.restartService
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
import java.io.File
import java.io.FileOutputStream
@@ -34,20 +38,35 @@ abstract class BaseListsViewModel(application: Application) : AndroidViewModel(a
var isRefreshing by mutableStateOf(false)
private set
private val _errorFlow = MutableStateFlow("")
val errorFlow = _errorFlow.asStateFlow()
private var _showNoPermissionDialog = MutableStateFlow(false)
val showNoPermissionDialog: StateFlow<Boolean> = _showNoPermissionDialog
abstract fun loadAllItems(): Array<String>
abstract fun loadActiveItems(): Array<String>
abstract fun onCheckedChange(item: String, isChecked: Boolean, snackbarHostState: SnackbarHostState, scope: CoroutineScope)
abstract fun deleteItem(item: String, snackbarHostState: SnackbarHostState, scope: CoroutineScope)
fun refresh() {
isRefreshing = true
allItems = loadAllItems().toList()
activeItems = loadActiveItems().toList()
checked.clear()
allItems.forEach { list ->
checked[list] = activeItems.contains(list)
when (checkStoragePermission(context)) {
true -> {
isRefreshing = true
allItems = loadAllItems().toList()
activeItems = loadActiveItems().toList()
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) {
@@ -57,12 +76,18 @@ abstract class BaseListsViewModel(application: Application) : AndroidViewModel(a
actionLabel = context.getString(R.string.btn_restart_service)
)
if (result == SnackbarResult.ActionPerformed) {
restartService {}
restartService { error ->
_errorFlow.value = error
}
snackbarHostState.showSnackbar(context.getString(R.string.snack_reload))
}
}
}
fun clearError() {
_errorFlow.value = ""
}
fun copySelectedFile(context: Context, path: String, uri: Uri) {
//if (!Environment.isExternalStorageManager()) return
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R){

View File

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

View File

@@ -45,6 +45,9 @@ class HomeViewModel(application: Application) : AndroidViewModel(application) {
private val _serviceStatus = MutableStateFlow(ServiceStatusUI())
val serviceStatus: StateFlow<ServiceStatusUI> = _serviceStatus.asStateFlow()
private val _errorFlow = MutableStateFlow("")
val errorFlow = _errorFlow.asStateFlow()
var moduleVer = mutableStateOf(context.getString(R.string.unknown_text))
private set
@@ -103,7 +106,7 @@ class HomeViewModel(application: Application) : AndroidViewModel(application) {
}
fun checkServiceStatus() {
val updateOnBoot = prefs.getBoolean("update_on_boot", false)
val updateOnBoot = prefs.getBoolean("update_on_boot", true)
if (updateOnBoot) {
val useModule = prefs.getBoolean("use_module", false)
updateServiceStatus(useModule)
@@ -129,7 +132,10 @@ class HomeViewModel(application: Application) : AndroidViewModel(application) {
)
)
}
if (!isEnabled) startService {}
if (!isEnabled) startService { error ->
_errorFlow.value = error
onCardClick()
}
}
} else {
if (ByeDpiVpnService.status == ServiceStatus.Disconnected || ByeDpiVpnService.status == ServiceStatus.Failed) {
@@ -169,7 +175,10 @@ class HomeViewModel(application: Application) : AndroidViewModel(application) {
)
)
}
if (isEnabled) stopService {}
if (isEnabled) stopService { error ->
_errorFlow.value = error
onCardClick()
}
}
} else {
if (ByeDpiVpnService.status == ServiceStatus.Connected) {
@@ -192,7 +201,10 @@ class HomeViewModel(application: Application) : AndroidViewModel(application) {
fun onBtnRestart(snackbarHostState: SnackbarHostState, scope: CoroutineScope) {
if (prefs.getBoolean("use_module", false)) {
restartService {}
restartService { error ->
_errorFlow.value = error
onCardClick()
}
scope.launch {
snackbarHostState.showSnackbar(context.getString(R.string.snack_reload))
}
@@ -242,6 +254,7 @@ class HomeViewModel(application: Application) : AndroidViewModel(application) {
}
}
// unused?
fun parseArgs(ip: String, port: String, lines: List<String>): Array<String> {
val regex = Regex("""--?\S+(?:=(?:[^"'\s]+|"[^"]*"|'[^']*'))?|[^\s]+""")
val parsedArgs = lines
@@ -249,4 +262,7 @@ class HomeViewModel(application: Application) : AndroidViewModel(application) {
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.data.ServiceStatus
import com.cherret.zaprett.data.StrategyCheckResult
import com.cherret.zaprett.data.StrategyTestingStatus
import com.cherret.zaprett.utils.disableStrategy
import com.cherret.zaprett.utils.enableStrategy
import com.cherret.zaprett.utils.getActiveLists
@@ -26,22 +27,24 @@ import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.withContext
import kotlinx.coroutines.withTimeoutOrNull
import okhttp3.OkHttpClient
import okhttp3.Request
import java.io.File
import java.net.InetSocketAddress
import java.net.Proxy
import java.util.concurrent.TimeUnit
class StrategySelectionViewModel(application: Application) : AndroidViewModel(application) {
val prefs = application.getSharedPreferences("settings", MODE_PRIVATE)
val client = OkHttpClient.Builder()
.callTimeout(prefs.getLong("probe_timeout", 1000L), TimeUnit.MILLISECONDS)
.build()
val context = application
private val _requestVpnPermission = MutableStateFlow(false)
val requestVpnPermission = _requestVpnPermission.asStateFlow()
private val _errorFlow = MutableStateFlow("")
val errorFlow = _errorFlow.asStateFlow()
val strategyStates = mutableStateListOf<StrategyCheckResult>()
var noHostsCard = mutableStateOf(false)
private set
@@ -51,14 +54,29 @@ class StrategySelectionViewModel(application: Application) : AndroidViewModel(ap
checkHosts()
}
fun buildHttpClient(): OkHttpClient {
val builder = OkHttpClient.Builder()
.callTimeout(prefs.getLong("probe_timeout", 1000L), TimeUnit.MILLISECONDS)
.followRedirects(true)
.followSslRedirects(true)
if (!prefs.getBoolean("use_module", false)) {
val ip = prefs.getString("ip", "127.0.0.1") ?: "127.0.0.1"
val port = prefs.getString("port", "1080")?.toIntOrNull() ?: 1080
val proxy = Proxy(Proxy.Type.SOCKS, InetSocketAddress(ip, port))
builder.proxy(proxy)
}
return builder.build()
}
fun loadStrategies() {
val strategyList = getAllStrategies(prefs)
strategyStates.clear()
strategyList.forEach { name ->
strategyStates += StrategyCheckResult(
path = name,
status = R.string.strategy_status_waiting,
progress = 0f
status = StrategyTestingStatus.Waiting,
progress = 0f,
domains = emptyList()
)
}
}
@@ -68,21 +86,23 @@ class StrategySelectionViewModel(application: Application) : AndroidViewModel(ap
.url("https://${domain}")
.build()
try {
client.newCall(request).execute().use { response ->
response.isSuccessful || (response.code in 300..399)
buildHttpClient().newCall(request).execute().use { response ->
val body = response.body.byteStream().readBytes()
val contentLength = response.body.contentLength()
contentLength <= 0 || body.size.toLong() >= contentLength
}
} catch (e: Exception) {
false
}
}
suspend fun countReachable(urls: List<String>): Float = coroutineScope {
suspend fun countReachable(index: Int, urls: List<String>): Float = coroutineScope {
if (urls.isEmpty()) return@coroutineScope 0f
val results: List<Boolean> = urls.map { url ->
async { testDomain(url) }
}.awaitAll()
val successCount = results.count { it }
(successCount.toFloat() / urls.size.toFloat()).coerceIn(0f, 1f)
val results: List<String> = urls.map { url ->
async { if (testDomain(url)) url else null }
}.awaitAll().filterNotNull()
strategyStates[index].domains = results
(results.size.toFloat() / urls.size.toFloat()).coerceIn(0f, 1f)
}
suspend fun readActiveListsLines(): List<String> = withContext(Dispatchers.IO) {
@@ -102,22 +122,33 @@ class StrategySelectionViewModel(application: Application) : AndroidViewModel(ap
}
suspend fun performTest() {
val targets = readActiveListsLines()
var stopTest : Boolean = false;
for (index in strategyStates.indices) {
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)
if (prefs.getBoolean("use_module", false)) {
getStatus { if (it) stopService {} }
startService {}
getStatus { if (it) stopService { error ->
_errorFlow.value = error
if (error.isNotEmpty()) stopTest = true
} }
startService { error ->
_errorFlow.value = error
if (error.isNotEmpty()) stopTest = true
}
try {
val progress = countReachable(targets)
val progress = countReachable(index, targets)
val old = strategyStates[index]
strategyStates[index] = old.copy(
progress = progress,
status = R.string.strategy_status_tested
status = StrategyTestingStatus.Completed
)
} finally {
stopService {}
stopService { error ->
_errorFlow.value = error
if (error.isNotEmpty()) stopTest = true
}
disableStrategy(current.path, prefs)
}
}
@@ -140,12 +171,12 @@ class StrategySelectionViewModel(application: Application) : AndroidViewModel(ap
} ?: false
if (connected) delay(150L)
try {
val progress = countReachable(targets)
val progress = countReachable(index,targets)
val old = strategyStates[index]
strategyStates[index] = old.copy(
progress = progress,
status = R.string.strategy_status_tested
status = StrategyTestingStatus.Completed
)
} finally {
context.startService(Intent(context, ByeDpiVpnService::class.java).apply {
@@ -162,8 +193,8 @@ class StrategySelectionViewModel(application: Application) : AndroidViewModel(ap
}
fun checkHosts() {
if (getActiveLists(prefs).isEmpty()) noHostsCard.value = true
Log.d("getActiveLists.isEmpty", getActiveLists(prefs).isEmpty().toString())
if (getActiveLists(prefs).isEmpty() || getAllStrategies(prefs).isEmpty()) noHostsCard.value = true
Log.d("getActiveLists.isEmpty || getAllStrategies.isEmpty", getActiveLists(prefs).isEmpty().toString())
}
fun startVpn() {
ContextCompat.startForegroundService(context, Intent(context, ByeDpiVpnService::class.java).apply { action = "START_VPN" })
@@ -171,4 +202,7 @@ class StrategySelectionViewModel(application: Application) : AndroidViewModel(ap
fun clearVpnPermissionRequest() {
_requestVpnPermission.value = false
}
fun clearError() {
_errorFlow.value = ""
}
}

View File

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

View File

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

View File

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

View File

@@ -11,7 +11,7 @@
<string name="btn_continue">Continue</string>
<string name="btn_update">Update</string>
<string name="btn_dismiss">Dismiss</string>
<string name="text_welcome">Hello to zaprett! This application is designed to bypass censorship and other blockages. For full functionality you need to install Magisk module.</string>
<string name="text_welcome">Welcome to zaprett! This application is designed to bypass censorship and other blockages. For full functionality you need to install Magisk module.</string>
<string name="btn_use_root">Use module</string>
<string name="error_root_title">Can\'t get root</string>
<string name="error_root_message">Couldn\'t get root access. Give root access to use the Magisk module</string>
@@ -24,7 +24,7 @@
<string name="status_enabled">zaprett service is working. Tap to update</string>
<string name="status_disabled">zaprett service disabled.</string>
<string name="status_crashed">zaprett service crashed. Tap restart button below</string>
<string name="update_available">New update available!</string>
<string name="update_available">Update available</string>
<string name="btn_update_on_boot">Update the status when the Home page is launched</string>
<string name="btn_start_service">Start service</string>
<string name="btn_stop_service">Stop service</string>
@@ -119,6 +119,10 @@
<string name="hint_enter_probe_timeout">Enter probe timeout</string>
<string name="strategy_selection_info_title">Tip</string>
<string name="strategy_selection_info_msg">This section of the application settings allows you to iterate through strategies.\n The selection is based on downloaded strategies, so download the strategies you\'re interested in from the repository or add them from the file system for comparison.\n Before starting, select one or more domain lists in the \"Lists\" tab, then click \"Start selection\". Avoid using lists with a large number of domains.</string>
<string name="selection_no_hosts_title">No active hosts</string>
<string name="selection_no_hosts_message">No active host lists found, please enable one or more, otherwise the selection will not work</string>
<string name="selection_no_hosts_title">No active hosts/strategies</string>
<string name="selection_no_hosts_message">No active host lists or available strategies were found. Please enable one or more, or download strategies for testing, otherwise the selection will not work.</string>
<string name="selection_available_domains">Available domains</string>
<string name="no_storage_permission_message">Missing permission to access files</string>
<string name="reset_settings_title">Reset settings</string>
<string name="reset_settings_message">Are you sure you want to reset the settings?</string>
</resources>

View File

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

View File

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

View File

@@ -15,3 +15,9 @@ crate-type = ["cdylib"]
[build-dependencies]
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();
argv.push(std::ptr::null());
info!("starting proxy");
unsafe {
optind = 1;
optreset = 1;
clear_params();
}
PROXY_RUNNING.store(true, Ordering::SeqCst);
let ret = unsafe { main(argc as i32, argv.as_ptr()) };
PROXY_RUNNING.store(false, Ordering::SeqCst);
@@ -65,10 +70,5 @@ pub unsafe extern "system" fn Java_com_cherret_zaprett_byedpi_NativeBridge_jniSt
}
info!("stopping proxy");
let ret = unsafe { shutdown(server_fd, SHUT_RDWR) };
unsafe {
optreset = 1;
optind = 1;
}
unsafe { clear_params() };
ret as jint
}

View File

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