Compare commits

..

1 Commits

Author SHA1 Message Date
dependabot[bot] 756d7cbea0 chore(deps): bump actions/setup-java from 4 to 5
Bumps [actions/setup-java](https://github.com/actions/setup-java) from 4 to 5.
- [Release notes](https://github.com/actions/setup-java/releases)
- [Commits](https://github.com/actions/setup-java/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/setup-java
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-08-21 13:17:07 +00:00
116 changed files with 1350 additions and 2527 deletions
+1 -1
View File
@@ -76,7 +76,7 @@ jobs:
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Set up JDK 17 - name: Set up JDK 17
uses: actions/setup-java@v4 uses: actions/setup-java@v5
with: with:
distribution: 'temurin' distribution: 'temurin'
java-version: '17' java-version: '17'
+1 -1
View File
@@ -12,7 +12,7 @@ jobs:
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Set up JDK 17 - name: Set up JDK 17
uses: actions/setup-java@v4 uses: actions/setup-java@v5
with: with:
distribution: 'temurin' distribution: 'temurin'
java-version: '17' java-version: '17'
+1 -1
View File
@@ -191,7 +191,7 @@ jobs:
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Set up JDK 17 - name: Set up JDK 17
uses: actions/setup-java@v4 uses: actions/setup-java@v5
with: with:
distribution: 'temurin' distribution: 'temurin'
java-version: '17' java-version: '17'
-1
View File
@@ -197,7 +197,6 @@ dependencies {
implementation(libs.zxing.android.embedded) implementation(libs.zxing.android.embedded)
implementation(libs.material.icons.core)
implementation(libs.material.icons.extended) implementation(libs.material.icons.extended)
implementation(libs.androidx.biometric.ktx) implementation(libs.androidx.biometric.ktx)
@@ -1,359 +0,0 @@
{
"formatVersion": 1,
"database": {
"version": 20,
"identityHash": "51f828868c0ea2f0f5c987410ff5c5a1",
"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_restore_on_boot_enabled` INTEGER NOT NULL DEFAULT false, `is_multi_tunnel_enabled` INTEGER NOT NULL DEFAULT false, `is_ping_enabled` INTEGER NOT NULL DEFAULT false, `is_wildcards_enabled` INTEGER NOT NULL DEFAULT false, `is_stop_on_no_internet_enabled` INTEGER NOT NULL DEFAULT false, `is_lan_on_kill_switch_enabled` INTEGER NOT NULL DEFAULT false, `debounce_delay_seconds` INTEGER NOT NULL DEFAULT 3, `is_disable_kill_switch_on_trusted_enabled` INTEGER NOT NULL DEFAULT false, `is_tunnel_on_unsecure_enabled` INTEGER NOT NULL DEFAULT false, `wifi_detection_method` INTEGER NOT NULL DEFAULT 0, `is_ping_monitoring_enabled` INTEGER NOT NULL DEFAULT true, `tunnel_ping_interval_sec` INTEGER NOT NULL DEFAULT 30, `tunnel_ping_attempts` INTEGER NOT NULL DEFAULT 3, `tunnel_ping_timeout_sec` INTEGER, `app_mode` INTEGER NOT NULL DEFAULT 0, `dns_protocol` INTEGER NOT NULL DEFAULT 0, `dns_endpoint` TEXT)",
"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": "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": "isPingEnabled",
"columnName": "is_ping_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isWildcardsEnabled",
"columnName": "is_wildcards_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isStopOnNoInternetEnabled",
"columnName": "is_stop_on_no_internet_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isLanOnKillSwitchEnabled",
"columnName": "is_lan_on_kill_switch_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "debounceDelaySeconds",
"columnName": "debounce_delay_seconds",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "3"
},
{
"fieldPath": "isDisableKillSwitchOnTrustedEnabled",
"columnName": "is_disable_kill_switch_on_trusted_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isTunnelOnUnsecureEnabled",
"columnName": "is_tunnel_on_unsecure_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "wifiDetectionMethod",
"columnName": "wifi_detection_method",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "isPingMonitoringEnabled",
"columnName": "is_ping_monitoring_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "true"
},
{
"fieldPath": "tunnelPingIntervalSeconds",
"columnName": "tunnel_ping_interval_sec",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "30"
},
{
"fieldPath": "tunnelPingAttempts",
"columnName": "tunnel_ping_attempts",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "3"
},
{
"fieldPath": "tunnelPingTimeoutSeconds",
"columnName": "tunnel_ping_timeout_sec",
"affinity": "INTEGER"
},
{
"fieldPath": "appMode",
"columnName": "app_mode",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "dnsProtocol",
"columnName": "dns_protocol",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "dnsEndpoint",
"columnName": "dns_endpoint",
"affinity": "TEXT"
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
}
},
{
"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, `restart_on_ping_failure` INTEGER NOT NULL DEFAULT false, `ping_target` TEXT DEFAULT null, `is_ethernet_tunnel` INTEGER NOT NULL DEFAULT false, `is_ipv4_preferred` INTEGER NOT NULL DEFAULT true, `position` INTEGER NOT NULL DEFAULT 0, `auto_tunnel_apps` TEXT NOT NULL DEFAULT '[]')",
"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": "restartOnPingFailure",
"columnName": "restart_on_ping_failure",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "pingTarget",
"columnName": "ping_target",
"affinity": "TEXT",
"defaultValue": "null"
},
{
"fieldPath": "isEthernetTunnel",
"columnName": "is_ethernet_tunnel",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isIpv4Preferred",
"columnName": "is_ipv4_preferred",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "true"
},
{
"fieldPath": "position",
"columnName": "position",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "autoTunnelApps",
"columnName": "auto_tunnel_apps",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "'[]'"
}
],
"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`)"
}
]
},
{
"tableName": "proxy_settings",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `socks5_proxy_enabled` INTEGER NOT NULL DEFAULT false, `socks5_proxy_bind_address` TEXT, `http_proxy_enable` INTEGER NOT NULL DEFAULT false, `http_proxy_bind_address` TEXT, `proxy_username` TEXT, `proxy_password` TEXT)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "socks5ProxyEnabled",
"columnName": "socks5_proxy_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "socks5ProxyBindAddress",
"columnName": "socks5_proxy_bind_address",
"affinity": "TEXT"
},
{
"fieldPath": "httpProxyEnabled",
"columnName": "http_proxy_enable",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "httpProxyBindAddress",
"columnName": "http_proxy_bind_address",
"affinity": "TEXT"
},
{
"fieldPath": "proxyUsername",
"columnName": "proxy_username",
"affinity": "TEXT"
},
{
"fieldPath": "proxyPassword",
"columnName": "proxy_password",
"affinity": "TEXT"
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
}
}
],
"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, '51f828868c0ea2f0f5c987410ff5c5a1')"
]
}
}
@@ -4,6 +4,7 @@ import androidx.room.testing.MigrationTestHelper
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry import androidx.test.platform.app.InstrumentationRegistry
import com.zaneschepke.wireguardautotunnel.data.AppDatabase import com.zaneschepke.wireguardautotunnel.data.AppDatabase
import com.zaneschepke.wireguardautotunnel.data.Queries
import java.io.IOException import java.io.IOException
import org.junit.Rule import org.junit.Rule
import org.junit.Test import org.junit.Test
@@ -23,6 +24,8 @@ class MigrationTest {
helper.createDatabase(dbName, 6).apply { helper.createDatabase(dbName, 6).apply {
// Database has schema version 1. Insert some data using SQL queries. // Database has schema version 1. Insert some data using SQL queries.
// You can't use DAO classes because they expect the latest schema. // You can't use DAO classes because they expect the latest schema.
execSQL(Queries.createDefaultSettings())
execSQL(Queries.createTunnelConfig())
// Prepare for the next version. // Prepare for the next version.
close() close()
} }
@@ -3,7 +3,6 @@ package com.zaneschepke.wireguardautotunnel
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.content.Intent import android.content.Intent
import android.graphics.Color import android.graphics.Color
import android.net.VpnService
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.provider.Settings import android.provider.Settings
@@ -25,9 +24,7 @@ import androidx.compose.material3.*
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.zIndex
import androidx.core.net.toUri import androidx.core.net.toUri
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
@@ -40,12 +37,10 @@ import androidx.navigation.toRoute
import com.zaneschepke.networkmonitor.NetworkMonitor import com.zaneschepke.networkmonitor.NetworkMonitor
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager
import com.zaneschepke.wireguardautotunnel.data.AppDatabase import com.zaneschepke.wireguardautotunnel.data.AppDatabase
import com.zaneschepke.wireguardautotunnel.data.model.AppMode
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
import com.zaneschepke.wireguardautotunnel.di.MainDispatcher import com.zaneschepke.wireguardautotunnel.di.MainDispatcher
import com.zaneschepke.wireguardautotunnel.domain.repository.AppStateRepository import com.zaneschepke.wireguardautotunnel.domain.repository.AppStateRepository
import com.zaneschepke.wireguardautotunnel.ui.Route import com.zaneschepke.wireguardautotunnel.ui.Route
import com.zaneschepke.wireguardautotunnel.ui.common.banner.AppAlertBanner
import com.zaneschepke.wireguardautotunnel.ui.common.dialog.VpnDeniedDialog import com.zaneschepke.wireguardautotunnel.ui.common.dialog.VpnDeniedDialog
import com.zaneschepke.wireguardautotunnel.ui.common.snackbar.CustomSnackBar import com.zaneschepke.wireguardautotunnel.ui.common.snackbar.CustomSnackBar
import com.zaneschepke.wireguardautotunnel.ui.navigation.LocalIsAndroidTV import com.zaneschepke.wireguardautotunnel.ui.navigation.LocalIsAndroidTV
@@ -65,18 +60,15 @@ import com.zaneschepke.wireguardautotunnel.ui.screens.main.splittunnel.SplitTunn
import com.zaneschepke.wireguardautotunnel.ui.screens.main.tunneloptions.TunnelOptionsScreen import com.zaneschepke.wireguardautotunnel.ui.screens.main.tunneloptions.TunnelOptionsScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.pin.PinLockScreen import com.zaneschepke.wireguardautotunnel.ui.screens.pin.PinLockScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.SettingsScreen import com.zaneschepke.wireguardautotunnel.ui.screens.settings.SettingsScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.advanced.SettingsAdvancedScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.appearance.AppearanceScreen import com.zaneschepke.wireguardautotunnel.ui.screens.settings.appearance.AppearanceScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.appearance.display.DisplayScreen import com.zaneschepke.wireguardautotunnel.ui.screens.settings.appearance.display.DisplayScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.appearance.language.LanguageScreen import com.zaneschepke.wireguardautotunnel.ui.screens.settings.appearance.language.LanguageScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.dns.DnsSettingsScreen import com.zaneschepke.wireguardautotunnel.ui.screens.settings.killswitch.KillSwitchScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.logs.LogsScreen import com.zaneschepke.wireguardautotunnel.ui.screens.settings.logs.LogsScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.monitoring.TunnelMonitoringScreen import com.zaneschepke.wireguardautotunnel.ui.screens.settings.monitoring.TunnelMonitoringScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.proxy.ProxySettingsScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.system.SystemFeaturesScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.support.SupportScreen import com.zaneschepke.wireguardautotunnel.ui.screens.support.SupportScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.support.license.LicenseScreen import com.zaneschepke.wireguardautotunnel.ui.screens.support.license.LicenseScreen
import com.zaneschepke.wireguardautotunnel.ui.theme.AlertRed
import com.zaneschepke.wireguardautotunnel.ui.theme.OffWhite
import com.zaneschepke.wireguardautotunnel.ui.theme.WireguardAutoTunnelTheme import com.zaneschepke.wireguardautotunnel.ui.theme.WireguardAutoTunnelTheme
import com.zaneschepke.wireguardautotunnel.util.extensions.isRunningOnTv import com.zaneschepke.wireguardautotunnel.util.extensions.isRunningOnTv
import com.zaneschepke.wireguardautotunnel.util.extensions.restartApp import com.zaneschepke.wireguardautotunnel.util.extensions.restartApp
@@ -85,11 +77,11 @@ import com.zaneschepke.wireguardautotunnel.viewmodel.AppViewModel
import com.zaneschepke.wireguardautotunnel.viewmodel.event.AppEvent import com.zaneschepke.wireguardautotunnel.viewmodel.event.AppEvent
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import de.raphaelebner.roomdatabasebackup.core.RoomBackup import de.raphaelebner.roomdatabasebackup.core.RoomBackup
import java.util.Locale
import javax.inject.Inject import javax.inject.Inject
import kotlin.system.exitProcess import kotlin.system.exitProcess
import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.amnezia.awg.backend.GoBackend.VpnService
@AndroidEntryPoint @AndroidEntryPoint
class MainActivity : AppCompatActivity() { class MainActivity : AppCompatActivity() {
@@ -230,143 +222,120 @@ class MainActivity : AppCompatActivity() {
}, },
) )
Box(modifier = Modifier.fillMaxSize()) { Scaffold(
// Top banner if in locked down mode modifier =
if (appUiState.appSettings.appMode == AppMode.LOCK_DOWN) { Modifier.pointerInput(Unit) {
AppAlertBanner( detectTapGestures {
stringResource(R.string.locked_down) viewModel.handleEvent(AppEvent.ClearSelectedTunnels)
.uppercase(Locale.getDefault()),
OffWhite,
AlertRed,
modifier =
Modifier.fillMaxWidth().zIndex(2f), // Draw above everything
)
}
Scaffold(
modifier =
Modifier.pointerInput(Unit) {
detectTapGestures {
viewModel.handleEvent(AppEvent.ClearSelectedTunnels)
}
},
snackbarHost = {
SnackbarHost(snackbar) { snackbarData: SnackbarData ->
CustomSnackBar(
snackbarData.visuals.message,
isRtl = false,
containerColor =
MaterialTheme.colorScheme.surfaceColorAtElevation(
2.dp
),
)
} }
}, },
topBar = { DynamicTopAppBar(navBarState) }, snackbarHost = {
bottomBar = { SnackbarHost(snackbar) { snackbarData: SnackbarData ->
AnimatedVisibility( CustomSnackBar(
visible = navBarState.showBottom, snackbarData.visuals.message,
enter = slideInVertically(initialOffsetY = { it }), isRtl = false,
exit = slideOutVertically(targetOffsetY = { it }), containerColor =
) { MaterialTheme.colorScheme.surfaceColorAtElevation(2.dp),
BottomNavbar(appUiState = appUiState) )
} }
}, },
) { padding -> topBar = { DynamicTopAppBar(navBarState) },
Box( bottomBar = {
modifier = AnimatedVisibility(
Modifier.fillMaxSize() visible = navBarState.showBottom,
.background(MaterialTheme.colorScheme.surface) enter = slideInVertically(initialOffsetY = { it }),
.padding(padding) exit = slideOutVertically(targetOffsetY = { it }),
.consumeWindowInsets(padding)
.imePadding()
) { ) {
NavHost( BottomNavbar(appUiState = appUiState)
navController, }
startDestination = },
(if (appUiState.appState.isPinLockEnabled) Route.Lock ) { padding ->
else Route.Main), Box(
) { modifier =
composable<Route.Main> { Modifier.fillMaxSize()
MainScreen(appUiState, appViewState, viewModel) .background(MaterialTheme.colorScheme.surface)
} .padding(padding)
composable<Route.Settings> { .consumeWindowInsets(padding)
SettingsScreen(appUiState, appViewState, viewModel) .imePadding()
} ) {
composable<Route.LocationDisclosure> { NavHost(
LocationDisclosureScreen(viewModel) navController,
} startDestination =
composable<Route.AutoTunnel> { (if (appUiState.appState.isPinLockEnabled) Route.Lock
AutoTunnelScreen(appUiState, viewModel) else Route.Main),
} ) {
composable<Route.Appearance> { AppearanceScreen() } composable<Route.Main> {
composable<Route.Language> { MainScreen(appUiState, appViewState, viewModel)
LanguageScreen(appUiState, viewModel) }
} composable<Route.Settings> {
composable<Route.Display> { SettingsScreen(appUiState, appViewState, viewModel)
DisplayScreen(appUiState, viewModel) }
} composable<Route.SettingsAdvanced> {
composable<Route.Support> { SettingsAdvancedScreen(appUiState, viewModel)
SupportScreen(appViewModel = viewModel) }
} composable<Route.LocationDisclosure> {
composable<Route.License> { LicenseScreen() } LocationDisclosureScreen(viewModel)
composable<Route.AutoTunnelAdvanced> { }
AutoTunnelAdvancedScreen(appUiState, viewModel) composable<Route.AutoTunnel> {
} AutoTunnelScreen(appUiState, viewModel)
composable<Route.WifiDetectionMethod> { }
WifiDetectionMethodScreen(appUiState, viewModel) composable<Route.Appearance> { AppearanceScreen() }
} composable<Route.Language> {
composable<Route.Logs> { LanguageScreen(appUiState, viewModel)
LogsScreen(appViewState, viewModel) }
} composable<Route.Display> {
composable<Route.Config> { backStack -> DisplayScreen(appUiState, viewModel)
val args = backStack.toRoute<Route.Config>() }
val config = composable<Route.Support> {
appUiState.tunnels.firstOrNull { it.id == args.id } SupportScreen(appViewModel = viewModel)
ConfigScreen(config, appUiState, viewModel) }
} composable<Route.License> { LicenseScreen() }
composable<Route.TunnelOptions> { backStack -> composable<Route.AutoTunnelAdvanced> {
val args = backStack.toRoute<Route.TunnelOptions>() AutoTunnelAdvancedScreen(appUiState, viewModel)
appUiState.tunnels }
.firstOrNull { it.id == args.id } composable<Route.WifiDetectionMethod> {
?.let { config -> WifiDetectionMethodScreen(appUiState, viewModel)
TunnelOptionsScreen( }
config, composable<Route.Logs> { LogsScreen(appViewState, viewModel) }
viewModel, composable<Route.Config> { backStack ->
appViewState, val args = backStack.toRoute<Route.Config>()
appUiState.appSettings, val config =
) appUiState.tunnels.firstOrNull { it.id == args.id }
} ConfigScreen(config, appUiState, viewModel)
} }
composable<Route.Lock> { PinLockScreen(viewModel) } composable<Route.TunnelOptions> { backStack ->
composable<Route.SplitTunnel> { val args = backStack.toRoute<Route.TunnelOptions>()
SplitTunnelScreen(viewModel) appUiState.tunnels
} .firstOrNull { it.id == args.id }
composable<Route.TunnelAutoTunnel> { backStack -> ?.let { config ->
val args = backStack.toRoute<Route.TunnelOptions>() TunnelOptionsScreen(
appUiState.tunnels config,
.firstOrNull { it.id == args.id } viewModel,
?.let { appViewState,
TunnelAutoTunnelScreen( appUiState.appSettings,
it, )
appUiState.appSettings, }
viewModel, }
) composable<Route.Lock> { PinLockScreen(viewModel) }
} composable<Route.KillSwitch> {
} KillSwitchScreen(appUiState, viewModel)
composable<Route.Sort> { SortScreen(appUiState, viewModel) } }
composable<Route.TunnelMonitoring> { composable<Route.SplitTunnel> { SplitTunnelScreen(viewModel) }
TunnelMonitoringScreen(appUiState, viewModel) composable<Route.TunnelAutoTunnel> { backStack ->
} val args = backStack.toRoute<Route.TunnelOptions>()
composable<Route.ProxySettings> { appUiState.tunnels
ProxySettingsScreen(appUiState, viewModel) .firstOrNull { it.id == args.id }
} ?.let {
composable<Route.SystemFeatures> { TunnelAutoTunnelScreen(
SystemFeaturesScreen(appUiState, viewModel) it,
} appUiState.appSettings,
composable<Route.Dns> { viewModel,
DnsSettingsScreen(appUiState, viewModel) )
} }
}
composable<Route.Sort> { SortScreen(appUiState, viewModel) }
composable<Route.TunnelMonitoring> {
TunnelMonitoringScreen(appUiState, viewModel)
} }
} }
} }
@@ -13,7 +13,7 @@ import com.zaneschepke.wireguardautotunnel.core.worker.ServiceWorker
import com.zaneschepke.wireguardautotunnel.di.ApplicationScope import com.zaneschepke.wireguardautotunnel.di.ApplicationScope
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
import com.zaneschepke.wireguardautotunnel.di.MainDispatcher import com.zaneschepke.wireguardautotunnel.di.MainDispatcher
import com.zaneschepke.wireguardautotunnel.domain.enums.BackendMode import com.zaneschepke.wireguardautotunnel.domain.enums.BackendStatus
import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository
import com.zaneschepke.wireguardautotunnel.util.LocaleUtil import com.zaneschepke.wireguardautotunnel.util.LocaleUtil
import com.zaneschepke.wireguardautotunnel.util.ReleaseTree import com.zaneschepke.wireguardautotunnel.util.ReleaseTree
@@ -91,7 +91,7 @@ class WireGuardAutoTunnel : Application(), Configuration.Provider {
override fun onTerminate() { override fun onTerminate() {
applicationScope.cancel() applicationScope.cancel()
tunnelManager.setBackendMode(BackendMode.Inactive) tunnelManager.setBackendStatus(BackendStatus.Inactive)
super.onTerminate() super.onTerminate()
} }
@@ -24,7 +24,6 @@ class RestartReceiver : BroadcastReceiver() {
@Inject lateinit var serviceManager: ServiceManager @Inject lateinit var serviceManager: ServiceManager
// injecting this should let tunnelManger handle clean startup
@Inject lateinit var tunnelManager: TunnelManager @Inject lateinit var tunnelManager: TunnelManager
@Inject lateinit var logReader: LogReader @Inject lateinit var logReader: LogReader
@@ -35,7 +34,22 @@ class RestartReceiver : BroadcastReceiver() {
Timber.d("RestartReceiver triggered with action: ${intent.action}") Timber.d("RestartReceiver triggered with action: ${intent.action}")
serviceManager.updateTunnelTile() serviceManager.updateTunnelTile()
serviceManager.updateAutoTunnelTile() serviceManager.updateAutoTunnelTile()
if (intent.action == Intent.ACTION_MY_PACKAGE_REPLACED) applicationScope.launch(ioDispatcher) {
applicationScope.launch(ioDispatcher) { logReader.deleteAndClearLogs() } val settings = appDataRepository.settings.get()
if (settings.isRestoreOnBootEnabled) {
if (
settings.isAutoTunnelEnabled && serviceManager.autoTunnelService.value == null
) {
Timber.d("Starting auto-tunnel on boot/update")
serviceManager.startAutoTunnel()
} else {
Timber.d("Restoring previous tunnel state")
tunnelManager.restorePreviousState()
}
} else {
Timber.d("Restore on boot disabled, skipping")
}
if (intent.action == Intent.ACTION_MY_PACKAGE_REPLACED) logReader.deleteAndClearLogs()
}
} }
} }
@@ -16,6 +16,7 @@ import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelMonitor import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelMonitor
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
import com.zaneschepke.wireguardautotunnel.domain.enums.BackendStatus
import com.zaneschepke.wireguardautotunnel.domain.enums.NotificationAction import com.zaneschepke.wireguardautotunnel.domain.enums.NotificationAction
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelStatus.StopReason.Ping import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelStatus.StopReason.Ping
import com.zaneschepke.wireguardautotunnel.domain.events.AutoTunnelEvent import com.zaneschepke.wireguardautotunnel.domain.events.AutoTunnelEvent
@@ -99,10 +100,26 @@ class AutoTunnelService : LifecycleService() {
override fun onDestroy() { override fun onDestroy() {
serviceManager.handleAutoTunnelServiceDestroy() serviceManager.handleAutoTunnelServiceDestroy()
restoreVpnKillSwitch()
ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_REMOVE) ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_REMOVE)
super.onDestroy() super.onDestroy()
} }
private fun restoreVpnKillSwitch() {
with(autoTunnelStateFlow.value) {
if (
settings.isVpnKillSwitchEnabled &&
tunnelManager.getBackendStatus() !is BackendStatus.KillSwitch
) {
eventHandlerJob?.cancel()
val allowedIps =
if (settings.isLanOnKillSwitchEnabled) TunnelConf.LAN_BYPASS_ALLOWED_IPS
else emptyList()
tunnelManager.setBackendStatus(BackendStatus.KillSwitch(allowedIps))
}
}
}
private fun launchWatcherNotification( private fun launchWatcherNotification(
description: String = getString(R.string.monitoring_state_changes) description: String = getString(R.string.monitoring_state_changes)
) { ) {
@@ -383,6 +400,14 @@ class AutoTunnelService : LifecycleService() {
AutoTunnelEvent.DoNothing -> Timber.i("Auto-tunneling: nothing to do") AutoTunnelEvent.DoNothing -> Timber.i("Auto-tunneling: nothing to do")
is AutoTunnelEvent.Bounce -> is AutoTunnelEvent.Bounce ->
handleBounceWithBackoff(event.configsPeerKeyResolvedMap) handleBounceWithBackoff(event.configsPeerKeyResolvedMap)
is AutoTunnelEvent.StartKillSwitch -> {
Timber.d("Starting kill switch")
tunnelManager.setBackendStatus(BackendStatus.KillSwitch(event.allowedIps))
}
AutoTunnelEvent.StopKillSwitch -> {
Timber.d("Stopping kill switch")
tunnelManager.setBackendStatus(BackendStatus.Active)
}
} }
} }
} }
@@ -192,7 +192,7 @@ abstract class BaseTunnel(
val (wg, amnezia) = updatedConfigProxy.buildConfigs() val (wg, amnezia) = updatedConfigProxy.buildConfigs()
currentConf = currentConf =
currentConf.copyWithCallback( currentConf.copyWithCallback(
amQuick = amnezia.toAwgQuickString(true, false), amQuick = amnezia.toAwgQuickString(true),
wgQuick = wg.toWgQuickString(true), wgQuick = wg.toWgQuickString(true),
) )
bouncingTunnelIds.remove(currentConf.id) bouncingTunnelIds.remove(currentConf.id)
@@ -5,8 +5,7 @@ import com.wireguard.android.backend.BackendException
import com.wireguard.android.backend.Tunnel import com.wireguard.android.backend.Tunnel
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
import com.zaneschepke.wireguardautotunnel.di.ApplicationScope import com.zaneschepke.wireguardautotunnel.di.ApplicationScope
import com.zaneschepke.wireguardautotunnel.di.Kernel import com.zaneschepke.wireguardautotunnel.domain.enums.BackendStatus
import com.zaneschepke.wireguardautotunnel.domain.enums.BackendMode
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelStatus import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelStatus
import com.zaneschepke.wireguardautotunnel.domain.events.BackendError import com.zaneschepke.wireguardautotunnel.domain.events.BackendError
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConf import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConf
@@ -24,7 +23,7 @@ constructor(
@ApplicationScope private val applicationScope: CoroutineScope, @ApplicationScope private val applicationScope: CoroutineScope,
serviceManager: ServiceManager, serviceManager: ServiceManager,
appDataRepository: AppDataRepository, appDataRepository: AppDataRepository,
@Kernel private val backend: Backend, private val backend: Backend,
) : BaseTunnel(applicationScope, appDataRepository, serviceManager) { ) : BaseTunnel(applicationScope, appDataRepository, serviceManager) {
override fun getStatistics(tunnelConf: TunnelConf): TunnelStatistics? { override fun getStatistics(tunnelConf: TunnelConf): TunnelStatistics? {
@@ -60,12 +59,12 @@ constructor(
} }
} }
override fun setBackendMode(backendMode: BackendMode) { override fun setBackendStatus(backendStatus: BackendStatus) {
Timber.w("Not yet implemented for kernel") Timber.w("Not yet implemented for kernel")
} }
override fun getBackendMode(): BackendMode { override fun getBackendStatus(): BackendStatus {
return BackendMode.Inactive return BackendStatus.Inactive
} }
override suspend fun runningTunnelNames(): Set<String> { override suspend fun runningTunnelNames(): Set<String> {
@@ -1,13 +1,9 @@
package com.zaneschepke.wireguardautotunnel.core.tunnel package com.zaneschepke.wireguardautotunnel.core.tunnel
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager import com.zaneschepke.wireguardautotunnel.domain.enums.BackendStatus
import com.zaneschepke.wireguardautotunnel.data.model.AppMode
import com.zaneschepke.wireguardautotunnel.di.*
import com.zaneschepke.wireguardautotunnel.domain.enums.BackendMode
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelStatus import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelStatus
import com.zaneschepke.wireguardautotunnel.domain.events.BackendError import com.zaneschepke.wireguardautotunnel.domain.events.BackendError
import com.zaneschepke.wireguardautotunnel.domain.events.BackendMessage import com.zaneschepke.wireguardautotunnel.domain.events.BackendMessage
import com.zaneschepke.wireguardautotunnel.domain.model.AppSettings
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConf import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConf
import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository
import com.zaneschepke.wireguardautotunnel.domain.state.PingState import com.zaneschepke.wireguardautotunnel.domain.state.PingState
@@ -15,106 +11,40 @@ import com.zaneschepke.wireguardautotunnel.domain.state.TunnelState
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelStatistics import com.zaneschepke.wireguardautotunnel.domain.state.TunnelStatistics
import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ConcurrentHashMap
import javax.inject.Inject import javax.inject.Inject
import kotlin.concurrent.atomics.AtomicBoolean
import kotlin.concurrent.atomics.AtomicReference
import kotlin.concurrent.atomics.ExperimentalAtomicApi
import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.* import kotlinx.coroutines.flow.*
import kotlinx.coroutines.plus import kotlinx.coroutines.plus
import org.amnezia.awg.crypto.Key import org.amnezia.awg.crypto.Key
import timber.log.Timber
@OptIn(ExperimentalCoroutinesApi::class) @OptIn(ExperimentalCoroutinesApi::class)
class TunnelManager class TunnelManager
@Inject @Inject
constructor( constructor(
@Kernel private val kernelTunnel: TunnelProvider, private val kernelTunnel: TunnelProvider,
@Userspace private val userspaceTunnel: TunnelProvider, private val userspaceTunnel: TunnelProvider,
@ProxyUserspace private val proxyUserspaceTunnel: TunnelProvider,
private val serviceManager: ServiceManager,
private val appDataRepository: AppDataRepository, private val appDataRepository: AppDataRepository,
@ApplicationScope applicationScope: CoroutineScope, applicationScope: CoroutineScope,
@IoDispatcher ioDispatcher: CoroutineDispatcher, ioDispatcher: CoroutineDispatcher,
) : TunnelProvider { ) : TunnelProvider {
@OptIn(ExperimentalAtomicApi::class) @OptIn(ExperimentalCoroutinesApi::class)
private val tunnelProviderFlow: StateFlow<TunnelProvider> = run { private val tunnelProviderFlow =
val currentBackend = AtomicReference(userspaceTunnel)
val currentSettings = AtomicReference(AppSettings())
val initialEmit = AtomicBoolean(true)
appDataRepository.settings.flow appDataRepository.settings.flow
.filterNotNull() .filterNotNull()
// ignore default state .flatMapLatest { settings ->
.filterNot { it == AppSettings() } val backend = if (settings.isKernelEnabled) kernelTunnel else userspaceTunnel
.distinctUntilChanged { old, new -> MutableStateFlow(backend)
old.appMode == new.appMode &&
old.isLanOnKillSwitchEnabled == new.isLanOnKillSwitchEnabled
} }
.map { settings ->
Timber.d("App mode changes with ${settings.appMode}")
val backend =
when (settings.appMode) {
AppMode.VPN -> userspaceTunnel
AppMode.PROXY -> proxyUserspaceTunnel
AppMode.LOCK_DOWN -> proxyUserspaceTunnel
AppMode.KERNEL -> kernelTunnel
}
settings to backend
}
.onEach { (settings, newBackend) ->
val isInitialEmit = initialEmit.exchange(false)
val oldBackend = currentBackend.exchange(newBackend)
val oldSettings = currentSettings.exchange(settings)
if ((oldSettings.appMode != settings.appMode) && !isInitialEmit) {
oldBackend.stopTunnel()
if (oldSettings.appMode == AppMode.LOCK_DOWN)
proxyUserspaceTunnel.setBackendMode(BackendMode.Inactive)
}
if (settings.appMode == AppMode.LOCK_DOWN) {
// kill switch will always catch all ipv6, just add ipv4 networks for allowsIps
val allowedIps =
if (settings.isLanOnKillSwitchEnabled) TunnelConf.IPV4_PUBLIC_NETWORKS
else emptySet()
proxyUserspaceTunnel.setBackendMode(BackendMode.KillSwitch(allowedIps))
}
// restore state if configured
if (isInitialEmit && settings.isRestoreOnBootEnabled) {
Timber.d("Restoring previous state")
if (
settings.isAutoTunnelEnabled &&
serviceManager.autoTunnelService.value == null
) {
serviceManager.startAutoTunnel()
} else {
val previouslyActiveTuns = appDataRepository.tunnels.getActive()
val tunsToStart =
previouslyActiveTuns.filterNot { tun ->
activeTunnels.value.any { tun.id == it.key.id }
}
tunsToStart.forEach { startTunnel(it) }
}
}
}
.map { (_, backend) -> backend }
.stateIn( .stateIn(
scope = applicationScope.plus(ioDispatcher), scope = applicationScope.plus(ioDispatcher),
started = SharingStarted.Eagerly, started = SharingStarted.Eagerly,
initialValue = userspaceTunnel, initialValue = userspaceTunnel,
) )
}
override val activeTunnels: StateFlow<Map<TunnelConf, TunnelState>> = override val activeTunnels: StateFlow<Map<TunnelConf, TunnelState>> =
tunnelProviderFlow tunnelProviderFlow.value.activeTunnels
.flatMapLatest { it.activeTunnels }
.stateIn(
scope = applicationScope.plus(ioDispatcher),
started = SharingStarted.Eagerly,
initialValue = emptyMap(),
)
@OptIn(ExperimentalCoroutinesApi::class) @OptIn(ExperimentalCoroutinesApi::class)
override val errorEvents: SharedFlow<Pair<TunnelConf, BackendError>> = override val errorEvents: SharedFlow<Pair<TunnelConf, BackendError>> =
@@ -160,12 +90,12 @@ constructor(
tunnelProviderFlow.value.bounceTunnel(tunnelConf, reason) tunnelProviderFlow.value.bounceTunnel(tunnelConf, reason)
} }
override fun setBackendMode(backendMode: BackendMode) { override fun setBackendStatus(backendStatus: BackendStatus) {
tunnelProviderFlow.value.setBackendMode(backendMode) tunnelProviderFlow.value.setBackendStatus(backendStatus)
} }
override fun getBackendMode(): BackendMode { override fun getBackendStatus(): BackendStatus {
return tunnelProviderFlow.value.getBackendMode() return tunnelProviderFlow.value.getBackendStatus()
} }
override suspend fun runningTunnelNames(): Set<String> { override suspend fun runningTunnelNames(): Set<String> {
@@ -187,4 +117,20 @@ constructor(
handshakeSuccessLogs, handshakeSuccessLogs,
) )
} }
suspend fun restorePreviousState() {
val settings = appDataRepository.settings.get()
if (settings.isRestoreOnBootEnabled) {
val previouslyActiveTuns = appDataRepository.tunnels.getActive()
val tunsToStart =
previouslyActiveTuns.filterNot { tun ->
activeTunnels.value.any { tun.id == it.key.id }
}
if (settings.isKernelEnabled) {
return tunsToStart.forEach { startTunnel(it) }
} else {
tunsToStart.firstOrNull()?.let { startTunnel(it) }
}
}
}
} }
@@ -1,6 +1,6 @@
package com.zaneschepke.wireguardautotunnel.core.tunnel package com.zaneschepke.wireguardautotunnel.core.tunnel
import com.zaneschepke.wireguardautotunnel.domain.enums.BackendMode import com.zaneschepke.wireguardautotunnel.domain.enums.BackendStatus
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelStatus import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelStatus
import com.zaneschepke.wireguardautotunnel.domain.events.BackendError import com.zaneschepke.wireguardautotunnel.domain.events.BackendError
import com.zaneschepke.wireguardautotunnel.domain.events.BackendMessage import com.zaneschepke.wireguardautotunnel.domain.events.BackendMessage
@@ -41,9 +41,9 @@ interface TunnelProvider {
reason: TunnelStatus.StopReason = TunnelStatus.StopReason.User, reason: TunnelStatus.StopReason = TunnelStatus.StopReason.User,
) )
fun setBackendMode(backendMode: BackendMode) fun setBackendStatus(backendStatus: BackendStatus)
fun getBackendMode(): BackendMode fun getBackendStatus(): BackendStatus
suspend fun runningTunnelNames(): Set<String> suspend fun runningTunnelNames(): Set<String>
@@ -1,30 +1,22 @@
package com.zaneschepke.wireguardautotunnel.core.tunnel package com.zaneschepke.wireguardautotunnel.core.tunnel
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
import com.zaneschepke.wireguardautotunnel.data.model.DnsProtocol import com.zaneschepke.wireguardautotunnel.domain.enums.BackendStatus
import com.zaneschepke.wireguardautotunnel.domain.enums.BackendMode
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelStatus import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelStatus
import com.zaneschepke.wireguardautotunnel.domain.events.BackendError import com.zaneschepke.wireguardautotunnel.domain.events.BackendError
import com.zaneschepke.wireguardautotunnel.domain.model.AppProxySettings
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConf import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConf
import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository
import com.zaneschepke.wireguardautotunnel.domain.state.AmneziaStatistics import com.zaneschepke.wireguardautotunnel.domain.state.AmneziaStatistics
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelStatistics import com.zaneschepke.wireguardautotunnel.domain.state.TunnelStatistics
import com.zaneschepke.wireguardautotunnel.util.extensions.asAmBackendMode import com.zaneschepke.wireguardautotunnel.util.extensions.asAmBackendStatus
import com.zaneschepke.wireguardautotunnel.util.extensions.asBackendMode import com.zaneschepke.wireguardautotunnel.util.extensions.asBackendStatus
import com.zaneschepke.wireguardautotunnel.util.extensions.toBackendError import com.zaneschepke.wireguardautotunnel.util.extensions.toBackendError
import java.util.*
import javax.inject.Inject import javax.inject.Inject
import kotlin.jvm.optionals.getOrNull
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import org.amnezia.awg.backend.Backend import org.amnezia.awg.backend.Backend
import org.amnezia.awg.backend.BackendException import org.amnezia.awg.backend.BackendException
import org.amnezia.awg.backend.ProxyGoBackend
import org.amnezia.awg.backend.Tunnel import org.amnezia.awg.backend.Tunnel
import org.amnezia.awg.config.Config
import org.amnezia.awg.config.DnsSettings
import org.amnezia.awg.config.proxy.HttpProxy
import org.amnezia.awg.config.proxy.Proxy
import org.amnezia.awg.config.proxy.Socks5Proxy
import timber.log.Timber import timber.log.Timber
class UserspaceTunnel class UserspaceTunnel
@@ -39,55 +31,18 @@ constructor(
override suspend fun startBackend(tunnel: TunnelConf) { override suspend fun startBackend(tunnel: TunnelConf) {
try { try {
updateTunnelStatus(tunnel, TunnelStatus.Starting) updateTunnelStatus(tunnel, TunnelStatus.Starting)
val amConfig = tunnel.toAmConfig()
val proxies: List<Proxy> = var previousKillSwitch: Backend.BackendStatus? = null
when (backend) { // prevent dns failures from bringing tuns up when vpn kill switch active
is ProxyGoBackend -> { if (
val proxySettings = appDataRepository.proxySettings.get() amConfig.peers.any { it.endpoint.getOrNull()?.toString()?.isUrl() == true } &&
Timber.d("Adding proxy configs") backend.backendStatus is Backend.BackendStatus.KillSwitchActive
buildList { ) {
if (proxySettings.socks5ProxyEnabled) { previousKillSwitch = backend.backendStatus
add( setBackendStatus(BackendStatus.Active)
Socks5Proxy( }
proxySettings.socks5ProxyBindAddress backend.setState(tunnel, Tunnel.State.UP, amConfig)
?: AppProxySettings.DEFAULT_SOCKS_BIND_ADDRESS, previousKillSwitch?.let { backend.backendStatus = it }
proxySettings.proxyUsername,
proxySettings.proxyPassword,
)
)
}
if (proxySettings.httpProxyEnabled) {
add(
HttpProxy(
proxySettings.httpProxyBindAddress
?: AppProxySettings.DEFAULT_HTTP_BIND_ADDRESS,
proxySettings.proxyUsername,
proxySettings.proxyPassword,
)
)
}
}
}
else -> emptyList()
}
val setting = appDataRepository.settings.get()
val config = tunnel.toAmConfig()
val updatedConfig =
Config.Builder()
.apply {
setInterface(config.`interface`)
addPeers(config.peers)
addProxies(proxies)
setDnsSettings(
DnsSettings(
setting.dnsProtocol == DnsProtocol.DOH,
Optional.ofNullable(setting.dnsEndpoint),
)
)
}
.build()
backend.setState(tunnel, Tunnel.State.UP, updatedConfig)
} catch (e: BackendException) { } catch (e: BackendException) {
Timber.e(e, "Failed to start up backend for tunnel ${tunnel.name}") Timber.e(e, "Failed to start up backend for tunnel ${tunnel.name}")
throw e.toBackendError() throw e.toBackendError()
@@ -107,17 +62,17 @@ constructor(
} }
} }
override fun setBackendMode(backendMode: BackendMode) { override fun setBackendStatus(backendStatus: BackendStatus) {
Timber.d("Setting backend mode: $backendMode") Timber.d("Setting backend state: $backendStatus")
try { try {
backend.backendMode = backendMode.asAmBackendMode() backend.backendStatus = backendStatus.asAmBackendStatus()
} catch (e: BackendException) { } catch (e: BackendException) {
throw e.toBackendError() throw e.toBackendError()
} }
} }
override fun getBackendMode(): BackendMode { override fun getBackendStatus(): BackendStatus {
return backend.backendMode.asBackendMode() return backend.backendStatus.asBackendStatus()
} }
override suspend fun runningTunnelNames(): Set<String> { override suspend fun runningTunnelNames(): Set<String> {
@@ -4,6 +4,7 @@ import android.content.Context
import androidx.hilt.work.HiltWorker import androidx.hilt.work.HiltWorker
import androidx.work.* import androidx.work.*
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository
import dagger.assisted.Assisted import dagger.assisted.Assisted
@@ -22,6 +23,7 @@ constructor(
private val serviceManager: ServiceManager, private val serviceManager: ServiceManager,
private val appDataRepository: AppDataRepository, private val appDataRepository: AppDataRepository,
@IoDispatcher private val ioDispatcher: CoroutineDispatcher, @IoDispatcher private val ioDispatcher: CoroutineDispatcher,
private val tunnelManager: TunnelManager,
) : CoroutineWorker(context, params) { ) : CoroutineWorker(context, params) {
companion object { companion object {
@@ -51,11 +53,10 @@ constructor(
withContext(ioDispatcher) { withContext(ioDispatcher) {
Timber.i("Service worker started") Timber.i("Service worker started")
with(appDataRepository.settings.get()) { with(appDataRepository.settings.get()) {
Timber.i("Checking to see if auto-tunnel has been killed by system") if (isAutoTunnelEnabled && serviceManager.autoTunnelService.value == null)
if (isAutoTunnelEnabled && serviceManager.autoTunnelService.value == null) { return@with serviceManager.startAutoTunnel()
Timber.i("Service has been killed by system, restoring.") if (tunnelManager.activeTunnels.value.isEmpty())
serviceManager.startAutoTunnel() tunnelManager.restorePreviousState()
}
} }
Result.success() Result.success()
} }
@@ -2,16 +2,14 @@ package com.zaneschepke.wireguardautotunnel.data
import androidx.room.* import androidx.room.*
import androidx.room.migration.AutoMigrationSpec import androidx.room.migration.AutoMigrationSpec
import com.zaneschepke.wireguardautotunnel.data.dao.ProxySettingsDao
import com.zaneschepke.wireguardautotunnel.data.dao.SettingsDao import com.zaneschepke.wireguardautotunnel.data.dao.SettingsDao
import com.zaneschepke.wireguardautotunnel.data.dao.TunnelConfigDao import com.zaneschepke.wireguardautotunnel.data.dao.TunnelConfigDao
import com.zaneschepke.wireguardautotunnel.data.entity.ProxySettings
import com.zaneschepke.wireguardautotunnel.data.entity.Settings import com.zaneschepke.wireguardautotunnel.data.entity.Settings
import com.zaneschepke.wireguardautotunnel.data.entity.TunnelConfig import com.zaneschepke.wireguardautotunnel.data.entity.TunnelConfig
@Database( @Database(
entities = [Settings::class, TunnelConfig::class, ProxySettings::class], entities = [Settings::class, TunnelConfig::class],
version = 20, version = 19,
autoMigrations = autoMigrations =
[ [
AutoMigration(from = 1, to = 2), AutoMigration(from = 1, to = 2),
@@ -32,7 +30,6 @@ import com.zaneschepke.wireguardautotunnel.data.entity.TunnelConfig
AutoMigration(from = 16, to = 17, spec = WifiDetectionMigration::class), AutoMigration(from = 16, to = 17, spec = WifiDetectionMigration::class),
AutoMigration(from = 17, to = 18), AutoMigration(from = 17, to = 18),
AutoMigration(from = 18, to = 19, spec = PingMigration::class), AutoMigration(from = 18, to = 19, spec = PingMigration::class),
AutoMigration(from = 19, to = 20, spec = ProxyMigration::class),
], ],
exportSchema = true, exportSchema = true,
) )
@@ -41,8 +38,6 @@ abstract class AppDatabase : RoomDatabase() {
abstract fun settingDao(): SettingsDao abstract fun settingDao(): SettingsDao
abstract fun tunnelConfigDoa(): TunnelConfigDao abstract fun tunnelConfigDoa(): TunnelConfigDao
abstract fun proxySettingsDoa(): ProxySettingsDao
} }
@DeleteColumn(tableName = "Settings", columnName = "default_tunnel") @DeleteColumn(tableName = "Settings", columnName = "default_tunnel")
@@ -73,11 +68,3 @@ class WifiDetectionMigration : AutoMigrationSpec
), ),
) )
class PingMigration : AutoMigrationSpec class PingMigration : AutoMigrationSpec
@DeleteColumn.Entries(
DeleteColumn(tableName = "Settings", columnName = "is_amnezia_enabled"),
DeleteColumn(tableName = "Settings", columnName = "is_vpn_kill_switch_enabled"),
DeleteColumn(tableName = "Settings", columnName = "is_kernel_kill_switch_enabled"),
DeleteColumn(tableName = "Settings", columnName = "is_kernel_enabled"),
)
class ProxyMigration : AutoMigrationSpec
@@ -2,25 +2,20 @@ package com.zaneschepke.wireguardautotunnel.data
import androidx.room.RoomDatabase import androidx.room.RoomDatabase
import androidx.sqlite.db.SupportSQLiteDatabase import androidx.sqlite.db.SupportSQLiteDatabase
import com.zaneschepke.wireguardautotunnel.data.entity.ProxySettings import timber.log.Timber
import com.zaneschepke.wireguardautotunnel.data.entity.Settings
import javax.inject.Inject
import javax.inject.Provider
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
class DatabaseCallback @Inject constructor(private val databaseProvider: Provider<AppDatabase>) : class DatabaseCallback : RoomDatabase.Callback() {
RoomDatabase.Callback() { override fun onCreate(db: SupportSQLiteDatabase) =
db.run {
override fun onCreate(db: SupportSQLiteDatabase) { beginTransaction()
super.onCreate(db) try {
execSQL(Queries.createDefaultSettings())
// Launch coroutine to insert default entry Timber.i("Bootstrapping settings data")
CoroutineScope(Dispatchers.IO).launch { setTransactionSuccessful()
val db = databaseProvider.get() } catch (e: Exception) {
db.settingDao().save(Settings()) Timber.e(e)
db.proxySettingsDoa().save(ProxySettings()) } finally {
endTransaction()
}
} }
}
} }
@@ -1,9 +1,7 @@
package com.zaneschepke.wireguardautotunnel.data package com.zaneschepke.wireguardautotunnel.data
import androidx.room.TypeConverter import androidx.room.TypeConverter
import com.zaneschepke.wireguardautotunnel.data.model.AppMode import com.zaneschepke.wireguardautotunnel.data.entity.Settings
import com.zaneschepke.wireguardautotunnel.data.model.DnsProtocol
import com.zaneschepke.wireguardautotunnel.data.model.WifiDetectionMethod
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
class DatabaseConverters { class DatabaseConverters {
@@ -24,16 +22,9 @@ class DatabaseConverters {
} }
} }
@TypeConverter fun fromStatus(status: WifiDetectionMethod): Int = status.value @TypeConverter fun fromStatus(status: Settings.WifiDetectionMethod): Int = status.value
@TypeConverter @TypeConverter
fun toStatus(value: Int): WifiDetectionMethod = WifiDetectionMethod.fromValue(value) fun toStatus(value: Int): Settings.WifiDetectionMethod =
Settings.WifiDetectionMethod.fromValue(value)
@TypeConverter fun toMode(value: Int): AppMode = AppMode.fromValue(value)
@TypeConverter fun fromMode(mode: AppMode): Int = mode.value
@TypeConverter fun toDnsProtocol(value: Int): DnsProtocol = DnsProtocol.fromValue(value)
@TypeConverter fun fromDnsProtocol(mode: DnsProtocol): Int = mode.value
} }
@@ -0,0 +1,37 @@
package com.zaneschepke.wireguardautotunnel.data
object Queries {
fun createDefaultSettings(): String {
return """
INSERT INTO Settings (is_tunnel_enabled,
is_tunnel_on_mobile_data_enabled,
trusted_network_ssids,
is_always_on_vpn_enabled,
is_tunnel_on_ethernet_enabled,
is_shortcuts_enabled,
is_tunnel_on_wifi_enabled,
is_kernel_enabled,
is_restore_on_boot_enabled,
is_multi_tunnel_enabled)
VALUES
('false',
'false',
'',
'false',
'false',
'false',
'false',
'false',
'false',
'false')
"""
.trimIndent()
}
fun createTunnelConfig(): String {
return """
INSERT INTO TunnelConfig (name, wg_quick) VALUES ('test', 'test')
"""
.trimIndent()
}
}
@@ -1,25 +0,0 @@
package com.zaneschepke.wireguardautotunnel.data.dao
import androidx.room.*
import com.zaneschepke.wireguardautotunnel.data.entity.ProxySettings
import kotlinx.coroutines.flow.Flow
@Dao
interface ProxySettingsDao {
@Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun save(t: ProxySettings)
@Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun saveAll(t: List<ProxySettings>)
@Query("SELECT * FROM proxy_settings WHERE id=:id")
suspend fun getById(id: Long): ProxySettings?
@Query("SELECT * FROM proxy_settings") suspend fun getAll(): List<ProxySettings>
@Query("SELECT * FROM proxy_settings LIMIT 1") fun getSettingsFlow(): Flow<ProxySettings>
@Query("SELECT * FROM proxy_settings") fun getAllFlow(): Flow<List<ProxySettings>>
@Delete suspend fun delete(t: ProxySettings)
@Query("SELECT COUNT('id') FROM proxy_settings") suspend fun count(): Long
}
@@ -16,7 +16,7 @@ interface SettingsDao {
@Query("SELECT * FROM settings LIMIT 1") fun getSettingsFlow(): Flow<Settings> @Query("SELECT * FROM settings LIMIT 1") fun getSettingsFlow(): Flow<Settings>
@Query("SELECT * FROM settings") fun getAllFlow(): Flow<List<Settings>> @Query("SELECT * FROM settings") fun getAllFlow(): Flow<MutableList<Settings>>
@Delete suspend fun delete(t: Settings) @Delete suspend fun delete(t: Settings)
@@ -1,18 +0,0 @@
package com.zaneschepke.wireguardautotunnel.data.entity
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
@Entity(tableName = "proxy_settings")
data class ProxySettings(
@PrimaryKey(autoGenerate = true) val id: Long = 0,
@ColumnInfo(name = "socks5_proxy_enabled", defaultValue = "false")
val socks5ProxyEnabled: Boolean = false,
@ColumnInfo(name = "socks5_proxy_bind_address") val socks5ProxyBindAddress: String? = null,
@ColumnInfo(name = "http_proxy_enable", defaultValue = "false")
val httpProxyEnabled: Boolean = false,
@ColumnInfo(name = "http_proxy_bind_address") val httpProxyBindAddress: String? = null,
@ColumnInfo(name = "proxy_username") val proxyUsername: String? = null,
@ColumnInfo(name = "proxy_password") val proxyPassword: String? = null,
)
@@ -3,9 +3,6 @@ package com.zaneschepke.wireguardautotunnel.data.entity
import androidx.room.ColumnInfo import androidx.room.ColumnInfo
import androidx.room.Entity import androidx.room.Entity
import androidx.room.PrimaryKey import androidx.room.PrimaryKey
import com.zaneschepke.wireguardautotunnel.data.model.AppMode
import com.zaneschepke.wireguardautotunnel.data.model.DnsProtocol
import com.zaneschepke.wireguardautotunnel.data.model.WifiDetectionMethod
@Entity @Entity
data class Settings( data class Settings(
@@ -21,16 +18,24 @@ data class Settings(
val isShortcutsEnabled: Boolean = false, val isShortcutsEnabled: Boolean = false,
@ColumnInfo(name = "is_tunnel_on_wifi_enabled", defaultValue = "false") @ColumnInfo(name = "is_tunnel_on_wifi_enabled", defaultValue = "false")
val isTunnelOnWifiEnabled: Boolean = false, val isTunnelOnWifiEnabled: Boolean = false,
@ColumnInfo(name = "is_kernel_enabled", defaultValue = "false")
val isKernelEnabled: Boolean = false,
@ColumnInfo(name = "is_restore_on_boot_enabled", defaultValue = "false") @ColumnInfo(name = "is_restore_on_boot_enabled", defaultValue = "false")
val isRestoreOnBootEnabled: Boolean = false, val isRestoreOnBootEnabled: Boolean = false,
@ColumnInfo(name = "is_multi_tunnel_enabled", defaultValue = "false") @ColumnInfo(name = "is_multi_tunnel_enabled", defaultValue = "false")
val isMultiTunnelEnabled: Boolean = false, val isMultiTunnelEnabled: Boolean = false,
@ColumnInfo(name = "is_ping_enabled", defaultValue = "false") @ColumnInfo(name = "is_ping_enabled", defaultValue = "false")
val isPingEnabled: Boolean = false, val isPingEnabled: Boolean = false,
@ColumnInfo(name = "is_amnezia_enabled", defaultValue = "false")
val isAmneziaEnabled: Boolean = false,
@ColumnInfo(name = "is_wildcards_enabled", defaultValue = "false") @ColumnInfo(name = "is_wildcards_enabled", defaultValue = "false")
val isWildcardsEnabled: Boolean = false, val isWildcardsEnabled: Boolean = false,
@ColumnInfo(name = "is_stop_on_no_internet_enabled", defaultValue = "false") @ColumnInfo(name = "is_stop_on_no_internet_enabled", defaultValue = "false")
val isStopOnNoInternetEnabled: Boolean = false, val isStopOnNoInternetEnabled: Boolean = false,
@ColumnInfo(name = "is_vpn_kill_switch_enabled", defaultValue = "false")
val isVpnKillSwitchEnabled: Boolean = false,
@ColumnInfo(name = "is_kernel_kill_switch_enabled", defaultValue = "false")
val isKernelKillSwitchEnabled: Boolean = false,
@ColumnInfo(name = "is_lan_on_kill_switch_enabled", defaultValue = "false") @ColumnInfo(name = "is_lan_on_kill_switch_enabled", defaultValue = "false")
val isLanOnKillSwitchEnabled: Boolean = false, val isLanOnKillSwitchEnabled: Boolean = false,
@ColumnInfo(name = "debounce_delay_seconds", defaultValue = "3") @ColumnInfo(name = "debounce_delay_seconds", defaultValue = "3")
@@ -47,8 +52,16 @@ data class Settings(
val tunnelPingIntervalSeconds: Int = 30, val tunnelPingIntervalSeconds: Int = 30,
@ColumnInfo(name = "tunnel_ping_attempts", defaultValue = "3") val tunnelPingAttempts: Int = 3, @ColumnInfo(name = "tunnel_ping_attempts", defaultValue = "3") val tunnelPingAttempts: Int = 3,
@ColumnInfo(name = "tunnel_ping_timeout_sec") val tunnelPingTimeoutSeconds: Int? = null, @ColumnInfo(name = "tunnel_ping_timeout_sec") val tunnelPingTimeoutSeconds: Int? = null,
@ColumnInfo(name = "app_mode", defaultValue = "0") val appMode: AppMode = AppMode.fromValue(0), ) {
@ColumnInfo(name = "dns_protocol", defaultValue = "0") enum class WifiDetectionMethod(val value: Int) {
val dnsProtocol: DnsProtocol = DnsProtocol.fromValue(0), DEFAULT(0),
@ColumnInfo(name = "dns_endpoint") val dnsEndpoint: String? = null, LEGACY(1),
) ROOT(2),
SHIZUKU(3);
companion object {
fun fromValue(value: Int): WifiDetectionMethod =
entries.find { it.value == value } ?: DEFAULT
}
}
}
@@ -1,32 +0,0 @@
package com.zaneschepke.wireguardautotunnel.data.mapper
import com.zaneschepke.wireguardautotunnel.data.entity.ProxySettings
import com.zaneschepke.wireguardautotunnel.domain.model.AppProxySettings
object ProxySettingsMapper {
fun to(proxySettings: ProxySettings): AppProxySettings =
with(proxySettings) {
AppProxySettings(
id,
socks5ProxyEnabled,
socks5ProxyBindAddress,
httpProxyEnabled,
httpProxyBindAddress,
proxyUsername,
proxyPassword,
)
}
fun to(proxySettings: AppProxySettings): ProxySettings =
with(proxySettings) {
ProxySettings(
id,
socks5ProxyEnabled,
socks5ProxyBindAddress,
httpProxyEnabled,
httpProxyBindAddress,
proxyUsername,
proxyPassword,
)
}
}
@@ -2,78 +2,70 @@ package com.zaneschepke.wireguardautotunnel.data.mapper
import com.zaneschepke.networkmonitor.AndroidNetworkMonitor import com.zaneschepke.networkmonitor.AndroidNetworkMonitor
import com.zaneschepke.wireguardautotunnel.data.entity.Settings import com.zaneschepke.wireguardautotunnel.data.entity.Settings
import com.zaneschepke.wireguardautotunnel.data.model.DnsProtocol
import com.zaneschepke.wireguardautotunnel.data.model.DnsSettings
import com.zaneschepke.wireguardautotunnel.data.model.WifiDetectionMethod
import com.zaneschepke.wireguardautotunnel.domain.model.AppSettings import com.zaneschepke.wireguardautotunnel.domain.model.AppSettings
fun Settings.toAppSettings(): AppSettings { object SettingsMapper {
return AppSettings( fun toAppSettings(settings: Settings): AppSettings {
id = id, return AppSettings(
isAutoTunnelEnabled = isAutoTunnelEnabled, id = settings.id,
isTunnelOnMobileDataEnabled = isTunnelOnMobileDataEnabled, isAutoTunnelEnabled = settings.isAutoTunnelEnabled,
trustedNetworkSSIDs = trustedNetworkSSIDs, isTunnelOnMobileDataEnabled = settings.isTunnelOnMobileDataEnabled,
isAlwaysOnVpnEnabled = isAlwaysOnVpnEnabled, trustedNetworkSSIDs = settings.trustedNetworkSSIDs,
isTunnelOnEthernetEnabled = isTunnelOnEthernetEnabled, isAlwaysOnVpnEnabled = settings.isAlwaysOnVpnEnabled,
isShortcutsEnabled = isShortcutsEnabled, isTunnelOnEthernetEnabled = settings.isTunnelOnEthernetEnabled,
isTunnelOnWifiEnabled = isTunnelOnWifiEnabled, isShortcutsEnabled = settings.isShortcutsEnabled,
isRestoreOnBootEnabled = isRestoreOnBootEnabled, isTunnelOnWifiEnabled = settings.isTunnelOnWifiEnabled,
isMultiTunnelEnabled = isMultiTunnelEnabled, isKernelEnabled = settings.isKernelEnabled,
isPingEnabled = isPingEnabled, isRestoreOnBootEnabled = settings.isRestoreOnBootEnabled,
isWildcardsEnabled = isWildcardsEnabled, isMultiTunnelEnabled = settings.isMultiTunnelEnabled,
isStopOnNoInternetEnabled = isStopOnNoInternetEnabled, isPingEnabled = settings.isPingEnabled,
isLanOnKillSwitchEnabled = isLanOnKillSwitchEnabled, isAmneziaEnabled = settings.isAmneziaEnabled,
debounceDelaySeconds = debounceDelaySeconds, isWildcardsEnabled = settings.isWildcardsEnabled,
isDisableKillSwitchOnTrustedEnabled = isDisableKillSwitchOnTrustedEnabled, isStopOnNoInternetEnabled = settings.isStopOnNoInternetEnabled,
isTunnelOnUnsecureEnabled = isTunnelOnUnsecureEnabled, isVpnKillSwitchEnabled = settings.isVpnKillSwitchEnabled,
wifiDetectionMethod = isKernelKillSwitchEnabled = settings.isKernelKillSwitchEnabled,
AndroidNetworkMonitor.WifiDetectionMethod.fromValue(wifiDetectionMethod.value), isLanOnKillSwitchEnabled = settings.isLanOnKillSwitchEnabled,
tunnelPingIntervalSeconds = tunnelPingIntervalSeconds, debounceDelaySeconds = settings.debounceDelaySeconds,
tunnelPingAttempts = tunnelPingAttempts, isDisableKillSwitchOnTrustedEnabled = settings.isDisableKillSwitchOnTrustedEnabled,
tunnelPingTimeoutSeconds = tunnelPingTimeoutSeconds, isTunnelOnUnsecureEnabled = settings.isTunnelOnUnsecureEnabled,
appMode = appMode, wifiDetectionMethod =
dnsProtocol = dnsProtocol, AndroidNetworkMonitor.WifiDetectionMethod.fromValue(
dnsEndpoint = dnsEndpoint, settings.wifiDetectionMethod.value
) ),
} tunnelPingIntervalSeconds = settings.tunnelPingIntervalSeconds,
tunnelPingAttempts = settings.tunnelPingAttempts,
tunnelPingTimeoutSeconds = settings.tunnelPingTimeoutSeconds,
)
}
fun AppSettings.toSettings(): Settings { fun toSettings(appSettings: AppSettings): Settings {
return Settings( return Settings(
id = id, id = appSettings.id,
isAutoTunnelEnabled = isAutoTunnelEnabled, isAutoTunnelEnabled = appSettings.isAutoTunnelEnabled,
isTunnelOnMobileDataEnabled = isTunnelOnMobileDataEnabled, isTunnelOnMobileDataEnabled = appSettings.isTunnelOnMobileDataEnabled,
trustedNetworkSSIDs = trustedNetworkSSIDs, trustedNetworkSSIDs = appSettings.trustedNetworkSSIDs,
isAlwaysOnVpnEnabled = isAlwaysOnVpnEnabled, isAlwaysOnVpnEnabled = appSettings.isAlwaysOnVpnEnabled,
isTunnelOnEthernetEnabled = isTunnelOnEthernetEnabled, isTunnelOnEthernetEnabled = appSettings.isTunnelOnEthernetEnabled,
isShortcutsEnabled = isShortcutsEnabled, isShortcutsEnabled = appSettings.isShortcutsEnabled,
isTunnelOnWifiEnabled = isTunnelOnWifiEnabled, isTunnelOnWifiEnabled = appSettings.isTunnelOnWifiEnabled,
isRestoreOnBootEnabled = isRestoreOnBootEnabled, isKernelEnabled = appSettings.isKernelEnabled,
isMultiTunnelEnabled = isMultiTunnelEnabled, isRestoreOnBootEnabled = appSettings.isRestoreOnBootEnabled,
isPingEnabled = isPingEnabled, isMultiTunnelEnabled = appSettings.isMultiTunnelEnabled,
isWildcardsEnabled = isWildcardsEnabled, isPingEnabled = appSettings.isPingEnabled,
isStopOnNoInternetEnabled = isStopOnNoInternetEnabled, isAmneziaEnabled = appSettings.isAmneziaEnabled,
isLanOnKillSwitchEnabled = isLanOnKillSwitchEnabled, isWildcardsEnabled = appSettings.isWildcardsEnabled,
debounceDelaySeconds = debounceDelaySeconds, isStopOnNoInternetEnabled = appSettings.isStopOnNoInternetEnabled,
isDisableKillSwitchOnTrustedEnabled = isDisableKillSwitchOnTrustedEnabled, isVpnKillSwitchEnabled = appSettings.isVpnKillSwitchEnabled,
isTunnelOnUnsecureEnabled = isTunnelOnUnsecureEnabled, isKernelKillSwitchEnabled = appSettings.isKernelKillSwitchEnabled,
wifiDetectionMethod = WifiDetectionMethod.fromValue(wifiDetectionMethod.value), isLanOnKillSwitchEnabled = appSettings.isLanOnKillSwitchEnabled,
tunnelPingIntervalSeconds = tunnelPingIntervalSeconds, debounceDelaySeconds = appSettings.debounceDelaySeconds,
tunnelPingAttempts = tunnelPingAttempts, isDisableKillSwitchOnTrustedEnabled = appSettings.isDisableKillSwitchOnTrustedEnabled,
tunnelPingTimeoutSeconds = tunnelPingTimeoutSeconds, isTunnelOnUnsecureEnabled = appSettings.isTunnelOnUnsecureEnabled,
appMode = appMode, wifiDetectionMethod =
dnsProtocol = dnsProtocol, Settings.WifiDetectionMethod.fromValue(appSettings.wifiDetectionMethod.value),
dnsEndpoint = dnsEndpoint, tunnelPingIntervalSeconds = appSettings.tunnelPingIntervalSeconds,
) tunnelPingAttempts = appSettings.tunnelPingAttempts,
} tunnelPingTimeoutSeconds = appSettings.tunnelPingTimeoutSeconds,
)
fun AppSettings.toDomain(): DnsSettings { }
return DnsSettings(
protocol =
DnsProtocol.entries.toTypedArray().getOrElse(dnsProtocol.value) { DnsProtocol.SYSTEM },
endpoint = dnsEndpoint,
)
}
fun DnsSettings.toAppSettings(existing: AppSettings): AppSettings {
return existing.copy(dnsProtocol = protocol, dnsEndpoint = endpoint)
} }
@@ -1,12 +0,0 @@
package com.zaneschepke.wireguardautotunnel.data.model
enum class AppMode(val value: Int) {
VPN(0),
PROXY(1),
LOCK_DOWN(2),
KERNEL(3);
companion object {
fun fromValue(value: Int): AppMode = entries.find { it.value == value } ?: VPN
}
}
@@ -1,45 +0,0 @@
package com.zaneschepke.wireguardautotunnel.data.model
import android.content.Context
import com.zaneschepke.wireguardautotunnel.R
enum class DnsProtocol(val value: Int) {
SYSTEM(0),
DOH(1);
fun asString(context: Context): String {
return when (this) {
SYSTEM -> context.getString(R.string.system)
DOH -> context.getString(R.string.doh)
}
}
companion object {
fun fromValue(value: Int): DnsProtocol =
DnsProtocol.entries.find { it.value == value } ?: SYSTEM
}
}
data class DnsSettings(
val protocol: DnsProtocol = DnsProtocol.SYSTEM,
val endpoint: String? = null,
)
enum class DnsProvider(private val systemAddress: String, private val dohAddress: String) {
CLOUDFLARE("1.1.1.1", "https://1.1.1.1/dns-query"),
ADGUARD("94.140.14.14", "https://94.140.14.14/dns-query");
fun asAddress(protocol: DnsProtocol): String {
return when (protocol) {
DnsProtocol.SYSTEM -> systemAddress
DnsProtocol.DOH -> dohAddress
}
}
companion object {
fun fromAddress(address: String): DnsProvider {
return entries.find { it.systemAddress == address || it.dohAddress == address }
?: CLOUDFLARE
}
}
}
@@ -1,13 +0,0 @@
package com.zaneschepke.wireguardautotunnel.data.model
enum class WifiDetectionMethod(val value: Int) {
DEFAULT(0),
LEGACY(1),
ROOT(2),
SHIZUKU(3);
companion object {
fun fromValue(value: Int): WifiDetectionMethod =
entries.find { it.value == value } ?: DEFAULT
}
}
@@ -1,7 +1,10 @@
package com.zaneschepke.wireguardautotunnel.data.repository package com.zaneschepke.wireguardautotunnel.data.repository
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConf import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConf
import com.zaneschepke.wireguardautotunnel.domain.repository.* import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository
import com.zaneschepke.wireguardautotunnel.domain.repository.AppSettingRepository
import com.zaneschepke.wireguardautotunnel.domain.repository.AppStateRepository
import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository
import javax.inject.Inject import javax.inject.Inject
class AppDataRoomRepository class AppDataRoomRepository
@@ -10,7 +13,6 @@ constructor(
override val settings: AppSettingRepository, override val settings: AppSettingRepository,
override val tunnels: TunnelRepository, override val tunnels: TunnelRepository,
override val appState: AppStateRepository, override val appState: AppStateRepository,
override val proxySettings: ProxySettingsRepository,
) : AppDataRepository { ) : AppDataRepository {
override suspend fun getPrimaryOrFirstTunnel(): TunnelConf? { override suspend fun getPrimaryOrFirstTunnel(): TunnelConf? {
@@ -41,7 +41,7 @@ class GitHubUpdateRepository(
val standaloneApkAsset = val standaloneApkAsset =
release.assets.find { asset -> release.assets.find { asset ->
asset.name.startsWith("wgtunnel-${Constants.STANDALONE_FLAVOR}-v") && asset.name.startsWith("wgtunnel-${Constants.STANDALONE_FLAVOR}-v") &&
asset.name.endsWith(".apk") asset.name.endsWith(".apk")
} }
val newVersion = val newVersion =
standaloneApkAsset standaloneApkAsset
@@ -53,10 +53,9 @@ class GitHubUpdateRepository(
if (isNightly && newVersion != currentVersion) if (isNightly && newVersion != currentVersion)
return@map GitHubReleaseMapper.toAppUpdate(release, newVersion) return@map GitHubReleaseMapper.toAppUpdate(release, newVersion)
if (NumberUtils.compareVersions(newVersion, currentVersion) > 0) { if (NumberUtils.compareVersions(newVersion, currentVersion) > 0) {
GitHubReleaseMapper.toAppUpdate( GitHubReleaseMapper.toAppUpdate(release.copy(
release.copy(assets = listOf(standaloneApkAsset)), assets = listOf(standaloneApkAsset)
newVersion, ), newVersion)
)
} else { } else {
null null
} }
@@ -104,4 +103,4 @@ class GitHubUpdateRepository(
Result.failure(e) Result.failure(e)
} }
} }
} }
@@ -1,30 +0,0 @@
package com.zaneschepke.wireguardautotunnel.data.repository
import com.zaneschepke.wireguardautotunnel.data.dao.ProxySettingsDao
import com.zaneschepke.wireguardautotunnel.data.entity.ProxySettings
import com.zaneschepke.wireguardautotunnel.data.mapper.ProxySettingsMapper
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
import com.zaneschepke.wireguardautotunnel.domain.model.AppProxySettings
import com.zaneschepke.wireguardautotunnel.domain.repository.ProxySettingsRepository
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.withContext
class RoomProxySettingsRepository(
private val proxySettingsDao: ProxySettingsDao,
@IoDispatcher private val ioDispatcher: CoroutineDispatcher,
) : ProxySettingsRepository {
override suspend fun save(proxySettings: AppProxySettings) {
withContext(ioDispatcher) { proxySettingsDao.save(ProxySettingsMapper.to(proxySettings)) }
}
override val flow =
proxySettingsDao.getSettingsFlow().flowOn(ioDispatcher).map(ProxySettingsMapper::to)
override suspend fun get(): AppProxySettings {
return withContext(ioDispatcher) {
ProxySettingsMapper.to(proxySettingsDao.getAll().firstOrNull() ?: ProxySettings())
}
}
}
@@ -2,8 +2,7 @@ package com.zaneschepke.wireguardautotunnel.data.repository
import com.zaneschepke.wireguardautotunnel.data.dao.SettingsDao import com.zaneschepke.wireguardautotunnel.data.dao.SettingsDao
import com.zaneschepke.wireguardautotunnel.data.entity.Settings import com.zaneschepke.wireguardautotunnel.data.entity.Settings
import com.zaneschepke.wireguardautotunnel.data.mapper.toAppSettings import com.zaneschepke.wireguardautotunnel.data.mapper.SettingsMapper
import com.zaneschepke.wireguardautotunnel.data.mapper.toSettings
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
import com.zaneschepke.wireguardautotunnel.domain.model.AppSettings import com.zaneschepke.wireguardautotunnel.domain.model.AppSettings
import com.zaneschepke.wireguardautotunnel.domain.repository.AppSettingRepository import com.zaneschepke.wireguardautotunnel.domain.repository.AppSettingRepository
@@ -18,15 +17,15 @@ class RoomSettingsRepository(
) : AppSettingRepository { ) : AppSettingRepository {
override suspend fun save(appSettings: AppSettings) { override suspend fun save(appSettings: AppSettings) {
withContext(ioDispatcher) { settingsDoa.save(appSettings.toSettings()) } withContext(ioDispatcher) { settingsDoa.save(SettingsMapper.toSettings(appSettings)) }
} }
override val flow = override val flow =
settingsDoa.getSettingsFlow().flowOn(ioDispatcher).map { it.toAppSettings() } settingsDoa.getSettingsFlow().flowOn(ioDispatcher).map(SettingsMapper::toAppSettings)
override suspend fun get(): AppSettings { override suspend fun get(): AppSettings {
return withContext(ioDispatcher) { return withContext(ioDispatcher) {
(settingsDoa.getAll().firstOrNull() ?: Settings()).toAppSettings() SettingsMapper.toAppSettings(settingsDoa.getAll().firstOrNull() ?: Settings())
} }
} }
} }
@@ -9,5 +9,3 @@ import javax.inject.Qualifier
@Qualifier @Retention(AnnotationRetention.BINARY) annotation class Kernel @Qualifier @Retention(AnnotationRetention.BINARY) annotation class Kernel
@Qualifier @Retention(AnnotationRetention.BINARY) annotation class Userspace @Qualifier @Retention(AnnotationRetention.BINARY) annotation class Userspace
@Qualifier @Retention(AnnotationRetention.BINARY) annotation class ProxyUserspace
@@ -6,7 +6,6 @@ import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.data.AppDatabase import com.zaneschepke.wireguardautotunnel.data.AppDatabase
import com.zaneschepke.wireguardautotunnel.data.DataStoreManager import com.zaneschepke.wireguardautotunnel.data.DataStoreManager
import com.zaneschepke.wireguardautotunnel.data.DatabaseCallback import com.zaneschepke.wireguardautotunnel.data.DatabaseCallback
import com.zaneschepke.wireguardautotunnel.data.dao.ProxySettingsDao
import com.zaneschepke.wireguardautotunnel.data.dao.SettingsDao import com.zaneschepke.wireguardautotunnel.data.dao.SettingsDao
import com.zaneschepke.wireguardautotunnel.data.dao.TunnelConfigDao import com.zaneschepke.wireguardautotunnel.data.dao.TunnelConfigDao
import com.zaneschepke.wireguardautotunnel.data.network.GitHubApi import com.zaneschepke.wireguardautotunnel.data.network.GitHubApi
@@ -28,17 +27,14 @@ import kotlinx.coroutines.CoroutineDispatcher
class RepositoryModule { class RepositoryModule {
@Provides @Provides
@Singleton @Singleton
fun provideDatabase( fun provideDatabase(@ApplicationContext context: Context): AppDatabase {
@ApplicationContext context: Context,
callback: DatabaseCallback,
): AppDatabase {
return Room.databaseBuilder( return Room.databaseBuilder(
context, context,
AppDatabase::class.java, AppDatabase::class.java,
context.getString(R.string.db_name), context.getString(R.string.db_name),
) )
.fallbackToDestructiveMigration(true) .fallbackToDestructiveMigration(true)
.addCallback(callback) .addCallback(DatabaseCallback())
.build() .build()
} }
@@ -48,12 +44,6 @@ class RepositoryModule {
return appDatabase.settingDao() return appDatabase.settingDao()
} }
@Singleton
@Provides
fun provideProxyDoa(appDatabase: AppDatabase): ProxySettingsDao {
return appDatabase.proxySettingsDoa()
}
@Singleton @Singleton
@Provides @Provides
fun provideTunnelConfigDoa(appDatabase: AppDatabase): TunnelConfigDao { fun provideTunnelConfigDoa(appDatabase: AppDatabase): TunnelConfigDao {
@@ -78,15 +68,6 @@ class RepositoryModule {
return RoomSettingsRepository(settingsDao, ioDispatcher) return RoomSettingsRepository(settingsDao, ioDispatcher)
} }
@Singleton
@Provides
fun provideProxySettingsRepository(
proxySettingsDao: ProxySettingsDao,
@IoDispatcher ioDispatcher: CoroutineDispatcher,
): ProxySettingsRepository {
return RoomProxySettingsRepository(proxySettingsDao, ioDispatcher)
}
@Singleton @Singleton
@Provides @Provides
fun providePreferencesDataStore( fun providePreferencesDataStore(
@@ -108,14 +89,8 @@ class RepositoryModule {
settingsRepository: AppSettingRepository, settingsRepository: AppSettingRepository,
tunnelRepository: TunnelRepository, tunnelRepository: TunnelRepository,
appStateRepository: AppStateRepository, appStateRepository: AppStateRepository,
proxySettingsRepository: ProxySettingsRepository,
): AppDataRepository { ): AppDataRepository {
return AppDataRoomRepository( return AppDataRoomRepository(settingsRepository, tunnelRepository, appStateRepository)
settingsRepository,
tunnelRepository,
appStateRepository,
proxySettingsRepository,
)
} }
@Provides @Provides
@@ -7,6 +7,7 @@ import com.wireguard.android.util.ToolsInstaller
import com.zaneschepke.logcatter.LogReader import com.zaneschepke.logcatter.LogReader
import com.zaneschepke.networkmonitor.AndroidNetworkMonitor import com.zaneschepke.networkmonitor.AndroidNetworkMonitor
import com.zaneschepke.networkmonitor.NetworkMonitor import com.zaneschepke.networkmonitor.NetworkMonitor
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationManager
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
import com.zaneschepke.wireguardautotunnel.core.tunnel.* import com.zaneschepke.wireguardautotunnel.core.tunnel.*
import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository
@@ -25,7 +26,6 @@ import kotlinx.coroutines.flow.distinctUntilChangedBy
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import org.amnezia.awg.backend.Backend import org.amnezia.awg.backend.Backend
import org.amnezia.awg.backend.GoBackend import org.amnezia.awg.backend.GoBackend
import org.amnezia.awg.backend.ProxyGoBackend
import org.amnezia.awg.backend.RootTunnelActionHandler import org.amnezia.awg.backend.RootTunnelActionHandler
@Module @Module
@@ -48,21 +48,10 @@ class TunnelModule {
@Provides @Provides
@Singleton @Singleton
@Userspace
fun provideAmneziaBackend(@ApplicationContext context: Context): Backend { fun provideAmneziaBackend(@ApplicationContext context: Context): Backend {
return GoBackend(context, RootTunnelActionHandler(org.amnezia.awg.util.RootShell(context))) return GoBackend(context, RootTunnelActionHandler(org.amnezia.awg.util.RootShell(context)))
} }
@Provides
@Singleton
@ProxyUserspace
fun provideAmneziaProxyBackend(@ApplicationContext context: Context): Backend {
return ProxyGoBackend(
context,
RootTunnelActionHandler(org.amnezia.awg.util.RootShell(context)),
)
}
@Provides @Provides
@Singleton @Singleton
fun provideKernelBackend( fun provideKernelBackend(
@@ -97,19 +86,7 @@ class TunnelModule {
@ApplicationScope applicationScope: CoroutineScope, @ApplicationScope applicationScope: CoroutineScope,
serviceManager: ServiceManager, serviceManager: ServiceManager,
appDataRepository: AppDataRepository, appDataRepository: AppDataRepository,
@Userspace backend: Backend, backend: Backend,
): TunnelProvider {
return UserspaceTunnel(applicationScope, serviceManager, appDataRepository, backend)
}
@Provides
@Singleton
@ProxyUserspace
fun provideProxyUserspaceProvider(
@ApplicationScope applicationScope: CoroutineScope,
serviceManager: ServiceManager,
appDataRepository: AppDataRepository,
@ProxyUserspace backend: Backend,
): TunnelProvider { ): TunnelProvider {
return UserspaceTunnel(applicationScope, serviceManager, appDataRepository, backend) return UserspaceTunnel(applicationScope, serviceManager, appDataRepository, backend)
} }
@@ -119,17 +96,14 @@ class TunnelModule {
fun provideTunnelManager( fun provideTunnelManager(
@Kernel kernelTunnel: TunnelProvider, @Kernel kernelTunnel: TunnelProvider,
@Userspace userspaceTunnel: TunnelProvider, @Userspace userspaceTunnel: TunnelProvider,
@ProxyUserspace proxyTunnel: TunnelProvider,
serviceManager: ServiceManager,
appDataRepository: AppDataRepository, appDataRepository: AppDataRepository,
@IoDispatcher ioDispatcher: CoroutineDispatcher, @IoDispatcher ioDispatcher: CoroutineDispatcher,
@ApplicationScope applicationScope: CoroutineScope, @ApplicationScope applicationScope: CoroutineScope,
notificationManager: NotificationManager,
): TunnelManager { ): TunnelManager {
return TunnelManager( return TunnelManager(
kernelTunnel, kernelTunnel,
userspaceTunnel, userspaceTunnel,
proxyTunnel,
serviceManager,
appDataRepository, appDataRepository,
applicationScope, applicationScope,
ioDispatcher, ioDispatcher,
@@ -1,7 +0,0 @@
package com.zaneschepke.wireguardautotunnel.domain.enums
sealed class BackendMode {
data object Inactive : BackendMode()
data class KillSwitch(val allowedIps: Set<String>) : BackendMode()
}
@@ -0,0 +1,9 @@
package com.zaneschepke.wireguardautotunnel.domain.enums
sealed class BackendStatus {
data object Inactive : BackendStatus()
data object Active : BackendStatus()
data class KillSwitch(val allowedIps: List<String>) : BackendStatus()
}
@@ -11,4 +11,8 @@ sealed class AutoTunnelEvent {
data object Stop : AutoTunnelEvent() data object Stop : AutoTunnelEvent()
data object DoNothing : AutoTunnelEvent() data object DoNothing : AutoTunnelEvent()
data class StartKillSwitch(val allowedIps: List<String>) : AutoTunnelEvent()
data object StopKillSwitch : AutoTunnelEvent()
} }
@@ -1,16 +0,0 @@
package com.zaneschepke.wireguardautotunnel.domain.model
data class AppProxySettings(
val id: Long = 0,
val socks5ProxyEnabled: Boolean = false,
val socks5ProxyBindAddress: String? = null,
val httpProxyEnabled: Boolean = false,
val httpProxyBindAddress: String? = null,
val proxyUsername: String? = null,
val proxyPassword: String? = null,
) {
companion object {
const val DEFAULT_SOCKS_BIND_ADDRESS = "127.0.0.1:25344"
const val DEFAULT_HTTP_BIND_ADDRESS = "127.0.0.1:25345"
}
}
@@ -1,8 +1,6 @@
package com.zaneschepke.wireguardautotunnel.domain.model package com.zaneschepke.wireguardautotunnel.domain.model
import com.zaneschepke.networkmonitor.AndroidNetworkMonitor import com.zaneschepke.networkmonitor.AndroidNetworkMonitor
import com.zaneschepke.wireguardautotunnel.data.model.AppMode
import com.zaneschepke.wireguardautotunnel.data.model.DnsProtocol
data class AppSettings( data class AppSettings(
val id: Int = 0, val id: Int = 0,
@@ -13,9 +11,11 @@ data class AppSettings(
val isTunnelOnEthernetEnabled: Boolean = false, val isTunnelOnEthernetEnabled: Boolean = false,
val isShortcutsEnabled: Boolean = false, val isShortcutsEnabled: Boolean = false,
val isTunnelOnWifiEnabled: Boolean = false, val isTunnelOnWifiEnabled: Boolean = false,
val isKernelEnabled: Boolean = false,
val isRestoreOnBootEnabled: Boolean = false, val isRestoreOnBootEnabled: Boolean = false,
val isMultiTunnelEnabled: Boolean = false, val isMultiTunnelEnabled: Boolean = false,
val isPingEnabled: Boolean = false, val isPingEnabled: Boolean = false,
val isAmneziaEnabled: Boolean = false,
val isWildcardsEnabled: Boolean = false, val isWildcardsEnabled: Boolean = false,
val isStopOnNoInternetEnabled: Boolean = false, val isStopOnNoInternetEnabled: Boolean = false,
val isVpnKillSwitchEnabled: Boolean = false, val isVpnKillSwitchEnabled: Boolean = false,
@@ -29,9 +29,6 @@ data class AppSettings(
val tunnelPingIntervalSeconds: Int = 30, val tunnelPingIntervalSeconds: Int = 30,
val tunnelPingAttempts: Int = 3, val tunnelPingAttempts: Int = 3,
val tunnelPingTimeoutSeconds: Int? = null, val tunnelPingTimeoutSeconds: Int? = null,
val appMode: AppMode = AppMode.VPN,
val dnsProtocol: DnsProtocol = DnsProtocol.SYSTEM,
val dnsEndpoint: String? = null,
) { ) {
fun toAutoTunnelStateString(): String { fun toAutoTunnelStateString(): String {
return """ return """
@@ -99,6 +99,8 @@ data class TunnelConf(
override fun getName(): String = tunName override fun getName(): String = tunName
override fun isIpv4ResolutionPreferred(): Boolean = isIpv4Preferred
override fun onStateChange(newState: org.amnezia.awg.backend.Tunnel.State) { override fun onStateChange(newState: org.amnezia.awg.backend.Tunnel.State) {
stateChangeCallback?.invoke(newState) stateChangeCallback?.invoke(newState)
} }
@@ -107,10 +109,6 @@ data class TunnelConf(
stateChangeCallback?.invoke(newState) stateChangeCallback?.invoke(newState)
} }
override fun isIpv4ResolutionPreferred(): Boolean {
return true
}
fun generateUniqueName(tunnelNames: List<String>): String { fun generateUniqueName(tunnelNames: List<String>): String {
var tunnelName = this.tunName var tunnelName = this.tunName
var num = 1 var num = 1
@@ -144,7 +142,7 @@ data class TunnelConf(
config: org.amnezia.awg.config.Config, config: org.amnezia.awg.config.Config,
name: String? = null, name: String? = null,
): TunnelConf { ): TunnelConf {
val amQuick = config.toAwgQuickString(true, false) val amQuick = config.toAwgQuickString(true)
val wgQuick = config.toWgQuickString() val wgQuick = config.toWgQuickString()
return TunnelConf( return TunnelConf(
tunName = name ?: config.defaultName(), tunName = name ?: config.defaultName(),
@@ -156,8 +154,8 @@ data class TunnelConf(
private const val IPV6_ALL_NETWORKS = "::/0" private const val IPV6_ALL_NETWORKS = "::/0"
private const val IPV4_ALL_NETWORKS = "0.0.0.0/0" private const val IPV4_ALL_NETWORKS = "0.0.0.0/0"
val ALL_IPS = listOf(IPV4_ALL_NETWORKS, IPV6_ALL_NETWORKS) val ALL_IPS = listOf(IPV4_ALL_NETWORKS, IPV6_ALL_NETWORKS)
val IPV4_PUBLIC_NETWORKS = private val IPV4_PUBLIC_NETWORKS =
setOf( listOf(
"0.0.0.0/5", "0.0.0.0/5",
"8.0.0.0/7", "8.0.0.0/7",
"11.0.0.0/8", "11.0.0.0/8",
@@ -189,6 +187,6 @@ data class TunnelConf(
"200.0.0.0/5", "200.0.0.0/5",
"208.0.0.0/4", "208.0.0.0/4",
) )
val LAN_BYPASS_ALLOWED_IPS = setOf(IPV6_ALL_NETWORKS) + IPV4_PUBLIC_NETWORKS val LAN_BYPASS_ALLOWED_IPS = listOf(IPV6_ALL_NETWORKS) + IPV4_PUBLIC_NETWORKS
} }
} }
@@ -10,6 +10,4 @@ interface AppDataRepository {
val settings: AppSettingRepository val settings: AppSettingRepository
val tunnels: TunnelRepository val tunnels: TunnelRepository
val appState: AppStateRepository val appState: AppStateRepository
val proxySettings: ProxySettingsRepository
} }
@@ -1,12 +0,0 @@
package com.zaneschepke.wireguardautotunnel.domain.repository
import com.zaneschepke.wireguardautotunnel.domain.model.AppProxySettings
import kotlinx.coroutines.flow.Flow
interface ProxySettingsRepository {
suspend fun save(proxySettings: AppProxySettings)
val flow: Flow<AppProxySettings>
suspend fun get(): AppProxySettings
}
@@ -53,6 +53,16 @@ data class AutoTunnelState(
return AutoTunnelEvent.Stop return AutoTunnelEvent.Stop
} }
} }
// Handle kill switch only if no user tunnel is or will be active
if (stopKillSwitchOnTrusted()) {
return AutoTunnelEvent.StopKillSwitch
}
if (startKillSwitch()) {
val allowedIps =
if (settings.isLanOnKillSwitchEnabled) TunnelConf.LAN_BYPASS_ALLOWED_IPS
else emptyList()
return StartKillSwitch(allowedIps)
}
} }
is StateChange.MonitoringChange -> { is StateChange.MonitoringChange -> {
val bounceTunnels = bounceOnPingFailed() val bounceTunnels = bounceOnPingFailed()
@@ -1,12 +1,12 @@
package com.zaneschepke.wireguardautotunnel.domain.state package com.zaneschepke.wireguardautotunnel.domain.state
import com.zaneschepke.wireguardautotunnel.domain.enums.BackendMode import com.zaneschepke.wireguardautotunnel.domain.enums.BackendStatus
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelStatus import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelStatus
import org.amnezia.awg.crypto.Key import org.amnezia.awg.crypto.Key
data class TunnelState( data class TunnelState(
val status: TunnelStatus = TunnelStatus.Down, val status: TunnelStatus = TunnelStatus.Down,
val backendState: BackendMode = BackendMode.Inactive, val backendState: BackendStatus = BackendStatus.Inactive,
val statistics: TunnelStatistics? = null, val statistics: TunnelStatistics? = null,
val pingStates: Map<Key, PingState>? = null, val pingStates: Map<Key, PingState>? = null,
val handshakeSuccessLogs: Boolean? = null, val handshakeSuccessLogs: Boolean? = null,
@@ -7,6 +7,8 @@ sealed class Route {
@Serializable data object Settings : Route() @Serializable data object Settings : Route()
@Serializable data object SettingsAdvanced : Route()
@Serializable data object AutoTunnel : Route() @Serializable data object AutoTunnel : Route()
@Serializable data object AutoTunnelAdvanced : Route() @Serializable data object AutoTunnelAdvanced : Route()
@@ -19,6 +21,8 @@ sealed class Route {
@Serializable data object Display : Route() @Serializable data object Display : Route()
@Serializable data object KillSwitch : Route()
@Serializable data object Language : Route() @Serializable data object Language : Route()
@Serializable data object Main : Route() @Serializable data object Main : Route()
@@ -45,10 +49,4 @@ sealed class Route {
@Serializable data object Sort : Route() @Serializable data object Sort : Route()
@Serializable data object TunnelMonitoring : Route() @Serializable data object TunnelMonitoring : Route()
@Serializable data object SystemFeatures : Route()
@Serializable data object ProxySettings : Route()
@Serializable data object Dns : Route()
} }
@@ -1,25 +0,0 @@
package com.zaneschepke.wireguardautotunnel.ui.common
import android.view.WindowManager
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.ui.platform.LocalContext
import com.zaneschepke.wireguardautotunnel.MainActivity
@Composable
fun SecureScreenFromRecording() {
val context = LocalContext.current
val activity = context as? MainActivity
// Secure screen due to sensitive information
DisposableEffect(Unit) {
activity
?.window
?.setFlags(
WindowManager.LayoutParams.FLAG_SECURE,
WindowManager.LayoutParams.FLAG_SECURE,
)
onDispose { activity?.window?.clearFlags(WindowManager.LayoutParams.FLAG_SECURE) }
}
}
@@ -0,0 +1,34 @@
package com.zaneschepke.wireguardautotunnel.ui.common.animation
import androidx.compose.animation.core.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
@Composable
fun ShimmerEffect(modifier: Modifier = Modifier): Brush {
val shimmerColors =
listOf(
Color.LightGray.copy(alpha = 0.9f),
Color.LightGray.copy(alpha = 0.3f),
Color.LightGray.copy(alpha = 0.9f),
)
val transition = rememberInfiniteTransition()
val translateAnim by
transition.animateFloat(
initialValue = 0f,
targetValue = 1000f,
animationSpec =
infiniteRepeatable(animation = tween(durationMillis = 1200, easing = LinearEasing)),
)
return Brush.linearGradient(
colors = shimmerColors,
start = Offset(0f, 0f),
end = Offset(translateAnim, translateAnim),
)
}
@@ -1,42 +0,0 @@
package com.zaneschepke.wireguardautotunnel.ui.common.banner
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
@Composable
fun AppAlertBanner(
message: String,
textColor: Color,
containerColor: Color,
modifier: Modifier = Modifier,
) {
Box(
modifier =
modifier
.fillMaxWidth()
.height(IntrinsicSize.Min)
.background(
color = containerColor,
shape = RoundedCornerShape(bottomStart = 8.dp, bottomEnd = 8.dp),
)
.clip(RoundedCornerShape(bottomStart = 8.dp, bottomEnd = 8.dp))
.statusBarsPadding()
) {
Text(
text = message,
color = textColor,
style = MaterialTheme.typography.labelSmall.copy(fontSize = 8.sp),
modifier = Modifier.align(Alignment.Center).padding(bottom = 5.dp),
)
}
}
@@ -13,7 +13,7 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
@Composable @androidx.compose.runtime.Composable
fun IconSurfaceButton( fun IconSurfaceButton(
title: String, title: String,
onClick: () -> Unit, onClick: () -> Unit,
@@ -1,19 +0,0 @@
package com.zaneschepke.wireguardautotunnel.ui.common.button
import androidx.compose.foundation.focusable
import androidx.compose.foundation.layout.size
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.outlined.OpenInNew
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import com.zaneschepke.wireguardautotunnel.ui.theme.iconSize
@Composable
fun LinkIconButton(modifier: Modifier = Modifier.focusable(), onClick: () -> Unit) {
IconButton(modifier = modifier, onClick = onClick) {
val icon = Icons.AutoMirrored.Outlined.OpenInNew
Icon(icon, icon.name, Modifier.size(iconSize))
}
}
@@ -37,20 +37,18 @@ fun SelectionItemButton(
) { ) {
Row( Row(
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween, horizontalArrangement = Arrangement.Start,
modifier = Modifier.fillMaxSize().padding(horizontal = 12.dp), modifier = Modifier.fillMaxSize().padding(horizontal = 12.dp),
) { ) {
Row { leading?.let { it() }
leading?.let { it() } Text(
Text( buttonText,
buttonText, style = MaterialTheme.typography.labelMedium,
style = MaterialTheme.typography.labelMedium, color = MaterialTheme.colorScheme.onSurface,
color = MaterialTheme.colorScheme.onSurface, modifier = Modifier.fillMaxWidth(3 / 4f),
modifier = Modifier.fillMaxWidth(3 / 4f), maxLines = 2,
maxLines = 2, overflow = TextOverflow.Ellipsis,
overflow = TextOverflow.Ellipsis, )
)
}
trailing?.let { it() } trailing?.let { it() }
} }
} }
@@ -1,70 +1,55 @@
package com.zaneschepke.wireguardautotunnel.ui.common.textbox package com.zaneschepke.wireguardautotunnel.ui.common.config
import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardCapitalization import androidx.compose.ui.text.input.KeyboardCapitalization
import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.unit.dp
@Composable @Composable
fun ConfigurationTextBox( fun ConfigurationTextBox(
value: String, value: String,
label: String,
hint: String, hint: String,
onValueChange: (String) -> Unit, onValueChange: (String) -> Unit,
modifier: Modifier = Modifier,
keyboardActions: KeyboardActions = KeyboardActions(), keyboardActions: KeyboardActions = KeyboardActions(),
label: String,
modifier: Modifier,
isError: Boolean = false, isError: Boolean = false,
keyboardOptions: KeyboardOptions = keyboardOptions: KeyboardOptions =
KeyboardOptions(capitalization = KeyboardCapitalization.None, imeAction = ImeAction.Done), KeyboardOptions(capitalization = KeyboardCapitalization.None, imeAction = ImeAction.Done),
leading: (@Composable () -> Unit)? = null,
trailing: (@Composable () -> Unit)? = null, trailing: (@Composable () -> Unit)? = null,
supportingText: (@Composable () -> Unit)? = null, interactionSource: MutableInteractionSource? = null,
interactionSource: MutableInteractionSource = MutableInteractionSource(),
visualTransformation: VisualTransformation = VisualTransformation.None,
enabled: Boolean = true,
readOnly: Boolean = false,
singleLine: Boolean = true,
) { ) {
CustomTextField( OutlinedTextField(
isError = isError, isError = isError,
textStyle = textStyle = MaterialTheme.typography.labelLarge,
MaterialTheme.typography.bodySmall.copy(color = MaterialTheme.colorScheme.onSurface), modifier = modifier,
modifier = modifier.fillMaxWidth().height(48.dp),
value = value, value = value,
visualTransformation = visualTransformation, singleLine = true,
singleLine = singleLine,
interactionSource = interactionSource, interactionSource = interactionSource,
onValueChange = { onValueChange(it) }, onValueChange = { onValueChange(it) },
label = { label = {
Text( Text(
label, label,
color = MaterialTheme.colorScheme.onSurface, color = MaterialTheme.colorScheme.onSurface,
style = MaterialTheme.typography.labelMedium, style = MaterialTheme.typography.bodyMedium,
) )
}, },
containerColor = MaterialTheme.colorScheme.surface, maxLines = 1,
placeholder = { placeholder = {
Text( Text(
hint, hint,
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.outline, color = MaterialTheme.colorScheme.outline,
style = MaterialTheme.typography.bodyMedium,
) )
}, },
keyboardOptions = keyboardOptions, keyboardOptions = keyboardOptions,
keyboardActions = keyboardActions, keyboardActions = keyboardActions,
trailing = trailing, trailingIcon = trailing,
supportingText = supportingText,
leading = leading,
readOnly = readOnly,
enabled = enabled,
) )
} }
@@ -0,0 +1,41 @@
package com.zaneschepke.wireguardautotunnel.ui.common.config
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextAlign
import com.zaneschepke.wireguardautotunnel.ui.common.button.ScaledSwitch
@Composable
fun ConfigurationToggle(
label: String,
enabled: Boolean = true,
checked: Boolean,
onCheckChanged: (checked: Boolean) -> Unit,
modifier: Modifier = Modifier,
) {
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween,
) {
Text(
label,
textAlign = TextAlign.Start,
style = MaterialTheme.typography.labelLarge,
modifier = Modifier.weight(weight = 1.0f, fill = false),
softWrap = true,
)
ScaledSwitch(
modifier = modifier,
enabled = enabled,
checked = checked,
onClick = { onCheckChanged(it) },
)
}
}
@@ -1,9 +1,9 @@
package com.zaneschepke.wireguardautotunnel.ui.common.textbox package com.zaneschepke.wireguardautotunnel.ui.common.config
import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.interaction.collectIsFocusedAsState import androidx.compose.foundation.interaction.collectIsFocusedAsState
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
@@ -19,13 +19,13 @@ import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardCapitalization import androidx.compose.ui.text.input.KeyboardCapitalization
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.zaneschepke.wireguardautotunnel.ui.common.textbox.CustomTextField
@Composable @Composable
fun SubmitConfigurationTextBox( fun SubmitConfigurationTextBox(
value: String?, value: String?,
label: String, label: String,
hint: String, hint: String,
modifier: Modifier = Modifier,
isErrorValue: (value: String?) -> Boolean, isErrorValue: (value: String?) -> Boolean,
onSubmit: (value: String) -> Unit, onSubmit: (value: String) -> Unit,
supportingText: @Composable (() -> Unit)? = null, supportingText: @Composable (() -> Unit)? = null,
@@ -62,7 +62,7 @@ fun SubmitConfigurationTextBox(
color = MaterialTheme.colorScheme.outline, color = MaterialTheme.colorScheme.outline,
) )
}, },
modifier = modifier.fillMaxWidth().height(48.dp), modifier = Modifier.padding(top = 5.dp, bottom = 10.dp).fillMaxWidth().padding(end = 16.dp),
singleLine = true, singleLine = true,
keyboardOptions = keyboardOptions, keyboardOptions = keyboardOptions,
keyboardActions = keyboardActions =
@@ -2,7 +2,7 @@ package com.zaneschepke.wireguardautotunnel.ui.common.dropdown
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.height
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowDropDown import androidx.compose.material.icons.filled.ArrowDropDown
@@ -23,29 +23,37 @@ fun <T> DropdownSelector(
label: @Composable (() -> Unit)? = null, label: @Composable (() -> Unit)? = null,
isExpanded: Boolean = false, isExpanded: Boolean = false,
onDismiss: () -> Unit = {}, onDismiss: () -> Unit = {},
optionToString: @Composable (T?) -> String = {
it?.toString() ?: stringResource(R.string._default)
},
) { ) {
Row( Row(
horizontalArrangement = Arrangement.spacedBy(5.dp), horizontalArrangement = Arrangement.spacedBy(5.dp),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
) { ) {
if (label != null) label() if (label != null) label()
Text(text = optionToString(currentValue), style = MaterialTheme.typography.bodyMedium) Text(
text = currentValue?.toString() ?: stringResource(R.string._default),
style = MaterialTheme.typography.bodyMedium,
)
Icon(Icons.Default.ArrowDropDown, contentDescription = stringResource(R.string.dropdown)) Icon(Icons.Default.ArrowDropDown, contentDescription = stringResource(R.string.dropdown))
} }
DropdownMenu( DropdownMenu(
modifier = modifier.heightIn(max = 250.dp), modifier = modifier.height(250.dp),
scrollState = rememberScrollState(), scrollState = rememberScrollState(),
containerColor = MaterialTheme.colorScheme.surface, containerColor = MaterialTheme.colorScheme.surface,
expanded = isExpanded, expanded = isExpanded,
onDismissRequest = onDismiss, onDismissRequest = onDismiss,
) { ) {
options.forEach { option -> options.forEach { option ->
if (option == null) {
return@forEach DropdownMenuItem(
text = { Text(text = stringResource(R.string._default)) },
onClick = {
onValueSelected(null)
onDismiss()
},
)
}
DropdownMenuItem( DropdownMenuItem(
text = { Text(optionToString(option)) }, text = { Text(text = option.toString()) },
onClick = { onClick = {
onValueSelected(option) onValueSelected(option)
onDismiss() onDismiss()
@@ -5,17 +5,15 @@ import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SelectionIte
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SurfaceSelectionGroupButton import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SurfaceSelectionGroupButton
@Composable @Composable
fun <T> LabelledDropdown( fun LabelledNumberDropdown(
title: @Composable () -> Unit, title: @Composable () -> Unit,
description: (@Composable () -> Unit)? = null, description: (@Composable () -> Unit)? = null,
leading: @Composable () -> Unit, leading: @Composable () -> Unit,
onSelected: (T?) -> Unit, onSelected: (Int?) -> Unit,
options: List<T?>, options: List<Int?>,
currentValue: T?, currentValue: Int?,
optionToString: @Composable (T?) -> String,
) { ) {
var isDropDownExpanded by remember { mutableStateOf(false) } var isDropDownExpanded by remember { mutableStateOf(false) }
SurfaceSelectionGroupButton( SurfaceSelectionGroupButton(
listOf( listOf(
SelectionItem( SelectionItem(
@@ -27,10 +25,9 @@ fun <T> LabelledDropdown(
DropdownSelector( DropdownSelector(
currentValue = currentValue, currentValue = currentValue,
options = options, options = options,
onValueSelected = { selected -> onSelected(selected) }, onValueSelected = { num -> onSelected(num) },
isExpanded = isDropDownExpanded, isExpanded = isDropDownExpanded,
onDismiss = { isDropDownExpanded = false }, onDismiss = { isDropDownExpanded = false },
optionToString = optionToString,
) )
}, },
) )
@@ -1,69 +0,0 @@
package com.zaneschepke.wireguardautotunnel.ui.common.sheet
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Check
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.zaneschepke.wireguardautotunnel.R
@Composable
fun SheetOption(
label: String,
leadingIcon: ImageVector? = null,
onClick: () -> Unit,
selected: Boolean,
modifier: Modifier = Modifier,
) {
Row(
modifier = modifier.fillMaxWidth().clickable(onClick = onClick).padding(10.dp),
horizontalArrangement = Arrangement.SpaceBetween,
) {
Row {
leadingIcon?.let {
Icon(
imageVector = it,
contentDescription = null,
modifier = Modifier.padding(10.dp),
)
}
Text(text = label, modifier = Modifier.padding(10.dp))
}
if (selected) {
Icon(
imageVector = Icons.Outlined.Check,
contentDescription = stringResource(R.string.selected),
modifier = Modifier.padding(10.dp),
)
}
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun CustomBottomSheet(options: List<SheetOption>, onDismiss: () -> Unit) {
ModalBottomSheet(
containerColor = MaterialTheme.colorScheme.surface,
onDismissRequest = onDismiss,
) {
options.forEachIndexed { index, option ->
SheetOption(option.label, option.leadingIcon, option.onClick, option.selected)
if (index != options.size - 1) HorizontalDivider()
}
}
}
data class SheetOption(
val leadingIcon: ImageVector,
val label: String,
val onClick: () -> Unit,
val selected: Boolean = false,
)
@@ -27,7 +27,7 @@ fun CustomTextField(
label: @Composable () -> Unit, label: @Composable () -> Unit,
containerColor: Color, containerColor: Color,
onValueChange: (value: String) -> Unit = {}, onValueChange: (value: String) -> Unit = {},
singleLine: Boolean = true, singleLine: Boolean = false,
placeholder: @Composable (() -> Unit)? = null, placeholder: @Composable (() -> Unit)? = null,
keyboardOptions: KeyboardOptions = KeyboardOptions(), keyboardOptions: KeyboardOptions = KeyboardOptions(),
keyboardActions: KeyboardActions = KeyboardActions(), keyboardActions: KeyboardActions = KeyboardActions(),
@@ -37,8 +37,7 @@ fun CustomTextField(
isError: Boolean = false, isError: Boolean = false,
readOnly: Boolean = false, readOnly: Boolean = false,
enabled: Boolean = true, enabled: Boolean = true,
visualTransformation: VisualTransformation = VisualTransformation.None, interactionSource: MutableInteractionSource,
interactionSource: MutableInteractionSource = MutableInteractionSource(),
) { ) {
val space = " " val space = " "
BasicTextField( BasicTextField(
@@ -53,7 +52,6 @@ fun CustomTextField(
interactionSource = interactionSource, interactionSource = interactionSource,
enabled = enabled, enabled = enabled,
singleLine = singleLine, singleLine = singleLine,
visualTransformation = visualTransformation,
) { ) {
OutlinedTextFieldDefaults.DecorationBox( OutlinedTextFieldDefaults.DecorationBox(
value = space + value, value = space + value,
@@ -83,14 +81,14 @@ fun CustomTextField(
), ),
enabled = enabled, enabled = enabled,
label = label, label = label,
visualTransformation = visualTransformation, visualTransformation = VisualTransformation.None,
interactionSource = interactionSource, interactionSource = interactionSource,
placeholder = placeholder, placeholder = placeholder,
container = { container = {
OutlinedTextFieldDefaults.Container( OutlinedTextFieldDefaults.ContainerBox(
enabled = enabled, enabled,
isError = isError, isError = isError,
interactionSource = interactionSource, interactionSource,
colors = colors =
TextFieldDefaults.colors() TextFieldDefaults.colors()
.copy( .copy(
@@ -11,13 +11,12 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.zaneschepke.wireguardautotunnel.ui.state.NavBarState import com.zaneschepke.wireguardautotunnel.ui.state.NavBarState
import com.zaneschepke.wireguardautotunnel.ui.theme.LockedDownBannerHeight
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun DynamicTopAppBar(navBarState: NavBarState, modifier: Modifier = Modifier) { fun DynamicTopAppBar(navBarState: NavBarState, modifier: Modifier = Modifier) {
TopAppBar( TopAppBar(
modifier = modifier.padding(top = LockedDownBannerHeight), modifier = modifier,
colors = TopAppBarDefaults.topAppBarColors().copy(Color.Transparent), colors = TopAppBarDefaults.topAppBarColors().copy(Color.Transparent),
title = { title = {
AnimatedVisibility( AnimatedVisibility(
@@ -15,7 +15,6 @@ import androidx.compose.runtime.produceState
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.navigation.NavBackStackEntry import androidx.navigation.NavBackStackEntry
import androidx.navigation.NavController import androidx.navigation.NavController
@@ -39,8 +38,6 @@ fun currentNavBackStackEntryAsNavBarState(
uiState: AppUiState, uiState: AppUiState,
appViewState: AppViewState, appViewState: AppViewState,
): State<NavBarState> { ): State<NavBarState> {
val context = LocalContext.current
fun isActiveSelected() = fun isActiveSelected() =
uiState.activeTunnels.any { active -> uiState.activeTunnels.any { active ->
appViewState.selectedTunnels.any { it.id == active.key.id } appViewState.selectedTunnels.any { it.id == active.key.id }
@@ -147,10 +144,7 @@ fun currentNavBackStackEntryAsNavBarState(
topTitle = { Text(stringResource(R.string.settings)) }, topTitle = { Text(stringResource(R.string.settings)) },
route = Route.Settings, route = Route.Settings,
topTrailing = { topTrailing = {
ActionIconButton( ActionIconButton(Icons.Rounded.Menu, R.string.quick_actions) {
Icons.Rounded.SettingsBackupRestore,
R.string.quick_actions,
) {
viewModel.handleEvent( viewModel.handleEvent(
AppEvent.SetBottomSheet( AppEvent.SetBottomSheet(
AppViewState.BottomSheet.BACKUP_AND_RESTORE AppViewState.BottomSheet.BACKUP_AND_RESTORE
@@ -184,33 +178,16 @@ fun currentNavBackStackEntryAsNavBarState(
route = Route.TunnelMonitoring, route = Route.TunnelMonitoring,
) )
backStackEntry.isCurrentRoute(Route.ProxySettings::class) ->
NavBarState(
topTitle = { Text(stringResource(R.string.proxy_settings)) },
route = Route.ProxySettings,
topTrailing = {
ActionIconButton(Icons.Rounded.Save, R.string.save) {
viewModel.handleEvent(AppEvent.InvokeScreenAction)
}
},
)
backStackEntry.isCurrentRoute(Route.WifiDetectionMethod::class) -> backStackEntry.isCurrentRoute(Route.WifiDetectionMethod::class) ->
NavBarState( NavBarState(
topTitle = { Text(stringResource(R.string.wifi_detection_method)) }, topTitle = { Text(stringResource(R.string.wifi_detection_method)) },
route = Route.WifiDetectionMethod, route = Route.WifiDetectionMethod,
) )
backStackEntry.isCurrentRoute(Route.SystemFeatures::class) -> backStackEntry.isCurrentRoute(Route.KillSwitch::class) ->
NavBarState( NavBarState(
topTitle = { Text(stringResource(R.string.android_integrations)) }, topTitle = { Text(stringResource(R.string.kill_switch)) },
route = Route.SystemFeatures, route = Route.KillSwitch,
)
backStackEntry.isCurrentRoute(Route.Dns::class) ->
NavBarState(
topTitle = { Text(stringResource(R.string.dns_settings)) },
route = Route.Dns,
) )
backStackEntry.isCurrentRoute(Route.Support::class) -> backStackEntry.isCurrentRoute(Route.Support::class) ->
@@ -247,7 +224,8 @@ fun currentNavBackStackEntryAsNavBarState(
) )
} }
backStackEntry.isCurrentRoute(Route.AutoTunnelAdvanced::class) -> backStackEntry.isCurrentRoute(Route.AutoTunnelAdvanced::class) ||
backStackEntry.isCurrentRoute(Route.SettingsAdvanced::class) ->
NavBarState( NavBarState(
showTop = true, showTop = true,
showBottom = true, showBottom = true,
@@ -300,14 +278,12 @@ fun currentNavBackStackEntryAsNavBarState(
backStackEntry.isCurrentRoute(Route.Config::class) -> { backStackEntry.isCurrentRoute(Route.Config::class) -> {
val args = backStackEntry?.toRoute<Route.Config>() val args = backStackEntry?.toRoute<Route.Config>()
val name = val name = uiState.tunnels.find { it.id == args?.id }?.name
uiState.tunnels.find { it.id == args?.id }?.name
?: context.getString(R.string.new_tunnel)
NavBarState( NavBarState(
showTop = true, showTop = true,
showBottom = true, showBottom = true,
topTitle = { Text(name) }, topTitle = { name?.let { Text(it) } },
topTrailing = { topTrailing = {
ActionIconButton(Icons.Rounded.Save, R.string.save) { ActionIconButton(Icons.Rounded.Save, R.string.save) {
viewModel.handleEvent(AppEvent.InvokeScreenAction) viewModel.handleEvent(AppEvent.InvokeScreenAction)
@@ -145,14 +145,7 @@ fun AutoTunnelScreen(uiState: AppUiState, viewModel: AppViewModel) {
title = { Text(title) }, title = { Text(title) },
trailing = { trailing = {
Button({ viewModel.handleEvent(AppEvent.ToggleAutoTunnel) }) { Button({ viewModel.handleEvent(AppEvent.ToggleAutoTunnel) }) {
Text( Text(buttonText, fontWeight = FontWeight.Bold)
buttonText,
fontWeight = FontWeight.Bold,
style =
MaterialTheme.typography.bodyMedium.copy(
color = MaterialTheme.colorScheme.surface
),
)
} }
}, },
) )
@@ -17,7 +17,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.zaneschepke.wireguardautotunnel.R import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.ui.common.dropdown.LabelledDropdown import com.zaneschepke.wireguardautotunnel.ui.common.dropdown.LabelledNumberDropdown
import com.zaneschepke.wireguardautotunnel.ui.state.AppUiState import com.zaneschepke.wireguardautotunnel.ui.state.AppUiState
import com.zaneschepke.wireguardautotunnel.viewmodel.AppViewModel import com.zaneschepke.wireguardautotunnel.viewmodel.AppViewModel
import com.zaneschepke.wireguardautotunnel.viewmodel.event.AppEvent import com.zaneschepke.wireguardautotunnel.viewmodel.event.AppEvent
@@ -33,7 +33,7 @@ fun AutoTunnelAdvancedScreen(appUiState: AppUiState, viewModel: AppViewModel) {
.padding(vertical = 24.dp) .padding(vertical = 24.dp)
.padding(horizontal = 12.dp), .padding(horizontal = 12.dp),
) { ) {
LabelledDropdown( LabelledNumberDropdown(
title = { title = {
Text( Text(
stringResource(R.string.debounce_delay), stringResource(R.string.debounce_delay),
@@ -49,7 +49,6 @@ fun AutoTunnelAdvancedScreen(appUiState: AppUiState, viewModel: AppViewModel) {
}, },
options = (0..10).toList(), options = (0..10).toList(),
currentValue = appUiState.appSettings.debounceDelaySeconds, currentValue = appUiState.appSettings.debounceDelaySeconds,
optionToString = { it?.toString() ?: stringResource(R.string._default) },
) )
} }
} }
@@ -27,7 +27,7 @@ import com.zaneschepke.wireguardautotunnel.ui.navigation.LocalNavController
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.components.LearnMoreLinkLabel import com.zaneschepke.wireguardautotunnel.ui.screens.settings.components.LearnMoreLinkLabel
import com.zaneschepke.wireguardautotunnel.ui.state.AppUiState import com.zaneschepke.wireguardautotunnel.ui.state.AppUiState
import com.zaneschepke.wireguardautotunnel.ui.theme.iconSize import com.zaneschepke.wireguardautotunnel.ui.theme.iconSize
import com.zaneschepke.wireguardautotunnel.util.extensions.asTitleString import com.zaneschepke.wireguardautotunnel.util.extensions.asString
import com.zaneschepke.wireguardautotunnel.util.extensions.openWebUrl import com.zaneschepke.wireguardautotunnel.util.extensions.openWebUrl
import com.zaneschepke.wireguardautotunnel.viewmodel.AppViewModel import com.zaneschepke.wireguardautotunnel.viewmodel.AppViewModel
import com.zaneschepke.wireguardautotunnel.viewmodel.event.AppEvent import com.zaneschepke.wireguardautotunnel.viewmodel.event.AppEvent
@@ -123,7 +123,7 @@ fun WifiTunnelingItems(
Text( Text(
stringResource( stringResource(
R.string.current_template, R.string.current_template,
uiState.appSettings.wifiDetectionMethod.asTitleString(context), uiState.appSettings.wifiDetectionMethod.asString(context),
), ),
style = style =
MaterialTheme.typography.bodySmall.copy( MaterialTheme.typography.bodySmall.copy(
@@ -13,7 +13,7 @@ import com.zaneschepke.networkmonitor.AndroidNetworkMonitor
import com.zaneschepke.wireguardautotunnel.ui.common.button.IconSurfaceButton import com.zaneschepke.wireguardautotunnel.ui.common.button.IconSurfaceButton
import com.zaneschepke.wireguardautotunnel.ui.state.AppUiState import com.zaneschepke.wireguardautotunnel.ui.state.AppUiState
import com.zaneschepke.wireguardautotunnel.util.extensions.asDescriptionString import com.zaneschepke.wireguardautotunnel.util.extensions.asDescriptionString
import com.zaneschepke.wireguardautotunnel.util.extensions.asTitleString import com.zaneschepke.wireguardautotunnel.util.extensions.asString
import com.zaneschepke.wireguardautotunnel.viewmodel.AppViewModel import com.zaneschepke.wireguardautotunnel.viewmodel.AppViewModel
import com.zaneschepke.wireguardautotunnel.viewmodel.event.AppEvent import com.zaneschepke.wireguardautotunnel.viewmodel.event.AppEvent
@@ -26,7 +26,7 @@ fun WifiDetectionMethodScreen(uiState: AppUiState, viewModel: AppViewModel) {
modifier = Modifier.fillMaxSize().padding(top = 24.dp).padding(horizontal = 24.dp), modifier = Modifier.fillMaxSize().padding(top = 24.dp).padding(horizontal = 24.dp),
) { ) {
enumValues<AndroidNetworkMonitor.WifiDetectionMethod>().forEach { enumValues<AndroidNetworkMonitor.WifiDetectionMethod>().forEach {
val title = it.asTitleString(context) val title = it.asString(context)
val description = it.asDescriptionString(context) val description = it.asDescriptionString(context)
IconSurfaceButton( IconSurfaceButton(
title = title, title = title,
@@ -1,16 +1,20 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.main.components package com.zaneschepke.wireguardautotunnel.ui.screens.main.components
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.FolderZip import androidx.compose.material.icons.outlined.FolderZip
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.*
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.zaneschepke.wireguardautotunnel.R import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.domain.enums.ConfigType import com.zaneschepke.wireguardautotunnel.domain.enums.ConfigType
import com.zaneschepke.wireguardautotunnel.ui.common.functions.rememberFileExportLauncherForResult import com.zaneschepke.wireguardautotunnel.ui.common.functions.rememberFileExportLauncherForResult
import com.zaneschepke.wireguardautotunnel.ui.common.sheet.CustomBottomSheet
import com.zaneschepke.wireguardautotunnel.ui.common.sheet.SheetOption
import com.zaneschepke.wireguardautotunnel.ui.navigation.LocalIsAndroidTV import com.zaneschepke.wireguardautotunnel.ui.navigation.LocalIsAndroidTV
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.components.AuthorizationPromptWrapper import com.zaneschepke.wireguardautotunnel.ui.screens.settings.components.AuthorizationPromptWrapper
import com.zaneschepke.wireguardautotunnel.ui.state.AppViewState import com.zaneschepke.wireguardautotunnel.ui.state.AppViewState
@@ -70,34 +74,47 @@ fun ExportTunnelsBottomSheet(viewModel: AppViewModel) {
) )
} }
CustomBottomSheet( ModalBottomSheet(
listOf( containerColor = MaterialTheme.colorScheme.surface,
SheetOption( onDismissRequest = {
Icons.Outlined.FolderZip, viewModel.handleEvent(AppEvent.SetBottomSheet(AppViewState.BottomSheet.NONE))
stringResource(R.string.export_tunnels_amnezia), },
onClick = {
exportConfigType = ConfigType.AM
if (!isAuthorized && !isTv) {
showAuthPrompt = true
} else {
shouldExport = true
}
},
),
SheetOption(
Icons.Outlined.FolderZip,
stringResource(R.string.export_tunnels_wireguard),
onClick = {
exportConfigType = ConfigType.WG
if (!isAuthorized && !isTv) {
showAuthPrompt = true
} else {
shouldExport = true
}
},
),
)
) { ) {
viewModel.handleEvent(AppEvent.SetBottomSheet(AppViewState.BottomSheet.NONE)) ExportOptionRow(
label = stringResource(R.string.export_tunnels_amnezia),
onClick = {
exportConfigType = ConfigType.AM
if (!isAuthorized && !isTv) {
showAuthPrompt = true
} else {
shouldExport = true
}
},
)
HorizontalDivider()
ExportOptionRow(
label = stringResource(R.string.export_tunnels_wireguard),
onClick = {
exportConfigType = ConfigType.WG
if (!isAuthorized && !isTv) {
showAuthPrompt = true
} else {
shouldExport = true
}
},
)
}
}
@Composable
private fun ExportOptionRow(label: String, onClick: () -> Unit) {
Row(modifier = Modifier.fillMaxWidth().clickable(onClick = onClick).padding(10.dp)) {
Icon(
imageVector = Icons.Outlined.FolderZip,
contentDescription = label,
modifier = Modifier.padding(10.dp),
)
Text(text = label, modifier = Modifier.padding(10.dp))
} }
} }
@@ -1,15 +1,21 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.main.components package com.zaneschepke.wireguardautotunnel.ui.screens.main.components
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.* import androidx.compose.material.icons.filled.*
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.*
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.zaneschepke.wireguardautotunnel.R import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.ui.common.sheet.CustomBottomSheet
import com.zaneschepke.wireguardautotunnel.ui.common.sheet.SheetOption
import com.zaneschepke.wireguardautotunnel.ui.navigation.LocalIsAndroidTV import com.zaneschepke.wireguardautotunnel.ui.navigation.LocalIsAndroidTV
// TODO refactor this component
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun TunnelImportSheet( fun TunnelImportSheet(
@@ -22,61 +28,99 @@ fun TunnelImportSheet(
) { ) {
val isTv = LocalIsAndroidTV.current val isTv = LocalIsAndroidTV.current
CustomBottomSheet( val sheetState = rememberModalBottomSheetState()
buildList {
add( val context = LocalContext.current
SheetOption( ModalBottomSheet(
Icons.Outlined.FileOpen, containerColor = MaterialTheme.colorScheme.surface,
stringResource(R.string.add_tunnels_text), onDismissRequest = { onDismiss() },
onClick = { sheetState = sheetState,
) {
Row(
modifier =
Modifier.fillMaxWidth()
.clickable {
onDismiss() onDismiss()
onFileClick() onFileClick()
}, }
) .padding(10.dp)
) {
Icon(
Icons.Filled.FileOpen,
contentDescription = stringResource(id = R.string.open_file),
modifier = Modifier.padding(10.dp),
) )
if (!isTv) Text(stringResource(id = R.string.add_tunnels_text), modifier = Modifier.padding(10.dp))
add( }
SheetOption( if (!isTv) {
Icons.Outlined.QrCode, HorizontalDivider()
stringResource(R.string.add_from_qr), Row(
onClick = { modifier =
Modifier.fillMaxWidth()
.clickable {
onDismiss() onDismiss()
onQrClick() onQrClick()
}, }
) .padding(10.dp)
) {
Icon(
Icons.Filled.QrCode,
contentDescription = stringResource(id = R.string.qr_scan),
modifier = Modifier.padding(10.dp),
) )
add( Text(stringResource(id = R.string.add_from_qr), modifier = Modifier.padding(10.dp))
SheetOption( }
Icons.Outlined.ContentPasteGo, HorizontalDivider()
stringResource(R.string.add_from_clipboard), Row(
onClick = { modifier =
onDismiss() Modifier.fillMaxWidth()
onClipboardClick() .clickable {
}, onDismiss()
onClipboardClick()
}
.padding(10.dp)
) {
val icon = Icons.Filled.ContentPasteGo
Icon(icon, contentDescription = icon.name, modifier = Modifier.padding(10.dp))
Text(
stringResource(id = R.string.add_from_clipboard),
modifier = Modifier.padding(10.dp),
) )
) }
add( }
SheetOption( HorizontalDivider()
Icons.Outlined.Link, Row(
stringResource(R.string.add_from_url), modifier =
onClick = { Modifier.fillMaxWidth()
.clickable {
onDismiss() onDismiss()
onUrlClick() onUrlClick()
}, }
) .padding(10.dp)
) {
Icon(
Icons.Filled.Link,
contentDescription = stringResource(id = R.string.add_from_url),
modifier = Modifier.padding(10.dp),
) )
add( Text(stringResource(id = R.string.add_from_url), modifier = Modifier.padding(10.dp))
SheetOption( }
Icons.Outlined.Create, HorizontalDivider()
stringResource(R.string.create_import), Row(
onClick = { modifier =
Modifier.fillMaxWidth()
.clickable {
onDismiss() onDismiss()
onManualImportClick() onManualImportClick()
}, }
) .padding(10.dp)
) {
Icon(
Icons.Filled.Create,
contentDescription = stringResource(id = R.string.create_import),
modifier = Modifier.padding(10.dp),
) )
Text(stringResource(id = R.string.create_import), modifier = Modifier.padding(10.dp))
} }
) {
onDismiss()
} }
} }
@@ -1,5 +1,6 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.main.config package com.zaneschepke.wireguardautotunnel.ui.screens.main.config
import android.view.WindowManager
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
@@ -14,9 +15,9 @@ import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.zaneschepke.wireguardautotunnel.MainActivity
import com.zaneschepke.wireguardautotunnel.R import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConf import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConf
import com.zaneschepke.wireguardautotunnel.ui.common.SecureScreenFromRecording
import com.zaneschepke.wireguardautotunnel.ui.common.prompt.AuthorizationPrompt import com.zaneschepke.wireguardautotunnel.ui.common.prompt.AuthorizationPrompt
import com.zaneschepke.wireguardautotunnel.ui.screens.main.config.components.AddPeerButton import com.zaneschepke.wireguardautotunnel.ui.screens.main.config.components.AddPeerButton
import com.zaneschepke.wireguardautotunnel.ui.screens.main.config.components.InterfaceSection import com.zaneschepke.wireguardautotunnel.ui.screens.main.config.components.InterfaceSection
@@ -38,6 +39,8 @@ fun ConfigScreen(
val uiState by viewModel.uiState.collectAsStateWithLifecycle() val uiState by viewModel.uiState.collectAsStateWithLifecycle()
val activity = context as? MainActivity
var save by remember { mutableStateOf(false) } var save by remember { mutableStateOf(false) }
val isTunnelNameTaken by val isTunnelNameTaken by
@@ -49,7 +52,16 @@ fun ConfigScreen(
} }
} }
SecureScreenFromRecording() // Secure screen due to sensitive information
DisposableEffect(Unit) {
activity
?.window
?.setFlags(
WindowManager.LayoutParams.FLAG_SECURE,
WindowManager.LayoutParams.FLAG_SECURE,
)
onDispose { activity?.window?.clearFlags(WindowManager.LayoutParams.FLAG_SECURE) }
}
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
// set callback for navbar to invoke save // set callback for navbar to invoke save
@@ -113,13 +125,9 @@ fun ConfigScreen(
} }
Column( Column(
horizontalAlignment = Alignment.Start,
verticalArrangement = Arrangement.spacedBy(24.dp, Alignment.Top), verticalArrangement = Arrangement.spacedBy(24.dp, Alignment.Top),
modifier = modifier =
Modifier.fillMaxSize() Modifier.fillMaxSize().verticalScroll(rememberScrollState()).padding(horizontal = 24.dp),
.verticalScroll(rememberScrollState())
.padding(top = 12.dp, bottom = 24.dp)
.padding(horizontal = 12.dp),
) { ) {
InterfaceSection(isTunnelNameTaken, uiState, viewModel) InterfaceSection(isTunnelNameTaken, uiState, viewModel)
PeersSection(uiState, viewModel) PeersSection(uiState, viewModel)
@@ -2,7 +2,6 @@ package com.zaneschepke.wireguardautotunnel.ui.screens.main.config.components
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardActions
@@ -10,9 +9,7 @@ import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.ContentCopy import androidx.compose.material.icons.rounded.ContentCopy
import androidx.compose.material.icons.rounded.Refresh import androidx.compose.material.icons.rounded.Refresh
import androidx.compose.material3.Icon import androidx.compose.material3.*
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.platform.LocalSoftwareKeyboardController
@@ -21,12 +18,10 @@ import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.PasswordVisualTransformation import androidx.compose.ui.text.input.PasswordVisualTransformation
import androidx.compose.ui.text.input.VisualTransformation import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.wireguard.crypto.KeyPair
import com.zaneschepke.wireguardautotunnel.R import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.ui.common.config.ConfigurationTextBox
import com.zaneschepke.wireguardautotunnel.ui.common.functions.rememberClipboardHelper import com.zaneschepke.wireguardautotunnel.ui.common.functions.rememberClipboardHelper
import com.zaneschepke.wireguardautotunnel.ui.common.textbox.ConfigurationTextBox
import com.zaneschepke.wireguardautotunnel.ui.state.InterfaceProxy import com.zaneschepke.wireguardautotunnel.ui.state.InterfaceProxy
import java.util.*
@Composable @Composable
fun InterfaceFields( fun InterfaceFields(
@@ -42,208 +37,189 @@ fun InterfaceFields(
val keyboardActions = KeyboardActions(onDone = { keyboardController?.hide() }) val keyboardActions = KeyboardActions(onDone = { keyboardController?.hide() })
val keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done) val keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done)
Column(verticalArrangement = Arrangement.spacedBy(12.dp)) { OutlinedTextField(
ConfigurationTextBox( value = interfaceState.privateKey,
value = interfaceState.privateKey, onValueChange = { onInterfaceChange(interfaceState.copy(privateKey = it)) },
hint = label = {
stringResource(R.string.hint_template, stringResource(R.string.base64_key)) Text(stringResource(R.string.private_key), style = MaterialTheme.typography.bodyMedium)
.lowercase(Locale.getDefault()), },
onValueChange = { onInterfaceChange(interfaceState.copy(privateKey = it)) }, modifier = Modifier.fillMaxWidth().clickable { if (!isAuthenticated) showAuthPrompt() },
label = stringResource(R.string.private_key), visualTransformation =
modifier = Modifier.fillMaxWidth().clickable { if (!isAuthenticated) showAuthPrompt() }, if (isAuthenticated) VisualTransformation.None else PasswordVisualTransformation(),
visualTransformation = trailingIcon = {
if (isAuthenticated) VisualTransformation.None else PasswordVisualTransformation(), IconButton(
trailing = { enabled = true,
IconButton( onClick = {
enabled = true, if (!isAuthenticated) return@IconButton showAuthPrompt()
onClick = { val keypair = com.wireguard.crypto.KeyPair()
if (!isAuthenticated) return@IconButton showAuthPrompt() onInterfaceChange(
val keypair = KeyPair() interfaceState.copy(
onInterfaceChange( privateKey = keypair.privateKey.toBase64(),
interfaceState.copy( publicKey = keypair.publicKey.toBase64(),
privateKey = keypair.privateKey.toBase64(),
publicKey = keypair.publicKey.toBase64(),
)
) )
},
) {
Icon(
Icons.Rounded.Refresh,
stringResource(R.string.rotate_keys),
tint =
if (isAuthenticated) MaterialTheme.colorScheme.onSurface
else MaterialTheme.colorScheme.outline,
) )
} },
) {
Icon(
Icons.Rounded.Refresh,
stringResource(R.string.rotate_keys),
tint =
if (isAuthenticated) MaterialTheme.colorScheme.onSurface
else MaterialTheme.colorScheme.outline,
)
}
},
enabled = isAuthenticated,
singleLine = true,
keyboardOptions = keyboardOptions,
keyboardActions = keyboardActions,
)
OutlinedTextField(
value = interfaceState.publicKey,
onValueChange = { onInterfaceChange(interfaceState.copy(publicKey = it)) },
label = {
Text(stringResource(R.string.public_key), style = MaterialTheme.typography.bodyMedium)
},
enabled = false,
modifier = Modifier.fillMaxWidth(),
singleLine = true,
trailingIcon = {
IconButton(onClick = { clipboardManager.copy(interfaceState.publicKey) }) {
Icon(Icons.Rounded.ContentCopy, stringResource(R.string.copy_public_key))
}
},
keyboardOptions = keyboardOptions,
keyboardActions = keyboardActions,
)
ConfigurationTextBox(
value = interfaceState.addresses,
onValueChange = { onInterfaceChange(interfaceState.copy(addresses = it)) },
label = stringResource(R.string.addresses),
hint = stringResource(R.string.comma_separated_list),
modifier = Modifier.fillMaxWidth(),
)
ConfigurationTextBox(
value = interfaceState.listenPort,
onValueChange = { onInterfaceChange(interfaceState.copy(listenPort = it)) },
label = stringResource(R.string.listen_port),
hint = stringResource(R.string.random),
modifier = Modifier.fillMaxWidth(),
)
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(6.dp)) {
ConfigurationTextBox(
value = interfaceState.dnsServers,
onValueChange = { onInterfaceChange(interfaceState.copy(dnsServers = it)) },
label = stringResource(R.string.dns_servers),
hint = stringResource(R.string.comma_separated_list),
modifier = Modifier.weight(3f),
)
ConfigurationTextBox(
value = interfaceState.mtu,
onValueChange = { onInterfaceChange(interfaceState.copy(mtu = it)) },
label = stringResource(R.string.mtu),
hint = stringResource(R.string.auto),
modifier = Modifier.weight(2f),
)
}
if (showScripts) {
ConfigurationTextBox(
value = interfaceState.preUp,
onValueChange = { onInterfaceChange(interfaceState.copy(preUp = it)) },
label = stringResource(R.string.pre_up),
hint = stringResource(R.string.comma_separated_list).lowercase(),
modifier = Modifier.fillMaxWidth(),
)
ConfigurationTextBox(
value = interfaceState.postUp,
onValueChange = { onInterfaceChange(interfaceState.copy(postUp = it)) },
label = stringResource(R.string.post_up),
hint = stringResource(R.string.comma_separated_list).lowercase(),
modifier = Modifier.fillMaxWidth(),
)
ConfigurationTextBox(
value = interfaceState.preDown,
onValueChange = { onInterfaceChange(interfaceState.copy(preDown = it)) },
label = stringResource(R.string.pre_down),
hint = stringResource(R.string.comma_separated_list).lowercase(),
modifier = Modifier.fillMaxWidth(),
)
ConfigurationTextBox(
value = interfaceState.postDown,
onValueChange = { onInterfaceChange(interfaceState.copy(postDown = it)) },
label = stringResource(R.string.post_down),
hint = stringResource(R.string.comma_separated_list).lowercase(),
modifier = Modifier.fillMaxWidth(),
)
}
if (showAmneziaValues) {
ConfigurationTextBox(
value = interfaceState.junkPacketCount,
onValueChange = { onInterfaceChange(interfaceState.copy(junkPacketCount = it)) },
label = stringResource(R.string.junk_packet_count),
hint = stringResource(R.string.junk_packet_count).lowercase(),
modifier = Modifier.fillMaxWidth(),
)
ConfigurationTextBox(
value = interfaceState.junkPacketMinSize,
onValueChange = { onInterfaceChange(interfaceState.copy(junkPacketMinSize = it)) },
label = stringResource(R.string.junk_packet_minimum_size),
hint = stringResource(R.string.junk_packet_minimum_size).lowercase(),
modifier = Modifier.fillMaxWidth(),
)
ConfigurationTextBox(
value = interfaceState.junkPacketMaxSize,
onValueChange = { onInterfaceChange(interfaceState.copy(junkPacketMaxSize = it)) },
label = stringResource(R.string.junk_packet_maximum_size),
hint = stringResource(R.string.junk_packet_maximum_size).lowercase(),
modifier = Modifier.fillMaxWidth(),
)
ConfigurationTextBox(
value = interfaceState.initPacketJunkSize,
onValueChange = { onInterfaceChange(interfaceState.copy(initPacketJunkSize = it)) },
label = stringResource(R.string.init_packet_junk_size),
hint = stringResource(R.string.init_packet_junk_size).lowercase(),
modifier = Modifier.fillMaxWidth(),
)
ConfigurationTextBox(
value = interfaceState.responsePacketJunkSize,
onValueChange = { onInterfaceChange(interfaceState.copy(responsePacketJunkSize = it)) },
label = stringResource(R.string.response_packet_junk_size),
hint = stringResource(R.string.response_packet_junk_size).lowercase(),
modifier = Modifier.fillMaxWidth(),
)
ConfigurationTextBox(
value = interfaceState.initPacketMagicHeader,
onValueChange = { onInterfaceChange(interfaceState.copy(initPacketMagicHeader = it)) },
label = stringResource(R.string.init_packet_magic_header),
hint = stringResource(R.string.init_packet_magic_header).lowercase(),
modifier = Modifier.fillMaxWidth(),
)
ConfigurationTextBox(
value = interfaceState.responsePacketMagicHeader,
onValueChange = {
onInterfaceChange(interfaceState.copy(responsePacketMagicHeader = it))
}, },
enabled = isAuthenticated, label = stringResource(R.string.response_packet_magic_header),
singleLine = true, hint = stringResource(R.string.response_packet_magic_header).lowercase(),
keyboardOptions = keyboardOptions, modifier = Modifier.fillMaxWidth(),
keyboardActions = keyboardActions,
) )
ConfigurationTextBox( ConfigurationTextBox(
value = interfaceState.publicKey, value = interfaceState.underloadPacketMagicHeader,
hint = onValueChange = {
stringResource(R.string.hint_template, stringResource(R.string.base64_key)) onInterfaceChange(interfaceState.copy(underloadPacketMagicHeader = it))
.lowercase(Locale.getDefault()),
onValueChange = { onInterfaceChange(interfaceState.copy(publicKey = it)) },
label = stringResource(R.string.public_key),
enabled = false,
modifier = Modifier.fillMaxWidth(),
singleLine = true,
trailing = {
IconButton(onClick = { clipboardManager.copy(interfaceState.publicKey) }) {
Icon(
Icons.Rounded.ContentCopy,
stringResource(R.string.copy_public_key),
tint = MaterialTheme.colorScheme.onSurface,
)
}
}, },
keyboardOptions = keyboardOptions, label = stringResource(R.string.underload_packet_magic_header),
keyboardActions = keyboardActions, hint = stringResource(R.string.underload_packet_magic_header).lowercase(),
)
ConfigurationTextBox(
value = interfaceState.addresses,
onValueChange = { onInterfaceChange(interfaceState.copy(addresses = it)) },
label = stringResource(R.string.addresses),
hint =
stringResource(R.string.hint_template, stringResource(R.string.comma_separated))
.lowercase(Locale.getDefault()),
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
) )
ConfigurationTextBox( ConfigurationTextBox(
value = interfaceState.listenPort, value = interfaceState.transportPacketMagicHeader,
onValueChange = { onInterfaceChange(interfaceState.copy(listenPort = it)) }, onValueChange = {
label = stringResource(R.string.listen_port), onInterfaceChange(interfaceState.copy(transportPacketMagicHeader = it))
hint = stringResource(R.string.random), },
label = stringResource(R.string.transport_packet_magic_header),
hint = stringResource(R.string.transport_packet_magic_header).lowercase(),
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
) )
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(6.dp),
) {
ConfigurationTextBox(
value = interfaceState.dnsServers,
onValueChange = { onInterfaceChange(interfaceState.copy(dnsServers = it)) },
label = stringResource(R.string.dns_servers),
hint =
stringResource(R.string.hint_template, stringResource(R.string.comma_separated))
.lowercase(Locale.getDefault()),
modifier = Modifier.weight(3f),
)
ConfigurationTextBox(
value = interfaceState.mtu,
onValueChange = { onInterfaceChange(interfaceState.copy(mtu = it)) },
label = stringResource(R.string.mtu),
hint = stringResource(R.string.auto).lowercase(Locale.getDefault()),
modifier = Modifier.weight(2f),
)
}
if (showScripts) {
ConfigurationTextBox(
value = interfaceState.preUp,
onValueChange = { onInterfaceChange(interfaceState.copy(preUp = it)) },
label = stringResource(R.string.pre_up),
hint = stringResource(R.string.hint_template, (R.string.comma_separated)),
modifier = Modifier.fillMaxWidth(),
)
ConfigurationTextBox(
value = interfaceState.postUp,
onValueChange = { onInterfaceChange(interfaceState.copy(postUp = it)) },
label = stringResource(R.string.post_up),
hint = stringResource(R.string.hint_template, (R.string.comma_separated)),
modifier = Modifier.fillMaxWidth(),
)
ConfigurationTextBox(
value = interfaceState.preDown,
onValueChange = { onInterfaceChange(interfaceState.copy(preDown = it)) },
label = stringResource(R.string.pre_down),
hint = stringResource(R.string.hint_template, (R.string.comma_separated)),
modifier = Modifier.fillMaxWidth(),
)
ConfigurationTextBox(
value = interfaceState.postDown,
onValueChange = { onInterfaceChange(interfaceState.copy(postDown = it)) },
label = stringResource(R.string.post_down),
hint = stringResource(R.string.hint_template, (R.string.comma_separated)),
modifier = Modifier.fillMaxWidth(),
)
}
if (showAmneziaValues) {
ConfigurationTextBox(
value = interfaceState.junkPacketCount,
onValueChange = { onInterfaceChange(interfaceState.copy(junkPacketCount = it)) },
label = stringResource(R.string.junk_packet_count),
hint = stringResource(R.string.hint_template, (R.string.comma_separated)),
modifier = Modifier.fillMaxWidth(),
)
ConfigurationTextBox(
value = interfaceState.junkPacketMinSize,
onValueChange = { onInterfaceChange(interfaceState.copy(junkPacketMinSize = it)) },
label = stringResource(R.string.junk_packet_minimum_size),
hint = stringResource(R.string.junk_packet_minimum_size).lowercase(),
modifier = Modifier.fillMaxWidth(),
)
ConfigurationTextBox(
value = interfaceState.junkPacketMaxSize,
onValueChange = { onInterfaceChange(interfaceState.copy(junkPacketMaxSize = it)) },
label = stringResource(R.string.junk_packet_maximum_size),
hint = stringResource(R.string.junk_packet_maximum_size).lowercase(),
modifier = Modifier.fillMaxWidth(),
)
ConfigurationTextBox(
value = interfaceState.initPacketJunkSize,
onValueChange = { onInterfaceChange(interfaceState.copy(initPacketJunkSize = it)) },
label = stringResource(R.string.init_packet_junk_size),
hint = stringResource(R.string.init_packet_junk_size).lowercase(),
modifier = Modifier.fillMaxWidth(),
)
ConfigurationTextBox(
value = interfaceState.responsePacketJunkSize,
onValueChange = {
onInterfaceChange(interfaceState.copy(responsePacketJunkSize = it))
},
label = stringResource(R.string.response_packet_junk_size),
hint = stringResource(R.string.response_packet_junk_size).lowercase(),
modifier = Modifier.fillMaxWidth(),
)
ConfigurationTextBox(
value = interfaceState.initPacketMagicHeader,
onValueChange = {
onInterfaceChange(interfaceState.copy(initPacketMagicHeader = it))
},
label = stringResource(R.string.init_packet_magic_header),
hint = stringResource(R.string.init_packet_magic_header).lowercase(),
modifier = Modifier.fillMaxWidth(),
)
ConfigurationTextBox(
value = interfaceState.responsePacketMagicHeader,
onValueChange = {
onInterfaceChange(interfaceState.copy(responsePacketMagicHeader = it))
},
label = stringResource(R.string.response_packet_magic_header),
hint = stringResource(R.string.response_packet_magic_header).lowercase(),
modifier = Modifier.fillMaxWidth(),
)
ConfigurationTextBox(
value = interfaceState.underloadPacketMagicHeader,
onValueChange = {
onInterfaceChange(interfaceState.copy(underloadPacketMagicHeader = it))
},
label = stringResource(R.string.underload_packet_magic_header),
hint = stringResource(R.string.underload_packet_magic_header).lowercase(),
modifier = Modifier.fillMaxWidth(),
)
ConfigurationTextBox(
value = interfaceState.transportPacketMagicHeader,
onValueChange = {
onInterfaceChange(interfaceState.copy(transportPacketMagicHeader = it))
},
label = stringResource(R.string.transport_packet_magic_header),
hint = stringResource(R.string.transport_packet_magic_header).lowercase(),
modifier = Modifier.fillMaxWidth(),
)
}
} }
} }
@@ -11,11 +11,10 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.zaneschepke.wireguardautotunnel.R import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.ui.common.config.ConfigurationTextBox
import com.zaneschepke.wireguardautotunnel.ui.common.label.GroupLabel import com.zaneschepke.wireguardautotunnel.ui.common.label.GroupLabel
import com.zaneschepke.wireguardautotunnel.ui.common.textbox.ConfigurationTextBox
import com.zaneschepke.wireguardautotunnel.ui.screens.main.config.ConfigViewModel import com.zaneschepke.wireguardautotunnel.ui.screens.main.config.ConfigViewModel
import com.zaneschepke.wireguardautotunnel.ui.screens.main.config.state.ConfigUiState import com.zaneschepke.wireguardautotunnel.ui.screens.main.config.state.ConfigUiState
import java.util.*
@Composable @Composable
fun InterfaceSection( fun InterfaceSection(
@@ -31,8 +30,8 @@ fun InterfaceSection(
Surface(shape = RoundedCornerShape(12.dp), color = MaterialTheme.colorScheme.surface) { Surface(shape = RoundedCornerShape(12.dp), color = MaterialTheme.colorScheme.surface) {
Column( Column(
verticalArrangement = Arrangement.spacedBy(12.dp), verticalArrangement = Arrangement.spacedBy(6.dp),
modifier = Modifier.padding(horizontal = 16.dp).focusGroup(), modifier = Modifier.padding(16.dp).focusGroup(),
) { ) {
Row( Row(
horizontalArrangement = Arrangement.SpaceBetween, horizontalArrangement = Arrangement.SpaceBetween,
@@ -56,9 +55,7 @@ fun InterfaceSection(
onValueChange = viewModel::updateTunnelName, onValueChange = viewModel::updateTunnelName,
label = stringResource(R.string.name), label = stringResource(R.string.name),
isError = isTunnelNameTaken, isError = isTunnelNameTaken,
hint = hint = stringResource(R.string.tunnel_name).lowercase(),
stringResource(R.string.hint_template, stringResource(R.string.tunnel_name))
.lowercase(Locale.getDefault()),
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
) )
InterfaceFields( InterfaceFields(
@@ -6,6 +6,7 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
@@ -16,9 +17,8 @@ import androidx.compose.ui.text.input.PasswordVisualTransformation
import androidx.compose.ui.text.input.VisualTransformation import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.zaneschepke.wireguardautotunnel.R import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.ui.common.textbox.ConfigurationTextBox import com.zaneschepke.wireguardautotunnel.ui.common.config.ConfigurationTextBox
import com.zaneschepke.wireguardautotunnel.ui.state.PeerProxy import com.zaneschepke.wireguardautotunnel.ui.state.PeerProxy
import java.util.*
@Composable @Composable
fun PeerFields( fun PeerFields(
@@ -35,32 +35,44 @@ fun PeerFields(
value = peer.publicKey, value = peer.publicKey,
onValueChange = { onPeerChange(peer.copy(publicKey = it)) }, onValueChange = { onPeerChange(peer.copy(publicKey = it)) },
label = stringResource(R.string.public_key), label = stringResource(R.string.public_key),
hint = hint = stringResource(R.string.base64_key),
stringResource(R.string.hint_template, stringResource(R.string.base64_key))
.lowercase(Locale.getDefault()),
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
) )
ConfigurationTextBox( OutlinedTextField(
visualTransformation = visualTransformation =
if (isAuthenticated) VisualTransformation.None else PasswordVisualTransformation(), if (isAuthenticated) VisualTransformation.None else PasswordVisualTransformation(),
value = peer.preSharedKey, value = peer.preSharedKey,
enabled = isAuthenticated, enabled = isAuthenticated,
hint = stringResource(R.string.optional),
onValueChange = { onPeerChange(peer.copy(preSharedKey = it)) }, onValueChange = { onPeerChange(peer.copy(preSharedKey = it)) },
label = stringResource(R.string.preshared_key), label = {
Text(
stringResource(R.string.preshared_key),
style = MaterialTheme.typography.bodyMedium,
)
},
placeholder = {
Text(stringResource(R.string.optional), style = MaterialTheme.typography.bodyMedium)
},
modifier = Modifier.fillMaxWidth().clickable { if (!isAuthenticated) showAuthPrompt() }, modifier = Modifier.fillMaxWidth().clickable { if (!isAuthenticated) showAuthPrompt() },
keyboardOptions = keyboardOptions, keyboardOptions = keyboardOptions,
keyboardActions = keyboardActions, keyboardActions = keyboardActions,
singleLine = true, singleLine = true,
) )
ConfigurationTextBox( OutlinedTextField(
value = peer.persistentKeepalive, value = peer.persistentKeepalive,
onValueChange = { onPeerChange(peer.copy(persistentKeepalive = it)) }, onValueChange = { onPeerChange(peer.copy(persistentKeepalive = it)) },
label = stringResource(R.string.persistent_keepalive), label = {
hint = stringResource(R.string.optional),
trailing = {
Text( Text(
stringResource(R.string.seconds).lowercase(Locale.getDefault()), stringResource(R.string.persistent_keepalive),
style = MaterialTheme.typography.bodyMedium,
)
},
placeholder = {
Text(stringResource(R.string.optional), style = MaterialTheme.typography.bodyMedium)
},
trailingIcon = {
Text(
stringResource(R.string.seconds),
style = MaterialTheme.typography.bodyMedium, style = MaterialTheme.typography.bodyMedium,
modifier = Modifier.padding(end = 10.dp), modifier = Modifier.padding(end = 10.dp),
) )
@@ -73,18 +85,21 @@ fun PeerFields(
value = peer.endpoint, value = peer.endpoint,
onValueChange = { onPeerChange(peer.copy(endpoint = it)) }, onValueChange = { onPeerChange(peer.copy(endpoint = it)) },
label = stringResource(R.string.endpoint), label = stringResource(R.string.endpoint),
hint = hint = stringResource(R.string.endpoint).lowercase(),
stringResource(R.string.hint_template, stringResource(R.string.server_port))
.lowercase(Locale.getDefault()),
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
) )
ConfigurationTextBox( OutlinedTextField(
value = peer.allowedIps, value = peer.allowedIps,
onValueChange = { onPeerChange(peer.copy(allowedIps = it)) }, onValueChange = { onPeerChange(peer.copy(allowedIps = it)) },
label = stringResource(R.string.allowed_ips), label = {
hint = Text(stringResource(R.string.allowed_ips), style = MaterialTheme.typography.bodyMedium)
stringResource(R.string.hint_template, stringResource(R.string.comma_separated)) },
.lowercase(Locale.getDefault()), placeholder = {
Text(
stringResource(R.string.comma_separated_list),
style = MaterialTheme.typography.bodyMedium,
)
},
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
keyboardOptions = keyboardOptions, keyboardOptions = keyboardOptions,
keyboardActions = keyboardActions, keyboardActions = keyboardActions,
@@ -27,8 +27,8 @@ fun PeersSection(uiState: ConfigUiState, viewModel: ConfigViewModel) {
Surface(shape = RoundedCornerShape(12.dp), color = MaterialTheme.colorScheme.surface) { Surface(shape = RoundedCornerShape(12.dp), color = MaterialTheme.colorScheme.surface) {
Column( Column(
verticalArrangement = Arrangement.spacedBy(12.dp), verticalArrangement = Arrangement.spacedBy(6.dp),
modifier = Modifier.padding(horizontal = 16.dp).focusGroup(), modifier = Modifier.padding(16.dp).focusGroup(),
) { ) {
Row( Row(
horizontalArrangement = Arrangement.SpaceBetween, horizontalArrangement = Arrangement.SpaceBetween,
@@ -172,7 +172,7 @@ constructor(
val (wg, am) = proxy.buildConfigs() val (wg, am) = proxy.buildConfigs()
tunnelRepository.save( tunnelRepository.save(
tunnel.copyWithCallback( tunnel.copyWithCallback(
amQuick = am.toAwgQuickString(true, false), amQuick = am.toAwgQuickString(true),
wgQuick = wg.toWgQuickString(true), wgQuick = wg.toWgQuickString(true),
) )
) )
@@ -67,7 +67,7 @@ private fun QrCodeContent(tunnelConf: TunnelConf) {
var selectedOption by remember { mutableStateOf(ConfigType.WG) } var selectedOption by remember { mutableStateOf(ConfigType.WG) }
val qrCodeText = val qrCodeText =
when (selectedOption) { when (selectedOption) {
ConfigType.AM -> tunnelConf.toAmConfig().toAwgQuickString(true, false) ConfigType.AM -> tunnelConf.toAmConfig().toAwgQuickString(true)
ConfigType.WG -> tunnelConf.toWgConfig().toWgQuickString(true) ConfigType.WG -> tunnelConf.toWgConfig().toWgQuickString(true)
} }
@@ -1,6 +1,5 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.main.tunneloptions.components package com.zaneschepke.wireguardautotunnel.ui.screens.main.tunneloptions.components
import android.util.Patterns
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.material3.Text import androidx.compose.material3.Text
@@ -10,7 +9,7 @@ import androidx.compose.ui.unit.dp
import com.zaneschepke.wireguardautotunnel.R import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConf import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConf
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SelectionItem import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SelectionItem
import com.zaneschepke.wireguardautotunnel.ui.common.textbox.SubmitConfigurationTextBox import com.zaneschepke.wireguardautotunnel.ui.common.config.SubmitConfigurationTextBox
import com.zaneschepke.wireguardautotunnel.util.extensions.isValidIpv4orIpv6Address import com.zaneschepke.wireguardautotunnel.util.extensions.isValidIpv4orIpv6Address
import com.zaneschepke.wireguardautotunnel.viewmodel.AppViewModel import com.zaneschepke.wireguardautotunnel.viewmodel.AppViewModel
import com.zaneschepke.wireguardautotunnel.viewmodel.event.AppEvent import com.zaneschepke.wireguardautotunnel.viewmodel.event.AppEvent
@@ -28,7 +27,7 @@ fun pingConfigItem(tunnelConf: TunnelConf, viewModel: AppViewModel): SelectionIt
isErrorValue = { isErrorValue = {
it?.isNotBlank() == true && it?.isNotBlank() == true &&
!it.isValidIpv4orIpv6Address() && !it.isValidIpv4orIpv6Address() &&
!Patterns.DOMAIN_NAME.matcher(it).matches() !android.util.Patterns.DOMAIN_NAME.matcher(it).matches()
}, },
supportingText = { Text(stringResource(R.string.ping_target_description)) }, supportingText = { Text(stringResource(R.string.ping_target_description)) },
onSubmit = { ip -> onSubmit = { ip ->
@@ -9,20 +9,18 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.zaneschepke.wireguardautotunnel.data.model.AppMode import com.zaneschepke.wireguardautotunnel.ui.Route
import com.zaneschepke.wireguardautotunnel.ui.common.SectionDivider import com.zaneschepke.wireguardautotunnel.ui.common.SectionDivider
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SurfaceSelectionGroupButton import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SurfaceSelectionGroupButton
import com.zaneschepke.wireguardautotunnel.ui.navigation.LocalIsAndroidTV import com.zaneschepke.wireguardautotunnel.ui.navigation.LocalIsAndroidTV
import com.zaneschepke.wireguardautotunnel.ui.navigation.LocalNavController import com.zaneschepke.wireguardautotunnel.ui.navigation.LocalNavController
import com.zaneschepke.wireguardautotunnel.ui.screens.autotunnel.components.AdvancedSettingsItem
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.components.* import com.zaneschepke.wireguardautotunnel.ui.screens.settings.components.*
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.proxy.compoents.BackendModeBottomSheet
import com.zaneschepke.wireguardautotunnel.ui.state.AppUiState import com.zaneschepke.wireguardautotunnel.ui.state.AppUiState
import com.zaneschepke.wireguardautotunnel.ui.state.AppViewState import com.zaneschepke.wireguardautotunnel.ui.state.AppViewState
import com.zaneschepke.wireguardautotunnel.viewmodel.AppViewModel import com.zaneschepke.wireguardautotunnel.viewmodel.AppViewModel
@@ -35,28 +33,12 @@ fun SettingsScreen(uiState: AppUiState, appViewState: AppViewState, viewModel: A
val interactionSource = remember { MutableInteractionSource() } val interactionSource = remember { MutableInteractionSource() }
val showBackupBottomSheet by when (appViewState.bottomSheet) {
remember(appViewState.bottomSheet) { AppViewState.BottomSheet.BACKUP_AND_RESTORE -> {
derivedStateOf { SettingsBottomSheet(viewModel)
appViewState.bottomSheet == AppViewState.BottomSheet.BACKUP_AND_RESTORE
}
} }
val showBottomSheet by else -> Unit
remember(appViewState.bottomSheet) { }
derivedStateOf { appViewState.bottomSheet == AppViewState.BottomSheet.BACKEND }
}
val showProxySettings by
remember(uiState.appSettings.appMode) {
derivedStateOf {
when (uiState.appSettings.appMode) {
AppMode.PROXY -> true
else -> false
}
}
}
if (showBackupBottomSheet) BackupBottomSheet(viewModel)
if (showBottomSheet) BackendModeBottomSheet(uiState.appSettings, viewModel)
Column( Column(
horizontalAlignment = Alignment.Start, horizontalAlignment = Alignment.Start,
@@ -78,23 +60,17 @@ fun SettingsScreen(uiState: AppUiState, appViewState: AppViewState, viewModel: A
} }
), ),
) { ) {
SurfaceSelectionGroupButton(buildList { add(backendModeItem(uiState, viewModel)) })
SectionDivider()
SurfaceSelectionGroupButton( SurfaceSelectionGroupButton(
items = items =
buildList { buildList {
if (uiState.appSettings.appMode == AppMode.LOCK_DOWN) {
add(lanTrafficItem(uiState, viewModel))
}
add(tunnelMonitoringItem()) add(tunnelMonitoringItem())
add(dnsSettingsItem()) add(appShortcutsItem(uiState, viewModel))
// TODO changing these settings won't work in certain app states if (!isTv) add(alwaysOnVpnItem(uiState, viewModel))
if (showProxySettings) add(proxYSettingsItem()) add(killSwitchItem())
add(RestartAtBootItem(uiState, viewModel))
} }
) )
SectionDivider() SectionDivider()
SurfaceSelectionGroupButton(listOf(systemFeaturesItem()))
SectionDivider()
SurfaceSelectionGroupButton( SurfaceSelectionGroupButton(
items = items =
buildList { buildList {
@@ -104,5 +80,18 @@ fun SettingsScreen(uiState: AppUiState, appViewState: AppViewState, viewModel: A
add(PinLockItem(uiState, viewModel)) add(PinLockItem(uiState, viewModel))
} }
) )
SectionDivider()
if (!isTv) {
SurfaceSelectionGroupButton(items = listOf(kernelModeItem(uiState, viewModel)))
SectionDivider()
}
SurfaceSelectionGroupButton(
items =
listOf(
AdvancedSettingsItem(
onClick = { navController.navigate(Route.SettingsAdvanced) }
)
)
)
} }
} }
@@ -1,5 +1,6 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.settings.system package com.zaneschepke.wireguardautotunnel.ui.screens.settings.advanced
import android.view.WindowManager
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
@@ -7,23 +8,33 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.zaneschepke.wireguardautotunnel.ui.common.SectionDivider import com.zaneschepke.wireguardautotunnel.MainActivity
import com.zaneschepke.wireguardautotunnel.ui.common.SecureScreenFromRecording
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SurfaceSelectionGroupButton import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SurfaceSelectionGroupButton
import com.zaneschepke.wireguardautotunnel.ui.navigation.LocalIsAndroidTV import com.zaneschepke.wireguardautotunnel.ui.screens.settings.advanced.components.RemoteControlItem
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.system.components.*
import com.zaneschepke.wireguardautotunnel.ui.state.AppUiState import com.zaneschepke.wireguardautotunnel.ui.state.AppUiState
import com.zaneschepke.wireguardautotunnel.viewmodel.AppViewModel import com.zaneschepke.wireguardautotunnel.viewmodel.AppViewModel
@Composable @Composable
fun SystemFeaturesScreen(appUiState: AppUiState, viewModel: AppViewModel) { fun SettingsAdvancedScreen(appUiState: AppUiState, viewModel: AppViewModel) {
val context = LocalContext.current
val isTv = LocalIsAndroidTV.current val activity = context as? MainActivity
SecureScreenFromRecording() // Secure screen due to sensitive information
DisposableEffect(Unit) {
activity
?.window
?.setFlags(
WindowManager.LayoutParams.FLAG_SECURE,
WindowManager.LayoutParams.FLAG_SECURE,
)
onDispose { activity?.window?.clearFlags(WindowManager.LayoutParams.FLAG_SECURE) }
}
Column( Column(
horizontalAlignment = Alignment.Start, horizontalAlignment = Alignment.Start,
@@ -34,20 +45,6 @@ fun SystemFeaturesScreen(appUiState: AppUiState, viewModel: AppViewModel) {
.padding(vertical = 24.dp) .padding(vertical = 24.dp)
.padding(horizontal = 12.dp), .padding(horizontal = 12.dp),
) { ) {
SurfaceSelectionGroupButton(buildList { if (!isTv) add(nativeKillSwitchItem()) }) SurfaceSelectionGroupButton(listOf(RemoteControlItem(appUiState, viewModel)))
SectionDivider()
SurfaceSelectionGroupButton(
buildList {
add(restartAtBootItem(appUiState, viewModel))
if (!isTv) add(alwaysOnVpnItem(appUiState, viewModel))
}
)
SectionDivider()
SurfaceSelectionGroupButton(
buildList {
add(appShortcutsItem(appUiState, viewModel))
add(remoteControlItem(appUiState, viewModel))
}
)
} }
} }
@@ -1,4 +1,4 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.settings.system.components package com.zaneschepke.wireguardautotunnel.ui.screens.settings.advanced.components
import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
@@ -20,7 +20,7 @@ import com.zaneschepke.wireguardautotunnel.viewmodel.AppViewModel
import com.zaneschepke.wireguardautotunnel.viewmodel.event.AppEvent import com.zaneschepke.wireguardautotunnel.viewmodel.event.AppEvent
@Composable @Composable
fun remoteControlItem(uiState: AppUiState, viewModel: AppViewModel): SelectionItem { fun RemoteControlItem(uiState: AppUiState, viewModel: AppViewModel): SelectionItem {
val clipboardManager = rememberClipboardHelper() val clipboardManager = rememberClipboardHelper()
return SelectionItem( return SelectionItem(
@@ -1,46 +0,0 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.settings.components
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Restore
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.res.vectorResource
import com.zaneschepke.wireguardautotunnel.MainActivity
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.ui.common.sheet.CustomBottomSheet
import com.zaneschepke.wireguardautotunnel.ui.common.sheet.SheetOption
import com.zaneschepke.wireguardautotunnel.ui.state.AppViewState
import com.zaneschepke.wireguardautotunnel.viewmodel.AppViewModel
import com.zaneschepke.wireguardautotunnel.viewmodel.event.AppEvent
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun BackupBottomSheet(viewModel: AppViewModel) {
val context = LocalContext.current
CustomBottomSheet(
listOf(
SheetOption(
ImageVector.vectorResource(R.drawable.database),
stringResource(R.string.backup_application),
onClick = {
viewModel.handleEvent(AppEvent.SetBottomSheet(AppViewState.BottomSheet.NONE))
(context as? MainActivity)?.performBackup()
},
),
SheetOption(
Icons.Outlined.Restore,
stringResource(R.string.restore_application),
onClick = {
viewModel.handleEvent(AppEvent.SetBottomSheet(AppViewState.BottomSheet.NONE))
(context as? MainActivity)?.performRestore()
},
),
)
) {
viewModel.handleEvent(AppEvent.SetBottomSheet(AppViewState.BottomSheet.NONE))
}
}
@@ -1,7 +1,7 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.settings.components package com.zaneschepke.wireguardautotunnel.ui.screens.settings.components
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Android import androidx.compose.material.icons.outlined.VpnKeyOff
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
@@ -14,18 +14,18 @@ import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SelectionIte
import com.zaneschepke.wireguardautotunnel.ui.navigation.LocalNavController import com.zaneschepke.wireguardautotunnel.ui.navigation.LocalNavController
@Composable @Composable
fun systemFeaturesItem(): SelectionItem { fun killSwitchItem(): SelectionItem {
val navController = LocalNavController.current val navController = LocalNavController.current
return SelectionItem( return SelectionItem(
leading = { Icon(Icons.Outlined.Android, null) }, leading = { Icon(Icons.Outlined.VpnKeyOff, contentDescription = null) },
trailing = { ForwardButton { navController.navigate(Route.SystemFeatures) } },
title = { title = {
Text( Text(
text = stringResource(R.string.system_features), text = stringResource(R.string.kill_switch_options),
style = style =
MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface), MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface),
) )
}, },
onClick = { navController.navigate(Route.SystemFeatures) }, trailing = { ForwardButton { navController.navigate(Route.KillSwitch) } },
onClick = { navController.navigate(Route.KillSwitch) },
) )
} }
@@ -1,4 +1,4 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.settings.system.components package com.zaneschepke.wireguardautotunnel.ui.screens.settings.components
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Restore import androidx.compose.material.icons.outlined.Restore
@@ -15,7 +15,7 @@ import com.zaneschepke.wireguardautotunnel.viewmodel.AppViewModel
import com.zaneschepke.wireguardautotunnel.viewmodel.event.AppEvent import com.zaneschepke.wireguardautotunnel.viewmodel.event.AppEvent
@Composable @Composable
fun restartAtBootItem(uiState: AppUiState, viewModel: AppViewModel): SelectionItem { fun RestartAtBootItem(uiState: AppUiState, viewModel: AppViewModel): SelectionItem {
return SelectionItem( return SelectionItem(
leading = { Icon(Icons.Outlined.Restore, contentDescription = null) }, leading = { Icon(Icons.Outlined.Restore, contentDescription = null) },
trailing = { trailing = {
@@ -0,0 +1,78 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.settings.components
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Restore
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.unit.dp
import com.zaneschepke.wireguardautotunnel.MainActivity
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.ui.state.AppViewState
import com.zaneschepke.wireguardautotunnel.viewmodel.AppViewModel
import com.zaneschepke.wireguardautotunnel.viewmodel.event.AppEvent
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun SettingsBottomSheet(viewModel: AppViewModel) {
val context = LocalContext.current
ModalBottomSheet(
containerColor = MaterialTheme.colorScheme.surface,
onDismissRequest = {
viewModel.handleEvent(AppEvent.SetBottomSheet(AppViewState.BottomSheet.NONE))
},
) {
Row(
modifier =
Modifier.fillMaxWidth()
.clickable {
viewModel.handleEvent(
AppEvent.SetBottomSheet(AppViewState.BottomSheet.NONE)
)
(context as? MainActivity)?.performBackup()
}
.padding(10.dp)
) {
Icon(
imageVector = ImageVector.vectorResource(R.drawable.database),
contentDescription = null,
modifier = Modifier.padding(10.dp),
)
Text(
text = stringResource(R.string.backup_application),
modifier = Modifier.padding(10.dp),
)
}
HorizontalDivider()
Row(
modifier =
Modifier.fillMaxWidth()
.clickable {
viewModel.handleEvent(
AppEvent.SetBottomSheet(AppViewState.BottomSheet.NONE)
)
(context as? MainActivity)?.performRestore()
}
.padding(10.dp)
) {
Icon(
imageVector = Icons.Outlined.Restore,
contentDescription = null,
modifier = Modifier.padding(10.dp),
)
Text(
text = stringResource(R.string.restore_application),
modifier = Modifier.padding(10.dp),
)
}
}
}
@@ -1,4 +1,4 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.settings.system.components package com.zaneschepke.wireguardautotunnel.ui.screens.settings.components
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.VpnLock import androidx.compose.material.icons.outlined.VpnLock
@@ -1,4 +1,4 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.settings.system.components package com.zaneschepke.wireguardautotunnel.ui.screens.settings.components
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.AppShortcut import androidx.compose.material.icons.filled.AppShortcut
@@ -1,45 +0,0 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.settings.components
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.ExpandMore
import androidx.compose.material3.Icon
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.res.vectorResource
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SelectionItem
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SelectionItemLabel
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SelectionLabelType
import com.zaneschepke.wireguardautotunnel.ui.state.AppUiState
import com.zaneschepke.wireguardautotunnel.ui.state.AppViewState
import com.zaneschepke.wireguardautotunnel.util.extensions.asTitleString
import com.zaneschepke.wireguardautotunnel.viewmodel.AppViewModel
import com.zaneschepke.wireguardautotunnel.viewmodel.event.AppEvent
@Composable
fun backendModeItem(uiState: AppUiState, viewModel: AppViewModel): SelectionItem {
val context = LocalContext.current
return SelectionItem(
leading = { Icon(ImageVector.vectorResource(R.drawable.sdk), contentDescription = null) },
trailing = {
Icon(Icons.Outlined.ExpandMore, contentDescription = stringResource(R.string.select))
},
title = {
SelectionItemLabel(stringResource(R.string.backend_mode), SelectionLabelType.TITLE)
},
description = {
SelectionItemLabel(
stringResource(
R.string.current_template,
uiState.appSettings.appMode.asTitleString(context),
),
SelectionLabelType.DESCRIPTION,
)
},
onClick = {
viewModel.handleEvent(AppEvent.SetBottomSheet(AppViewState.BottomSheet.BACKEND))
},
)
}
@@ -0,0 +1,46 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.settings.components
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Code
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.res.stringResource
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.ui.common.button.ScaledSwitch
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SelectionItem
import com.zaneschepke.wireguardautotunnel.ui.state.AppUiState
import com.zaneschepke.wireguardautotunnel.viewmodel.AppViewModel
import com.zaneschepke.wireguardautotunnel.viewmodel.event.AppEvent
@Composable
fun kernelModeItem(uiState: AppUiState, viewModel: AppViewModel): SelectionItem {
return SelectionItem(
leading = { Icon(Icons.Outlined.Code, contentDescription = null) },
title = {
Text(
text = stringResource(R.string.kernel),
style =
MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface),
)
},
description = {
Text(
text = stringResource(R.string.use_kernel),
style = MaterialTheme.typography.bodySmall.copy(MaterialTheme.colorScheme.outline),
)
},
trailing = {
ScaledSwitch(
checked = uiState.appSettings.isKernelEnabled,
enabled =
!(uiState.appSettings.isAutoTunnelEnabled ||
uiState.appSettings.isAlwaysOnVpnEnabled ||
uiState.activeTunnels.isNotEmpty()),
onClick = { viewModel.handleEvent(AppEvent.ToggleKernelMode) },
)
},
onClick = { viewModel.handleEvent(AppEvent.ToggleKernelMode) },
)
}
@@ -1,31 +0,0 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.settings.components
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.res.vectorResource
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.ui.Route
import com.zaneschepke.wireguardautotunnel.ui.common.button.ForwardButton
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SelectionItem
import com.zaneschepke.wireguardautotunnel.ui.navigation.LocalNavController
@Composable
fun proxYSettingsItem(): SelectionItem {
val navController = LocalNavController.current
return SelectionItem(
leading = { Icon(ImageVector.vectorResource(R.drawable.proxy), null) },
trailing = { ForwardButton { navController.navigate(Route.ProxySettings) } },
title = {
Text(
text = stringResource(R.string.proxy_settings),
style =
MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface),
)
},
onClick = { navController.navigate(Route.ProxySettings) },
)
}
@@ -1,83 +0,0 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.settings.dns
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Cloud
import androidx.compose.material.icons.outlined.Dns
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.data.model.DnsProtocol
import com.zaneschepke.wireguardautotunnel.data.model.DnsProvider
import com.zaneschepke.wireguardautotunnel.ui.common.dropdown.LabelledDropdown
import com.zaneschepke.wireguardautotunnel.ui.state.AppUiState
import com.zaneschepke.wireguardautotunnel.viewmodel.AppViewModel
import com.zaneschepke.wireguardautotunnel.viewmodel.event.AppEvent
@Composable
fun DnsSettingsScreen(uiState: AppUiState, viewModel: AppViewModel) {
val context = LocalContext.current
Column(
horizontalAlignment = Alignment.Start,
verticalArrangement = Arrangement.spacedBy(12.dp, Alignment.Top),
modifier =
Modifier.fillMaxSize()
.verticalScroll(rememberScrollState())
.padding(vertical = 24.dp)
.padding(horizontal = 12.dp),
) {
LabelledDropdown(
title = {
Text(
text = stringResource(R.string.dns_protocol),
style =
MaterialTheme.typography.bodyMedium.copy(
color = MaterialTheme.colorScheme.onSurface
),
)
},
leading = { Icon(Icons.Outlined.Dns, contentDescription = null) },
currentValue = uiState.appSettings.dnsProtocol,
onSelected = { selected ->
selected?.let { viewModel.handleEvent(AppEvent.SetDnsProtocol(it)) }
},
options = DnsProtocol.entries,
optionToString = { (it ?: DnsProtocol.SYSTEM).asString(context) },
)
AnimatedVisibility(uiState.appSettings.dnsProtocol != DnsProtocol.SYSTEM) {
LabelledDropdown(
title = {
Text(
text = stringResource(R.string.dns_provider),
style =
MaterialTheme.typography.bodyMedium.copy(
color = MaterialTheme.colorScheme.onSurface
),
)
},
leading = { Icon(Icons.Outlined.Cloud, contentDescription = null) },
currentValue =
uiState.appSettings.dnsEndpoint?.let { DnsProvider.fromAddress(it) }
?: DnsProvider.CLOUDFLARE,
onSelected = { selected ->
selected?.let { viewModel.handleEvent(AppEvent.SetDnsProvider(it)) }
},
options = DnsProvider.entries,
optionToString = { it?.name ?: DnsProvider.CLOUDFLARE.name },
)
}
}
}
@@ -0,0 +1,50 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.settings.killswitch
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.zaneschepke.wireguardautotunnel.ui.common.SectionDivider
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SurfaceSelectionGroupButton
import com.zaneschepke.wireguardautotunnel.ui.navigation.LocalIsAndroidTV
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.killswitch.components.LanTrafficItem
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.killswitch.components.VpnKillSwitchItem
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.killswitch.components.nativeKillSwitchItem
import com.zaneschepke.wireguardautotunnel.ui.state.AppUiState
import com.zaneschepke.wireguardautotunnel.viewmodel.AppViewModel
import com.zaneschepke.wireguardautotunnel.viewmodel.event.AppEvent
@Composable
fun KillSwitchScreen(uiState: AppUiState, viewModel: AppViewModel) {
val isTv = LocalIsAndroidTV.current
Column(
horizontalAlignment = Alignment.Start,
verticalArrangement = Arrangement.spacedBy(12.dp, Alignment.Top),
modifier = Modifier.fillMaxSize().padding(vertical = 24.dp).padding(horizontal = 12.dp),
) {
if (!isTv) {
SurfaceSelectionGroupButton(items = listOf(nativeKillSwitchItem()))
SectionDivider()
}
SurfaceSelectionGroupButton(
items =
buildList {
if (!uiState.appSettings.isKernelEnabled) {
add(
VpnKillSwitchItem(uiState) {
viewModel.handleEvent(AppEvent.ToggleVpnKillSwitch)
}
)
if (uiState.appSettings.isVpnKillSwitchEnabled) {
add(LanTrafficItem(uiState, viewModel))
}
}
}
)
}
}
@@ -1,4 +1,4 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.settings.components package com.zaneschepke.wireguardautotunnel.ui.screens.settings.killswitch.components
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Lan import androidx.compose.material.icons.outlined.Lan
@@ -15,7 +15,7 @@ import com.zaneschepke.wireguardautotunnel.viewmodel.AppViewModel
import com.zaneschepke.wireguardautotunnel.viewmodel.event.AppEvent import com.zaneschepke.wireguardautotunnel.viewmodel.event.AppEvent
@Composable @Composable
fun lanTrafficItem(uiState: AppUiState, viewModel: AppViewModel): SelectionItem { fun LanTrafficItem(uiState: AppUiState, viewModel: AppViewModel): SelectionItem {
return SelectionItem( return SelectionItem(
leading = { Icon(Icons.Outlined.Lan, contentDescription = null) }, leading = { Icon(Icons.Outlined.Lan, contentDescription = null) },
title = { title = {
@@ -1,31 +1,34 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.settings.components package com.zaneschepke.wireguardautotunnel.ui.screens.settings.killswitch.components
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Dns import androidx.compose.material.icons.outlined.VpnKey
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import com.zaneschepke.wireguardautotunnel.R import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.ui.Route import com.zaneschepke.wireguardautotunnel.ui.common.button.ScaledSwitch
import com.zaneschepke.wireguardautotunnel.ui.common.button.ForwardButton
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SelectionItem import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SelectionItem
import com.zaneschepke.wireguardautotunnel.ui.navigation.LocalNavController import com.zaneschepke.wireguardautotunnel.ui.state.AppUiState
@Composable @Composable
fun dnsSettingsItem(): SelectionItem { fun VpnKillSwitchItem(uiState: AppUiState, toggleVpnSwitch: () -> Unit): SelectionItem {
val navController = LocalNavController.current
return SelectionItem( return SelectionItem(
leading = { Icon(Icons.Outlined.Dns, null) }, leading = { Icon(Icons.Outlined.VpnKey, contentDescription = null) },
trailing = { ForwardButton { navController.navigate(Route.Dns) } },
title = { title = {
Text( Text(
text = stringResource(R.string.dns_settings), text = stringResource(R.string.vpn_kill_switch),
style = style =
MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface), MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface),
) )
}, },
onClick = { navController.navigate(Route.Dns) }, trailing = {
ScaledSwitch(
checked = uiState.appSettings.isVpnKillSwitchEnabled,
onClick = { toggleVpnSwitch() },
)
},
onClick = { toggleVpnSwitch() },
) )
} }
@@ -1,4 +1,4 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.settings.system.components package com.zaneschepke.wireguardautotunnel.ui.screens.settings.killswitch.components
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.AdminPanelSettings import androidx.compose.material.icons.outlined.AdminPanelSettings
@@ -1,14 +1,18 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.settings.logs.components package com.zaneschepke.wireguardautotunnel.ui.screens.settings.logs.components
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Delete import androidx.compose.material.icons.filled.Delete
import androidx.compose.material.icons.outlined.FolderZip import androidx.compose.material.icons.filled.FolderZip
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.*
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.zaneschepke.wireguardautotunnel.R import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.ui.common.sheet.CustomBottomSheet
import com.zaneschepke.wireguardautotunnel.ui.common.sheet.SheetOption
import com.zaneschepke.wireguardautotunnel.ui.state.AppViewState import com.zaneschepke.wireguardautotunnel.ui.state.AppViewState
import com.zaneschepke.wireguardautotunnel.viewmodel.AppViewModel import com.zaneschepke.wireguardautotunnel.viewmodel.AppViewModel
import com.zaneschepke.wireguardautotunnel.viewmodel.event.AppEvent import com.zaneschepke.wireguardautotunnel.viewmodel.event.AppEvent
@@ -16,26 +20,48 @@ import com.zaneschepke.wireguardautotunnel.viewmodel.event.AppEvent
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun LogsBottomSheet(viewModel: AppViewModel) { fun LogsBottomSheet(viewModel: AppViewModel) {
CustomBottomSheet( ModalBottomSheet(
listOf( containerColor = MaterialTheme.colorScheme.surface,
SheetOption( onDismissRequest = {
Icons.Outlined.FolderZip, viewModel.handleEvent(AppEvent.SetBottomSheet(AppViewState.BottomSheet.NONE))
stringResource(R.string.export_logs), },
onClick = {
viewModel.handleEvent(AppEvent.SetBottomSheet(AppViewState.BottomSheet.NONE))
viewModel.handleEvent(AppEvent.ExportLogs)
},
),
SheetOption(
Icons.Outlined.Delete,
stringResource(R.string.delete_logs),
onClick = {
viewModel.handleEvent(AppEvent.SetBottomSheet(AppViewState.BottomSheet.NONE))
viewModel.handleEvent(AppEvent.DeleteLogs)
},
),
)
) { ) {
viewModel.handleEvent(AppEvent.SetBottomSheet(AppViewState.BottomSheet.NONE)) Row(
modifier =
Modifier.fillMaxWidth()
.clickable {
viewModel.handleEvent(
AppEvent.SetBottomSheet(AppViewState.BottomSheet.NONE)
)
viewModel.handleEvent(AppEvent.ExportLogs)
}
.padding(10.dp)
) {
Icon(
imageVector = Icons.Filled.FolderZip,
contentDescription = stringResource(R.string.export_logs),
modifier = Modifier.padding(10.dp),
)
Text(text = stringResource(R.string.export_logs), modifier = Modifier.padding(10.dp))
}
HorizontalDivider()
Row(
modifier =
Modifier.fillMaxWidth()
.clickable {
viewModel.handleEvent(
AppEvent.SetBottomSheet(AppViewState.BottomSheet.NONE)
)
viewModel.handleEvent(AppEvent.DeleteLogs)
}
.padding(10.dp)
) {
Icon(
imageVector = Icons.Filled.Delete,
contentDescription = stringResource(R.string.delete_logs),
modifier = Modifier.padding(10.dp),
)
Text(text = stringResource(R.string.delete_logs), modifier = Modifier.padding(10.dp))
}
} }
} }
@@ -20,7 +20,7 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.zaneschepke.wireguardautotunnel.R import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SurfaceSelectionGroupButton import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SurfaceSelectionGroupButton
import com.zaneschepke.wireguardautotunnel.ui.common.dropdown.LabelledDropdown import com.zaneschepke.wireguardautotunnel.ui.common.dropdown.LabelledNumberDropdown
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.monitoring.components.detailedPingStatsItem import com.zaneschepke.wireguardautotunnel.ui.screens.settings.monitoring.components.detailedPingStatsItem
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.monitoring.components.enablePingMonitoringItem import com.zaneschepke.wireguardautotunnel.ui.screens.settings.monitoring.components.enablePingMonitoringItem
import com.zaneschepke.wireguardautotunnel.ui.state.AppUiState import com.zaneschepke.wireguardautotunnel.ui.state.AppUiState
@@ -51,7 +51,7 @@ fun TunnelMonitoringScreen(uiState: AppUiState, viewModel: AppViewModel) {
) { ) {
SurfaceSelectionGroupButton(listOf(enablePingMonitoringItem(uiState, viewModel))) SurfaceSelectionGroupButton(listOf(enablePingMonitoringItem(uiState, viewModel)))
if (uiState.appSettings.isPingEnabled) { if (uiState.appSettings.isPingEnabled) {
LabelledDropdown( LabelledNumberDropdown(
title = { title = {
Text( Text(
text = stringResource(R.string.tunnel_ping_interval), text = stringResource(R.string.tunnel_ping_interval),
@@ -67,9 +67,8 @@ fun TunnelMonitoringScreen(uiState: AppUiState, viewModel: AppViewModel) {
viewModel.handleEvent(AppEvent.SetPingInterval(selected!!)) viewModel.handleEvent(AppEvent.SetPingInterval(selected!!))
}, },
options = (10..60).step(10).toList(), options = (10..60).step(10).toList(),
optionToString = { it?.toString() ?: stringResource(R.string._default) },
) )
LabelledDropdown( LabelledNumberDropdown(
title = { title = {
Text( Text(
text = stringResource(R.string.attempts_per_interval), text = stringResource(R.string.attempts_per_interval),
@@ -85,9 +84,8 @@ fun TunnelMonitoringScreen(uiState: AppUiState, viewModel: AppViewModel) {
viewModel.handleEvent(AppEvent.SetPingAttempts(selected!!)) viewModel.handleEvent(AppEvent.SetPingAttempts(selected!!))
}, },
options = (1..5).toList(), options = (1..5).toList(),
optionToString = { it?.toString() ?: stringResource(R.string._default) },
) )
LabelledDropdown( LabelledNumberDropdown(
title = { title = {
Text( Text(
text = stringResource(R.string.ping_timeout), text = stringResource(R.string.ping_timeout),
@@ -112,7 +110,6 @@ fun TunnelMonitoringScreen(uiState: AppUiState, viewModel: AppViewModel) {
viewModel.handleEvent(AppEvent.SetPingTimeout(selected)) viewModel.handleEvent(AppEvent.SetPingTimeout(selected))
}, },
options = (10..20).toList() + null, options = (10..20).toList() + null,
optionToString = { it?.toString() ?: stringResource(R.string._default) },
) )
SurfaceSelectionGroupButton(listOf(detailedPingStatsItem(uiState, viewModel))) SurfaceSelectionGroupButton(listOf(detailedPingStatsItem(uiState, viewModel)))
} }
@@ -1,244 +0,0 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.settings.proxy
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Forward5
import androidx.compose.material.icons.outlined.Http
import androidx.compose.material.icons.outlined.RemoveRedEye
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
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.PasswordVisualTransformation
import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.unit.dp
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.domain.model.AppProxySettings
import com.zaneschepke.wireguardautotunnel.ui.common.SecureScreenFromRecording
import com.zaneschepke.wireguardautotunnel.ui.common.button.ScaledSwitch
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SelectionItem
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SelectionItemLabel
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SelectionLabelType
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SurfaceSelectionGroupButton
import com.zaneschepke.wireguardautotunnel.ui.common.label.GroupLabel
import com.zaneschepke.wireguardautotunnel.ui.common.textbox.ConfigurationTextBox
import com.zaneschepke.wireguardautotunnel.ui.state.AppUiState
import com.zaneschepke.wireguardautotunnel.util.StringValue
import com.zaneschepke.wireguardautotunnel.util.extensions.isValidProxyBindAddress
import com.zaneschepke.wireguardautotunnel.viewmodel.AppViewModel
import com.zaneschepke.wireguardautotunnel.viewmodel.event.AppEvent
@Composable
fun ProxySettingsScreen(uiState: AppUiState, viewModel: AppViewModel) {
val keyboardController = LocalSoftwareKeyboardController.current
var socks5ProxyEnabled by rememberSaveable {
mutableStateOf(uiState.proxySettings.socks5ProxyEnabled)
}
var httpProxyEnabled by rememberSaveable {
mutableStateOf(uiState.proxySettings.httpProxyEnabled)
}
val showAuthSettings by
remember(httpProxyEnabled, socks5ProxyEnabled) {
derivedStateOf { httpProxyEnabled || socks5ProxyEnabled }
}
var socks5bindAddress by
rememberSaveable(uiState.proxySettings.socks5ProxyBindAddress) {
mutableStateOf(uiState.proxySettings.socks5ProxyBindAddress)
}
var httpBindAddress by
rememberSaveable(uiState.proxySettings.httpProxyBindAddress) {
mutableStateOf(uiState.proxySettings.httpProxyBindAddress)
}
val isSocks5BindAddressError by
remember(socks5bindAddress) {
derivedStateOf {
socks5bindAddress.let { it?.isNotBlank() == true && !it.isValidProxyBindAddress() }
}
}
val isHttpBindAddressError by
remember(httpBindAddress) {
derivedStateOf {
httpBindAddress.let { it?.isNotBlank() == true && !it.isValidProxyBindAddress() }
}
}
var username by rememberSaveable { mutableStateOf(uiState.proxySettings.proxyUsername ?: "") }
var password by rememberSaveable { mutableStateOf(uiState.proxySettings.proxyPassword ?: "") }
val keyboardActions = KeyboardActions(onDone = { keyboardController?.hide() })
val keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done)
var passwordVisibility by rememberSaveable { mutableStateOf(false) }
var usernameError by rememberSaveable { mutableStateOf(false) }
var passwordError by rememberSaveable { mutableStateOf(false) }
LaunchedEffect(username) { if (username.isNotBlank() && usernameError) usernameError = false }
LaunchedEffect(password) { if (password.isNotBlank() && passwordError) passwordError = false }
LaunchedEffect(Unit) {
viewModel.handleEvent(
AppEvent.SetScreenAction {
keyboardController?.hide()
if (username.isBlank() && password.isNotBlank()) {
usernameError = true
return@SetScreenAction
}
if (password.isBlank() && username.isNotBlank()) {
passwordError = true
return@SetScreenAction
}
if (isSocks5BindAddressError || isHttpBindAddressError) return@SetScreenAction
keyboardController?.hide()
viewModel.handleEvent(
AppEvent.SetProxySettings(
socks5ProxyEnabled,
httpProxyEnabled,
httpBindAddress,
socks5bindAddress,
username,
password,
)
)
viewModel.handleEvent(
AppEvent.ShowMessage(StringValue.StringResource(R.string.config_changes_saved))
)
viewModel.handleEvent(AppEvent.PopBackStack(true))
}
)
}
SecureScreenFromRecording()
Column(
horizontalAlignment = Alignment.Start,
verticalArrangement = Arrangement.spacedBy(24.dp, Alignment.Top),
modifier = Modifier.fillMaxSize().padding(top = 24.dp).padding(horizontal = 12.dp),
) {
SurfaceSelectionGroupButton(
listOf(
SelectionItem(
leading = { Icon(Icons.Outlined.Forward5, contentDescription = null) },
title = {
SelectionItemLabel(
stringResource(R.string.socks_5_proxy),
SelectionLabelType.TITLE,
)
},
trailing = {
ScaledSwitch(
checked = socks5ProxyEnabled,
onClick = { socks5ProxyEnabled = !socks5ProxyEnabled },
)
},
onClick = { socks5ProxyEnabled = !socks5ProxyEnabled },
)
)
)
if (socks5ProxyEnabled) {
ConfigurationTextBox(
hint =
stringResource(
R.string.defaults_to_template,
AppProxySettings.DEFAULT_SOCKS_BIND_ADDRESS,
),
label = stringResource(R.string.socks_5_bind_address),
value = socks5bindAddress ?: "",
isError = isSocks5BindAddressError,
onValueChange = { socks5bindAddress = it },
)
}
SurfaceSelectionGroupButton(
listOf(
SelectionItem(
leading = { Icon(Icons.Outlined.Http, contentDescription = null) },
title = {
SelectionItemLabel(
stringResource(R.string.http_proxy),
SelectionLabelType.TITLE,
)
},
trailing = {
ScaledSwitch(
checked = httpProxyEnabled,
onClick = { httpProxyEnabled = !httpProxyEnabled },
)
},
onClick = { httpProxyEnabled = !httpProxyEnabled },
)
)
)
if (httpProxyEnabled) {
ConfigurationTextBox(
hint =
stringResource(
R.string.defaults_to_template,
AppProxySettings.DEFAULT_HTTP_BIND_ADDRESS,
),
label = stringResource(R.string.http_bind_address),
value = httpBindAddress ?: "",
isError = isHttpBindAddressError,
onValueChange = { httpBindAddress = it },
)
}
if (showAuthSettings) {
Column(
horizontalAlignment = Alignment.Start,
verticalArrangement = Arrangement.spacedBy(24.dp, Alignment.Top),
modifier = Modifier.padding(horizontal = 12.dp),
) {
GroupLabel(
stringResource(
R.string.recommended_template,
stringResource((R.string.proxy_credentials)),
)
)
ConfigurationTextBox(
value = username,
onValueChange = { username = it },
label = stringResource(R.string.username),
isError = usernameError,
hint = "",
keyboardActions = keyboardActions,
keyboardOptions = keyboardOptions,
modifier = Modifier.fillMaxWidth(),
)
ConfigurationTextBox(
value = password,
onValueChange = { password = it },
label = stringResource(R.string.password),
isError = passwordError,
hint = "",
keyboardActions = keyboardActions,
keyboardOptions = keyboardOptions,
modifier = Modifier.fillMaxWidth(),
trailing = {
IconButton(onClick = { passwordVisibility = !passwordVisibility }) {
Icon(
Icons.Outlined.RemoveRedEye,
stringResource(R.string.show_password),
)
}
},
visualTransformation =
if (!passwordVisibility) PasswordVisualTransformation()
else VisualTransformation.None,
)
}
}
}
}
@@ -1,34 +0,0 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.settings.proxy.compoents
import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.LocalContext
import com.zaneschepke.wireguardautotunnel.data.model.AppMode
import com.zaneschepke.wireguardautotunnel.domain.model.AppSettings
import com.zaneschepke.wireguardautotunnel.ui.common.sheet.CustomBottomSheet
import com.zaneschepke.wireguardautotunnel.ui.common.sheet.SheetOption
import com.zaneschepke.wireguardautotunnel.ui.state.AppViewState
import com.zaneschepke.wireguardautotunnel.util.extensions.asIcon
import com.zaneschepke.wireguardautotunnel.util.extensions.asTitleString
import com.zaneschepke.wireguardautotunnel.viewmodel.AppViewModel
import com.zaneschepke.wireguardautotunnel.viewmodel.event.AppEvent
@Composable
fun BackendModeBottomSheet(appSettings: AppSettings, viewModel: AppViewModel) {
val context = LocalContext.current
CustomBottomSheet(
enumValues<AppMode>().map {
val icon = it.asIcon()
SheetOption(
icon,
label = it.asTitleString(context),
onClick = {
viewModel.handleEvent(AppEvent.SetBottomSheet(AppViewState.BottomSheet.NONE))
viewModel.handleEvent(AppEvent.SetAppMode(it))
},
selected = appSettings.appMode == it,
)
}
) {
viewModel.handleEvent(AppEvent.SetBottomSheet(AppViewState.BottomSheet.NONE))
}
}
@@ -21,11 +21,9 @@ import kotlinx.coroutines.withContext
@HiltViewModel @HiltViewModel
class SupportViewModel class SupportViewModel
@Inject @Inject
constructor( constructor(private val updateRepository: UpdateRepository, private val fileUtils: FileUtils,
private val updateRepository: UpdateRepository, @MainDispatcher private val mainDispatcher: CoroutineDispatcher) :
private val fileUtils: FileUtils, ViewModel() {
@MainDispatcher private val mainDispatcher: CoroutineDispatcher,
) : ViewModel() {
private val _uiState = MutableStateFlow(SupportUiState()) private val _uiState = MutableStateFlow(SupportUiState())
val uiState = _uiState.asStateFlow() val uiState = _uiState.asStateFlow()
@@ -5,7 +5,7 @@ data class LicenseFileEntry(
val groupId: String, val groupId: String,
val artifactId: String, val artifactId: String,
val version: String, val version: String,
val name: String? = null, val name: String,
val spdxLicenses: List<SpdxLicense> = emptyList(), val spdxLicenses: List<SpdxLicense> = emptyList(),
val scm: Scm? = null, val scm: Scm? = null,
) )
@@ -1,50 +1,42 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.support.license.components package com.zaneschepke.wireguardautotunnel.ui.screens.support.license.components
import LicenseFileEntry import LicenseFileEntry
import androidx.compose.foundation.focusable import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.zaneschepke.wireguardautotunnel.ui.common.button.LinkIconButton
import com.zaneschepke.wireguardautotunnel.util.extensions.openWebUrl
@Composable @Composable
fun LicenseList(licenses: List<LicenseFileEntry>) { fun LicenseList(licenses: List<LicenseFileEntry>) {
val context = LocalContext.current LazyColumn(modifier = Modifier.fillMaxSize().padding(16.dp)) {
LazyColumn(modifier = Modifier.fillMaxSize().padding(24.dp)) {
items(licenses) { entry -> items(licenses) { entry ->
Column(modifier = Modifier.padding(bottom = 12.dp)) { Column(modifier = Modifier.padding(bottom = 12.dp)) {
Row( Text(
verticalAlignment = Alignment.CenterVertically, text = "${entry.name} (${entry.version})",
horizontalArrangement = Arrangement.SpaceBetween, style = MaterialTheme.typography.titleSmall,
) { )
Column(modifier = Modifier.weight(1f)) {
Text(
text = "${entry.artifactId} (${entry.version})",
style = MaterialTheme.typography.titleSmall,
)
entry.spdxLicenses.forEach { license -> entry.spdxLicenses.forEach { license ->
Text( Text(
text = license.name, text = license.name,
style = MaterialTheme.typography.bodySmall, style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.primary, color = MaterialTheme.colorScheme.primary,
) )
} }
}
entry.scm?.url?.let { scmUrl -> entry.scm?.url?.let { scmUrl ->
LinkIconButton(modifier = Modifier.size(20.dp).focusable()) { Text(
context.openWebUrl(scmUrl) text = scmUrl,
} style = MaterialTheme.typography.labelSmall,
} modifier = Modifier.padding(top = 4.dp),
color = MaterialTheme.colorScheme.secondary,
)
} }
} }
} }

Some files were not shown because too many files have changed in this diff Show More