Compare commits

..

6 Commits

Author SHA1 Message Date
Zane Schepke e0cce8fba4 fix: converter backwards compatibility
Fixes database converter to allow for backwards compatabilty.
2023-10-27 00:11:31 -04:00
Zane Schepke b70ecbdfff fix: fdroid build and ssid comma
Fixes bug where commas in SSID names were splitting into multiple SSIDs due to database type converters.
Closes #48

Fixes bug where F-Droid pipeline was failing to build due to lack of proguard rule. Closes #47

Fixes bug where crashes could happen if config QR code or file has improperly configured data.

Bump versions.
2023-10-26 22:05:20 -04:00
Zane Schepke 513d08998b fix: config save bug
Fixes a bug where config changes were saving on the wrong thread, causing a failure to save changes.

Fixes a bug where the quick tile could cause a crash by initializing the tile state before it was ready.
2023-10-23 16:13:37 -04:00
Zane Schepke 79583e0e61 docs: update README 2023-10-21 15:17:09 -04:00
Zane Schepke 75790ec6d5 bump: app version to 3.1.6 2023-10-21 14:13:47 -04:00
Zane Schepke a1941b7229 feat: shortcut intents and battery saver
Added the ability to turn on and off tunnels via intents to the shortcut activity.

Added a setting to enable or disable shortcut/intent control of tunnels.

Added an experimental battery saver setting to auto-tunneling to fix the battery drain issue caused by wakelock.

Fixes a bug where sometimes the config screen could crash if there are issues parsing the tunnel config data.

Database migration
2023-10-21 14:03:36 -04:00
36 changed files with 538 additions and 216 deletions
+9 -3
View File
@@ -14,10 +14,12 @@ android {
applicationId = "com.zaneschepke.wireguardautotunnel"
minSdk = 26
targetSdk = 34
versionCode = 31400
versionName = "3.1.4"
versionCode = 32000
versionName = "3.2.0"
multiDexEnabled = true
ksp {
arg("room.schemaLocation", "$projectDir/schemas")
}
resourceConfigurations.addAll(listOf("en"))
@@ -45,6 +47,7 @@ android {
productFlavors {
create("fdroid") {
dimension = "type"
proguardFile("fdroid-rules.pro")
}
create("general") {
dimension = "type"
@@ -149,4 +152,7 @@ dependencies {
//bio
implementation(libs.androidx.biometric.ktx)
//shortcuts
implementation(libs.androidx.core)
implementation(libs.androidx.core.google.shortcuts)
}
+1
View File
@@ -0,0 +1 @@
-dontwarn com.google.errorprone.annotations.**
+1 -1
View File
@@ -18,4 +18,4 @@
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
#-renamesourcefileattribute SourceFile
@@ -0,0 +1,112 @@
{
"formatVersion": 1,
"database": {
"version": 1,
"identityHash": "ba86153e6fb0b823197b987239b03e64",
"entities": [
{
"tableName": "Settings",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_tunnel_enabled` INTEGER NOT NULL, `is_tunnel_on_mobile_data_enabled` INTEGER NOT NULL, `trusted_network_ssids` TEXT NOT NULL, `default_tunnel` TEXT, `is_always_on_vpn_enabled` INTEGER NOT NULL, `is_tunnel_on_ethernet_enabled` INTEGER NOT NULL)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isAutoTunnelEnabled",
"columnName": "is_tunnel_enabled",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isTunnelOnMobileDataEnabled",
"columnName": "is_tunnel_on_mobile_data_enabled",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "trustedNetworkSSIDs",
"columnName": "trusted_network_ssids",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "defaultTunnel",
"columnName": "default_tunnel",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "isAlwaysOnVpnEnabled",
"columnName": "is_always_on_vpn_enabled",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isTunnelOnEthernetEnabled",
"columnName": "is_tunnel_on_ethernet_enabled",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "TunnelConfig",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `wg_quick` TEXT NOT NULL)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "wgQuick",
"columnName": "wg_quick",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
},
"indices": [
{
"name": "index_TunnelConfig_name",
"unique": true,
"columnNames": [
"name"
],
"orders": [],
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_TunnelConfig_name` ON `${TABLE_NAME}` (`name`)"
}
],
"foreignKeys": []
}
],
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'ba86153e6fb0b823197b987239b03e64')"
]
}
}
@@ -0,0 +1,126 @@
{
"formatVersion": 1,
"database": {
"version": 2,
"identityHash": "65b1c9efff61712231fa64d1f19f3915",
"entities": [
{
"tableName": "Settings",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_tunnel_enabled` INTEGER NOT NULL, `is_tunnel_on_mobile_data_enabled` INTEGER NOT NULL, `trusted_network_ssids` TEXT NOT NULL, `default_tunnel` TEXT, `is_always_on_vpn_enabled` INTEGER NOT NULL, `is_tunnel_on_ethernet_enabled` INTEGER NOT NULL, `is_shortcuts_enabled` INTEGER NOT NULL DEFAULT false, `is_battery_saver_enabled` INTEGER NOT NULL DEFAULT false)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isAutoTunnelEnabled",
"columnName": "is_tunnel_enabled",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isTunnelOnMobileDataEnabled",
"columnName": "is_tunnel_on_mobile_data_enabled",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "trustedNetworkSSIDs",
"columnName": "trusted_network_ssids",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "defaultTunnel",
"columnName": "default_tunnel",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "isAlwaysOnVpnEnabled",
"columnName": "is_always_on_vpn_enabled",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isTunnelOnEthernetEnabled",
"columnName": "is_tunnel_on_ethernet_enabled",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isShortcutsEnabled",
"columnName": "is_shortcuts_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isBatterySaverEnabled",
"columnName": "is_battery_saver_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "TunnelConfig",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `wg_quick` TEXT NOT NULL)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "wgQuick",
"columnName": "wg_quick",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
},
"indices": [
{
"name": "index_TunnelConfig_name",
"unique": true,
"columnNames": [
"name"
],
"orders": [],
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_TunnelConfig_name` ON `${TABLE_NAME}` (`name`)"
}
],
"foreignKeys": []
}
],
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '65b1c9efff61712231fa64d1f19f3915')"
]
}
}
@@ -1,13 +1,11 @@
package com.zaneschepke.wireguardautotunnel
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import org.junit.Assert.assertEquals
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.Assert.*
/**
* Instrumented test, which will execute on an Android device.
*
@@ -2,6 +2,7 @@ package com.zaneschepke.wireguardautotunnel
object Constants {
const val MANUAL_TUNNEL_CONFIG_ID = "0"
const val WATCHER_SERVICE_WAKE_LOCK_TIMEOUT = 10*60*1000L /*10 minute*/
const val VPN_CONNECTIVITY_CHECK_INTERVAL = 3000L
const val VPN_STATISTIC_CHECK_INTERVAL = 10000L
const val TOGGLE_TUNNEL_DELAY = 500L
@@ -8,8 +8,6 @@ import androidx.lifecycle.lifecycleScope
import com.zaneschepke.wireguardautotunnel.repository.SettingsDoa
import com.zaneschepke.wireguardautotunnel.repository.model.Settings
import dagger.hilt.android.HiltAndroidApp
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject
@@ -1,12 +1,15 @@
package com.zaneschepke.wireguardautotunnel.repository
import androidx.room.AutoMigration
import androidx.room.Database
import androidx.room.RoomDatabase
import androidx.room.TypeConverters
import com.zaneschepke.wireguardautotunnel.repository.model.Settings
import com.zaneschepke.wireguardautotunnel.repository.model.TunnelConfig
@Database(entities = [Settings::class, TunnelConfig::class], version = 1, exportSchema = false)
@Database(entities = [Settings::class, TunnelConfig::class], version = 2, autoMigrations = [
AutoMigration(from = 1, to = 2)
], exportSchema = true)
@TypeConverters(DatabaseListConverters::class)
abstract class AppDatabase : RoomDatabase() {
abstract fun settingDao(): SettingsDoa
@@ -1,15 +1,23 @@
package com.zaneschepke.wireguardautotunnel.repository
import androidx.room.TypeConverter
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
class DatabaseListConverters {
@TypeConverter
fun listToString(value: MutableList<String>): String {
return value.joinToString(",")
return Json.encodeToString(value)
}
@TypeConverter
fun <T> stringToList(value: String): MutableList<String> {
fun stringToList(value: String): MutableList<String> {
if(value.isEmpty()) return mutableListOf()
return value.split(",").toMutableList()
return try {
Json.decodeFromString<MutableList<String>>(value)
} catch (e : Exception) {
val list = value.split(",").toMutableList()
val json = listToString(list)
Json.decodeFromString<MutableList<String>>(json)
}
}
}
@@ -13,6 +13,8 @@ data class Settings(
@ColumnInfo(name = "default_tunnel") var defaultTunnel : String? = null,
@ColumnInfo(name = "is_always_on_vpn_enabled") var isAlwaysOnVpnEnabled : Boolean = false,
@ColumnInfo(name = "is_tunnel_on_ethernet_enabled") var isTunnelOnEthernetEnabled : Boolean = false,
@ColumnInfo(name = "is_shortcuts_enabled", defaultValue = "false") var isShortcutsEnabled : Boolean = false,
@ColumnInfo(name = "is_battery_saver_enabled", defaultValue = "false") var isBatterySaverEnabled : Boolean = false,
) {
fun isTunnelConfigDefault(tunnelConfig: TunnelConfig) : Boolean {
return if (defaultTunnel != null) {
@@ -91,14 +91,6 @@ object ServiceManager {
WireGuardConnectivityWatcherService::class.java)
}
fun toggleWatcherService(context: Context, tunnelConfig : String) {
when(getServiceState( context,
WireGuardConnectivityWatcherService::class.java,)) {
ServiceState.STARTED -> stopWatcherService(context)
ServiceState.STOPPED -> startWatcherService(context, tunnelConfig)
}
}
fun toggleWatcherServiceForeground(context: Context, tunnelConfig : String) {
when(getServiceState( context,
WireGuardConnectivityWatcherService::class.java,)) {
@@ -21,24 +21,24 @@ import com.zaneschepke.wireguardautotunnel.service.network.WifiService
import com.zaneschepke.wireguardautotunnel.service.notification.NotificationService
import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnService
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import timber.log.Timber
import javax.inject.Inject
@AndroidEntryPoint
class WireGuardConnectivityWatcherService : ForegroundService() {
private val foregroundId = 122;
private val foregroundId = 122
@Inject
lateinit var wifiService : NetworkService<WifiService>
lateinit var wifiService: NetworkService<WifiService>
@Inject
lateinit var mobileDataService : NetworkService<MobileDataService>
lateinit var mobileDataService: NetworkService<MobileDataService>
@Inject
lateinit var ethernetService: NetworkService<EthernetService>
@@ -47,22 +47,22 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
lateinit var settingsRepo: SettingsDoa
@Inject
lateinit var notificationService : NotificationService
lateinit var notificationService: NotificationService
@Inject
lateinit var vpnService : VpnService
lateinit var vpnService: VpnService
private var isWifiConnected = false;
private var isEthernetConnected = false;
private var isMobileDataConnected = false;
private var currentNetworkSSID = "";
private var isWifiConnected = false
private var isEthernetConnected = false
private var isMobileDataConnected = false
private var currentNetworkSSID = ""
private lateinit var watcherJob : Job;
private lateinit var setting : Settings
private lateinit var watcherJob: Job
private lateinit var setting: Settings
private lateinit var tunnelConfig: String
private var wakeLock: PowerManager.WakeLock? = null
private val tag = this.javaClass.name;
private val tag = this.javaClass.name
override fun onCreate() {
@@ -80,9 +80,11 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
this.tunnelConfig = tunnelId
}
// we need this lock so our service gets not affected by Doze Mode
initWakeLock()
lifecycleScope.launch {
initWakeLock()
}
cancelWatcherJob()
if(this::tunnelConfig.isInitialized) {
if (this::tunnelConfig.isInitialized) {
startWatcherJob()
} else {
stopService(extras)
@@ -104,7 +106,8 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
val notification = notificationService.createNotification(
channelId = getString(R.string.watcher_channel_id),
channelName = getString(R.string.watcher_channel_name),
description = getString(R.string.watcher_notification_text))
description = getString(R.string.watcher_notification_text)
)
super.startForeground(foregroundId, notification)
}
@@ -112,46 +115,59 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
override fun onTaskRemoved(rootIntent: Intent) {
Timber.d("Task Removed called")
val restartServiceIntent = Intent(rootIntent)
val restartServicePendingIntent: PendingIntent = PendingIntent.getService(this, 1, restartServiceIntent,
PendingIntent.FLAG_ONE_SHOT or PendingIntent.FLAG_IMMUTABLE);
applicationContext.getSystemService(Context.ALARM_SERVICE);
val alarmService: AlarmManager = applicationContext.getSystemService(Context.ALARM_SERVICE) as AlarmManager;
alarmService.set(AlarmManager.ELAPSED_REALTIME, SystemClock.elapsedRealtime() + 1000, restartServicePendingIntent);
val restartServicePendingIntent: PendingIntent = PendingIntent.getService(
this, 1, restartServiceIntent,
PendingIntent.FLAG_ONE_SHOT or PendingIntent.FLAG_IMMUTABLE
)
applicationContext.getSystemService(Context.ALARM_SERVICE)
val alarmService: AlarmManager =
applicationContext.getSystemService(Context.ALARM_SERVICE) as AlarmManager
alarmService.set(
AlarmManager.ELAPSED_REALTIME,
SystemClock.elapsedRealtime() + 1000,
restartServicePendingIntent
)
}
private fun initWakeLock() {
private suspend fun initWakeLock() {
val isBatterySaverOn = withContext(lifecycleScope.coroutineContext) {
settingsRepo.getAll().firstOrNull()?.isBatterySaverEnabled ?: false
}
wakeLock =
(getSystemService(Context.POWER_SERVICE) as PowerManager).run {
newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "$tag::lock").apply {
//TODO decide what to do here with the wakelock
//this is draining battery. Perhaps users only care for VPN to connect when their screen is on
//and they are actively using apps
acquire()
if (isBatterySaverOn) {
Timber.d("Initiating wakelock with timeout")
acquire(Constants.WATCHER_SERVICE_WAKE_LOCK_TIMEOUT)
} else {
Timber.d("Initiating wakelock with zero timeout")
acquire()
}
}
}
}
private fun cancelWatcherJob() {
if(this::watcherJob.isInitialized) {
if (this::watcherJob.isInitialized) {
watcherJob.cancel()
}
}
private fun startWatcherJob() {
watcherJob = lifecycleScope.launch(Dispatchers.IO) {
val settings = settingsRepo.getAll();
if(settings.isNotEmpty()) {
val settings = settingsRepo.getAll()
if (settings.isNotEmpty()) {
setting = settings[0]
}
launch {
watchForWifiConnectivityChanges()
}
if(setting.isTunnelOnMobileDataEnabled) {
if (setting.isTunnelOnMobileDataEnabled) {
launch {
watchForMobileDataConnectivityChanges()
}
}
if(setting.isTunnelOnEthernetEnabled) {
if (setting.isTunnelOnEthernetEnabled) {
launch {
watchForEthernetConnectivityChanges()
}
@@ -164,15 +180,17 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
private suspend fun watchForMobileDataConnectivityChanges() {
mobileDataService.networkStatus.collect {
when(it) {
when (it) {
is NetworkStatus.Available -> {
Timber.d("Gained Mobile data connection")
isMobileDataConnected = true
}
is NetworkStatus.CapabilitiesChanged -> {
isMobileDataConnected = true
Timber.d("Mobile data capabilities changed")
}
is NetworkStatus.Unavailable -> {
isMobileDataConnected = false
Timber.d("Lost mobile data connection")
@@ -188,10 +206,12 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
Timber.d("Gained Ethernet connection")
isEthernetConnected = true
}
is NetworkStatus.CapabilitiesChanged -> {
Timber.d("Ethernet capabilities changed")
isEthernetConnected = true
}
is NetworkStatus.Unavailable -> {
isEthernetConnected = false
Timber.d("Lost Ethernet connection")
@@ -202,45 +222,51 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
private suspend fun watchForWifiConnectivityChanges() {
wifiService.networkStatus.collect {
when (it) {
is NetworkStatus.Available -> {
Timber.d("Gained Wi-Fi connection")
isWifiConnected = true
}
is NetworkStatus.CapabilitiesChanged -> {
Timber.d("Wifi capabilities changed")
isWifiConnected = true
currentNetworkSSID = wifiService.getNetworkName(it.networkCapabilities) ?: "";
}
is NetworkStatus.Unavailable -> {
isWifiConnected = false
Timber.d("Lost Wi-Fi connection")
}
when (it) {
is NetworkStatus.Available -> {
Timber.d("Gained Wi-Fi connection")
isWifiConnected = true
}
is NetworkStatus.CapabilitiesChanged -> {
Timber.d("Wifi capabilities changed")
isWifiConnected = true
currentNetworkSSID = wifiService.getNetworkName(it.networkCapabilities) ?: ""
}
is NetworkStatus.Unavailable -> {
isWifiConnected = false
Timber.d("Lost Wi-Fi connection")
}
}
}
}
private suspend fun manageVpn() {
while(true) {
if(isEthernetConnected && setting.isTunnelOnEthernetEnabled && vpnService.getState() == Tunnel.State.DOWN) {
while (true) {
if (isEthernetConnected && setting.isTunnelOnEthernetEnabled && vpnService.getState() == Tunnel.State.DOWN) {
ServiceManager.startVpnService(this, tunnelConfig)
}
if(!isEthernetConnected && setting.isTunnelOnMobileDataEnabled &&
if (!isEthernetConnected && setting.isTunnelOnMobileDataEnabled &&
!isWifiConnected &&
isMobileDataConnected
&& vpnService.getState() == Tunnel.State.DOWN) {
&& vpnService.getState() == Tunnel.State.DOWN
) {
ServiceManager.startVpnService(this, tunnelConfig)
} else if(!isEthernetConnected && !setting.isTunnelOnMobileDataEnabled &&
} else if (!isEthernetConnected && !setting.isTunnelOnMobileDataEnabled &&
!isWifiConnected &&
vpnService.getState() == Tunnel.State.UP) {
vpnService.getState() == Tunnel.State.UP
) {
ServiceManager.stopVpnService(this)
} else if(!isEthernetConnected && isWifiConnected &&
} else if (!isEthernetConnected && isWifiConnected &&
!setting.trustedNetworkSSIDs.contains(currentNetworkSSID) &&
(vpnService.getState() != Tunnel.State.UP)) {
(vpnService.getState() != Tunnel.State.UP)
) {
ServiceManager.startVpnService(this, tunnelConfig)
} else if(!isEthernetConnected && (isWifiConnected &&
} else if (!isEthernetConnected && (isWifiConnected &&
setting.trustedNetworkSSIDs.contains(currentNetworkSSID)) &&
(vpnService.getState() == Tunnel.State.UP)) {
(vpnService.getState() == Tunnel.State.UP)
) {
ServiceManager.stopVpnService(this)
}
delay(Constants.VPN_CONNECTIVITY_CHECK_INTERVAL)
@@ -3,17 +3,15 @@ package com.zaneschepke.wireguardautotunnel.service.foreground
import android.app.PendingIntent
import android.content.Intent
import android.os.Bundle
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.lifecycleScope
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.receiver.NotificationActionReceiver
import com.zaneschepke.wireguardautotunnel.repository.SettingsDoa
import com.zaneschepke.wireguardautotunnel.repository.model.TunnelConfig
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.repository.model.TunnelConfig
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
@@ -23,7 +21,7 @@ import javax.inject.Inject
@AndroidEntryPoint
class WireGuardTunnelService : ForegroundService() {
private val foregroundId = 123;
private val foregroundId = 123
@Inject
lateinit var vpnService : VpnService
@@ -63,7 +61,7 @@ class WireGuardTunnelService : ForegroundService() {
}
} else {
Timber.d("Tunnel config null, starting default tunnel")
val settings = settingsRepo.getAll();
val settings = settingsRepo.getAll()
if(settings.isNotEmpty()) {
val setting = settings[0]
if(setting.defaultTunnel != null && setting.isAlwaysOnVpnEnabled) {
@@ -14,7 +14,7 @@ import javax.inject.Inject
class WireGuardNotification @Inject constructor(@ApplicationContext private val context: Context) : NotificationService {
private val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager;
private val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
override fun createNotification(
channelId: String,
@@ -12,9 +12,7 @@ import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
import com.zaneschepke.wireguardautotunnel.service.foreground.WireGuardTunnelService
import com.zaneschepke.wireguardautotunnel.util.WgTunnelException
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject
@@ -28,7 +26,6 @@ class ShortcutsActivity : ComponentActivity() {
@Inject
lateinit var tunnelConfigRepo : TunnelConfigDao
private fun attemptWatcherServiceToggle(tunnelConfig : String) {
lifecycleScope.launch(Dispatchers.Main) {
val settings = getSettings()
@@ -43,20 +40,28 @@ class ShortcutsActivity : ComponentActivity() {
if(intent.getStringExtra(CLASS_NAME_EXTRA_KEY)
.equals(WireGuardTunnelService::class.java.simpleName)) {
lifecycleScope.launch(Dispatchers.Main) {
try {
val settings = getSettings()
val tunnelConfig = if(settings.defaultTunnel == null) {
tunnelConfigRepo.getAll().first()
} else {
TunnelConfig.from(settings.defaultTunnel!!)
val settings = getSettings()
if(settings.isShortcutsEnabled) {
try {
val tunnelName = intent.getStringExtra(TUNNEL_NAME_EXTRA_KEY)
val tunnelConfig = if(tunnelName != null) {
tunnelConfigRepo.getAll().firstOrNull { it.name == tunnelName }
} else {
if(settings.defaultTunnel == null) {
tunnelConfigRepo.getAll().first()
} else {
TunnelConfig.from(settings.defaultTunnel!!)
}
}
tunnelConfig ?: return@launch
attemptWatcherServiceToggle(tunnelConfig.toString())
when(intent.action){
Action.STOP.name -> ServiceManager.stopVpnService(this@ShortcutsActivity)
Action.START.name -> ServiceManager.startVpnService(this@ShortcutsActivity, tunnelConfig.toString())
}
} catch (e : Exception) {
Timber.e(e.message)
}
attemptWatcherServiceToggle(tunnelConfig.toString())
when(intent.action){
Action.STOP.name -> ServiceManager.stopVpnService(this@ShortcutsActivity)
Action.START.name -> ServiceManager.startVpnService(this@ShortcutsActivity, tunnelConfig.toString())
}
} catch (e : Exception) {
Timber.e(e.message)
}
}
}
@@ -72,6 +77,7 @@ class ShortcutsActivity : ComponentActivity() {
}
}
companion object {
const val TUNNEL_NAME_EXTRA_KEY = "tunnelName"
const val CLASS_NAME_EXTRA_KEY = "className"
}
}
@@ -31,7 +31,7 @@ class TunnelControlTile : TileService() {
@Inject
lateinit var vpnService : VpnService
private val scope = CoroutineScope(Dispatchers.Main);
private val scope = CoroutineScope(Dispatchers.Main)
private lateinit var job : Job
@@ -42,14 +42,6 @@ class TunnelControlTile : TileService() {
super.onStartListening()
}
override fun onTileAdded() {
super.onTileAdded()
qsTile.contentDescription = this.resources.getString(R.string.toggle_vpn)
scope.launch {
updateTileState();
}
}
override fun onTileRemoved() {
super.onTileRemoved()
cancelJob()
@@ -65,7 +57,7 @@ class TunnelControlTile : TileService() {
unlockAndRun {
scope.launch {
try {
val tunnel = determineTileTunnel();
val tunnel = determineTileTunnel()
if(tunnel != null) {
attemptWatcherServiceToggle(tunnel.toString())
if(vpnService.getState() == Tunnel.State.UP) {
@@ -84,23 +76,23 @@ class TunnelControlTile : TileService() {
}
private suspend fun determineTileTunnel() : TunnelConfig? {
var tunnelConfig : TunnelConfig? = null;
var tunnelConfig : TunnelConfig? = null
val settings = settingsRepo.getAll()
if (settings.isNotEmpty()) {
val setting = settings.first()
tunnelConfig = if (setting.defaultTunnel != null) {
TunnelConfig.from(setting.defaultTunnel!!);
TunnelConfig.from(setting.defaultTunnel!!)
} else {
val configs = configRepo.getAll();
val configs = configRepo.getAll()
val config = if(configs.isNotEmpty()) {
configs.first();
configs.first()
} else {
null
}
config
}
}
return tunnelConfig;
return tunnelConfig
}
@@ -123,13 +115,13 @@ class TunnelControlTile : TileService() {
qsTile.state = Tile.STATE_ACTIVE
}
Tunnel.State.DOWN -> {
qsTile.state = Tile.STATE_INACTIVE;
qsTile.state = Tile.STATE_INACTIVE
}
else -> {
qsTile.state = Tile.STATE_UNAVAILABLE
}
}
val config = determineTileTunnel();
val config = determineTileTunnel()
setTileDescription(config?.name ?: this.resources.getString(R.string.no_tunnel_available))
qsTile.updateTile()
}
@@ -140,13 +132,13 @@ class TunnelControlTile : TileService() {
qsTile.subtitle = description
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
qsTile.stateDescription = description;
qsTile.stateDescription = description
}
}
private fun cancelJob() {
if(this::job.isInitialized) {
job.cancel();
job.cancel()
}
}
}
@@ -11,7 +11,6 @@ import com.zaneschepke.wireguardautotunnel.util.NumberUtils
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.cancel
import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableSharedFlow
@@ -47,28 +46,36 @@ class WireGuardTunnel @Inject constructor(private val backend : Backend,
override val handshakeStatus: SharedFlow<HandshakeStatus>
get() = _handshakeStatus.asSharedFlow()
private val scope = CoroutineScope(Dispatchers.IO);
private val scope = CoroutineScope(Dispatchers.IO)
private lateinit var statsJob : Job
override suspend fun startTunnel(tunnelConfig: TunnelConfig) : Tunnel.State{
return try {
if(getState() == Tunnel.State.UP && _tunnelName.value != tunnelConfig.name) {
stopTunnel()
}
_tunnelName.emit(tunnelConfig.name)
stopTunnelOnConfigChange(tunnelConfig)
emitTunnelName(tunnelConfig.name)
val config = TunnelConfig.configFromQuick(tunnelConfig.wgQuick)
val state = backend.setState(
this, Tunnel.State.UP, config)
_state.emit(state)
state;
state
} catch (e : Exception) {
Timber.e("Failed to start tunnel with error: ${e.message}")
Tunnel.State.DOWN
}
}
private suspend fun emitTunnelName(name : String) {
_tunnelName.emit(name)
}
private suspend fun stopTunnelOnConfigChange(tunnelConfig: TunnelConfig) {
if(getState() == Tunnel.State.UP && _tunnelName.value != tunnelConfig.name) {
stopTunnel()
}
}
override fun getName(): String {
return _tunnelName.value
}
@@ -89,7 +96,7 @@ class WireGuardTunnel @Inject constructor(private val backend : Backend,
}
override fun onStateChange(state : Tunnel.State) {
val tunnel = this;
val tunnel = this
_state.tryEmit(state)
if(state == Tunnel.State.UP) {
statsJob = scope.launch {
@@ -7,14 +7,10 @@ import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.unit.dp
@OptIn(ExperimentalFoundationApi::class)
@@ -1,6 +1,5 @@
package com.zaneschepke.wireguardautotunnel.ui.common.config
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.OutlinedTextField
@@ -1,10 +1,12 @@
package com.zaneschepke.wireguardautotunnel.ui.common.prompt
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.IntrinsicSize
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.Info
@@ -42,14 +44,15 @@ fun CustomSnackBar(
if (isRtl) LayoutDirection.Rtl else LayoutDirection.Ltr
) {
Row(
modifier = Modifier.fillMaxSize(),
modifier = Modifier.width(IntrinsicSize.Max).height(IntrinsicSize.Min),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceEvenly
horizontalArrangement = Arrangement.Start
) {
Icon(
Icons.Rounded.Info,
contentDescription = stringResource(R.string.info),
tint = Color.White
tint = Color.White,
modifier = Modifier.padding(end = 10.dp)
)
Text(message, color = Color.White, modifier = Modifier.padding(end = 5.dp))
}
@@ -1,7 +1,6 @@
package com.zaneschepke.wireguardautotunnel.ui.models
import com.wireguard.config.Interface
import com.wireguard.config.Peer
data class InterfaceProxy(
var privateKey : String = "",
@@ -81,6 +81,7 @@ import com.zaneschepke.wireguardautotunnel.ui.common.SearchBar
import com.zaneschepke.wireguardautotunnel.ui.common.config.ConfigurationTextBox
import com.zaneschepke.wireguardautotunnel.ui.common.prompt.AuthorizationPrompt
import com.zaneschepke.wireguardautotunnel.ui.common.text.SectionTitle
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import timber.log.Timber
@@ -136,11 +137,13 @@ fun ConfigScreen(
val screenPadding = 5.dp
LaunchedEffect(Unit) {
try {
viewModel.onScreenLoad(id)
} catch (e : Exception) {
showSnackbarMessage(e.message!!)
navController.navigate(Routes.Main.name)
scope.launch(Dispatchers.IO) {
try {
viewModel.onScreenLoad(id)
} catch (e : Exception) {
showSnackbarMessage(e.message!!)
navController.navigate(Routes.Main.name)
}
}
}
@@ -161,7 +164,7 @@ fun ConfigScreen(
},
onFailure = {
showAuthPrompt = false
showSnackbarMessage("Authentication failed")
showSnackbarMessage(context.getString(R.string.authentication_failed))
})
}
@@ -245,7 +248,7 @@ fun ConfigScreen(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween
) {
SearchBar(viewModel::emitQueriedPackages);
SearchBar(viewModel::emitQueriedPackages)
}
Spacer(Modifier.padding(5.dp))
LazyColumn(
@@ -27,7 +27,6 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import javax.inject.Inject
@HiltViewModel
@@ -63,14 +62,10 @@ class ConfigViewModel @Inject constructor(private val application : Application,
private lateinit var tunnelConfig: TunnelConfig
fun onScreenLoad(id : String) {
suspend fun onScreenLoad(id : String) {
if(id != Constants.MANUAL_TUNNEL_CONFIG_ID) {
viewModelScope.launch(Dispatchers.IO) {
tunnelConfig = withContext(this.coroutineContext) {
getTunnelConfigById(id) ?: throw WgTunnelException("Config not found")
}
emitScreenData()
}
tunnelConfig = getTunnelConfigById(id) ?: throw WgTunnelException("Config not found")
emitScreenData()
} else {
emitEmptyScreenData()
}
@@ -8,9 +8,7 @@ import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
@@ -110,7 +108,7 @@ fun DetailScreen(
})
Box(modifier = Modifier.padding(10.dp))
tunnel?.peers?.forEach{
val peerKey = it.publicKey.toBase64().toString()
val peerKey = it.publicKey.toBase64()
val allowedIps = it.allowedIps.joinToString()
val endpoint = if(it.endpoint.isPresent) it.endpoint.get().toString() else stringResource(
id = R.string.none
@@ -106,7 +106,7 @@ fun MainScreen(
val haptic = LocalHapticFeedback.current
val context = LocalContext.current
val isVisible = rememberSaveable { mutableStateOf(true) }
val scope = rememberCoroutineScope()
val scope = rememberCoroutineScope { Dispatchers.IO }
val sheetState = rememberModalBottomSheetState()
var showBottomSheet by remember { mutableStateOf(false) }
@@ -150,7 +150,7 @@ fun MainScreen(
val name = it.activityInfo.packageName
name.startsWith(Constants.GOOGLE_TV_EXPLORER_STUB) || name.startsWith(Constants.ANDROID_TV_EXPLORER_STUB)
}) {
throw WgTunnelException("No file explorer installed")
throw WgTunnelException(context.getString(R.string.no_file_explorer))
}
return intent
}
@@ -159,8 +159,8 @@ fun MainScreen(
scope.launch(Dispatchers.IO) {
try {
viewModel.onTunnelFileSelected(data)
} catch (e : Exception) {
showSnackbarMessage(e.message ?: "Unknown error occurred")
} catch (e : WgTunnelException) {
showSnackbarMessage(e.message)
}
}
}
@@ -168,10 +168,12 @@ fun MainScreen(
val scanLauncher = rememberLauncherForActivityResult(
contract = ScanContract(),
onResult = {
try {
viewModel.onTunnelQrResult(it.contents)
} catch (e: Exception) {
showSnackbarMessage(context.getString(R.string.qr_result_failed))
scope.launch {
try {
viewModel.onTunnelQrResult(it.contents)
} catch (e: WgTunnelException) {
showSnackbarMessage(e.message)
}
}
}
)
@@ -198,7 +200,7 @@ fun MainScreen(
{ Text(text = stringResource(R.string.cancel)) }
},
title = { Text(text = stringResource(R.string.primary_tunnel_change)) },
text = { Text(text = stringResource(R.string.primary_tunnnel_change_question)) }
text = { Text(text = stringResource(R.string.primary_tunnel_change_question)) }
)
}
@@ -363,12 +365,12 @@ fun MainScreen(
RowListItem(icon = {
if (settings.isTunnelConfigDefault(tunnel))
Icon(
Icons.Rounded.Star, "status",
Icons.Rounded.Star, stringResource(R.string.status),
tint = leadingIconColor,
modifier = Modifier.padding(end = 10.dp).size(20.dp)
)
else Icon(
Icons.Rounded.Circle, "status",
Icons.Rounded.Circle, stringResource(R.string.status),
tint = leadingIconColor,
modifier = Modifier.padding(end = 15.dp).size(15.dp)
)
@@ -433,7 +435,7 @@ fun MainScreen(
onClick = {
navController.navigate("${Routes.Detail.name}/${tunnel.id}")
}) {
Icon(Icons.Rounded.Info, "Info")
Icon(Icons.Rounded.Info, stringResource(R.string.info))
}
IconButton(onClick = {
if (state == Tunnel.State.UP && tunnel.name == tunnelName)
@@ -28,6 +28,7 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.io.InputStream
import java.util.zip.ZipInputStream
import javax.inject.Inject
@@ -117,32 +118,26 @@ class MainViewModel @Inject constructor(
}
private fun validateConfigString(config: String) {
if (!config.contains(application.getString(R.string.config_validation))) {
throw WgTunnelException(application.getString(R.string.config_validation))
TunnelConfig.configFromQuick(config)
}
suspend fun onTunnelQrResult(result: String) {
try {
validateConfigString(result)
val tunnelConfig =
TunnelConfig(name = NumberUtils.generateRandomTunnelName(), wgQuick = result)
addTunnel(tunnelConfig)
} catch (e : Exception) {
throw WgTunnelException(e)
}
}
fun onTunnelQrResult(result: String) {
viewModelScope.launch(Dispatchers.IO) {
try {
validateConfigString(result)
val tunnelConfig =
TunnelConfig(name = NumberUtils.generateRandomTunnelName(), wgQuick = result)
addTunnel(tunnelConfig)
} catch (e: WgTunnelException) {
throw WgTunnelException(
e.message ?: application.getString(R.string.unknown_error_message)
)
}
}
}
private fun saveTunnelConfigFromStream(stream: InputStream, fileName: String) {
viewModelScope.launch(Dispatchers.IO) {
val bufferReader = stream.bufferedReader(charset = Charsets.UTF_8)
val config = Config.parse(bufferReader)
val tunnelName = getNameFromFileName(fileName)
addTunnel(TunnelConfig(name = tunnelName, wgQuick = config.toWgQuickString()))
private suspend fun saveTunnelConfigFromStream(stream: InputStream, fileName: String) {
val bufferReader = stream.bufferedReader(charset = Charsets.UTF_8)
val config = Config.parse(bufferReader)
val tunnelName = getNameFromFileName(fileName)
addTunnel(TunnelConfig(name = tunnelName, wgQuick = config.toWgQuickString()))
withContext(Dispatchers.IO) {
stream.close()
}
}
@@ -161,9 +156,8 @@ class MainViewModel @Inject constructor(
Constants.ZIP_FILE_EXTENSION -> saveTunnelsFromZipUri(uri)
else -> throw WgTunnelException(application.getString(R.string.file_extension_message))
}
} catch (e: Exception) {
throw WgTunnelException(e.message ?: "Error importing file")
throw WgTunnelException(e)
}
}
@@ -182,7 +176,7 @@ class MainViewModel @Inject constructor(
}
}
private fun saveTunnelFromConfUri(name : String, uri: Uri) {
private suspend fun saveTunnelFromConfUri(name : String, uri: Uri) {
val stream = getInputStreamFromUri(uri)
saveTunnelConfigFromStream(stream, name)
}
@@ -15,7 +15,6 @@ import androidx.compose.foundation.layout.IntrinsicSize
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
@@ -72,9 +71,9 @@ import com.zaneschepke.wireguardautotunnel.ui.common.config.ConfigurationToggle
import com.zaneschepke.wireguardautotunnel.ui.common.prompt.AuthorizationPrompt
import com.zaneschepke.wireguardautotunnel.ui.common.text.SectionTitle
import com.zaneschepke.wireguardautotunnel.util.StorageUtil
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import java.io.File
import kotlin.math.exp
@OptIn(
ExperimentalPermissionsApi::class,
@@ -88,7 +87,7 @@ fun SettingsScreen(
focusRequester: FocusRequester,
) {
val scope = rememberCoroutineScope()
val scope = rememberCoroutineScope { Dispatchers.IO }
val context = LocalContext.current
val focusManager = LocalFocusManager.current
val keyboardController = LocalSoftwareKeyboardController.current
@@ -111,14 +110,14 @@ fun SettingsScreen(
fun exportAllConfigs() {
try {
val files = tunnels.map { File(context.cacheDir, "${it.name}.conf") }
files.forEachIndexed() { index, file ->
files.forEachIndexed { index, file ->
file.outputStream().use {
it.write(tunnels[index].wgQuick.toByteArray())
}
}
StorageUtil.saveFilesToZip(context, files)
didExportFiles = true
showSnackbarMessage("Exported configs to downloads")
showSnackbarMessage(context.getString(R.string.exported_configs_message))
} catch (e : Exception) {
showSnackbarMessage(e.message!!)
}
@@ -132,7 +131,7 @@ fun SettingsScreen(
viewModel.onSaveTrustedSSID(currentText)
currentText = ""
} catch (e : Exception) {
showSnackbarMessage(e.message ?: "Unknown error")
showSnackbarMessage(e.message ?: context.getString(R.string.unknown_error))
}
}
}
@@ -223,7 +222,7 @@ fun SettingsScreen(
},
onFailure = {
showAuthPrompt = false
showSnackbarMessage("Authentication failed")
showSnackbarMessage(context.getString(R.string.authentication_failed))
})
}
@@ -350,6 +349,17 @@ fun SettingsScreen(
}
}
)
ConfigurationToggle(
stringResource(R.string.battery_saver),
enabled = !(settings.isAutoTunnelEnabled || settings.isAlwaysOnVpnEnabled),
checked = settings.isBatterySaverEnabled,
padding = screenPadding,
onCheckChanged = {
scope.launch {
viewModel.onToggleBatterySaver()
}
}
)
ConfigurationToggle(stringResource(R.string.enable_auto_tunnel),
enabled = !settings.isAlwaysOnVpnEnabled,
checked = settings.isAutoTunnelEnabled,
@@ -357,11 +367,11 @@ fun SettingsScreen(
onCheckChanged = {
if(!isAllAutoTunnelPermissionsEnabled()) {
val message = if(viewModel.isLocationServicesNeeded()){
"Location services required"
context.getString(R.string.location_services_required)
} else if(!isBackgroundLocationGranted){
"Background location required"
context.getString(R.string.background_location_required)
} else {
"Precise location required"
context.getString(R.string.precise_location_required)
}
showSnackbarMessage(message)
} else scope.launch {
@@ -398,6 +408,16 @@ fun SettingsScreen(
}
}
)
ConfigurationToggle(stringResource(R.string.enabled_app_shortcuts),
enabled = true,
checked = settings.isShortcutsEnabled,
padding = screenPadding,
onCheckChanged = {
scope.launch {
viewModel.onToggleShortcutsEnabled()
}
}
)
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
@@ -410,7 +430,7 @@ fun SettingsScreen(
onClick = {
showAuthPrompt = true
}) {
Text("Export configs")
Text(stringResource(R.string.export_configs))
}
}
}
@@ -84,7 +84,7 @@ class SettingsViewModel @Inject constructor(private val application : Applicatio
}
private suspend fun getFirstTunnelConfig() : TunnelConfig {
return tunnelRepo.getAll().first();
return tunnelRepo.getAll().first()
}
suspend fun onToggleAlwaysOnVPN() {
@@ -125,4 +125,16 @@ class SettingsViewModel @Inject constructor(private val application : Applicatio
fun isLocationServicesNeeded() : Boolean {
return(!isLocationServicesEnabled() && Build.VERSION.SDK_INT > Build.VERSION_CODES.P)
}
suspend fun onToggleShortcutsEnabled() {
settingsRepo.save(_settings.value.copy(
isShortcutsEnabled = !_settings.value.isShortcutsEnabled
))
}
suspend fun onToggleBatterySaver() {
settingsRepo.save(_settings.value.copy(
isBatterySaverEnabled = !_settings.value.isBatterySaverEnabled
))
}
}
@@ -1,3 +1,15 @@
package com.zaneschepke.wireguardautotunnel.util
class WgTunnelException(message: String) : Exception(message)
import com.wireguard.config.BadConfigException
class WgTunnelException(e: Exception) : Exception() {
constructor(message : String) : this(Exception(message))
override val message: String = generateExceptionMessage(e)
private fun generateExceptionMessage(e : Exception) : String {
return when(e) {
is BadConfigException -> "${e.section.name} ${e.location.name} ${e.reason.name}"
else -> e.message ?: "Unknown error occurred"
}
}
}
+12 -1
View File
@@ -126,5 +126,16 @@
<string name="persistent_keepalive">Persistent keepalive</string>
<string name="cancel">Cancel</string>
<string name="primary_tunnel_change">Primary tunnel change</string>
<string name="primary_tunnnel_change_question">Would you like to make this your primary tunnel?</string>
<string name="primary_tunnel_change_question">Would you like to make this your primary tunnel?</string>
<string name="authentication_failed">Authentication failed</string>
<string name="enabled_app_shortcuts">Enable app shortcuts</string>
<string name="export_configs">Export configs</string>
<string name="battery_saver">Battery saver (experimental)</string>
<string name="location_services_required">Location services required</string>
<string name="background_location_required">Background location required</string>
<string name="precise_location_required">Precise location required</string>
<string name="unknown_error">Unknown error occurred</string>
<string name="exported_configs_message">Exported configs to downloads</string>
<string name="no_file_explorer">No file explorer installed</string>
<string name="status">status</string>
</resources>
@@ -1,9 +1,8 @@
package com.zaneschepke.wireguardautotunnel
import org.junit.Assert.assertEquals
import org.junit.Test
import org.junit.Assert.*
/**
* Example local unit test, which will execute on the development machine (host).
*
Binary file not shown.

Before

Width:  |  Height:  |  Size: 99 KiB

After

Width:  |  Height:  |  Size: 125 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 89 KiB

After

Width:  |  Height:  |  Size: 99 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 99 KiB

After

Width:  |  Height:  |  Size: 125 KiB

+10 -7
View File
@@ -4,6 +4,7 @@ activityCompose = "1.8.0"
androidx-junit = "1.1.5"
appcompat = "1.6.1"
biometricKtx = "1.2.0-alpha05"
coreGoogleShortcuts = "1.1.0"
coreKtx = "1.12.0"
espressoCore = "3.5.1"
firebase-crashlytics-gradle = "2.9.9"
@@ -13,20 +14,20 @@ hiltNavigationCompose = "1.0.0"
junit = "4.13.2"
kotlinx-serialization-json = "1.5.1"
lifecycle-runtime-compose = "2.6.2"
material-icons-extended = "1.5.3"
material-icons-extended = "1.5.4"
material3 = "1.1.2"
navigationCompose = "2.7.4"
roomVersion = "2.6.0-rc01"
roomVersion = "2.6.0"
timber = "5.0.1"
tunnel = "1.0.20230706"
androidGradlePlugin = "8.3.0-alpha06"
kotlin="1.9.10"
ksp="1.9.10-1.0.13"
composeBom="2023.10.00"
firebaseBom="32.3.1"
compose="1.5.3"
crashlytics="18.4.3"
analytics="21.3.0"
composeBom="2023.10.01"
firebaseBom="32.4.0"
compose="1.5.4"
crashlytics="18.5.0"
analytics="21.4.0"
composeCompiler="1.5.3"
zxingAndroidEmbedded = "4.3.0"
zxingCore = "3.4.1"
@@ -42,6 +43,8 @@ accompanist-systemuicontroller = { module = "com.google.accompanist:accompanist-
#room
androidx-biometric-ktx = { module = "androidx.biometric:biometric-ktx", version.ref = "biometricKtx" }
androidx-core = { module = "androidx.core:core", version.ref = "coreKtx" }
androidx-core-google-shortcuts = { module = "androidx.core:core-google-shortcuts", version.ref = "coreGoogleShortcuts" }
androidx-lifecycle-process = { module = "androidx.lifecycle:lifecycle-process", version.ref = "lifecycle-runtime-compose" }
androidx-lifecycle-service = { module = "androidx.lifecycle:lifecycle-service", version.ref = "lifecycle-runtime-compose" }
androidx-room-compiler = { module = "androidx.room:room-compiler", version.ref = "roomVersion" }