diff --git a/app/src/main/java/com/cherret/zaprett/MainActivity.kt b/app/src/main/java/com/cherret/zaprett/MainActivity.kt index bb03570..f2cb24e 100644 --- a/app/src/main/java/com/cherret/zaprett/MainActivity.kt +++ b/app/src/main/java/com/cherret/zaprett/MainActivity.kt @@ -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() { } ) } - } \ No newline at end of file diff --git a/app/src/main/java/com/cherret/zaprett/ui/screen/HostsScreen.kt b/app/src/main/java/com/cherret/zaprett/ui/screen/HostsScreen.kt index 09d28ec..ac2c95e 100644 --- a/app/src/main/java/com/cherret/zaprett/ui/screen/HostsScreen.kt +++ b/app/src/main/java/com/cherret/zaprett/ui/screen/HostsScreen.kt @@ -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>) { var expanded by remember { mutableStateOf(false) } diff --git a/app/src/main/java/com/cherret/zaprett/ui/screen/IpsetsScreen.kt b/app/src/main/java/com/cherret/zaprett/ui/screen/IpsetsScreen.kt index c1c0ef6..d5e80ad 100644 --- a/app/src/main/java/com/cherret/zaprett/ui/screen/IpsetsScreen.kt +++ b/app/src/main/java/com/cherret/zaprett/ui/screen/IpsetsScreen.kt @@ -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>) { var expanded by remember { mutableStateOf(false) } diff --git a/app/src/main/java/com/cherret/zaprett/ui/screen/RepoScreen.kt b/app/src/main/java/com/cherret/zaprett/ui/screen/RepoScreen.kt index 4202e30..4cdfadd 100644 --- a/app/src/main/java/com/cherret/zaprett/ui/screen/RepoScreen.kt +++ b/app/src/main/java/com/cherret/zaprett/ui/screen/RepoScreen.kt @@ -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 = { diff --git a/app/src/main/java/com/cherret/zaprett/ui/screen/StrategyScreen.kt b/app/src/main/java/com/cherret/zaprett/ui/screen/StrategyScreen.kt index 2823d91..bc28e38 100644 --- a/app/src/main/java/com/cherret/zaprett/ui/screen/StrategyScreen.kt +++ b/app/src/main/java/com/cherret/zaprett/ui/screen/StrategyScreen.kt @@ -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 diff --git a/app/src/main/java/com/cherret/zaprett/ui/screen/StrategySelectionScreen.kt b/app/src/main/java/com/cherret/zaprett/ui/screen/StrategySelectionScreen.kt index f288a1b..0bc189b 100644 --- a/app/src/main/java/com/cherret/zaprett/ui/screen/StrategySelectionScreen.kt +++ b/app/src/main/java/com/cherret/zaprett/ui/screen/StrategySelectionScreen.kt @@ -192,5 +192,4 @@ private fun NoHostsCard(noHostsCard: MutableState) { } ) } -} - +} \ No newline at end of file diff --git a/app/src/main/java/com/cherret/zaprett/ui/viewmodel/BaseListsViewModel.kt b/app/src/main/java/com/cherret/zaprett/ui/viewmodel/BaseListsViewModel.kt index 6368680..d1691b8 100644 --- a/app/src/main/java/com/cherret/zaprett/ui/viewmodel/BaseListsViewModel.kt +++ b/app/src/main/java/com/cherret/zaprett/ui/viewmodel/BaseListsViewModel.kt @@ -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 = _showNoPermissionDialog + abstract fun loadAllItems(): Array abstract fun loadActiveItems(): Array 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) { diff --git a/app/src/main/java/com/cherret/zaprett/ui/viewmodel/BaseRepoViewModel.kt b/app/src/main/java/com/cherret/zaprett/ui/viewmodel/BaseRepoViewModel.kt index 98bfbf0..7c1ecc1 100644 --- a/app/src/main/java/com/cherret/zaprett/ui/viewmodel/BaseRepoViewModel.kt +++ b/app/src/main/java/com/cherret/zaprett/ui/viewmodel/BaseRepoViewModel.kt @@ -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 = _downloadErrorFlow + private var _showPermissionDialog = MutableStateFlow(false) + val showPermissionDialog: StateFlow = _showPermissionDialog + var hostLists = mutableStateOf>(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( diff --git a/app/src/main/java/com/cherret/zaprett/ui/viewmodel/HomeViewModel.kt b/app/src/main/java/com/cherret/zaprett/ui/viewmodel/HomeViewModel.kt index 0102e33..a29079b 100644 --- a/app/src/main/java/com/cherret/zaprett/ui/viewmodel/HomeViewModel.kt +++ b/app/src/main/java/com/cherret/zaprett/ui/viewmodel/HomeViewModel.kt @@ -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) diff --git a/app/src/main/java/com/cherret/zaprett/ui/viewmodel/StrategyViewModel.kt b/app/src/main/java/com/cherret/zaprett/ui/viewmodel/StrategyViewModel.kt index 801395f..9143231 100644 --- a/app/src/main/java/com/cherret/zaprett/ui/viewmodel/StrategyViewModel.kt +++ b/app/src/main/java/com/cherret/zaprett/ui/viewmodel/StrategyViewModel.kt @@ -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 = strategyProvider.getAll() override fun loadActiveItems(): Array = strategyProvider.getActive() diff --git a/app/src/main/java/com/cherret/zaprett/utils/ZaprettManager.kt b/app/src/main/java/com/cherret/zaprett/utils/ZaprettManager.kt index 3ba5595..118b740 100644 --- a/app/src/main/java/com/cherret/zaprett/utils/ZaprettManager.kt +++ b/app/src/main/java/com/cherret/zaprett/utils/ZaprettManager.kt @@ -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) } } diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 80d58fa..691ea57 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -117,4 +117,5 @@ Нет активных листов Не обнаружено активных списков хостов, включите один или несколько, иначе подбор не сработает Доступные домены + Нет разрешения на доступ к файлам \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 458a254..3f055c3 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -122,4 +122,5 @@ No active hosts No active host lists found, please enable one or more, otherwise the selection will not work Available domains + Missing permission to access files \ No newline at end of file