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