feat: check for always-on VPN (#289)
This commit is contained in:
parent
5a77661fb3
commit
a5e9aa83b8
|
@ -10,6 +10,8 @@ import androidx.activity.enableEdgeToEdge
|
|||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.compose.foundation.focusable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
|
@ -19,15 +21,19 @@ import androidx.compose.material3.SnackbarDuration
|
|||
import androidx.compose.material3.SnackbarHost
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.material3.SnackbarResult
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.surfaceColorAtElevation
|
||||
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.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusProperties
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.toArgb
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
|
@ -41,11 +47,13 @@ import androidx.navigation.navArgument
|
|||
import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
||||
import com.google.accompanist.permissions.isGranted
|
||||
import com.google.accompanist.permissions.rememberPermissionState
|
||||
import com.google.accompanist.permissions.shouldShowRationale
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
|
||||
import com.zaneschepke.wireguardautotunnel.data.repository.AppStateRepository
|
||||
import com.zaneschepke.wireguardautotunnel.data.repository.SettingsRepository
|
||||
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.dialog.InfoDialog
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.BottomNavBar
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.prompt.CustomSnackBar
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.config.ConfigScreen
|
||||
|
@ -97,6 +105,7 @@ class MainActivity : AppCompatActivity() {
|
|||
val appUiState by appViewModel.appUiState.collectAsStateWithLifecycle()
|
||||
val navController = rememberNavController()
|
||||
val navBackStackEntry by navController.currentBackStackEntryAsState()
|
||||
var showVpnPermissionDialog by remember { mutableStateOf(false) }
|
||||
|
||||
val notificationPermissionState =
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
|
@ -114,6 +123,8 @@ class MainActivity : AppCompatActivity() {
|
|||
val accepted = (it.resultCode == RESULT_OK)
|
||||
if (accepted) {
|
||||
appViewModel.onVpnPermissionAccepted()
|
||||
} else {
|
||||
showVpnPermissionDialog = true
|
||||
}
|
||||
},
|
||||
)
|
||||
|
@ -140,17 +151,21 @@ class MainActivity : AppCompatActivity() {
|
|||
appViewModel.permissionsRequested()
|
||||
if (notificationPermissionState != null && !notificationPermissionState.status.isGranted
|
||||
) {
|
||||
showSnackBarMessage(
|
||||
StringValue.StringResource(R.string.notification_permission_required),
|
||||
)
|
||||
return@LaunchedEffect notificationPermissionState.launchPermissionRequest()
|
||||
notificationPermissionState.launchPermissionRequest()
|
||||
return@LaunchedEffect if (notificationPermissionState.status.shouldShowRationale || !notificationPermissionState.status.isGranted) {
|
||||
showSnackBarMessage(
|
||||
StringValue.StringResource(R.string.notification_permission_required),
|
||||
)
|
||||
} else {
|
||||
Unit
|
||||
}
|
||||
}
|
||||
if (!appUiState.vpnPermissionAccepted) {
|
||||
return@LaunchedEffect appViewModel.vpnIntent?.let {
|
||||
vpnActivityResultState.launch(
|
||||
it,
|
||||
)
|
||||
}!!
|
||||
} ?: Unit
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -171,6 +186,21 @@ class MainActivity : AppCompatActivity() {
|
|||
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
|
||||
if (showVpnPermissionDialog) {
|
||||
InfoDialog(
|
||||
onDismiss = { showVpnPermissionDialog = false },
|
||||
onAttest = { showVpnPermissionDialog = false },
|
||||
title = { Text(text = stringResource(R.string.vpn_denied_dialog_title)) },
|
||||
body = {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(15.dp)) {
|
||||
Text(text = stringResource(R.string.vpn_denied_dialog_message))
|
||||
Text(text = stringResource(R.string.vpn_denied_dialog_message2))
|
||||
}
|
||||
},
|
||||
confirmText = { Text(text = stringResource(R.string.okay)) },
|
||||
)
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
snackbarHost = {
|
||||
SnackbarHost(snackbarHostState) { snackbarData: SnackbarData ->
|
||||
|
|
|
@ -0,0 +1,37 @@
|
|||
package com.zaneschepke.wireguardautotunnel.ui.common.dialog
|
||||
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
|
||||
@Composable
|
||||
fun InfoDialog(
|
||||
onAttest: () -> Unit,
|
||||
onDismiss: () -> Unit,
|
||||
title: @Composable () -> Unit,
|
||||
body: @Composable () -> Unit,
|
||||
confirmText: @Composable () -> Unit,
|
||||
) {
|
||||
AlertDialog(
|
||||
onDismissRequest = { onDismiss() },
|
||||
confirmButton = {
|
||||
TextButton(
|
||||
onClick = {
|
||||
onAttest()
|
||||
},
|
||||
) {
|
||||
confirmText()
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = { onDismiss() }) {
|
||||
Text(text = stringResource(R.string.cancel))
|
||||
}
|
||||
},
|
||||
title = { title() },
|
||||
text = { body() },
|
||||
)
|
||||
}
|
|
@ -43,7 +43,6 @@ import androidx.compose.material.icons.rounded.Info
|
|||
import androidx.compose.material.icons.rounded.Settings
|
||||
import androidx.compose.material.icons.rounded.Smartphone
|
||||
import androidx.compose.material.icons.rounded.Star
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.FabPosition
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
|
@ -103,6 +102,7 @@ import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelState
|
|||
import com.zaneschepke.wireguardautotunnel.ui.AppViewModel
|
||||
import com.zaneschepke.wireguardautotunnel.ui.Screen
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.RowListItem
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.dialog.InfoDialog
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.screen.LoadingScreen
|
||||
import com.zaneschepke.wireguardautotunnel.ui.theme.corn
|
||||
import com.zaneschepke.wireguardautotunnel.ui.theme.mint
|
||||
|
@ -219,27 +219,17 @@ fun MainScreen(
|
|||
},
|
||||
)
|
||||
|
||||
AnimatedVisibility(showDeleteTunnelAlertDialog) {
|
||||
AlertDialog(
|
||||
onDismissRequest = { showDeleteTunnelAlertDialog = false },
|
||||
confirmButton = {
|
||||
TextButton(
|
||||
onClick = {
|
||||
selectedTunnel?.let { viewModel.onDelete(it, context) }
|
||||
showDeleteTunnelAlertDialog = false
|
||||
selectedTunnel = null
|
||||
},
|
||||
) {
|
||||
Text(text = stringResource(R.string.yes))
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = { showDeleteTunnelAlertDialog = false }) {
|
||||
Text(text = stringResource(R.string.cancel))
|
||||
}
|
||||
if (showDeleteTunnelAlertDialog) {
|
||||
InfoDialog(
|
||||
onDismiss = { showDeleteTunnelAlertDialog = false },
|
||||
onAttest = {
|
||||
selectedTunnel?.let { viewModel.onDelete(it, context) }
|
||||
showDeleteTunnelAlertDialog = false
|
||||
selectedTunnel = null
|
||||
},
|
||||
title = { Text(text = stringResource(R.string.delete_tunnel)) },
|
||||
text = { Text(text = stringResource(R.string.delete_tunnel_message)) },
|
||||
body = { Text(text = stringResource(R.string.delete_tunnel_message)) },
|
||||
confirmText = { Text(text = stringResource(R.string.yes)) },
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -31,8 +31,6 @@ object Constants {
|
|||
const val PING_INTERVAL = 60_000L
|
||||
const val PING_COOLDOWN = PING_INTERVAL * 60 // one hour
|
||||
|
||||
const val ALLOWED_DISPLAY_NAME_LENGTH = 20
|
||||
|
||||
const val TUNNEL_EXTRA_KEY = "tunnelId"
|
||||
|
||||
const val UNREADABLE_SSID = "<unknown ssid>"
|
||||
|
|
|
@ -177,4 +177,7 @@
|
|||
<string name="wireguard" translatable="false">WireGuard</string>
|
||||
<string name="error_file_format">Invalid tunnel config format</string>
|
||||
<string name="restart_at_boot">Restart on boot</string>
|
||||
</resources>
|
||||
<string name="vpn_denied_dialog_title">Permission Denied</string>
|
||||
<string name="vpn_denied_dialog_message">Permission to start the VPN has either been explicitly denied or is being blocked by the system.</string>
|
||||
<string name="vpn_denied_dialog_message2">If VPN permission is being blocked by the system, please confirm no other app is using the Always-on VPN feature and try again.</string>
|
||||
</resources>
|
||||
|
|
Loading…
Reference in New Issue