mirror of
https://github.com/CherretGit/zaprett-app.git
synced 2025-12-10 21:49:38 +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
|
rustup update stable
|
||||||
cargo install cargo-ndk
|
cargo install cargo-ndk
|
||||||
rustup target add armv7-linux-androideabi
|
rustup target add armv7-linux-androideabi
|
||||||
rustup target install aarch64-linux-android
|
rustup target add aarch64-linux-android
|
||||||
rustup target add i686-linux-android
|
rustup target add i686-linux-android
|
||||||
rustup target add x86_64-linux-android
|
rustup target add x86_64-linux-android
|
||||||
|
|
||||||
|
|||||||
21
.idea/appInsightsSettings.xml
generated
21
.idea/appInsightsSettings.xml
generated
@@ -2,5 +2,26 @@
|
|||||||
<project version="4">
|
<project version="4">
|
||||||
<component name="AppInsightsSettings">
|
<component name="AppInsightsSettings">
|
||||||
<option name="selectedTabId" value="Android Vitals" />
|
<option name="selectedTabId" value="Android Vitals" />
|
||||||
|
<option name="tabSettings">
|
||||||
|
<map>
|
||||||
|
<entry key="Firebase Crashlytics">
|
||||||
|
<value>
|
||||||
|
<InsightsFilterSettings>
|
||||||
|
<option name="connection">
|
||||||
|
<ConnectionSetting>
|
||||||
|
<option name="appId" value="com.cherret.zaprett" />
|
||||||
|
<option name="mobileSdkAppId" value="1:1005804036856:android:e7db5546b8bb4daf91510d" />
|
||||||
|
<option name="projectId" value="zaprett-app" />
|
||||||
|
<option name="projectNumber" value="1005804036856" />
|
||||||
|
</ConnectionSetting>
|
||||||
|
</option>
|
||||||
|
<option name="signal" value="SIGNAL_UNSPECIFIED" />
|
||||||
|
<option name="timeIntervalDays" value="THIRTY_DAYS" />
|
||||||
|
<option name="visibilityType" value="ALL" />
|
||||||
|
</InsightsFilterSettings>
|
||||||
|
</value>
|
||||||
|
</entry>
|
||||||
|
</map>
|
||||||
|
</option>
|
||||||
</component>
|
</component>
|
||||||
</project>
|
</project>
|
||||||
@@ -1,5 +1,10 @@
|
|||||||
# zaprett
|
# zaprett
|
||||||
## О приложении
|
## О приложении
|
||||||
|
|
||||||
|

|
||||||
|

|
||||||
|

|
||||||
|
|
||||||
Приложение разработано для работы с модулем [zaprett](https://github.com/egor-white/zaprett)
|
Приложение разработано для работы с модулем [zaprett](https://github.com/egor-white/zaprett)
|
||||||
> [!IMPORTANT]
|
> [!IMPORTANT]
|
||||||
> 📢 [Официальный Telegram-канал приложения](https://t.me/zaprett_module)
|
> 📢 [Официальный Telegram-канал приложения](https://t.me/zaprett_module)
|
||||||
|
|||||||
@@ -16,8 +16,8 @@ android {
|
|||||||
applicationId = "com.cherret.zaprett"
|
applicationId = "com.cherret.zaprett"
|
||||||
minSdk = 29
|
minSdk = 29
|
||||||
targetSdk = 35
|
targetSdk = 35
|
||||||
versionCode = 22
|
versionCode = 25
|
||||||
versionName = "2.10"
|
versionName = "2.13"
|
||||||
|
|
||||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -61,6 +61,7 @@ import com.cherret.zaprett.ui.viewmodel.HostRepoViewModel
|
|||||||
import com.cherret.zaprett.ui.viewmodel.IpsetRepoViewModel
|
import com.cherret.zaprett.ui.viewmodel.IpsetRepoViewModel
|
||||||
import com.cherret.zaprett.ui.viewmodel.StrategyRepoViewModel
|
import com.cherret.zaprett.ui.viewmodel.StrategyRepoViewModel
|
||||||
import com.cherret.zaprett.utils.checkModuleInstallation
|
import com.cherret.zaprett.utils.checkModuleInstallation
|
||||||
|
import com.cherret.zaprett.utils.checkStoragePermission
|
||||||
import com.google.firebase.Firebase
|
import com.google.firebase.Firebase
|
||||||
import com.google.firebase.analytics.FirebaseAnalytics
|
import com.google.firebase.analytics.FirebaseAnalytics
|
||||||
import com.google.firebase.analytics.analytics
|
import com.google.firebase.analytics.analytics
|
||||||
@@ -110,16 +111,7 @@ class MainActivity : ComponentActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
var showStoragePermissionDialog by remember {
|
var showStoragePermissionDialog by remember {
|
||||||
mutableStateOf(
|
mutableStateOf(!checkStoragePermission(this))
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
|
||||||
!Environment.isExternalStorageManager()
|
|
||||||
} else {
|
|
||||||
ContextCompat.checkSelfPermission(
|
|
||||||
this,
|
|
||||||
Manifest.permission.WRITE_EXTERNAL_STORAGE
|
|
||||||
) != PackageManager.PERMISSION_GRANTED
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
var showNotificationPermissionDialog by remember {
|
var showNotificationPermissionDialog by remember {
|
||||||
mutableStateOf(
|
mutableStateOf(
|
||||||
@@ -217,7 +209,7 @@ class MainActivity : ComponentActivity() {
|
|||||||
startDestination = Screen.home.route,
|
startDestination = Screen.home.route,
|
||||||
Modifier.padding(innerPadding)
|
Modifier.padding(innerPadding)
|
||||||
) {
|
) {
|
||||||
composable(Screen.home.route) { HomeScreen(viewModel = viewModel, vpnPermissionLauncher) }
|
composable(Screen.home.route) { HomeScreen(viewModel = viewModel,vpnPermissionLauncher) }
|
||||||
composable(Screen.hosts.route) { HostsScreen(navController) }
|
composable(Screen.hosts.route) { HostsScreen(navController) }
|
||||||
composable(Screen.strategies.route) { StrategyScreen(navController) }
|
composable(Screen.strategies.route) { StrategyScreen(navController) }
|
||||||
composable(Screen.ipsets.route) { IpsetsScreen(navController) }
|
composable(Screen.ipsets.route) { IpsetsScreen(navController) }
|
||||||
@@ -272,5 +264,4 @@ class MainActivity : ComponentActivity() {
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -266,24 +266,18 @@ class ByeDpiVpnService : VpnService() {
|
|||||||
.flatMap { arg ->
|
.flatMap { arg ->
|
||||||
if (getHostListMode(sharedPreferences) == "whitelist") {
|
if (getHostListMode(sharedPreferences) == "whitelist") {
|
||||||
when {
|
when {
|
||||||
arg == "\$hostlist" && list.isNotEmpty() -> listOf("-H", list)
|
arg == "\$hostlist" && list.isNotEmpty() -> listOf("--hosts", list)
|
||||||
arg == "\$hostlist" && list.isEmpty() -> emptyList()
|
arg == "\$hostlist" && list.isEmpty() -> emptyList()
|
||||||
arg == "\$ipset" && list.isNotEmpty() -> listOf("-H", list)
|
arg == "\$ipset" && list.isNotEmpty() -> listOf("--ipset", list)
|
||||||
arg == "\$ipset" && list.isEmpty() -> emptyList()
|
arg == "\$ipset" && list.isEmpty() -> emptyList()
|
||||||
else -> listOf(arg)
|
else -> listOf(arg)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (list.isNotEmpty()) {
|
when {
|
||||||
listOf("-H", list, "-An", arg).filter { it != "\$hostlist" }
|
arg == "\$hostlist" && list.isNotEmpty() -> listOf("--hosts", list, "-An", list)
|
||||||
} else {
|
arg == "\$ipset" && ipset.isNotEmpty() -> listOf("--ipset", ipset, "-An", ipset)
|
||||||
listOf("-An", arg).filter { it != "\$hostlist" }
|
arg == "\$hostlist" || arg == "\$ipset" -> emptyList()
|
||||||
}
|
else -> listOf(arg)
|
||||||
if (ipset.isEmpty()) {
|
|
||||||
listOf("-H", list, "-An", arg).filter { it != "\$ipset" }
|
|
||||||
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
listOf("-An", arg).filter { it != "\$ipset" }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,5 +3,6 @@ package com.cherret.zaprett.data
|
|||||||
data class StrategyCheckResult (
|
data class StrategyCheckResult (
|
||||||
val path : String,
|
val path : String,
|
||||||
val progress : Float,
|
val progress : Float,
|
||||||
val status : Int
|
var domains: List<String>,
|
||||||
|
val status : StrategyTestingStatus,
|
||||||
)
|
)
|
||||||
@@ -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.Context
|
||||||
import android.content.SharedPreferences
|
import android.content.SharedPreferences
|
||||||
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
|
import androidx.compose.animation.expandVertically
|
||||||
|
import androidx.compose.animation.shrinkVertically
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.heightIn
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.lazy.items
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.Check
|
import androidx.compose.material.icons.filled.Check
|
||||||
import androidx.compose.material.icons.filled.Delete
|
import androidx.compose.material.icons.filled.Delete
|
||||||
import androidx.compose.material.icons.filled.InstallMobile
|
import androidx.compose.material.icons.filled.InstallMobile
|
||||||
import androidx.compose.material.icons.filled.Update
|
import androidx.compose.material.icons.filled.Update
|
||||||
|
import androidx.compose.material3.Card
|
||||||
import androidx.compose.material3.CardDefaults
|
import androidx.compose.material3.CardDefaults
|
||||||
import androidx.compose.material3.ElevatedCard
|
import androidx.compose.material3.ElevatedCard
|
||||||
import androidx.compose.material3.FilledTonalButton
|
import androidx.compose.material3.FilledTonalButton
|
||||||
@@ -26,8 +33,11 @@ import androidx.compose.material3.SnackbarHostState
|
|||||||
import androidx.compose.material3.Switch
|
import androidx.compose.material3.Switch
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
@@ -36,6 +46,7 @@ import androidx.compose.ui.unit.dp
|
|||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import com.cherret.zaprett.R
|
import com.cherret.zaprett.R
|
||||||
import com.cherret.zaprett.data.StrategyCheckResult
|
import com.cherret.zaprett.data.StrategyCheckResult
|
||||||
|
import com.cherret.zaprett.data.StrategyTestingStatus
|
||||||
import com.cherret.zaprett.ui.viewmodel.BaseRepoViewModel
|
import com.cherret.zaprett.ui.viewmodel.BaseRepoViewModel
|
||||||
import com.cherret.zaprett.utils.RepoItemInfo
|
import com.cherret.zaprett.utils.RepoItemInfo
|
||||||
import com.cherret.zaprett.utils.disableStrategy
|
import com.cherret.zaprett.utils.disableStrategy
|
||||||
@@ -184,9 +195,15 @@ fun RepoItem(
|
|||||||
@Composable
|
@Composable
|
||||||
fun StrategySelectionItem(strategy : StrategyCheckResult, prefs : SharedPreferences, context : Context, snackbarHostState : SnackbarHostState) {
|
fun StrategySelectionItem(strategy : StrategyCheckResult, prefs : SharedPreferences, context : Context, snackbarHostState : SnackbarHostState) {
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
|
var expanded by remember { mutableStateOf(false) }
|
||||||
ElevatedCard (
|
ElevatedCard (
|
||||||
elevation = CardDefaults.cardElevation(defaultElevation = 6.dp),
|
elevation = CardDefaults.cardElevation(defaultElevation = 6.dp),
|
||||||
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceContainer),
|
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceContainer),
|
||||||
|
onClick = {
|
||||||
|
if (strategy.status == StrategyTestingStatus.Completed && strategy.domains.isNotEmpty()) {
|
||||||
|
expanded = !expanded
|
||||||
|
}
|
||||||
|
},
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(start = 10.dp, end = 10.dp, top = 25.dp, bottom = 0.dp)
|
.padding(start = 10.dp, end = 10.dp, top = 25.dp, bottom = 0.dp)
|
||||||
@@ -213,7 +230,7 @@ fun StrategySelectionItem(strategy : StrategyCheckResult, prefs : SharedPreferen
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
enabled = strategy.status == R.string.strategy_status_tested
|
enabled = strategy.status == StrategyTestingStatus.Completed
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Default.Check,
|
imageVector = Icons.Default.Check,
|
||||||
@@ -223,7 +240,7 @@ fun StrategySelectionItem(strategy : StrategyCheckResult, prefs : SharedPreferen
|
|||||||
}
|
}
|
||||||
Row {
|
Row {
|
||||||
Text(
|
Text(
|
||||||
text = stringResource(strategy.status),
|
text = stringResource(strategy.status.resId),
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.weight(1f),
|
.weight(1f),
|
||||||
fontSize = 12.sp,
|
fontSize = 12.sp,
|
||||||
@@ -255,5 +272,37 @@ fun StrategySelectionItem(strategy : StrategyCheckResult, prefs : SharedPreferen
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
AnimatedVisibility(
|
||||||
|
visible = expanded,
|
||||||
|
enter = expandVertically(),
|
||||||
|
exit = shrinkVertically()
|
||||||
|
) {
|
||||||
|
Card (
|
||||||
|
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceContainer),
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(horizontal = 8.dp)
|
||||||
|
.fillMaxWidth()
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.selection_available_domains)
|
||||||
|
)
|
||||||
|
LazyColumn(modifier = Modifier.heightIn(max = 300.dp)) {
|
||||||
|
items(strategy.domains) { item ->
|
||||||
|
Card(
|
||||||
|
elevation = CardDefaults.cardElevation(4.dp),
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(vertical = 4.dp)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = item,
|
||||||
|
modifier = Modifier.padding(16.dp),
|
||||||
|
style = MaterialTheme.typography.bodyLarge
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -35,9 +35,9 @@ fun SettingsItem(title: String, checked: Boolean, onToggle: (Boolean) -> Unit, o
|
|||||||
ElevatedCard(
|
ElevatedCard(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.height(80.dp)
|
.height(80.dp),
|
||||||
.clickable { onToggle(!checked) },
|
|
||||||
elevation = CardDefaults.cardElevation(defaultElevation = 4.dp),
|
elevation = CardDefaults.cardElevation(defaultElevation = 4.dp),
|
||||||
|
onClick = { onToggle(!checked) },
|
||||||
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceVariant)
|
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceVariant)
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
@@ -63,9 +63,9 @@ fun SettingsActionItem(title: String, onClick: () -> Unit) {
|
|||||||
ElevatedCard(
|
ElevatedCard(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.height(80.dp)
|
.height(80.dp),
|
||||||
.clickable { onClick() },
|
|
||||||
elevation = CardDefaults.cardElevation(defaultElevation = 4.dp),
|
elevation = CardDefaults.cardElevation(defaultElevation = 4.dp),
|
||||||
|
onClick = { onClick() },
|
||||||
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceVariant)
|
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceVariant)
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package com.cherret.zaprett.ui.screen
|
package com.cherret.zaprett.ui.screen
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
@@ -11,6 +12,7 @@ import androidx.compose.foundation.lazy.items
|
|||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||||
import androidx.compose.material.icons.filled.MoreVert
|
import androidx.compose.material.icons.filled.MoreVert
|
||||||
|
import androidx.compose.material3.AlertDialog
|
||||||
import androidx.compose.material3.DropdownMenu
|
import androidx.compose.material3.DropdownMenu
|
||||||
import androidx.compose.material3.DropdownMenuItem
|
import androidx.compose.material3.DropdownMenuItem
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
@@ -18,6 +20,7 @@ import androidx.compose.material3.Icon
|
|||||||
import androidx.compose.material3.IconButton
|
import androidx.compose.material3.IconButton
|
||||||
import androidx.compose.material3.Scaffold
|
import androidx.compose.material3.Scaffold
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TextButton
|
||||||
import androidx.compose.material3.TopAppBar
|
import androidx.compose.material3.TopAppBar
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
@@ -48,6 +51,7 @@ fun DebugScreen(navController: NavController) {
|
|||||||
val editor = remember { sharedPreferences.edit() }
|
val editor = remember { sharedPreferences.edit() }
|
||||||
val showUpdateUrlDialog = remember { mutableStateOf(false) }
|
val showUpdateUrlDialog = remember { mutableStateOf(false) }
|
||||||
val textDialogValue = remember { mutableStateOf("") }
|
val textDialogValue = remember { mutableStateOf("") }
|
||||||
|
val showResetSettingsDialog = remember { mutableStateOf(false) }
|
||||||
val settingsList = listOf(
|
val settingsList = listOf(
|
||||||
Setting.Action(
|
Setting.Action(
|
||||||
title = stringResource(R.string.btn_update_repository_url),
|
title = stringResource(R.string.btn_update_repository_url),
|
||||||
@@ -56,6 +60,12 @@ fun DebugScreen(navController: NavController) {
|
|||||||
showUpdateUrlDialog.value = true
|
showUpdateUrlDialog.value = true
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
|
Setting.Action(
|
||||||
|
title = stringResource(R.string.reset_settings_title),
|
||||||
|
onClick = {
|
||||||
|
showResetSettingsDialog.value = true
|
||||||
|
}
|
||||||
|
)
|
||||||
)
|
)
|
||||||
Scaffold(
|
Scaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
@@ -115,4 +125,31 @@ fun DebugScreen(navController: NavController) {
|
|||||||
editor.putString("update_repo_url", it).apply()
|
editor.putString("update_repo_url", it).apply()
|
||||||
}, onDismiss = { showUpdateUrlDialog.value = false })
|
}, onDismiss = { showUpdateUrlDialog.value = false })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (showResetSettingsDialog.value) {
|
||||||
|
AlertDialog(title = { Text(text = stringResource(R.string.reset_settings_title)) },
|
||||||
|
text = { Text(text = stringResource(R.string.reset_settings_message)) },
|
||||||
|
onDismissRequest = {
|
||||||
|
showResetSettingsDialog.value = false
|
||||||
|
},
|
||||||
|
dismissButton = {
|
||||||
|
TextButton(onClick = {
|
||||||
|
showResetSettingsDialog.value = false
|
||||||
|
}) {
|
||||||
|
Text(stringResource(R.string.btn_dismiss))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
confirmButton = {
|
||||||
|
TextButton(onClick = {
|
||||||
|
context.deleteSharedPreferences("settings")
|
||||||
|
showResetSettingsDialog.value = false
|
||||||
|
val activity = context as Activity
|
||||||
|
val intent = activity.intent
|
||||||
|
activity.finish()
|
||||||
|
activity.startActivity(intent)
|
||||||
|
}) {
|
||||||
|
Text(text = stringResource(R.string.btn_continue))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,9 +1,13 @@
|
|||||||
package com.cherret.zaprett.ui.screen
|
package com.cherret.zaprett.ui.screen
|
||||||
|
|
||||||
|
import android.content.ClipData
|
||||||
|
import android.content.ClipboardManager
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.SharedPreferences
|
import android.content.SharedPreferences
|
||||||
import android.net.VpnService
|
import android.net.VpnService
|
||||||
|
import android.os.Build
|
||||||
|
import android.widget.Toast
|
||||||
import androidx.activity.result.ActivityResultLauncher
|
import androidx.activity.result.ActivityResultLauncher
|
||||||
import androidx.compose.animation.AnimatedVisibility
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
import androidx.compose.animation.expandVertically
|
import androidx.compose.animation.expandVertically
|
||||||
@@ -65,12 +69,15 @@ import androidx.compose.ui.text.style.TextAlign
|
|||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
|
import androidx.navigation.NavController
|
||||||
import com.cherret.zaprett.BuildConfig
|
import com.cherret.zaprett.BuildConfig
|
||||||
import com.cherret.zaprett.R
|
import com.cherret.zaprett.R
|
||||||
import com.cherret.zaprett.data.ServiceStatusUI
|
import com.cherret.zaprett.data.ServiceStatusUI
|
||||||
import com.cherret.zaprett.ui.viewmodel.HomeViewModel
|
import com.cherret.zaprett.ui.viewmodel.HomeViewModel
|
||||||
import dev.jeziellago.compose.markdowntext.MarkdownText
|
import dev.jeziellago.compose.markdowntext.MarkdownText
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.serialization.SerializationException
|
||||||
|
import java.io.IOException
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
@@ -89,6 +96,7 @@ fun HomeScreen(viewModel: HomeViewModel = viewModel(), vpnLauncher: ActivityResu
|
|||||||
val nfqwsVer = viewModel.nfqwsVer;
|
val nfqwsVer = viewModel.nfqwsVer;
|
||||||
val byedpiVer = viewModel.byedpiVer;
|
val byedpiVer = viewModel.byedpiVer;
|
||||||
val serviceMode = viewModel.serviceMode
|
val serviceMode = viewModel.serviceMode
|
||||||
|
val error by viewModel.errorFlow.collectAsState()
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
viewModel.checkForUpdate()
|
viewModel.checkForUpdate()
|
||||||
viewModel.checkServiceStatus()
|
viewModel.checkServiceStatus()
|
||||||
@@ -107,6 +115,37 @@ fun HomeScreen(viewModel: HomeViewModel = viewModel(), vpnLauncher: ActivityResu
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (error.isNotEmpty()) {
|
||||||
|
AlertDialog(
|
||||||
|
onDismissRequest = {
|
||||||
|
viewModel.clearError()
|
||||||
|
},
|
||||||
|
title = { Text(stringResource(R.string.error_text)) },
|
||||||
|
text = {
|
||||||
|
Text(stringResource(R.string.error_unknown))
|
||||||
|
},
|
||||||
|
dismissButton = {
|
||||||
|
TextButton(onClick = {
|
||||||
|
val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
||||||
|
val clip: ClipData = ClipData.newPlainText("Error log", error)
|
||||||
|
clipboard.setPrimaryClip(clip)
|
||||||
|
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.S) {
|
||||||
|
Toast.makeText(context, context.getString(R.string.log_copied), Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
}) {
|
||||||
|
Text(stringResource(R.string.btn_copy_log))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
confirmButton = {
|
||||||
|
TextButton(onClick = {
|
||||||
|
viewModel.clearError()
|
||||||
|
}) {
|
||||||
|
Text(stringResource(R.string.btn_continue))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
TopAppBar(
|
TopAppBar(
|
||||||
|
|||||||
@@ -1,7 +1,11 @@
|
|||||||
package com.cherret.zaprett.ui.screen
|
package com.cherret.zaprett.ui.screen
|
||||||
|
|
||||||
|
import android.content.ClipData
|
||||||
|
import android.content.ClipboardManager
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.SharedPreferences
|
import android.content.SharedPreferences
|
||||||
|
import android.os.Build
|
||||||
|
import android.widget.Toast
|
||||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
import androidx.activity.result.ActivityResultLauncher
|
import androidx.activity.result.ActivityResultLauncher
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
@@ -19,6 +23,7 @@ import androidx.compose.material.icons.Icons
|
|||||||
import androidx.compose.material.icons.filled.Add
|
import androidx.compose.material.icons.filled.Add
|
||||||
import androidx.compose.material.icons.filled.Download
|
import androidx.compose.material.icons.filled.Download
|
||||||
import androidx.compose.material.icons.filled.UploadFile
|
import androidx.compose.material.icons.filled.UploadFile
|
||||||
|
import androidx.compose.material3.AlertDialog
|
||||||
import androidx.compose.material3.DropdownMenu
|
import androidx.compose.material3.DropdownMenu
|
||||||
import androidx.compose.material3.DropdownMenuItem
|
import androidx.compose.material3.DropdownMenuItem
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
@@ -31,10 +36,13 @@ import androidx.compose.material3.SingleChoiceSegmentedButtonRow
|
|||||||
import androidx.compose.material3.SnackbarHost
|
import androidx.compose.material3.SnackbarHost
|
||||||
import androidx.compose.material3.SnackbarHostState
|
import androidx.compose.material3.SnackbarHostState
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TextButton
|
||||||
import androidx.compose.material3.TopAppBar
|
import androidx.compose.material3.TopAppBar
|
||||||
import androidx.compose.material3.pulltorefresh.PullToRefreshBox
|
import androidx.compose.material3.pulltorefresh.PullToRefreshBox
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.MutableState
|
||||||
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
@@ -67,6 +75,7 @@ fun HostsScreen(navController: NavController, viewModel: HostsViewModel = viewMo
|
|||||||
val allLists = viewModel.allItems
|
val allLists = viewModel.allItems
|
||||||
val checked = viewModel.checked
|
val checked = viewModel.checked
|
||||||
val isRefreshing = viewModel.isRefreshing
|
val isRefreshing = viewModel.isRefreshing
|
||||||
|
val showPermissionDialog by viewModel.showNoPermissionDialog.collectAsState()
|
||||||
val filePickerLauncher = rememberLauncherForActivityResult(
|
val filePickerLauncher = rememberLauncherForActivityResult(
|
||||||
contract = ActivityResultContracts.OpenDocument()
|
contract = ActivityResultContracts.OpenDocument()
|
||||||
) { uri ->
|
) { uri ->
|
||||||
@@ -74,11 +83,43 @@ fun HostsScreen(navController: NavController, viewModel: HostsViewModel = viewMo
|
|||||||
if (getHostListMode(prefs) == "whitelist") viewModel.copySelectedFile(context, "/lists/include", it)
|
if (getHostListMode(prefs) == "whitelist") viewModel.copySelectedFile(context, "/lists/include", it)
|
||||||
else viewModel.copySelectedFile(context, "/lists/exclude", it) }
|
else viewModel.copySelectedFile(context, "/lists/exclude", it) }
|
||||||
}
|
}
|
||||||
|
val error by viewModel.errorFlow.collectAsState()
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
viewModel.refresh()
|
viewModel.refresh()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (error.isNotEmpty()) {
|
||||||
|
AlertDialog(
|
||||||
|
onDismissRequest = {
|
||||||
|
viewModel.clearError()
|
||||||
|
},
|
||||||
|
title = { Text(stringResource(R.string.error_text)) },
|
||||||
|
text = {
|
||||||
|
Text(stringResource(R.string.error_unknown))
|
||||||
|
},
|
||||||
|
dismissButton = {
|
||||||
|
TextButton(onClick = {
|
||||||
|
val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
||||||
|
val clip: ClipData = ClipData.newPlainText("Error log", error)
|
||||||
|
clipboard.setPrimaryClip(clip)
|
||||||
|
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.S) {
|
||||||
|
Toast.makeText(context, context.getString(R.string.log_copied), Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
}) {
|
||||||
|
Text(stringResource(R.string.btn_copy_log))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
confirmButton = {
|
||||||
|
TextButton(onClick = {
|
||||||
|
viewModel.clearError()
|
||||||
|
}) {
|
||||||
|
Text(stringResource(R.string.btn_continue))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
TopAppBar(
|
TopAppBar(
|
||||||
@@ -147,10 +188,26 @@ fun HostsScreen(navController: NavController, viewModel: HostsViewModel = viewMo
|
|||||||
},
|
},
|
||||||
snackbarHost = { SnackbarHost(hostState = snackbarHostState) }
|
snackbarHost = { SnackbarHost(hostState = snackbarHostState) }
|
||||||
)
|
)
|
||||||
|
if (showPermissionDialog) {
|
||||||
|
AlertDialog(
|
||||||
|
title = { Text(text = stringResource(R.string.error_no_storage_title)) },
|
||||||
|
text = { Text(text = stringResource(R.string.no_storage_permission_message)) },
|
||||||
|
onDismissRequest = {
|
||||||
|
viewModel.hideNoPermissionDialog()
|
||||||
|
navController.popBackStack()
|
||||||
|
},
|
||||||
|
confirmButton = {
|
||||||
|
TextButton(onClick = {
|
||||||
|
viewModel.hideNoPermissionDialog()
|
||||||
|
navController.popBackStack()
|
||||||
|
}) {
|
||||||
|
Text(stringResource(R.string.btn_continue))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun FloatingMenu(navController: NavController, launcher: ActivityResultLauncher<Array<String>>) {
|
private fun FloatingMenu(navController: NavController, launcher: ActivityResultLauncher<Array<String>>) {
|
||||||
var expanded by remember { mutableStateOf(false) }
|
var expanded by remember { mutableStateOf(false) }
|
||||||
|
|||||||
@@ -1,7 +1,11 @@
|
|||||||
package com.cherret.zaprett.ui.screen
|
package com.cherret.zaprett.ui.screen
|
||||||
|
|
||||||
|
import android.content.ClipData
|
||||||
|
import android.content.ClipboardManager
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.SharedPreferences
|
import android.content.SharedPreferences
|
||||||
|
import android.os.Build
|
||||||
|
import android.widget.Toast
|
||||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
import androidx.activity.result.ActivityResultLauncher
|
import androidx.activity.result.ActivityResultLauncher
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
@@ -19,6 +23,7 @@ import androidx.compose.material.icons.Icons
|
|||||||
import androidx.compose.material.icons.filled.Add
|
import androidx.compose.material.icons.filled.Add
|
||||||
import androidx.compose.material.icons.filled.Download
|
import androidx.compose.material.icons.filled.Download
|
||||||
import androidx.compose.material.icons.filled.UploadFile
|
import androidx.compose.material.icons.filled.UploadFile
|
||||||
|
import androidx.compose.material3.AlertDialog
|
||||||
import androidx.compose.material3.DropdownMenu
|
import androidx.compose.material3.DropdownMenu
|
||||||
import androidx.compose.material3.DropdownMenuItem
|
import androidx.compose.material3.DropdownMenuItem
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
@@ -31,10 +36,12 @@ import androidx.compose.material3.SingleChoiceSegmentedButtonRow
|
|||||||
import androidx.compose.material3.SnackbarHost
|
import androidx.compose.material3.SnackbarHost
|
||||||
import androidx.compose.material3.SnackbarHostState
|
import androidx.compose.material3.SnackbarHostState
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TextButton
|
||||||
import androidx.compose.material3.TopAppBar
|
import androidx.compose.material3.TopAppBar
|
||||||
import androidx.compose.material3.pulltorefresh.PullToRefreshBox
|
import androidx.compose.material3.pulltorefresh.PullToRefreshBox
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
@@ -68,6 +75,7 @@ fun IpsetsScreen(navController: NavController, viewModel: IpsetViewModel = viewM
|
|||||||
val allLists = viewModel.allItems
|
val allLists = viewModel.allItems
|
||||||
val checked = viewModel.checked
|
val checked = viewModel.checked
|
||||||
val isRefreshing = viewModel.isRefreshing
|
val isRefreshing = viewModel.isRefreshing
|
||||||
|
val showPermissionDialog by viewModel.showNoPermissionDialog.collectAsState()
|
||||||
val filePickerLauncher = rememberLauncherForActivityResult(
|
val filePickerLauncher = rememberLauncherForActivityResult(
|
||||||
contract = ActivityResultContracts.OpenDocument()
|
contract = ActivityResultContracts.OpenDocument()
|
||||||
) { uri ->
|
) { uri ->
|
||||||
@@ -75,11 +83,43 @@ fun IpsetsScreen(navController: NavController, viewModel: IpsetViewModel = viewM
|
|||||||
if (getHostListMode(prefs) == "whitelist") viewModel.copySelectedFile(context, "/ipset/include", it)
|
if (getHostListMode(prefs) == "whitelist") viewModel.copySelectedFile(context, "/ipset/include", it)
|
||||||
else viewModel.copySelectedFile(context, "/ipset/exclude", it) }
|
else viewModel.copySelectedFile(context, "/ipset/exclude", it) }
|
||||||
}
|
}
|
||||||
|
val error by viewModel.errorFlow.collectAsState()
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
viewModel.refresh()
|
viewModel.refresh()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (error.isNotEmpty()) {
|
||||||
|
AlertDialog(
|
||||||
|
onDismissRequest = {
|
||||||
|
viewModel.clearError()
|
||||||
|
},
|
||||||
|
title = { Text(stringResource(R.string.error_text)) },
|
||||||
|
text = {
|
||||||
|
Text(stringResource(R.string.error_unknown))
|
||||||
|
},
|
||||||
|
dismissButton = {
|
||||||
|
TextButton(onClick = {
|
||||||
|
val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
||||||
|
val clip: ClipData = ClipData.newPlainText("Error log", error)
|
||||||
|
clipboard.setPrimaryClip(clip)
|
||||||
|
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.S) {
|
||||||
|
Toast.makeText(context, context.getString(R.string.log_copied), Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
}) {
|
||||||
|
Text(stringResource(R.string.btn_copy_log))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
confirmButton = {
|
||||||
|
TextButton(onClick = {
|
||||||
|
viewModel.clearError()
|
||||||
|
}) {
|
||||||
|
Text(stringResource(R.string.btn_continue))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
TopAppBar(
|
TopAppBar(
|
||||||
@@ -148,10 +188,26 @@ fun IpsetsScreen(navController: NavController, viewModel: IpsetViewModel = viewM
|
|||||||
},
|
},
|
||||||
snackbarHost = { SnackbarHost(hostState = snackbarHostState) }
|
snackbarHost = { SnackbarHost(hostState = snackbarHostState) }
|
||||||
)
|
)
|
||||||
|
if (showPermissionDialog) {
|
||||||
|
AlertDialog(
|
||||||
|
title = { Text(text = stringResource(R.string.error_no_storage_title)) },
|
||||||
|
text = { Text(text = stringResource(R.string.no_storage_permission_message)) },
|
||||||
|
onDismissRequest = {
|
||||||
|
viewModel.hideNoPermissionDialog()
|
||||||
|
navController.popBackStack()
|
||||||
|
},
|
||||||
|
confirmButton = {
|
||||||
|
TextButton(onClick = {
|
||||||
|
viewModel.hideNoPermissionDialog()
|
||||||
|
navController.popBackStack()
|
||||||
|
}) {
|
||||||
|
Text(stringResource(R.string.btn_continue))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun FloatingMenu(navController: NavController, launcher: ActivityResultLauncher<Array<String>>) {
|
private fun FloatingMenu(navController: NavController, launcher: ActivityResultLauncher<Array<String>>) {
|
||||||
var expanded by remember { mutableStateOf(false) }
|
var expanded by remember { mutableStateOf(false) }
|
||||||
|
|||||||
@@ -57,6 +57,7 @@ fun RepoScreen(navController: NavController, viewModel: BaseRepoViewModel) {
|
|||||||
val snackbarHostState = remember { SnackbarHostState() }
|
val snackbarHostState = remember { SnackbarHostState() }
|
||||||
val error by viewModel.errorFlow.collectAsState()
|
val error by viewModel.errorFlow.collectAsState()
|
||||||
val downloadError by viewModel.downloadErrorFlow.collectAsState()
|
val downloadError by viewModel.downloadErrorFlow.collectAsState()
|
||||||
|
val showPermissionDialog by viewModel.showPermissionDialog.collectAsState()
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
viewModel.refresh()
|
viewModel.refresh()
|
||||||
}
|
}
|
||||||
@@ -131,6 +132,24 @@ fun RepoScreen(navController: NavController, viewModel: BaseRepoViewModel) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (showPermissionDialog) {
|
||||||
|
AlertDialog(
|
||||||
|
title = { Text(text = stringResource(R.string.error_no_storage_title)) },
|
||||||
|
text = { Text(text = stringResource(R.string.no_storage_permission_message)) },
|
||||||
|
onDismissRequest = {
|
||||||
|
viewModel.hideNoPermissionDialog()
|
||||||
|
navController.popBackStack()
|
||||||
|
},
|
||||||
|
confirmButton = {
|
||||||
|
TextButton(onClick = {
|
||||||
|
viewModel.hideNoPermissionDialog()
|
||||||
|
navController.popBackStack()
|
||||||
|
}) {
|
||||||
|
Text(stringResource(R.string.btn_continue))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
package com.cherret.zaprett.ui.screen
|
package com.cherret.zaprett.ui.screen
|
||||||
|
|
||||||
|
import android.content.ClipData
|
||||||
|
import android.content.ClipboardManager
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.os.Build
|
||||||
|
import android.widget.Toast
|
||||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
import androidx.activity.result.ActivityResultLauncher
|
import androidx.activity.result.ActivityResultLauncher
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
@@ -16,6 +20,7 @@ import androidx.compose.material.icons.Icons
|
|||||||
import androidx.compose.material.icons.filled.Add
|
import androidx.compose.material.icons.filled.Add
|
||||||
import androidx.compose.material.icons.filled.Download
|
import androidx.compose.material.icons.filled.Download
|
||||||
import androidx.compose.material.icons.filled.UploadFile
|
import androidx.compose.material.icons.filled.UploadFile
|
||||||
|
import androidx.compose.material3.AlertDialog
|
||||||
import androidx.compose.material3.DropdownMenu
|
import androidx.compose.material3.DropdownMenu
|
||||||
import androidx.compose.material3.DropdownMenuItem
|
import androidx.compose.material3.DropdownMenuItem
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
@@ -25,10 +30,12 @@ import androidx.compose.material3.Scaffold
|
|||||||
import androidx.compose.material3.SnackbarHost
|
import androidx.compose.material3.SnackbarHost
|
||||||
import androidx.compose.material3.SnackbarHostState
|
import androidx.compose.material3.SnackbarHostState
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TextButton
|
||||||
import androidx.compose.material3.TopAppBar
|
import androidx.compose.material3.TopAppBar
|
||||||
import androidx.compose.material3.pulltorefresh.PullToRefreshBox
|
import androidx.compose.material3.pulltorefresh.PullToRefreshBox
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
@@ -60,6 +67,7 @@ fun StrategyScreen(navController: NavController, viewModel: StrategyViewModel =
|
|||||||
val allLists = viewModel.allItems
|
val allLists = viewModel.allItems
|
||||||
val checked = viewModel.checked
|
val checked = viewModel.checked
|
||||||
val isRefreshing = viewModel.isRefreshing
|
val isRefreshing = viewModel.isRefreshing
|
||||||
|
val showPermissionDialog by viewModel.showNoPermissionDialog.collectAsState()
|
||||||
val filePickerLauncher = rememberLauncherForActivityResult(
|
val filePickerLauncher = rememberLauncherForActivityResult(
|
||||||
contract = ActivityResultContracts.OpenDocument()
|
contract = ActivityResultContracts.OpenDocument()
|
||||||
) { uri ->
|
) { uri ->
|
||||||
@@ -69,11 +77,43 @@ fun StrategyScreen(navController: NavController, viewModel: StrategyViewModel =
|
|||||||
it
|
it
|
||||||
) }
|
) }
|
||||||
}
|
}
|
||||||
|
val error by viewModel.errorFlow.collectAsState()
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
viewModel.refresh()
|
viewModel.refresh()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (error.isNotEmpty()) {
|
||||||
|
AlertDialog(
|
||||||
|
onDismissRequest = {
|
||||||
|
viewModel.clearError()
|
||||||
|
},
|
||||||
|
title = { Text(stringResource(R.string.error_text)) },
|
||||||
|
text = {
|
||||||
|
Text(stringResource(R.string.error_unknown))
|
||||||
|
},
|
||||||
|
dismissButton = {
|
||||||
|
TextButton(onClick = {
|
||||||
|
val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
||||||
|
val clip: ClipData = ClipData.newPlainText("Error log", error)
|
||||||
|
clipboard.setPrimaryClip(clip)
|
||||||
|
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.S) {
|
||||||
|
Toast.makeText(context, context.getString(R.string.log_copied), Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
}) {
|
||||||
|
Text(stringResource(R.string.btn_copy_log))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
confirmButton = {
|
||||||
|
TextButton(onClick = {
|
||||||
|
viewModel.clearError()
|
||||||
|
}) {
|
||||||
|
Text(stringResource(R.string.btn_continue))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
TopAppBar(
|
TopAppBar(
|
||||||
@@ -139,6 +179,24 @@ fun StrategyScreen(navController: NavController, viewModel: StrategyViewModel =
|
|||||||
},
|
},
|
||||||
snackbarHost = { SnackbarHost(hostState = snackbarHostState) }
|
snackbarHost = { SnackbarHost(hostState = snackbarHostState) }
|
||||||
)
|
)
|
||||||
|
if (showPermissionDialog) {
|
||||||
|
AlertDialog(
|
||||||
|
title = { Text(text = stringResource(R.string.error_no_storage_title)) },
|
||||||
|
text = { Text(text = stringResource(R.string.no_storage_permission_message)) },
|
||||||
|
onDismissRequest = {
|
||||||
|
viewModel.hideNoPermissionDialog()
|
||||||
|
navController.popBackStack()
|
||||||
|
},
|
||||||
|
confirmButton = {
|
||||||
|
TextButton(onClick = {
|
||||||
|
viewModel.hideNoPermissionDialog()
|
||||||
|
navController.popBackStack()
|
||||||
|
}) {
|
||||||
|
Text(stringResource(R.string.btn_continue))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
|||||||
@@ -1,47 +1,55 @@
|
|||||||
package com.cherret.zaprett.ui.screen
|
package com.cherret.zaprett.ui.screen
|
||||||
|
|
||||||
|
import android.content.ClipData
|
||||||
|
import android.content.ClipboardManager
|
||||||
|
import android.content.Context
|
||||||
import android.content.Context.MODE_PRIVATE
|
import android.content.Context.MODE_PRIVATE
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.net.VpnService
|
import android.net.VpnService
|
||||||
|
import android.os.Build
|
||||||
|
import android.widget.Toast
|
||||||
import androidx.activity.result.ActivityResultLauncher
|
import androidx.activity.result.ActivityResultLauncher
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.WindowInsets
|
import androidx.compose.foundation.layout.WindowInsets
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.navigationBarsPadding
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
|
||||||
import androidx.compose.material3.Scaffold
|
|
||||||
import androidx.compose.material3.SnackbarHost
|
|
||||||
import androidx.compose.material3.SnackbarHostState
|
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.material3.TopAppBar
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.res.stringResource
|
|
||||||
import androidx.compose.ui.text.font.Font
|
|
||||||
import androidx.compose.ui.text.font.FontFamily
|
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
|
||||||
import androidx.compose.ui.unit.sp
|
|
||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.items
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||||
import androidx.compose.material.icons.filled.Info
|
import androidx.compose.material.icons.filled.Info
|
||||||
import androidx.compose.material3.AlertDialog
|
import androidx.compose.material3.AlertDialog
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.FilledTonalButton
|
import androidx.compose.material3.FilledTonalButton
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.IconButton
|
import androidx.compose.material3.IconButton
|
||||||
|
import androidx.compose.material3.Scaffold
|
||||||
|
import androidx.compose.material3.SnackbarHost
|
||||||
|
import androidx.compose.material3.SnackbarHostState
|
||||||
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TextButton
|
import androidx.compose.material3.TextButton
|
||||||
|
import androidx.compose.material3.TopAppBar
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.MutableState
|
import androidx.compose.runtime.MutableState
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.font.Font
|
||||||
|
import androidx.compose.ui.text.font.FontFamily
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
import androidx.navigation.NavController
|
import androidx.navigation.NavController
|
||||||
@@ -59,6 +67,7 @@ fun StrategySelectionScreen(navController: NavController, vpnLauncher: ActivityR
|
|||||||
val prefs = context.getSharedPreferences("settings", MODE_PRIVATE)
|
val prefs = context.getSharedPreferences("settings", MODE_PRIVATE)
|
||||||
var showDialog = remember { mutableStateOf(false) }
|
var showDialog = remember { mutableStateOf(false) }
|
||||||
val requestVpnPermission by viewModel.requestVpnPermission.collectAsState()
|
val requestVpnPermission by viewModel.requestVpnPermission.collectAsState()
|
||||||
|
val error by viewModel.errorFlow.collectAsState()
|
||||||
|
|
||||||
if (showDialog.value) {
|
if (showDialog.value) {
|
||||||
InfoAlert { showDialog.value = false }
|
InfoAlert { showDialog.value = false }
|
||||||
@@ -76,13 +85,44 @@ fun StrategySelectionScreen(navController: NavController, vpnLauncher: ActivityR
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (error.isNotEmpty()) {
|
||||||
|
AlertDialog(
|
||||||
|
onDismissRequest = {
|
||||||
|
viewModel.clearError()
|
||||||
|
},
|
||||||
|
title = { Text(stringResource(R.string.error_text)) },
|
||||||
|
text = {
|
||||||
|
Text(stringResource(R.string.error_unknown))
|
||||||
|
},
|
||||||
|
dismissButton = {
|
||||||
|
TextButton(onClick = {
|
||||||
|
val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
||||||
|
val clip: ClipData = ClipData.newPlainText("Error log", error)
|
||||||
|
clipboard.setPrimaryClip(clip)
|
||||||
|
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.S) {
|
||||||
|
Toast.makeText(context, context.getString(R.string.log_copied), Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
}) {
|
||||||
|
Text(stringResource(R.string.btn_copy_log))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
confirmButton = {
|
||||||
|
TextButton(onClick = {
|
||||||
|
viewModel.clearError()
|
||||||
|
}) {
|
||||||
|
Text(stringResource(R.string.btn_continue))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
TopAppBar(
|
TopAppBar(
|
||||||
title = {
|
title = {
|
||||||
Text(
|
Text(
|
||||||
text = stringResource(R.string.title_selection),
|
text = stringResource(R.string.title_selection),
|
||||||
fontSize = 40.sp,
|
fontSize = 30.sp,
|
||||||
fontFamily = FontFamily(Font(R.font.unbounded, FontWeight.Normal))
|
fontFamily = FontFamily(Font(R.font.unbounded, FontWeight.Normal))
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
@@ -112,8 +152,11 @@ fun StrategySelectionScreen(navController: NavController, vpnLauncher: ActivityR
|
|||||||
snackbarHost = { SnackbarHost(snackbarHostState) },
|
snackbarHost = { SnackbarHost(snackbarHostState) },
|
||||||
content = { paddingValues ->
|
content = { paddingValues ->
|
||||||
LazyColumn (
|
LazyColumn (
|
||||||
modifier = Modifier
|
contentPadding = PaddingValues(
|
||||||
.padding(paddingValues)
|
top = paddingValues.calculateTopPadding(),
|
||||||
|
bottom = paddingValues.calculateBottomPadding() + 40.dp
|
||||||
|
),
|
||||||
|
modifier = Modifier.fillMaxSize()
|
||||||
) {
|
) {
|
||||||
item {
|
item {
|
||||||
Row (
|
Row (
|
||||||
@@ -193,4 +236,3 @@ private fun NoHostsCard(noHostsCard: MutableState<Boolean>) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -16,9 +16,13 @@ import androidx.compose.runtime.setValue
|
|||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.lifecycle.AndroidViewModel
|
import androidx.lifecycle.AndroidViewModel
|
||||||
import com.cherret.zaprett.R
|
import com.cherret.zaprett.R
|
||||||
|
import com.cherret.zaprett.utils.checkStoragePermission
|
||||||
import com.cherret.zaprett.utils.getZaprettPath
|
import com.cherret.zaprett.utils.getZaprettPath
|
||||||
import com.cherret.zaprett.utils.restartService
|
import com.cherret.zaprett.utils.restartService
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.FileOutputStream
|
import java.io.FileOutputStream
|
||||||
@@ -34,12 +38,20 @@ abstract class BaseListsViewModel(application: Application) : AndroidViewModel(a
|
|||||||
var isRefreshing by mutableStateOf(false)
|
var isRefreshing by mutableStateOf(false)
|
||||||
private set
|
private set
|
||||||
|
|
||||||
|
private val _errorFlow = MutableStateFlow("")
|
||||||
|
val errorFlow = _errorFlow.asStateFlow()
|
||||||
|
|
||||||
|
private var _showNoPermissionDialog = MutableStateFlow(false)
|
||||||
|
val showNoPermissionDialog: StateFlow<Boolean> = _showNoPermissionDialog
|
||||||
|
|
||||||
abstract fun loadAllItems(): Array<String>
|
abstract fun loadAllItems(): Array<String>
|
||||||
abstract fun loadActiveItems(): Array<String>
|
abstract fun loadActiveItems(): Array<String>
|
||||||
abstract fun onCheckedChange(item: String, isChecked: Boolean, snackbarHostState: SnackbarHostState, scope: CoroutineScope)
|
abstract fun onCheckedChange(item: String, isChecked: Boolean, snackbarHostState: SnackbarHostState, scope: CoroutineScope)
|
||||||
abstract fun deleteItem(item: String, snackbarHostState: SnackbarHostState, scope: CoroutineScope)
|
abstract fun deleteItem(item: String, snackbarHostState: SnackbarHostState, scope: CoroutineScope)
|
||||||
|
|
||||||
fun refresh() {
|
fun refresh() {
|
||||||
|
when (checkStoragePermission(context)) {
|
||||||
|
true -> {
|
||||||
isRefreshing = true
|
isRefreshing = true
|
||||||
allItems = loadAllItems().toList()
|
allItems = loadAllItems().toList()
|
||||||
activeItems = loadActiveItems().toList()
|
activeItems = loadActiveItems().toList()
|
||||||
@@ -49,6 +61,13 @@ abstract class BaseListsViewModel(application: Application) : AndroidViewModel(a
|
|||||||
}
|
}
|
||||||
isRefreshing = false
|
isRefreshing = false
|
||||||
}
|
}
|
||||||
|
false -> _showNoPermissionDialog.value = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun hideNoPermissionDialog() {
|
||||||
|
_showNoPermissionDialog.value = false
|
||||||
|
}
|
||||||
|
|
||||||
fun showRestartSnackbar(context: Context, snackbarHostState: SnackbarHostState, scope: CoroutineScope) {
|
fun showRestartSnackbar(context: Context, snackbarHostState: SnackbarHostState, scope: CoroutineScope) {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
@@ -57,12 +76,18 @@ abstract class BaseListsViewModel(application: Application) : AndroidViewModel(a
|
|||||||
actionLabel = context.getString(R.string.btn_restart_service)
|
actionLabel = context.getString(R.string.btn_restart_service)
|
||||||
)
|
)
|
||||||
if (result == SnackbarResult.ActionPerformed) {
|
if (result == SnackbarResult.ActionPerformed) {
|
||||||
restartService {}
|
restartService { error ->
|
||||||
|
_errorFlow.value = error
|
||||||
|
}
|
||||||
snackbarHostState.showSnackbar(context.getString(R.string.snack_reload))
|
snackbarHostState.showSnackbar(context.getString(R.string.snack_reload))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun clearError() {
|
||||||
|
_errorFlow.value = ""
|
||||||
|
}
|
||||||
|
|
||||||
fun copySelectedFile(context: Context, path: String, uri: Uri) {
|
fun copySelectedFile(context: Context, path: String, uri: Uri) {
|
||||||
//if (!Environment.isExternalStorageManager()) return
|
//if (!Environment.isExternalStorageManager()) return
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R){
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R){
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import androidx.lifecycle.viewModelScope
|
|||||||
import com.cherret.zaprett.utils.RepoItemInfo
|
import com.cherret.zaprett.utils.RepoItemInfo
|
||||||
import com.cherret.zaprett.R
|
import com.cherret.zaprett.R
|
||||||
import com.cherret.zaprett.data.ItemType
|
import com.cherret.zaprett.data.ItemType
|
||||||
|
import com.cherret.zaprett.utils.checkStoragePermission
|
||||||
import com.cherret.zaprett.utils.download
|
import com.cherret.zaprett.utils.download
|
||||||
import com.cherret.zaprett.utils.getFileSha256
|
import com.cherret.zaprett.utils.getFileSha256
|
||||||
import com.cherret.zaprett.utils.getHostListMode
|
import com.cherret.zaprett.utils.getHostListMode
|
||||||
@@ -36,6 +37,9 @@ abstract class BaseRepoViewModel(application: Application) : AndroidViewModel(ap
|
|||||||
|
|
||||||
val downloadErrorFlow: StateFlow<String?> = _downloadErrorFlow
|
val downloadErrorFlow: StateFlow<String?> = _downloadErrorFlow
|
||||||
|
|
||||||
|
private var _showPermissionDialog = MutableStateFlow(false)
|
||||||
|
val showPermissionDialog: StateFlow<Boolean> = _showPermissionDialog
|
||||||
|
|
||||||
var hostLists = mutableStateOf<List<RepoItemInfo>>(emptyList())
|
var hostLists = mutableStateOf<List<RepoItemInfo>>(emptyList())
|
||||||
protected set
|
protected set
|
||||||
|
|
||||||
@@ -95,6 +99,8 @@ abstract class BaseRepoViewModel(application: Application) : AndroidViewModel(ap
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun install(item: RepoItemInfo) {
|
fun install(item: RepoItemInfo) {
|
||||||
|
when (checkStoragePermission(context)) {
|
||||||
|
true -> {
|
||||||
isInstalling[item.name] = true
|
isInstalling[item.name] = true
|
||||||
val downloadId = download(context, item.url)
|
val downloadId = download(context, item.url)
|
||||||
registerDownloadListener(context, downloadId, { uri ->
|
registerDownloadListener(context, downloadId, { uri ->
|
||||||
@@ -122,6 +128,13 @@ abstract class BaseRepoViewModel(application: Application) : AndroidViewModel(ap
|
|||||||
_downloadErrorFlow.value = it
|
_downloadErrorFlow.value = it
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
false -> _showPermissionDialog.value = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun hideNoPermissionDialog() {
|
||||||
|
_showPermissionDialog.value = false
|
||||||
|
}
|
||||||
|
|
||||||
fun update(item: RepoItemInfo) {
|
fun update(item: RepoItemInfo) {
|
||||||
isUpdateInstalling[item.name] = true
|
isUpdateInstalling[item.name] = true
|
||||||
@@ -157,7 +170,6 @@ abstract class BaseRepoViewModel(application: Application) : AndroidViewModel(ap
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
fun showRestartSnackbar(snackbarHostState: SnackbarHostState) {
|
fun showRestartSnackbar(snackbarHostState: SnackbarHostState) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
val result = snackbarHostState.showSnackbar(
|
val result = snackbarHostState.showSnackbar(
|
||||||
|
|||||||
@@ -45,6 +45,9 @@ class HomeViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
private val _serviceStatus = MutableStateFlow(ServiceStatusUI())
|
private val _serviceStatus = MutableStateFlow(ServiceStatusUI())
|
||||||
val serviceStatus: StateFlow<ServiceStatusUI> = _serviceStatus.asStateFlow()
|
val serviceStatus: StateFlow<ServiceStatusUI> = _serviceStatus.asStateFlow()
|
||||||
|
|
||||||
|
private val _errorFlow = MutableStateFlow("")
|
||||||
|
val errorFlow = _errorFlow.asStateFlow()
|
||||||
|
|
||||||
var moduleVer = mutableStateOf(context.getString(R.string.unknown_text))
|
var moduleVer = mutableStateOf(context.getString(R.string.unknown_text))
|
||||||
private set
|
private set
|
||||||
|
|
||||||
@@ -103,7 +106,7 @@ class HomeViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun checkServiceStatus() {
|
fun checkServiceStatus() {
|
||||||
val updateOnBoot = prefs.getBoolean("update_on_boot", false)
|
val updateOnBoot = prefs.getBoolean("update_on_boot", true)
|
||||||
if (updateOnBoot) {
|
if (updateOnBoot) {
|
||||||
val useModule = prefs.getBoolean("use_module", false)
|
val useModule = prefs.getBoolean("use_module", false)
|
||||||
updateServiceStatus(useModule)
|
updateServiceStatus(useModule)
|
||||||
@@ -129,7 +132,10 @@ class HomeViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (!isEnabled) startService {}
|
if (!isEnabled) startService { error ->
|
||||||
|
_errorFlow.value = error
|
||||||
|
onCardClick()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (ByeDpiVpnService.status == ServiceStatus.Disconnected || ByeDpiVpnService.status == ServiceStatus.Failed) {
|
if (ByeDpiVpnService.status == ServiceStatus.Disconnected || ByeDpiVpnService.status == ServiceStatus.Failed) {
|
||||||
@@ -169,7 +175,10 @@ class HomeViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (isEnabled) stopService {}
|
if (isEnabled) stopService { error ->
|
||||||
|
_errorFlow.value = error
|
||||||
|
onCardClick()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (ByeDpiVpnService.status == ServiceStatus.Connected) {
|
if (ByeDpiVpnService.status == ServiceStatus.Connected) {
|
||||||
@@ -192,7 +201,10 @@ class HomeViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
|
|
||||||
fun onBtnRestart(snackbarHostState: SnackbarHostState, scope: CoroutineScope) {
|
fun onBtnRestart(snackbarHostState: SnackbarHostState, scope: CoroutineScope) {
|
||||||
if (prefs.getBoolean("use_module", false)) {
|
if (prefs.getBoolean("use_module", false)) {
|
||||||
restartService {}
|
restartService { error ->
|
||||||
|
_errorFlow.value = error
|
||||||
|
onCardClick()
|
||||||
|
}
|
||||||
scope.launch {
|
scope.launch {
|
||||||
snackbarHostState.showSnackbar(context.getString(R.string.snack_reload))
|
snackbarHostState.showSnackbar(context.getString(R.string.snack_reload))
|
||||||
}
|
}
|
||||||
@@ -242,6 +254,7 @@ class HomeViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// unused?
|
||||||
fun parseArgs(ip: String, port: String, lines: List<String>): Array<String> {
|
fun parseArgs(ip: String, port: String, lines: List<String>): Array<String> {
|
||||||
val regex = Regex("""--?\S+(?:=(?:[^"'\s]+|"[^"]*"|'[^']*'))?|[^\s]+""")
|
val regex = Regex("""--?\S+(?:=(?:[^"'\s]+|"[^"]*"|'[^']*'))?|[^\s]+""")
|
||||||
val parsedArgs = lines
|
val parsedArgs = lines
|
||||||
@@ -249,4 +262,7 @@ class HomeViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
return arrayOf("ciadpi", "--ip", ip, "--port", port) + parsedArgs
|
return arrayOf("ciadpi", "--ip", ip, "--port", port) + parsedArgs
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun clearError() {
|
||||||
|
_errorFlow.value = ""
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -12,6 +12,7 @@ import com.cherret.zaprett.R
|
|||||||
import com.cherret.zaprett.byedpi.ByeDpiVpnService
|
import com.cherret.zaprett.byedpi.ByeDpiVpnService
|
||||||
import com.cherret.zaprett.data.ServiceStatus
|
import com.cherret.zaprett.data.ServiceStatus
|
||||||
import com.cherret.zaprett.data.StrategyCheckResult
|
import com.cherret.zaprett.data.StrategyCheckResult
|
||||||
|
import com.cherret.zaprett.data.StrategyTestingStatus
|
||||||
import com.cherret.zaprett.utils.disableStrategy
|
import com.cherret.zaprett.utils.disableStrategy
|
||||||
import com.cherret.zaprett.utils.enableStrategy
|
import com.cherret.zaprett.utils.enableStrategy
|
||||||
import com.cherret.zaprett.utils.getActiveLists
|
import com.cherret.zaprett.utils.getActiveLists
|
||||||
@@ -26,22 +27,24 @@ import kotlinx.coroutines.awaitAll
|
|||||||
import kotlinx.coroutines.coroutineScope
|
import kotlinx.coroutines.coroutineScope
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import kotlinx.coroutines.withTimeoutOrNull
|
import kotlinx.coroutines.withTimeoutOrNull
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
import java.net.InetSocketAddress
|
||||||
|
import java.net.Proxy
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
class StrategySelectionViewModel(application: Application) : AndroidViewModel(application) {
|
class StrategySelectionViewModel(application: Application) : AndroidViewModel(application) {
|
||||||
val prefs = application.getSharedPreferences("settings", MODE_PRIVATE)
|
val prefs = application.getSharedPreferences("settings", MODE_PRIVATE)
|
||||||
val client = OkHttpClient.Builder()
|
|
||||||
.callTimeout(prefs.getLong("probe_timeout", 1000L), TimeUnit.MILLISECONDS)
|
|
||||||
.build()
|
|
||||||
val context = application
|
val context = application
|
||||||
private val _requestVpnPermission = MutableStateFlow(false)
|
private val _requestVpnPermission = MutableStateFlow(false)
|
||||||
val requestVpnPermission = _requestVpnPermission.asStateFlow()
|
val requestVpnPermission = _requestVpnPermission.asStateFlow()
|
||||||
|
private val _errorFlow = MutableStateFlow("")
|
||||||
|
val errorFlow = _errorFlow.asStateFlow()
|
||||||
val strategyStates = mutableStateListOf<StrategyCheckResult>()
|
val strategyStates = mutableStateListOf<StrategyCheckResult>()
|
||||||
var noHostsCard = mutableStateOf(false)
|
var noHostsCard = mutableStateOf(false)
|
||||||
private set
|
private set
|
||||||
@@ -51,14 +54,29 @@ class StrategySelectionViewModel(application: Application) : AndroidViewModel(ap
|
|||||||
checkHosts()
|
checkHosts()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun buildHttpClient(): OkHttpClient {
|
||||||
|
val builder = OkHttpClient.Builder()
|
||||||
|
.callTimeout(prefs.getLong("probe_timeout", 1000L), TimeUnit.MILLISECONDS)
|
||||||
|
.followRedirects(true)
|
||||||
|
.followSslRedirects(true)
|
||||||
|
if (!prefs.getBoolean("use_module", false)) {
|
||||||
|
val ip = prefs.getString("ip", "127.0.0.1") ?: "127.0.0.1"
|
||||||
|
val port = prefs.getString("port", "1080")?.toIntOrNull() ?: 1080
|
||||||
|
val proxy = Proxy(Proxy.Type.SOCKS, InetSocketAddress(ip, port))
|
||||||
|
builder.proxy(proxy)
|
||||||
|
}
|
||||||
|
return builder.build()
|
||||||
|
}
|
||||||
|
|
||||||
fun loadStrategies() {
|
fun loadStrategies() {
|
||||||
val strategyList = getAllStrategies(prefs)
|
val strategyList = getAllStrategies(prefs)
|
||||||
strategyStates.clear()
|
strategyStates.clear()
|
||||||
strategyList.forEach { name ->
|
strategyList.forEach { name ->
|
||||||
strategyStates += StrategyCheckResult(
|
strategyStates += StrategyCheckResult(
|
||||||
path = name,
|
path = name,
|
||||||
status = R.string.strategy_status_waiting,
|
status = StrategyTestingStatus.Waiting,
|
||||||
progress = 0f
|
progress = 0f,
|
||||||
|
domains = emptyList()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -68,21 +86,23 @@ class StrategySelectionViewModel(application: Application) : AndroidViewModel(ap
|
|||||||
.url("https://${domain}")
|
.url("https://${domain}")
|
||||||
.build()
|
.build()
|
||||||
try {
|
try {
|
||||||
client.newCall(request).execute().use { response ->
|
buildHttpClient().newCall(request).execute().use { response ->
|
||||||
response.isSuccessful || (response.code in 300..399)
|
val body = response.body.byteStream().readBytes()
|
||||||
|
val contentLength = response.body.contentLength()
|
||||||
|
contentLength <= 0 || body.size.toLong() >= contentLength
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun countReachable(urls: List<String>): Float = coroutineScope {
|
suspend fun countReachable(index: Int, urls: List<String>): Float = coroutineScope {
|
||||||
if (urls.isEmpty()) return@coroutineScope 0f
|
if (urls.isEmpty()) return@coroutineScope 0f
|
||||||
val results: List<Boolean> = urls.map { url ->
|
val results: List<String> = urls.map { url ->
|
||||||
async { testDomain(url) }
|
async { if (testDomain(url)) url else null }
|
||||||
}.awaitAll()
|
}.awaitAll().filterNotNull()
|
||||||
val successCount = results.count { it }
|
strategyStates[index].domains = results
|
||||||
(successCount.toFloat() / urls.size.toFloat()).coerceIn(0f, 1f)
|
(results.size.toFloat() / urls.size.toFloat()).coerceIn(0f, 1f)
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun readActiveListsLines(): List<String> = withContext(Dispatchers.IO) {
|
suspend fun readActiveListsLines(): List<String> = withContext(Dispatchers.IO) {
|
||||||
@@ -102,22 +122,33 @@ class StrategySelectionViewModel(application: Application) : AndroidViewModel(ap
|
|||||||
}
|
}
|
||||||
suspend fun performTest() {
|
suspend fun performTest() {
|
||||||
val targets = readActiveListsLines()
|
val targets = readActiveListsLines()
|
||||||
|
var stopTest : Boolean = false;
|
||||||
for (index in strategyStates.indices) {
|
for (index in strategyStates.indices) {
|
||||||
val current = strategyStates[index]
|
val current = strategyStates[index]
|
||||||
strategyStates[index] = current.copy(status = R.string.strategy_status_testing)
|
if (stopTest) break
|
||||||
|
strategyStates[index] = current.copy(status = StrategyTestingStatus.Testing)
|
||||||
enableStrategy(current.path, prefs)
|
enableStrategy(current.path, prefs)
|
||||||
if (prefs.getBoolean("use_module", false)) {
|
if (prefs.getBoolean("use_module", false)) {
|
||||||
getStatus { if (it) stopService {} }
|
getStatus { if (it) stopService { error ->
|
||||||
startService {}
|
_errorFlow.value = error
|
||||||
|
if (error.isNotEmpty()) stopTest = true
|
||||||
|
} }
|
||||||
|
startService { error ->
|
||||||
|
_errorFlow.value = error
|
||||||
|
if (error.isNotEmpty()) stopTest = true
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
val progress = countReachable(targets)
|
val progress = countReachable(index, targets)
|
||||||
val old = strategyStates[index]
|
val old = strategyStates[index]
|
||||||
strategyStates[index] = old.copy(
|
strategyStates[index] = old.copy(
|
||||||
progress = progress,
|
progress = progress,
|
||||||
status = R.string.strategy_status_tested
|
status = StrategyTestingStatus.Completed
|
||||||
)
|
)
|
||||||
} finally {
|
} finally {
|
||||||
stopService {}
|
stopService { error ->
|
||||||
|
_errorFlow.value = error
|
||||||
|
if (error.isNotEmpty()) stopTest = true
|
||||||
|
}
|
||||||
disableStrategy(current.path, prefs)
|
disableStrategy(current.path, prefs)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -140,12 +171,12 @@ class StrategySelectionViewModel(application: Application) : AndroidViewModel(ap
|
|||||||
} ?: false
|
} ?: false
|
||||||
if (connected) delay(150L)
|
if (connected) delay(150L)
|
||||||
try {
|
try {
|
||||||
val progress = countReachable(targets)
|
val progress = countReachable(index,targets)
|
||||||
val old = strategyStates[index]
|
val old = strategyStates[index]
|
||||||
|
|
||||||
strategyStates[index] = old.copy(
|
strategyStates[index] = old.copy(
|
||||||
progress = progress,
|
progress = progress,
|
||||||
status = R.string.strategy_status_tested
|
status = StrategyTestingStatus.Completed
|
||||||
)
|
)
|
||||||
} finally {
|
} finally {
|
||||||
context.startService(Intent(context, ByeDpiVpnService::class.java).apply {
|
context.startService(Intent(context, ByeDpiVpnService::class.java).apply {
|
||||||
@@ -162,8 +193,8 @@ class StrategySelectionViewModel(application: Application) : AndroidViewModel(ap
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun checkHosts() {
|
fun checkHosts() {
|
||||||
if (getActiveLists(prefs).isEmpty()) noHostsCard.value = true
|
if (getActiveLists(prefs).isEmpty() || getAllStrategies(prefs).isEmpty()) noHostsCard.value = true
|
||||||
Log.d("getActiveLists.isEmpty", getActiveLists(prefs).isEmpty().toString())
|
Log.d("getActiveLists.isEmpty || getAllStrategies.isEmpty", getActiveLists(prefs).isEmpty().toString())
|
||||||
}
|
}
|
||||||
fun startVpn() {
|
fun startVpn() {
|
||||||
ContextCompat.startForegroundService(context, Intent(context, ByeDpiVpnService::class.java).apply { action = "START_VPN" })
|
ContextCompat.startForegroundService(context, Intent(context, ByeDpiVpnService::class.java).apply { action = "START_VPN" })
|
||||||
@@ -171,4 +202,7 @@ class StrategySelectionViewModel(application: Application) : AndroidViewModel(ap
|
|||||||
fun clearVpnPermissionRequest() {
|
fun clearVpnPermissionRequest() {
|
||||||
_requestVpnPermission.value = false
|
_requestVpnPermission.value = false
|
||||||
}
|
}
|
||||||
|
fun clearError() {
|
||||||
|
_errorFlow.value = ""
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -18,8 +18,8 @@ import java.io.File
|
|||||||
|
|
||||||
class StrategyViewModel(application: Application): BaseListsViewModel(application) {
|
class StrategyViewModel(application: Application): BaseListsViewModel(application) {
|
||||||
private val sharedPreferences = application.getSharedPreferences("settings", Context.MODE_PRIVATE)
|
private val sharedPreferences = application.getSharedPreferences("settings", Context.MODE_PRIVATE)
|
||||||
private val useModule = sharedPreferences.getBoolean("use_module", false)
|
private val strategyProvider: StrategyProvider
|
||||||
private val strategyProvider: StrategyProvider = if (useModule) {
|
get() = if (sharedPreferences.getBoolean("use_module", false)) {
|
||||||
NfqwsStrategyProvider()
|
NfqwsStrategyProvider()
|
||||||
} else {
|
} else {
|
||||||
ByeDPIStrategyProvider(sharedPreferences)
|
ByeDPIStrategyProvider(sharedPreferences)
|
||||||
|
|||||||
@@ -1,18 +1,63 @@
|
|||||||
package com.cherret.zaprett.utils
|
package com.cherret.zaprett.utils
|
||||||
|
|
||||||
|
import android.Manifest
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Context.MODE_PRIVATE
|
import android.content.Context.MODE_PRIVATE
|
||||||
import android.content.SharedPreferences
|
import android.content.SharedPreferences
|
||||||
|
import android.content.pm.PackageManager
|
||||||
|
import android.os.Build
|
||||||
import android.os.Environment
|
import android.os.Environment
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import com.topjohnwu.superuser.Shell
|
import androidx.core.content.ContextCompat
|
||||||
import java.io.File
|
|
||||||
import java.io.FileInputStream
|
|
||||||
import java.io.FileOutputStream
|
|
||||||
import java.io.IOException
|
|
||||||
import java.util.Properties
|
|
||||||
import androidx.core.content.edit
|
import androidx.core.content.edit
|
||||||
import com.cherret.zaprett.data.AppListType
|
import com.cherret.zaprett.data.AppListType
|
||||||
|
import com.cherret.zaprett.data.ZaprettConfig
|
||||||
|
import com.topjohnwu.superuser.Shell
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
private val json = Json {
|
||||||
|
prettyPrint = true
|
||||||
|
ignoreUnknownKeys = true
|
||||||
|
coerceInputValues = true
|
||||||
|
encodeDefaults = true
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun readConfig(): ZaprettConfig {
|
||||||
|
val configFile = getConfigFile()
|
||||||
|
if (!configFile.exists()) {
|
||||||
|
return ZaprettConfig()
|
||||||
|
}
|
||||||
|
return try {
|
||||||
|
val content = configFile.readText()
|
||||||
|
if (content.isBlank()) ZaprettConfig() else json.decodeFromString<ZaprettConfig>(content)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e("ZaprettManager", "Error reading config, returning defaults", e)
|
||||||
|
ZaprettConfig()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun writeConfig(config: ZaprettConfig) {
|
||||||
|
val configFile = getConfigFile()
|
||||||
|
try {
|
||||||
|
configFile.parentFile?.mkdirs()
|
||||||
|
val content = json.encodeToString(config)
|
||||||
|
configFile.writeText(content)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e("ZaprettManager", "Error writing config", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun checkStoragePermission(context: Context): Boolean {
|
||||||
|
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||||
|
Environment.isExternalStorageManager()
|
||||||
|
} else {
|
||||||
|
ContextCompat.checkSelfPermission(
|
||||||
|
context,
|
||||||
|
Manifest.permission.WRITE_EXTERNAL_STORAGE
|
||||||
|
) == PackageManager.PERMISSION_GRANTED
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun checkRoot(callback: (Boolean) -> Unit) {
|
fun checkRoot(callback: (Boolean) -> Unit) {
|
||||||
Shell.getShell().isRoot.let { callback(it) }
|
Shell.getShell().isRoot.let { callback(it) }
|
||||||
@@ -30,75 +75,75 @@ fun getStatus(callback: (Boolean) -> Unit) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun startService(callback: (Boolean) -> Unit) {
|
fun startService(callback: (String) -> Unit) {
|
||||||
Shell.cmd("zaprett start").submit { result ->
|
Shell.cmd("zaprett start 2>&1").submit { result ->
|
||||||
callback(result.isSuccess)
|
callback(
|
||||||
|
if (result.isSuccess) ""
|
||||||
|
else result.out.joinToString("\n")
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun stopService(callback: (Boolean) -> Unit) {
|
|
||||||
Shell.cmd("zaprett stop").submit { result ->
|
fun stopService(callback: (String) -> Unit) {
|
||||||
callback(result.isSuccess)
|
Shell.cmd("zaprett stop 2>&1").submit { result ->
|
||||||
|
callback(
|
||||||
|
if (result.isSuccess) ""
|
||||||
|
else result.out.joinToString("\n")
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun restartService(callback: (Boolean) -> Unit) {
|
fun restartService(callback: (String) -> Unit) {
|
||||||
Shell.cmd("zaprett restart").submit { result ->
|
Shell.cmd("zaprett restart 2>&1").submit { result ->
|
||||||
callback(result.isSuccess)
|
callback(
|
||||||
|
if (result.isSuccess) ""
|
||||||
|
else result.out.joinToString("\n")
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getModuleVersion(callback: (String) -> Unit) {
|
fun getModuleVersion(callback: (String) -> Unit) {
|
||||||
Shell.cmd("zaprett module-ver").submit { result ->
|
Shell.cmd("zaprett module-version").submit { result ->
|
||||||
if (result.out.isNotEmpty()) callback(result.out.first()) else "undefined"
|
if (result.out.isNotEmpty()) callback(result.out.first()) else "undefined"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getBinVersion(callback: (String) -> Unit) {
|
fun getBinVersion(callback: (String) -> Unit) {
|
||||||
Shell.cmd("zaprett bin-ver").submit { result ->
|
Shell.cmd("zaprett binary-version").submit { result ->
|
||||||
if (result.out.isNotEmpty()) callback(result.out.first()) else "undefined"
|
if (result.out.isNotEmpty()) callback(result.out.first()) else "undefined"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getConfigFile(): File {
|
fun getConfigFile(): File {
|
||||||
return File(Environment.getExternalStorageDirectory(), "zaprett/config")
|
return File(Environment.getExternalStorageDirectory(), "zaprett/config.json")
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setStartOnBoot(prefs : SharedPreferences, callback: (Boolean) -> Unit) {
|
fun setStartOnBoot(prefs: SharedPreferences, callback: (Boolean) -> Unit) {
|
||||||
if (prefs.getBoolean("use_module", false)) {
|
if (prefs.getBoolean("use_module", false)) {
|
||||||
Shell.cmd("zaprett autostart").submit { result ->
|
Shell.cmd("zaprett set-autostart").submit { result ->
|
||||||
if (result.out.isNotEmpty() && result.out.toString().contains("true")) callback(true) else callback(false)
|
if (result.out.isNotEmpty() && result.out.toString().contains("true")) callback(true) else callback(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getStartOnBoot(prefs : SharedPreferences, callback: (Boolean) -> Unit) {
|
fun getStartOnBoot(prefs: SharedPreferences, callback: (Boolean) -> Unit) {
|
||||||
if (prefs.getBoolean("use_module", false)) {
|
if (prefs.getBoolean("use_module", false)) {
|
||||||
Shell.cmd("zaprett get-autostart").submit { result ->
|
Shell.cmd("zaprett get-autostart").submit { result ->
|
||||||
if (result.out.isNotEmpty() && result.out.toString().contains("true")) callback(true) else callback(false)
|
if (result.out.isNotEmpty() && result.out.toString().contains("true")) callback(true) else callback(false)
|
||||||
}
|
}
|
||||||
} else { callback(false) }
|
} else {
|
||||||
|
callback(false)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getZaprettPath(): String {
|
fun getZaprettPath(): String {
|
||||||
val props = Properties()
|
|
||||||
val configFile = getConfigFile()
|
|
||||||
if (configFile.exists()) {
|
|
||||||
return try {
|
|
||||||
FileInputStream(configFile).use { input ->
|
|
||||||
props.load(input)
|
|
||||||
}
|
|
||||||
props.getProperty("zaprettdir", Environment.getExternalStorageDirectory().path + "/zaprett")
|
|
||||||
} catch (e: IOException) {
|
|
||||||
throw RuntimeException(e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return Environment.getExternalStorageDirectory().path + "/zaprett"
|
return Environment.getExternalStorageDirectory().path + "/zaprett"
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getAllLists(): Array<String> {
|
fun getAllLists(): Array<String> {
|
||||||
val listsDir = File("${getZaprettPath()}/lists/include")
|
val listsDir = File("${getZaprettPath()}/lists/include")
|
||||||
return listsDir.listFiles { file -> file.isFile }
|
return listsDir.listFiles { file -> file.isFile && file.extension.lowercase() == "txt" }
|
||||||
?.map { it.absolutePath }
|
?.map { it.absolutePath }
|
||||||
?.toTypedArray()
|
?.toTypedArray()
|
||||||
?: emptyArray()
|
?: emptyArray()
|
||||||
@@ -106,7 +151,7 @@ fun getAllLists(): Array<String> {
|
|||||||
|
|
||||||
fun getAllIpsets(): Array<String> {
|
fun getAllIpsets(): Array<String> {
|
||||||
val listsDir = File("${getZaprettPath()}/ipset/include")
|
val listsDir = File("${getZaprettPath()}/ipset/include")
|
||||||
return listsDir.listFiles { file -> file.isFile }
|
return listsDir.listFiles { file -> file.isFile && file.extension.lowercase() == "txt" }
|
||||||
?.map { it.absolutePath }
|
?.map { it.absolutePath }
|
||||||
?.toTypedArray()
|
?.toTypedArray()
|
||||||
?: emptyArray()
|
?: emptyArray()
|
||||||
@@ -114,7 +159,7 @@ fun getAllIpsets(): Array<String> {
|
|||||||
|
|
||||||
fun getAllExcludeLists(): Array<String> {
|
fun getAllExcludeLists(): Array<String> {
|
||||||
val listsDir = File("${getZaprettPath()}/lists/exclude/")
|
val listsDir = File("${getZaprettPath()}/lists/exclude/")
|
||||||
return listsDir.listFiles { file -> file.isFile }
|
return listsDir.listFiles { file -> file.isFile && file.extension.lowercase() == "txt" }
|
||||||
?.map { it.absolutePath }
|
?.map { it.absolutePath }
|
||||||
?.toTypedArray()
|
?.toTypedArray()
|
||||||
?: emptyArray()
|
?: emptyArray()
|
||||||
@@ -122,7 +167,7 @@ fun getAllExcludeLists(): Array<String> {
|
|||||||
|
|
||||||
fun getAllExcludeIpsets(): Array<String> {
|
fun getAllExcludeIpsets(): Array<String> {
|
||||||
val listsDir = File("${getZaprettPath()}/ipset/exclude/")
|
val listsDir = File("${getZaprettPath()}/ipset/exclude/")
|
||||||
return listsDir.listFiles { file -> file.isFile }
|
return listsDir.listFiles { file -> file.isFile && file.extension.lowercase() == "txt" }
|
||||||
?.map { it.absolutePath }
|
?.map { it.absolutePath }
|
||||||
?.toTypedArray()
|
?.toTypedArray()
|
||||||
?: emptyArray()
|
?: emptyArray()
|
||||||
@@ -130,7 +175,7 @@ fun getAllExcludeIpsets(): Array<String> {
|
|||||||
|
|
||||||
fun getAllNfqwsStrategies(): Array<String> {
|
fun getAllNfqwsStrategies(): Array<String> {
|
||||||
val listsDir = File("${getZaprettPath()}/strategies/nfqws")
|
val listsDir = File("${getZaprettPath()}/strategies/nfqws")
|
||||||
return listsDir.listFiles { file -> file.isFile }
|
return listsDir.listFiles { file -> file.isFile && file.extension.lowercase() == "txt" }
|
||||||
?.map { it.absolutePath }
|
?.map { it.absolutePath }
|
||||||
?.toTypedArray()
|
?.toTypedArray()
|
||||||
?: emptyArray()
|
?: emptyArray()
|
||||||
@@ -138,123 +183,48 @@ fun getAllNfqwsStrategies(): Array<String> {
|
|||||||
|
|
||||||
fun getAllByeDPIStrategies(): Array<String> {
|
fun getAllByeDPIStrategies(): Array<String> {
|
||||||
val listsDir = File("${getZaprettPath()}/strategies/byedpi")
|
val listsDir = File("${getZaprettPath()}/strategies/byedpi")
|
||||||
return listsDir.listFiles { file -> file.isFile }
|
return listsDir.listFiles { file -> file.isFile && file.extension.lowercase() == "txt" }
|
||||||
?.map { it.absolutePath }
|
?.map { it.absolutePath }
|
||||||
?.toTypedArray()
|
?.toTypedArray()
|
||||||
?: emptyArray()
|
?: emptyArray()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getAllStrategies(sharedPreferences : SharedPreferences) : Array<String> {
|
fun getAllStrategies(sharedPreferences: SharedPreferences): Array<String> {
|
||||||
return if (sharedPreferences.getBoolean("use_module", false)) getAllNfqwsStrategies()
|
return if (sharedPreferences.getBoolean("use_module", false)) getAllNfqwsStrategies()
|
||||||
else getAllByeDPIStrategies()
|
else getAllByeDPIStrategies()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
fun getActiveLists(sharedPreferences: SharedPreferences): Array<String> {
|
fun getActiveLists(sharedPreferences: SharedPreferences): Array<String> {
|
||||||
if (sharedPreferences.getBoolean("use_module", false)) {
|
if (sharedPreferences.getBoolean("use_module", false)) {
|
||||||
val configFile = getConfigFile()
|
return readConfig().activeLists.toTypedArray()
|
||||||
if (configFile.exists()) {
|
} else {
|
||||||
val props = Properties()
|
|
||||||
return try {
|
|
||||||
FileInputStream(configFile).use { input ->
|
|
||||||
props.load(input)
|
|
||||||
}
|
|
||||||
val activeLists = props.getProperty("active_lists", "")
|
|
||||||
Log.d("Active lists", activeLists)
|
|
||||||
if (activeLists.isNotEmpty()) activeLists.split(",")
|
|
||||||
.toTypedArray() else emptyArray()
|
|
||||||
} catch (e: IOException) {
|
|
||||||
throw RuntimeException(e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return emptyArray()
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
return sharedPreferences.getStringSet("lists", emptySet())?.toTypedArray() ?: emptyArray()
|
return sharedPreferences.getStringSet("lists", emptySet())?.toTypedArray() ?: emptyArray()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getActiveIpsets(sharedPreferences: SharedPreferences): Array<String> {
|
fun getActiveIpsets(sharedPreferences: SharedPreferences): Array<String> {
|
||||||
if (sharedPreferences.getBoolean("use_module", false)) {
|
if (sharedPreferences.getBoolean("use_module", false)) {
|
||||||
val configFile = getConfigFile()
|
return readConfig().activeIpsets.toTypedArray()
|
||||||
if (configFile.exists()) {
|
} else return sharedPreferences.getStringSet("ipsets", emptySet())?.toTypedArray() ?: emptyArray()
|
||||||
val props = Properties()
|
|
||||||
return try {
|
|
||||||
FileInputStream(configFile).use { input ->
|
|
||||||
props.load(input)
|
|
||||||
}
|
|
||||||
val activeLists = props.getProperty("active_ipsets", "")
|
|
||||||
Log.d("Active ipsets", activeLists)
|
|
||||||
if (activeLists.isNotEmpty()) activeLists.split(",")
|
|
||||||
.toTypedArray() else emptyArray()
|
|
||||||
} catch (e: IOException) {
|
|
||||||
throw RuntimeException(e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return emptyArray()
|
|
||||||
}
|
|
||||||
else return sharedPreferences.getStringSet("ipsets", emptySet())?.toTypedArray() ?: emptyArray()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getActiveExcludeLists(sharedPreferences: SharedPreferences): Array<String> {
|
fun getActiveExcludeLists(sharedPreferences: SharedPreferences): Array<String> {
|
||||||
if (sharedPreferences.getBoolean("use_module", false)) {
|
if (sharedPreferences.getBoolean("use_module", false)) {
|
||||||
val configFile = getConfigFile()
|
return readConfig().activeExcludeLists.toTypedArray()
|
||||||
if (configFile.exists()) {
|
} else {
|
||||||
val props = Properties()
|
|
||||||
return try {
|
|
||||||
FileInputStream(configFile).use { input ->
|
|
||||||
props.load(input)
|
|
||||||
}
|
|
||||||
val activeLists = props.getProperty("active_exclude_lists", "")
|
|
||||||
if (activeLists.isNotEmpty()) activeLists.split(",")
|
|
||||||
.toTypedArray() else emptyArray()
|
|
||||||
} catch (e: IOException) {
|
|
||||||
throw RuntimeException(e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return emptyArray()
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
return sharedPreferences.getStringSet("exclude_lists", emptySet())?.toTypedArray() ?: emptyArray()
|
return sharedPreferences.getStringSet("exclude_lists", emptySet())?.toTypedArray() ?: emptyArray()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getActiveExcludeIpsets(sharedPreferences: SharedPreferences): Array<String> {
|
fun getActiveExcludeIpsets(sharedPreferences: SharedPreferences): Array<String> {
|
||||||
if (sharedPreferences.getBoolean("use_module", false)) {
|
if (sharedPreferences.getBoolean("use_module", false)) {
|
||||||
val configFile = getConfigFile()
|
return readConfig().activeExcludeIpsets.toTypedArray()
|
||||||
if (configFile.exists()) {
|
} else return sharedPreferences.getStringSet("exclude_ipsets", emptySet())?.toTypedArray() ?: emptyArray()
|
||||||
val props = Properties()
|
|
||||||
return try {
|
|
||||||
FileInputStream(configFile).use { input ->
|
|
||||||
props.load(input)
|
|
||||||
}
|
|
||||||
val activeLists = props.getProperty("active_exclude_ipsets", "")
|
|
||||||
Log.d("Active ipsets", activeLists)
|
|
||||||
if (activeLists.isNotEmpty()) activeLists.split(",")
|
|
||||||
.toTypedArray() else emptyArray()
|
|
||||||
} catch (e: IOException) {
|
|
||||||
throw RuntimeException(e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return emptyArray()
|
|
||||||
}
|
|
||||||
else return sharedPreferences.getStringSet("exclude_ipsets", emptySet())?.toTypedArray() ?: emptyArray()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getActiveNfqwsStrategy(): Array<String> {
|
fun getActiveNfqwsStrategy(): Array<String> {
|
||||||
val configFile = File("${getZaprettPath()}/config")
|
val strategy = readConfig().strategy
|
||||||
if (configFile.exists()) {
|
return if (strategy.isNotBlank()) arrayOf(strategy) else emptyArray()
|
||||||
val props = Properties()
|
|
||||||
return try {
|
|
||||||
FileInputStream(configFile).use { input ->
|
|
||||||
props.load(input)
|
|
||||||
}
|
|
||||||
val activeStrategies = props.getProperty("strategy", "")
|
|
||||||
Log.d("Active strategies", activeStrategies)
|
|
||||||
if (activeStrategies.isNotEmpty()) activeStrategies.split(",").toTypedArray() else emptyArray()
|
|
||||||
} catch (e: IOException) {
|
|
||||||
throw RuntimeException(e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return emptyArray()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getActiveByeDPIStrategy(sharedPreferences: SharedPreferences): Array<String> {
|
fun getActiveByeDPIStrategy(sharedPreferences: SharedPreferences): Array<String> {
|
||||||
@@ -276,351 +246,172 @@ fun getActiveByeDPIStrategyContent(sharedPreferences: SharedPreferences): List<S
|
|||||||
fun getActiveStrategy(sharedPreferences: SharedPreferences): Array<String> {
|
fun getActiveStrategy(sharedPreferences: SharedPreferences): Array<String> {
|
||||||
return if (sharedPreferences.getBoolean("use_module", false)) getActiveNfqwsStrategy()
|
return if (sharedPreferences.getBoolean("use_module", false)) getActiveNfqwsStrategy()
|
||||||
else getActiveByeDPIStrategy(sharedPreferences)
|
else getActiveByeDPIStrategy(sharedPreferences)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun enableList(path: String, sharedPreferences: SharedPreferences) {
|
fun enableList(path: String, sharedPreferences: SharedPreferences) {
|
||||||
if (sharedPreferences.getBoolean("use_module", false)) {
|
if (sharedPreferences.getBoolean("use_module", false)) {
|
||||||
val configFile = getConfigFile()
|
val config = readConfig()
|
||||||
try {
|
val isWhitelist = getHostListMode(sharedPreferences) == "whitelist"
|
||||||
val props = Properties()
|
val currentLists = if (isWhitelist) config.activeLists else config.activeExcludeLists
|
||||||
if (configFile.exists()) {
|
if (path !in currentLists) {
|
||||||
FileInputStream(configFile).use { input ->
|
val updatedLists = currentLists + path
|
||||||
props.load(input)
|
val newConfig = if (isWhitelist) {
|
||||||
|
config.copy(activeLists = updatedLists)
|
||||||
|
} else {
|
||||||
|
config.copy(activeExcludeLists = updatedLists)
|
||||||
}
|
}
|
||||||
|
writeConfig(newConfig)
|
||||||
}
|
}
|
||||||
val activeLists = props.getProperty(
|
} else {
|
||||||
if (getHostListMode(sharedPreferences) == "whitelist") "active_lists"
|
val key = if (getHostListMode(sharedPreferences) == "whitelist") "lists" else "exclude_lists"
|
||||||
else "active_exclude_lists",
|
val currentSet = sharedPreferences.getStringSet(key, emptySet())?.toMutableSet() ?: mutableSetOf()
|
||||||
"")
|
|
||||||
.split(",")
|
|
||||||
.filter { it.isNotBlank() }
|
|
||||||
.toMutableList()
|
|
||||||
if (path !in activeLists) {
|
|
||||||
activeLists.add(path)
|
|
||||||
}
|
|
||||||
props.setProperty(
|
|
||||||
if (getHostListMode(sharedPreferences) == "whitelist") "active_lists"
|
|
||||||
else "active_exclude_lists",
|
|
||||||
activeLists.joinToString(",")
|
|
||||||
)
|
|
||||||
FileOutputStream(configFile).use { output ->
|
|
||||||
props.store(output, "Don't place '/' in end of directory! Example: /sdcard")
|
|
||||||
}
|
|
||||||
} catch (e: IOException) {
|
|
||||||
throw RuntimeException(e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
val currentSet = sharedPreferences.getStringSet(
|
|
||||||
if (getHostListMode(sharedPreferences) == "whitelist") "lists"
|
|
||||||
else "exclude_lists", emptySet())?.toMutableSet() ?: mutableSetOf()
|
|
||||||
if (path !in currentSet) {
|
if (path !in currentSet) {
|
||||||
currentSet.add(path)
|
currentSet.add(path)
|
||||||
sharedPreferences.edit { putStringSet(
|
sharedPreferences.edit { putStringSet(key, currentSet) }
|
||||||
if (getHostListMode(sharedPreferences) == "whitelist") "lists"
|
|
||||||
else "exclude_lists", currentSet) }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun enableIpset(path: String, sharedPreferences: SharedPreferences) {
|
fun enableIpset(path: String, sharedPreferences: SharedPreferences) {
|
||||||
if (sharedPreferences.getBoolean("use_module", false)) {
|
if (sharedPreferences.getBoolean("use_module", false)) {
|
||||||
val configFile = getConfigFile()
|
val config = readConfig()
|
||||||
try {
|
val isWhitelist = getHostListMode(sharedPreferences) == "whitelist"
|
||||||
val props = Properties()
|
val currentIpsets = if (isWhitelist) config.activeIpsets else config.activeExcludeIpsets
|
||||||
if (configFile.exists()) {
|
if (path !in currentIpsets) {
|
||||||
FileInputStream(configFile).use { input ->
|
val updatedIpsets = currentIpsets + path
|
||||||
props.load(input)
|
val newConfig = if (isWhitelist) {
|
||||||
|
config.copy(activeIpsets = updatedIpsets)
|
||||||
|
} else {
|
||||||
|
config.copy(activeExcludeIpsets = updatedIpsets)
|
||||||
}
|
}
|
||||||
|
writeConfig(newConfig)
|
||||||
}
|
}
|
||||||
val activeLists = props.getProperty(
|
} else {
|
||||||
if (getHostListMode(sharedPreferences) == "whitelist") "active_ipsets"
|
val key = if (getHostListMode(sharedPreferences) == "whitelist") "ipsets" else "exclude_ipsets"
|
||||||
else "active_exclude_ipsets",
|
val currentSet = sharedPreferences.getStringSet(key, emptySet())?.toMutableSet() ?: mutableSetOf()
|
||||||
"")
|
|
||||||
.split(",")
|
|
||||||
.filter { it.isNotBlank() }
|
|
||||||
.toMutableList()
|
|
||||||
if (path !in activeLists) {
|
|
||||||
activeLists.add(path)
|
|
||||||
}
|
|
||||||
props.setProperty(
|
|
||||||
if (getHostListMode(sharedPreferences) == "whitelist") "active_ipsets"
|
|
||||||
else "active_exclude_ipsets",
|
|
||||||
activeLists.joinToString(",")
|
|
||||||
)
|
|
||||||
FileOutputStream(configFile).use { output ->
|
|
||||||
props.store(output, "Don't place '/' in end of directory! Example: /sdcard")
|
|
||||||
}
|
|
||||||
} catch (e: IOException) {
|
|
||||||
throw RuntimeException(e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
val currentSet = sharedPreferences.getStringSet(
|
|
||||||
if (getHostListMode(sharedPreferences) == "whitelist") "ipsets"
|
|
||||||
else "exclude_ipsets", emptySet())?.toMutableSet() ?: mutableSetOf()
|
|
||||||
if (path !in currentSet) {
|
if (path !in currentSet) {
|
||||||
currentSet.add(path)
|
currentSet.add(path)
|
||||||
sharedPreferences.edit { putStringSet(
|
sharedPreferences.edit { putStringSet(key, currentSet) }
|
||||||
if (getHostListMode(sharedPreferences) == "whitelist") "ipsets"
|
|
||||||
else "exclude_ipsets", currentSet) }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun enableStrategy(path: String, sharedPreferences: SharedPreferences) {
|
fun enableStrategy(path: String, sharedPreferences: SharedPreferences) {
|
||||||
if (sharedPreferences.getBoolean("use_module", false)) {
|
if (sharedPreferences.getBoolean("use_module", false)) {
|
||||||
val props = Properties()
|
val config = readConfig()
|
||||||
val configFile = getConfigFile()
|
if (config.strategy != path) {
|
||||||
try {
|
writeConfig(config.copy(strategy = path))
|
||||||
if (configFile.exists()) {
|
|
||||||
FileInputStream(configFile).use { input ->
|
|
||||||
props.load(input)
|
|
||||||
}
|
}
|
||||||
}
|
} else {
|
||||||
val activeStrategies = props.getProperty("strategy", "")
|
|
||||||
.split(",")
|
|
||||||
.filter { it.isNotBlank() }
|
|
||||||
.toMutableList()
|
|
||||||
if (path !in activeStrategies) {
|
|
||||||
activeStrategies.add(path)
|
|
||||||
}
|
|
||||||
props.setProperty("strategy", activeStrategies.joinToString(","))
|
|
||||||
FileOutputStream(configFile).use { output ->
|
|
||||||
props.store(output, "Don't place '/' in end of directory! Example: /sdcard")
|
|
||||||
}
|
|
||||||
} catch (e: IOException) {
|
|
||||||
throw RuntimeException(e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
sharedPreferences.edit { putString("active_strategy", path) }
|
sharedPreferences.edit { putString("active_strategy", path) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun disableList(path: String, sharedPreferences: SharedPreferences) {
|
fun disableList(path: String, sharedPreferences: SharedPreferences) {
|
||||||
if (sharedPreferences.getBoolean("use_module", false)) {
|
if (sharedPreferences.getBoolean("use_module", false)) {
|
||||||
val props = Properties()
|
val config = readConfig()
|
||||||
val configFile = getConfigFile()
|
val isWhitelist = getHostListMode(sharedPreferences) == "whitelist"
|
||||||
try {
|
val currentLists = if (isWhitelist) config.activeLists else config.activeExcludeLists
|
||||||
if (configFile.exists()) {
|
if (path in currentLists) {
|
||||||
FileInputStream(configFile).use { input ->
|
val updatedLists = currentLists.filter { it != path }
|
||||||
props.load(input)
|
val newConfig = if (isWhitelist) {
|
||||||
|
config.copy(activeLists = updatedLists)
|
||||||
|
} else {
|
||||||
|
config.copy(activeExcludeLists = updatedLists)
|
||||||
}
|
}
|
||||||
|
writeConfig(newConfig)
|
||||||
}
|
}
|
||||||
val activeLists = props.getProperty(
|
} else {
|
||||||
if (getHostListMode(sharedPreferences) == "whitelist") "active_lists"
|
val key = if (getHostListMode(sharedPreferences) == "whitelist") "lists" else "exclude_lists"
|
||||||
else "active_exclude_lists",
|
val currentSet = sharedPreferences.getStringSet(key, emptySet())?.toMutableSet() ?: mutableSetOf()
|
||||||
"")
|
|
||||||
.split(",")
|
|
||||||
.filter { it.isNotBlank() }
|
|
||||||
.toMutableList()
|
|
||||||
if (path in activeLists) {
|
|
||||||
activeLists.remove(path)
|
|
||||||
}
|
|
||||||
props.setProperty(
|
|
||||||
if (getHostListMode(sharedPreferences) == "whitelist") "active_lists"
|
|
||||||
else "active_exclude_lists",
|
|
||||||
activeLists.joinToString(",")
|
|
||||||
)
|
|
||||||
FileOutputStream(configFile).use { output ->
|
|
||||||
props.store(output, "Don't place '/' in end of directory! Example: /sdcard")
|
|
||||||
}
|
|
||||||
} catch (e: IOException) {
|
|
||||||
throw RuntimeException(e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
val currentSet = sharedPreferences.getStringSet(
|
|
||||||
if (getHostListMode(sharedPreferences) == "whitelist") "lists"
|
|
||||||
else "exclude_lists", emptySet())?.toMutableSet() ?: mutableSetOf()
|
|
||||||
if (path in currentSet) {
|
if (path in currentSet) {
|
||||||
currentSet.remove(path)
|
currentSet.remove(path)
|
||||||
sharedPreferences.edit { putStringSet(
|
sharedPreferences.edit { putStringSet(key, currentSet) }
|
||||||
if (getHostListMode(sharedPreferences) == "whitelist") "lists"
|
|
||||||
else "exclude_lists", currentSet) }
|
|
||||||
}
|
}
|
||||||
if (currentSet.isEmpty()) {
|
if (currentSet.isEmpty()) {
|
||||||
sharedPreferences.edit { remove(
|
sharedPreferences.edit { remove(key) }
|
||||||
if (getHostListMode(sharedPreferences) == "whitelist") "lists"
|
|
||||||
else "exclude_lists"
|
|
||||||
) }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun disableIpset(path: String, sharedPreferences: SharedPreferences) {
|
fun disableIpset(path: String, sharedPreferences: SharedPreferences) {
|
||||||
if (sharedPreferences.getBoolean("use_module", false)) {
|
if (sharedPreferences.getBoolean("use_module", false)) {
|
||||||
val props = Properties()
|
val config = readConfig()
|
||||||
val configFile = getConfigFile()
|
val isWhitelist = getHostListMode(sharedPreferences) == "whitelist"
|
||||||
try {
|
val currentIpsets = if (isWhitelist) config.activeIpsets else config.activeExcludeIpsets
|
||||||
if (configFile.exists()) {
|
if (path in currentIpsets) {
|
||||||
FileInputStream(configFile).use { input ->
|
val updatedIpsets = currentIpsets.filter { it != path }
|
||||||
props.load(input)
|
val newConfig = if (isWhitelist) {
|
||||||
|
config.copy(activeIpsets = updatedIpsets)
|
||||||
|
} else {
|
||||||
|
config.copy(activeExcludeIpsets = updatedIpsets)
|
||||||
}
|
}
|
||||||
|
writeConfig(newConfig)
|
||||||
}
|
}
|
||||||
val activeLists = props.getProperty(
|
} else {
|
||||||
if (getHostListMode(sharedPreferences) == "whitelist") "active_ipsets"
|
val key = if (getHostListMode(sharedPreferences) == "whitelist") "ipsets" else "exclude_ipsets"
|
||||||
else "active_exclude_ipsets",
|
val currentSet = sharedPreferences.getStringSet(key, emptySet())?.toMutableSet() ?: mutableSetOf()
|
||||||
"")
|
|
||||||
.split(",")
|
|
||||||
.filter { it.isNotBlank() }
|
|
||||||
.toMutableList()
|
|
||||||
if (path in activeLists) {
|
|
||||||
activeLists.remove(path)
|
|
||||||
}
|
|
||||||
props.setProperty(
|
|
||||||
if (getHostListMode(sharedPreferences) == "whitelist") "active_ipsets"
|
|
||||||
else "active_exclude_ipsets",
|
|
||||||
activeLists.joinToString(",")
|
|
||||||
)
|
|
||||||
FileOutputStream(configFile).use { output ->
|
|
||||||
props.store(output, "Don't place '/' in end of directory! Example: /sdcard")
|
|
||||||
}
|
|
||||||
} catch (e: IOException) {
|
|
||||||
throw RuntimeException(e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
val currentSet = sharedPreferences.getStringSet(
|
|
||||||
if (getHostListMode(sharedPreferences) == "whitelist") "ipsets"
|
|
||||||
else "exclude_ipsets", emptySet())?.toMutableSet() ?: mutableSetOf()
|
|
||||||
if (path in currentSet) {
|
if (path in currentSet) {
|
||||||
currentSet.remove(path)
|
currentSet.remove(path)
|
||||||
sharedPreferences.edit { putStringSet(
|
sharedPreferences.edit { putStringSet(key, currentSet) }
|
||||||
if (getHostListMode(sharedPreferences) == "whitelist") "ipsets"
|
|
||||||
else "exclude_ipsets", currentSet) }
|
|
||||||
}
|
}
|
||||||
if (currentSet.isEmpty()) {
|
if (currentSet.isEmpty()) {
|
||||||
sharedPreferences.edit { remove(
|
sharedPreferences.edit { remove(key) }
|
||||||
if (getHostListMode(sharedPreferences) == "whitelist") "ipsets"
|
|
||||||
else "exclude_ipsets"
|
|
||||||
) }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun disableStrategy(path: String, sharedPreferences: SharedPreferences) {
|
fun disableStrategy(path: String, sharedPreferences: SharedPreferences) {
|
||||||
if (sharedPreferences.getBoolean("use_module", false)) {
|
if (sharedPreferences.getBoolean("use_module", false)) {
|
||||||
val props = Properties()
|
val config = readConfig()
|
||||||
val configFile = getConfigFile()
|
if (config.strategy == path) {
|
||||||
try {
|
writeConfig(config.copy(strategy = ""))
|
||||||
if (configFile.exists()) {
|
|
||||||
FileInputStream(configFile).use { input ->
|
|
||||||
props.load(input)
|
|
||||||
}
|
}
|
||||||
}
|
} else {
|
||||||
val activeStrategies = props.getProperty("strategy", "")
|
|
||||||
.split(",")
|
|
||||||
.filter { it.isNotBlank() }
|
|
||||||
.toMutableList()
|
|
||||||
if (path in activeStrategies) {
|
|
||||||
activeStrategies.remove(path)
|
|
||||||
}
|
|
||||||
props.setProperty("strategy", activeStrategies.joinToString(","))
|
|
||||||
FileOutputStream(configFile).use { output ->
|
|
||||||
props.store(output, "Don't place '/' in end of directory! Example: /sdcard")
|
|
||||||
}
|
|
||||||
} catch (e: IOException) {
|
|
||||||
throw RuntimeException(e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
sharedPreferences.edit { remove("active_strategy") }
|
sharedPreferences.edit { remove("active_strategy") }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun addPackageToList(listType: AppListType, packageName: String, prefs : SharedPreferences, context : Context) {
|
fun addPackageToList(listType: AppListType, packageName: String, prefs: SharedPreferences, context: Context) {
|
||||||
if (prefs.getBoolean("use_module", false)){
|
if (prefs.getBoolean("use_module", false)) {
|
||||||
val configFile = getConfigFile()
|
val config = readConfig()
|
||||||
try {
|
|
||||||
val props = Properties()
|
|
||||||
if (configFile.exists()) {
|
|
||||||
FileInputStream(configFile).use { input ->
|
|
||||||
props.load(input)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (listType == AppListType.Whitelist) {
|
if (listType == AppListType.Whitelist) {
|
||||||
val whitelist = props.getProperty("whitelist", "")
|
if (packageName !in config.whitelist) {
|
||||||
.split(",")
|
writeConfig(config.copy(whitelist = config.whitelist + packageName))
|
||||||
.filter { it.isNotBlank() }
|
|
||||||
.toMutableList()
|
|
||||||
if (packageName !in whitelist) {
|
|
||||||
whitelist.add(packageName)
|
|
||||||
}
|
}
|
||||||
props.setProperty("whitelist", whitelist.joinToString(","))
|
} else if (listType == AppListType.Blacklist) {
|
||||||
}
|
if (packageName !in config.blacklist) {
|
||||||
if (listType == AppListType.Blacklist) {
|
writeConfig(config.copy(blacklist = config.blacklist + packageName))
|
||||||
val blacklist = props.getProperty("blacklist", "")
|
|
||||||
.split(",")
|
|
||||||
.filter { it.isNotBlank() }
|
|
||||||
.toMutableList()
|
|
||||||
if (packageName !in blacklist) {
|
|
||||||
blacklist.add(packageName)
|
|
||||||
}
|
|
||||||
props.setProperty("blacklist", blacklist.joinToString(","))
|
|
||||||
}
|
|
||||||
FileOutputStream(configFile).use { output ->
|
|
||||||
props.store(output, "Don't place '/' in end of directory! Example: /sdcard")
|
|
||||||
}
|
|
||||||
} catch (e: IOException) {
|
|
||||||
throw RuntimeException(e)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else {
|
} else {
|
||||||
val prefs = context.getSharedPreferences("settings", MODE_PRIVATE)
|
val prefs = context.getSharedPreferences("settings", MODE_PRIVATE)
|
||||||
if (listType == AppListType.Whitelist){
|
if (listType == AppListType.Whitelist) {
|
||||||
val set = prefs.getStringSet("whitelist", emptySet())?.toMutableSet() ?: mutableSetOf()
|
val set = prefs.getStringSet("whitelist", emptySet())?.toMutableSet() ?: mutableSetOf()
|
||||||
set.add(packageName)
|
set.add(packageName)
|
||||||
prefs.edit().putStringSet("whitelist", set).apply()
|
prefs.edit().putStringSet("whitelist", set).apply()
|
||||||
}
|
}
|
||||||
if (listType == AppListType.Blacklist){
|
if (listType == AppListType.Blacklist) {
|
||||||
val set = prefs.getStringSet("blacklist", emptySet())?.toMutableSet() ?: mutableSetOf()
|
val set = prefs.getStringSet("blacklist", emptySet())?.toMutableSet() ?: mutableSetOf()
|
||||||
set.add(packageName)
|
set.add(packageName)
|
||||||
prefs.edit().putStringSet("blacklist", set).apply()
|
prefs.edit().putStringSet("blacklist", set).apply()
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun removePackageFromList(listType: AppListType, packageName: String, prefs: SharedPreferences, context: Context) {
|
fun removePackageFromList(listType: AppListType, packageName: String, prefs: SharedPreferences, context: Context) {
|
||||||
if (prefs.getBoolean("use_module", false)){
|
if (prefs.getBoolean("use_module", false)) {
|
||||||
val props = Properties()
|
val config = readConfig()
|
||||||
val configFile = getConfigFile()
|
if (listType == AppListType.Whitelist) {
|
||||||
try {
|
if (packageName in config.whitelist) {
|
||||||
if (configFile.exists()) {
|
writeConfig(config.copy(whitelist = config.whitelist.filter { it != packageName }))
|
||||||
FileInputStream(configFile).use { input ->
|
}
|
||||||
props.load(input)
|
} else if (listType == AppListType.Blacklist) {
|
||||||
|
if (packageName in config.blacklist) {
|
||||||
|
writeConfig(config.copy(blacklist = config.blacklist.filter { it != packageName }))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (listType == AppListType.Whitelist){
|
} else {
|
||||||
val whitelist = props.getProperty("whitelist", "")
|
|
||||||
.split(",")
|
|
||||||
.filter { it.isNotBlank() }
|
|
||||||
.toMutableList()
|
|
||||||
if (packageName in whitelist) {
|
|
||||||
whitelist.remove(packageName)
|
|
||||||
}
|
|
||||||
props.setProperty("whitelist", whitelist.joinToString(","))
|
|
||||||
}
|
|
||||||
if (listType == AppListType.Blacklist) {
|
|
||||||
val blacklist = props.getProperty("blacklist", "")
|
|
||||||
.split(",")
|
|
||||||
.filter { it.isNotBlank() }
|
|
||||||
.toMutableList()
|
|
||||||
if (packageName in blacklist) {
|
|
||||||
blacklist.remove(packageName)
|
|
||||||
}
|
|
||||||
props.setProperty("blacklist", blacklist.joinToString(","))
|
|
||||||
}
|
|
||||||
|
|
||||||
FileOutputStream(configFile).use { output ->
|
|
||||||
props.store(output, "Don't place '/' in end of directory! Example: /sdcard")
|
|
||||||
}
|
|
||||||
} catch (e: IOException) {
|
|
||||||
throw RuntimeException(e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
val prefs = context.getSharedPreferences("settings", MODE_PRIVATE)
|
val prefs = context.getSharedPreferences("settings", MODE_PRIVATE)
|
||||||
if (listType == AppListType.Whitelist) {
|
if (listType == AppListType.Whitelist) {
|
||||||
val set = prefs.getStringSet("whitelist", emptySet())?.toMutableSet() ?: mutableSetOf()
|
val set = prefs.getStringSet("whitelist", emptySet())?.toMutableSet() ?: mutableSetOf()
|
||||||
@@ -635,70 +426,35 @@ fun removePackageFromList(listType: AppListType, packageName: String, prefs: Sha
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun isInList(listType: AppListType, packageName: String, prefs: SharedPreferences, context: Context) : Boolean {
|
fun isInList(listType: AppListType, packageName: String, prefs: SharedPreferences, context: Context): Boolean {
|
||||||
if (prefs.getBoolean("use_module", false)) {
|
if (prefs.getBoolean("use_module", false)) {
|
||||||
val configFile = getConfigFile()
|
val config = readConfig()
|
||||||
if (configFile.exists()) {
|
return if (listType == AppListType.Whitelist) {
|
||||||
val props = Properties()
|
packageName in config.whitelist
|
||||||
try {
|
} else {
|
||||||
FileInputStream(configFile).use { input ->
|
packageName in config.blacklist
|
||||||
props.load(input)
|
|
||||||
}
|
}
|
||||||
if (listType == AppListType.Whitelist) {
|
} else {
|
||||||
val whitelist = props.getProperty("whitelist", "")
|
|
||||||
return if (whitelist.isNotEmpty()) whitelist.split(",")
|
|
||||||
.toTypedArray().contains(packageName) else false
|
|
||||||
}
|
|
||||||
if (listType == AppListType.Blacklist) {
|
|
||||||
val blacklist = props.getProperty("blacklist", "")
|
|
||||||
return if (blacklist.isNotEmpty()) blacklist.split(",")
|
|
||||||
.toTypedArray().contains(packageName) else false
|
|
||||||
}
|
|
||||||
} catch (e: IOException) {
|
|
||||||
throw RuntimeException(e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
val prefs = context.getSharedPreferences("settings", MODE_PRIVATE)
|
val prefs = context.getSharedPreferences("settings", MODE_PRIVATE)
|
||||||
if(listType == AppListType.Whitelist){
|
if (listType == AppListType.Whitelist) {
|
||||||
val whitelist = prefs.getStringSet("whitelist", emptySet()) ?: emptySet()
|
val whitelist = prefs.getStringSet("whitelist", emptySet()) ?: emptySet()
|
||||||
return packageName in whitelist
|
return packageName in whitelist
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
val blacklist = prefs.getStringSet("blacklist", emptySet()) ?: emptySet()
|
val blacklist = prefs.getStringSet("blacklist", emptySet()) ?: emptySet()
|
||||||
return packageName in blacklist
|
return packageName in blacklist
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getAppList(listType: AppListType, sharedPreferences : SharedPreferences, context : Context) : Set<String> {
|
fun getAppList(listType: AppListType, sharedPreferences: SharedPreferences, context: Context): Set<String> {
|
||||||
if (sharedPreferences.getBoolean("use_module", false)) {
|
if (sharedPreferences.getBoolean("use_module", false)) {
|
||||||
val configFile = File("${getZaprettPath()}/config")
|
val config = readConfig()
|
||||||
if (configFile.exists()) {
|
return if (listType == AppListType.Whitelist) {
|
||||||
val props = Properties()
|
config.whitelist.toSet()
|
||||||
try {
|
} else {
|
||||||
FileInputStream(configFile).use { input ->
|
config.blacklist.toSet()
|
||||||
props.load(input)
|
|
||||||
}
|
}
|
||||||
if (listType == AppListType.Whitelist) {
|
} else {
|
||||||
val whitelist = props.getProperty("whitelist", "")
|
|
||||||
return if (whitelist.isNotEmpty()) whitelist.split(",")
|
|
||||||
.toSet() else emptySet()
|
|
||||||
}
|
|
||||||
if (listType == AppListType.Blacklist) {
|
|
||||||
val blacklist = props.getProperty("blacklist", "")
|
|
||||||
return if (blacklist.isNotEmpty()) blacklist.split(",")
|
|
||||||
.toSet() else emptySet()
|
|
||||||
}
|
|
||||||
} catch (e: IOException) {
|
|
||||||
throw RuntimeException(e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return emptySet()
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
return if (listType == AppListType.Whitelist) context.getSharedPreferences("settings", MODE_PRIVATE)
|
return if (listType == AppListType.Whitelist) context.getSharedPreferences("settings", MODE_PRIVATE)
|
||||||
.getStringSet("whitelist", emptySet()) ?: emptySet()
|
.getStringSet("whitelist", emptySet()) ?: emptySet()
|
||||||
else context.getSharedPreferences("settings", MODE_PRIVATE)
|
else context.getSharedPreferences("settings", MODE_PRIVATE)
|
||||||
@@ -706,104 +462,43 @@ fun getAppList(listType: AppListType, sharedPreferences : SharedPreferences, con
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getAppsListMode(prefs : SharedPreferences) : String {
|
fun getAppsListMode(prefs: SharedPreferences): String {
|
||||||
if(prefs.getBoolean("use_module", false)) {
|
if (prefs.getBoolean("use_module", false)) {
|
||||||
val configFile = getConfigFile()
|
val applist = readConfig().appList
|
||||||
if (configFile.exists()) {
|
|
||||||
val props = Properties()
|
|
||||||
try {
|
|
||||||
FileInputStream(configFile).use { input ->
|
|
||||||
props.load(input)
|
|
||||||
}
|
|
||||||
val applist = props.getProperty("app_list", "")!!
|
|
||||||
Log.d("App list", "Equals to $applist")
|
Log.d("App list", "Equals to $applist")
|
||||||
return if (applist == "whitelist" || applist == "blacklist" || applist == "none") applist
|
return if (applist == "whitelist" || applist == "blacklist" || applist == "none") applist
|
||||||
else "none"
|
else "none"
|
||||||
} catch (e: IOException) {
|
} else {
|
||||||
throw RuntimeException(e)
|
return prefs.getString("app_list", "none")!!
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
return prefs.getString("applist", "")!!
|
|
||||||
}
|
|
||||||
return "none"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setAppsListMode(prefs: SharedPreferences, mode: String) {
|
fun setAppsListMode(prefs: SharedPreferences, mode: String) {
|
||||||
if (prefs.getBoolean("use_module", false)) {
|
if (prefs.getBoolean("use_module", false)) {
|
||||||
val configFile = getConfigFile()
|
val config = readConfig()
|
||||||
val props = Properties()
|
writeConfig(config.copy(appList = mode))
|
||||||
if (configFile.exists()) {
|
} else {
|
||||||
try {
|
prefs.edit { putString("app_list", mode) }
|
||||||
FileInputStream(configFile).use { input ->
|
|
||||||
props.load(input)
|
|
||||||
}
|
|
||||||
} catch (e: IOException) {
|
|
||||||
throw RuntimeException(e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
props.setProperty("app_list", mode)
|
|
||||||
try {
|
|
||||||
FileOutputStream(configFile).use { output ->
|
|
||||||
props.store(output, "Don't place '/' in end of directory! Example: /sdcard")
|
|
||||||
}
|
|
||||||
} catch (e: IOException) {
|
|
||||||
throw RuntimeException(e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
prefs.edit { putString("app-list", mode) }
|
|
||||||
}
|
}
|
||||||
Log.d("App List", "Changed to $mode")
|
Log.d("App List", "Changed to $mode")
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setHostListMode(prefs: SharedPreferences, mode: String) {
|
fun setHostListMode(prefs: SharedPreferences, mode: String) {
|
||||||
if (prefs.getBoolean("use_module", false)) {
|
if (prefs.getBoolean("use_module", false)) {
|
||||||
val configFile = getConfigFile()
|
val config = readConfig()
|
||||||
val props = Properties()
|
writeConfig(config.copy(listType = mode))
|
||||||
if (configFile.exists()) {
|
} else {
|
||||||
try {
|
|
||||||
FileInputStream(configFile).use { input ->
|
|
||||||
props.load(input)
|
|
||||||
}
|
|
||||||
} catch (e: IOException) {
|
|
||||||
throw RuntimeException(e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
props.setProperty("list_type", mode)
|
|
||||||
try {
|
|
||||||
FileOutputStream(configFile).use { output ->
|
|
||||||
props.store(output, "Don't place '/' in end of directory! Example: /sdcard")
|
|
||||||
}
|
|
||||||
} catch (e: IOException) {
|
|
||||||
throw RuntimeException(e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
prefs.edit { putString("list_type", mode) }
|
prefs.edit { putString("list_type", mode) }
|
||||||
}
|
}
|
||||||
Log.d("App List", "Changed to $mode")
|
Log.d("App List", "Changed to $mode")
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getHostListMode(prefs : SharedPreferences) : String {
|
fun getHostListMode(prefs: SharedPreferences): String {
|
||||||
if(prefs.getBoolean("use_module", false)) {
|
if (prefs.getBoolean("use_module", false)) {
|
||||||
val configFile = getConfigFile()
|
val hostlist = readConfig().listType
|
||||||
if (configFile.exists()) {
|
|
||||||
val props = Properties()
|
|
||||||
try {
|
|
||||||
FileInputStream(configFile).use { input ->
|
|
||||||
props.load(input)
|
|
||||||
}
|
|
||||||
val hostlist = props.getProperty("list_type", "whitelist")!!
|
|
||||||
return if (hostlist == "whitelist" || hostlist == "blacklist") hostlist
|
return if (hostlist == "whitelist" || hostlist == "blacklist") hostlist
|
||||||
else "whitelist"
|
else "whitelist"
|
||||||
} catch (e: IOException) {
|
} else {
|
||||||
throw RuntimeException(e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
return prefs.getString("list_type", "whitelist")!!
|
return prefs.getString("list_type", "whitelist")!!
|
||||||
}
|
}
|
||||||
return "whitelist"
|
|
||||||
}
|
}
|
||||||
@@ -11,7 +11,7 @@
|
|||||||
<string name="btn_continue">Продолжить</string>
|
<string name="btn_continue">Продолжить</string>
|
||||||
<string name="btn_update">Обновить</string>
|
<string name="btn_update">Обновить</string>
|
||||||
<string name="btn_dismiss">Отмена</string>
|
<string name="btn_dismiss">Отмена</string>
|
||||||
<string name="text_welcome">Привет в zaprett! Это приложение предназначено для обхода цензуры и иных блокировок. Для полноценной работоспособности необходимо установить Magisk модуль.</string>
|
<string name="text_welcome">Добро пожаловать в zaprett! Это приложение предназначено для обхода цензуры и иных блокировок. Для полноценной работоспособности необходимо установить Magisk модуль.</string>
|
||||||
<string name="btn_use_root">Использовать модуль</string>
|
<string name="btn_use_root">Использовать модуль</string>
|
||||||
<string name="error_root_title">Root не получен</string>
|
<string name="error_root_title">Root не получен</string>
|
||||||
<string name="error_root_message">Не получилось получить root доступ. Дайте права root для использования модуля Magisk</string>
|
<string name="error_root_message">Не получилось получить root доступ. Дайте права root для использования модуля Magisk</string>
|
||||||
@@ -24,7 +24,7 @@
|
|||||||
<string name="status_enabled">Сервис zaprett работает. Нажми для обновления состояния</string>
|
<string name="status_enabled">Сервис zaprett работает. Нажми для обновления состояния</string>
|
||||||
<string name="status_disabled">Сервис zaprett не работает. Нажми для обновления состояния</string>
|
<string name="status_disabled">Сервис zaprett не работает. Нажми для обновления состояния</string>
|
||||||
<string name="status_crashed">Сервис zaprett вероятно крашнулся. Нажмите кнопку перезапуска ниже</string>
|
<string name="status_crashed">Сервис zaprett вероятно крашнулся. Нажмите кнопку перезапуска ниже</string>
|
||||||
<string name="update_available">Доступно новое обновление!</string>
|
<string name="update_available">Доступно обновление</string>
|
||||||
<string name="btn_update_on_boot">Обновлять статус при запуске Главной страницы</string>
|
<string name="btn_update_on_boot">Обновлять статус при запуске Главной страницы</string>
|
||||||
<string name="btn_start_service">Запустить сервис</string>
|
<string name="btn_start_service">Запустить сервис</string>
|
||||||
<string name="btn_stop_service">Остановить сервис</string>
|
<string name="btn_stop_service">Остановить сервис</string>
|
||||||
@@ -114,6 +114,10 @@
|
|||||||
<string name="hint_enter_probe_timeout">Введите таймаут пробы</string>
|
<string name="hint_enter_probe_timeout">Введите таймаут пробы</string>
|
||||||
<string name="strategy_selection_info_title">Информация</string>
|
<string name="strategy_selection_info_title">Информация</string>
|
||||||
<string name="strategy_selection_info_msg">"В этом разделе настроек приложения представлен перебор стратегий\n Подбор проходит среди скачанных стратегий, поэтому заранее скачайте из репозитория или добавьте из файловой системы интересующие вас стратегии для сравнения. \n Перед началом так же выберете один или несколько листов доменов на вкладке \"Листы\", затем нажмите на \"Начать подбор\". Не используйте для перебора списки с большим количеством доменов."</string>
|
<string name="strategy_selection_info_msg">"В этом разделе настроек приложения представлен перебор стратегий\n Подбор проходит среди скачанных стратегий, поэтому заранее скачайте из репозитория или добавьте из файловой системы интересующие вас стратегии для сравнения. \n Перед началом так же выберете один или несколько листов доменов на вкладке \"Листы\", затем нажмите на \"Начать подбор\". Не используйте для перебора списки с большим количеством доменов."</string>
|
||||||
<string name="selection_no_hosts_title">Нет активных листов</string>
|
<string name="selection_no_hosts_title">Нет включенных листов/стратегий</string>
|
||||||
<string name="selection_no_hosts_message">Не обнаружено активных списков хостов, включите один или несколько, иначе подбор не сработает</string>
|
<string name="selection_no_hosts_message">Не обнаружено активных списков хостов, либо доступных стратегий. Включите один или несколько, или скачайте стратегии для проверки, иначе подбор не сработает</string>
|
||||||
|
<string name="selection_available_domains">Доступные домены</string>
|
||||||
|
<string name="no_storage_permission_message">Нет разрешения на доступ к файлам</string>
|
||||||
|
<string name="reset_settings_title">Сброс настроек</string>
|
||||||
|
<string name="reset_settings_message">Вы действительно хотите сбросить настройки?</string>
|
||||||
</resources>
|
</resources>
|
||||||
@@ -11,7 +11,7 @@
|
|||||||
<string name="btn_continue">Continue</string>
|
<string name="btn_continue">Continue</string>
|
||||||
<string name="btn_update">Update</string>
|
<string name="btn_update">Update</string>
|
||||||
<string name="btn_dismiss">Dismiss</string>
|
<string name="btn_dismiss">Dismiss</string>
|
||||||
<string name="text_welcome">Hello to zaprett! This application is designed to bypass censorship and other blockages. For full functionality you need to install Magisk module.</string>
|
<string name="text_welcome">Welcome to zaprett! This application is designed to bypass censorship and other blockages. For full functionality you need to install Magisk module.</string>
|
||||||
<string name="btn_use_root">Use module</string>
|
<string name="btn_use_root">Use module</string>
|
||||||
<string name="error_root_title">Can\'t get root</string>
|
<string name="error_root_title">Can\'t get root</string>
|
||||||
<string name="error_root_message">Couldn\'t get root access. Give root access to use the Magisk module</string>
|
<string name="error_root_message">Couldn\'t get root access. Give root access to use the Magisk module</string>
|
||||||
@@ -24,7 +24,7 @@
|
|||||||
<string name="status_enabled">zaprett service is working. Tap to update</string>
|
<string name="status_enabled">zaprett service is working. Tap to update</string>
|
||||||
<string name="status_disabled">zaprett service disabled.</string>
|
<string name="status_disabled">zaprett service disabled.</string>
|
||||||
<string name="status_crashed">zaprett service crashed. Tap restart button below</string>
|
<string name="status_crashed">zaprett service crashed. Tap restart button below</string>
|
||||||
<string name="update_available">New update available!</string>
|
<string name="update_available">Update available</string>
|
||||||
<string name="btn_update_on_boot">Update the status when the Home page is launched</string>
|
<string name="btn_update_on_boot">Update the status when the Home page is launched</string>
|
||||||
<string name="btn_start_service">Start service</string>
|
<string name="btn_start_service">Start service</string>
|
||||||
<string name="btn_stop_service">Stop service</string>
|
<string name="btn_stop_service">Stop service</string>
|
||||||
@@ -119,6 +119,10 @@
|
|||||||
<string name="hint_enter_probe_timeout">Enter probe timeout</string>
|
<string name="hint_enter_probe_timeout">Enter probe timeout</string>
|
||||||
<string name="strategy_selection_info_title">Tip</string>
|
<string name="strategy_selection_info_title">Tip</string>
|
||||||
<string name="strategy_selection_info_msg">This section of the application settings allows you to iterate through strategies.\n The selection is based on downloaded strategies, so download the strategies you\'re interested in from the repository or add them from the file system for comparison.\n Before starting, select one or more domain lists in the \"Lists\" tab, then click \"Start selection\". Avoid using lists with a large number of domains.</string>
|
<string name="strategy_selection_info_msg">This section of the application settings allows you to iterate through strategies.\n The selection is based on downloaded strategies, so download the strategies you\'re interested in from the repository or add them from the file system for comparison.\n Before starting, select one or more domain lists in the \"Lists\" tab, then click \"Start selection\". Avoid using lists with a large number of domains.</string>
|
||||||
<string name="selection_no_hosts_title">No active hosts</string>
|
<string name="selection_no_hosts_title">No active hosts/strategies</string>
|
||||||
<string name="selection_no_hosts_message">No active host lists found, please enable one or more, otherwise the selection will not work</string>
|
<string name="selection_no_hosts_message">No active host lists or available strategies were found. Please enable one or more, or download strategies for testing, otherwise the selection will not work.</string>
|
||||||
|
<string name="selection_available_domains">Available domains</string>
|
||||||
|
<string name="no_storage_permission_message">Missing permission to access files</string>
|
||||||
|
<string name="reset_settings_title">Reset settings</string>
|
||||||
|
<string name="reset_settings_message">Are you sure you want to reset the settings?</string>
|
||||||
</resources>
|
</resources>
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
Это последнее летнее обновление 😭
|
1. Добавлено отображение ошибок при запуске сервиса
|
||||||
|
2. Оптимизация библиотеки byedpi
|
||||||
1) Добавление поддержки markdown в окне обновления
|
3. Исправление мелких ошибок
|
||||||
2) Обработка ошибок загрузки и получения обновления
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
[versions]
|
[versions]
|
||||||
agp = "8.13.0"
|
agp = "8.13.1"
|
||||||
kotlin = "2.2.10"
|
kotlin = "2.2.10"
|
||||||
coreKtx = "1.17.0"
|
coreKtx = "1.17.0"
|
||||||
junit = "4.13.2"
|
junit = "4.13.2"
|
||||||
|
|||||||
@@ -15,3 +15,9 @@ crate-type = ["cdylib"]
|
|||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
cmake = "0.1.49"
|
cmake = "0.1.49"
|
||||||
|
|
||||||
|
[profile.release]
|
||||||
|
opt-level = "z"
|
||||||
|
lto = true
|
||||||
|
strip = true
|
||||||
|
codegen-units = 1
|
||||||
|
|||||||
@@ -47,6 +47,11 @@ pub unsafe extern "system" fn Java_com_cherret_zaprett_byedpi_NativeBridge_jniSt
|
|||||||
let mut argv: Vec<*const c_char> = cstrings.iter().map(|s| s.as_ptr()).collect();
|
let mut argv: Vec<*const c_char> = cstrings.iter().map(|s| s.as_ptr()).collect();
|
||||||
argv.push(std::ptr::null());
|
argv.push(std::ptr::null());
|
||||||
info!("starting proxy");
|
info!("starting proxy");
|
||||||
|
unsafe {
|
||||||
|
optind = 1;
|
||||||
|
optreset = 1;
|
||||||
|
clear_params();
|
||||||
|
}
|
||||||
PROXY_RUNNING.store(true, Ordering::SeqCst);
|
PROXY_RUNNING.store(true, Ordering::SeqCst);
|
||||||
let ret = unsafe { main(argc as i32, argv.as_ptr()) };
|
let ret = unsafe { main(argc as i32, argv.as_ptr()) };
|
||||||
PROXY_RUNNING.store(false, Ordering::SeqCst);
|
PROXY_RUNNING.store(false, Ordering::SeqCst);
|
||||||
@@ -65,10 +70,5 @@ pub unsafe extern "system" fn Java_com_cherret_zaprett_byedpi_NativeBridge_jniSt
|
|||||||
}
|
}
|
||||||
info!("stopping proxy");
|
info!("stopping proxy");
|
||||||
let ret = unsafe { shutdown(server_fd, SHUT_RDWR) };
|
let ret = unsafe { shutdown(server_fd, SHUT_RDWR) };
|
||||||
unsafe {
|
|
||||||
optreset = 1;
|
|
||||||
optind = 1;
|
|
||||||
}
|
|
||||||
unsafe { clear_params() };
|
|
||||||
ret as jint
|
ret as jint
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"version": "2.9",
|
"version": "2.13",
|
||||||
"versionCode": 21,
|
"versionCode": 25,
|
||||||
"downloadUrl": "https://github.com/CherretGit/zaprett-app/releases/download/2.9.0/app-release.apk",
|
"downloadUrl": "https://github.com/CherretGit/zaprett-app/releases/download/2.13.0/app-release.apk",
|
||||||
"changelogUrl": "https://raw.githubusercontent.com/CherretGit/zaprett-app/refs/heads/main/changelog.md"
|
"changelogUrl": "https://raw.githubusercontent.com/CherretGit/zaprett-app/refs/heads/main/changelog.md"
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user