feat: support for Always-On VPN and Android TV
Added support for Android TV Added support for Always-On VPN in settings Fixes bug where handshake notification is not dismissed after successful handshake Closes #2, Closes #5
This commit is contained in:
parent
f6612abe28
commit
baed8ff2e7
|
@ -29,8 +29,8 @@ This is an alternative Android Application for [WireGuard](https://www.wireguard
|
|||
<p float="center">
|
||||
<img label="Main" style="padding-right:25px" src="asset/main_screen.png" width="200" />
|
||||
<img label="Config" style="padding-left:25px" src="./asset/config_screen.png" width="200" />
|
||||
<img label="Settings" style="padding-left:25px" src="./asset/settings_screen.png" width="200" />
|
||||
<img label="Support" style="padding-left:25px" src="./asset/support_screen.png" width="200" />
|
||||
<img label="Settings" style="padding-left:25px" src="asset/settings_screen.png" width="200" />
|
||||
<img label="Support" style="padding-left:25px" src="asset/support_screen.png" width="200" />
|
||||
</p>
|
||||
|
||||
<span align="left">
|
||||
|
@ -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
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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": []
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -5,7 +5,8 @@
|
|||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"
|
||||
android:maxSdkVersion="32" />
|
||||
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE"
|
||||
android:maxSdkVersion="30" />
|
||||
android:maxSdkVersion="30"
|
||||
tools:ignore="LeanbackUsesWifi" />
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
|
||||
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
|
||||
|
@ -19,6 +20,14 @@
|
|||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||
<!--start service on boot permission-->
|
||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
|
||||
<!--android tv support-->
|
||||
<uses-feature android:name="android.software.leanback"
|
||||
android:required="false" />
|
||||
<uses-feature android:name="android.hardware.touchscreen"
|
||||
android:required="false" />
|
||||
<uses-feature
|
||||
android:name="android.hardware.location.gps"
|
||||
android:required="false" />
|
||||
<queries>
|
||||
<intent>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
@ -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 @@
|
|||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<service
|
||||
|
@ -52,9 +63,15 @@
|
|||
</service>
|
||||
<service
|
||||
android:name=".service.foreground.WireGuardTunnelService"
|
||||
android:permission="android.permission.BIND_VPN_SERVICE"
|
||||
android:enabled="true"
|
||||
android:foregroundServiceType="remoteMessaging"
|
||||
android:exported="false">
|
||||
<intent-filter>
|
||||
<action android:name="android.net.VpnService"/>
|
||||
</intent-filter>
|
||||
<meta-data android:name="android.net.VpnService.SUPPORTS_ALWAYS_ON"
|
||||
android:value="true"/>
|
||||
</service>
|
||||
<service
|
||||
android:name=".service.foreground.WireGuardConnectivityWatcherService"
|
||||
|
|
|
@ -7,7 +7,6 @@ import com.google.mlkit.vision.codescanner.GmsBarcodeScannerOptions
|
|||
import com.google.mlkit.vision.codescanner.GmsBarcodeScanning
|
||||
import com.zaneschepke.wireguardautotunnel.service.barcode.CodeScanner
|
||||
import com.zaneschepke.wireguardautotunnel.service.barcode.QRScanner
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
|
|
|
@ -4,7 +4,10 @@ import android.app.Service
|
|||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.os.IBinder
|
||||
import com.zaneschepke.wireguardautotunnel.repository.Repository
|
||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.model.Settings
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
|
||||
open class ForegroundService : Service() {
|
||||
|
@ -24,6 +27,10 @@ open class ForegroundService : Service() {
|
|||
when (action) {
|
||||
Action.START.name -> 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 {
|
||||
|
|
|
@ -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<Settings>
|
||||
|
||||
@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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,5 +10,6 @@ data class Settings(
|
|||
var isAutoTunnelEnabled : Boolean = false,
|
||||
var isTunnelOnMobileDataEnabled : Boolean = false,
|
||||
var trustedNetworkSSIDs : MutableList<String> = mutableListOf(),
|
||||
var defaultTunnel : String? = null
|
||||
var defaultTunnel : String? = null,
|
||||
var isAlwaysOnVpnEnabled : Boolean = false
|
||||
)
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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<TunnelConfig>, private val settingsRepo : Repository<Settings>
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -77,4 +77,9 @@
|
|||
<string name="failed_connection_to">Failed connection to -</string>
|
||||
<string name="initial_connection_failure_message">Attempting to connect to server after 30 seconds of no response.</string>
|
||||
<string name="lost_connection_failure_message">Attempting to reconnect to server after more than one minute of no response.</string>
|
||||
<string name="always_on_vpn_support">Enable Always-On VPN support</string>
|
||||
<string name="select_tunnel_message">Please select a tunnel first</string>
|
||||
<string name="location_services_not_detected">Unable to detect Location Services which are required for this feature. Please enable Location Services.</string>
|
||||
<string name="check_again">Check again</string>
|
||||
<string name="detecting_location_services_disabled">Detecting Location Services disabled</string>
|
||||
</resources>
|
Binary file not shown.
Before Width: | Height: | Size: 75 KiB After Width: | Height: | Size: 89 KiB |
Binary file not shown.
After Width: | Height: | Size: 75 KiB |
Binary file not shown.
Before Width: | Height: | Size: 94 KiB After Width: | Height: | Size: 101 KiB |
Binary file not shown.
After Width: | Height: | Size: 94 KiB |
Loading…
Reference in New Issue