From 1a41ab3b66e57e47437d203352088a8e6293585c Mon Sep 17 00:00:00 2001 From: kari-ts <135075563+kari-ts@users.noreply.github.com> Date: Tue, 20 Aug 2024 15:14:55 -0700 Subject: [PATCH] android: check if other VPN is active (#475) Detect when another VPN is active and launch dialog giving user the option to navigate to settings to disable. Update state string and toggle to require successful VPN preparation To do in a follow-up: monitor VPN connection, and if Tailscale VPN disconnects due to another VPN connecting, update toggle and text Updates tailscale/tailscale#12850 Signed-off-by: kari-ts --- .../java/com/tailscale/ipn/MainActivity.kt | 40 ++++++++++++++++++- .../ipn/ui/viewModel/MainViewModel.kt | 8 ++-- android/src/main/res/values/strings.xml | 7 ++++ 3 files changed, 50 insertions(+), 5 deletions(-) diff --git a/android/src/main/java/com/tailscale/ipn/MainActivity.kt b/android/src/main/java/com/tailscale/ipn/MainActivity.kt index 98b046a..336edd9 100644 --- a/android/src/main/java/com/tailscale/ipn/MainActivity.kt +++ b/android/src/main/java/com/tailscale/ipn/MainActivity.kt @@ -5,12 +5,15 @@ package com.tailscale.ipn import android.annotation.SuppressLint import android.app.Activity +import android.app.AlertDialog import android.content.Context import android.content.Intent import android.content.RestrictionsManager import android.content.pm.ActivityInfo import android.content.res.Configuration.SCREENLAYOUT_SIZE_LARGE import android.content.res.Configuration.SCREENLAYOUT_SIZE_MASK +import android.net.ConnectivityManager +import android.net.NetworkCapabilities import android.os.Bundle import android.provider.Settings import android.util.Log @@ -129,8 +132,13 @@ class MainActivity : ComponentActivity() { vpnViewModel.setVpnPrepared(true) App.get().startVPN() } else { - Log.d("VpnPermission", "VPN permission denied") - vpnViewModel.setVpnPrepared(false) + if (isAnotherVpnActive(this)) { + Log.d("VpnPermission", "Another VPN is likely active") + showOtherVPNConflictDialog() + } else { + Log.d("VpnPermission", "Permission was denied by the user") + viewModel.setVpnPrepared(false) + } } } viewModel.setVpnPermissionLauncher(vpnPermissionLauncher) @@ -285,6 +293,34 @@ class MainActivity : ComponentActivity() { lifecycleScope.launch { Notifier.loginFinished.collect { _ -> loginQRCode.set(null) } } } + private fun showOtherVPNConflictDialog() { + AlertDialog.Builder(this) + .setTitle(R.string.vpn_permission_denied) + .setMessage(R.string.multiple_vpn_explainer) + .setPositiveButton(R.string.go_to_settings) { _, _ -> + // Intent to open the VPN settings + val intent = Intent(Settings.ACTION_VPN_SETTINGS) + startActivity(intent) + } + .setNegativeButton(R.string.cancel, null) + .show() + } + + fun isAnotherVpnActive(context: Context): Boolean { + val connectivityManager = + context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager + + val activeNetwork = connectivityManager.activeNetwork + if (activeNetwork != null) { + val networkCapabilities = connectivityManager.getNetworkCapabilities(activeNetwork) + if (networkCapabilities != null && + networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_VPN)) { + return true + } + } + return false + } + // Returns true if we should render a QR code instead of launching a browser // for login requests private fun useQRCodeLogin(): Boolean { diff --git a/android/src/main/java/com/tailscale/ipn/ui/viewModel/MainViewModel.kt b/android/src/main/java/com/tailscale/ipn/ui/viewModel/MainViewModel.kt index 8bf07c0..e583c1a 100644 --- a/android/src/main/java/com/tailscale/ipn/ui/viewModel/MainViewModel.kt +++ b/android/src/main/java/com/tailscale/ipn/ui/viewModel/MainViewModel.kt @@ -100,8 +100,10 @@ class MainViewModel(private val vpnViewModel: VpnViewModel) : IpnViewModel() { val isOn = when { - currentState == State.Running || currentState == State.Starting -> true - previousState == State.NoState && currentState == State.Starting -> true + prepared && currentState == State.Running || currentState == State.Starting -> + true + previousState == State.NoState && currentState == State.Starting -> + true else -> false } @@ -182,7 +184,7 @@ private fun userStringRes(currentState: State?, previousState: State?, vpnPrepar currentState == State.NeedsMachineAuth -> R.string.needs_machine_auth currentState == State.Stopped -> R.string.stopped currentState == State.Starting -> R.string.starting - currentState == State.Running -> R.string.connected + currentState == State.Running -> if (vpnPrepared) R.string.connected else R.string.placeholder else -> R.string.placeholder } } diff --git a/android/src/main/res/values/strings.xml b/android/src/main/res/values/strings.xml index 1f9a033..6d79df3 100644 --- a/android/src/main/res/values/strings.xml +++ b/android/src/main/res/values/strings.xml @@ -292,4 +292,11 @@ Health warnings No issues found Tailscale is operating normally. + + + VPN permission denied + Only one VPN can be active, and it appears another is already running. Before starting Tailscale, disable the other VPN. + Go to Settings + Cancel +