diff --git a/app/schemas/com.zaneschepke.wireguardautotunnel.data.AppDatabase/10.json b/app/schemas/com.zaneschepke.wireguardautotunnel.data.AppDatabase/10.json
new file mode 100644
index 0000000..4907030
--- /dev/null
+++ b/app/schemas/com.zaneschepke.wireguardautotunnel.data.AppDatabase/10.json
@@ -0,0 +1,225 @@
+{
+ "formatVersion": 1,
+ "database": {
+ "version": 10,
+ "identityHash": "c8621055524f90b4d1972f6171f59e80",
+ "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, `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_tunnel_on_wifi_enabled` INTEGER NOT NULL DEFAULT false, `is_kernel_enabled` INTEGER NOT NULL DEFAULT false, `is_restore_on_boot_enabled` INTEGER NOT NULL DEFAULT false, `is_multi_tunnel_enabled` INTEGER NOT NULL DEFAULT false, `is_auto_tunnel_paused` INTEGER NOT NULL DEFAULT false, `is_ping_enabled` INTEGER NOT NULL DEFAULT false, `is_amnezia_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": "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": "isTunnelOnWifiEnabled",
+ "columnName": "is_tunnel_on_wifi_enabled",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "false"
+ },
+ {
+ "fieldPath": "isKernelEnabled",
+ "columnName": "is_kernel_enabled",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "false"
+ },
+ {
+ "fieldPath": "isRestoreOnBootEnabled",
+ "columnName": "is_restore_on_boot_enabled",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "false"
+ },
+ {
+ "fieldPath": "isMultiTunnelEnabled",
+ "columnName": "is_multi_tunnel_enabled",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "false"
+ },
+ {
+ "fieldPath": "isAutoTunnelPaused",
+ "columnName": "is_auto_tunnel_paused",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "false"
+ },
+ {
+ "fieldPath": "isPingEnabled",
+ "columnName": "is_ping_enabled",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "false"
+ },
+ {
+ "fieldPath": "isAmneziaEnabled",
+ "columnName": "is_amnezia_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, `tunnel_networks` TEXT NOT NULL DEFAULT '', `is_mobile_data_tunnel` INTEGER NOT NULL DEFAULT false, `is_primary_tunnel` INTEGER NOT NULL DEFAULT false, `am_quick` TEXT NOT NULL DEFAULT '', `is_Active` INTEGER NOT NULL DEFAULT false, `is_ping_enabled` INTEGER NOT NULL DEFAULT false, `ping_interval` INTEGER DEFAULT null, `ping_cooldown` INTEGER DEFAULT null, `ping_ip` TEXT DEFAULT 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
+ },
+ {
+ "fieldPath": "tunnelNetworks",
+ "columnName": "tunnel_networks",
+ "affinity": "TEXT",
+ "notNull": true,
+ "defaultValue": "''"
+ },
+ {
+ "fieldPath": "isMobileDataTunnel",
+ "columnName": "is_mobile_data_tunnel",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "false"
+ },
+ {
+ "fieldPath": "isPrimaryTunnel",
+ "columnName": "is_primary_tunnel",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "false"
+ },
+ {
+ "fieldPath": "amQuick",
+ "columnName": "am_quick",
+ "affinity": "TEXT",
+ "notNull": true,
+ "defaultValue": "''"
+ },
+ {
+ "fieldPath": "isActive",
+ "columnName": "is_Active",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "false"
+ },
+ {
+ "fieldPath": "isPingEnabled",
+ "columnName": "is_ping_enabled",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "false"
+ },
+ {
+ "fieldPath": "pingInterval",
+ "columnName": "ping_interval",
+ "affinity": "INTEGER",
+ "notNull": false,
+ "defaultValue": "null"
+ },
+ {
+ "fieldPath": "pingCooldown",
+ "columnName": "ping_cooldown",
+ "affinity": "INTEGER",
+ "notNull": false,
+ "defaultValue": "null"
+ },
+ {
+ "fieldPath": "pingIp",
+ "columnName": "ping_ip",
+ "affinity": "TEXT",
+ "notNull": false,
+ "defaultValue": "null"
+ }
+ ],
+ "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, 'c8621055524f90b4d1972f6171f59e80')"
+ ]
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index d8c8cc1..4dc121b 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -103,12 +103,6 @@
android:launchMode="singleInstance"
android:theme="@android:style/Theme.NoDisplay" />
-
removeFromDataStore(key: Preferences.Key) {
+ withContext(ioDispatcher) {
+ try {
+ context.dataStore.edit { it.remove(key) }
+ } catch (e: IOException) {
+ Timber.e(e)
+ } catch (e: Exception) {
+ Timber.e(e)
+ }
+ }
+ }
+
fun getFromStoreFlow(key: Preferences.Key) = context.dataStore.data.map { it[key] }
suspend fun getFromStore(key: Preferences.Key): T? {
diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/domain/TunnelConfig.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/domain/TunnelConfig.kt
index c225b2d..4a38a76 100644
--- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/domain/TunnelConfig.kt
+++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/domain/TunnelConfig.kt
@@ -37,11 +37,28 @@ data class TunnelConfig(
defaultValue = "false",
)
val isActive: Boolean = false,
+ @ColumnInfo(
+ name = "is_ping_enabled",
+ defaultValue = "false",
+ )
+ val isPingEnabled: Boolean = false,
+ @ColumnInfo(
+ name = "ping_interval",
+ defaultValue = "null",
+ )
+ val pingInterval: Long? = null,
+ @ColumnInfo(
+ name = "ping_cooldown",
+ defaultValue = "null",
+ )
+ val pingCooldown: Long? = null,
+ @ColumnInfo(
+ name = "ping_ip",
+ defaultValue = "null",
+ )
+ var pingIp: String? = null,
) {
companion object {
- fun findDefault(tunnels: List): TunnelConfig? {
- return tunnels.find { it.isPrimaryTunnel } ?: tunnels.firstOrNull()
- }
fun configFromWgQuick(wgQuick: String): Config {
val inputStream: InputStream = wgQuick.byteInputStream()
diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/repository/AppDataRoomRepository.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/repository/AppDataRoomRepository.kt
index 521a9d4..c023f0e 100644
--- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/repository/AppDataRoomRepository.kt
+++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/repository/AppDataRoomRepository.kt
@@ -15,8 +15,9 @@ constructor(
}
override suspend fun getStartTunnelConfig(): TunnelConfig? {
- return appState.getLastActiveTunnelId()?.let {
- tunnels.getById(it)
- } ?: getPrimaryOrFirstTunnel()
+ tunnels.getActive().let {
+ if (it.isNotEmpty()) return it.first()
+ return getPrimaryOrFirstTunnel()
+ }
}
}
diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/repository/AppStateRepository.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/repository/AppStateRepository.kt
index 195bed4..dc36609 100644
--- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/repository/AppStateRepository.kt
+++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/repository/AppStateRepository.kt
@@ -16,10 +16,6 @@ interface AppStateRepository {
suspend fun setBatteryOptimizationDisableShown(shown: Boolean)
- suspend fun getLastActiveTunnelId(): Int?
-
- suspend fun setLastActiveTunnelId(id: Int)
-
suspend fun getCurrentSsid(): String?
suspend fun setCurrentSsid(ssid: String)
diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/repository/DataStoreAppStateRepository.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/repository/DataStoreAppStateRepository.kt
index 18ce1e9..3374969 100644
--- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/repository/DataStoreAppStateRepository.kt
+++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/repository/DataStoreAppStateRepository.kt
@@ -47,14 +47,6 @@ class DataStoreAppStateRepository(
withContext(ioDispatcher) { dataStoreManager.saveToDataStore(DataStoreManager.BATTERY_OPTIMIZE_DISABLE_SHOWN, shown) }
}
- override suspend fun getLastActiveTunnelId(): Int? {
- return withContext(ioDispatcher) { dataStoreManager.getFromStore(DataStoreManager.LAST_ACTIVE_TUNNEL) }
- }
-
- override suspend fun setLastActiveTunnelId(id: Int) {
- return withContext(ioDispatcher) { dataStoreManager.saveToDataStore(DataStoreManager.LAST_ACTIVE_TUNNEL, id) }
- }
-
override suspend fun getCurrentSsid(): String? {
return withContext(ioDispatcher) { dataStoreManager.getFromStore(DataStoreManager.CURRENT_SSID) }
}
@@ -77,7 +69,6 @@ class DataStoreAppStateRepository(
isPinLockEnabled =
pref[DataStoreManager.IS_PIN_LOCK_ENABLED]
?: GeneralState.PIN_LOCK_ENABLED_DEFAULT,
- lastActiveTunnelId = pref[DataStoreManager.LAST_ACTIVE_TUNNEL],
)
} catch (e: IllegalArgumentException) {
Timber.e(e)
diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/module/NavigationModule.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/module/NavigationModule.kt
new file mode 100644
index 0000000..862781e
--- /dev/null
+++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/module/NavigationModule.kt
@@ -0,0 +1,22 @@
+package com.zaneschepke.wireguardautotunnel.module
+
+import android.content.Context
+import androidx.navigation.NavHostController
+import com.zaneschepke.wireguardautotunnel.ui.common.navigation.NavigationService
+import dagger.Module
+import dagger.Provides
+import dagger.hilt.InstallIn
+import dagger.hilt.android.components.ActivityRetainedComponent
+import dagger.hilt.android.qualifiers.ApplicationContext
+import dagger.hilt.android.scopes.ActivityRetainedScoped
+
+@Module
+@InstallIn(ActivityRetainedComponent::class)
+object NavigationModule {
+
+ @Provides
+ @ActivityRetainedScoped
+ fun provideNestedNavController(@ApplicationContext context: Context): NavHostController {
+ return NavigationService(context).navController
+ }
+}
diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/module/TunnelModule.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/module/TunnelModule.kt
index 8d55e05..bb9eb27 100644
--- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/module/TunnelModule.kt
+++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/module/TunnelModule.kt
@@ -4,11 +4,8 @@ import android.content.Context
import com.wireguard.android.backend.Backend
import com.wireguard.android.backend.GoBackend
import com.wireguard.android.backend.RootTunnelActionHandler
-import com.wireguard.android.backend.WgQuickBackend
import com.wireguard.android.util.RootShell
-import com.wireguard.android.util.ToolsInstaller
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
-import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelService
import com.zaneschepke.wireguardautotunnel.service.tunnel.WireGuardTunnel
import dagger.Module
@@ -46,8 +43,8 @@ class TunnelModule {
@Provides
@Singleton
@Kernel
- fun provideKernelBackend(@ApplicationContext context: Context, rootShell: RootShell): Backend {
- return WgQuickBackend(context, rootShell, ToolsInstaller(context, rootShell), RootTunnelActionHandler(rootShell))
+ fun provideKernelBackend(@ApplicationContext context: Context, rootShell: org.amnezia.awg.util.RootShell): org.amnezia.awg.backend.Backend {
+ return org.amnezia.awg.backend.AwgQuickBackend(context, rootShell, org.amnezia.awg.util.ToolsInstaller(context, rootShell))
}
@Provides
@@ -61,7 +58,7 @@ class TunnelModule {
fun provideVpnService(
amneziaBackend: Provider,
@Userspace userspaceBackend: Provider,
- @Kernel kernelBackend: Provider,
+ @Kernel kernelBackend: Provider,
appDataRepository: AppDataRepository,
@ApplicationScope applicationScope: CoroutineScope,
@IoDispatcher ioDispatcher: CoroutineDispatcher,
@@ -75,10 +72,4 @@ class TunnelModule {
ioDispatcher,
)
}
-
- @Provides
- @Singleton
- fun provideServiceManager(): ServiceManager {
- return ServiceManager()
- }
}
diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/receiver/AppUpdateReceiver.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/receiver/AppUpdateReceiver.kt
index e9a17f6..b60e9a4 100644
--- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/receiver/AppUpdateReceiver.kt
+++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/receiver/AppUpdateReceiver.kt
@@ -24,9 +24,6 @@ class AppUpdateReceiver : BroadcastReceiver() {
@Inject
lateinit var appDataRepository: AppDataRepository
- @Inject
- lateinit var serviceManager: ServiceManager
-
@Inject
lateinit var tunnelService: TunnelService
@@ -36,7 +33,7 @@ class AppUpdateReceiver : BroadcastReceiver() {
val settings = appDataRepository.settings.getSettings()
if (settings.isAutoTunnelEnabled) {
Timber.i("Restarting services after upgrade")
- serviceManager.startWatcherServiceForeground(context)
+ ServiceManager.startWatcherServiceForeground(context)
}
if (!settings.isAutoTunnelEnabled || settings.isAutoTunnelPaused) {
val tunnels = appDataRepository.tunnels.getAll().filter { it.isActive }
diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/receiver/BackgroundActionReceiver.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/receiver/BackgroundActionReceiver.kt
index 700c4e8..aea9c42 100644
--- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/receiver/BackgroundActionReceiver.kt
+++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/receiver/BackgroundActionReceiver.kt
@@ -27,9 +27,6 @@ class BackgroundActionReceiver : BroadcastReceiver() {
@Inject
lateinit var tunnelConfigRepository: TunnelConfigRepository
- @Inject
- lateinit var serviceManager: ServiceManager
-
override fun onReceive(context: Context, intent: Intent) {
val id = intent.getIntExtra(TUNNEL_ID_EXTRA_KEY, 0)
if (id == 0) return
@@ -39,7 +36,7 @@ class BackgroundActionReceiver : BroadcastReceiver() {
applicationScope.launch {
val tunnel = tunnelConfigRepository.getById(id)
tunnel?.let {
- serviceManager.startTunnelBackgroundService(context)
+ ServiceManager.startTunnelBackgroundService(context)
tunnelService.get().startTunnel(it)
}
}
@@ -48,7 +45,7 @@ class BackgroundActionReceiver : BroadcastReceiver() {
applicationScope.launch {
val tunnel = tunnelConfigRepository.getById(id)
tunnel?.let {
- serviceManager.stopTunnelBackgroundService(context)
+ ServiceManager.stopTunnelBackgroundService(context)
tunnelService.get().stopTunnel(it)
}
}
diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/receiver/BootReceiver.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/receiver/BootReceiver.kt
index 592d490..9a12b63 100644
--- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/receiver/BootReceiver.kt
+++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/receiver/BootReceiver.kt
@@ -23,9 +23,6 @@ class BootReceiver : BroadcastReceiver() {
@Inject
lateinit var tunnelService: Provider
- @Inject
- lateinit var serviceManager: ServiceManager
-
@Inject
@ApplicationScope
lateinit var applicationScope: CoroutineScope
@@ -33,16 +30,18 @@ class BootReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
if (Intent.ACTION_BOOT_COMPLETED != intent.action) return
applicationScope.launch {
- val settings = appDataRepository.settings.getSettings()
- if (settings.isRestoreOnBootEnabled) {
- appDataRepository.getStartTunnelConfig()?.let {
- context.startTunnelBackground(it.id)
+ with(appDataRepository.settings.getSettings()) {
+ if (isRestoreOnBootEnabled) {
+ val activeTunnels = appDataRepository.tunnels.getActive()
+ if (activeTunnels.isNotEmpty()) {
+ context.startTunnelBackground(activeTunnels.first().id)
+ }
+ if (isAutoTunnelEnabled) {
+ Timber.i("Starting watcher service from boot")
+ ServiceManager.startWatcherServiceForeground(context)
+ }
}
}
- if (settings.isAutoTunnelEnabled) {
- Timber.i("Starting watcher service from boot")
- serviceManager.startWatcherServiceForeground(context)
- }
}
}
}
diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/AutoTunnelService.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/AutoTunnelService.kt
index 9f98b37..10e0432 100644
--- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/AutoTunnelService.kt
+++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/AutoTunnelService.kt
@@ -1,11 +1,14 @@
package com.zaneschepke.wireguardautotunnel.service.foreground
import android.content.Context
-import android.os.Bundle
+import android.content.Intent
+import android.os.IBinder
import android.os.PowerManager
import androidx.core.app.ServiceCompat
+import androidx.lifecycle.LifecycleService
import androidx.lifecycle.lifecycleScope
import com.zaneschepke.wireguardautotunnel.R
+import com.zaneschepke.wireguardautotunnel.data.domain.Settings
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
import com.zaneschepke.wireguardautotunnel.module.IoDispatcher
@@ -19,11 +22,17 @@ import com.zaneschepke.wireguardautotunnel.service.notification.NotificationServ
import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelService
import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelState
import com.zaneschepke.wireguardautotunnel.util.Constants
+import com.zaneschepke.wireguardautotunnel.util.extensions.cancelWithMessage
+import com.zaneschepke.wireguardautotunnel.util.extensions.isMatchingToWildcardList
+import com.zaneschepke.wireguardautotunnel.util.extensions.isReachable
+import com.zaneschepke.wireguardautotunnel.util.extensions.onNotRunning
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.collectLatest
+import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
@@ -33,7 +42,7 @@ import javax.inject.Inject
import javax.inject.Provider
@AndroidEntryPoint
-class AutoTunnelService : ForegroundService() {
+class AutoTunnelService : LifecycleService() {
private val foregroundId = 122
@Inject
@@ -65,8 +74,14 @@ class AutoTunnelService : ForegroundService() {
private val networkEventsFlow = MutableStateFlow(AutoTunnelState())
private var wakeLock: PowerManager.WakeLock? = null
- private val tag = this.javaClass.name
+ private var wifiJob: Job? = null
+ private var mobileDataJob: Job? = null
+ private var ethernetJob: Job? = null
+ private var pingJob: Job? = null
+ private var networkEventJob: Job? = null
+
+ @get:Synchronized @set:Synchronized
private var running: Boolean = false
override fun onCreate() {
@@ -80,6 +95,26 @@ class AutoTunnelService : ForegroundService() {
}
}
+ override fun onBind(intent: Intent): IBinder? {
+ super.onBind(intent)
+ // We don't provide binding, so return null
+ return null
+ }
+
+ override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
+ Timber.d("onStartCommand executed with startId: $startId")
+ if (intent != null) {
+ val action = intent.action
+ when (action) {
+ Action.START.name,
+ Action.START_FOREGROUND.name,
+ -> startService()
+ Action.STOP.name, Action.STOP_FOREGROUND.name -> stopService()
+ }
+ }
+ return super.onStartCommand(intent, flags, startId)
+ }
+
private suspend fun launchNotification() {
if (appDataRepository.settings.getSettings().isAutoTunnelPaused) {
launchWatcherPausedNotification()
@@ -88,27 +123,33 @@ class AutoTunnelService : ForegroundService() {
}
}
- override fun startService(extras: Bundle?) {
- super.startService(extras)
+ private fun startService() {
if (running) return
+ running = true
kotlin.runCatching {
lifecycleScope.launch(mainImmediateDispatcher) {
launchNotification()
initWakeLock()
}
- startWatcherJob()
+ startSettingsJob()
}.onFailure {
Timber.e(it)
}
}
- override fun stopService() {
- super.stopService()
+ private fun stopService() {
wakeLock?.let {
if (it.isHeld) {
it.release()
}
}
+ stopSelf()
+ }
+
+ override fun onDestroy() {
+ cancelAndResetNetworkJobs()
+ cancelAndResetPingJob()
+ super.onDestroy()
}
private fun launchWatcherNotification(description: String = getString(R.string.watcher_notification_text_active)) {
@@ -134,6 +175,7 @@ class AutoTunnelService : ForegroundService() {
private fun initWakeLock() {
wakeLock =
(getSystemService(Context.POWER_SERVICE) as PowerManager).run {
+ val tag = this.javaClass.name
newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "$tag::lock").apply {
try {
Timber.i("Initiating wakelock with 10 min timeout")
@@ -145,43 +187,33 @@ class AutoTunnelService : ForegroundService() {
}
}
- private fun startWatcherJob() = lifecycleScope.launch {
- val setting = appDataRepository.settings.getSettings()
- launch {
- Timber.i("Starting wifi watcher")
- watchForWifiConnectivityChanges()
- }
- if (setting.isTunnelOnMobileDataEnabled) {
- launch {
- Timber.i("Starting mobile data watcher")
- watchForMobileDataConnectivityChanges()
- }
- }
- if (setting.isTunnelOnEthernetEnabled) {
- launch {
- Timber.i("Starting ethernet data watcher")
- watchForEthernetConnectivityChanges()
- }
- }
- launch {
- Timber.i("Starting settings watcher")
- watchForSettingsChanges()
- }
- if (setting.isPingEnabled) {
- launch {
- Timber.i("Starting ping watcher")
- watchForPingFailure()
- }
- }
- launch {
- Timber.i("Starting management watcher")
- manageVpn()
- }
- running = true
+ private fun startSettingsJob() = lifecycleScope.launch {
+ watchForSettingsChanges()
+ }
+
+ private fun startWifiJob() = lifecycleScope.launch {
+ watchForWifiConnectivityChanges()
+ }
+
+ private fun startMobileDataJob() = lifecycleScope.launch {
+ watchForMobileDataConnectivityChanges()
+ }
+
+ private fun startEthernetJob() = lifecycleScope.launch {
+ watchForEthernetConnectivityChanges()
+ }
+
+ private fun startPingJob() = lifecycleScope.launch {
+ watchForPingFailure()
+ }
+
+ private fun startNetworkEventJob() = lifecycleScope.launch {
+ handleNetworkEventChanges()
}
private suspend fun watchForMobileDataConnectivityChanges() {
withContext(ioDispatcher) {
+ Timber.i("Starting mobile data watcher")
mobileDataService.networkStatus.collect { status ->
when (status) {
is NetworkStatus.Available -> {
@@ -217,92 +249,155 @@ class AutoTunnelService : ForegroundService() {
private suspend fun watchForPingFailure() {
withContext(ioDispatcher) {
- try {
+ Timber.i("Starting ping watcher")
+ runCatching {
do {
- if (tunnelService.get().vpnState.value.status == TunnelState.UP) {
- val tunnelConfig = tunnelService.get().vpnState.value.tunnelConfig
- tunnelConfig?.let {
- val config = TunnelConfig.configFromWgQuick(it.wgQuick)
- val results =
+ val vpnState = tunnelService.get().vpnState.value
+ if (vpnState.status == TunnelState.UP) {
+ if (vpnState.tunnelConfig != null) {
+ val config = TunnelConfig.configFromWgQuick(vpnState.tunnelConfig.wgQuick)
+ val results = if (vpnState.tunnelConfig.pingIp != null) {
+ Timber.d("Pinging custom ip : ${vpnState.tunnelConfig.pingIp}")
+ listOf(InetAddress.getByName(vpnState.tunnelConfig.pingIp).isReachable(Constants.PING_TIMEOUT.toInt()))
+ } else {
+ Timber.d("Pinging all peers")
config.peers.map { peer ->
- val host =
- if (peer.endpoint.isPresent &&
- peer.endpoint.get().resolved.isPresent
- ) {
- peer.endpoint.get().resolved.get().host
- } else {
- Constants.DEFAULT_PING_IP
- }
- Timber.i("Checking reachability of: $host")
- val reachable =
- InetAddress.getByName(host)
- .isReachable(Constants.PING_TIMEOUT.toInt())
- Timber.i("Result: reachable - $reachable")
- reachable
+ peer.isReachable()
}
+ }
+ Timber.i("Ping results reachable: $results")
if (results.contains(false)) {
Timber.i("Restarting VPN for ping failure")
- tunnelService.get().stopTunnel(it)
- delay(Constants.VPN_RESTART_DELAY)
- tunnelService.get().startTunnel(it)
- delay(Constants.PING_COOLDOWN)
+ val cooldown = vpnState.tunnelConfig.pingCooldown
+ tunnelService.get().bounceTunnel(vpnState.tunnelConfig)
+ delay(cooldown ?: Constants.PING_COOLDOWN)
+ continue
}
}
}
- delay(Constants.PING_INTERVAL)
+ delay(vpnState.tunnelConfig?.pingInterval ?: Constants.PING_INTERVAL)
} while (true)
- } catch (e: Exception) {
- Timber.e(e)
+ }.onFailure {
+ Timber.e(it)
+ }
+ }
+ }
+
+ private fun updateSettings(settings: Settings) {
+ networkEventsFlow.update {
+ it.copy(
+ settings = settings,
+ )
+ }
+ }
+
+ private fun onAutoTunnelPause(paused: Boolean) {
+ if (networkEventsFlow.value.settings.isAutoTunnelPaused
+ != paused
+ ) {
+ when (paused) {
+ true -> launchWatcherPausedNotification()
+ false -> launchWatcherNotification()
}
}
}
private suspend fun watchForSettingsChanges() {
- appDataRepository.settings.getSettingsFlow().collect { settings ->
- if (networkEventsFlow.value.settings.isAutoTunnelPaused
- != settings.isAutoTunnelPaused
- ) {
- when (settings.isAutoTunnelPaused) {
- true -> launchWatcherPausedNotification()
- false -> launchWatcherNotification()
+ Timber.i("Starting settings watcher")
+ withContext(ioDispatcher) {
+ appDataRepository.settings.getSettingsFlow().combine(
+ appDataRepository.tunnels.getTunnelConfigsFlow(),
+ ) { settings, tunnels ->
+ val activeTunnel = tunnels.firstOrNull { it.isActive }
+ if (!settings.isPingEnabled) {
+ settings.copy(isPingEnabled = activeTunnel?.isPingEnabled ?: false)
+ } else {
+ settings
}
+ }.collect {
+ Timber.d("Settings change: $it")
+ onAutoTunnelPause(it.isAutoTunnelPaused)
+ updateSettings(it)
+ manageJobsBySettings(it)
}
- networkEventsFlow.update {
- it.copy(
- settings = settings,
- )
+ }
+ }
+
+ private fun manageJobsBySettings(settings: Settings) {
+ with(settings) {
+ if (isPingEnabled) {
+ pingJob.onNotRunning { pingJob = startPingJob() }
+ } else {
+ cancelAndResetPingJob()
}
+ if (isTunnelOnWifiEnabled || isTunnelOnEthernetEnabled || isTunnelOnMobileDataEnabled) {
+ startNetworkJobs()
+ } else {
+ cancelAndResetNetworkJobs()
+ }
+ }
+ }
+
+ private fun startNetworkJobs() {
+ wifiJob.onNotRunning {
+ Timber.i("Wifi job starting")
+ wifiJob = startWifiJob()
+ }
+ ethernetJob.onNotRunning {
+ ethernetJob = startEthernetJob()
+ Timber.i("Ethernet job starting")
+ }
+ mobileDataJob.onNotRunning {
+ mobileDataJob = startMobileDataJob()
+ Timber.i("Mobile data job starting")
+ }
+ networkEventJob.onNotRunning {
+ Timber.i("Network event job starting")
+ networkEventJob = startNetworkEventJob()
+ }
+ }
+
+ private fun cancelAndResetPingJob() {
+ pingJob?.cancelWithMessage("Ping job canceled")
+ pingJob = null
+ }
+
+ private fun cancelAndResetNetworkJobs() {
+ networkEventJob?.cancelWithMessage("Network event job canceled")
+ wifiJob?.cancelWithMessage("Wifi job canceled")
+ ethernetJob?.cancelWithMessage("Ethernet job canceled")
+ mobileDataJob?.cancelWithMessage("Mobile data job canceled")
+ networkEventJob = null
+ wifiJob = null
+ ethernetJob = null
+ mobileDataJob = null
+ }
+
+ private fun updateEthernet(connected: Boolean) {
+ networkEventsFlow.update {
+ it.copy(
+ isEthernetConnected = connected,
+ )
}
}
private suspend fun watchForEthernetConnectivityChanges() {
withContext(ioDispatcher) {
+ Timber.i("Starting ethernet data watcher")
ethernetService.networkStatus.collect { status ->
when (status) {
is NetworkStatus.Available -> {
Timber.i("Gained Ethernet connection")
- networkEventsFlow.update {
- it.copy(
- isEthernetConnected = true,
- )
- }
+ updateEthernet(true)
}
is NetworkStatus.CapabilitiesChanged -> {
Timber.i("Ethernet capabilities changed")
- networkEventsFlow.update {
- it.copy(
- isEthernetConnected = true,
- )
- }
+ updateEthernet(true)
}
is NetworkStatus.Unavailable -> {
- networkEventsFlow.update {
- it.copy(
- isEthernetConnected = false,
- )
- }
+ updateEthernet(false)
Timber.i("Lost Ethernet connection")
}
}
@@ -312,6 +407,7 @@ class AutoTunnelService : ForegroundService() {
private suspend fun watchForWifiConnectivityChanges() {
withContext(ioDispatcher) {
+ Timber.i("Starting wifi watcher")
wifiService.networkStatus.collect { status ->
when (status) {
is NetworkStatus.Available -> {
@@ -371,8 +467,9 @@ class AutoTunnelService : ForegroundService() {
return tunnelService.get().vpnState.value.status == TunnelState.DOWN
}
- private suspend fun manageVpn() {
+ private suspend fun handleNetworkEventChanges() {
withContext(ioDispatcher) {
+ Timber.i("Starting network event watcher")
networkEventsFlow.collectLatest { watcherState ->
val autoTunnel = "Auto-tunnel watcher"
if (!watcherState.settings.isAutoTunnelPaused) {
@@ -412,8 +509,9 @@ class AutoTunnelService : ForegroundService() {
}
watcherState.isUntrustedWifiConditionMet() -> {
- if (activeTunnel?.tunnelNetworks?.contains(watcherState.currentNetworkSSID) == false ||
- activeTunnel == null
+ Timber.i("Untrusted wifi condition met")
+ if (activeTunnel?.tunnelNetworks?.isMatchingToWildcardList(watcherState.currentNetworkSSID) == false ||
+ activeTunnel == null || isTunnelDown()
) {
Timber.i(
"$autoTunnel - tunnel on ssid not associated with current tunnel condition met",
diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/AutoTunnelState.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/AutoTunnelState.kt
index 518b07e..c030879 100644
--- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/AutoTunnelState.kt
+++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/AutoTunnelState.kt
@@ -1,6 +1,7 @@
package com.zaneschepke.wireguardautotunnel.service.foreground
import com.zaneschepke.wireguardautotunnel.data.domain.Settings
+import com.zaneschepke.wireguardautotunnel.util.extensions.isMatchingToWildcardList
data class AutoTunnelState(
val isWifiConnected: Boolean = false,
@@ -38,7 +39,7 @@ data class AutoTunnelState(
return (
!isEthernetConnected &&
isWifiConnected &&
- !settings.trustedNetworkSSIDs.contains(currentNetworkSSID) &&
+ !settings.trustedNetworkSSIDs.isMatchingToWildcardList(currentNetworkSSID) &&
settings.isTunnelOnWifiEnabled
)
}
@@ -48,7 +49,7 @@ data class AutoTunnelState(
!isEthernetConnected &&
(
isWifiConnected &&
- settings.trustedNetworkSSIDs.contains(currentNetworkSSID)
+ settings.trustedNetworkSSIDs.isMatchingToWildcardList(currentNetworkSSID)
)
)
}
diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/ForegroundService.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/ForegroundService.kt
deleted file mode 100644
index 908b802..0000000
--- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/ForegroundService.kt
+++ /dev/null
@@ -1,57 +0,0 @@
-package com.zaneschepke.wireguardautotunnel.service.foreground
-
-import android.content.Intent
-import android.os.Bundle
-import android.os.IBinder
-import androidx.lifecycle.LifecycleService
-import com.zaneschepke.wireguardautotunnel.util.Constants
-import timber.log.Timber
-
-open class ForegroundService : LifecycleService() {
- private var isServiceStarted = false
-
- override fun onBind(intent: Intent): IBinder? {
- super.onBind(intent)
- // We don't provide binding, so return null
- return null
- }
-
- override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
- super.onStartCommand(intent, flags, startId)
- Timber.d("onStartCommand executed with startId: $startId")
- if (intent != null) {
- val action = intent.action
- when (action) {
- Action.START.name,
- Action.START_FOREGROUND.name,
- -> startService(intent.extras)
-
- Action.STOP.name, Action.STOP_FOREGROUND.name -> stopService()
- Constants.ALWAYS_ON_VPN_ACTION -> {
- Timber.i("Always-on VPN starting service")
- startService(intent.extras)
- }
-
- else -> Timber.d("This should never happen. No action in the received intent")
- }
- } else {
- Timber.d(
- "with a null intent. It has been probably restarted by the system.",
- )
- }
- return START_STICKY
- }
-
- protected open fun startService(extras: Bundle?) {
- if (isServiceStarted) return
- Timber.d("Starting ${this.javaClass.simpleName}")
- isServiceStarted = true
- }
-
- protected open fun stopService() {
- Timber.d("Stopping ${this.javaClass.simpleName}")
- stopForeground(STOP_FOREGROUND_REMOVE)
- stopSelf()
- isServiceStarted = false
- }
-}
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 5f20da3..71939ea 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
@@ -3,10 +3,12 @@ package com.zaneschepke.wireguardautotunnel.service.foreground
import android.app.Service
import android.content.Context
import android.content.Intent
+import android.net.VpnService
import timber.log.Timber
-class ServiceManager {
+object ServiceManager {
private fun actionOnService(action: Action, context: Context, cls: Class, extras: Map? = null) {
+ if (VpnService.prepare(context) != null) return
val intent =
Intent(context, cls).also {
it.action = action.name
diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/TunnelBackgroundService.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/TunnelBackgroundService.kt
index fb94715..b59082f 100644
--- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/TunnelBackgroundService.kt
+++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/TunnelBackgroundService.kt
@@ -1,14 +1,16 @@
package com.zaneschepke.wireguardautotunnel.service.foreground
import android.app.Notification
-import android.os.Bundle
+import android.content.Intent
+import android.os.IBinder
+import androidx.lifecycle.LifecycleService
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.service.notification.NotificationService
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
@AndroidEntryPoint
-class TunnelBackgroundService : ForegroundService() {
+class TunnelBackgroundService : LifecycleService() {
@Inject
lateinit var notificationService: NotificationService
@@ -20,14 +22,32 @@ class TunnelBackgroundService : ForegroundService() {
startForeground(foregroundId, createNotification())
}
- override fun startService(extras: Bundle?) {
- super.startService(extras)
+ override fun onBind(intent: Intent): IBinder? {
+ super.onBind(intent)
+ // We don't provide binding, so return null
+ return null
+ }
+
+ override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
+ if (intent != null) {
+ val action = intent.action
+ when (action) {
+ Action.START.name,
+ Action.START_FOREGROUND.name,
+ -> startService()
+ Action.STOP.name, Action.STOP_FOREGROUND.name -> stopService()
+ }
+ }
+ return super.onStartCommand(intent, flags, startId)
+ }
+
+ private fun startService() {
startForeground(foregroundId, createNotification())
}
- override fun stopService() {
- super.stopService()
+ private fun stopService() {
stopForeground(STOP_FOREGROUND_REMOVE)
+ stopSelf()
}
private fun createNotification(): Notification {
diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tile/AutoTunnelControlTile.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tile/AutoTunnelControlTile.kt
index 8036765..3bcd9db 100644
--- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tile/AutoTunnelControlTile.kt
+++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tile/AutoTunnelControlTile.kt
@@ -12,7 +12,6 @@ import androidx.lifecycle.lifecycleScope
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
import com.zaneschepke.wireguardautotunnel.module.ApplicationScope
-import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
@@ -24,9 +23,6 @@ class AutoTunnelControlTile : TileService(), LifecycleOwner {
@Inject
lateinit var appDataRepository: AppDataRepository
- @Inject
- lateinit var serviceManager: ServiceManager
-
@Inject
@ApplicationScope
lateinit var applicationScope: CoroutineScope
diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tunnel/TunnelService.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tunnel/TunnelService.kt
index d40af70..d1279dc 100644
--- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tunnel/TunnelService.kt
+++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tunnel/TunnelService.kt
@@ -9,6 +9,8 @@ interface TunnelService : Tunnel, org.amnezia.awg.backend.Tunnel {
suspend fun stopTunnel(tunnelConfig: TunnelConfig): Result
+ suspend fun bounceTunnel(tunnelConfig: TunnelConfig): Result
+
val vpnState: StateFlow
suspend fun runningTunnelNames(): Set
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 27f20cc..c510ad1 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
@@ -33,11 +33,12 @@ class WireGuardTunnel
constructor(
private val amneziaBackend: Provider,
@Userspace private val userspaceBackend: Provider,
- @Kernel private val kernelBackend: Provider,
+ @Kernel private val kernelBackend: Provider,
private val appDataRepository: AppDataRepository,
@ApplicationScope private val applicationScope: CoroutineScope,
@IoDispatcher private val ioDispatcher: CoroutineDispatcher,
) : TunnelService {
+
private val _vpnState = MutableStateFlow(VpnState())
override val vpnState: StateFlow = _vpnState.asStateFlow()
@@ -84,30 +85,42 @@ constructor(
override suspend fun startTunnel(tunnelConfig: TunnelConfig): Result {
return withContext(ioDispatcher) {
if (_vpnState.value.status == TunnelState.UP) vpnState.value.tunnelConfig?.let { stopTunnel(it) }
- appDataRepository.tunnels.save(tunnelConfig.copy(isActive = true))
- appDataRepository.appState.setLastActiveTunnelId(tunnelConfig.id)
emitTunnelConfig(tunnelConfig)
setState(tunnelConfig, TunnelState.UP).onSuccess {
emitTunnelState(it)
- WireGuardAutoTunnel.instance.requestTunnelTileServiceStateUpdate()
+ appDataRepository.tunnels.save(tunnelConfig.copy(isActive = true))
}.onFailure {
- appDataRepository.tunnels.save(tunnelConfig.copy(isActive = false))
- WireGuardAutoTunnel.instance.requestTunnelTileServiceStateUpdate()
+ Timber.e(it)
}
}
}
override suspend fun stopTunnel(tunnelConfig: TunnelConfig): Result {
return withContext(ioDispatcher) {
- appDataRepository.tunnels.save(tunnelConfig.copy(isActive = false))
setState(tunnelConfig, TunnelState.DOWN).onSuccess {
+ emitTunnelState(it)
+ appDataRepository.tunnels.save(tunnelConfig.copy(isActive = false))
+ resetBackendStatistics()
+ }.onFailure {
+ Timber.e(it)
+ }
+ }
+ }
+
+ // use this when we just want to bounce tunnel and not change tunnelConfig active state
+ override suspend fun bounceTunnel(tunnelConfig: TunnelConfig): Result {
+ toggleTunnel(tunnelConfig)
+ delay(Constants.VPN_RESTART_DELAY)
+ return toggleTunnel(tunnelConfig)
+ }
+
+ private suspend fun toggleTunnel(tunnelConfig: TunnelConfig): Result {
+ return withContext(ioDispatcher) {
+ setState(tunnelConfig, TunnelState.TOGGLE).onSuccess {
emitTunnelState(it)
resetBackendStatistics()
- WireGuardAutoTunnel.instance.requestTunnelTileServiceStateUpdate()
}.onFailure {
Timber.e(it)
- appDataRepository.tunnels.save(tunnelConfig.copy(isActive = true))
- WireGuardAutoTunnel.instance.requestTunnelTileServiceStateUpdate()
}
}
}
diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/AppUiState.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/AppUiState.kt
index 281f346..04cdace 100644
--- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/AppUiState.kt
+++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/AppUiState.kt
@@ -1,8 +1,13 @@
package com.zaneschepke.wireguardautotunnel.ui
+import com.zaneschepke.wireguardautotunnel.data.domain.GeneralState
+import com.zaneschepke.wireguardautotunnel.data.domain.Settings
+import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
+import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnState
+
data class AppUiState(
- val snackbarMessage: String = "",
- val snackbarMessageConsumed: Boolean = true,
- val notificationPermissionAccepted: Boolean = false,
- val requestPermissions: Boolean = false,
+ val settings: Settings = Settings(),
+ val tunnels: List = emptyList(),
+ val vpnState: VpnState = VpnState(),
+ val generalState: GeneralState = GeneralState(),
)
diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/AppViewModel.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/AppViewModel.kt
index 620ad54..a4ffafe 100644
--- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/AppViewModel.kt
+++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/AppViewModel.kt
@@ -2,11 +2,16 @@ package com.zaneschepke.wireguardautotunnel.ui
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
+import androidx.navigation.NavHostController
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
+import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelService
+import com.zaneschepke.wireguardautotunnel.util.Constants
+import com.zaneschepke.wireguardautotunnel.util.extensions.TunnelConfigs
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.asStateFlow
-import kotlinx.coroutines.flow.update
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import xyz.teamgravity.pin_lock_compose.PinManager
import javax.inject.Inject
@@ -16,30 +21,38 @@ class AppViewModel
@Inject
constructor(
private val appDataRepository: AppDataRepository,
+ private val tunnelService: TunnelService,
+ val navHostController: NavHostController,
) : ViewModel() {
- private val _appUiState =
- MutableStateFlow(
- AppUiState(),
+ private val _appUiState = MutableStateFlow(AppUiState())
+
+ val uiState =
+ combine(
+ appDataRepository.settings.getSettingsFlow(),
+ appDataRepository.tunnels.getTunnelConfigsFlow(),
+ tunnelService.vpnState,
+ appDataRepository.appState.generalStateFlow,
+ ) { settings, tunnels, tunnelState, generalState ->
+ AppUiState(
+ settings,
+ tunnels,
+ tunnelState,
+ generalState,
+ )
+ }
+ .stateIn(
+ viewModelScope,
+ SharingStarted.WhileSubscribed(Constants.SUBSCRIPTION_TIMEOUT),
+ _appUiState.value,
+ )
+
+ fun setTunnels(tunnels: TunnelConfigs) = viewModelScope.launch {
+ _appUiState.emit(
+ _appUiState.value.copy(
+ tunnels = tunnels,
+ ),
)
- val appUiState = _appUiState.asStateFlow()
-
- fun showSnackbarMessage(message: String) {
- _appUiState.update {
- it.copy(
- snackbarMessage = message,
- snackbarMessageConsumed = false,
- )
- }
- }
-
- fun snackbarMessageConsumed() {
- _appUiState.update {
- it.copy(
- snackbarMessage = "",
- snackbarMessageConsumed = true,
- )
- }
}
fun onPinLockDisabled() = viewModelScope.launch {
diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/MainActivity.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/MainActivity.kt
index 071e1ab..6a2487a 100644
--- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/MainActivity.kt
+++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/MainActivity.kt
@@ -14,10 +14,7 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.SnackbarData
-import androidx.compose.material3.SnackbarDuration
import androidx.compose.material3.SnackbarHost
-import androidx.compose.material3.SnackbarHostState
-import androidx.compose.material3.SnackbarResult
import androidx.compose.material3.Surface
import androidx.compose.material3.surfaceColorAtElevation
import androidx.compose.runtime.LaunchedEffect
@@ -31,17 +28,18 @@ import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
-import androidx.lifecycle.lifecycleScope
import androidx.navigation.NavType
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.currentBackStackEntryAsState
-import androidx.navigation.compose.rememberNavController
import androidx.navigation.navArgument
import com.zaneschepke.wireguardautotunnel.data.repository.AppStateRepository
+import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelService
+import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelState
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.BottomNavBar
import com.zaneschepke.wireguardautotunnel.ui.common.prompt.CustomSnackBar
+import com.zaneschepke.wireguardautotunnel.ui.common.snackbar.SnackbarControllerProvider
import com.zaneschepke.wireguardautotunnel.ui.screens.config.ConfigScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.main.ConfigType
import com.zaneschepke.wireguardautotunnel.ui.screens.main.MainScreen
@@ -52,10 +50,8 @@ import com.zaneschepke.wireguardautotunnel.ui.screens.support.SupportScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.support.logs.LogsScreen
import com.zaneschepke.wireguardautotunnel.ui.theme.WireguardAutoTunnelTheme
import com.zaneschepke.wireguardautotunnel.util.Constants
-import com.zaneschepke.wireguardautotunnel.util.StringValue
+import com.zaneschepke.wireguardautotunnel.util.extensions.requestTunnelTileServiceStateUpdate
import dagger.hilt.android.AndroidEntryPoint
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.launch
import javax.inject.Inject
@AndroidEntryPoint
@@ -71,158 +67,148 @@ class MainActivity : AppCompatActivity() {
val isPinLockEnabled = intent.extras?.getBoolean(SplashActivity.IS_PIN_LOCK_ENABLED_KEY)
- enableEdgeToEdge(navigationBarStyle = SystemBarStyle.dark(Color.Transparent.toArgb()))
+ enableEdgeToEdge(
+ navigationBarStyle = SystemBarStyle.auto(
+ lightScrim = Color.Transparent.toArgb(),
+ darkScrim = Color.Transparent.toArgb(),
+ ),
+ )
setContent {
val appViewModel = hiltViewModel()
- val appUiState by appViewModel.appUiState.collectAsStateWithLifecycle()
- val navController = rememberNavController()
+ val appUiState by appViewModel.uiState.collectAsStateWithLifecycle(lifecycle = this.lifecycle)
+ val navController = appViewModel.navHostController
val navBackStackEntry by navController.currentBackStackEntryAsState()
- val snackbarHostState = remember { SnackbarHostState() }
-
- fun showSnackBarMessage(message: StringValue) {
- lifecycleScope.launch(Dispatchers.Main) {
- val result =
- snackbarHostState.showSnackbar(
- message = message.asString(this@MainActivity),
- duration = SnackbarDuration.Short,
- )
- when (result) {
- SnackbarResult.ActionPerformed,
- SnackbarResult.Dismissed,
- -> {
- snackbarHostState.currentSnackbarData?.dismiss()
- }
- }
+ LaunchedEffect(appUiState.vpnState.status) {
+ val context = this@MainActivity
+ when (appUiState.vpnState.status) {
+ TunnelState.DOWN -> ServiceManager.stopTunnelBackgroundService(context)
+ else -> Unit
}
+ context.requestTunnelTileServiceStateUpdate()
}
- WireguardAutoTunnelTheme {
- LaunchedEffect(appUiState.snackbarMessageConsumed) {
- if (!appUiState.snackbarMessageConsumed) {
- showSnackBarMessage(StringValue.DynamicString(appUiState.snackbarMessage))
- appViewModel.snackbarMessageConsumed()
- }
- }
-
- val focusRequester = remember { FocusRequester() }
-
- Scaffold(
- snackbarHost = {
- SnackbarHost(snackbarHostState) { snackbarData: SnackbarData ->
- CustomSnackBar(
- snackbarData.visuals.message,
- isRtl = false,
- containerColor =
- MaterialTheme.colorScheme.surfaceColorAtElevation(
- 2.dp,
- ),
- )
- }
- },
- containerColor = MaterialTheme.colorScheme.background,
- modifier =
- Modifier
- .focusable()
- .focusProperties {
- when (navBackStackEntry?.destination?.route) {
- Screen.Lock.route -> Unit
- else -> up = focusRequester
+ SnackbarControllerProvider { host ->
+ WireguardAutoTunnelTheme {
+ val focusRequester = remember { FocusRequester() }
+ Scaffold(
+ snackbarHost = {
+ SnackbarHost(host) { snackbarData: SnackbarData ->
+ CustomSnackBar(
+ snackbarData.visuals.message,
+ isRtl = false,
+ containerColor =
+ MaterialTheme.colorScheme.surfaceColorAtElevation(
+ 2.dp,
+ ),
+ )
}
},
- bottomBar = {
- BottomNavBar(
- navController,
- listOf(
- Screen.Main.navItem,
- Screen.Settings.navItem,
- Screen.Support.navItem,
- ),
- )
- },
- ) { padding ->
- Surface(modifier = Modifier.fillMaxSize().padding(padding)) {
- NavHost(
- navController,
- enterTransition = { fadeIn(tween(Constants.TRANSITION_ANIMATION_TIME)) },
- exitTransition = { fadeOut(tween(Constants.TRANSITION_ANIMATION_TIME)) },
- startDestination = (if (isPinLockEnabled == true) Screen.Lock.route else Screen.Main.route),
- ) {
- composable(
- Screen.Main.route,
- ) {
- MainScreen(
- focusRequester = focusRequester,
- appViewModel = appViewModel,
- navController = navController,
- )
- }
- composable(
- Screen.Settings.route,
- ) {
- SettingsScreen(
- appViewModel = appViewModel,
- navController = navController,
- focusRequester = focusRequester,
- )
- }
- composable(
- Screen.Support.route,
- ) {
- SupportScreen(
- focusRequester = focusRequester,
- navController = navController,
- )
- }
- composable(Screen.Support.Logs.route) {
- LogsScreen()
- }
- composable(
- "${Screen.Config.route}/{id}?configType={configType}",
- arguments =
+ containerColor = MaterialTheme.colorScheme.background,
+ modifier =
+ Modifier
+ .focusable()
+ .focusProperties {
+ when (navBackStackEntry?.destination?.route) {
+ Screen.Lock.route -> Unit
+ else -> up = focusRequester
+ }
+ },
+ bottomBar = {
+ BottomNavBar(
+ navController,
listOf(
- navArgument("id") {
- type = NavType.StringType
- defaultValue = "0"
- },
- navArgument("configType") {
- type = NavType.StringType
- defaultValue = ConfigType.WIREGUARD.name
- },
+ Screen.Main.navItem,
+ Screen.Settings.navItem,
+ Screen.Support.navItem,
),
+ )
+ },
+ ) { padding ->
+ Surface(modifier = Modifier.fillMaxSize().padding(padding)) {
+ NavHost(
+ navController,
+ enterTransition = { fadeIn(tween(Constants.TRANSITION_ANIMATION_TIME)) },
+ exitTransition = { fadeOut(tween(Constants.TRANSITION_ANIMATION_TIME)) },
+ startDestination = (if (isPinLockEnabled == true) Screen.Lock.route else Screen.Main.route),
) {
- val id = it.arguments?.getString("id")
- val configType =
- ConfigType.valueOf(
- it.arguments?.getString("configType") ?: ConfigType.WIREGUARD.name,
- )
- if (!id.isNullOrBlank()) {
- ConfigScreen(
- navController = navController,
- tunnelId = id,
- appViewModel = appViewModel,
+ composable(
+ Screen.Main.route,
+ ) {
+ MainScreen(
focusRequester = focusRequester,
- configType = configType,
+ uiState = appUiState,
+ navController = navController,
)
}
- }
- composable("${Screen.Option.route}/{id}") {
- val id = it.arguments?.getString("id")
- if (!id.isNullOrBlank()) {
- OptionsScreen(
- navController = navController,
- tunnelId = id,
+ composable(
+ Screen.Settings.route,
+ ) {
+ SettingsScreen(
appViewModel = appViewModel,
+ uiState = appUiState,
+ navController = navController,
focusRequester = focusRequester,
)
}
- }
- composable(Screen.Lock.route) {
- PinLockScreen(
- navController = navController,
- appViewModel = appViewModel,
- )
+ composable(
+ Screen.Support.route,
+ ) {
+ SupportScreen(
+ focusRequester = focusRequester,
+ navController = navController,
+ appUiState = appUiState,
+ )
+ }
+ composable(Screen.Support.Logs.route) {
+ LogsScreen()
+ }
+ composable(
+ "${Screen.Config.route}/{id}?configType={configType}",
+ arguments =
+ listOf(
+ navArgument("id") {
+ type = NavType.StringType
+ defaultValue = "0"
+ },
+ navArgument("configType") {
+ type = NavType.StringType
+ defaultValue = ConfigType.WIREGUARD.name
+ },
+ ),
+ ) {
+ val id = it.arguments?.getString("id")
+ val configType =
+ ConfigType.valueOf(
+ it.arguments?.getString("configType") ?: ConfigType.WIREGUARD.name,
+ )
+ if (!id.isNullOrBlank()) {
+ ConfigScreen(
+ navController = navController,
+ tunnelId = id,
+ focusRequester = focusRequester,
+ configType = configType,
+ )
+ }
+ }
+ composable("${Screen.Option.route}/{id}") {
+ val id = it.arguments?.getString("id")
+ if (!id.isNullOrBlank()) {
+ OptionsScreen(
+ navController = navController,
+ tunnelId = id.toInt(),
+ focusRequester = focusRequester,
+ appUiState = appUiState,
+ )
+ }
+ }
+ composable(Screen.Lock.route) {
+ PinLockScreen(
+ navController = navController,
+ appViewModel = appViewModel,
+ )
+ }
}
}
}
diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/SplashActivity.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/SplashActivity.kt
index 8d2d946..f21f106 100644
--- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/SplashActivity.kt
+++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/SplashActivity.kt
@@ -5,6 +5,7 @@ import android.content.Intent
import android.os.Build
import android.os.Bundle
import androidx.activity.ComponentActivity
+import androidx.activity.viewModels
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
@@ -16,8 +17,8 @@ import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelService
import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelState
import com.zaneschepke.wireguardautotunnel.util.extensions.requestAutoTunnelTileServiceUpdate
-import com.zaneschepke.wireguardautotunnel.util.extensions.requestTunnelTileServiceStateUpdate
import dagger.hilt.android.AndroidEntryPoint
+import kotlinx.coroutines.async
import kotlinx.coroutines.launch
import xyz.teamgravity.pin_lock_compose.PinManager
import javax.inject.Inject
@@ -35,8 +36,7 @@ class SplashActivity : ComponentActivity() {
@Inject
lateinit var tunnelService: Provider
- @Inject
- lateinit var serviceManager: ServiceManager
+ private val appViewModel: AppViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
@@ -47,16 +47,28 @@ class SplashActivity : ComponentActivity() {
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.CREATED) {
- val pinLockEnabled = appStateRepository.isPinLockEnabled()
- if (pinLockEnabled) {
- PinManager.initialize(WireGuardAutoTunnel.instance)
- }
- val settings = appDataRepository.settings.getSettings()
- if (settings.isAutoTunnelEnabled) serviceManager.startWatcherService(application.applicationContext)
- if (tunnelService.get().getState() == TunnelState.UP) tunnelService.get().startStatsJob()
- val tunnels = appDataRepository.tunnels.getActive()
- if (tunnels.isNotEmpty() && tunnelService.get().getState() == TunnelState.DOWN) tunnelService.get().startTunnel(tunnels.first())
- requestTunnelTileServiceStateUpdate()
+ val pinLockEnabled = async {
+ appStateRepository.isPinLockEnabled().also {
+ if (it) PinManager.initialize(WireGuardAutoTunnel.instance)
+ }
+ }.await()
+ async {
+ val settings = appDataRepository.settings.getSettings()
+ if (settings.isAutoTunnelEnabled) ServiceManager.startWatcherService(application.applicationContext)
+ if (tunnelService.get().getState() == TunnelState.UP) tunnelService.get().startStatsJob()
+ val activeTunnels = appDataRepository.tunnels.getActive()
+ if (activeTunnels.isNotEmpty() &&
+ tunnelService.get().getState() == TunnelState.DOWN
+ ) {
+ tunnelService.get().startTunnel(activeTunnels.first())
+ }
+ }.await()
+
+ async {
+ val tunnels = appDataRepository.tunnels.getAll()
+ appViewModel.setTunnels(tunnels)
+ }.await()
+
requestAutoTunnelTileServiceUpdate()
val intent =
diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/ClickableIconButton.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/ClickableIconButton.kt
index ae1f193..cd1ed79 100644
--- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/ClickableIconButton.kt
+++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/ClickableIconButton.kt
@@ -12,7 +12,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
@Composable
-fun ClickableIconButton(onClick: () -> Unit, onIconClick: () -> Unit, text: String, icon: ImageVector, enabled: Boolean) {
+fun ClickableIconButton(onClick: () -> Unit, onIconClick: () -> Unit, text: String, icon: ImageVector, enabled: Boolean = true) {
TextButton(
onClick = onClick,
enabled = enabled,
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 24eae6c..9330a7a 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,5 +1,6 @@
package com.zaneschepke.wireguardautotunnel.ui.common.config
+import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.OutlinedTextField
@@ -14,23 +15,29 @@ fun ConfigurationTextBox(
value: String,
hint: String,
onValueChange: (String) -> Unit,
- keyboardActions: KeyboardActions,
+ keyboardActions: KeyboardActions = KeyboardActions(),
label: String,
modifier: Modifier,
+ isError: Boolean = false,
+ keyboardOptions: KeyboardOptions = KeyboardOptions(
+ capitalization = KeyboardCapitalization.None,
+ imeAction = ImeAction.Done,
+ ),
+ trailing: @Composable () -> Unit = {},
+ interactionSource: MutableInteractionSource? = null,
) {
OutlinedTextField(
+ isError = isError,
modifier = modifier,
value = value,
singleLine = true,
+ interactionSource = interactionSource,
onValueChange = { onValueChange(it) },
label = { Text(label) },
maxLines = 1,
placeholder = { Text(hint) },
- keyboardOptions =
- KeyboardOptions(
- capitalization = KeyboardCapitalization.None,
- imeAction = ImeAction.Done,
- ),
+ keyboardOptions = keyboardOptions,
keyboardActions = keyboardActions,
+ trailingIcon = trailing,
)
}
diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/config/ConfigurationToggle.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/config/ConfigurationToggle.kt
index f990030..bae787d 100644
--- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/config/ConfigurationToggle.kt
+++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/config/ConfigurationToggle.kt
@@ -13,7 +13,14 @@ import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.Dp
@Composable
-fun ConfigurationToggle(label: String, enabled: Boolean, checked: Boolean, padding: Dp, onCheckChanged: () -> Unit, modifier: Modifier = Modifier) {
+fun ConfigurationToggle(
+ label: String,
+ enabled: Boolean = true,
+ checked: Boolean,
+ padding: Dp,
+ onCheckChanged: () -> Unit,
+ modifier: Modifier = Modifier,
+) {
Row(
modifier =
Modifier
diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/config/SubmitConfigurationTextBox.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/config/SubmitConfigurationTextBox.kt
new file mode 100644
index 0000000..0986526
--- /dev/null
+++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/config/SubmitConfigurationTextBox.kt
@@ -0,0 +1,76 @@
+package com.zaneschepke.wireguardautotunnel.ui.common.config
+
+import androidx.compose.foundation.interaction.MutableInteractionSource
+import androidx.compose.foundation.interaction.collectIsFocusedAsState
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.text.KeyboardOptions
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.outlined.Save
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.focus.FocusRequester
+import androidx.compose.ui.focus.focusRequester
+import androidx.compose.ui.platform.LocalFocusManager
+import androidx.compose.ui.platform.LocalSoftwareKeyboardController
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.input.ImeAction
+import androidx.compose.ui.text.input.KeyboardCapitalization
+import com.zaneschepke.wireguardautotunnel.R
+
+@Composable
+fun SubmitConfigurationTextBox(
+ value: String?,
+ label: String,
+ hint: String,
+ focusRequester: FocusRequester,
+ isErrorValue: (value: String?) -> Boolean,
+ onSubmit: (value: String) -> Unit,
+ keyboardOptions: KeyboardOptions = KeyboardOptions(
+ capitalization = KeyboardCapitalization.None,
+ imeAction = ImeAction.Done,
+ ),
+) {
+ val focusManager = LocalFocusManager.current
+ val interactionSource = remember { MutableInteractionSource() }
+ val isFocused by interactionSource.collectIsFocusedAsState()
+ val keyboardController = LocalSoftwareKeyboardController.current
+
+ var stateValue by remember { mutableStateOf(value) }
+
+ ConfigurationTextBox(
+ isError = isErrorValue(stateValue),
+ interactionSource = interactionSource,
+ value = stateValue ?: "",
+ onValueChange = {
+ stateValue = it
+ },
+ keyboardOptions = keyboardOptions,
+ label = label,
+ hint = hint,
+ modifier = Modifier
+ .fillMaxWidth()
+ .focusRequester(focusRequester),
+ trailing = {
+ if (!stateValue.isNullOrBlank() && !isErrorValue(stateValue) && isFocused) {
+ IconButton(onClick = {
+ onSubmit(stateValue!!)
+ keyboardController?.hide()
+ focusManager.clearFocus()
+ }) {
+ Icon(
+ imageVector = Icons.Outlined.Save,
+ contentDescription = stringResource(R.string.save_changes),
+ tint = MaterialTheme.colorScheme.primary,
+ )
+ }
+ }
+ },
+ )
+}
diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/navigation/NavigationService.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/navigation/NavigationService.kt
new file mode 100644
index 0000000..685e037
--- /dev/null
+++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/navigation/NavigationService.kt
@@ -0,0 +1,15 @@
+package com.zaneschepke.wireguardautotunnel.ui.common.navigation
+
+import android.content.Context
+import androidx.navigation.NavHostController
+import androidx.navigation.compose.ComposeNavigator
+import androidx.navigation.compose.DialogNavigator
+
+class NavigationService constructor(
+ context: Context,
+) {
+ val navController = NavHostController(context).apply {
+ navigatorProvider.addNavigator(ComposeNavigator())
+ navigatorProvider.addNavigator(DialogNavigator())
+ }
+}
diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/snackbar/SnackbarController.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/snackbar/SnackbarController.kt
new file mode 100644
index 0000000..e7b445e
--- /dev/null
+++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/snackbar/SnackbarController.kt
@@ -0,0 +1,108 @@
+package com.zaneschepke.wireguardautotunnel.ui.common.snackbar
+
+import androidx.compose.material3.SnackbarDuration
+import androidx.compose.material3.SnackbarHostState
+import androidx.compose.material3.SnackbarResult
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.runtime.DisposableEffect
+import androidx.compose.runtime.Immutable
+import androidx.compose.runtime.ReadOnlyComposable
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.runtime.staticCompositionLocalOf
+import androidx.compose.ui.platform.LocalContext
+import com.zaneschepke.wireguardautotunnel.util.StringValue
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.channels.Channel
+import kotlinx.coroutines.launch
+import kotlin.coroutines.EmptyCoroutineContext
+
+private val LocalSnackbarController = staticCompositionLocalOf {
+ SnackbarController(
+ host = SnackbarHostState(),
+ scope = CoroutineScope(EmptyCoroutineContext),
+ )
+}
+private val channel = Channel(capacity = 1)
+
+@Composable
+fun SnackbarControllerProvider(content: @Composable (snackbarHost: SnackbarHostState) -> Unit) {
+ val snackHostState = remember { SnackbarHostState() }
+ val scope = rememberCoroutineScope()
+ val snackController = remember(scope) { SnackbarController(snackHostState, scope) }
+ val context = LocalContext.current
+
+ DisposableEffect(snackController, scope) {
+ val job = scope.launch {
+ for (payload in channel) {
+ snackController.showMessage(
+ message = payload.message.asString(context),
+ duration = payload.duration,
+ action = payload.action,
+ )
+ }
+ }
+
+ onDispose {
+ job.cancel()
+ }
+ }
+
+ CompositionLocalProvider(LocalSnackbarController provides snackController) {
+ content(
+ snackHostState,
+ )
+ }
+}
+
+@Immutable
+class SnackbarController(
+ private val host: SnackbarHostState,
+ private val scope: CoroutineScope,
+) {
+ companion object {
+ val current
+ @Composable
+ @ReadOnlyComposable
+ get() = LocalSnackbarController.current
+
+ fun showMessage(message: StringValue, action: SnackbarAction? = null, duration: SnackbarDuration = SnackbarDuration.Short) {
+ channel.trySend(
+ SnackbarChannelMessage(
+ message = message,
+ duration = duration,
+ action = action,
+ ),
+ )
+ }
+ }
+
+ fun showMessage(message: String, action: SnackbarAction? = null, duration: SnackbarDuration = SnackbarDuration.Short) {
+ scope.launch {
+ /**
+ * note: uncomment this line if you want snackbar to be displayed immediately,
+ * rather than being enqueued and waiting [duration] * current_queue_size
+ */
+ host.currentSnackbarData?.dismiss()
+ val result =
+ host.showSnackbar(
+ message = message,
+ actionLabel = action?.title,
+ duration = duration,
+ )
+
+ if (result == SnackbarResult.ActionPerformed) {
+ action?.onActionPress?.invoke()
+ }
+ }
+ }
+}
+
+data class SnackbarChannelMessage(
+ val message: StringValue,
+ val action: SnackbarAction?,
+ val duration: SnackbarDuration = SnackbarDuration.Short,
+)
+
+data class SnackbarAction(val title: String, val onActionPress: () -> Unit)
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 b1115ad..326cc02 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
@@ -70,12 +70,12 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.navigation.NavController
import com.google.accompanist.drawablepainter.DrawablePainter
import com.zaneschepke.wireguardautotunnel.R
-import com.zaneschepke.wireguardautotunnel.ui.AppViewModel
import com.zaneschepke.wireguardautotunnel.ui.Screen
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.screen.LoadingScreen
+import com.zaneschepke.wireguardautotunnel.ui.common.snackbar.SnackbarController
import com.zaneschepke.wireguardautotunnel.ui.common.text.SectionTitle
import com.zaneschepke.wireguardautotunnel.ui.screens.main.ConfigType
import com.zaneschepke.wireguardautotunnel.util.Constants
@@ -92,11 +92,11 @@ fun ConfigScreen(
viewModel: ConfigViewModel = hiltViewModel(),
focusRequester: FocusRequester,
navController: NavController,
- appViewModel: AppViewModel,
tunnelId: String,
configType: ConfigType,
) {
val context = LocalContext.current
+ val snackbar = SnackbarController.current
val clipboardManager: ClipboardManager = LocalClipboardManager.current
val keyboardController = LocalSoftwareKeyboardController.current
var showApplicationsDialog by remember { mutableStateOf(false) }
@@ -160,13 +160,13 @@ fun ConfigScreen(
},
onError = {
showAuthPrompt = false
- appViewModel.showSnackbarMessage(
+ snackbar.showMessage(
context.getString(R.string.error_authentication_failed),
)
},
onFailure = {
showAuthPrompt = false
- appViewModel.showSnackbarMessage(
+ snackbar.showMessage(
context.getString(R.string.error_authorization_failed),
)
},
@@ -341,12 +341,12 @@ fun ConfigScreen(
},
onClick = {
viewModel.onSaveAllChanges(configType).onSuccess {
- appViewModel.showSnackbarMessage(
+ snackbar.showMessage(
context.getString(R.string.config_changes_saved),
)
navController.navigate(Screen.Main.route)
}.onFailure {
- appViewModel.showSnackbarMessage(it.getMessage(context))
+ snackbar.showMessage(it.getMessage(context))
}
},
containerColor = fobColor,
diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/config/model/InterfaceProxy.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/config/model/InterfaceProxy.kt
index 2b4872e..f59bc3e 100644
--- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/config/model/InterfaceProxy.kt
+++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/config/model/InterfaceProxy.kt
@@ -28,7 +28,7 @@ data class InterfaceProxy(
dnsServers = listOf(
i.dnsServers.joinToString(", ").replace("/", "").trim(),
i.dnsSearchDomains.joinToString(", ").trim(),
- ).filter { it.length > 0 } .joinToString(", "),
+ ).filter { it.length > 0 }.joinToString(", "),
listenPort =
if (i.listenPort.isPresent) {
i.listenPort.get().toString().trim()
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 9b7dbd2..5a5cec2 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
@@ -58,7 +58,6 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
-import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.navigation.NavController
import com.journeyapps.barcodescanner.ScanContract
import com.journeyapps.barcodescanner.ScanOptions
@@ -67,12 +66,12 @@ import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
import com.zaneschepke.wireguardautotunnel.service.tunnel.HandshakeStatus
import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelState
-import com.zaneschepke.wireguardautotunnel.ui.AppViewModel
+import com.zaneschepke.wireguardautotunnel.ui.AppUiState
import com.zaneschepke.wireguardautotunnel.ui.Screen
import com.zaneschepke.wireguardautotunnel.ui.common.RowListItem
import com.zaneschepke.wireguardautotunnel.ui.common.dialog.InfoDialog
import com.zaneschepke.wireguardautotunnel.ui.common.functions.rememberFileImportLauncherForResult
-import com.zaneschepke.wireguardautotunnel.ui.common.screen.LoadingScreen
+import com.zaneschepke.wireguardautotunnel.ui.common.snackbar.SnackbarController
import com.zaneschepke.wireguardautotunnel.ui.screens.main.components.GettingStartedLabel
import com.zaneschepke.wireguardautotunnel.ui.screens.main.components.ScrollDismissMultiFab
import com.zaneschepke.wireguardautotunnel.ui.screens.main.components.TunnelImportSheet
@@ -93,14 +92,10 @@ import timber.log.Timber
@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter")
@OptIn(ExperimentalFoundationApi::class)
@Composable
-fun MainScreen(
- viewModel: MainViewModel = hiltViewModel(),
- appViewModel: AppViewModel,
- focusRequester: FocusRequester,
- navController: NavController,
-) {
+fun MainScreen(viewModel: MainViewModel = hiltViewModel(), uiState: AppUiState, focusRequester: FocusRequester, navController: NavController) {
val haptic = LocalHapticFeedback.current
val context = LocalContext.current
+ val snackbar = SnackbarController.current
val scope = rememberCoroutineScope()
var showBottomSheet by remember { mutableStateOf(false) }
@@ -109,7 +104,6 @@ fun MainScreen(
val isVisible = rememberSaveable { mutableStateOf(true) }
var showDeleteTunnelAlertDialog by remember { mutableStateOf(false) }
var selectedTunnel by remember { mutableStateOf(null) }
- val uiState by viewModel.uiState.collectAsStateWithLifecycle()
val nestedScrollConnection =
remember {
@@ -154,13 +148,13 @@ fun MainScreen(
}
val tunnelFileImportResultLauncher = rememberFileImportLauncherForResult(onNoFileExplorer = {
- appViewModel.showSnackbarMessage(
+ snackbar.showMessage(
context.getString(R.string.error_no_file_explorer),
)
}, onData = { data ->
scope.launch {
viewModel.onTunnelFileSelected(data, configType, context).onFailure {
- appViewModel.showSnackbarMessage(it.getMessage(context))
+ snackbar.showMessage(it.getMessage(context))
}
}
})
@@ -172,7 +166,7 @@ fun MainScreen(
if (it.contents != null) {
scope.launch {
viewModel.onTunnelQrResult(it.contents, configType).onFailure { error ->
- appViewModel.showSnackbarMessage(error.getMessage(context))
+ snackbar.showMessage(error.getMessage(context))
}
}
}
@@ -209,10 +203,6 @@ fun MainScreen(
}
}
- if (uiState.loading) {
- return LoadingScreen()
- }
-
fun launchQrScanner() {
val scanOptions = ScanOptions()
scanOptions.setDesiredBarcodeFormats(ScanOptions.QR_CODE)
@@ -398,7 +388,7 @@ fun MainScreen(
(uiState.vpnState.status == TunnelState.UP) &&
(tunnel.name == uiState.vpnState.tunnelConfig?.name)
) {
- appViewModel.showSnackbarMessage(
+ snackbar.showMessage(
context.getString(R.string.turn_off_tunnel),
)
return@RowListItem
@@ -433,7 +423,7 @@ fun MainScreen(
uiState.settings.isAutoTunnelEnabled &&
!uiState.settings.isAutoTunnelPaused
) {
- appViewModel.showSnackbarMessage(
+ snackbar.showMessage(
context.getString(R.string.turn_off_tunnel),
)
} else {
@@ -482,7 +472,7 @@ fun MainScreen(
IconButton(
onClick = {
if (uiState.settings.isAutoTunnelEnabled && !uiState.settings.isAutoTunnelPaused) {
- appViewModel.showSnackbarMessage(
+ snackbar.showMessage(
context.getString(R.string.turn_off_auto),
)
} else {
@@ -508,7 +498,7 @@ fun MainScreen(
) {
expanded.value = !expanded.value
} else {
- appViewModel.showSnackbarMessage(
+ snackbar.showMessage(
context.getString(R.string.turn_on_tunnel),
)
}
@@ -529,7 +519,7 @@ fun MainScreen(
uiState.vpnState.status == TunnelState.UP &&
tunnel.name == uiState.vpnState.tunnelConfig?.name
) {
- appViewModel.showSnackbarMessage(
+ snackbar.showMessage(
context.getString(R.string.turn_off_tunnel),
)
} else {
diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/MainUiState.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/MainUiState.kt
deleted file mode 100644
index 0de6edc..0000000
--- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/MainUiState.kt
+++ /dev/null
@@ -1,12 +0,0 @@
-package com.zaneschepke.wireguardautotunnel.ui.screens.main
-
-import com.zaneschepke.wireguardautotunnel.data.domain.Settings
-import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnState
-import com.zaneschepke.wireguardautotunnel.util.extensions.TunnelConfigs
-
-data class MainUiState(
- val settings: Settings = Settings(),
- val tunnels: TunnelConfigs = emptyList(),
- val vpnState: VpnState = VpnState(),
- val loading: Boolean = true,
-)
diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/MainViewModel.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/MainViewModel.kt
index f18712f..e6bd8fb 100644
--- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/MainViewModel.kt
+++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/MainViewModel.kt
@@ -19,9 +19,6 @@ import com.zaneschepke.wireguardautotunnel.util.WgTunnelExceptions
import com.zaneschepke.wireguardautotunnel.util.extensions.toWgQuickString
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.CoroutineDispatcher
-import kotlinx.coroutines.flow.SharingStarted
-import kotlinx.coroutines.flow.combine
-import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import timber.log.Timber
@@ -34,26 +31,12 @@ class MainViewModel
@Inject
constructor(
private val appDataRepository: AppDataRepository,
- private val serviceManager: ServiceManager,
val tunnelService: TunnelService,
@IoDispatcher private val ioDispatcher: CoroutineDispatcher,
) : ViewModel() {
- val uiState =
- combine(
- appDataRepository.settings.getSettingsFlow(),
- appDataRepository.tunnels.getTunnelConfigsFlow(),
- tunnelService.vpnState,
- ) { settings, tunnels, vpnState ->
- MainUiState(settings, tunnels, vpnState, false)
- }
- .stateIn(
- viewModelScope,
- SharingStarted.WhileSubscribed(Constants.SUBSCRIPTION_TIMEOUT),
- MainUiState(),
- )
private fun stopWatcherService(context: Context) {
- serviceManager.stopWatcherService(context)
+ ServiceManager.stopWatcherService(context)
}
fun onDelete(tunnel: TunnelConfig, context: Context) {
@@ -299,14 +282,16 @@ constructor(
}
fun pauseAutoTunneling() = viewModelScope.launch {
+ val settings = appDataRepository.settings.getSettings()
appDataRepository.settings.save(
- uiState.value.settings.copy(isAutoTunnelPaused = true),
+ settings.copy(isAutoTunnelPaused = true),
)
}
fun resumeAutoTunneling() = viewModelScope.launch {
+ val settings = appDataRepository.settings.getSettings()
appDataRepository.settings.save(
- uiState.value.settings.copy(isAutoTunnelPaused = false),
+ settings.copy(isAutoTunnelPaused = false),
)
}
diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/options/OptionsScreen.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/options/OptionsScreen.kt
index 0bb1a39..fb928a5 100644
--- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/options/OptionsScreen.kt
+++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/options/OptionsScreen.kt
@@ -32,7 +32,6 @@ import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
-import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@@ -44,23 +43,23 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardCapitalization
+import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
-import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.navigation.NavController
import com.zaneschepke.wireguardautotunnel.R
-import com.zaneschepke.wireguardautotunnel.ui.AppViewModel
+import com.zaneschepke.wireguardautotunnel.ui.AppUiState
import com.zaneschepke.wireguardautotunnel.ui.Screen
import com.zaneschepke.wireguardautotunnel.ui.common.ClickableIconButton
import com.zaneschepke.wireguardautotunnel.ui.common.config.ConfigurationToggle
+import com.zaneschepke.wireguardautotunnel.ui.common.config.SubmitConfigurationTextBox
import com.zaneschepke.wireguardautotunnel.ui.common.text.SectionTitle
import com.zaneschepke.wireguardautotunnel.ui.screens.main.ConfigType
import com.zaneschepke.wireguardautotunnel.ui.screens.main.components.ScrollDismissMultiFab
import com.zaneschepke.wireguardautotunnel.util.Constants
-import com.zaneschepke.wireguardautotunnel.util.extensions.getMessage
import com.zaneschepke.wireguardautotunnel.util.extensions.isRunningOnTv
+import com.zaneschepke.wireguardautotunnel.util.extensions.isValidIpv4orIpv6Address
import kotlinx.coroutines.delay
-import kotlinx.coroutines.launch
@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter")
@OptIn(ExperimentalLayoutApi::class)
@@ -68,16 +67,15 @@ import kotlinx.coroutines.launch
fun OptionsScreen(
optionsViewModel: OptionsViewModel = hiltViewModel(),
navController: NavController,
- appViewModel: AppViewModel,
focusRequester: FocusRequester,
- tunnelId: String,
+ appUiState: AppUiState,
+ tunnelId: Int,
) {
val scrollState = rememberScrollState()
- val uiState by optionsViewModel.uiState.collectAsStateWithLifecycle()
val context = LocalContext.current
+ val config = appUiState.tunnels.first { it.id == tunnelId }
val interactionSource = remember { MutableInteractionSource() }
- val scope = rememberCoroutineScope()
val focusManager = LocalFocusManager.current
val screenPadding = 5.dp
val fillMaxWidth = .85f
@@ -85,7 +83,6 @@ fun OptionsScreen(
var currentText by remember { mutableStateOf("") }
LaunchedEffect(Unit) {
- optionsViewModel.init(tunnelId)
if (context.isRunningOnTv()) {
delay(Constants.FOCUS_REQUEST_DELAY)
kotlin.runCatching {
@@ -99,13 +96,8 @@ fun OptionsScreen(
fun saveTrustedSSID() {
if (currentText.isNotEmpty()) {
- scope.launch {
- optionsViewModel.onSaveRunSSID(currentText).onSuccess {
- currentText = ""
- }.onFailure {
- appViewModel.showSnackbarMessage(it.getMessage(context))
- }
- }
+ optionsViewModel.onSaveRunSSID(currentText, config)
+ currentText = ""
}
}
@@ -114,7 +106,7 @@ fun OptionsScreen(
ScrollDismissMultiFab(R.drawable.edit, focusRequester, isVisible = true, onFabItemClicked = {
val configType = ConfigType.valueOf(it.value)
navController.navigate(
- "${Screen.Config.route}/$tunnelId?configType=${configType.name}",
+ "${Screen.Config.route}/${config.id}?configType=${configType.name}",
)
})
},
@@ -165,12 +157,12 @@ fun OptionsScreen(
ConfigurationToggle(
stringResource(R.string.set_primary_tunnel),
enabled = true,
- checked = uiState.isDefaultTunnel,
+ checked = config.isPrimaryTunnel,
modifier =
Modifier
.focusRequester(focusRequester),
padding = screenPadding,
- onCheckChanged = { optionsViewModel.onTogglePrimaryTunnel() },
+ onCheckChanged = { optionsViewModel.onTogglePrimaryTunnel(config) },
)
}
}
@@ -206,9 +198,9 @@ fun OptionsScreen(
ConfigurationToggle(
stringResource(R.string.mobile_data_tunnel),
enabled = true,
- checked = uiState.tunnel?.isMobileDataTunnel == true,
+ checked = config.isMobileDataTunnel,
padding = screenPadding,
- onCheckChanged = { optionsViewModel.onToggleIsMobileDataTunnel() },
+ onCheckChanged = { optionsViewModel.onToggleIsMobileDataTunnel(config) },
)
Column {
FlowRow(
@@ -218,24 +210,24 @@ fun OptionsScreen(
.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(5.dp),
) {
- uiState.tunnel?.tunnelNetworks?.forEach { ssid ->
+ config.tunnelNetworks.forEach { ssid ->
ClickableIconButton(
onClick = {
if (context.isRunningOnTv()) {
focusRequester.requestFocus()
- optionsViewModel.onDeleteRunSSID(ssid)
+ optionsViewModel.onDeleteRunSSID(ssid, config)
}
},
onIconClick = {
if (context.isRunningOnTv()) focusRequester.requestFocus()
- optionsViewModel.onDeleteRunSSID(ssid)
+ optionsViewModel.onDeleteRunSSID(ssid, config)
},
text = ssid,
icon = Icons.Filled.Close,
enabled = true,
)
}
- if (uiState.tunnel == null || uiState.tunnel?.tunnelNetworks?.isEmpty() == true) {
+ if (config.tunnelNetworks.isEmpty()) {
Text(
stringResource(R.string.no_wifi_names_configured),
fontStyle = FontStyle.Italic,
@@ -267,26 +259,67 @@ fun OptionsScreen(
IconButton(onClick = { saveTrustedSSID() }) {
Icon(
imageVector = Icons.Outlined.Add,
- contentDescription =
- if (currentText == "") {
- stringResource(
- id =
- R.string
- .trusted_ssid_empty_description,
- )
- } else {
- stringResource(
- id =
- R.string
- .trusted_ssid_value_description,
- )
- },
+ contentDescription = stringResource(R.string.save_changes),
tint = MaterialTheme.colorScheme.primary,
)
}
}
},
)
+ ConfigurationToggle(
+ stringResource(R.string.restart_on_ping),
+ enabled = !appUiState.settings.isPingEnabled,
+ checked = config.isPingEnabled || appUiState.settings.isPingEnabled,
+ padding = screenPadding,
+ onCheckChanged = { optionsViewModel.onToggleRestartOnPing(config) },
+ )
+ if (config.isPingEnabled || appUiState.settings.isPingEnabled) {
+ SubmitConfigurationTextBox(
+ config.pingIp,
+ stringResource(R.string.set_custom_ping_ip),
+ stringResource(R.string.default_ping_ip),
+ focusRequester,
+ isErrorValue = { !(it?.isValidIpv4orIpv6Address() ?: true) },
+ onSubmit = {
+ optionsViewModel.saveTunnelChanges(
+ config.copy(pingIp = it),
+ )
+ },
+ )
+ fun isSecondsError(seconds: String?): Boolean {
+ return seconds?.let { value -> if (value.isBlank()) false else value.toLong() >= Long.MAX_VALUE / 1000 } ?: false
+ }
+ SubmitConfigurationTextBox(
+ config.pingInterval?.let { (it / 1000).toString() },
+ stringResource(R.string.set_custom_ping_internal),
+ "(${stringResource(R.string.optional_default)} ${Constants.PING_INTERVAL / 1000})",
+ focusRequester,
+ keyboardOptions = KeyboardOptions(
+ keyboardType = KeyboardType.Number,
+ ),
+ isErrorValue = ::isSecondsError,
+ onSubmit = {
+ optionsViewModel.saveTunnelChanges(
+ config.copy(pingInterval = it.toLong() * 1000),
+ )
+ },
+ )
+ SubmitConfigurationTextBox(
+ config.pingCooldown?.let { (it / 1000).toString() },
+ stringResource(R.string.set_custom_ping_cooldown),
+ "(${stringResource(R.string.optional_default)} ${Constants.PING_COOLDOWN / 1000})",
+ focusRequester,
+ keyboardOptions = KeyboardOptions(
+ keyboardType = KeyboardType.Number,
+ ),
+ isErrorValue = ::isSecondsError,
+ onSubmit = {
+ optionsViewModel.saveTunnelChanges(
+ config.copy(pingCooldown = it.toLong() * 1000),
+ )
+ },
+ )
+ }
}
}
}
diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/options/OptionsUiState.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/options/OptionsUiState.kt
deleted file mode 100644
index 60f57ff..0000000
--- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/options/OptionsUiState.kt
+++ /dev/null
@@ -1,9 +0,0 @@
-package com.zaneschepke.wireguardautotunnel.ui.screens.options
-
-import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
-
-data class OptionsUiState(
- val id: String? = null,
- val tunnel: TunnelConfig? = null,
- val isDefaultTunnel: Boolean = false,
-)
diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/options/OptionsViewModel.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/options/OptionsViewModel.kt
index 0844973..eecd3df 100644
--- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/options/OptionsViewModel.kt
+++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/options/OptionsViewModel.kt
@@ -1,20 +1,14 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.options
-import androidx.compose.ui.util.fastFirstOrNull
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
+import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
-import com.zaneschepke.wireguardautotunnel.util.Constants
-import com.zaneschepke.wireguardautotunnel.util.WgTunnelExceptions
+import com.zaneschepke.wireguardautotunnel.ui.common.snackbar.SnackbarController
+import com.zaneschepke.wireguardautotunnel.util.StringValue
import dagger.hilt.android.lifecycle.HiltViewModel
-import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.SharingStarted
-import kotlinx.coroutines.flow.combine
-import kotlinx.coroutines.flow.stateIn
-import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
-import kotlinx.coroutines.withContext
import javax.inject.Inject
@HiltViewModel
@@ -23,86 +17,63 @@ class OptionsViewModel
constructor(
private val appDataRepository: AppDataRepository,
) : ViewModel() {
- private val _optionState = MutableStateFlow(OptionsUiState())
- val uiState =
- combine(
- appDataRepository.tunnels.getTunnelConfigsFlow(),
- _optionState,
- ) { tunnels, optionState ->
- if (optionState.id != null) {
- val tunnelConfig = tunnels.fastFirstOrNull { it.id.toString() == optionState.id }
- val isPrimaryTunnel = tunnelConfig?.isPrimaryTunnel == true
- OptionsUiState(optionState.id, tunnelConfig, isPrimaryTunnel)
- } else {
- OptionsUiState()
- }
- }.stateIn(
- viewModelScope,
- SharingStarted.WhileSubscribed(Constants.SUBSCRIPTION_TIMEOUT),
- OptionsUiState(),
+ fun onDeleteRunSSID(ssid: String, tunnelConfig: TunnelConfig) = viewModelScope.launch {
+ appDataRepository.tunnels.save(
+ tunnelConfig =
+ tunnelConfig.copy(
+ tunnelNetworks = (tunnelConfig.tunnelNetworks - ssid).toMutableList(),
+ ),
)
-
- fun init(tunnelId: String) {
- _optionState.update {
- it.copy(
- id = tunnelId,
- )
- }
}
- fun onDeleteRunSSID(ssid: String) = viewModelScope.launch {
- uiState.value.tunnel?.let {
- appDataRepository.tunnels.save(
- tunnelConfig =
- it.copy(
- tunnelNetworks = (uiState.value.tunnel!!.tunnelNetworks - ssid).toMutableList(),
+ fun saveTunnelChanges(tunnelConfig: TunnelConfig) = viewModelScope.launch {
+ appDataRepository.tunnels.save(tunnelConfig)
+ }
+
+ fun onSaveRunSSID(ssid: String, tunnelConfig: TunnelConfig) = viewModelScope.launch {
+ val trimmed = ssid.trim()
+ val tunnelsWithName = appDataRepository.tunnels.findByTunnelNetworksName(trimmed)
+
+ if (!tunnelConfig.tunnelNetworks.contains(trimmed) &&
+ tunnelsWithName.isEmpty()
+ ) {
+ saveTunnelChanges(
+ tunnelConfig.copy(
+ tunnelNetworks = (tunnelConfig.tunnelNetworks + ssid).toMutableList(),
+ ),
+ )
+ } else {
+ SnackbarController.showMessage(
+ StringValue.StringResource(
+ R.string.error_ssid_exists,
),
)
}
}
- private fun saveTunnel(tunnelConfig: TunnelConfig?) = viewModelScope.launch {
- tunnelConfig?.let {
- appDataRepository.tunnels.save(it)
- }
- }
-
- suspend fun onSaveRunSSID(ssid: String): Result {
- val trimmed = ssid.trim()
- val tunnelsWithName =
- withContext(viewModelScope.coroutineContext) {
- appDataRepository.tunnels.findByTunnelNetworksName(trimmed)
- }
- return if (uiState.value.tunnel?.tunnelNetworks?.contains(trimmed) != true &&
- tunnelsWithName.isEmpty()
- ) {
- uiState.value.tunnel?.tunnelNetworks?.add(trimmed)
- saveTunnel(uiState.value.tunnel)
- Result.success(Unit)
+ fun onToggleIsMobileDataTunnel(tunnelConfig: TunnelConfig) = viewModelScope.launch {
+ if (tunnelConfig.isMobileDataTunnel) {
+ appDataRepository.tunnels.updateMobileDataTunnel(null)
} else {
- Result.failure(WgTunnelExceptions.SsidConflict())
+ appDataRepository.tunnels.updateMobileDataTunnel(tunnelConfig)
}
}
- fun onToggleIsMobileDataTunnel() = viewModelScope.launch {
- uiState.value.tunnel?.let {
- if (it.isMobileDataTunnel) {
- appDataRepository.tunnels.updateMobileDataTunnel(null)
- } else {
- appDataRepository.tunnels.updateMobileDataTunnel(it)
- }
- }
+ fun onTogglePrimaryTunnel(tunnelConfig: TunnelConfig) = viewModelScope.launch {
+ appDataRepository.tunnels.updatePrimaryTunnel(
+ when (tunnelConfig.isPrimaryTunnel) {
+ true -> null
+ false -> tunnelConfig
+ },
+ )
}
- fun onTogglePrimaryTunnel() = viewModelScope.launch {
- if (uiState.value.tunnel != null) {
- appDataRepository.tunnels.updatePrimaryTunnel(
- when (uiState.value.isDefaultTunnel) {
- true -> null
- false -> uiState.value.tunnel
- },
- )
- }
+ fun onToggleRestartOnPing(tunnelConfig: TunnelConfig) = viewModelScope.launch {
+ appDataRepository.tunnels.save(
+ tunnelConfig.copy(
+ isPingEnabled = !tunnelConfig.isPingEnabled,
+ ),
+ )
}
}
diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/pinlock/PinLockScreen.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/pinlock/PinLockScreen.kt
index 56a9ce9..7750665 100644
--- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/pinlock/PinLockScreen.kt
+++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/pinlock/PinLockScreen.kt
@@ -9,6 +9,7 @@ import androidx.navigation.NavController
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.ui.AppViewModel
import com.zaneschepke.wireguardautotunnel.ui.Screen
+import com.zaneschepke.wireguardautotunnel.ui.common.snackbar.SnackbarController
import com.zaneschepke.wireguardautotunnel.util.StringValue
import com.zaneschepke.wireguardautotunnel.util.extensions.isRunningOnTv
import xyz.teamgravity.pin_lock_compose.PinLock
@@ -16,9 +17,11 @@ import xyz.teamgravity.pin_lock_compose.PinLock
@Composable
fun PinLockScreen(navController: NavController, appViewModel: AppViewModel) {
val context = LocalContext.current
+ val snackbar = SnackbarController.current
PinLock(
title = { pinExists ->
Text(
+ color = MaterialTheme.colorScheme.onSecondary,
text =
if (pinExists) {
stringResource(id = R.string.enter_pin)
@@ -29,7 +32,7 @@ fun PinLockScreen(navController: NavController, appViewModel: AppViewModel) {
},
)
},
- color = MaterialTheme.colorScheme.surface,
+ color = MaterialTheme.colorScheme.secondary,
onPinCorrect = {
// pin is correct, navigate or hide pin lock
if (context.isRunningOnTv()) {
@@ -43,13 +46,13 @@ fun PinLockScreen(navController: NavController, appViewModel: AppViewModel) {
},
onPinIncorrect = {
// pin is incorrect, show error
- appViewModel.showSnackbarMessage(
+ snackbar.showMessage(
StringValue.StringResource(R.string.incorrect_pin).asString(context),
)
},
onPinCreated = {
// pin created for the first time, navigate or hide pin lock
- appViewModel.showSnackbarMessage(
+ snackbar.showMessage(
StringValue.StringResource(R.string.pin_created).asString(context),
)
appViewModel.onPinLockEnabled()
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 40a7b9f..f538cb0 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
@@ -45,7 +45,6 @@ import androidx.compose.runtime.SideEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
-import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@@ -67,21 +66,23 @@ import com.google.accompanist.permissions.rememberPermissionState
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelState
+import com.zaneschepke.wireguardautotunnel.ui.AppUiState
import com.zaneschepke.wireguardautotunnel.ui.AppViewModel
import com.zaneschepke.wireguardautotunnel.ui.Screen
import com.zaneschepke.wireguardautotunnel.ui.common.ClickableIconButton
import com.zaneschepke.wireguardautotunnel.ui.common.config.ConfigurationToggle
import com.zaneschepke.wireguardautotunnel.ui.common.prompt.AuthorizationPrompt
+import com.zaneschepke.wireguardautotunnel.ui.common.snackbar.SnackbarController
import com.zaneschepke.wireguardautotunnel.ui.common.text.SectionTitle
import com.zaneschepke.wireguardautotunnel.ui.screens.main.components.VpnDeniedDialog
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.components.BackgroundLocationDialog
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.components.BackgroundLocationDisclosure
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.components.LocationServicesDialog
-import com.zaneschepke.wireguardautotunnel.util.extensions.getMessage
+import com.zaneschepke.wireguardautotunnel.ui.screens.settings.components.WildcardSupportingLabel
import com.zaneschepke.wireguardautotunnel.util.extensions.isRunningOnTv
import com.zaneschepke.wireguardautotunnel.util.extensions.launchAppSettings
+import com.zaneschepke.wireguardautotunnel.util.extensions.openWebUrl
import com.zaneschepke.wireguardautotunnel.util.extensions.showToast
-import kotlinx.coroutines.launch
import xyz.teamgravity.pin_lock_compose.PinManager
@OptIn(
@@ -92,16 +93,17 @@ import xyz.teamgravity.pin_lock_compose.PinManager
fun SettingsScreen(
viewModel: SettingsViewModel = hiltViewModel(),
appViewModel: AppViewModel,
+ uiState: AppUiState,
navController: NavController,
focusRequester: FocusRequester,
) {
val context = LocalContext.current
val focusManager = LocalFocusManager.current
- val scope = rememberCoroutineScope()
+ val snackbar = SnackbarController.current
val scrollState = rememberScrollState()
val interactionSource = remember { MutableInteractionSource() }
+ val isRunningOnTv = context.isRunningOnTv()
- val uiState by viewModel.uiState.collectAsStateWithLifecycle()
val kernelSupport by viewModel.kernelSupport.collectAsStateWithLifecycle()
val fineLocationState = rememberPermissionState(Manifest.permission.ACCESS_FINE_LOCATION)
@@ -120,6 +122,10 @@ fun SettingsScreen(
viewModel.checkKernelSupport()
}
+ LaunchedEffect(uiState.settings.trustedNetworkSSIDs) {
+ currentText = ""
+ }
+
val notificationPermissionState =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
rememberPermissionState(Manifest.permission.POST_NOTIFICATIONS)
@@ -166,9 +172,9 @@ fun SettingsScreen(
}
fun handleAutoTunnelToggle() {
- if (!uiState.isBatteryOptimizeDisableShown || !isBatteryOptimizationsDisabled()) return requestBatteryOptimizationsDisabled()
+ if (!uiState.generalState.isBatteryOptimizationDisableShown || !isBatteryOptimizationsDisabled()) return requestBatteryOptimizationsDisabled()
if (notificationPermissionState != null && !notificationPermissionState.status.isGranted) {
- appViewModel.showSnackbarMessage(
+ snackbar.showMessage(
context.getString(R.string.notification_permission_required),
)
return notificationPermissionState.launchPermissionRequest()
@@ -184,11 +190,7 @@ fun SettingsScreen(
fun saveTrustedSSID() {
if (currentText.isNotEmpty()) {
- viewModel.onSaveTrustedSSID(currentText).onSuccess {
- currentText = ""
- }.onFailure {
- appViewModel.showSnackbarMessage(it.getMessage(context))
- }
+ viewModel.onSaveTrustedSSID(currentText)
}
}
@@ -202,13 +204,9 @@ fun SettingsScreen(
}
}
- fun onRootDenied() = appViewModel.showSnackbarMessage(context.getString(R.string.error_root_denied))
-
- fun onRootAccepted() = appViewModel.showSnackbarMessage(context.getString(R.string.root_accepted))
-
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
if (
- context.isRunningOnTv() &&
+ isRunningOnTv &&
Build.VERSION.SDK_INT == Build.VERSION_CODES.Q
) {
checkFineLocationGranted()
@@ -228,7 +226,7 @@ fun SettingsScreen(
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.P) {
checkFineLocationGranted()
}
- if (!uiState.isLocationDisclosureShown) {
+ if (!uiState.generalState.isLocationDisclosureShown) {
BackgroundLocationDisclosure(
onDismiss = { viewModel.setLocationDisclosureShown() },
onAttest = {
@@ -259,410 +257,376 @@ fun SettingsScreen(
AuthorizationPrompt(
onSuccess = {
showAuthPrompt = false
- scope.launch {
- viewModel.exportAllConfigs().onSuccess {
- appViewModel.showSnackbarMessage(context.getString(R.string.exported_configs_message))
- }.onFailure {
- appViewModel.showSnackbarMessage(context.getString(R.string.export_configs_failed))
- }
- }
+ viewModel.exportAllConfigs()
},
onError = { _ ->
showAuthPrompt = false
- appViewModel.showSnackbarMessage(
+ snackbar.showMessage(
context.getString(R.string.error_authentication_failed),
)
},
onFailure = {
showAuthPrompt = false
- appViewModel.showSnackbarMessage(
+ snackbar.showMessage(
context.getString(R.string.error_authorization_failed),
)
},
)
}
- if (uiState.isLocationDisclosureShown) {
- Column(
- horizontalAlignment = Alignment.CenterHorizontally,
- verticalArrangement = Arrangement.Top,
+ Column(
+ horizontalAlignment = Alignment.CenterHorizontally,
+ verticalArrangement = Arrangement.Top,
+ modifier =
+ Modifier
+ .fillMaxSize()
+ .verticalScroll(scrollState)
+ .clickable(
+ indication = null,
+ interactionSource = interactionSource,
+ ) {
+ focusManager.clearFocus()
+ },
+ ) {
+ Surface(
+ tonalElevation = 2.dp,
+ shadowElevation = 2.dp,
+ shape = RoundedCornerShape(12.dp),
+ color = MaterialTheme.colorScheme.surface,
modifier =
- Modifier
- .fillMaxSize()
- .verticalScroll(scrollState)
- .clickable(
- indication = null,
- interactionSource = interactionSource,
- ) {
- focusManager.clearFocus()
- },
+ (
+ if (isRunningOnTv) {
+ Modifier
+ .height(IntrinsicSize.Min)
+ .fillMaxWidth(fillMaxWidth)
+ .padding(top = 10.dp)
+ } else {
+ Modifier
+ .fillMaxWidth(fillMaxWidth)
+ .padding(top = 20.dp)
+ }
+ )
+ .padding(bottom = 10.dp),
) {
- Surface(
- tonalElevation = 2.dp,
- shadowElevation = 2.dp,
- shape = RoundedCornerShape(12.dp),
- color = MaterialTheme.colorScheme.surface,
- modifier =
- (
- if (context.isRunningOnTv()) {
+ Column(
+ horizontalAlignment = Alignment.Start,
+ verticalArrangement = Arrangement.Top,
+ modifier = Modifier.padding(15.dp),
+ ) {
+ SectionTitle(
+ title = stringResource(id = R.string.auto_tunneling),
+ padding = screenPadding,
+ )
+ ConfigurationToggle(
+ stringResource(id = R.string.tunnel_on_wifi),
+ enabled = !uiState.settings.isAlwaysOnVpnEnabled,
+ checked = uiState.settings.isTunnelOnWifiEnabled,
+ padding = screenPadding,
+ onCheckChanged = { viewModel.onToggleTunnelOnWifi() },
+ modifier =
+ if (uiState.settings.isAutoTunnelEnabled) {
Modifier
- .height(IntrinsicSize.Min)
- .fillMaxWidth(fillMaxWidth)
- .padding(top = 10.dp)
} else {
Modifier
- .fillMaxWidth(fillMaxWidth)
- .padding(top = 20.dp)
- }
- )
- .padding(bottom = 10.dp),
- ) {
- Column(
- horizontalAlignment = Alignment.Start,
- verticalArrangement = Arrangement.Top,
- modifier = Modifier.padding(15.dp),
- ) {
- SectionTitle(
- title = stringResource(id = R.string.auto_tunneling),
- padding = screenPadding,
- )
- ConfigurationToggle(
- stringResource(id = R.string.tunnel_on_wifi),
- enabled =
- !(
- uiState.settings.isAutoTunnelEnabled ||
- uiState.settings.isAlwaysOnVpnEnabled
+ .focusRequester(focusRequester)
+ },
+ )
+ if (uiState.settings.isTunnelOnWifiEnabled) {
+ Column {
+ FlowRow(
+ modifier =
+ Modifier
+ .padding(screenPadding)
+ .fillMaxWidth(),
+ horizontalArrangement = Arrangement.spacedBy(5.dp),
+ ) {
+ uiState.settings.trustedNetworkSSIDs.forEach { ssid ->
+ ClickableIconButton(
+ onClick = {
+ if (isRunningOnTv) {
+ focusRequester.requestFocus()
+ viewModel.onDeleteTrustedSSID(ssid)
+ }
+ },
+ onIconClick = {
+ if (isRunningOnTv) focusRequester.requestFocus()
+ viewModel.onDeleteTrustedSSID(ssid)
+ },
+ text = ssid,
+ icon = Icons.Filled.Close,
+ )
+ }
+ if (uiState.settings.trustedNetworkSSIDs.isEmpty()) {
+ Text(
+ stringResource(R.string.none),
+ fontStyle = FontStyle.Italic,
+ style = MaterialTheme.typography.bodySmall,
+ color = MaterialTheme.colorScheme.onSurface,
+ )
+ }
+ }
+ OutlinedTextField(
+ value = currentText,
+ onValueChange = { currentText = it },
+ label = { Text(stringResource(R.string.add_trusted_ssid)) },
+ modifier =
+ Modifier
+ .padding(
+ start = screenPadding,
+ top = 5.dp,
+ bottom = 10.dp,
+ ),
+ supportingText = { WildcardSupportingLabel { context.openWebUrl(it) } },
+ maxLines = 1,
+ keyboardOptions =
+ KeyboardOptions(
+ capitalization = KeyboardCapitalization.None,
+ imeAction = ImeAction.Done,
),
- checked = uiState.settings.isTunnelOnWifiEnabled,
- padding = screenPadding,
- onCheckChanged = { viewModel.onToggleTunnelOnWifi() },
- modifier =
- if (uiState.settings.isAutoTunnelEnabled) {
+ keyboardActions = KeyboardActions(onDone = { saveTrustedSSID() }),
+ trailingIcon = {
+ if (currentText != "") {
+ IconButton(onClick = { saveTrustedSSID() }) {
+ Icon(
+ imageVector = Icons.Outlined.Add,
+ contentDescription =
+ if (currentText == "") {
+ stringResource(
+ id =
+ R.string
+ .trusted_ssid_empty_description,
+ )
+ } else {
+ stringResource(
+ id =
+ R.string
+ .trusted_ssid_value_description,
+ )
+ },
+ tint = MaterialTheme.colorScheme.primary,
+ )
+ }
+ }
+ },
+ )
+ }
+ }
+ ConfigurationToggle(
+ stringResource(R.string.tunnel_mobile_data),
+ enabled = !uiState.settings.isAlwaysOnVpnEnabled,
+ checked = uiState.settings.isTunnelOnMobileDataEnabled,
+ padding = screenPadding,
+ onCheckChanged = { viewModel.onToggleTunnelOnMobileData() },
+ )
+ ConfigurationToggle(
+ stringResource(id = R.string.tunnel_on_ethernet),
+ enabled = !uiState.settings.isAlwaysOnVpnEnabled,
+ checked = uiState.settings.isTunnelOnEthernetEnabled,
+ padding = screenPadding,
+ onCheckChanged = { viewModel.onToggleTunnelOnEthernet() },
+ )
+ ConfigurationToggle(
+ stringResource(R.string.restart_on_ping),
+ checked = uiState.settings.isPingEnabled,
+ padding = screenPadding,
+ onCheckChanged = { viewModel.onToggleRestartOnPing() },
+ )
+ Row(
+ verticalAlignment = Alignment.CenterVertically,
+ modifier =
+ (
+ if (!uiState.settings.isAutoTunnelEnabled) {
Modifier
} else {
- Modifier
- .focusRequester(focusRequester)
- },
- )
- if (uiState.settings.isTunnelOnWifiEnabled) {
- Column {
- FlowRow(
- modifier =
- Modifier
- .padding(screenPadding)
- .fillMaxWidth(),
- horizontalArrangement = Arrangement.spacedBy(5.dp),
- ) {
- uiState.settings.trustedNetworkSSIDs.forEach { ssid ->
- ClickableIconButton(
- onClick = {
- if (context.isRunningOnTv()) {
- focusRequester.requestFocus()
- viewModel.onDeleteTrustedSSID(ssid)
- }
- },
- onIconClick = {
- if (context.isRunningOnTv()) focusRequester.requestFocus()
- viewModel.onDeleteTrustedSSID(ssid)
- },
- text = ssid,
- icon = Icons.Filled.Close,
- enabled =
- !(
- uiState.settings.isAutoTunnelEnabled ||
- uiState.settings.isAlwaysOnVpnEnabled
- ),
- )
- }
- if (uiState.settings.trustedNetworkSSIDs.isEmpty()) {
- Text(
- stringResource(R.string.none),
- fontStyle = FontStyle.Italic,
- style = MaterialTheme.typography.bodySmall,
- color = MaterialTheme.colorScheme.onSurface,
- )
- }
- }
- OutlinedTextField(
- enabled =
- !(
- uiState.settings.isAutoTunnelEnabled ||
- uiState.settings.isAlwaysOnVpnEnabled
- ),
- value = currentText,
- onValueChange = { currentText = it },
- label = { Text(stringResource(R.string.add_trusted_ssid)) },
- modifier =
- Modifier
- .padding(
- start = screenPadding,
- top = 5.dp,
- bottom = 10.dp,
- ),
- maxLines = 1,
- keyboardOptions =
- KeyboardOptions(
- capitalization = KeyboardCapitalization.None,
- imeAction = ImeAction.Done,
- ),
- keyboardActions = KeyboardActions(onDone = { saveTrustedSSID() }),
- trailingIcon = {
- if (currentText != "") {
- IconButton(onClick = { saveTrustedSSID() }) {
- Icon(
- imageVector = Icons.Outlined.Add,
- contentDescription =
- if (currentText == "") {
- stringResource(
- id =
- R.string
- .trusted_ssid_empty_description,
- )
- } else {
- stringResource(
- id =
- R.string
- .trusted_ssid_value_description,
- )
- },
- tint = MaterialTheme.colorScheme.primary,
- )
- }
- }
- },
+ Modifier.focusRequester(
+ focusRequester,
)
}
+ )
+ .fillMaxSize()
+ .padding(top = 5.dp),
+ horizontalArrangement = Arrangement.Center,
+ ) {
+ TextButton(
+ onClick = {
+ if (uiState.tunnels.isEmpty()) return@TextButton context.showToast(R.string.tunnel_required)
+ if (
+ uiState.settings.isTunnelOnWifiEnabled &&
+ !uiState.settings.isAutoTunnelEnabled
+ ) {
+ when (false) {
+ isBackgroundLocationGranted -> showLocationDialog = true
+ fineLocationState.status.isGranted -> showLocationDialog = true
+ viewModel.isLocationEnabled(context) ->
+ showLocationServicesAlertDialog = true
+
+ else -> {
+ handleAutoTunnelToggle()
+ }
+ }
+ } else {
+ handleAutoTunnelToggle()
+ }
+ },
+ ) {
+ val autoTunnelButtonText =
+ if (uiState.settings.isAutoTunnelEnabled) {
+ stringResource(R.string.disable_auto_tunnel)
+ } else {
+ stringResource(id = R.string.enable_auto_tunnel)
+ }
+ Text(autoTunnelButtonText)
}
+ }
+ }
+ }
+ Surface(
+ tonalElevation = 2.dp,
+ shadowElevation = 2.dp,
+ shape = RoundedCornerShape(12.dp),
+ color = MaterialTheme.colorScheme.surface,
+ modifier =
+ Modifier
+ .fillMaxWidth(fillMaxWidth)
+ .padding(vertical = 10.dp),
+ ) {
+ Column(
+ horizontalAlignment = Alignment.Start,
+ verticalArrangement = Arrangement.Top,
+ modifier = Modifier.padding(15.dp),
+ ) {
+ SectionTitle(
+ title = stringResource(id = R.string.backend),
+ padding = screenPadding,
+ )
+ ConfigurationToggle(
+ stringResource(R.string.use_amnezia),
+ enabled =
+ !(
+ uiState.settings.isAutoTunnelEnabled ||
+ uiState.settings.isAlwaysOnVpnEnabled ||
+ (uiState.vpnState.status == TunnelState.UP) || uiState.settings.isKernelEnabled
+ ),
+ checked = uiState.settings.isAmneziaEnabled,
+ padding = screenPadding,
+ onCheckChanged = {
+ viewModel.onToggleAmnezia()
+ },
+ )
+ ConfigurationToggle(
+ stringResource(R.string.use_kernel),
+ enabled =
+ !(
+ uiState.settings.isAutoTunnelEnabled ||
+ uiState.settings.isAlwaysOnVpnEnabled ||
+ (uiState.vpnState.status == TunnelState.UP) ||
+ kernelSupport
+ ),
+ checked = uiState.settings.isKernelEnabled,
+ padding = screenPadding,
+ onCheckChanged = {
+ viewModel.onToggleKernelMode()
+ },
+ )
+ Row(
+ verticalAlignment = Alignment.CenterVertically,
+ modifier =
+ Modifier
+ .fillMaxSize()
+ .padding(top = 5.dp),
+ horizontalArrangement = Arrangement.Center,
+ ) {
+ TextButton(
+ onClick = {
+ viewModel.onRequestRoot()
+ },
+ ) {
+ Text(stringResource(R.string.request_root))
+ }
+ }
+ }
+ }
+ Surface(
+ tonalElevation = 2.dp,
+ shadowElevation = 2.dp,
+ shape = RoundedCornerShape(12.dp),
+ color = MaterialTheme.colorScheme.surface,
+ modifier =
+ Modifier
+ .fillMaxWidth(fillMaxWidth)
+ .padding(vertical = 10.dp)
+ .padding(bottom = 10.dp),
+ ) {
+ Column(
+ horizontalAlignment = Alignment.Start,
+ verticalArrangement = Arrangement.Top,
+ modifier = Modifier.padding(15.dp),
+ ) {
+ SectionTitle(
+ title = stringResource(id = R.string.other),
+ padding = screenPadding,
+ )
+ if (!isRunningOnTv) {
ConfigurationToggle(
- stringResource(R.string.tunnel_mobile_data),
- enabled =
- !(
- uiState.settings.isAutoTunnelEnabled ||
- uiState.settings.isAlwaysOnVpnEnabled
+ stringResource(R.string.always_on_vpn_support),
+ enabled = !(
+ uiState.settings.isTunnelOnWifiEnabled ||
+ uiState.settings.isTunnelOnWifiEnabled ||
+ uiState.settings.isTunnelOnMobileDataEnabled
),
- checked = uiState.settings.isTunnelOnMobileDataEnabled,
+ checked = uiState.settings.isAlwaysOnVpnEnabled,
padding = screenPadding,
- onCheckChanged = { viewModel.onToggleTunnelOnMobileData() },
+ onCheckChanged = { viewModel.onToggleAlwaysOnVPN() },
)
ConfigurationToggle(
- stringResource(id = R.string.tunnel_on_ethernet),
- enabled =
- !(
- uiState.settings.isAutoTunnelEnabled ||
- uiState.settings.isAlwaysOnVpnEnabled
- ),
- checked = uiState.settings.isTunnelOnEthernetEnabled,
+ stringResource(R.string.enabled_app_shortcuts),
+ enabled = true,
+ checked = uiState.settings.isShortcutsEnabled,
padding = screenPadding,
- onCheckChanged = { viewModel.onToggleTunnelOnEthernet() },
- )
- ConfigurationToggle(
- stringResource(R.string.restart_on_ping),
- enabled =
- !(
- uiState.settings.isAutoTunnelEnabled ||
- uiState.settings.isAlwaysOnVpnEnabled
- ),
- checked = uiState.settings.isPingEnabled,
- padding = screenPadding,
- onCheckChanged = { viewModel.onToggleRestartOnPing() },
+ onCheckChanged = { viewModel.onToggleShortcutsEnabled() },
)
+ }
+ ConfigurationToggle(
+ stringResource(R.string.restart_at_boot),
+ enabled = true,
+ checked = uiState.settings.isRestoreOnBootEnabled,
+ padding = screenPadding,
+ onCheckChanged = {
+ viewModel.onToggleRestartAtBoot()
+ },
+ )
+ ConfigurationToggle(
+ stringResource(R.string.enable_app_lock),
+ enabled = true,
+ checked = uiState.generalState.isPinLockEnabled,
+ padding = screenPadding,
+ onCheckChanged = {
+ if (uiState.generalState.isPinLockEnabled) {
+ appViewModel.onPinLockDisabled()
+ } else {
+ // TODO may want to show a dialog before proceeding in the future
+ PinManager.initialize(WireGuardAutoTunnel.instance)
+ navController.navigate(Screen.Lock.route)
+ }
+ },
+ )
+ if (!isRunningOnTv) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier =
- (
- if (!uiState.settings.isAutoTunnelEnabled) {
- Modifier
- } else {
- Modifier.focusRequester(
- focusRequester,
- )
- }
- )
+ Modifier
.fillMaxSize()
.padding(top = 5.dp),
horizontalArrangement = Arrangement.Center,
) {
TextButton(
- enabled = !uiState.settings.isAlwaysOnVpnEnabled,
+ enabled = !didExportFiles,
onClick = {
if (uiState.tunnels.isEmpty()) return@TextButton context.showToast(R.string.tunnel_required)
- if (
- uiState.settings.isTunnelOnWifiEnabled &&
- !uiState.settings.isAutoTunnelEnabled
- ) {
- when (false) {
- isBackgroundLocationGranted -> showLocationDialog = true
- fineLocationState.status.isGranted -> showLocationDialog = true
- viewModel.isLocationEnabled(context) ->
- showLocationServicesAlertDialog = true
-
- else -> {
- handleAutoTunnelToggle()
- }
- }
- } else {
- handleAutoTunnelToggle()
- }
+ showAuthPrompt = true
},
) {
- val autoTunnelButtonText =
- if (uiState.settings.isAutoTunnelEnabled) {
- stringResource(R.string.disable_auto_tunnel)
- } else {
- stringResource(id = R.string.enable_auto_tunnel)
- }
- Text(autoTunnelButtonText)
- }
- }
- }
- }
- Surface(
- tonalElevation = 2.dp,
- shadowElevation = 2.dp,
- shape = RoundedCornerShape(12.dp),
- color = MaterialTheme.colorScheme.surface,
- modifier =
- Modifier
- .fillMaxWidth(fillMaxWidth)
- .padding(vertical = 10.dp),
- ) {
- Column(
- horizontalAlignment = Alignment.Start,
- verticalArrangement = Arrangement.Top,
- modifier = Modifier.padding(15.dp),
- ) {
- SectionTitle(
- title = stringResource(id = R.string.backend),
- padding = screenPadding,
- )
- ConfigurationToggle(
- stringResource(R.string.use_amnezia),
- enabled =
- !(
- uiState.settings.isAutoTunnelEnabled ||
- uiState.settings.isAlwaysOnVpnEnabled ||
- (uiState.vpnState.status == TunnelState.UP) || uiState.settings.isKernelEnabled
- ),
- checked = uiState.settings.isAmneziaEnabled,
- padding = screenPadding,
- onCheckChanged = {
- viewModel.onToggleAmnezia()
- },
- )
- if (kernelSupport) {
- ConfigurationToggle(
- stringResource(R.string.use_kernel),
- enabled =
- !(
- uiState.settings.isAutoTunnelEnabled ||
- uiState.settings.isAlwaysOnVpnEnabled ||
- (uiState.vpnState.status == TunnelState.UP)
- ),
- checked = uiState.settings.isKernelEnabled,
- padding = screenPadding,
- onCheckChanged = {
- scope.launch {
- viewModel.onToggleKernelMode({ onRootAccepted() }, { onRootDenied() })
- }
- },
- )
- Row(
- verticalAlignment = Alignment.CenterVertically,
- modifier =
- Modifier
- .fillMaxSize()
- .padding(top = 5.dp),
- horizontalArrangement = Arrangement.Center,
- ) {
- TextButton(
- onClick = {
- viewModel.requestRoot({ onRootAccepted() }, { onRootDenied() })
- },
- ) {
- Text(stringResource(R.string.request_root))
- }
- }
- }
- }
- }
- Surface(
- tonalElevation = 2.dp,
- shadowElevation = 2.dp,
- shape = RoundedCornerShape(12.dp),
- color = MaterialTheme.colorScheme.surface,
- modifier =
- Modifier
- .fillMaxWidth(fillMaxWidth)
- .padding(vertical = 10.dp)
- .padding(bottom = 10.dp),
- ) {
- Column(
- horizontalAlignment = Alignment.Start,
- verticalArrangement = Arrangement.Top,
- modifier = Modifier.padding(15.dp),
- ) {
- SectionTitle(
- title = stringResource(id = R.string.other),
- padding = screenPadding,
- )
- if (!context.isRunningOnTv()) {
- ConfigurationToggle(
- stringResource(R.string.always_on_vpn_support),
- enabled = !uiState.settings.isAutoTunnelEnabled,
- checked = uiState.settings.isAlwaysOnVpnEnabled,
- padding = screenPadding,
- onCheckChanged = { viewModel.onToggleAlwaysOnVPN() },
- )
- ConfigurationToggle(
- stringResource(R.string.enabled_app_shortcuts),
- enabled = true,
- checked = uiState.settings.isShortcutsEnabled,
- padding = screenPadding,
- onCheckChanged = { viewModel.onToggleShortcutsEnabled() },
- )
- }
- ConfigurationToggle(
- stringResource(R.string.restart_at_boot),
- enabled = true,
- checked = uiState.settings.isRestoreOnBootEnabled,
- padding = screenPadding,
- onCheckChanged = {
- viewModel.onToggleRestartAtBoot()
- },
- )
- ConfigurationToggle(
- stringResource(R.string.enable_app_lock),
- enabled = true,
- checked = uiState.isPinLockEnabled,
- padding = screenPadding,
- onCheckChanged = {
- if (uiState.isPinLockEnabled) {
- appViewModel.onPinLockDisabled()
- } else {
- // TODO may want to show a dialog before proceeding in the future
- PinManager.initialize(WireGuardAutoTunnel.instance)
- navController.navigate(Screen.Lock.route)
- }
- },
- )
- if (!context.isRunningOnTv()) {
- Row(
- verticalAlignment = Alignment.CenterVertically,
- modifier =
- Modifier
- .fillMaxSize()
- .padding(top = 5.dp),
- horizontalArrangement = Arrangement.Center,
- ) {
- TextButton(
- enabled = !didExportFiles,
- onClick = {
- if (uiState.tunnels.isEmpty()) return@TextButton context.showToast(R.string.tunnel_required)
- showAuthPrompt = true
- },
- ) {
- Text(stringResource(R.string.export_configs))
- }
+ Text(stringResource(R.string.export_configs))
}
}
}
diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/SettingsUiState.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/SettingsUiState.kt
deleted file mode 100644
index 9adcf85..0000000
--- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/SettingsUiState.kt
+++ /dev/null
@@ -1,14 +0,0 @@
-package com.zaneschepke.wireguardautotunnel.ui.screens.settings
-
-import com.zaneschepke.wireguardautotunnel.data.domain.Settings
-import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
-import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnState
-
-data class SettingsUiState(
- val settings: Settings = Settings(),
- val tunnels: List = emptyList(),
- val vpnState: VpnState = VpnState(),
- val isLocationDisclosureShown: Boolean = true,
- val isBatteryOptimizeDisableShown: Boolean = false,
- val isPinLockEnabled: Boolean = false,
-)
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 97e7fd9..7eaf6dd 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
@@ -7,25 +7,23 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.wireguard.android.backend.WgQuickBackend
import com.wireguard.android.util.RootShell
+import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.data.domain.Settings
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
import com.zaneschepke.wireguardautotunnel.module.IoDispatcher
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
-import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelService
-import com.zaneschepke.wireguardautotunnel.util.Constants
+import com.zaneschepke.wireguardautotunnel.ui.common.snackbar.SnackbarController
import com.zaneschepke.wireguardautotunnel.util.FileUtils
-import com.zaneschepke.wireguardautotunnel.util.WgTunnelExceptions
+import com.zaneschepke.wireguardautotunnel.util.StringValue
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.asStateFlow
-import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
-import timber.log.Timber
import java.io.File
import javax.inject.Inject
import javax.inject.Provider
@@ -35,45 +33,29 @@ class SettingsViewModel
@Inject
constructor(
private val appDataRepository: AppDataRepository,
- private val serviceManager: ServiceManager,
private val rootShell: Provider,
private val fileUtils: FileUtils,
@IoDispatcher private val ioDispatcher: CoroutineDispatcher,
- tunnelService: TunnelService,
) : ViewModel() {
+
private val _kernelSupport = MutableStateFlow(false)
val kernelSupport = _kernelSupport.asStateFlow()
+ private val settings = appDataRepository.settings.getSettingsFlow()
+ .stateIn(viewModelScope, SharingStarted.Eagerly, Settings())
- val uiState =
- combine(
- appDataRepository.settings.getSettingsFlow(),
- appDataRepository.tunnels.getTunnelConfigsFlow(),
- tunnelService.vpnState,
- appDataRepository.appState.generalStateFlow,
- ) { settings, tunnels, tunnelState, generalState ->
- SettingsUiState(
- settings,
- tunnels,
- tunnelState,
- generalState.isLocationDisclosureShown,
- generalState.isBatteryOptimizationDisableShown,
- generalState.isPinLockEnabled,
- )
- }
- .stateIn(
- viewModelScope,
- SharingStarted.WhileSubscribed(Constants.SUBSCRIPTION_TIMEOUT),
- SettingsUiState(),
- )
-
- fun onSaveTrustedSSID(ssid: String): Result {
+ fun onSaveTrustedSSID(ssid: String) = viewModelScope.launch {
val trimmed = ssid.trim()
- return if (!uiState.value.settings.trustedNetworkSSIDs.contains(trimmed)) {
- uiState.value.settings.trustedNetworkSSIDs.add(trimmed)
- saveSettings(uiState.value.settings)
- Result.success(Unit)
- } else {
- Result.failure(WgTunnelExceptions.SsidConflict())
+ with(settings.value) {
+ if (!trustedNetworkSSIDs.contains(trimmed)) {
+ this.trustedNetworkSSIDs.add(ssid)
+ appDataRepository.settings.save(this)
+ } else {
+ SnackbarController.showMessage(
+ StringValue.StringResource(
+ R.string.error_ssid_exists,
+ ),
+ )
+ }
}
}
@@ -85,61 +67,70 @@ constructor(
appDataRepository.appState.setBatteryOptimizationDisableShown(true)
}
- fun onToggleTunnelOnMobileData() {
- saveSettings(
- uiState.value.settings.copy(
- isTunnelOnMobileDataEnabled = !uiState.value.settings.isTunnelOnMobileDataEnabled,
- ),
- )
+ fun onToggleTunnelOnMobileData() = viewModelScope.launch {
+ with(settings.value) {
+ appDataRepository.settings.save(
+ copy(
+ isTunnelOnMobileDataEnabled = !this.isTunnelOnMobileDataEnabled,
+ ),
+ )
+ }
}
- fun onDeleteTrustedSSID(ssid: String) {
- saveSettings(
- uiState.value.settings.copy(
- trustedNetworkSSIDs =
- (uiState.value.settings.trustedNetworkSSIDs - ssid).toMutableList(),
- ),
- )
+ fun onDeleteTrustedSSID(ssid: String) = viewModelScope.launch {
+ with(settings.value) {
+ appDataRepository.settings.save(
+ copy(
+ trustedNetworkSSIDs = (this.trustedNetworkSSIDs - ssid).toMutableList(),
+ ),
+ )
+ }
}
- suspend fun onExportTunnels(files: List): Result {
- return fileUtils.saveFilesToZip(files)
+ private fun exportTunnels(files: List) = viewModelScope.launch {
+ fileUtils.saveFilesToZip(files).onSuccess {
+ SnackbarController.showMessage(StringValue.StringResource(R.string.exported_configs_message))
+ }.onFailure {
+ SnackbarController.showMessage(StringValue.StringResource(R.string.export_configs_failed))
+ }
}
fun onToggleAutoTunnel(context: Context) = viewModelScope.launch {
- val isAutoTunnelEnabled = uiState.value.settings.isAutoTunnelEnabled
- var isAutoTunnelPaused = uiState.value.settings.isAutoTunnelPaused
-
- if (isAutoTunnelEnabled) {
- serviceManager.stopWatcherService(context)
- } else {
- serviceManager.startWatcherService(context)
- isAutoTunnelPaused = false
+ with(settings.value) {
+ var isAutoTunnelPaused = this.isAutoTunnelPaused
+ if (isAutoTunnelEnabled) {
+ ServiceManager.stopWatcherService(context)
+ } else {
+ ServiceManager.startWatcherService(context)
+ isAutoTunnelPaused = false
+ }
+ appDataRepository.settings.save(
+ copy(
+ isAutoTunnelEnabled = !isAutoTunnelEnabled,
+ isAutoTunnelPaused = isAutoTunnelPaused,
+ ),
+ )
}
- saveSettings(
- uiState.value.settings.copy(
- isAutoTunnelEnabled = !isAutoTunnelEnabled,
- isAutoTunnelPaused = isAutoTunnelPaused,
- ),
- )
}
fun onToggleAlwaysOnVPN() = viewModelScope.launch {
- saveSettings(
- uiState.value.settings.copy(
- isAlwaysOnVpnEnabled = !uiState.value.settings.isAlwaysOnVpnEnabled,
- ),
- )
+ with(settings.value) {
+ appDataRepository.settings.save(
+ copy(
+ isAlwaysOnVpnEnabled = !isAlwaysOnVpnEnabled,
+ ),
+ )
+ }
}
- private fun saveSettings(settings: Settings) = viewModelScope.launch { appDataRepository.settings.save(settings) }
-
- fun onToggleTunnelOnEthernet() {
- saveSettings(
- uiState.value.settings.copy(
- isTunnelOnEthernetEnabled = !uiState.value.settings.isTunnelOnEthernetEnabled,
- ),
- )
+ fun onToggleTunnelOnEthernet() = viewModelScope.launch {
+ with(settings.value) {
+ appDataRepository.settings.save(
+ copy(
+ isTunnelOnEthernetEnabled = !isTunnelOnEthernetEnabled,
+ ),
+ )
+ }
}
fun isLocationEnabled(context: Context): Boolean {
@@ -150,73 +141,74 @@ constructor(
return LocationManagerCompat.isLocationEnabled(locationManager)
}
- fun onToggleShortcutsEnabled() {
- saveSettings(
- uiState.value.settings.copy(
- isShortcutsEnabled = !uiState.value.settings.isShortcutsEnabled,
- ),
- )
+ fun onToggleShortcutsEnabled() = viewModelScope.launch {
+ with(settings.value) {
+ appDataRepository.settings.save(
+ this.copy(
+ isShortcutsEnabled = !isShortcutsEnabled,
+ ),
+ )
+ }
}
- private fun saveKernelMode(enabled: Boolean) {
- saveSettings(
- uiState.value.settings.copy(
- isKernelEnabled = enabled,
- ),
- )
+ private fun saveKernelMode(enabled: Boolean) = viewModelScope.launch {
+ with(settings.value) {
+ appDataRepository.settings.save(
+ this.copy(
+ isKernelEnabled = enabled,
+ ),
+ )
+ }
}
- fun onToggleTunnelOnWifi() {
- saveSettings(
- uiState.value.settings.copy(
- isTunnelOnWifiEnabled = !uiState.value.settings.isTunnelOnWifiEnabled,
- ),
- )
+ fun onToggleTunnelOnWifi() = viewModelScope.launch {
+ with(settings.value) {
+ appDataRepository.settings.save(
+ copy(
+ isTunnelOnWifiEnabled = !isTunnelOnWifiEnabled,
+ ),
+ )
+ }
}
fun onToggleAmnezia() = viewModelScope.launch {
- if (uiState.value.settings.isKernelEnabled) {
- saveKernelMode(false)
+ with(settings.value) {
+ if (isKernelEnabled) {
+ saveKernelMode(false)
+ }
+ appDataRepository.settings.save(
+ copy(
+ isAmneziaEnabled = !isAmneziaEnabled,
+ ),
+ )
}
- saveAmneziaMode(!uiState.value.settings.isAmneziaEnabled)
}
- private fun saveAmneziaMode(on: Boolean) {
- saveSettings(
- uiState.value.settings.copy(
- isAmneziaEnabled = on,
- ),
- )
- }
-
- fun onToggleKernelMode(onSuccess: () -> Unit, onFailure: () -> Unit) = viewModelScope.launch {
- if (!uiState.value.settings.isKernelEnabled) {
- requestRoot(
- {
- onSuccess()
- saveSettings(
- uiState.value.settings.copy(
+ fun onToggleKernelMode() = viewModelScope.launch {
+ with(settings.value) {
+ if (!isKernelEnabled) {
+ requestRoot().onSuccess {
+ appDataRepository.settings.save(
+ copy(
isKernelEnabled = true,
isAmneziaEnabled = false,
),
)
- },
- {
- onFailure()
- saveKernelMode(enabled = false)
- },
- )
- } else {
- saveKernelMode(enabled = false)
+ }
+ } else {
+ saveKernelMode(enabled = false)
+ }
}
}
fun onToggleRestartOnPing() = viewModelScope.launch {
- saveSettings(
- uiState.value.settings.copy(
- isPingEnabled = !uiState.value.settings.isPingEnabled,
- ),
- )
+ with(settings.value) {
+ appDataRepository.settings.save(
+ copy(
+ isPingEnabled = !isPingEnabled,
+ ),
+ )
+ }
}
fun checkKernelSupport() = viewModelScope.launch {
@@ -230,31 +222,36 @@ constructor(
}
fun onToggleRestartAtBoot() = viewModelScope.launch {
- saveSettings(
- uiState.value.settings.copy(
- isRestoreOnBootEnabled = !uiState.value.settings.isRestoreOnBootEnabled,
- ),
- )
- }
-
- fun requestRoot(onSuccess: () -> Unit, onFailure: () -> Unit) = viewModelScope.launch(ioDispatcher) {
- kotlin.runCatching {
- rootShell.get().start()
- Timber.i("Root shell accepted!")
- onSuccess()
- }.onFailure {
- onFailure()
- }.onSuccess {
- onSuccess()
+ with(settings.value) {
+ appDataRepository.settings.save(
+ copy(
+ isRestoreOnBootEnabled = !isRestoreOnBootEnabled,
+ ),
+ )
}
}
- suspend fun exportAllConfigs(): Result {
- return kotlin.runCatching {
+ private suspend fun requestRoot(): Result {
+ return withContext(ioDispatcher) {
+ kotlin.runCatching {
+ rootShell.get().start()
+ SnackbarController.showMessage(StringValue.StringResource(R.string.root_accepted))
+ }.onFailure {
+ SnackbarController.showMessage(StringValue.StringResource(R.string.error_root_denied))
+ }
+ }
+ }
+
+ fun onRequestRoot() = viewModelScope.launch {
+ requestRoot()
+ }
+
+ fun exportAllConfigs() = viewModelScope.launch {
+ kotlin.runCatching {
val tunnels = appDataRepository.tunnels.getAll()
val wgFiles = fileUtils.createWgFiles(tunnels)
val amFiles = fileUtils.createAmFiles(tunnels)
- onExportTunnels(wgFiles + amFiles)
+ exportTunnels(wgFiles + amFiles)
}
}
}
diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/components/WildcardSupportingLabel.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/components/WildcardSupportingLabel.kt
new file mode 100644
index 0000000..ec6986b
--- /dev/null
+++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/components/WildcardSupportingLabel.kt
@@ -0,0 +1,44 @@
+package com.zaneschepke.wireguardautotunnel.ui.screens.settings.components
+
+import androidx.compose.foundation.text.ClickableText
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.SpanStyle
+import androidx.compose.ui.text.buildAnnotatedString
+import androidx.compose.ui.text.font.FontStyle
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.text.withStyle
+import com.zaneschepke.wireguardautotunnel.R
+
+@Composable
+fun WildcardSupportingLabel(onClick: (url: String) -> Unit) {
+ // TODO update link when docs are fully updated
+ val gettingStarted =
+ buildAnnotatedString {
+ pushStringAnnotation(
+ tag = "details",
+ annotation = stringResource(id = R.string.docs_features),
+ )
+ withStyle(
+ style = SpanStyle(color = MaterialTheme.colorScheme.primary),
+ ) {
+ append(stringResource(id = R.string.wildcard_supported))
+ }
+ pop()
+ }
+ ClickableText(
+ text = gettingStarted,
+ style =
+ MaterialTheme.typography.bodySmall.copy(
+ color = MaterialTheme.colorScheme.onSurfaceVariant,
+ textAlign = TextAlign.Start,
+ fontStyle = FontStyle.Italic,
+ ),
+ ) {
+ gettingStarted.getStringAnnotations(tag = "details", it, it)
+ .firstOrNull()?.let { annotation ->
+ onClick(annotation.item)
+ }
+ }
+}
diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/support/SupportScreen.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/support/SupportScreen.kt
index be87dae..d13b1da 100644
--- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/support/SupportScreen.kt
+++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/support/SupportScreen.kt
@@ -27,7 +27,6 @@ import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
-import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
@@ -43,23 +42,20 @@ import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
-import androidx.hilt.navigation.compose.hiltViewModel
-import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.navigation.NavController
import com.zaneschepke.wireguardautotunnel.BuildConfig
import com.zaneschepke.wireguardautotunnel.R
+import com.zaneschepke.wireguardautotunnel.ui.AppUiState
import com.zaneschepke.wireguardautotunnel.ui.Screen
import com.zaneschepke.wireguardautotunnel.util.extensions.isRunningOnTv
import com.zaneschepke.wireguardautotunnel.util.extensions.launchSupportEmail
import com.zaneschepke.wireguardautotunnel.util.extensions.openWebUrl
@Composable
-fun SupportScreen(viewModel: SupportViewModel = hiltViewModel(), navController: NavController, focusRequester: FocusRequester) {
+fun SupportScreen(navController: NavController, focusRequester: FocusRequester, appUiState: AppUiState) {
val context = LocalContext.current
val fillMaxWidth = .85f
- val uiState by viewModel.uiState.collectAsStateWithLifecycle()
-
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Top,
@@ -301,7 +297,7 @@ fun SupportScreen(viewModel: SupportViewModel = hiltViewModel(), navController:
buildAnnotatedString {
append(stringResource(R.string.mode))
append(": ")
- when (uiState.settings.isKernelEnabled) {
+ when (appUiState.settings.isKernelEnabled) {
true -> append(stringResource(id = R.string.kernel))
false -> append(stringResource(id = R.string.userspace))
}
diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/support/SupportUiState.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/support/SupportUiState.kt
deleted file mode 100644
index 9484409..0000000
--- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/support/SupportUiState.kt
+++ /dev/null
@@ -1,5 +0,0 @@
-package com.zaneschepke.wireguardautotunnel.ui.screens.support
-
-import com.zaneschepke.wireguardautotunnel.data.domain.Settings
-
-data class SupportUiState(val settings: Settings = Settings())
diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/support/SupportViewModel.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/support/SupportViewModel.kt
deleted file mode 100644
index 6874a3b..0000000
--- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/support/SupportViewModel.kt
+++ /dev/null
@@ -1,27 +0,0 @@
-package com.zaneschepke.wireguardautotunnel.ui.screens.support
-
-import androidx.lifecycle.ViewModel
-import androidx.lifecycle.viewModelScope
-import com.zaneschepke.wireguardautotunnel.data.repository.SettingsRepository
-import com.zaneschepke.wireguardautotunnel.util.Constants
-import dagger.hilt.android.lifecycle.HiltViewModel
-import kotlinx.coroutines.flow.SharingStarted
-import kotlinx.coroutines.flow.map
-import kotlinx.coroutines.flow.stateIn
-import javax.inject.Inject
-
-@HiltViewModel
-class SupportViewModel
-@Inject
-constructor(settingsRepository: SettingsRepository) :
- ViewModel() {
- val uiState =
- settingsRepository
- .getSettingsFlow()
- .map { SupportUiState(it) }
- .stateIn(
- viewModelScope,
- SharingStarted.WhileSubscribed(Constants.SUBSCRIPTION_TIMEOUT),
- SupportUiState(),
- )
-}
diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/WgTunnelExceptions.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/WgTunnelExceptions.kt
index afd38f4..dd3dee8 100644
--- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/WgTunnelExceptions.kt
+++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/WgTunnelExceptions.kt
@@ -6,24 +6,6 @@ import com.zaneschepke.wireguardautotunnel.R
sealed class WgTunnelExceptions : Exception() {
abstract fun getMessage(context: Context): String
- data class General(private val userMessage: StringValue) : WgTunnelExceptions() {
- override fun getMessage(context: Context): String {
- return userMessage.asString(context)
- }
- }
-
- data class SsidConflict(
- private val userMessage: StringValue =
- StringValue.StringResource(
- R.string.error_ssid_exists,
- ),
- ) :
- WgTunnelExceptions() {
- override fun getMessage(context: Context): String {
- return userMessage.asString(context)
- }
- }
-
data class ConfigExportFailed(
private val userMessage: StringValue =
StringValue.StringResource(
@@ -44,18 +26,6 @@ sealed class WgTunnelExceptions : Exception() {
}
}
- data class RootDenied(
- private val userMessage: StringValue =
- StringValue.StringResource(
- R.string.error_root_denied,
- ),
- ) :
- WgTunnelExceptions() {
- override fun getMessage(context: Context): String {
- return userMessage.asString(context)
- }
- }
-
data class InvalidQrCode(
private val userMessage: StringValue =
StringValue.StringResource(
@@ -90,70 +60,4 @@ sealed class WgTunnelExceptions : Exception() {
return userMessage.asString(context)
}
}
-
- data class AuthenticationFailed(
- private val userMessage: StringValue =
- StringValue.StringResource(
- R.string.error_authentication_failed,
- ),
- ) : WgTunnelExceptions() {
- override fun getMessage(context: Context): String {
- return userMessage.asString(context)
- }
- }
-
- data class AuthorizationFailed(
- private val userMessage: StringValue =
- StringValue.StringResource(
- R.string.error_authorization_failed,
- ),
- ) : WgTunnelExceptions() {
- override fun getMessage(context: Context): String {
- return userMessage.asString(context)
- }
- }
-
- data class BackgroundLocationRequired(
- private val userMessage: StringValue =
- StringValue.StringResource(
- R.string.background_location_required,
- ),
- ) : WgTunnelExceptions() {
- override fun getMessage(context: Context): String {
- return userMessage.asString(context)
- }
- }
-
- data class LocationServicesRequired(
- private val userMessage: StringValue =
- StringValue.StringResource(
- R.string.location_services_required,
- ),
- ) : WgTunnelExceptions() {
- override fun getMessage(context: Context): String {
- return userMessage.asString(context)
- }
- }
-
- data class PreciseLocationRequired(
- private val userMessage: StringValue =
- StringValue.StringResource(
- R.string.precise_location_required,
- ),
- ) : WgTunnelExceptions() {
- override fun getMessage(context: Context): String {
- return userMessage.asString(context)
- }
- }
-
- data class FileExplorerRequired(
- private val userMessage: StringValue =
- StringValue.StringResource(
- R.string.error_no_file_explorer,
- ),
- ) : WgTunnelExceptions() {
- override fun getMessage(context: Context): String {
- return userMessage.asString(context)
- }
- }
}
diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/extensions/CoroutineExtensions.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/extensions/CoroutineExtensions.kt
index d56586d..ecbc3bd 100644
--- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/extensions/CoroutineExtensions.kt
+++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/extensions/CoroutineExtensions.kt
@@ -3,6 +3,7 @@ package com.zaneschepke.wireguardautotunnel.util.extensions
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.Job
import kotlinx.coroutines.ObsoleteCoroutinesApi
import kotlinx.coroutines.channels.ClosedReceiveChannelException
import kotlinx.coroutines.channels.ReceiveChannel
@@ -76,3 +77,16 @@ fun CoroutineScope.asChannel(flow: Flow): ReceiveChannel = produce {
channel.send(value)
}
}
+
+fun Job?.onNotRunning(callback: () -> Unit) {
+ if (this == null || this.isCompleted || this.isCompleted) {
+ callback.invoke()
+ }
+}
+
+fun Job.cancelWithMessage(message: String) {
+ kotlin.runCatching {
+ this.cancel()
+ Timber.i(message)
+ }
+}
diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/extensions/StringExtensions.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/extensions/StringExtensions.kt
new file mode 100644
index 0000000..786e32d
--- /dev/null
+++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/extensions/StringExtensions.kt
@@ -0,0 +1,30 @@
+package com.zaneschepke.wireguardautotunnel.util.extensions
+
+import timber.log.Timber
+import java.util.regex.Pattern
+
+fun String.isValidIpv4orIpv6Address(): Boolean {
+ val ipv4Pattern = Pattern.compile(
+ "^(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})\$",
+ )
+ val ipv6Pattern = Pattern.compile(
+ "^([0-9A-Fa-f]{1,4}:){7}[0-9A-Fa-f]{1,4}\$",
+ )
+ return ipv4Pattern.matcher(this).matches() || ipv6Pattern.matcher(this).matches()
+}
+
+fun List.isMatchingToWildcardList(value: String): Boolean {
+ val excludeValues = this.filter { it.startsWith("!") }.map { it.removePrefix("!").toRegexWithWildcards() }
+ Timber.d("Excluded values: $excludeValues")
+ val includedValues = this.filter { !it.startsWith("!") }.map { it.toRegexWithWildcards() }
+ Timber.d("Included values: $includedValues")
+ val matches = includedValues.filter { it.matches(value) }
+ val excludedMatches = excludeValues.filter { it.matches(value) }
+ Timber.d("Excluded matches: $excludedMatches")
+ Timber.d("Matches: $matches")
+ return matches.isNotEmpty() && excludedMatches.isEmpty()
+}
+
+fun String.toRegexWithWildcards(): Regex {
+ return this.replace("*", ".*").replace("?", ".").toRegex()
+}
diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/extensions/TunnelExtensions.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/extensions/TunnelExtensions.kt
index 0b86cc2..1e1bd44 100644
--- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/extensions/TunnelExtensions.kt
+++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/extensions/TunnelExtensions.kt
@@ -1,10 +1,13 @@
package com.zaneschepke.wireguardautotunnel.util.extensions
+import com.wireguard.config.Peer
import com.zaneschepke.wireguardautotunnel.service.tunnel.HandshakeStatus
import com.zaneschepke.wireguardautotunnel.service.tunnel.statistics.TunnelStatistics
import com.zaneschepke.wireguardautotunnel.util.Constants
import com.zaneschepke.wireguardautotunnel.util.NumberUtils
import org.amnezia.awg.config.Config
+import timber.log.Timber
+import java.net.InetAddress
fun TunnelStatistics.mapPeerStats(): Map {
return this.getPeers().associateWith { key -> (this.peerStats(key)) }
@@ -28,6 +31,23 @@ fun TunnelStatistics.PeerStats.handshakeStatus(): HandshakeStatus {
}
}
+fun Peer.isReachable(): Boolean {
+ val host =
+ if (this.endpoint.isPresent &&
+ this.endpoint.get().resolved.isPresent
+ ) {
+ this.endpoint.get().resolved.get().host
+ } else {
+ Constants.DEFAULT_PING_IP
+ }
+ Timber.i("Checking reachability of peer: $host")
+ val reachable =
+ InetAddress.getByName(host)
+ .isReachable(Constants.PING_TIMEOUT.toInt())
+ Timber.i("Result: reachable - $reachable")
+ return reachable
+}
+
fun Config.toWgQuickString(): String {
val amQuick = toAwgQuickString(true)
val lines = amQuick.lines().toMutableList()
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index e0b2dfd..5cd3497 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -6,6 +6,7 @@
Watcher Notification Channel
https://github.com/zaneschepke/wgtunnel/issues
https://zaneschepke.com/wgtunnel-docs/overview.html
+ https://zaneschepke.com/wgtunnel-docs/features.html
https://zaneschepke.com/wgtunnel-docs/privacypolicy.html
File is not a .conf or .zip
Action requires tunnel off
@@ -186,4 +187,11 @@
app settings
to make sure these permissions are enabled.
Root shell accepted
+ Set custom ping ip
+ (optional, defaults to peers)
+ Ping interval (sec)
+ "optional, default: "
+ Ping restart cooldown (sec)
+ Learn about supported wildcards.
+ details
diff --git a/buildSrc/src/main/kotlin/Constants.kt b/buildSrc/src/main/kotlin/Constants.kt
index 07881d6..86afb18 100644
--- a/buildSrc/src/main/kotlin/Constants.kt
+++ b/buildSrc/src/main/kotlin/Constants.kt
@@ -1,7 +1,7 @@
object Constants {
const val VERSION_NAME = "3.5.1"
const val JVM_TARGET = "17"
- const val VERSION_CODE = 35102
+ const val VERSION_CODE = 35103
const val TARGET_SDK = 34
const val MIN_SDK = 26
const val APP_ID = "com.zaneschepke.wireguardautotunnel"
@@ -19,5 +19,5 @@ object Constants {
const val TYPE = "type"
const val NIGHTLY_CODE = 42
- const val PRERELEASE_CODE = 53
+ const val PRERELEASE_CODE = 54
}
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 348f6a5..aece179 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -1,6 +1,6 @@
[versions]
-accompanist = "0.34.0"
-activityCompose = "1.9.1"
+accompanist = "0.36.0"
+activityCompose = "1.9.2"
amneziawgAndroid = "1.2.2"
androidx-junit = "1.2.1"
appcompat = "1.7.0"
@@ -8,25 +8,25 @@ biometricKtx = "1.2.0-alpha05"
coreGoogleShortcuts = "1.1.0"
coreKtx = "1.13.1"
datastorePreferences = "1.1.1"
-desugar_jdk_libs = "2.0.4"
+desugar_jdk_libs = "2.1.2"
espressoCore = "3.6.1"
hiltAndroid = "2.52"
hiltNavigationCompose = "1.2.0"
junit = "4.13.2"
-kotlinx-serialization-json = "1.7.1"
-lifecycle-runtime-compose = "2.8.4"
-material3 = "1.2.1"
+kotlinx-serialization-json = "1.7.2"
+lifecycle-runtime-compose = "2.8.5"
+material3 = "1.3.0"
multifabVersion = "1.1.1"
-navigationCompose = "2.7.7"
+navigationCompose = "2.8.0"
pinLockCompose = "1.0.3"
roomVersion = "2.6.1"
timber = "5.0.1"
-tunnel = "1.2.3"
+tunnel = "1.2.4"
androidGradlePlugin = "8.6.0"
kotlin = "2.0.20"
ksp = "2.0.20-1.0.24"
-composeBom = "2024.08.00"
-compose = "1.6.8"
+composeBom = "2024.09.00"
+compose = "1.7.0"
zxingAndroidEmbedded = "4.3.0"
coreSplashscreen = "1.0.1"
gradlePlugins-grgit = "5.2.2"
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index c861f8e..ac7bef3 100644
--- a/gradle/wrapper/gradle-wrapper.properties
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -2,7 +2,7 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip
-distributionSha256Sum=544c35d6bd849ae8a5ed0bcea39ba677dc40f49df7d1835561582da2009b961d
+distributionSha256Sum=d725d707bfabd4dfdc958c624003b3c80accc03f7037b5122c4b1d0ef15cecab
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists