From a5e9aa83b8b1699844fb51476c6dedb787ababc2 Mon Sep 17 00:00:00 2001 From: Zane Schepke Date: Sun, 28 Jul 2024 22:21:32 -0400 Subject: [PATCH] feat: check for always-on VPN (#289) --- .../wireguardautotunnel/ui/MainActivity.kt | 40 ++++++++++++++++--- .../ui/common/dialog/InfoDialog.kt | 37 +++++++++++++++++ .../ui/screens/main/MainScreen.kt | 30 +++++--------- .../wireguardautotunnel/util/Constants.kt | 2 - app/src/main/res/values/strings.xml | 5 ++- 5 files changed, 86 insertions(+), 28 deletions(-) create mode 100644 app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/dialog/InfoDialog.kt diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/MainActivity.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/MainActivity.kt index c19a882..1beeb5e 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/MainActivity.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/MainActivity.kt @@ -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 -> diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/dialog/InfoDialog.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/dialog/InfoDialog.kt new file mode 100644 index 0000000..15bd482 --- /dev/null +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/dialog/InfoDialog.kt @@ -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() }, + ) +} diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/MainScreen.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/MainScreen.kt index 14eb1ec..ac356ed 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/MainScreen.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/MainScreen.kt @@ -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)) }, ) } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/Constants.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/Constants.kt index afa7c88..a81b0a9 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/Constants.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/Constants.kt @@ -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 = "" diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 9f136bf..ccb18e3 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -177,4 +177,7 @@ WireGuard Invalid tunnel config format Restart on boot - \ No newline at end of file + Permission Denied + Permission to start the VPN has either been explicitly denied or is being blocked by the system. + If VPN permission is being blocked by the system, please confirm no other app is using the Always-on VPN feature and try again. +