diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 3858032..924a62f 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -14,11 +14,15 @@ android { applicationId = "com.zaneschepke.wireguardautotunnel" minSdk = 26 targetSdk = 34 - versionCode = 31400 - versionName = "3.1.4" + versionCode = 31500 + versionName = "3.1.5" multiDexEnabled = true + ksp { + arg("room.schemaLocation", "$projectDir/schemas") + } + resourceConfigurations.addAll(listOf("en")) testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" @@ -149,4 +153,7 @@ dependencies { //bio implementation(libs.androidx.biometric.ktx) + //shortcuts + implementation(libs.androidx.core) + implementation(libs.androidx.core.google.shortcuts) } \ No newline at end of file diff --git a/app/schemas/com.zaneschepke.wireguardautotunnel.repository.AppDatabase/1.json b/app/schemas/com.zaneschepke.wireguardautotunnel.repository.AppDatabase/1.json new file mode 100644 index 0000000..bb8a547 --- /dev/null +++ b/app/schemas/com.zaneschepke.wireguardautotunnel.repository.AppDatabase/1.json @@ -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')" + ] + } +} \ No newline at end of file diff --git a/app/schemas/com.zaneschepke.wireguardautotunnel.repository.AppDatabase/2.json b/app/schemas/com.zaneschepke.wireguardautotunnel.repository.AppDatabase/2.json new file mode 100644 index 0000000..01fca8f --- /dev/null +++ b/app/schemas/com.zaneschepke.wireguardautotunnel.repository.AppDatabase/2.json @@ -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')" + ] + } +} \ No newline at end of file diff --git a/app/src/androidTest/java/com/zaneschepke/wireguardautotunnel/ExampleInstrumentedTest.kt b/app/src/androidTest/java/com/zaneschepke/wireguardautotunnel/ExampleInstrumentedTest.kt index 0cd6894..52139b4 100644 --- a/app/src/androidTest/java/com/zaneschepke/wireguardautotunnel/ExampleInstrumentedTest.kt +++ b/app/src/androidTest/java/com/zaneschepke/wireguardautotunnel/ExampleInstrumentedTest.kt @@ -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. * diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/Constants.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/Constants.kt index 264130a..b8cdc1d 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/Constants.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/Constants.kt @@ -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 diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/WireGuardAutoTunnel.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/WireGuardAutoTunnel.kt index 3d40cdd..c67dee5 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/WireGuardAutoTunnel.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/WireGuardAutoTunnel.kt @@ -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 diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/repository/AppDatabase.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/repository/AppDatabase.kt index 633762e..ebfff9f 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/repository/AppDatabase.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/repository/AppDatabase.kt @@ -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 diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/repository/DatabaseListConverters.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/repository/DatabaseListConverters.kt index aee8e76..6ff8d31 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/repository/DatabaseListConverters.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/repository/DatabaseListConverters.kt @@ -8,7 +8,7 @@ class DatabaseListConverters { return value.joinToString(",") } @TypeConverter - fun stringToList(value: String): MutableList { + fun stringToList(value: String): MutableList { if(value.isEmpty()) return mutableListOf() return value.split(",").toMutableList() } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/repository/model/Settings.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/repository/model/Settings.kt index ce67a99..68d6aab 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/repository/model/Settings.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/repository/model/Settings.kt @@ -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) { diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/ServiceManager.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/ServiceManager.kt index 35b00a8..b57cb7e 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/ServiceManager.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/ServiceManager.kt @@ -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,)) { diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/WireGuardConnectivityWatcherService.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/WireGuardConnectivityWatcherService.kt index 00eb43d..6505c59 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/WireGuardConnectivityWatcherService.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/WireGuardConnectivityWatcherService.kt @@ -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 + lateinit var wifiService: NetworkService @Inject - lateinit var mobileDataService : NetworkService + lateinit var mobileDataService: NetworkService @Inject lateinit var ethernetService: NetworkService @@ -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) 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 44ca2fc..aad5568 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 @@ -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) { diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/notification/WireGuardNotification.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/notification/WireGuardNotification.kt index ad68582..a982de7 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/notification/WireGuardNotification.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/notification/WireGuardNotification.kt @@ -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, diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/shortcut/ShortcutsActivity.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/shortcut/ShortcutsActivity.kt index 872084d..76c23e2 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/shortcut/ShortcutsActivity.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/shortcut/ShortcutsActivity.kt @@ -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" } } \ No newline at end of file diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tile/TunnelControlTile.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tile/TunnelControlTile.kt index 0866f09..9e5fb78 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tile/TunnelControlTile.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tile/TunnelControlTile.kt @@ -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 @@ -46,7 +46,7 @@ class TunnelControlTile : TileService() { super.onTileAdded() qsTile.contentDescription = this.resources.getString(R.string.toggle_vpn) scope.launch { - updateTileState(); + updateTileState() } } @@ -65,7 +65,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 +84,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 +123,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 +140,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() } } } \ No newline at end of file diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tunnel/WireGuardTunnel.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tunnel/WireGuardTunnel.kt index 32bac9c..bbeaed9 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tunnel/WireGuardTunnel.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tunnel/WireGuardTunnel.kt @@ -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 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 { diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/RowListItem.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/RowListItem.kt index f254a36..c04b159 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/RowListItem.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/RowListItem.kt @@ -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) diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/config/ConfigurationTextBox.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/config/ConfigurationTextBox.kt index c4e30ea..a65eb99 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/config/ConfigurationTextBox.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/config/ConfigurationTextBox.kt @@ -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 diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/models/InterfaceProxy.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/models/InterfaceProxy.kt index e67a772..d5b92f9 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/models/InterfaceProxy.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/models/InterfaceProxy.kt @@ -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 = "", diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/config/ConfigScreen.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/config/ConfigScreen.kt index 713bab6..6d15e36 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/config/ConfigScreen.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/config/ConfigScreen.kt @@ -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 @@ -98,7 +99,7 @@ fun ConfigScreen( ) { val context = LocalContext.current - val scope = rememberCoroutineScope() + val scope = rememberCoroutineScope { Dispatchers.IO } val clipboardManager: ClipboardManager = LocalClipboardManager.current val keyboardController = LocalSoftwareKeyboardController.current @@ -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( diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/config/ConfigViewModel.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/config/ConfigViewModel.kt index 6d3ef8b..6e56359 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/config/ConfigViewModel.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/config/ConfigViewModel.kt @@ -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() } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/detail/DetailScreen.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/detail/DetailScreen.kt index b951d5f..07f90ad 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/detail/DetailScreen.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/detail/DetailScreen.kt @@ -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 diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/MainScreen.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/MainScreen.kt index 593f842..4eb2fde 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/MainScreen.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/MainScreen.kt @@ -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 } @@ -160,7 +160,7 @@ fun MainScreen( try { viewModel.onTunnelFileSelected(data) } catch (e : Exception) { - showSnackbarMessage(e.message ?: "Unknown error occurred") + showSnackbarMessage(e.message ?: context.getString(R.string.unknown_error)) } } } @@ -198,7 +198,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 +363,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 +433,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) 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 bb0f2ec..259d9c8 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 @@ -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)) } } } 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 67c7a6b..a72dd95 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 @@ -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 + )) + } } \ 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 34c4694..9572b63 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -126,5 +126,16 @@ Persistent keepalive Cancel Primary tunnel change - Would you like to make this your primary tunnel? + Would you like to make this your primary tunnel? + Authentication failed + Enable app shortcuts + Export configs + Battery saver (experimental) + Location services required + Background location required + Precise location required + Unknown error occurred + Exported configs to downloads + No file explorer installed + status \ No newline at end of file diff --git a/app/src/test/java/com/zaneschepke/wireguardautotunnel/ExampleUnitTest.kt b/app/src/test/java/com/zaneschepke/wireguardautotunnel/ExampleUnitTest.kt index 0dcc33d..8730d6a 100644 --- a/app/src/test/java/com/zaneschepke/wireguardautotunnel/ExampleUnitTest.kt +++ b/app/src/test/java/com/zaneschepke/wireguardautotunnel/ExampleUnitTest.kt @@ -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). * diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 0f9a6ff..4a97dc7 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -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" @@ -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" }