diff --git a/README.md b/README.md index 12dc1ee..a29bb70 100644 --- a/README.md +++ b/README.md @@ -29,8 +29,8 @@ This is an alternative Android Application for [WireGuard](https://www.wireguard

- - + +

@@ -44,6 +44,7 @@ The inspiration for this app came from the inconvenience of constantly having to * Add tunnels via .conf file * Auto connect to VPN based on Wi-Fi SSID * Split tunneling by application +* Always-on VPN for Android support * Configurable Trusted Network list * Optional auto connect on mobile data * Automatic service restart after reboot diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 37d6f8d..842779e 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -16,8 +16,8 @@ android { compileSdk = 34 val versionMajor = 2 - val versionMinor = 1 - val versionPatch = 4 + val versionMinor = 2 + val versionPatch = 0 val versionBuild = 0 defaultConfig { diff --git a/app/objectbox-models/default.json b/app/objectbox-models/default.json index d408486..dd7a8ab 100644 --- a/app/objectbox-models/default.json +++ b/app/objectbox-models/default.json @@ -31,7 +31,7 @@ }, { "id": "2:8887605597748372702", - "lastPropertyId": "8:4981008812459251156", + "lastPropertyId": "9:4468844863383145378", "name": "Settings", "properties": [ { @@ -59,6 +59,11 @@ "id": "6:3370284381040192129", "name": "defaultTunnel", "type": 9 + }, + { + "id": "9:4468844863383145378", + "name": "isAlwaysOnVpnEnabled", + "type": 1 } ], "relations": [] diff --git a/app/objectbox-models/default.json.bak b/app/objectbox-models/default.json.bak index 534dc44..d408486 100644 --- a/app/objectbox-models/default.json.bak +++ b/app/objectbox-models/default.json.bak @@ -59,11 +59,6 @@ "id": "6:3370284381040192129", "name": "defaultTunnel", "type": 9 - }, - { - "id": "8:4981008812459251156", - "name": "showProminentDisclosure", - "type": 1 } ], "relations": [] @@ -91,7 +86,8 @@ 7555225587864607050, 969146862000617878, 5057486545428188436, - 2814640993034665120 + 2814640993034665120, + 4981008812459251156 ], "retiredRelationUids": [], "version": 1 diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 898ae9a..c10fa56 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -5,7 +5,8 @@ + android:maxSdkVersion="30" + tools:ignore="LeanbackUsesWifi" /> @@ -19,6 +20,14 @@ + + + + @@ -30,6 +39,7 @@ android:dataExtractionRules="@xml/data_extraction_rules" android:fullBackupContent="@xml/backup_rules" android:icon="@mipmap/ic_launcher" + android:banner="@mipmap/ic_launcher_foreground" android:label="@string/app_name" android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" @@ -42,6 +52,7 @@ + + + + + startService(intent.extras) Action.STOP.name -> stopService(intent.extras) + "android.net.VpnService" -> { + Timber.d("Always-on VPN starting service") + startService(intent.extras) + } else -> Timber.d("This should never happen. No action in the received intent") } } else { diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/WireGuardTunnelService.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/WireGuardTunnelService.kt index 68a2853..26ad186 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/WireGuardTunnelService.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/WireGuardTunnelService.kt @@ -5,9 +5,11 @@ import android.content.Intent import android.os.Bundle import com.zaneschepke.wireguardautotunnel.R import com.zaneschepke.wireguardautotunnel.receiver.NotificationActionReceiver +import com.zaneschepke.wireguardautotunnel.repository.Repository import com.zaneschepke.wireguardautotunnel.service.notification.NotificationService import com.zaneschepke.wireguardautotunnel.service.tunnel.HandshakeStatus import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnService +import com.zaneschepke.wireguardautotunnel.service.tunnel.model.Settings import com.zaneschepke.wireguardautotunnel.service.tunnel.model.TunnelConfig import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.CoroutineScope @@ -26,6 +28,9 @@ class WireGuardTunnelService : ForegroundService() { @Inject lateinit var vpnService : VpnService + @Inject + lateinit var settingsRepo: Repository + @Inject lateinit var notificationService : NotificationService @@ -48,7 +53,16 @@ class WireGuardTunnelService : ForegroundService() { stopService(extras) } } else { - Timber.e("Tunnel config null") + Timber.d("Tunnel config null, starting default tunnel") + val settings = settingsRepo.getAll(); + if(!settings.isNullOrEmpty()) { + val setting = settings[0] + if(setting.defaultTunnel != null && setting.isAlwaysOnVpnEnabled) { + val tunnelConfig = TunnelConfig.from(setting.defaultTunnel!!) + tunnelName = tunnelConfig.name + vpnService.startTunnel(tunnelConfig) + } + } } } CoroutineScope(job).launch { @@ -62,6 +76,7 @@ class WireGuardTunnelService : ForegroundService() { if(!didShowFailedHandshakeNotification) { launchVpnConnectionFailedNotification(getString(R.string.initial_connection_failure_message)) didShowFailedHandshakeNotification = true + didShowConnected = false } } HandshakeStatus.HEALTHY -> { @@ -74,6 +89,7 @@ class WireGuardTunnelService : ForegroundService() { if(!didShowFailedHandshakeNotification) { launchVpnConnectionFailedNotification(getString(R.string.lost_connection_failure_message)) didShowFailedHandshakeNotification = true + didShowConnected = false } } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tunnel/model/Settings.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tunnel/model/Settings.kt index d802e65..742baa2 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tunnel/model/Settings.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tunnel/model/Settings.kt @@ -10,5 +10,6 @@ data class Settings( var isAutoTunnelEnabled : Boolean = false, var isTunnelOnMobileDataEnabled : Boolean = false, var trustedNetworkSSIDs : MutableList = mutableListOf(), - var defaultTunnel : String? = null + var defaultTunnel : String? = null, + var isAlwaysOnVpnEnabled : Boolean = false ) diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/MainViewModel.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/MainViewModel.kt index 1400cf1..e1c151f 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/MainViewModel.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/MainViewModel.kt @@ -86,6 +86,7 @@ class MainViewModel @Inject constructor(private val application : Application, val setting = settings[0] setting.defaultTunnel = null setting.isAutoTunnelEnabled = false + setting.isAlwaysOnVpnEnabled = false settingsRepo.save(setting) } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/SettingsScreen.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/SettingsScreen.kt index b02deee..8feb52e 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/SettingsScreen.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/SettingsScreen.kt @@ -94,6 +94,7 @@ fun SettingsScreen( rememberPermissionState(Manifest.permission.ACCESS_BACKGROUND_LOCATION) var currentText by remember { mutableStateOf("") } val scrollState = rememberScrollState() + var isLocationServicesEnabled by remember { mutableStateOf(viewModel.checkLocationServicesEnabled())} LaunchedEffect(viewState) { if (viewState.showSnackbarMessage) { @@ -176,6 +177,34 @@ fun SettingsScreen( } return } + if(!isLocationServicesEnabled) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center, + modifier = Modifier + .fillMaxSize() + .padding(padding) + ) { + Text( + stringResource(id = R.string.location_services_not_detected), + textAlign = TextAlign.Center, + modifier = Modifier.padding(15.dp), + fontStyle = FontStyle.Italic + ) + Button(onClick = { + val locationServicesEnabled = viewModel.checkLocationServicesEnabled() + isLocationServicesEnabled = locationServicesEnabled + if(!locationServicesEnabled) { + scope.launch { + viewModel.showSnackBarMessage(context.getString(R.string.detecting_location_services_disabled)) + } + } + }) { + Text(stringResource(id = R.string.check_again)) + } + } + return + } Column( horizontalAlignment = Alignment.Start, @@ -197,6 +226,7 @@ fun SettingsScreen( ) { Text(stringResource(R.string.enable_auto_tunnel)) Switch( + enabled = !settings.isAlwaysOnVpnEnabled, checked = settings.isAutoTunnelEnabled, onCheckedChange = { scope.launch { @@ -213,12 +243,12 @@ fun SettingsScreen( ExposedDropdownMenuBox( expanded = expanded, onExpandedChange = { - if(!settings.isAutoTunnelEnabled) { + if(!(settings.isAutoTunnelEnabled || settings.isAlwaysOnVpnEnabled)) { expanded = !expanded }}, modifier = Modifier.padding(start = 15.dp, top = 5.dp, bottom = 10.dp), ) { TextField( - enabled = !settings.isAutoTunnelEnabled, + enabled = !(settings.isAutoTunnelEnabled || settings.isAlwaysOnVpnEnabled), value = settings.defaultTunnel?.let { TunnelConfig.from(it).name } ?: "", @@ -266,11 +296,11 @@ fun SettingsScreen( scope.launch { viewModel.onDeleteTrustedSSID(ssid) } - }, text = ssid, icon = Icons.Filled.Close, enabled = !settings.isAutoTunnelEnabled) + }, text = ssid, icon = Icons.Filled.Close, enabled = !(settings.isAutoTunnelEnabled || settings.isAlwaysOnVpnEnabled)) } } OutlinedTextField( - enabled = !settings.isAutoTunnelEnabled, + enabled = !(settings.isAutoTunnelEnabled || settings.isAlwaysOnVpnEnabled), value = currentText, onValueChange = { currentText = it }, label = { Text(stringResource(R.string.add_trusted_ssid)) }, @@ -306,7 +336,7 @@ fun SettingsScreen( ) { Text(stringResource(R.string.tunnel_mobile_data)) Switch( - enabled = !settings.isAutoTunnelEnabled, + enabled = !(settings.isAutoTunnelEnabled || settings.isAlwaysOnVpnEnabled), checked = settings.isTunnelOnMobileDataEnabled, onCheckedChange = { scope.launch { @@ -315,6 +345,24 @@ fun SettingsScreen( } ) } + Row( + modifier = Modifier + .fillMaxWidth() + .padding(14.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween + ) { + Text(stringResource(R.string.always_on_vpn_support)) + Switch( + enabled = !settings.isAutoTunnelEnabled, + checked = settings.isAlwaysOnVpnEnabled, + onCheckedChange = { + scope.launch { + viewModel.onToggleAlwaysOnVPN() + } + } + ) + } } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/SettingsViewModel.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/SettingsViewModel.kt index cc39543..b8a3505 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/SettingsViewModel.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/SettingsViewModel.kt @@ -1,6 +1,8 @@ package com.zaneschepke.wireguardautotunnel.ui.screens.settings import android.app.Application +import android.content.Context +import android.location.LocationManager import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.zaneschepke.wireguardautotunnel.R @@ -17,6 +19,7 @@ import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch import javax.inject.Inject + @HiltViewModel class SettingsViewModel @Inject constructor(private val application : Application, private val tunnelRepo : Repository, private val settingsRepo : Repository @@ -31,6 +34,7 @@ class SettingsViewModel @Inject constructor(private val application : Applicatio val viewState get() = _viewState.asStateFlow() init { + checkLocationServicesEnabled() viewModelScope.launch { settingsRepo.itemFlow.collect { val settings = it.first() @@ -69,7 +73,7 @@ class SettingsViewModel @Inject constructor(private val application : Applicatio suspend fun toggleAutoTunnel() { if(_settings.value.defaultTunnel.isNullOrEmpty() && !_settings.value.isAutoTunnelEnabled) { - showSnackBarMessage("Please select a tunnel first") + showSnackBarMessage(application.getString(R.string.select_tunnel_message)) return } if(_settings.value.isAutoTunnelEnabled) { @@ -99,8 +103,7 @@ class SettingsViewModel @Inject constructor(private val application : Applicatio } } } - - private suspend fun showSnackBarMessage(message : String) { + suspend fun showSnackBarMessage(message : String) { _viewState.emit(_viewState.value.copy( showSnackbarMessage = true, snackbarMessage = message, @@ -118,4 +121,20 @@ class SettingsViewModel @Inject constructor(private val application : Applicatio showSnackbarMessage = false )) } + + suspend fun onToggleAlwaysOnVPN() { + if(_settings.value.defaultTunnel != null) { + _settings.emit( + _settings.value.copy(isAlwaysOnVpnEnabled = !_settings.value.isAlwaysOnVpnEnabled) + ) + settingsRepo.save(_settings.value) + } else { + showSnackBarMessage(application.getString(R.string.select_tunnel_message)) + } + } + fun checkLocationServicesEnabled() : Boolean { + val locationManager = + application.getSystemService(Context.LOCATION_SERVICE) as LocationManager + return locationManager.isProviderEnabled(LocationManager.NETWORK_PROVIDER) + } } \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 030e873..ddee92f 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -77,4 +77,9 @@ Failed connection to - Attempting to connect to server after 30 seconds of no response. Attempting to reconnect to server after more than one minute of no response. + Enable Always-On VPN support + Please select a tunnel first + Unable to detect Location Services which are required for this feature. Please enable Location Services. + Check again + Detecting Location Services disabled \ No newline at end of file diff --git a/asset/settings_screen.png b/asset/settings_screen.png index 3bbe5da..38d6600 100644 Binary files a/asset/settings_screen.png and b/asset/settings_screen.png differ diff --git a/asset/settings_screen_old.png b/asset/settings_screen_old.png new file mode 100644 index 0000000..3bbe5da Binary files /dev/null and b/asset/settings_screen_old.png differ diff --git a/asset/support_screen.png b/asset/support_screen.png index 1d91904..9c952ce 100644 Binary files a/asset/support_screen.png and b/asset/support_screen.png differ diff --git a/asset/support_screen_old.png b/asset/support_screen_old.png new file mode 100644 index 0000000..1d91904 Binary files /dev/null and b/asset/support_screen_old.png differ