mirror of
https://github.com/CherretGit/zaprett-app.git
synced 2025-12-10 13:39:41 +05:00
Compare commits
42 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
774948ae87 | ||
|
|
297ce0cdea | ||
|
|
35b0068b8c | ||
|
|
cdcd8b85b5 | ||
|
|
2dd181494a | ||
|
|
77823252f2 | ||
|
|
87a9740aad | ||
|
|
efb60d0c93 | ||
|
|
f0c3baa3bd | ||
|
|
57217bbe1c | ||
|
|
6f9bd86d70 | ||
|
|
80f5934060 | ||
|
|
0065b8a92b | ||
|
|
b5e3d256a6 | ||
|
|
77bbd41609 | ||
|
|
4e1568b405 | ||
|
|
b8844c8cca | ||
|
|
1d6eb32473 | ||
|
|
ded89165dc | ||
|
|
ed7452b7cb | ||
|
|
717368b4e9 | ||
|
|
36885a8410 | ||
|
|
ad614b3b4a | ||
|
|
5c8425573a | ||
|
|
59e30206f7 | ||
|
|
c4dcf934aa | ||
|
|
1bbeb4e987 | ||
|
|
003a4c57a3 | ||
|
|
432873409c | ||
|
|
c5b60b1ee9 | ||
|
|
2f349e5108 | ||
|
|
e8a8fa69db | ||
|
|
c094a072b8 | ||
|
|
786321d852 | ||
|
|
7a17c93622 | ||
|
|
cf38ef7ac2 | ||
|
|
94979d2b2e | ||
|
|
bd1bdf8298 | ||
|
|
adc98db3f1 | ||
|
|
9bbc3b771f | ||
|
|
51356e63eb | ||
|
|
f3f2cf999c |
2
.github/workflows/workflow.yml
vendored
2
.github/workflows/workflow.yml
vendored
@@ -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
|
||||
|
||||
|
||||
21
.idea/appInsightsSettings.xml
generated
21
.idea/appInsightsSettings.xml
generated
@@ -2,5 +2,26 @@
|
||||
<project version="4">
|
||||
<component name="AppInsightsSettings">
|
||||
<option name="selectedTabId" value="Android Vitals" />
|
||||
<option name="tabSettings">
|
||||
<map>
|
||||
<entry key="Firebase Crashlytics">
|
||||
<value>
|
||||
<InsightsFilterSettings>
|
||||
<option name="connection">
|
||||
<ConnectionSetting>
|
||||
<option name="appId" value="com.cherret.zaprett" />
|
||||
<option name="mobileSdkAppId" value="1:1005804036856:android:e7db5546b8bb4daf91510d" />
|
||||
<option name="projectId" value="zaprett-app" />
|
||||
<option name="projectNumber" value="1005804036856" />
|
||||
</ConnectionSetting>
|
||||
</option>
|
||||
<option name="signal" value="SIGNAL_UNSPECIFIED" />
|
||||
<option name="timeIntervalDays" value="THIRTY_DAYS" />
|
||||
<option name="visibilityType" value="ALL" />
|
||||
</InsightsFilterSettings>
|
||||
</value>
|
||||
</entry>
|
||||
</map>
|
||||
</option>
|
||||
</component>
|
||||
</project>
|
||||
@@ -1,5 +1,10 @@
|
||||
# zaprett
|
||||
## О приложении
|
||||
|
||||

|
||||

|
||||

|
||||
|
||||
Приложение разработано для работы с модулем [zaprett](https://github.com/egor-white/zaprett)
|
||||
> [!IMPORTANT]
|
||||
> 📢 [Официальный Telegram-канал приложения](https://t.me/zaprett_module)
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
@@ -272,5 +264,4 @@ class MainActivity : ComponentActivity() {
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
@@ -0,0 +1,7 @@
|
||||
package com.cherret.zaprett.data
|
||||
|
||||
import com.cherret.zaprett.R
|
||||
|
||||
enum class StrategyTestingStatus(val resId: Int) {
|
||||
Waiting(R.string.strategy_status_waiting), Testing(R.string.strategy_status_testing), Completed(R.string.strategy_status_tested)
|
||||
}
|
||||
26
app/src/main/java/com/cherret/zaprett/data/ZaprettConfig.kt
Normal file
26
app/src/main/java/com/cherret/zaprett/data/ZaprettConfig.kt
Normal file
@@ -0,0 +1,26 @@
|
||||
package com.cherret.zaprett.data
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class ZaprettConfig(
|
||||
@SerialName("active_lists")
|
||||
val activeLists: List<String> = emptyList(),
|
||||
@SerialName("active_ipsets")
|
||||
val activeIpsets: List<String> = emptyList(),
|
||||
@SerialName("active_exclude_lists")
|
||||
val activeExcludeLists: List<String> = emptyList(),
|
||||
@SerialName("active_exclude_ipsets")
|
||||
val activeExcludeIpsets: List<String> = emptyList(),
|
||||
@SerialName("list_type")
|
||||
val listType: String = "whitelist",
|
||||
@SerialName("strategy")
|
||||
val strategy: String = "",
|
||||
@SerialName("app_list")
|
||||
val appList: String = "none",
|
||||
@SerialName("whitelist")
|
||||
val whitelist: List<String> = emptyList(),
|
||||
@SerialName("blacklist")
|
||||
val blacklist: List<String> = emptyList()
|
||||
)
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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(
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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(
|
||||
|
||||
@@ -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,9 +188,25 @@ 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>>) {
|
||||
|
||||
@@ -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,9 +188,25 @@ 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>>) {
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>) {
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -16,9 +16,13 @@ import androidx.compose.runtime.setValue
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.lifecycle.AndroidViewModel
|
||||
import com.cherret.zaprett.R
|
||||
import com.cherret.zaprett.utils.checkStoragePermission
|
||||
import com.cherret.zaprett.utils.getZaprettPath
|
||||
import com.cherret.zaprett.utils.restartService
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
@@ -34,12 +38,20 @@ abstract class BaseListsViewModel(application: Application) : AndroidViewModel(a
|
||||
var isRefreshing by mutableStateOf(false)
|
||||
private set
|
||||
|
||||
private val _errorFlow = MutableStateFlow("")
|
||||
val errorFlow = _errorFlow.asStateFlow()
|
||||
|
||||
private var _showNoPermissionDialog = MutableStateFlow(false)
|
||||
val showNoPermissionDialog: StateFlow<Boolean> = _showNoPermissionDialog
|
||||
|
||||
abstract fun loadAllItems(): Array<String>
|
||||
abstract fun loadActiveItems(): Array<String>
|
||||
abstract fun onCheckedChange(item: String, isChecked: Boolean, snackbarHostState: SnackbarHostState, scope: CoroutineScope)
|
||||
abstract fun deleteItem(item: String, snackbarHostState: SnackbarHostState, scope: CoroutineScope)
|
||||
|
||||
fun refresh() {
|
||||
when (checkStoragePermission(context)) {
|
||||
true -> {
|
||||
isRefreshing = true
|
||||
allItems = loadAllItems().toList()
|
||||
activeItems = loadActiveItems().toList()
|
||||
@@ -49,6 +61,13 @@ abstract class BaseListsViewModel(application: Application) : AndroidViewModel(a
|
||||
}
|
||||
isRefreshing = false
|
||||
}
|
||||
false -> _showNoPermissionDialog.value = true
|
||||
}
|
||||
}
|
||||
|
||||
fun hideNoPermissionDialog() {
|
||||
_showNoPermissionDialog.value = false
|
||||
}
|
||||
|
||||
fun showRestartSnackbar(context: Context, snackbarHostState: SnackbarHostState, scope: CoroutineScope) {
|
||||
scope.launch {
|
||||
@@ -57,12 +76,18 @@ abstract class BaseListsViewModel(application: Application) : AndroidViewModel(a
|
||||
actionLabel = context.getString(R.string.btn_restart_service)
|
||||
)
|
||||
if (result == SnackbarResult.ActionPerformed) {
|
||||
restartService {}
|
||||
restartService { error ->
|
||||
_errorFlow.value = error
|
||||
}
|
||||
snackbarHostState.showSnackbar(context.getString(R.string.snack_reload))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun clearError() {
|
||||
_errorFlow.value = ""
|
||||
}
|
||||
|
||||
fun copySelectedFile(context: Context, path: String, uri: Uri) {
|
||||
//if (!Environment.isExternalStorageManager()) return
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R){
|
||||
|
||||
@@ -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,6 +99,8 @@ abstract class BaseRepoViewModel(application: Application) : AndroidViewModel(ap
|
||||
}
|
||||
|
||||
fun install(item: RepoItemInfo) {
|
||||
when (checkStoragePermission(context)) {
|
||||
true -> {
|
||||
isInstalling[item.name] = true
|
||||
val downloadId = download(context, item.url)
|
||||
registerDownloadListener(context, downloadId, { uri ->
|
||||
@@ -122,6 +128,13 @@ abstract class BaseRepoViewModel(application: Application) : AndroidViewModel(ap
|
||||
_downloadErrorFlow.value = it
|
||||
})
|
||||
}
|
||||
false -> _showPermissionDialog.value = true
|
||||
}
|
||||
}
|
||||
|
||||
fun hideNoPermissionDialog() {
|
||||
_showPermissionDialog.value = false
|
||||
}
|
||||
|
||||
fun update(item: RepoItemInfo) {
|
||||
isUpdateInstalling[item.name] = true
|
||||
@@ -157,7 +170,6 @@ abstract class BaseRepoViewModel(application: Application) : AndroidViewModel(ap
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
fun showRestartSnackbar(snackbarHostState: SnackbarHostState) {
|
||||
viewModelScope.launch {
|
||||
val result = snackbarHostState.showSnackbar(
|
||||
|
||||
@@ -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 = ""
|
||||
}
|
||||
}
|
||||
@@ -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 = ""
|
||||
}
|
||||
}
|
||||
@@ -18,8 +18,8 @@ import java.io.File
|
||||
|
||||
class StrategyViewModel(application: Application): BaseListsViewModel(application) {
|
||||
private val sharedPreferences = application.getSharedPreferences("settings", Context.MODE_PRIVATE)
|
||||
private val useModule = sharedPreferences.getBoolean("use_module", false)
|
||||
private val strategyProvider: StrategyProvider = if (useModule) {
|
||||
private val strategyProvider: StrategyProvider
|
||||
get() = if (sharedPreferences.getBoolean("use_module", false)) {
|
||||
NfqwsStrategyProvider()
|
||||
} else {
|
||||
ByeDPIStrategyProvider(sharedPreferences)
|
||||
|
||||
@@ -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,43 +75,53 @@ 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) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -77,28 +132,18 @@ fun getStartOnBoot(prefs : SharedPreferences, callback: (Boolean) -> Unit) {
|
||||
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,7 +183,7 @@ 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()
|
||||
@@ -149,112 +194,37 @@ fun getAllStrategies(sharedPreferences : SharedPreferences) : Array<String> {
|
||||
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> {
|
||||
@@ -276,297 +246,145 @@ fun getActiveByeDPIStrategyContent(sharedPreferences: SharedPreferences): List<S
|
||||
fun getActiveStrategy(sharedPreferences: SharedPreferences): Array<String> {
|
||||
return if (sharedPreferences.getBoolean("use_module", false)) getActiveNfqwsStrategy()
|
||||
else getActiveByeDPIStrategy(sharedPreferences)
|
||||
|
||||
}
|
||||
|
||||
fun enableList(path: String, sharedPreferences: SharedPreferences) {
|
||||
if (sharedPreferences.getBoolean("use_module", false)) {
|
||||
val configFile = getConfigFile()
|
||||
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)
|
||||
}
|
||||
writeConfig(newConfig)
|
||||
}
|
||||
val activeLists = props.getProperty(
|
||||
if (getHostListMode(sharedPreferences) == "whitelist") "active_lists"
|
||||
else "active_exclude_lists",
|
||||
"")
|
||||
.split(",")
|
||||
.filter { it.isNotBlank() }
|
||||
.toMutableList()
|
||||
if (path !in activeLists) {
|
||||
activeLists.add(path)
|
||||
}
|
||||
props.setProperty(
|
||||
if (getHostListMode(sharedPreferences) == "whitelist") "active_lists"
|
||||
else "active_exclude_lists",
|
||||
activeLists.joinToString(",")
|
||||
)
|
||||
FileOutputStream(configFile).use { output ->
|
||||
props.store(output, "Don't place '/' in end of directory! Example: /sdcard")
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
throw RuntimeException(e)
|
||||
}
|
||||
}
|
||||
else {
|
||||
val currentSet = sharedPreferences.getStringSet(
|
||||
if (getHostListMode(sharedPreferences) == "whitelist") "lists"
|
||||
else "exclude_lists", emptySet())?.toMutableSet() ?: mutableSetOf()
|
||||
} 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)
|
||||
}
|
||||
writeConfig(newConfig)
|
||||
}
|
||||
val activeLists = props.getProperty(
|
||||
if (getHostListMode(sharedPreferences) == "whitelist") "active_ipsets"
|
||||
else "active_exclude_ipsets",
|
||||
"")
|
||||
.split(",")
|
||||
.filter { it.isNotBlank() }
|
||||
.toMutableList()
|
||||
if (path !in activeLists) {
|
||||
activeLists.add(path)
|
||||
}
|
||||
props.setProperty(
|
||||
if (getHostListMode(sharedPreferences) == "whitelist") "active_ipsets"
|
||||
else "active_exclude_ipsets",
|
||||
activeLists.joinToString(",")
|
||||
)
|
||||
FileOutputStream(configFile).use { output ->
|
||||
props.store(output, "Don't place '/' in end of directory! Example: /sdcard")
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
throw RuntimeException(e)
|
||||
}
|
||||
}
|
||||
else {
|
||||
val currentSet = sharedPreferences.getStringSet(
|
||||
if (getHostListMode(sharedPreferences) == "whitelist") "ipsets"
|
||||
else "exclude_ipsets", emptySet())?.toMutableSet() ?: mutableSetOf()
|
||||
} 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 config = readConfig()
|
||||
if (config.strategy != path) {
|
||||
writeConfig(config.copy(strategy = path))
|
||||
}
|
||||
}
|
||||
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) }
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
writeConfig(newConfig)
|
||||
}
|
||||
val activeLists = props.getProperty(
|
||||
if (getHostListMode(sharedPreferences) == "whitelist") "active_lists"
|
||||
else "active_exclude_lists",
|
||||
"")
|
||||
.split(",")
|
||||
.filter { it.isNotBlank() }
|
||||
.toMutableList()
|
||||
if (path in activeLists) {
|
||||
activeLists.remove(path)
|
||||
}
|
||||
props.setProperty(
|
||||
if (getHostListMode(sharedPreferences) == "whitelist") "active_lists"
|
||||
else "active_exclude_lists",
|
||||
activeLists.joinToString(",")
|
||||
)
|
||||
FileOutputStream(configFile).use { output ->
|
||||
props.store(output, "Don't place '/' in end of directory! Example: /sdcard")
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
throw RuntimeException(e)
|
||||
}
|
||||
}
|
||||
else {
|
||||
val currentSet = sharedPreferences.getStringSet(
|
||||
if (getHostListMode(sharedPreferences) == "whitelist") "lists"
|
||||
else "exclude_lists", emptySet())?.toMutableSet() ?: mutableSetOf()
|
||||
} 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)
|
||||
}
|
||||
writeConfig(newConfig)
|
||||
}
|
||||
val activeLists = props.getProperty(
|
||||
if (getHostListMode(sharedPreferences) == "whitelist") "active_ipsets"
|
||||
else "active_exclude_ipsets",
|
||||
"")
|
||||
.split(",")
|
||||
.filter { it.isNotBlank() }
|
||||
.toMutableList()
|
||||
if (path in activeLists) {
|
||||
activeLists.remove(path)
|
||||
}
|
||||
props.setProperty(
|
||||
if (getHostListMode(sharedPreferences) == "whitelist") "active_ipsets"
|
||||
else "active_exclude_ipsets",
|
||||
activeLists.joinToString(",")
|
||||
)
|
||||
FileOutputStream(configFile).use { output ->
|
||||
props.store(output, "Don't place '/' in end of directory! Example: /sdcard")
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
throw RuntimeException(e)
|
||||
}
|
||||
}
|
||||
else {
|
||||
val currentSet = sharedPreferences.getStringSet(
|
||||
if (getHostListMode(sharedPreferences) == "whitelist") "ipsets"
|
||||
else "exclude_ipsets", emptySet())?.toMutableSet() ?: mutableSetOf()
|
||||
} 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 config = readConfig()
|
||||
if (config.strategy == path) {
|
||||
writeConfig(config.copy(strategy = ""))
|
||||
}
|
||||
}
|
||||
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") }
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
val config = readConfig()
|
||||
if (listType == AppListType.Whitelist) {
|
||||
val whitelist = props.getProperty("whitelist", "")
|
||||
.split(",")
|
||||
.filter { it.isNotBlank() }
|
||||
.toMutableList()
|
||||
if (packageName !in whitelist) {
|
||||
whitelist.add(packageName)
|
||||
if (packageName !in config.whitelist) {
|
||||
writeConfig(config.copy(whitelist = config.whitelist + 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 if (listType == AppListType.Blacklist) {
|
||||
if (packageName !in config.blacklist) {
|
||||
writeConfig(config.copy(blacklist = config.blacklist + packageName))
|
||||
}
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
val prefs = context.getSharedPreferences("settings", MODE_PRIVATE)
|
||||
if (listType == AppListType.Whitelist) {
|
||||
val set = prefs.getStringSet("whitelist", emptySet())?.toMutableSet() ?: mutableSetOf()
|
||||
@@ -578,49 +396,22 @@ fun addPackageToList(listType: AppListType, packageName: String, prefs : SharedP
|
||||
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)
|
||||
}
|
||||
}
|
||||
val config = readConfig()
|
||||
if (listType == AppListType.Whitelist) {
|
||||
val whitelist = props.getProperty("whitelist", "")
|
||||
.split(",")
|
||||
.filter { it.isNotBlank() }
|
||||
.toMutableList()
|
||||
if (packageName in whitelist) {
|
||||
whitelist.remove(packageName)
|
||||
if (packageName in config.whitelist) {
|
||||
writeConfig(config.copy(whitelist = config.whitelist.filter { it != 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 if (listType == AppListType.Blacklist) {
|
||||
if (packageName in config.blacklist) {
|
||||
writeConfig(config.copy(blacklist = config.blacklist.filter { it != packageName }))
|
||||
}
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
val prefs = context.getSharedPreferences("settings", MODE_PRIVATE)
|
||||
if (listType == AppListType.Whitelist) {
|
||||
val set = prefs.getStringSet("whitelist", emptySet())?.toMutableSet() ?: mutableSetOf()
|
||||
@@ -637,68 +428,33 @@ fun removePackageFromList(listType: AppListType, packageName: String, prefs: Sha
|
||||
|
||||
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)
|
||||
val config = readConfig()
|
||||
return if (listType == AppListType.Whitelist) {
|
||||
packageName in config.whitelist
|
||||
} else {
|
||||
packageName in config.blacklist
|
||||
}
|
||||
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)
|
||||
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> {
|
||||
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)
|
||||
val config = readConfig()
|
||||
return if (listType == AppListType.Whitelist) {
|
||||
config.whitelist.toSet()
|
||||
} else {
|
||||
config.blacklist.toSet()
|
||||
}
|
||||
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)
|
||||
.getStringSet("whitelist", emptySet()) ?: emptySet()
|
||||
else context.getSharedPreferences("settings", MODE_PRIVATE)
|
||||
@@ -708,78 +464,30 @@ fun getAppList(listType: AppListType, sharedPreferences : SharedPreferences, con
|
||||
|
||||
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", "")!!
|
||||
val applist = readConfig().appList
|
||||
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("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")
|
||||
@@ -787,23 +495,10 @@ fun setHostListMode(prefs: SharedPreferences, mode: String) {
|
||||
|
||||
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")!!
|
||||
val hostlist = readConfig().listType
|
||||
return if (hostlist == "whitelist" || hostlist == "blacklist") hostlist
|
||||
else "whitelist"
|
||||
} catch (e: IOException) {
|
||||
throw RuntimeException(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
return prefs.getString("list_type", "whitelist")!!
|
||||
}
|
||||
return "whitelist"
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -1,4 +1,3 @@
|
||||
Это последнее летнее обновление 😭
|
||||
|
||||
1) Добавление поддержки markdown в окне обновления
|
||||
2) Обработка ошибок загрузки и получения обновления
|
||||
1. Добавлено отображение ошибок при запуске сервиса
|
||||
2. Оптимизация библиотеки byedpi
|
||||
3. Исправление мелких ошибок
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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.13",
|
||||
"versionCode": 25,
|
||||
"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"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user