diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/MainActivity.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/MainActivity.kt index b973efc0..19e81fba 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/MainActivity.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/MainActivity.kt @@ -1,12 +1,14 @@ package com.zaneschepke.wireguardautotunnel import ProxySettingsScreen +import android.Manifest import android.content.Intent import android.graphics.Color import android.net.Uri import android.net.VpnService import android.os.Build import android.os.Bundle +import android.provider.Settings import android.view.WindowManager import androidx.activity.SystemBarStyle import androidx.activity.compose.rememberLauncherForActivityResult @@ -50,6 +52,7 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -61,6 +64,7 @@ import androidx.compose.ui.text.intl.Locale import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.dp import androidx.compose.ui.zIndex +import androidx.core.app.ActivityCompat import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen import androidx.lifecycle.lifecycleScope import androidx.lifecycle.viewmodel.navigation3.rememberViewModelStoreNavEntryDecorator @@ -83,6 +87,7 @@ import com.zaneschepke.wireguardautotunnel.ui.LocalIsAndroidTV import com.zaneschepke.wireguardautotunnel.ui.LocalNavController import com.zaneschepke.wireguardautotunnel.ui.common.banner.AppAlertBanner import com.zaneschepke.wireguardautotunnel.ui.common.dialog.InfoDialog +import com.zaneschepke.wireguardautotunnel.ui.common.dialog.LocalNetworkPermissionDialog import com.zaneschepke.wireguardautotunnel.ui.common.dialog.VpnDeniedDialog import com.zaneschepke.wireguardautotunnel.ui.navigation.Route import com.zaneschepke.wireguardautotunnel.ui.navigation.SecureRoute @@ -132,6 +137,7 @@ import com.zaneschepke.wireguardautotunnel.util.extensions.installApk import com.zaneschepke.wireguardautotunnel.util.extensions.isRunningOnTv import com.zaneschepke.wireguardautotunnel.util.extensions.openWebUrl import com.zaneschepke.wireguardautotunnel.util.extensions.restartApp +import com.zaneschepke.wireguardautotunnel.util.permission.LocalNetworkPermissionHelper import com.zaneschepke.wireguardautotunnel.viewmodel.ConfigEditViewModel import com.zaneschepke.wireguardautotunnel.viewmodel.SharedAppViewModel import com.zaneschepke.wireguardautotunnel.viewmodel.SplitTunnelViewModel @@ -202,6 +208,68 @@ class MainActivity : AppCompatActivity() { var requestingTunnelMode by remember { mutableStateOf>(Pair(null, null)) } + var showLocalNetworkRationale by remember { mutableStateOf(false) } + var hasPromptedLocalNetwork by rememberSaveable { mutableStateOf(false) } + + val localNetworkPermissionLauncher = + rememberLauncherForActivityResult( + contract = ActivityResultContracts.RequestPermission() + ) { isGranted -> + if (!isGranted) { + val canAskAgain = + ActivityCompat.shouldShowRequestPermissionRationale( + this, + Manifest.permission.ACCESS_LOCAL_NETWORK, + ) + + if (!canAskAgain) { + val intent = + Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply { + data = Uri.fromParts("package", packageName, null) + } + startActivity(intent) + } else { + toaster.show( + message = + context.getString(R.string.local_network_permission_denied), + type = ToastType.Warning, + duration = 6000.milliseconds, + ) + } + } + } + + LaunchedEffect(uiState.isAppLoaded) { + if ( + uiState.isAppLoaded && + !hasPromptedLocalNetwork && + LocalNetworkPermissionHelper.shouldRequestPermission() && + !LocalNetworkPermissionHelper.isPermissionGranted(context) + ) { + hasPromptedLocalNetwork = true + showLocalNetworkRationale = true + } + } + + if (showLocalNetworkRationale) { + LocalNetworkPermissionDialog( + onDismiss = { + showLocalNetworkRationale = false + toaster.show( + message = context.getString(R.string.local_network_permission_denied), + type = ToastType.Warning, + duration = 6000.milliseconds, + ) + }, + onContinue = { + showLocalNetworkRationale = false + + localNetworkPermissionLauncher.launch( + Manifest.permission.ACCESS_LOCAL_NETWORK + ) + }, + ) + } val startingStack = buildList { add(Route.Tunnels) diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/dialog/LocalNetworkPermissionDialog.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/dialog/LocalNetworkPermissionDialog.kt new file mode 100644 index 00000000..0fba1beb --- /dev/null +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/dialog/LocalNetworkPermissionDialog.kt @@ -0,0 +1,75 @@ +package com.zaneschepke.wireguardautotunnel.ui.common.dialog + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.height +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import com.zaneschepke.wireguardautotunnel.R + +@Composable +fun LocalNetworkPermissionDialog(onDismiss: () -> Unit, onContinue: () -> Unit) { + AlertDialog( + onDismissRequest = onDismiss, + title = { Text(text = stringResource(R.string.local_network_permission_title)) }, + text = { + Column { + Text( + text = stringResource(R.string.local_network_permission_intro), + style = MaterialTheme.typography.bodyMedium, + ) + + Spacer(modifier = Modifier.height(16.dp)) + + Text( + text = stringResource(R.string.local_network_permission_issues_intro), + style = MaterialTheme.typography.bodyMedium, + fontWeight = FontWeight.Medium, + ) + + Spacer(modifier = Modifier.height(4.dp)) + + Text( + text = stringResource(R.string.local_network_permission_feature_tunnels), + style = MaterialTheme.typography.bodyMedium, + ) + Text( + text = stringResource(R.string.local_network_permission_feature_autotunnel), + style = MaterialTheme.typography.bodyMedium, + ) + Text( + text = stringResource(R.string.local_network_permission_feature_proxy), + style = MaterialTheme.typography.bodyMedium, + ) + + Spacer(modifier = Modifier.height(16.dp)) + + Text( + text = stringResource(R.string.local_network_permission_nearby_devices), + style = MaterialTheme.typography.bodyMedium, + ) + + Spacer(modifier = Modifier.height(16.dp)) + + Text( + text = stringResource(R.string.local_network_permission_recommendation), + style = MaterialTheme.typography.bodyMedium, + fontWeight = FontWeight.Medium, + ) + } + }, + confirmButton = { + TextButton(onClick = onContinue) { Text(text = stringResource(R.string._continue)) } + }, + dismissButton = { + TextButton(onClick = onDismiss) { Text(text = stringResource(R.string.not_now)) } + }, + ) +} diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/permission/LocalNetworkPermissionHelper.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/permission/LocalNetworkPermissionHelper.kt new file mode 100644 index 00000000..b6aee011 --- /dev/null +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/permission/LocalNetworkPermissionHelper.kt @@ -0,0 +1,23 @@ +package com.zaneschepke.wireguardautotunnel.util.permission + +import android.Manifest +import android.content.Context +import android.content.pm.PackageManager +import android.os.Build +import androidx.core.content.ContextCompat + +object LocalNetworkPermissionHelper { + + fun shouldRequestPermission(): Boolean { + return Build.VERSION.SDK_INT >= Build.VERSION_CODES.CINNAMON_BUN + } + + fun isPermissionGranted(context: Context): Boolean { + return if (shouldRequestPermission()) { + ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_LOCAL_NETWORK) == + PackageManager.PERMISSION_GRANTED + } else { + true + } + } +} diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 7aab8c66..9faa392f 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -537,4 +537,21 @@ Restore failed. Wrong password Restore failed. Select a valid backup file (.sqlite3 or .sqlite3.aes) This link returned an invalid config file. Make sure you are using a direct download link + + Local Network Access Needed + + WG Tunnel needs access to your local network for several features to work properly. + + Without this permission, you may experience issues with: + + - Connecting to certain tunnels + - Auto-tunneling and split tunneling features + - Local proxy and bypass functionality + + Granting this permission is strongly recommended. + Note: Android labels this permission as “nearby devices”. + + Not now + + Local network access denied. Some features may not work properly diff --git a/build.gradle.kts b/build.gradle.kts index fc65eb44..c0bb68ca 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,5 +1,4 @@ import com.ncorti.ktfmt.gradle.tasks.KtfmtFormatTask -import org.jetbrains.kotlin.gradle.dsl.JvmTarget plugins { alias(libs.plugins.android.application) apply false diff --git a/tunnel/src/main/AndroidManifest.xml b/tunnel/src/main/AndroidManifest.xml index 2abcf780..c214a26b 100644 --- a/tunnel/src/main/AndroidManifest.xml +++ b/tunnel/src/main/AndroidManifest.xml @@ -1,6 +1,7 @@ +