9 Commits
2_1 ... 2_2

Author SHA1 Message Date
egor-white
c8c3b6de96 change ver 2025-07-07 12:25:46 +03:00
egor-white
b832be9556 add icon to status card 2025-07-07 12:22:38 +03:00
egor-white
817f3a0e7d move setupProxy checks to HomeViewModel.kt 2025-07-07 11:18:20 +03:00
egor-white
7d25f7e600 fix startVpn crashes 2025-07-06 20:48:35 +03:00
egor-white
8dcb4d1997 fix startVpn crashes 2025-07-06 12:34:39 +03:00
egor-white
ba2cf523bd Merge remote-tracking branch 'origin/main' 2025-07-05 14:35:51 +03:00
egor-white
c268ee8ccd get install unknown apps permission before loading, add module installation check on up startup 2025-07-05 14:30:23 +03:00
egor-white
72905f4180 change navbar icons 2025-07-05 13:19:38 +03:00
egor-white
a7bb250a64 move utils to com.cherret.zaprett.utils 2025-07-05 12:57:44 +03:00
18 changed files with 171 additions and 96 deletions

View File

@@ -15,8 +15,8 @@ android {
applicationId = "com.cherret.zaprett"
minSdk = 30
targetSdk = 35
versionCode = 13
versionName = "2.1"
versionCode = 14
versionName = "2.2"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
ndk {

View File

@@ -45,7 +45,7 @@
</intent-filter>
</activity>
<service
android:name=".QSTileService"
android:name=".utils.QSTileService"
android:exported="true"
android:label="@string/qs_name"
android:icon="@drawable/ic_launcher_monochrome"

View File

@@ -2,6 +2,7 @@ package com.cherret.zaprett
import android.Manifest
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri
@@ -18,9 +19,9 @@ import androidx.activity.viewModels
import androidx.annotation.StringRes
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Dashboard
import androidx.compose.material.icons.filled.Dns
import androidx.compose.material.icons.filled.Home
import androidx.compose.material.icons.filled.Lan
import androidx.compose.material.icons.filled.MultipleStop
import androidx.compose.material.icons.filled.Settings
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Icon
@@ -30,13 +31,13 @@ import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.core.content.ContextCompat
import androidx.core.content.edit
@@ -55,14 +56,15 @@ import com.cherret.zaprett.ui.theme.ZaprettTheme
import com.cherret.zaprett.ui.viewmodel.HomeViewModel
import com.cherret.zaprett.ui.viewmodel.HostRepoViewModel
import com.cherret.zaprett.ui.viewmodel.StrategyRepoViewModel
import com.cherret.zaprett.utils.checkModuleInstallation
import com.google.firebase.Firebase
import com.google.firebase.analytics.FirebaseAnalytics
import com.google.firebase.analytics.analytics
sealed class Screen(val route: String, @StringRes val nameResId: Int, val icon: ImageVector) {
object home : Screen("home", R.string.title_home, Icons.Default.Home)
object hosts : Screen("hosts", R.string.title_hosts, Icons.Default.Dashboard)
object strategies : Screen("strategies", R.string.title_strategies, Icons.Default.Dns)
object hosts : Screen("hosts", R.string.title_hosts, Icons.Default.Lan)
object strategies : Screen("strategies", R.string.title_strategies, Icons.Default.MultipleStop)
object settings : Screen("settings", R.string.title_settings, Icons.Default.Settings)
}
val topLevelRoutes = listOf(Screen.home, Screen.hosts, Screen.strategies, Screen.settings)
@@ -86,6 +88,17 @@ class MainActivity : ComponentActivity() {
setContent {
ZaprettTheme {
val sharedPreferences = remember { getSharedPreferences("settings", MODE_PRIVATE) }
LaunchedEffect(Unit) {
checkModuleInstallation { result ->
if (getSharedPreferences("settings", Context.MODE_PRIVATE).getBoolean("use_module", false) && !result) sharedPreferences.edit {
putBoolean(
"use_module",
false
)
}
}
}
var showStoragePermissionDialog by remember { mutableStateOf(!Environment.isExternalStorageManager()) }
var showNotificationPermissionDialog by remember {
mutableStateOf(
@@ -133,6 +146,7 @@ class MainActivity : ComponentActivity() {
}
}
@Composable
fun BottomBar() {
val navController = rememberNavController()

View File

@@ -14,7 +14,7 @@ import android.widget.Toast
import androidx.core.app.NotificationCompat
import com.cherret.zaprett.MainActivity
import com.cherret.zaprett.R
import com.cherret.zaprett.getActiveStrategy
import com.cherret.zaprett.utils.getActiveStrategy
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@@ -40,6 +40,7 @@ class ByeDpiVpnService : VpnService() {
super.onStartCommand(intent, flags, startId)
return when (intent?.action) {
"START_VPN" -> {
startForeground(NOTIFICATION_ID, createNotification())
setupProxy()
START_STICKY
}
@@ -98,23 +99,14 @@ class ByeDpiVpnService : VpnService() {
}
private fun setupProxy() {
if (getActiveStrategy(sharedPreferences).isNotEmpty()) {
startForeground(NOTIFICATION_ID, createNotification())
try {
startSocksProxy()
startByeDpi()
status = ServiceStatus.Connected
} catch (e: Exception) {
Log.e("proxy", "Failed to start")
status = ServiceStatus.Failed
}
}
else {
Toast.makeText(
this@ByeDpiVpnService,
getString(R.string.toast_no_strategy_selected),
Toast.LENGTH_SHORT
).show()
try {
startSocksProxy()
startByeDpi()
status = ServiceStatus.Connected
} catch (e: Exception) {
Log.e("proxy", "Failed to start")
status = ServiceStatus.Failed
stopSelf()
}
}

View File

@@ -10,9 +10,11 @@ import androidx.compose.animation.expandVertically
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.shrinkVertically
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
@@ -49,6 +51,7 @@ import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.graphics.vector.rememberVectorPainter
@@ -75,6 +78,7 @@ fun HomeScreen(viewModel: HomeViewModel = viewModel(), vpnLauncher: ActivityResu
val scope = rememberCoroutineScope()
val snackbarHostState = remember { SnackbarHostState() }
val cardText = viewModel.cardText
val cardIcon = viewModel.cardIcon;
val changeLog = viewModel.changeLog
val newVersion = viewModel.newVersion
val updateAvailable = viewModel.updateAvailable
@@ -119,7 +123,7 @@ fun HomeScreen(viewModel: HomeViewModel = viewModel(), vpnLauncher: ActivityResu
Column(modifier = Modifier
.padding(paddingValues)
.verticalScroll(rememberScrollState())) {
ServiceStatusCard(viewModel, cardText, snackbarHostState, scope)
ServiceStatusCard(viewModel, cardText, cardIcon, snackbarHostState, scope)
UpdateCard(updateAvailable) { viewModel.showUpdateDialog() }
if (showUpdateDialog) {
UpdateDialog(viewModel, changeLog.value.orEmpty(), newVersion) { viewModel.dismissUpdateDialog() }
@@ -137,25 +141,40 @@ fun HomeScreen(viewModel: HomeViewModel = viewModel(), vpnLauncher: ActivityResu
}
@Composable
private fun ServiceStatusCard(viewModel: HomeViewModel, cardText: MutableState<Int>, snackbarHostState: SnackbarHostState, scope: CoroutineScope) {
private fun ServiceStatusCard(viewModel: HomeViewModel, cardText: MutableState<Int>, cardIcon : MutableState<ImageVector>, snackbarHostState: SnackbarHostState, scope: CoroutineScope) {
ElevatedCard(
elevation = CardDefaults.cardElevation(6.dp),
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.primary),
modifier = Modifier
.fillMaxWidth()
.padding(start = 10.dp, top = 25.dp, end = 10.dp)
.width(240.dp)
.height(150.dp),
onClick = { viewModel.onCardClick() }
) {
Text(
text = stringResource(cardText.value),
fontFamily = FontFamily(Font(R.font.unbounded, FontWeight.Normal)),
Row (
modifier = Modifier
.fillMaxWidth()
.fillMaxSize()
.padding(16.dp),
textAlign = TextAlign.Center
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween
)
{
Icon(
painter = rememberVectorPainter(cardIcon.value),
modifier = Modifier
.width(60.dp)
.height(60.dp),
contentDescription = "icon"
)
Text(
text = stringResource(cardText.value),
fontFamily = FontFamily(Font(R.font.unbounded, FontWeight.Normal)),
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
textAlign = TextAlign.Center
)
}
}
}

View File

@@ -25,10 +25,11 @@ import com.cherret.zaprett.BuildConfig
import com.cherret.zaprett.byedpi.ByeDpiVpnService
import com.cherret.zaprett.R
import com.cherret.zaprett.byedpi.ServiceStatus
import com.cherret.zaprett.checkModuleInstallation
import com.cherret.zaprett.checkRoot
import com.cherret.zaprett.getStartOnBoot
import com.cherret.zaprett.setStartOnBoot
import com.cherret.zaprett.utils.checkModuleInstallation
import com.cherret.zaprett.utils.checkRoot
import com.cherret.zaprett.utils.getStartOnBoot
import com.cherret.zaprett.utils.setStartOnBoot
import com.cherret.zaprett.utils.stopService
@OptIn(ExperimentalMaterial3Api::class)
@Composable
@@ -66,7 +67,10 @@ fun SettingsScreen() {
openNoRootDialog = openNoRootDialog,
openNoModuleDialog = openNoModuleDialog
) { success ->
if (success) useModule.value = isChecked
if (success) {
useModule.value = isChecked
if (!isChecked) stopService { }
}
}
}
),

View File

@@ -13,8 +13,8 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.AndroidViewModel
import com.cherret.zaprett.R
import com.cherret.zaprett.getZaprettPath
import com.cherret.zaprett.restartService
import com.cherret.zaprett.utils.getZaprettPath
import com.cherret.zaprett.utils.restartService
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import java.io.File

View File

@@ -9,13 +9,13 @@ import androidx.compose.runtime.mutableStateMapOf
import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.viewModelScope
import com.cherret.zaprett.RepoItemInfo
import com.cherret.zaprett.utils.RepoItemInfo
import com.cherret.zaprett.R
import com.cherret.zaprett.download
import com.cherret.zaprett.getFileSha256
import com.cherret.zaprett.getZaprettPath
import com.cherret.zaprett.registerDownloadListenerHost
import com.cherret.zaprett.restartService
import com.cherret.zaprett.utils.download
import com.cherret.zaprett.utils.getFileSha256
import com.cherret.zaprett.utils.getZaprettPath
import com.cherret.zaprett.utils.registerDownloadListenerHost
import com.cherret.zaprett.utils.restartService
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import java.io.File

View File

@@ -3,6 +3,12 @@ package com.cherret.zaprett.ui.viewmodel
import android.app.Application
import android.content.Context
import android.content.Intent
import android.widget.Toast
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.Help
import androidx.compose.material.icons.filled.Cancel
import androidx.compose.material.icons.filled.CheckCircle
import androidx.compose.material3.Icon
import androidx.compose.material3.SnackbarHostState
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
@@ -11,17 +17,18 @@ import androidx.lifecycle.AndroidViewModel
import com.cherret.zaprett.byedpi.ByeDpiVpnService
import com.cherret.zaprett.R
import com.cherret.zaprett.byedpi.ServiceStatus
import com.cherret.zaprett.download
import com.cherret.zaprett.getBinVersion
import com.cherret.zaprett.getChangelog
import com.cherret.zaprett.getModuleVersion
import com.cherret.zaprett.getStatus
import com.cherret.zaprett.getUpdate
import com.cherret.zaprett.installApk
import com.cherret.zaprett.registerDownloadListener
import com.cherret.zaprett.restartService
import com.cherret.zaprett.startService
import com.cherret.zaprett.stopService
import com.cherret.zaprett.utils.download
import com.cherret.zaprett.utils.getActiveStrategy
import com.cherret.zaprett.utils.getBinVersion
import com.cherret.zaprett.utils.getChangelog
import com.cherret.zaprett.utils.getModuleVersion
import com.cherret.zaprett.utils.getStatus
import com.cherret.zaprett.utils.getUpdate
import com.cherret.zaprett.utils.installApk
import com.cherret.zaprett.utils.registerDownloadListener
import com.cherret.zaprett.utils.restartService
import com.cherret.zaprett.utils.startService
import com.cherret.zaprett.utils.stopService
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
@@ -34,6 +41,8 @@ class HomeViewModel(application: Application) : AndroidViewModel(application) {
val requestVpnPermission = _requestVpnPermission.asStateFlow()
var cardText = mutableIntStateOf(R.string.status_not_availible) // MVP temporarily(maybe)
private set
var cardIcon = mutableStateOf(Icons.AutoMirrored.Filled.Help)
private set
var moduleVer = mutableStateOf(context.getString(R.string.unknown_text))
private set
@@ -77,21 +86,49 @@ class HomeViewModel(application: Application) : AndroidViewModel(application) {
fun checkServiceStatus() {
if (prefs.getBoolean("use_module", false) && prefs.getBoolean("update_on_boot", false)) {
getStatus { isEnabled ->
cardText.intValue = if (isEnabled) R.string.status_enabled else R.string.status_disabled
if (isEnabled){
cardText.value = R.string.status_enabled
cardIcon.value = Icons.Filled.CheckCircle
}
else {
cardText.value = R.string.status_disabled
cardIcon.value = Icons.Filled.Cancel
}
}
}
else {
cardText.value = if (ByeDpiVpnService.status == ServiceStatus.Connected) R.string.status_enabled else R.string.status_disabled
if (ByeDpiVpnService.status == ServiceStatus.Connected){
cardText.value = R.string.status_enabled
cardIcon.value = Icons.Filled.CheckCircle
}
else {
cardText.value = R.string.status_disabled
cardIcon.value = Icons.Filled.Cancel
}
}
}
fun onCardClick() {
if (prefs.getBoolean("use_module", false)) {
getStatus { isEnabled ->
cardText.value = if (isEnabled) R.string.status_enabled else R.string.status_disabled
if (isEnabled){
cardText.value = R.string.status_enabled
cardIcon.value = Icons.Filled.CheckCircle
}
else {
cardText.value = R.string.status_disabled
cardIcon.value = Icons.Filled.Cancel
}
}
} else {
cardText.value = if (ByeDpiVpnService.status == ServiceStatus.Connected) R.string.status_enabled else R.string.status_disabled
if (ByeDpiVpnService.status == ServiceStatus.Connected){
cardText.value = R.string.status_enabled
cardIcon.value = Icons.Filled.CheckCircle
}
else {
cardText.value = R.string.status_disabled
cardIcon.value = Icons.Filled.Cancel
}
}
}
@@ -113,10 +150,19 @@ class HomeViewModel(application: Application) : AndroidViewModel(application) {
}
} else {
if (ByeDpiVpnService.status == ServiceStatus.Disconnected || ByeDpiVpnService.status == ServiceStatus.Failed) {
scope.launch {
snackbarHostState.showSnackbar(context.getString(R.string.snack_starting_service))
if (getActiveStrategy(prefs).isNotEmpty()) {
scope.launch {
snackbarHostState.showSnackbar(context.getString(R.string.snack_starting_service))
}
_requestVpnPermission.value = true
}
else {
Toast.makeText(
context,
context.getString(R.string.toast_no_strategy_selected),
Toast.LENGTH_SHORT
).show()
}
_requestVpnPermission.value = true
}
else {
scope.launch {

View File

@@ -1,16 +1,9 @@
package com.cherret.zaprett.ui.viewmodel
import android.app.Application
import androidx.lifecycle.viewModelScope
import com.cherret.zaprett.RepoItemInfo
import com.cherret.zaprett.download
import com.cherret.zaprett.getAllLists
import com.cherret.zaprett.getHostList
import com.cherret.zaprett.getZaprettPath
import com.cherret.zaprett.registerDownloadListenerHost
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import java.io.File
import com.cherret.zaprett.utils.RepoItemInfo
import com.cherret.zaprett.utils.getAllLists
import com.cherret.zaprett.utils.getHostList
class HostRepoViewModel(application: Application): BaseRepoViewModel(application) {
override fun getInstalledLists(): Array<String> = getAllLists()

View File

@@ -3,11 +3,11 @@ package com.cherret.zaprett.ui.viewmodel
import android.app.Application
import android.content.Context
import androidx.compose.material3.SnackbarHostState
import com.cherret.zaprett.disableList
import com.cherret.zaprett.enableList
import com.cherret.zaprett.getActiveLists
import com.cherret.zaprett.getAllLists
import com.cherret.zaprett.getStatus
import com.cherret.zaprett.utils.disableList
import com.cherret.zaprett.utils.enableList
import com.cherret.zaprett.utils.getActiveLists
import com.cherret.zaprett.utils.getAllLists
import com.cherret.zaprett.utils.getStatus
import kotlinx.coroutines.CoroutineScope
import java.io.File

View File

@@ -1,10 +1,10 @@
package com.cherret.zaprett.ui.viewmodel
import android.app.Application
import com.cherret.zaprett.RepoItemInfo
import com.cherret.zaprett.getAllByeDPIStrategies
import com.cherret.zaprett.getAllNfqwsStrategies
import com.cherret.zaprett.getStrategiesList
import com.cherret.zaprett.utils.RepoItemInfo
import com.cherret.zaprett.utils.getAllByeDPIStrategies
import com.cherret.zaprett.utils.getAllNfqwsStrategies
import com.cherret.zaprett.utils.getStrategiesList
class StrategyRepoViewModel(application: Application): BaseRepoViewModel(application) {
override fun getInstalledLists(): Array<String> =

View File

@@ -6,13 +6,13 @@ import android.content.SharedPreferences
import androidx.compose.material3.SnackbarHostState
import com.cherret.zaprett.byedpi.ByeDpiVpnService
import com.cherret.zaprett.byedpi.ServiceStatus
import com.cherret.zaprett.disableStrategy
import com.cherret.zaprett.enableStrategy
import com.cherret.zaprett.getActiveByeDPIStrategies
import com.cherret.zaprett.getActiveNfqwsStrategies
import com.cherret.zaprett.getAllByeDPIStrategies
import com.cherret.zaprett.getAllNfqwsStrategies
import com.cherret.zaprett.getStatus
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.getAllByeDPIStrategies
import com.cherret.zaprett.utils.getAllNfqwsStrategies
import com.cherret.zaprett.utils.getStatus
import kotlinx.coroutines.CoroutineScope
import java.io.File

View File

@@ -1,4 +1,4 @@
package com.cherret.zaprett
package com.cherret.zaprett.utils
import android.content.SharedPreferences
import android.os.Environment

View File

@@ -1,7 +1,8 @@
package com.cherret.zaprett
package com.cherret.zaprett.utils
import android.service.quicksettings.Tile
import android.service.quicksettings.TileService
import com.cherret.zaprett.R
class QSTileService: TileService() {
override fun onTileAdded() {

View File

@@ -1,4 +1,4 @@
package com.cherret.zaprett
package com.cherret.zaprett.utils
import android.annotation.SuppressLint
import android.app.DownloadManager

View File

@@ -1,4 +1,4 @@
package com.cherret.zaprett
package com.cherret.zaprett.utils
import android.annotation.SuppressLint
import android.app.DownloadManager
@@ -13,6 +13,7 @@ import android.provider.Settings
import androidx.core.content.ContextCompat
import androidx.core.content.FileProvider
import androidx.core.net.toUri
import com.cherret.zaprett.BuildConfig
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import okhttp3.Call
@@ -101,6 +102,11 @@ fun installApk(context: Context, uri: Uri) {
}
fun registerDownloadListener(context: Context, downloadId: Long, onDownloaded: (Uri) -> Unit) {// AI Generated
if (!context.packageManager.canRequestPackageInstalls()){
val packageUri = Uri.fromParts("package", context.packageName, null)
val intent = Intent(Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES, packageUri)
context.startActivity(intent)
}
val receiver = object : BroadcastReceiver() {
@SuppressLint("Range")
override fun onReceive(context: Context?, intent: Intent?) {