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 <kari@tailscale.com>
pull/476/head
kari-ts 3 months ago committed by GitHub
parent 10a4350c02
commit 1a41ab3b66
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -5,12 +5,15 @@ package com.tailscale.ipn
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.Activity import android.app.Activity
import android.app.AlertDialog
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.RestrictionsManager import android.content.RestrictionsManager
import android.content.pm.ActivityInfo import android.content.pm.ActivityInfo
import android.content.res.Configuration.SCREENLAYOUT_SIZE_LARGE import android.content.res.Configuration.SCREENLAYOUT_SIZE_LARGE
import android.content.res.Configuration.SCREENLAYOUT_SIZE_MASK import android.content.res.Configuration.SCREENLAYOUT_SIZE_MASK
import android.net.ConnectivityManager
import android.net.NetworkCapabilities
import android.os.Bundle import android.os.Bundle
import android.provider.Settings import android.provider.Settings
import android.util.Log import android.util.Log
@ -129,8 +132,13 @@ class MainActivity : ComponentActivity() {
vpnViewModel.setVpnPrepared(true) vpnViewModel.setVpnPrepared(true)
App.get().startVPN() App.get().startVPN()
} else { } else {
Log.d("VpnPermission", "VPN permission denied") if (isAnotherVpnActive(this)) {
vpnViewModel.setVpnPrepared(false) 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) viewModel.setVpnPermissionLauncher(vpnPermissionLauncher)
@ -285,6 +293,34 @@ class MainActivity : ComponentActivity() {
lifecycleScope.launch { Notifier.loginFinished.collect { _ -> loginQRCode.set(null) } } 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 // Returns true if we should render a QR code instead of launching a browser
// for login requests // for login requests
private fun useQRCodeLogin(): Boolean { private fun useQRCodeLogin(): Boolean {

@ -100,8 +100,10 @@ class MainViewModel(private val vpnViewModel: VpnViewModel) : IpnViewModel() {
val isOn = val isOn =
when { when {
currentState == State.Running || currentState == State.Starting -> true prepared && currentState == State.Running || currentState == State.Starting ->
previousState == State.NoState && currentState == State.Starting -> true true
previousState == State.NoState && currentState == State.Starting ->
true
else -> false else -> false
} }
@ -182,7 +184,7 @@ private fun userStringRes(currentState: State?, previousState: State?, vpnPrepar
currentState == State.NeedsMachineAuth -> R.string.needs_machine_auth currentState == State.NeedsMachineAuth -> R.string.needs_machine_auth
currentState == State.Stopped -> R.string.stopped currentState == State.Stopped -> R.string.stopped
currentState == State.Starting -> R.string.starting 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 else -> R.string.placeholder
} }
} }

@ -292,4 +292,11 @@
<string name="health_warnings">Health warnings</string> <string name="health_warnings">Health warnings</string>
<string name="no_issues_found">No issues found</string> <string name="no_issues_found">No issues found</string>
<string name="tailscale_is_operating_normally">Tailscale is operating normally.</string> <string name="tailscale_is_operating_normally">Tailscale is operating normally.</string>
<!-- Strings for the multiple VPNs dialog -->
<string name="vpn_permission_denied">VPN permission denied</string>
<string name="multiple_vpn_explainer">Only one VPN can be active, and it appears another is already running. Before starting Tailscale, disable the other VPN.</string>
<string name="go_to_settings">Go to Settings</string>
<string name="cancel">Cancel</string>
</resources> </resources>

Loading…
Cancel
Save