diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 604ce16..f481133 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -29,6 +29,12 @@ android { getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro" ) + buildConfigField("boolean", "send_firebase_analytics", "true") + buildConfigField("boolean", "auto_update", "true") + } + debug { + buildConfigField("boolean", "send_firebase_analytics", "false") + buildConfigField("boolean", "auto_update", "false") } } compileOptions { diff --git a/app/src/main/java/com/cherret/zaprett/MainActivity.kt b/app/src/main/java/com/cherret/zaprett/MainActivity.kt index 584bdd6..f41782d 100644 --- a/app/src/main/java/com/cherret/zaprett/MainActivity.kt +++ b/app/src/main/java/com/cherret/zaprett/MainActivity.kt @@ -54,6 +54,7 @@ import com.cherret.zaprett.ui.screen.IpsetsScreen import com.cherret.zaprett.ui.screen.RepoScreen import com.cherret.zaprett.ui.screen.SettingsScreen import com.cherret.zaprett.ui.screen.StrategyScreen +import com.cherret.zaprett.ui.screen.StrategySelectionScreen import com.cherret.zaprett.ui.theme.ZaprettTheme import com.cherret.zaprett.ui.viewmodel.HomeViewModel import com.cherret.zaprett.ui.viewmodel.HostRepoViewModel @@ -72,7 +73,7 @@ sealed class Screen(val route: String, @StringRes val nameResId: Int, val icon: object settings : Screen("settings", R.string.title_settings, Icons.Default.Settings) } val topLevelRoutes = listOf(Screen.home, Screen.hosts, Screen.strategies, Screen.ipsets, Screen.settings) -val hideNavBar = listOf("repo?source={source}", "debugScreen") +val hideNavBar = listOf("repo?source={source}", "debugScreen", "selectionScreen") class MainActivity : ComponentActivity() { private val viewModel: HomeViewModel by viewModels() private lateinit var notificationPermissionLauncher: ActivityResultLauncher @@ -127,7 +128,7 @@ class MainActivity : ComponentActivity() { ) } var showWelcomeDialog by remember { mutableStateOf(sharedPreferences.getBoolean("welcome_dialog", true)) } - firebaseAnalytics.setAnalyticsCollectionEnabled(sharedPreferences.getBoolean("send_firebase_analytics", true)) + firebaseAnalytics.setAnalyticsCollectionEnabled(sharedPreferences.getBoolean("send_firebase_analytics", BuildConfig.send_firebase_analytics)) BottomBar() if (showStoragePermissionDialog) { PermissionDialog( @@ -239,6 +240,7 @@ class MainActivity : ComponentActivity() { } } composable("debugScreen") { DebugScreen(navController) } + composable("selectionScreen") { StrategySelectionScreen(navController) } } } } diff --git a/app/src/main/java/com/cherret/zaprett/byedpi/ByeDpiVpnService.kt b/app/src/main/java/com/cherret/zaprett/byedpi/ByeDpiVpnService.kt index c7f7923..55a27eb 100644 --- a/app/src/main/java/com/cherret/zaprett/byedpi/ByeDpiVpnService.kt +++ b/app/src/main/java/com/cherret/zaprett/byedpi/ByeDpiVpnService.kt @@ -15,6 +15,7 @@ import com.cherret.zaprett.MainActivity import com.cherret.zaprett.R import com.cherret.zaprett.data.ServiceStatus import com.cherret.zaprett.utils.disableList +import com.cherret.zaprett.utils.getActiveByeDPIStrategyContent import com.cherret.zaprett.utils.getActiveExcludeIpsets import com.cherret.zaprett.utils.getActiveExcludeLists import com.cherret.zaprett.utils.getActiveIpsets @@ -191,7 +192,13 @@ class ByeDpiVpnService : VpnService() { val listSet = if (getHostListMode(sharedPreferences) == "whitelist") getActiveLists(sharedPreferences) else getActiveExcludeLists(sharedPreferences) val ipsetSet = if (getHostListMode(sharedPreferences) == "whitelist") getActiveIpsets(sharedPreferences) else getActiveExcludeIpsets(sharedPreferences) CoroutineScope(Dispatchers.IO).launch { - val args = parseArgs(socksIp, socksPort, getActiveStrategy(sharedPreferences), prepareList(listSet), prepareIpset(ipsetSet), sharedPreferences) + val args = parseArgs( + socksIp, + socksPort, + getActiveByeDPIStrategyContent(sharedPreferences), + prepareList(listSet), + prepareIpset(ipsetSet), + sharedPreferences) val result = NativeBridge().jniStartProxy(args) if (result < 0) { Log.d("proxy","Failed to start byedpi proxy") diff --git a/app/src/main/java/com/cherret/zaprett/data/StrategyCheckResult.kt b/app/src/main/java/com/cherret/zaprett/data/StrategyCheckResult.kt new file mode 100644 index 0000000..78016b7 --- /dev/null +++ b/app/src/main/java/com/cherret/zaprett/data/StrategyCheckResult.kt @@ -0,0 +1,7 @@ +package com.cherret.zaprett.data + +data class StrategyCheckResult ( + val path : String, + val progress : Float, + val status : Int +) \ No newline at end of file diff --git a/app/src/main/java/com/cherret/zaprett/ui/component/ListItems.kt b/app/src/main/java/com/cherret/zaprett/ui/component/ListItems.kt index 9fb60a6..b3d259e 100644 --- a/app/src/main/java/com/cherret/zaprett/ui/component/ListItems.kt +++ b/app/src/main/java/com/cherret/zaprett/ui/component/ListItems.kt @@ -1,5 +1,7 @@ package com.cherret.zaprett.ui.component +import android.content.Context +import android.content.SharedPreferences import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row @@ -7,26 +9,37 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Check import androidx.compose.material.icons.filled.Delete import androidx.compose.material.icons.filled.InstallMobile import androidx.compose.material.icons.filled.Update import androidx.compose.material3.CardDefaults import androidx.compose.material3.ElevatedCard import androidx.compose.material3.FilledTonalButton +import androidx.compose.material3.FilledTonalIconButton import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon +import androidx.compose.material3.LinearProgressIndicator import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.ProgressIndicatorDefaults +import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.Switch import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp import com.cherret.zaprett.R +import com.cherret.zaprett.data.StrategyCheckResult import com.cherret.zaprett.ui.viewmodel.BaseRepoViewModel import com.cherret.zaprett.utils.RepoItemInfo +import com.cherret.zaprett.utils.enableStrategy +import kotlinx.coroutines.launch @Composable fun ListSwitchItem(item: String, isChecked: Boolean, onCheckedChange: (Boolean) -> Unit, onDeleteClick: () -> Unit) { @@ -164,3 +177,80 @@ fun RepoItem( } } } + + +@Composable +fun StrategySelectionItem(strategy : StrategyCheckResult, prefs : SharedPreferences, context : Context, snackbarHostState : SnackbarHostState) { + val scope = rememberCoroutineScope() + ElevatedCard ( + elevation = CardDefaults.cardElevation(defaultElevation = 6.dp), + colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceContainer), + modifier = Modifier + .fillMaxWidth() + .padding(start = 10.dp, end = 10.dp, top = 25.dp, bottom = 0.dp) + ) { + Column ( + Modifier + .fillMaxWidth() + .padding(16.dp) + ) + { + Row { + Text( + text = strategy.path, + modifier = Modifier + .weight(1f) + ) + FilledTonalIconButton( + onClick = { + enableStrategy(strategy.path, prefs) + scope.launch { + snackbarHostState.showSnackbar( + message = context.getString(R.string.strategy_applied) + ) + } + }, + enabled = strategy.status == R.string.strategy_status_tested + ) { + Icon( + imageVector = Icons.Default.Check, + contentDescription = "apply" + ) + } + } + Row { + Text( + text = stringResource(strategy.status), + modifier = Modifier + .weight(1f), + fontSize = 12.sp, + ) + } + Row ( + modifier = Modifier + .fillMaxWidth() + .padding(top = 8.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.End + ) { + LinearProgressIndicator( + modifier = Modifier + .weight(1f), + + progress = { + strategy.progress + }, + color = ProgressIndicatorDefaults.linearColor, + trackColor = ProgressIndicatorDefaults.linearTrackColor, + strokeCap = ProgressIndicatorDefaults.LinearStrokeCap, + ) + Text( + text = "${(strategy.progress*100).toInt()}%", + modifier = Modifier + .padding(start = 16.dp), + + ) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/cherret/zaprett/ui/screen/SettingsScreen.kt b/app/src/main/java/com/cherret/zaprett/ui/screen/SettingsScreen.kt index e05469d..5f2f888 100644 --- a/app/src/main/java/com/cherret/zaprett/ui/screen/SettingsScreen.kt +++ b/app/src/main/java/com/cherret/zaprett/ui/screen/SettingsScreen.kt @@ -100,8 +100,8 @@ fun SettingsScreen(navController: NavController, viewModel : SettingsViewModel = val useModule = viewModel.useModule.collectAsState() val updateOnBoot = remember { mutableStateOf(sharedPreferences.getBoolean("update_on_boot", true)) } val autoRestart = viewModel.autoRestart.collectAsState() - val autoUpdate = remember { mutableStateOf(sharedPreferences.getBoolean("auto_update", true)) } - val sendFirebaseAnalytics = remember { mutableStateOf(sharedPreferences.getBoolean("send_firebase_analytics", true)) } + val autoUpdate = remember { mutableStateOf(sharedPreferences.getBoolean("auto_update", BuildConfig.auto_update)) } + val sendFirebaseAnalytics = remember { mutableStateOf(sharedPreferences.getBoolean("send_firebase_analytics", BuildConfig.send_firebase_analytics)) } val ipv6 = remember { mutableStateOf(sharedPreferences.getBoolean("ipv6",false)) } val openNoRootDialog = remember { mutableStateOf(false) } val openNoModuleDialog = remember { mutableStateOf(false) } @@ -117,6 +117,7 @@ fun SettingsScreen(navController: NavController, viewModel : SettingsViewModel = val showBlackDialog = remember { mutableStateOf(false) } val showAppsListsSheet = remember { mutableStateOf(false) } val showSystemApps = remember { mutableStateOf(sharedPreferences.getBoolean("show_system_apps", false)) } + val showChangeProbeTimeout = remember { mutableStateOf(false) } val settingsList = listOf( Setting.Section(stringResource(R.string.general_section)), @@ -155,13 +156,6 @@ fun SettingsScreen(navController: NavController, viewModel : SettingsViewModel = editor.putBoolean("send_firebase_analytics", it).apply() } ), - Setting.Toggle( - title = "", - checked = false, - onToggle = { - - } - ), Setting.Action( title = stringResource(R.string.btn_repository_url_lists), onClick = { @@ -183,9 +177,7 @@ fun SettingsScreen(navController: NavController, viewModel : SettingsViewModel = showStrategyRepoUrlDialog.value = true } ), - Setting.Section( - title = stringResource(R.string.shared_section) - ), + Setting.Section(title = stringResource(R.string.shared_section)), Setting.Action( title = stringResource(R.string.btn_applist), onClick = { @@ -204,6 +196,20 @@ fun SettingsScreen(navController: NavController, viewModel : SettingsViewModel = showBlackDialog.value = true } ), + Setting.Section(stringResource(R.string.title_selection)), + Setting.Action( + title = stringResource(R.string.begin_selection), + onClick = { + navController.navigate("selectionScreen") + } + ), + Setting.Action( + title = stringResource(R.string.change_probe_timeout), + onClick = { + textDialogValue.value = sharedPreferences.getLong("probe_timeout", 1000L).toString() + showChangeProbeTimeout.value = true + } + ), Setting.Section(stringResource(R.string.byedpi_section)), Setting.Toggle( title = stringResource(R.string.btn_ipv6), @@ -234,9 +240,7 @@ fun SettingsScreen(navController: NavController, viewModel : SettingsViewModel = showDNSDialog.value = true } ), - Setting.Section( - title = stringResource(R.string.zapret_section) - ), + Setting.Section(title = stringResource(R.string.zapret_section)), Setting.Toggle( title = stringResource(R.string.btn_autorestart), checked = autoRestart.value, @@ -271,6 +275,11 @@ fun SettingsScreen(navController: NavController, viewModel : SettingsViewModel = editor.putString("hosts_repo_url", it).apply() }, onDismiss = { showHostsRepoUrlDialog.value = false }) } + if (showIpsetRepoUrlDialog.value) { + TextDialog(stringResource(R.string.btn_repository_url_ipsets), stringResource(R.string.hint_enter_repository_url_ipsets), textDialogValue.value, onConfirm = { + editor.putString("ipsets_repo_url", it).apply() + }, onDismiss = { showIpsetRepoUrlDialog.value = false }) + } if (showStrategyRepoUrlDialog.value) { TextDialog(stringResource(R.string.btn_repository_url_strategies), stringResource(R.string.hint_enter_repository_url_strategies), textDialogValue.value, onConfirm = { @@ -332,6 +341,12 @@ fun SettingsScreen(navController: NavController, viewModel : SettingsViewModel = ) } + if (showChangeProbeTimeout.value) { + TextDialog(stringResource(R.string.probe_timeout), stringResource(R.string.hint_enter_probe_timeout), textDialogValue.value, onConfirm = { + editor.putLong("probe_timeout", it.toLong()).apply() + }, onDismiss = { showChangeProbeTimeout.value = false }) + } + Scaffold( topBar = { TopAppBar( 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 new file mode 100644 index 0000000..d02c5b7 --- /dev/null +++ b/app/src/main/java/com/cherret/zaprett/ui/screen/StrategySelectionScreen.kt @@ -0,0 +1,164 @@ +package com.cherret.zaprett.ui.screen + +import android.content.Context.MODE_PRIVATE +import android.util.Log +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Scaffold +import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.SnackbarHostState +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.Font +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.sp +import androidx.compose.foundation.lazy.items +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material.icons.filled.Info +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.FilledIconButton +import androidx.compose.material3.FilledTonalButton +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.TextButton +import androidx.compose.runtime.key +import androidx.compose.runtime.mutableStateOf +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource +import androidx.lifecycle.viewModelScope +import androidx.lifecycle.viewmodel.compose.viewModel +import androidx.navigation.NavController +import com.cherret.zaprett.R +import com.cherret.zaprett.ui.component.StrategySelectionItem +import com.cherret.zaprett.ui.viewmodel.StrategySelectionViewModel +import kotlinx.coroutines.launch + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun StrategySelectionScreen(navController: NavController, viewModel : StrategySelectionViewModel = viewModel()){ + val snackbarHostState = remember { SnackbarHostState() } + val strategyStates = viewModel.strategyStates + val context = LocalContext.current + val prefs = context.getSharedPreferences("settings", MODE_PRIVATE) + var showDialog = remember { mutableStateOf(false) } + + if (showDialog.value) { + InfoAlert { showDialog.value = false } + } + + Scaffold( + topBar = { + TopAppBar( + title = { + Text( + text = stringResource(R.string.title_selection), + fontSize = 40.sp, + fontFamily = FontFamily(Font(R.font.unbounded, FontWeight.Normal)) + ) + }, + navigationIcon = { + IconButton(onClick = { navController.popBackStack() }) { + Icon( + imageVector = Icons.AutoMirrored.Filled.ArrowBack, + contentDescription = stringResource(R.string.btn_back) + ) + } + }, + actions = { + IconButton( + onClick = { + showDialog.value = true + } + ) { + Icon( + imageVector = Icons.Default.Info, + contentDescription = "info" + ) + } + }, + windowInsets = WindowInsets(0) + ) + }, + snackbarHost = { SnackbarHost(snackbarHostState) }, + content = { paddingValues -> + LazyColumn ( + modifier = Modifier + .padding(paddingValues) + ) { + item { + Row ( + modifier = Modifier + .fillMaxWidth(), + horizontalArrangement = Arrangement.Center + ) + { + FilledTonalButton( + onClick = { + viewModel.viewModelScope.launch { + snackbarHostState.showSnackbar( + context.getString(R.string.begin_selection_snack) + ) + viewModel.performTest() + } + } + ) { + Text(stringResource(R.string.begin_selection)) + } + } + } + when { + strategyStates.isEmpty() -> { + item { + Box( + modifier = Modifier.fillParentMaxSize(), + contentAlignment = Alignment.Center + ) { + Text( + stringResource(R.string.empty_list), + textAlign = TextAlign.Center + ) + } + } + } + else -> { + items(strategyStates, key = { it.path }) { item -> + StrategySelectionItem(item, prefs, context, snackbarHostState) + } + } + } + } + } + ) +} + +@Composable +fun InfoAlert(onDismiss: () -> Unit) { + AlertDialog( + title = { Text(text = stringResource(R.string.strategy_selection_info_title)) }, + text = { Text(text = stringResource(R.string.strategy_selection_info_msg)) }, + onDismissRequest = onDismiss, + confirmButton = { + TextButton(onClick = onDismiss) { + Text(stringResource(R.string.btn_continue)) + } + } + ) +} + 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 daee709..0102e33 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 @@ -14,6 +14,7 @@ import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf import androidx.core.content.ContextCompat import androidx.lifecycle.AndroidViewModel +import com.cherret.zaprett.BuildConfig import com.cherret.zaprett.R import com.cherret.zaprett.byedpi.ByeDpiVpnService import com.cherret.zaprett.data.ServiceStatus @@ -71,7 +72,7 @@ class HomeViewModel(application: Application) : AndroidViewModel(application) { var showUpdateDialog = mutableStateOf(false) fun checkForUpdate() { - if (prefs.getBoolean("auto_update", true)) { + if (prefs.getBoolean("auto_update", BuildConfig.auto_update)) { getUpdate(prefs) { if (it != null) { downloadUrl.value = it.downloadUrl.toString() diff --git a/app/src/main/java/com/cherret/zaprett/ui/viewmodel/SettingsViewModel.kt b/app/src/main/java/com/cherret/zaprett/ui/viewmodel/SettingsViewModel.kt index e550302..460f818 100644 --- a/app/src/main/java/com/cherret/zaprett/ui/viewmodel/SettingsViewModel.kt +++ b/app/src/main/java/com/cherret/zaprett/ui/viewmodel/SettingsViewModel.kt @@ -42,7 +42,7 @@ class SettingsViewModel(application: Application) : AndroidViewModel(application init { refreshApplications() _useModule.value = context.getSharedPreferences("settings", MODE_PRIVATE).getBoolean("use_module", false) - getStartOnBoot { value -> + getStartOnBoot(prefs) { value -> _autoRestart.value = value } } @@ -155,7 +155,7 @@ class SettingsViewModel(application: Application) : AndroidViewModel(application fun handleAutoRestart(context: Context) { val sharedPreferences = context.getSharedPreferences("settings", MODE_PRIVATE) if (sharedPreferences.getBoolean("use_module", false)) { - setStartOnBoot{ value -> + setStartOnBoot(prefs) { value -> _autoRestart.value = value } } diff --git a/app/src/main/java/com/cherret/zaprett/ui/viewmodel/StrategySelectionViewModel.kt b/app/src/main/java/com/cherret/zaprett/ui/viewmodel/StrategySelectionViewModel.kt new file mode 100644 index 0000000..2a0d185 --- /dev/null +++ b/app/src/main/java/com/cherret/zaprett/ui/viewmodel/StrategySelectionViewModel.kt @@ -0,0 +1,170 @@ +package com.cherret.zaprett.ui.viewmodel + +import android.app.Application +import android.content.Context.MODE_PRIVATE +import android.content.Intent +import android.util.Log +import androidx.compose.runtime.mutableStateListOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.lifecycle.AndroidViewModel +import androidx.lifecycle.viewModelScope +import com.cherret.zaprett.R +import com.cherret.zaprett.byedpi.ByeDpiVpnService +import com.cherret.zaprett.data.ServiceStatus +import com.cherret.zaprett.data.StrategyCheckResult +import com.cherret.zaprett.utils.disableStrategy +import com.cherret.zaprett.utils.enableStrategy +import com.cherret.zaprett.utils.getActiveLists +import com.cherret.zaprett.utils.getActiveStrategy +import com.cherret.zaprett.utils.getAllNfqwsStrategies +import com.cherret.zaprett.utils.getAllStrategies +import com.cherret.zaprett.utils.getStatus +import com.cherret.zaprett.utils.startService +import com.cherret.zaprett.utils.stopService +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import kotlinx.coroutines.withTimeoutOrNull +import okhttp3.OkHttpClient +import okhttp3.Request +import java.io.File +import java.util.concurrent.TimeUnit + +class StrategySelectionViewModel(application: Application) : AndroidViewModel(application) { + val prefs = application.getSharedPreferences("settings", MODE_PRIVATE) + val client = OkHttpClient.Builder() + .callTimeout(prefs.getLong("probe_timeout", 1000L), TimeUnit.MILLISECONDS) + .build() + val context = application + + val strategyStates = mutableStateListOf() + + init { + loadStrategies() + } + + fun loadStrategies() { + val strategyList = getAllStrategies(prefs) + strategyStates.clear() + strategyList.forEach { name -> + strategyStates += StrategyCheckResult( + path = name, + status = R.string.strategy_status_waiting, + progress = 0f + ) + } + } + + suspend fun testDomain(domain : String) : Boolean = withContext(Dispatchers.IO) { + val request = Request.Builder() + .url("https://${domain}") + .build() + + try { + client.newCall(request).execute().use { response -> + response.isSuccessful || (response.code in 300..399) + } + } catch (e: Exception) { + false + } + } + + suspend fun countReachable(urls: List): Float = coroutineScope { + if (urls.isEmpty()) return@coroutineScope 0f + val results: List = urls.map { url -> + async { testDomain(url) } + }.awaitAll() + val successCount = results.count { it } + (successCount.toFloat() / urls.size.toFloat()).coerceIn(0f, 1f) + } + + suspend fun readActiveListsLines(): List = withContext(Dispatchers.IO) { + val result = mutableListOf() + + getActiveLists(prefs).forEach { path -> + runCatching { + File(path).useLines { lines -> + lines.forEach { line -> + result += line + } + } + }.onFailure { + Log.e("Error", "Occured error when creating list for check") + } + } + result + } + suspend fun performTest() { + val targets = readActiveListsLines() + + for (index in strategyStates.indices) { + val current = strategyStates[index] + strategyStates[index] = current.copy(status = R.string.strategy_status_testing) + + enableStrategy(current.path, prefs) + + if (prefs.getBoolean("use_module", false)) { + getStatus { if (it) stopService {} } + startService {} + + try { + val progress = countReachable(targets) + + val old = strategyStates[index] + strategyStates[index] = old.copy( + progress = progress, + status = R.string.strategy_status_tested + ) + } finally { + stopService {} + disableStrategy(current.path, prefs) + } + } + else { + if (ByeDpiVpnService.status == ServiceStatus.Connected) { + context.startService(Intent(context, ByeDpiVpnService::class.java).apply { + action = "STOP_VPN" + }) + delay(300L) + } + context.startService(Intent(context, ByeDpiVpnService::class.java).apply { + action = "START_VPN" + }) + + val connected = withTimeoutOrNull(10_000L) { + while (ByeDpiVpnService.status != ServiceStatus.Connected) { + delay(100L) + } + true + } ?: false + + if (connected) delay(150L) + try { + val progress = countReachable(targets) + + val old = strategyStates[index] + strategyStates[index] = old.copy( + progress = progress, + status = R.string.strategy_status_tested + ) + } finally { + context.startService(Intent(context, ByeDpiVpnService::class.java).apply { + action = "STOP_VPN" + }) + delay(200L) + disableStrategy(current.path, prefs) + } + } + } + + val sorted = strategyStates.sortedByDescending { it.progress } + strategyStates.clear() + strategyStates.addAll(sorted) + } +} \ No newline at end of file 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 81289a9..801395f 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 @@ -8,8 +8,8 @@ import com.cherret.zaprett.byedpi.ByeDpiVpnService import com.cherret.zaprett.data.ServiceStatus import com.cherret.zaprett.utils.disableStrategy import com.cherret.zaprett.utils.enableStrategy -import com.cherret.zaprett.utils.getActiveByeDPIStrategies -import com.cherret.zaprett.utils.getActiveNfqwsStrategies +import com.cherret.zaprett.utils.getActiveByeDPIStrategy +import com.cherret.zaprett.utils.getActiveNfqwsStrategy import com.cherret.zaprett.utils.getAllByeDPIStrategies import com.cherret.zaprett.utils.getAllNfqwsStrategies import com.cherret.zaprett.utils.getStatus @@ -84,10 +84,10 @@ interface StrategyProvider { class NfqwsStrategyProvider : StrategyProvider { override fun getAll() = getAllNfqwsStrategies() - override fun getActive() = getActiveNfqwsStrategies() + override fun getActive() = getActiveNfqwsStrategy() } class ByeDPIStrategyProvider(private val sharedPreferences: SharedPreferences) : StrategyProvider { override fun getAll() = getAllByeDPIStrategies() - override fun getActive() = getActiveByeDPIStrategies(sharedPreferences) + override fun getActive() = getActiveByeDPIStrategy(sharedPreferences) } \ No newline at end of file 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 e723491..478a1a4 100644 --- a/app/src/main/java/com/cherret/zaprett/utils/ZaprettManager.kt +++ b/app/src/main/java/com/cherret/zaprett/utils/ZaprettManager.kt @@ -64,16 +64,20 @@ fun getConfigFile(): File { return File(Environment.getExternalStorageDirectory(), "zaprett/config") } -fun setStartOnBoot(callback: (Boolean) -> Unit) { - Shell.cmd("zaprett autostart").submit { result -> - if (result.out.isNotEmpty() && result.out.toString().contains("true")) callback(true) else callback(false) +fun setStartOnBoot(prefs : SharedPreferences, callback: (Boolean) -> Unit) { + if (prefs.getBoolean("use_module", false)) { + Shell.cmd("zaprett autostart").submit { result -> + if (result.out.isNotEmpty() && result.out.toString().contains("true")) callback(true) else callback(false) + } } } -fun getStartOnBoot(callback: (Boolean) -> Unit) { - Shell.cmd("zaprett get-autostart").submit { result -> - if (result.out.isNotEmpty() && result.out.toString().contains("true")) callback(true) else callback(false) - } +fun getStartOnBoot(prefs : SharedPreferences, callback: (Boolean) -> Unit) { + if (prefs.getBoolean("use_module", false)) { + Shell.cmd("zaprett get-autostart").submit { result -> + if (result.out.isNotEmpty() && result.out.toString().contains("true")) callback(true) else callback(false) + } + } else { callback(false) } } fun getZaprettPath(): String { @@ -140,6 +144,11 @@ fun getAllByeDPIStrategies(): Array { ?: emptyArray() } +fun getAllStrategies(sharedPreferences : SharedPreferences) : Array { + return if (sharedPreferences.getBoolean("use_module", false)) getAllNfqwsStrategies() + else getAllByeDPIStrategies() +} + fun getActiveLists(sharedPreferences: SharedPreferences): Array { if (sharedPreferences.getBoolean("use_module", false)) { @@ -230,7 +239,7 @@ fun getActiveExcludeIpsets(sharedPreferences: SharedPreferences): Array else return emptyArray() } -fun getActiveNfqwsStrategies(): Array { +fun getActiveNfqwsStrategy(): Array { val configFile = File("${getZaprettPath()}/config") if (configFile.exists()) { val props = Properties() @@ -248,7 +257,7 @@ fun getActiveNfqwsStrategies(): Array { return emptyArray() } -fun getActiveByeDPIStrategies(sharedPreferences: SharedPreferences): Array { +fun getActiveByeDPIStrategy(sharedPreferences: SharedPreferences): Array { val path = sharedPreferences.getString("active_strategy", "") if (!path.isNullOrBlank() && File(path).exists()) { return arrayOf(path) @@ -256,7 +265,7 @@ fun getActiveByeDPIStrategies(sharedPreferences: SharedPreferences): Array { +fun getActiveByeDPIStrategyContent(sharedPreferences: SharedPreferences): List { val path = sharedPreferences.getString("active_strategy", "") if (!path.isNullOrBlank() && File(path).exists()) { return File(path).readLines() @@ -264,6 +273,12 @@ fun getActiveStrategy(sharedPreferences: SharedPreferences): List { return emptyList() } +fun getActiveStrategy(sharedPreferences: SharedPreferences): Array { + return if (sharedPreferences.getBoolean("use_module", false)) getActiveNfqwsStrategy() + else getActiveByeDPIStrategy(sharedPreferences) + +} + fun enableList(path: String, sharedPreferences: SharedPreferences) { if (sharedPreferences.getBoolean("use_module", false)) { val configFile = getConfigFile() diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 0fd945b..ef47fe0 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -100,4 +100,18 @@ Лог скопирован Произошла ошибка скачивания, пожалуйста, сообщите о ней разработчикам URL репозитория ipset + Подбор стратегии + URL репозитория ipset + Введите URL репозитория ipset + ожидание... + проверка... + проверка завершена + Стратегия применена + Начать подбор + Начат подбор стратегии. Не закрывайте эту вкладку + Изменить таймаут пробы + Таймаут пробы + Введите таймаут пробы + Информация + "В этом разделе настроек приложения представлен перебор стратегий\n Подбор проходит среди скачанных стратегий, поэтому заранее скачайте из репозитория или добавьте из файловой системы интересующие вас стратегии для сравнения. \n Перед началом так же выберете один или несколько листов доменов на вкладке \"Листы\", затем нажмите на \"Начать подбор\". Не используйте для перебора списки с большим количеством доменов." \ 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 8fd08f8..9fa5412 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -105,4 +105,18 @@ Occurred a file download error, please report to developers Ipset repository URL Ipset + Strategy selection + Ipsets repository url + Enter ipsets repository URL + waiting... + testing... + testing ended + Strategy applied + Begin selection + Strategy selection has begun. Please keep this tab open. + Change probe timeout + Probe timeout + Enter probe timeout + Tip + 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. \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index c73a232..0fa29b1 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,5 +1,5 @@ [versions] -agp = "8.12.0" +agp = "8.13.0" kotlin = "2.2.10" coreKtx = "1.17.0" junit = "4.13.2"