2 Commits

Author SHA1 Message Date
CherretGit
717368b4e9 bump version 2025-11-03 18:03:43 +07:00
CherretGit
36885a8410 fix some crashed from firebase crashlytics 2025-11-03 17:59:27 +07:00
14 changed files with 175 additions and 59 deletions

View File

@@ -16,8 +16,8 @@ android {
applicationId = "com.cherret.zaprett"
minSdk = 29
targetSdk = 35
versionCode = 23
versionName = "2.11"
versionCode = 24
versionName = "2.12"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}

View File

@@ -61,6 +61,7 @@ import com.cherret.zaprett.ui.viewmodel.HostRepoViewModel
import com.cherret.zaprett.ui.viewmodel.IpsetRepoViewModel
import com.cherret.zaprett.ui.viewmodel.StrategyRepoViewModel
import com.cherret.zaprett.utils.checkModuleInstallation
import com.cherret.zaprett.utils.checkStoragePermission
import com.google.firebase.Firebase
import com.google.firebase.analytics.FirebaseAnalytics
import com.google.firebase.analytics.analytics
@@ -110,16 +111,7 @@ class MainActivity : ComponentActivity() {
}
}
var showStoragePermissionDialog by remember {
mutableStateOf(
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
!Environment.isExternalStorageManager()
} else {
ContextCompat.checkSelfPermission(
this,
Manifest.permission.WRITE_EXTERNAL_STORAGE
) != PackageManager.PERMISSION_GRANTED
}
)
mutableStateOf(!checkStoragePermission(this))
}
var showNotificationPermissionDialog by remember {
mutableStateOf(
@@ -272,5 +264,4 @@ class MainActivity : ComponentActivity() {
}
)
}
}

View File

@@ -19,6 +19,7 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.Download
import androidx.compose.material.icons.filled.UploadFile
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.ExperimentalMaterial3Api
@@ -31,10 +32,13 @@ import androidx.compose.material3.SingleChoiceSegmentedButtonRow
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.pulltorefresh.PullToRefreshBox
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
@@ -67,6 +71,7 @@ fun HostsScreen(navController: NavController, viewModel: HostsViewModel = viewMo
val allLists = viewModel.allItems
val checked = viewModel.checked
val isRefreshing = viewModel.isRefreshing
val showPermissionDialog by viewModel.showNoPermissionDialog.collectAsState()
val filePickerLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.OpenDocument()
) { uri ->
@@ -147,10 +152,26 @@ fun HostsScreen(navController: NavController, viewModel: HostsViewModel = viewMo
},
snackbarHost = { SnackbarHost(hostState = snackbarHostState) }
)
if (showPermissionDialog) {
AlertDialog(
title = { Text(text = stringResource(R.string.error_no_storage_title)) },
text = { Text(text = stringResource(R.string.no_storage_permission_message)) },
onDismissRequest = {
viewModel.hideNoPermissionDialog()
navController.popBackStack()
},
confirmButton = {
TextButton(onClick = {
viewModel.hideNoPermissionDialog()
navController.popBackStack()
}) {
Text(stringResource(R.string.btn_continue))
}
}
)
}
}
@Composable
private fun FloatingMenu(navController: NavController, launcher: ActivityResultLauncher<Array<String>>) {
var expanded by remember { mutableStateOf(false) }

View File

@@ -19,6 +19,7 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.Download
import androidx.compose.material.icons.filled.UploadFile
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.ExperimentalMaterial3Api
@@ -31,10 +32,12 @@ import androidx.compose.material3.SingleChoiceSegmentedButtonRow
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.pulltorefresh.PullToRefreshBox
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
@@ -68,6 +71,7 @@ fun IpsetsScreen(navController: NavController, viewModel: IpsetViewModel = viewM
val allLists = viewModel.allItems
val checked = viewModel.checked
val isRefreshing = viewModel.isRefreshing
val showPermissionDialog by viewModel.showNoPermissionDialog.collectAsState()
val filePickerLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.OpenDocument()
) { uri ->
@@ -148,10 +152,26 @@ fun IpsetsScreen(navController: NavController, viewModel: IpsetViewModel = viewM
},
snackbarHost = { SnackbarHost(hostState = snackbarHostState) }
)
if (showPermissionDialog) {
AlertDialog(
title = { Text(text = stringResource(R.string.error_no_storage_title)) },
text = { Text(text = stringResource(R.string.no_storage_permission_message)) },
onDismissRequest = {
viewModel.hideNoPermissionDialog()
navController.popBackStack()
},
confirmButton = {
TextButton(onClick = {
viewModel.hideNoPermissionDialog()
navController.popBackStack()
}) {
Text(stringResource(R.string.btn_continue))
}
}
)
}
}
@Composable
private fun FloatingMenu(navController: NavController, launcher: ActivityResultLauncher<Array<String>>) {
var expanded by remember { mutableStateOf(false) }

View File

@@ -57,6 +57,7 @@ fun RepoScreen(navController: NavController, viewModel: BaseRepoViewModel) {
val snackbarHostState = remember { SnackbarHostState() }
val error by viewModel.errorFlow.collectAsState()
val downloadError by viewModel.downloadErrorFlow.collectAsState()
val showPermissionDialog by viewModel.showPermissionDialog.collectAsState()
LaunchedEffect(Unit) {
viewModel.refresh()
}
@@ -131,6 +132,24 @@ fun RepoScreen(navController: NavController, viewModel: BaseRepoViewModel) {
)
}
if (showPermissionDialog) {
AlertDialog(
title = { Text(text = stringResource(R.string.error_no_storage_title)) },
text = { Text(text = stringResource(R.string.no_storage_permission_message)) },
onDismissRequest = {
viewModel.hideNoPermissionDialog()
navController.popBackStack()
},
confirmButton = {
TextButton(onClick = {
viewModel.hideNoPermissionDialog()
navController.popBackStack()
}) {
Text(stringResource(R.string.btn_continue))
}
}
)
}
Scaffold(
topBar = {

View File

@@ -16,6 +16,7 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.Download
import androidx.compose.material.icons.filled.UploadFile
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.ExperimentalMaterial3Api
@@ -25,10 +26,12 @@ import androidx.compose.material3.Scaffold
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.pulltorefresh.PullToRefreshBox
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
@@ -60,6 +63,7 @@ fun StrategyScreen(navController: NavController, viewModel: StrategyViewModel =
val allLists = viewModel.allItems
val checked = viewModel.checked
val isRefreshing = viewModel.isRefreshing
val showPermissionDialog by viewModel.showNoPermissionDialog.collectAsState()
val filePickerLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.OpenDocument()
) { uri ->
@@ -139,6 +143,24 @@ fun StrategyScreen(navController: NavController, viewModel: StrategyViewModel =
},
snackbarHost = { SnackbarHost(hostState = snackbarHostState) }
)
if (showPermissionDialog) {
AlertDialog(
title = { Text(text = stringResource(R.string.error_no_storage_title)) },
text = { Text(text = stringResource(R.string.no_storage_permission_message)) },
onDismissRequest = {
viewModel.hideNoPermissionDialog()
navController.popBackStack()
},
confirmButton = {
TextButton(onClick = {
viewModel.hideNoPermissionDialog()
navController.popBackStack()
}) {
Text(stringResource(R.string.btn_continue))
}
}
)
}
}
@Composable

View File

@@ -192,5 +192,4 @@ private fun NoHostsCard(noHostsCard: MutableState<Boolean>) {
}
)
}
}
}

View File

@@ -16,9 +16,12 @@ import androidx.compose.runtime.setValue
import androidx.core.content.ContextCompat
import androidx.lifecycle.AndroidViewModel
import com.cherret.zaprett.R
import com.cherret.zaprett.utils.checkStoragePermission
import com.cherret.zaprett.utils.getZaprettPath
import com.cherret.zaprett.utils.restartService
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
import java.io.File
import java.io.FileOutputStream
@@ -34,20 +37,32 @@ abstract class BaseListsViewModel(application: Application) : AndroidViewModel(a
var isRefreshing by mutableStateOf(false)
private set
private var _showNoPermissionDialog = MutableStateFlow(false)
val showNoPermissionDialog: StateFlow<Boolean> = _showNoPermissionDialog
abstract fun loadAllItems(): Array<String>
abstract fun loadActiveItems(): Array<String>
abstract fun onCheckedChange(item: String, isChecked: Boolean, snackbarHostState: SnackbarHostState, scope: CoroutineScope)
abstract fun deleteItem(item: String, snackbarHostState: SnackbarHostState, scope: CoroutineScope)
fun refresh() {
isRefreshing = true
allItems = loadAllItems().toList()
activeItems = loadActiveItems().toList()
checked.clear()
allItems.forEach { list ->
checked[list] = activeItems.contains(list)
when (checkStoragePermission(context)) {
true -> {
isRefreshing = true
allItems = loadAllItems().toList()
activeItems = loadActiveItems().toList()
checked.clear()
allItems.forEach { list ->
checked[list] = activeItems.contains(list)
}
isRefreshing = false
}
false -> _showNoPermissionDialog.value = true
}
isRefreshing = false
}
fun hideNoPermissionDialog() {
_showNoPermissionDialog.value = false
}
fun showRestartSnackbar(context: Context, snackbarHostState: SnackbarHostState, scope: CoroutineScope) {

View File

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

View File

@@ -103,7 +103,7 @@ class HomeViewModel(application: Application) : AndroidViewModel(application) {
}
fun checkServiceStatus() {
val updateOnBoot = prefs.getBoolean("update_on_boot", false)
val updateOnBoot = prefs.getBoolean("update_on_boot", true)
if (updateOnBoot) {
val useModule = prefs.getBoolean("use_module", false)
updateServiceStatus(useModule)

View File

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

View File

@@ -1,10 +1,14 @@
package com.cherret.zaprett.utils
import android.Manifest
import android.content.Context
import android.content.Context.MODE_PRIVATE
import android.content.SharedPreferences
import android.content.pm.PackageManager
import android.os.Build
import android.os.Environment
import android.util.Log
import androidx.core.content.ContextCompat
import androidx.core.content.edit
import com.cherret.zaprett.data.AppListType
import com.cherret.zaprett.data.ZaprettConfig
@@ -44,6 +48,17 @@ private fun writeConfig(config: ZaprettConfig) {
}
}
fun checkStoragePermission(context: Context): Boolean {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
Environment.isExternalStorageManager()
} else {
ContextCompat.checkSelfPermission(
context,
Manifest.permission.WRITE_EXTERNAL_STORAGE
) == PackageManager.PERMISSION_GRANTED
}
}
fun checkRoot(callback: (Boolean) -> Unit) {
Shell.getShell().isRoot.let { callback(it) }
}

View File

@@ -117,4 +117,5 @@
<string name="selection_no_hosts_title">Нет активных листов</string>
<string name="selection_no_hosts_message">Не обнаружено активных списков хостов, включите один или несколько, иначе подбор не сработает</string>
<string name="selection_available_domains">Доступные домены</string>
<string name="no_storage_permission_message">Нет разрешения на доступ к файлам</string>
</resources>

View File

@@ -122,4 +122,5 @@
<string name="selection_no_hosts_title">No active hosts</string>
<string name="selection_no_hosts_message">No active host lists found, please enable one or more, otherwise the selection will not work</string>
<string name="selection_available_domains">Available domains</string>
<string name="no_storage_permission_message">Missing permission to access files</string>
</resources>