From 8a08bd15c90eb6b3a4e0198f788863359e3b84d2 Mon Sep 17 00:00:00 2001 From: Fabien Date: Tue, 10 Mar 2026 14:16:27 +0100 Subject: [PATCH 1/8] feat(data): add monitoring settings schema for auto-restart MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Introduces MonitoringSettings Room entity and domain model to persist auto-restart configuration: enabled flag, ping failure threshold, cooldown duration, max restart attempts, exponential backoff toggle, and on-max-attempts action (keep waiting or stop tunnel). BackendMessage sealed class defines typed tunnel lifecycle events: ConnectionDegrading, ConnectionRestored, ConnectionPermanentlyLost. TunnelRestartProgress domain state tracks the full restart lifecycle (idle → restarting → verifying → cooldown → awaiting recovery). DB migrated from version 29 to 35. Co-Authored-By: Claude Sonnet 4.6 --- .../30.json | 138 +++++ .../31.json | 579 +++++++++++++++++ .../32.json | 579 +++++++++++++++++ .../33.json | 572 +++++++++++++++++ .../34.json | 565 +++++++++++++++++ .../35.json | 586 ++++++++++++++++++ .../wireguardautotunnel/data/AppDatabase.kt | 23 +- .../data/DatabaseConverters.kt | 6 + .../data/entity/MonitoringSettings.kt | 12 + .../data/mapper/MonitoringSettingsMapper.kt | 12 + .../data/model/MaxAttemptsAction.kt | 11 + .../domain/events/BackendMessage.kt | 46 +- .../domain/model/MonitoringSettings.kt | 8 + .../domain/state/TunnelRestartProgress.kt | 17 + 14 files changed, 3149 insertions(+), 5 deletions(-) create mode 100755 app/schemas/com.zaneschepke.wireguardautotunnel.data.AppDatabase/30.json create mode 100644 app/schemas/com.zaneschepke.wireguardautotunnel.data.AppDatabase/31.json create mode 100644 app/schemas/com.zaneschepke.wireguardautotunnel.data.AppDatabase/32.json create mode 100644 app/schemas/com.zaneschepke.wireguardautotunnel.data.AppDatabase/33.json create mode 100644 app/schemas/com.zaneschepke.wireguardautotunnel.data.AppDatabase/34.json create mode 100644 app/schemas/com.zaneschepke.wireguardautotunnel.data.AppDatabase/35.json mode change 100644 => 100755 app/src/main/java/com/zaneschepke/wireguardautotunnel/data/AppDatabase.kt mode change 100644 => 100755 app/src/main/java/com/zaneschepke/wireguardautotunnel/data/DatabaseConverters.kt mode change 100644 => 100755 app/src/main/java/com/zaneschepke/wireguardautotunnel/data/entity/MonitoringSettings.kt mode change 100644 => 100755 app/src/main/java/com/zaneschepke/wireguardautotunnel/data/mapper/MonitoringSettingsMapper.kt create mode 100644 app/src/main/java/com/zaneschepke/wireguardautotunnel/data/model/MaxAttemptsAction.kt mode change 100644 => 100755 app/src/main/java/com/zaneschepke/wireguardautotunnel/domain/events/BackendMessage.kt mode change 100644 => 100755 app/src/main/java/com/zaneschepke/wireguardautotunnel/domain/model/MonitoringSettings.kt create mode 100755 app/src/main/java/com/zaneschepke/wireguardautotunnel/domain/state/TunnelRestartProgress.kt diff --git a/app/schemas/com.zaneschepke.wireguardautotunnel.data.AppDatabase/30.json b/app/schemas/com.zaneschepke.wireguardautotunnel.data.AppDatabase/30.json new file mode 100755 index 000000000..6d194b466 --- /dev/null +++ b/app/schemas/com.zaneschepke.wireguardautotunnel.data.AppDatabase/30.json @@ -0,0 +1,138 @@ +{ + "formatVersion": 1, + "database": { + "version": 30, + "identityHash": "10b2fbf87de9ea4d4ffd6ebd42a30602", + "entities": [ + { + "tableName": "tunnel_config", + "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 '[]', `is_metered` INTEGER NOT NULL DEFAULT false)", + "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": "'[]'"}, + {"fieldPath": "isMetered", "columnName": "is_metered", "affinity": "INTEGER", "notNull": true, "defaultValue": "false"} + ], + "primaryKey": {"autoGenerate": true, "columnNames": ["id"]}, + "indices": [ + { + "name": "index_tunnel_config_name", + "unique": true, + "columnNames": ["name"], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_tunnel_config_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 0, `socks5_proxy_bind_address` TEXT, `http_proxy_enable` INTEGER NOT NULL DEFAULT 0, `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": "0"}, + {"fieldPath": "socks5ProxyBindAddress", "columnName": "socks5_proxy_bind_address", "affinity": "TEXT"}, + {"fieldPath": "httpProxyEnabled", "columnName": "http_proxy_enable", "affinity": "INTEGER", "notNull": true, "defaultValue": "0"}, + {"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"]} + }, + { + "tableName": "general_settings", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_shortcuts_enabled` INTEGER NOT NULL DEFAULT 0, `is_restore_on_boot_enabled` INTEGER NOT NULL DEFAULT 0, `is_multi_tunnel_enabled` INTEGER NOT NULL DEFAULT 0, `global_split_tunnel_enabled` INTEGER NOT NULL DEFAULT 0, `app_mode` INTEGER NOT NULL DEFAULT 0, `theme` TEXT NOT NULL DEFAULT 'AUTOMATIC', `locale` TEXT, `remote_key` TEXT, `is_remote_control_enabled` INTEGER NOT NULL DEFAULT 0, `is_pin_lock_enabled` INTEGER NOT NULL DEFAULT 0, `is_always_on_vpn_enabled` INTEGER NOT NULL DEFAULT 0, `already_donated` INTEGER NOT NULL DEFAULT 0)", + "fields": [ + {"fieldPath": "id", "columnName": "id", "affinity": "INTEGER", "notNull": true}, + {"fieldPath": "isShortcutsEnabled", "columnName": "is_shortcuts_enabled", "affinity": "INTEGER", "notNull": true, "defaultValue": "0"}, + {"fieldPath": "isRestoreOnBootEnabled", "columnName": "is_restore_on_boot_enabled", "affinity": "INTEGER", "notNull": true, "defaultValue": "0"}, + {"fieldPath": "isMultiTunnelEnabled", "columnName": "is_multi_tunnel_enabled", "affinity": "INTEGER", "notNull": true, "defaultValue": "0"}, + {"fieldPath": "isGlobalSplitTunnelEnabled", "columnName": "global_split_tunnel_enabled", "affinity": "INTEGER", "notNull": true, "defaultValue": "0"}, + {"fieldPath": "appMode", "columnName": "app_mode", "affinity": "INTEGER", "notNull": true, "defaultValue": "0"}, + {"fieldPath": "theme", "columnName": "theme", "affinity": "TEXT", "notNull": true, "defaultValue": "'AUTOMATIC'"}, + {"fieldPath": "locale", "columnName": "locale", "affinity": "TEXT"}, + {"fieldPath": "remoteKey", "columnName": "remote_key", "affinity": "TEXT"}, + {"fieldPath": "isRemoteControlEnabled", "columnName": "is_remote_control_enabled", "affinity": "INTEGER", "notNull": true, "defaultValue": "0"}, + {"fieldPath": "isPinLockEnabled", "columnName": "is_pin_lock_enabled", "affinity": "INTEGER", "notNull": true, "defaultValue": "0"}, + {"fieldPath": "isAlwaysOnVpnEnabled", "columnName": "is_always_on_vpn_enabled", "affinity": "INTEGER", "notNull": true, "defaultValue": "0"}, + {"fieldPath": "alreadyDonated", "columnName": "already_donated", "affinity": "INTEGER", "notNull": true, "defaultValue": "0"} + ], + "primaryKey": {"autoGenerate": true, "columnNames": ["id"]} + }, + { + "tableName": "auto_tunnel_settings", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_tunnel_enabled` INTEGER NOT NULL DEFAULT 0, `is_tunnel_on_mobile_data_enabled` INTEGER NOT NULL DEFAULT 0, `trusted_network_ssids` TEXT NOT NULL DEFAULT '', `is_tunnel_on_ethernet_enabled` INTEGER NOT NULL DEFAULT 0, `is_tunnel_on_wifi_enabled` INTEGER NOT NULL DEFAULT 0, `is_wildcards_enabled` INTEGER NOT NULL DEFAULT 0, `is_stop_on_no_internet_enabled` INTEGER NOT NULL DEFAULT 0, `debounce_delay_seconds` INTEGER NOT NULL DEFAULT 3, `is_tunnel_on_unsecure_enabled` INTEGER NOT NULL DEFAULT 0, `wifi_detection_method` INTEGER NOT NULL DEFAULT 0, `start_on_boot` INTEGER NOT NULL DEFAULT 0)", + "fields": [ + {"fieldPath": "id", "columnName": "id", "affinity": "INTEGER", "notNull": true}, + {"fieldPath": "isAutoTunnelEnabled", "columnName": "is_tunnel_enabled", "affinity": "INTEGER", "notNull": true, "defaultValue": "0"}, + {"fieldPath": "isTunnelOnMobileDataEnabled", "columnName": "is_tunnel_on_mobile_data_enabled", "affinity": "INTEGER", "notNull": true, "defaultValue": "0"}, + {"fieldPath": "trustedNetworkSSIDs", "columnName": "trusted_network_ssids", "affinity": "TEXT", "notNull": true, "defaultValue": "''"}, + {"fieldPath": "isTunnelOnEthernetEnabled", "columnName": "is_tunnel_on_ethernet_enabled", "affinity": "INTEGER", "notNull": true, "defaultValue": "0"}, + {"fieldPath": "isTunnelOnWifiEnabled", "columnName": "is_tunnel_on_wifi_enabled", "affinity": "INTEGER", "notNull": true, "defaultValue": "0"}, + {"fieldPath": "isWildcardsEnabled", "columnName": "is_wildcards_enabled", "affinity": "INTEGER", "notNull": true, "defaultValue": "0"}, + {"fieldPath": "isStopOnNoInternetEnabled", "columnName": "is_stop_on_no_internet_enabled", "affinity": "INTEGER", "notNull": true, "defaultValue": "0"}, + {"fieldPath": "debounceDelaySeconds", "columnName": "debounce_delay_seconds", "affinity": "INTEGER", "notNull": true, "defaultValue": "3"}, + {"fieldPath": "isTunnelOnUnsecureEnabled", "columnName": "is_tunnel_on_unsecure_enabled", "affinity": "INTEGER", "notNull": true, "defaultValue": "0"}, + {"fieldPath": "wifiDetectionMethod", "columnName": "wifi_detection_method", "affinity": "INTEGER", "notNull": true, "defaultValue": "0"}, + {"fieldPath": "startOnBoot", "columnName": "start_on_boot", "affinity": "INTEGER", "notNull": true, "defaultValue": "0"} + ], + "primaryKey": {"autoGenerate": true, "columnNames": ["id"]} + }, + { + "tableName": "monitoring_settings", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_ping_enabled` INTEGER NOT NULL DEFAULT 0, `is_ping_monitoring_enabled` INTEGER NOT NULL DEFAULT 1, `tunnel_ping_interval_sec` INTEGER NOT NULL DEFAULT 30, `tunnel_ping_attempts` INTEGER NOT NULL DEFAULT 3, `tunnel_ping_timeout_sec` INTEGER, `show_detailed_ping_stats` INTEGER NOT NULL DEFAULT 0, `is_local_logs_enabled` INTEGER NOT NULL DEFAULT 0, `is_restart_on_handshake_timeout_enabled` INTEGER NOT NULL DEFAULT 0, `max_handshake_restart_attempts` INTEGER NOT NULL DEFAULT 5, `restart_cooldown_seconds` INTEGER NOT NULL DEFAULT 30, `is_recovery_notification_enabled` INTEGER NOT NULL DEFAULT 1)", + "fields": [ + {"fieldPath": "id", "columnName": "id", "affinity": "INTEGER", "notNull": true}, + {"fieldPath": "isPingEnabled", "columnName": "is_ping_enabled", "affinity": "INTEGER", "notNull": true, "defaultValue": "0"}, + {"fieldPath": "isPingMonitoringEnabled", "columnName": "is_ping_monitoring_enabled", "affinity": "INTEGER", "notNull": true, "defaultValue": "1"}, + {"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": "showDetailedPingStats", "columnName": "show_detailed_ping_stats", "affinity": "INTEGER", "notNull": true, "defaultValue": "0"}, + {"fieldPath": "isLocalLogsEnabled", "columnName": "is_local_logs_enabled", "affinity": "INTEGER", "notNull": true, "defaultValue": "0"}, + {"fieldPath": "isRestartOnHandshakeTimeoutEnabled", "columnName": "is_restart_on_handshake_timeout_enabled", "affinity": "INTEGER", "notNull": true, "defaultValue": "0"}, + {"fieldPath": "maxHandshakeRestartAttempts", "columnName": "max_handshake_restart_attempts", "affinity": "INTEGER", "notNull": true, "defaultValue": "5"}, + {"fieldPath": "restartCooldownSeconds", "columnName": "restart_cooldown_seconds", "affinity": "INTEGER", "notNull": true, "defaultValue": "30"}, + {"fieldPath": "isRecoveryNotificationEnabled", "columnName": "is_recovery_notification_enabled", "affinity": "INTEGER", "notNull": true, "defaultValue": "1"} + ], + "primaryKey": {"autoGenerate": true, "columnNames": ["id"]} + }, + { + "tableName": "dns_settings", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `dns_protocol` INTEGER NOT NULL DEFAULT 0, `dns_endpoint` TEXT, `global_tunnel_dns_enabled` INTEGER NOT NULL DEFAULT 0)", + "fields": [ + {"fieldPath": "id", "columnName": "id", "affinity": "INTEGER", "notNull": true}, + {"fieldPath": "dnsProtocol", "columnName": "dns_protocol", "affinity": "INTEGER", "notNull": true, "defaultValue": "0"}, + {"fieldPath": "dnsEndpoint", "columnName": "dns_endpoint", "affinity": "TEXT"}, + {"fieldPath": "isGlobalTunnelDnsEnabled", "columnName": "global_tunnel_dns_enabled", "affinity": "INTEGER", "notNull": true, "defaultValue": "0"} + ], + "primaryKey": {"autoGenerate": true, "columnNames": ["id"]} + }, + { + "tableName": "lockdown_settings", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `bypass_lan` INTEGER NOT NULL DEFAULT 0, `metered` INTEGER NOT NULL DEFAULT 0, `dual_stack` INTEGER NOT NULL DEFAULT 0)", + "fields": [ + {"fieldPath": "id", "columnName": "id", "affinity": "INTEGER", "notNull": true}, + {"fieldPath": "bypassLan", "columnName": "bypass_lan", "affinity": "INTEGER", "notNull": true, "defaultValue": "0"}, + {"fieldPath": "metered", "columnName": "metered", "affinity": "INTEGER", "notNull": true, "defaultValue": "0"}, + {"fieldPath": "dualStack", "columnName": "dual_stack", "affinity": "INTEGER", "notNull": true, "defaultValue": "0"} + ], + "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, '10b2fbf87de9ea4d4ffd6ebd42a30602')" + ] + } +} diff --git a/app/schemas/com.zaneschepke.wireguardautotunnel.data.AppDatabase/31.json b/app/schemas/com.zaneschepke.wireguardautotunnel.data.AppDatabase/31.json new file mode 100644 index 000000000..b8cf62fe1 --- /dev/null +++ b/app/schemas/com.zaneschepke.wireguardautotunnel.data.AppDatabase/31.json @@ -0,0 +1,579 @@ +{ + "formatVersion": 1, + "database": { + "version": 31, + "identityHash": "b5e1017771aedf06bec44bb5566529fb", + "entities": [ + { + "tableName": "tunnel_config", + "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 '[]', `is_metered` INTEGER NOT NULL DEFAULT false)", + "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": "'[]'" + }, + { + "fieldPath": "isMetered", + "columnName": "is_metered", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "false" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_tunnel_config_name", + "unique": true, + "columnNames": [ + "name" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_tunnel_config_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 0, `socks5_proxy_bind_address` TEXT, `http_proxy_enable` INTEGER NOT NULL DEFAULT 0, `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": "0" + }, + { + "fieldPath": "socks5ProxyBindAddress", + "columnName": "socks5_proxy_bind_address", + "affinity": "TEXT" + }, + { + "fieldPath": "httpProxyEnabled", + "columnName": "http_proxy_enable", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "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" + ] + } + }, + { + "tableName": "general_settings", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_shortcuts_enabled` INTEGER NOT NULL DEFAULT 0, `is_restore_on_boot_enabled` INTEGER NOT NULL DEFAULT 0, `is_multi_tunnel_enabled` INTEGER NOT NULL DEFAULT 0, `global_split_tunnel_enabled` INTEGER NOT NULL DEFAULT 0, `app_mode` INTEGER NOT NULL DEFAULT 0, `theme` TEXT NOT NULL DEFAULT 'AUTOMATIC', `locale` TEXT, `remote_key` TEXT, `is_remote_control_enabled` INTEGER NOT NULL DEFAULT 0, `is_pin_lock_enabled` INTEGER NOT NULL DEFAULT 0, `is_always_on_vpn_enabled` INTEGER NOT NULL DEFAULT 0, `already_donated` INTEGER NOT NULL DEFAULT 0)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isShortcutsEnabled", + "columnName": "is_shortcuts_enabled", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "isRestoreOnBootEnabled", + "columnName": "is_restore_on_boot_enabled", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "isMultiTunnelEnabled", + "columnName": "is_multi_tunnel_enabled", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "isGlobalSplitTunnelEnabled", + "columnName": "global_split_tunnel_enabled", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "appMode", + "columnName": "app_mode", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "theme", + "columnName": "theme", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "'AUTOMATIC'" + }, + { + "fieldPath": "locale", + "columnName": "locale", + "affinity": "TEXT" + }, + { + "fieldPath": "remoteKey", + "columnName": "remote_key", + "affinity": "TEXT" + }, + { + "fieldPath": "isRemoteControlEnabled", + "columnName": "is_remote_control_enabled", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "isPinLockEnabled", + "columnName": "is_pin_lock_enabled", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "isAlwaysOnVpnEnabled", + "columnName": "is_always_on_vpn_enabled", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "alreadyDonated", + "columnName": "already_donated", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + } + }, + { + "tableName": "auto_tunnel_settings", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_tunnel_enabled` INTEGER NOT NULL DEFAULT 0, `is_tunnel_on_mobile_data_enabled` INTEGER NOT NULL DEFAULT 0, `trusted_network_ssids` TEXT NOT NULL DEFAULT '', `is_tunnel_on_ethernet_enabled` INTEGER NOT NULL DEFAULT 0, `is_tunnel_on_wifi_enabled` INTEGER NOT NULL DEFAULT 0, `is_wildcards_enabled` INTEGER NOT NULL DEFAULT 0, `is_stop_on_no_internet_enabled` INTEGER NOT NULL DEFAULT 0, `debounce_delay_seconds` INTEGER NOT NULL DEFAULT 3, `is_tunnel_on_unsecure_enabled` INTEGER NOT NULL DEFAULT 0, `wifi_detection_method` INTEGER NOT NULL DEFAULT 0, `start_on_boot` INTEGER NOT NULL DEFAULT 0)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isAutoTunnelEnabled", + "columnName": "is_tunnel_enabled", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "isTunnelOnMobileDataEnabled", + "columnName": "is_tunnel_on_mobile_data_enabled", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "trustedNetworkSSIDs", + "columnName": "trusted_network_ssids", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "isTunnelOnEthernetEnabled", + "columnName": "is_tunnel_on_ethernet_enabled", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "isTunnelOnWifiEnabled", + "columnName": "is_tunnel_on_wifi_enabled", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "isWildcardsEnabled", + "columnName": "is_wildcards_enabled", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "isStopOnNoInternetEnabled", + "columnName": "is_stop_on_no_internet_enabled", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "debounceDelaySeconds", + "columnName": "debounce_delay_seconds", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "3" + }, + { + "fieldPath": "isTunnelOnUnsecureEnabled", + "columnName": "is_tunnel_on_unsecure_enabled", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "wifiDetectionMethod", + "columnName": "wifi_detection_method", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "startOnBoot", + "columnName": "start_on_boot", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + } + }, + { + "tableName": "monitoring_settings", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_ping_enabled` INTEGER NOT NULL DEFAULT 0, `is_ping_monitoring_enabled` INTEGER NOT NULL DEFAULT 1, `tunnel_ping_interval_sec` INTEGER NOT NULL DEFAULT 30, `tunnel_ping_attempts` INTEGER NOT NULL DEFAULT 3, `tunnel_ping_timeout_sec` INTEGER, `show_detailed_ping_stats` INTEGER NOT NULL DEFAULT 0, `is_local_logs_enabled` INTEGER NOT NULL DEFAULT 0, `is_restart_on_handshake_timeout_enabled` INTEGER NOT NULL DEFAULT 0, `max_handshake_restart_attempts` INTEGER NOT NULL DEFAULT 5, `restart_cooldown_seconds` INTEGER NOT NULL DEFAULT 30, `is_recovery_notification_enabled` INTEGER NOT NULL DEFAULT 1, `max_attempts_action` INTEGER NOT NULL DEFAULT 0, `ping_failures_before_restart` INTEGER NOT NULL DEFAULT 1, `is_backoff_enabled` INTEGER NOT NULL DEFAULT 0, `startup_grace_seconds` INTEGER NOT NULL DEFAULT 30)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isPingEnabled", + "columnName": "is_ping_enabled", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "isPingMonitoringEnabled", + "columnName": "is_ping_monitoring_enabled", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "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": "showDetailedPingStats", + "columnName": "show_detailed_ping_stats", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "isLocalLogsEnabled", + "columnName": "is_local_logs_enabled", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "isRestartOnHandshakeTimeoutEnabled", + "columnName": "is_restart_on_handshake_timeout_enabled", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "maxHandshakeRestartAttempts", + "columnName": "max_handshake_restart_attempts", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "5" + }, + { + "fieldPath": "restartCooldownSeconds", + "columnName": "restart_cooldown_seconds", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "30" + }, + { + "fieldPath": "isRecoveryNotificationEnabled", + "columnName": "is_recovery_notification_enabled", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "maxAttemptsAction", + "columnName": "max_attempts_action", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "pingFailuresBeforeRestart", + "columnName": "ping_failures_before_restart", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "isBackoffEnabled", + "columnName": "is_backoff_enabled", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "startupGraceSeconds", + "columnName": "startup_grace_seconds", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "30" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + } + }, + { + "tableName": "dns_settings", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `dns_protocol` INTEGER NOT NULL DEFAULT 0, `dns_endpoint` TEXT, `global_tunnel_dns_enabled` INTEGER NOT NULL DEFAULT 0)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "dnsProtocol", + "columnName": "dns_protocol", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "dnsEndpoint", + "columnName": "dns_endpoint", + "affinity": "TEXT" + }, + { + "fieldPath": "isGlobalTunnelDnsEnabled", + "columnName": "global_tunnel_dns_enabled", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + } + }, + { + "tableName": "lockdown_settings", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `bypass_lan` INTEGER NOT NULL DEFAULT 0, `metered` INTEGER NOT NULL DEFAULT 0, `dual_stack` INTEGER NOT NULL DEFAULT 0)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "bypassLan", + "columnName": "bypass_lan", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "metered", + "columnName": "metered", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "dualStack", + "columnName": "dual_stack", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "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, 'b5e1017771aedf06bec44bb5566529fb')" + ] + } +} \ No newline at end of file diff --git a/app/schemas/com.zaneschepke.wireguardautotunnel.data.AppDatabase/32.json b/app/schemas/com.zaneschepke.wireguardautotunnel.data.AppDatabase/32.json new file mode 100644 index 000000000..bc9b60184 --- /dev/null +++ b/app/schemas/com.zaneschepke.wireguardautotunnel.data.AppDatabase/32.json @@ -0,0 +1,579 @@ +{ + "formatVersion": 1, + "database": { + "version": 32, + "identityHash": "769d76a0bb9becbbe42d4c3c31727a42", + "entities": [ + { + "tableName": "tunnel_config", + "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 '[]', `is_metered` INTEGER NOT NULL DEFAULT false)", + "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": "'[]'" + }, + { + "fieldPath": "isMetered", + "columnName": "is_metered", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "false" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_tunnel_config_name", + "unique": true, + "columnNames": [ + "name" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_tunnel_config_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 0, `socks5_proxy_bind_address` TEXT, `http_proxy_enable` INTEGER NOT NULL DEFAULT 0, `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": "0" + }, + { + "fieldPath": "socks5ProxyBindAddress", + "columnName": "socks5_proxy_bind_address", + "affinity": "TEXT" + }, + { + "fieldPath": "httpProxyEnabled", + "columnName": "http_proxy_enable", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "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" + ] + } + }, + { + "tableName": "general_settings", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_shortcuts_enabled` INTEGER NOT NULL DEFAULT 0, `is_restore_on_boot_enabled` INTEGER NOT NULL DEFAULT 0, `is_multi_tunnel_enabled` INTEGER NOT NULL DEFAULT 0, `global_split_tunnel_enabled` INTEGER NOT NULL DEFAULT 0, `app_mode` INTEGER NOT NULL DEFAULT 0, `theme` TEXT NOT NULL DEFAULT 'AUTOMATIC', `locale` TEXT, `remote_key` TEXT, `is_remote_control_enabled` INTEGER NOT NULL DEFAULT 0, `is_pin_lock_enabled` INTEGER NOT NULL DEFAULT 0, `is_always_on_vpn_enabled` INTEGER NOT NULL DEFAULT 0, `already_donated` INTEGER NOT NULL DEFAULT 0)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isShortcutsEnabled", + "columnName": "is_shortcuts_enabled", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "isRestoreOnBootEnabled", + "columnName": "is_restore_on_boot_enabled", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "isMultiTunnelEnabled", + "columnName": "is_multi_tunnel_enabled", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "isGlobalSplitTunnelEnabled", + "columnName": "global_split_tunnel_enabled", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "appMode", + "columnName": "app_mode", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "theme", + "columnName": "theme", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "'AUTOMATIC'" + }, + { + "fieldPath": "locale", + "columnName": "locale", + "affinity": "TEXT" + }, + { + "fieldPath": "remoteKey", + "columnName": "remote_key", + "affinity": "TEXT" + }, + { + "fieldPath": "isRemoteControlEnabled", + "columnName": "is_remote_control_enabled", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "isPinLockEnabled", + "columnName": "is_pin_lock_enabled", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "isAlwaysOnVpnEnabled", + "columnName": "is_always_on_vpn_enabled", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "alreadyDonated", + "columnName": "already_donated", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + } + }, + { + "tableName": "auto_tunnel_settings", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_tunnel_enabled` INTEGER NOT NULL DEFAULT 0, `is_tunnel_on_mobile_data_enabled` INTEGER NOT NULL DEFAULT 0, `trusted_network_ssids` TEXT NOT NULL DEFAULT '', `is_tunnel_on_ethernet_enabled` INTEGER NOT NULL DEFAULT 0, `is_tunnel_on_wifi_enabled` INTEGER NOT NULL DEFAULT 0, `is_wildcards_enabled` INTEGER NOT NULL DEFAULT 0, `is_stop_on_no_internet_enabled` INTEGER NOT NULL DEFAULT 0, `debounce_delay_seconds` INTEGER NOT NULL DEFAULT 3, `is_tunnel_on_unsecure_enabled` INTEGER NOT NULL DEFAULT 0, `wifi_detection_method` INTEGER NOT NULL DEFAULT 0, `start_on_boot` INTEGER NOT NULL DEFAULT 0)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isAutoTunnelEnabled", + "columnName": "is_tunnel_enabled", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "isTunnelOnMobileDataEnabled", + "columnName": "is_tunnel_on_mobile_data_enabled", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "trustedNetworkSSIDs", + "columnName": "trusted_network_ssids", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "isTunnelOnEthernetEnabled", + "columnName": "is_tunnel_on_ethernet_enabled", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "isTunnelOnWifiEnabled", + "columnName": "is_tunnel_on_wifi_enabled", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "isWildcardsEnabled", + "columnName": "is_wildcards_enabled", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "isStopOnNoInternetEnabled", + "columnName": "is_stop_on_no_internet_enabled", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "debounceDelaySeconds", + "columnName": "debounce_delay_seconds", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "3" + }, + { + "fieldPath": "isTunnelOnUnsecureEnabled", + "columnName": "is_tunnel_on_unsecure_enabled", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "wifiDetectionMethod", + "columnName": "wifi_detection_method", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "startOnBoot", + "columnName": "start_on_boot", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + } + }, + { + "tableName": "monitoring_settings", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_ping_enabled` INTEGER NOT NULL DEFAULT 0, `is_ping_monitoring_enabled` INTEGER NOT NULL DEFAULT 1, `tunnel_ping_interval_sec` INTEGER NOT NULL DEFAULT 30, `tunnel_ping_attempts` INTEGER NOT NULL DEFAULT 3, `tunnel_ping_timeout_sec` INTEGER, `show_detailed_ping_stats` INTEGER NOT NULL DEFAULT 0, `is_local_logs_enabled` INTEGER NOT NULL DEFAULT 0, `is_restart_on_handshake_timeout_enabled` INTEGER NOT NULL DEFAULT 0, `max_restart_attempts` INTEGER NOT NULL DEFAULT 5, `restart_cooldown_seconds` INTEGER NOT NULL DEFAULT 30, `is_recovery_notification_enabled` INTEGER NOT NULL DEFAULT 1, `max_attempts_action` INTEGER NOT NULL DEFAULT 0, `ping_failures_before_restart` INTEGER NOT NULL DEFAULT 1, `is_backoff_enabled` INTEGER NOT NULL DEFAULT 0, `startup_grace_seconds` INTEGER NOT NULL DEFAULT 30)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isPingEnabled", + "columnName": "is_ping_enabled", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "isPingMonitoringEnabled", + "columnName": "is_ping_monitoring_enabled", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "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": "showDetailedPingStats", + "columnName": "show_detailed_ping_stats", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "isLocalLogsEnabled", + "columnName": "is_local_logs_enabled", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "isRestartOnHandshakeTimeoutEnabled", + "columnName": "is_restart_on_handshake_timeout_enabled", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "maxRestartAttempts", + "columnName": "max_restart_attempts", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "5" + }, + { + "fieldPath": "restartCooldownSeconds", + "columnName": "restart_cooldown_seconds", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "30" + }, + { + "fieldPath": "isRecoveryNotificationEnabled", + "columnName": "is_recovery_notification_enabled", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "maxAttemptsAction", + "columnName": "max_attempts_action", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "pingFailuresBeforeRestart", + "columnName": "ping_failures_before_restart", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "isBackoffEnabled", + "columnName": "is_backoff_enabled", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "startupGraceSeconds", + "columnName": "startup_grace_seconds", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "30" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + } + }, + { + "tableName": "dns_settings", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `dns_protocol` INTEGER NOT NULL DEFAULT 0, `dns_endpoint` TEXT, `global_tunnel_dns_enabled` INTEGER NOT NULL DEFAULT 0)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "dnsProtocol", + "columnName": "dns_protocol", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "dnsEndpoint", + "columnName": "dns_endpoint", + "affinity": "TEXT" + }, + { + "fieldPath": "isGlobalTunnelDnsEnabled", + "columnName": "global_tunnel_dns_enabled", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + } + }, + { + "tableName": "lockdown_settings", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `bypass_lan` INTEGER NOT NULL DEFAULT 0, `metered` INTEGER NOT NULL DEFAULT 0, `dual_stack` INTEGER NOT NULL DEFAULT 0)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "bypassLan", + "columnName": "bypass_lan", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "metered", + "columnName": "metered", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "dualStack", + "columnName": "dual_stack", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "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, '769d76a0bb9becbbe42d4c3c31727a42')" + ] + } +} \ No newline at end of file diff --git a/app/schemas/com.zaneschepke.wireguardautotunnel.data.AppDatabase/33.json b/app/schemas/com.zaneschepke.wireguardautotunnel.data.AppDatabase/33.json new file mode 100644 index 000000000..061975a3d --- /dev/null +++ b/app/schemas/com.zaneschepke.wireguardautotunnel.data.AppDatabase/33.json @@ -0,0 +1,572 @@ +{ + "formatVersion": 1, + "database": { + "version": 33, + "identityHash": "62b500b2c8cdbf0b32aef6074c9c13a9", + "entities": [ + { + "tableName": "tunnel_config", + "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 '[]', `is_metered` INTEGER NOT NULL DEFAULT false)", + "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": "'[]'" + }, + { + "fieldPath": "isMetered", + "columnName": "is_metered", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "false" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_tunnel_config_name", + "unique": true, + "columnNames": [ + "name" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_tunnel_config_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 0, `socks5_proxy_bind_address` TEXT, `http_proxy_enable` INTEGER NOT NULL DEFAULT 0, `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": "0" + }, + { + "fieldPath": "socks5ProxyBindAddress", + "columnName": "socks5_proxy_bind_address", + "affinity": "TEXT" + }, + { + "fieldPath": "httpProxyEnabled", + "columnName": "http_proxy_enable", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "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" + ] + } + }, + { + "tableName": "general_settings", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_shortcuts_enabled` INTEGER NOT NULL DEFAULT 0, `is_restore_on_boot_enabled` INTEGER NOT NULL DEFAULT 0, `is_multi_tunnel_enabled` INTEGER NOT NULL DEFAULT 0, `global_split_tunnel_enabled` INTEGER NOT NULL DEFAULT 0, `app_mode` INTEGER NOT NULL DEFAULT 0, `theme` TEXT NOT NULL DEFAULT 'AUTOMATIC', `locale` TEXT, `remote_key` TEXT, `is_remote_control_enabled` INTEGER NOT NULL DEFAULT 0, `is_pin_lock_enabled` INTEGER NOT NULL DEFAULT 0, `is_always_on_vpn_enabled` INTEGER NOT NULL DEFAULT 0, `already_donated` INTEGER NOT NULL DEFAULT 0)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isShortcutsEnabled", + "columnName": "is_shortcuts_enabled", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "isRestoreOnBootEnabled", + "columnName": "is_restore_on_boot_enabled", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "isMultiTunnelEnabled", + "columnName": "is_multi_tunnel_enabled", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "isGlobalSplitTunnelEnabled", + "columnName": "global_split_tunnel_enabled", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "appMode", + "columnName": "app_mode", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "theme", + "columnName": "theme", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "'AUTOMATIC'" + }, + { + "fieldPath": "locale", + "columnName": "locale", + "affinity": "TEXT" + }, + { + "fieldPath": "remoteKey", + "columnName": "remote_key", + "affinity": "TEXT" + }, + { + "fieldPath": "isRemoteControlEnabled", + "columnName": "is_remote_control_enabled", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "isPinLockEnabled", + "columnName": "is_pin_lock_enabled", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "isAlwaysOnVpnEnabled", + "columnName": "is_always_on_vpn_enabled", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "alreadyDonated", + "columnName": "already_donated", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + } + }, + { + "tableName": "auto_tunnel_settings", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_tunnel_enabled` INTEGER NOT NULL DEFAULT 0, `is_tunnel_on_mobile_data_enabled` INTEGER NOT NULL DEFAULT 0, `trusted_network_ssids` TEXT NOT NULL DEFAULT '', `is_tunnel_on_ethernet_enabled` INTEGER NOT NULL DEFAULT 0, `is_tunnel_on_wifi_enabled` INTEGER NOT NULL DEFAULT 0, `is_wildcards_enabled` INTEGER NOT NULL DEFAULT 0, `is_stop_on_no_internet_enabled` INTEGER NOT NULL DEFAULT 0, `debounce_delay_seconds` INTEGER NOT NULL DEFAULT 3, `is_tunnel_on_unsecure_enabled` INTEGER NOT NULL DEFAULT 0, `wifi_detection_method` INTEGER NOT NULL DEFAULT 0, `start_on_boot` INTEGER NOT NULL DEFAULT 0)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isAutoTunnelEnabled", + "columnName": "is_tunnel_enabled", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "isTunnelOnMobileDataEnabled", + "columnName": "is_tunnel_on_mobile_data_enabled", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "trustedNetworkSSIDs", + "columnName": "trusted_network_ssids", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "isTunnelOnEthernetEnabled", + "columnName": "is_tunnel_on_ethernet_enabled", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "isTunnelOnWifiEnabled", + "columnName": "is_tunnel_on_wifi_enabled", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "isWildcardsEnabled", + "columnName": "is_wildcards_enabled", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "isStopOnNoInternetEnabled", + "columnName": "is_stop_on_no_internet_enabled", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "debounceDelaySeconds", + "columnName": "debounce_delay_seconds", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "3" + }, + { + "fieldPath": "isTunnelOnUnsecureEnabled", + "columnName": "is_tunnel_on_unsecure_enabled", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "wifiDetectionMethod", + "columnName": "wifi_detection_method", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "startOnBoot", + "columnName": "start_on_boot", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + } + }, + { + "tableName": "monitoring_settings", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_ping_enabled` INTEGER NOT NULL DEFAULT 0, `is_ping_monitoring_enabled` INTEGER NOT NULL DEFAULT 1, `tunnel_ping_interval_sec` INTEGER NOT NULL DEFAULT 30, `tunnel_ping_attempts` INTEGER NOT NULL DEFAULT 3, `tunnel_ping_timeout_sec` INTEGER, `show_detailed_ping_stats` INTEGER NOT NULL DEFAULT 0, `is_local_logs_enabled` INTEGER NOT NULL DEFAULT 0, `is_restart_on_handshake_timeout_enabled` INTEGER NOT NULL DEFAULT 0, `max_restart_attempts` INTEGER NOT NULL DEFAULT 5, `restart_cooldown_seconds` INTEGER NOT NULL DEFAULT 30, `is_recovery_notification_enabled` INTEGER NOT NULL DEFAULT 1, `max_attempts_action` INTEGER NOT NULL DEFAULT 0, `ping_failures_before_restart` INTEGER NOT NULL DEFAULT 1, `is_backoff_enabled` INTEGER NOT NULL DEFAULT 0)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isPingEnabled", + "columnName": "is_ping_enabled", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "isPingMonitoringEnabled", + "columnName": "is_ping_monitoring_enabled", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "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": "showDetailedPingStats", + "columnName": "show_detailed_ping_stats", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "isLocalLogsEnabled", + "columnName": "is_local_logs_enabled", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "isRestartOnHandshakeTimeoutEnabled", + "columnName": "is_restart_on_handshake_timeout_enabled", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "maxRestartAttempts", + "columnName": "max_restart_attempts", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "5" + }, + { + "fieldPath": "restartCooldownSeconds", + "columnName": "restart_cooldown_seconds", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "30" + }, + { + "fieldPath": "isRecoveryNotificationEnabled", + "columnName": "is_recovery_notification_enabled", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "maxAttemptsAction", + "columnName": "max_attempts_action", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "pingFailuresBeforeRestart", + "columnName": "ping_failures_before_restart", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "isBackoffEnabled", + "columnName": "is_backoff_enabled", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + } + }, + { + "tableName": "dns_settings", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `dns_protocol` INTEGER NOT NULL DEFAULT 0, `dns_endpoint` TEXT, `global_tunnel_dns_enabled` INTEGER NOT NULL DEFAULT 0)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "dnsProtocol", + "columnName": "dns_protocol", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "dnsEndpoint", + "columnName": "dns_endpoint", + "affinity": "TEXT" + }, + { + "fieldPath": "isGlobalTunnelDnsEnabled", + "columnName": "global_tunnel_dns_enabled", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + } + }, + { + "tableName": "lockdown_settings", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `bypass_lan` INTEGER NOT NULL DEFAULT 0, `metered` INTEGER NOT NULL DEFAULT 0, `dual_stack` INTEGER NOT NULL DEFAULT 0)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "bypassLan", + "columnName": "bypass_lan", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "metered", + "columnName": "metered", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "dualStack", + "columnName": "dual_stack", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "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, '62b500b2c8cdbf0b32aef6074c9c13a9')" + ] + } +} \ No newline at end of file diff --git a/app/schemas/com.zaneschepke.wireguardautotunnel.data.AppDatabase/34.json b/app/schemas/com.zaneschepke.wireguardautotunnel.data.AppDatabase/34.json new file mode 100644 index 000000000..3d7b699f7 --- /dev/null +++ b/app/schemas/com.zaneschepke.wireguardautotunnel.data.AppDatabase/34.json @@ -0,0 +1,565 @@ +{ + "formatVersion": 1, + "database": { + "version": 34, + "identityHash": "e705dec570ab54c2f61510bddc42bcae", + "entities": [ + { + "tableName": "tunnel_config", + "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 '[]', `is_metered` INTEGER NOT NULL DEFAULT false)", + "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": "'[]'" + }, + { + "fieldPath": "isMetered", + "columnName": "is_metered", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "false" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_tunnel_config_name", + "unique": true, + "columnNames": [ + "name" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_tunnel_config_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 0, `socks5_proxy_bind_address` TEXT, `http_proxy_enable` INTEGER NOT NULL DEFAULT 0, `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": "0" + }, + { + "fieldPath": "socks5ProxyBindAddress", + "columnName": "socks5_proxy_bind_address", + "affinity": "TEXT" + }, + { + "fieldPath": "httpProxyEnabled", + "columnName": "http_proxy_enable", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "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" + ] + } + }, + { + "tableName": "general_settings", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_shortcuts_enabled` INTEGER NOT NULL DEFAULT 0, `is_restore_on_boot_enabled` INTEGER NOT NULL DEFAULT 0, `is_multi_tunnel_enabled` INTEGER NOT NULL DEFAULT 0, `global_split_tunnel_enabled` INTEGER NOT NULL DEFAULT 0, `app_mode` INTEGER NOT NULL DEFAULT 0, `theme` TEXT NOT NULL DEFAULT 'AUTOMATIC', `locale` TEXT, `remote_key` TEXT, `is_remote_control_enabled` INTEGER NOT NULL DEFAULT 0, `is_pin_lock_enabled` INTEGER NOT NULL DEFAULT 0, `is_always_on_vpn_enabled` INTEGER NOT NULL DEFAULT 0, `already_donated` INTEGER NOT NULL DEFAULT 0)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isShortcutsEnabled", + "columnName": "is_shortcuts_enabled", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "isRestoreOnBootEnabled", + "columnName": "is_restore_on_boot_enabled", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "isMultiTunnelEnabled", + "columnName": "is_multi_tunnel_enabled", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "isGlobalSplitTunnelEnabled", + "columnName": "global_split_tunnel_enabled", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "appMode", + "columnName": "app_mode", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "theme", + "columnName": "theme", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "'AUTOMATIC'" + }, + { + "fieldPath": "locale", + "columnName": "locale", + "affinity": "TEXT" + }, + { + "fieldPath": "remoteKey", + "columnName": "remote_key", + "affinity": "TEXT" + }, + { + "fieldPath": "isRemoteControlEnabled", + "columnName": "is_remote_control_enabled", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "isPinLockEnabled", + "columnName": "is_pin_lock_enabled", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "isAlwaysOnVpnEnabled", + "columnName": "is_always_on_vpn_enabled", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "alreadyDonated", + "columnName": "already_donated", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + } + }, + { + "tableName": "auto_tunnel_settings", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_tunnel_enabled` INTEGER NOT NULL DEFAULT 0, `is_tunnel_on_mobile_data_enabled` INTEGER NOT NULL DEFAULT 0, `trusted_network_ssids` TEXT NOT NULL DEFAULT '', `is_tunnel_on_ethernet_enabled` INTEGER NOT NULL DEFAULT 0, `is_tunnel_on_wifi_enabled` INTEGER NOT NULL DEFAULT 0, `is_wildcards_enabled` INTEGER NOT NULL DEFAULT 0, `is_stop_on_no_internet_enabled` INTEGER NOT NULL DEFAULT 0, `debounce_delay_seconds` INTEGER NOT NULL DEFAULT 3, `is_tunnel_on_unsecure_enabled` INTEGER NOT NULL DEFAULT 0, `wifi_detection_method` INTEGER NOT NULL DEFAULT 0, `start_on_boot` INTEGER NOT NULL DEFAULT 0)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isAutoTunnelEnabled", + "columnName": "is_tunnel_enabled", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "isTunnelOnMobileDataEnabled", + "columnName": "is_tunnel_on_mobile_data_enabled", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "trustedNetworkSSIDs", + "columnName": "trusted_network_ssids", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "isTunnelOnEthernetEnabled", + "columnName": "is_tunnel_on_ethernet_enabled", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "isTunnelOnWifiEnabled", + "columnName": "is_tunnel_on_wifi_enabled", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "isWildcardsEnabled", + "columnName": "is_wildcards_enabled", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "isStopOnNoInternetEnabled", + "columnName": "is_stop_on_no_internet_enabled", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "debounceDelaySeconds", + "columnName": "debounce_delay_seconds", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "3" + }, + { + "fieldPath": "isTunnelOnUnsecureEnabled", + "columnName": "is_tunnel_on_unsecure_enabled", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "wifiDetectionMethod", + "columnName": "wifi_detection_method", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "startOnBoot", + "columnName": "start_on_boot", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + } + }, + { + "tableName": "monitoring_settings", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_ping_enabled` INTEGER NOT NULL DEFAULT 0, `is_ping_monitoring_enabled` INTEGER NOT NULL DEFAULT 1, `tunnel_ping_interval_sec` INTEGER NOT NULL DEFAULT 30, `tunnel_ping_attempts` INTEGER NOT NULL DEFAULT 3, `tunnel_ping_timeout_sec` INTEGER, `show_detailed_ping_stats` INTEGER NOT NULL DEFAULT 0, `is_local_logs_enabled` INTEGER NOT NULL DEFAULT 0, `is_restart_on_handshake_timeout_enabled` INTEGER NOT NULL DEFAULT 0, `max_restart_attempts` INTEGER NOT NULL DEFAULT 5, `restart_cooldown_seconds` INTEGER NOT NULL DEFAULT 30, `max_attempts_action` INTEGER NOT NULL DEFAULT 0, `ping_failures_before_restart` INTEGER NOT NULL DEFAULT 1, `is_backoff_enabled` INTEGER NOT NULL DEFAULT 0)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isPingEnabled", + "columnName": "is_ping_enabled", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "isPingMonitoringEnabled", + "columnName": "is_ping_monitoring_enabled", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "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": "showDetailedPingStats", + "columnName": "show_detailed_ping_stats", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "isLocalLogsEnabled", + "columnName": "is_local_logs_enabled", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "isRestartOnHandshakeTimeoutEnabled", + "columnName": "is_restart_on_handshake_timeout_enabled", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "maxRestartAttempts", + "columnName": "max_restart_attempts", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "5" + }, + { + "fieldPath": "restartCooldownSeconds", + "columnName": "restart_cooldown_seconds", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "30" + }, + { + "fieldPath": "maxAttemptsAction", + "columnName": "max_attempts_action", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "pingFailuresBeforeRestart", + "columnName": "ping_failures_before_restart", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "isBackoffEnabled", + "columnName": "is_backoff_enabled", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + } + }, + { + "tableName": "dns_settings", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `dns_protocol` INTEGER NOT NULL DEFAULT 0, `dns_endpoint` TEXT, `global_tunnel_dns_enabled` INTEGER NOT NULL DEFAULT 0)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "dnsProtocol", + "columnName": "dns_protocol", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "dnsEndpoint", + "columnName": "dns_endpoint", + "affinity": "TEXT" + }, + { + "fieldPath": "isGlobalTunnelDnsEnabled", + "columnName": "global_tunnel_dns_enabled", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + } + }, + { + "tableName": "lockdown_settings", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `bypass_lan` INTEGER NOT NULL DEFAULT 0, `metered` INTEGER NOT NULL DEFAULT 0, `dual_stack` INTEGER NOT NULL DEFAULT 0)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "bypassLan", + "columnName": "bypass_lan", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "metered", + "columnName": "metered", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "dualStack", + "columnName": "dual_stack", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "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, 'e705dec570ab54c2f61510bddc42bcae')" + ] + } +} \ No newline at end of file diff --git a/app/schemas/com.zaneschepke.wireguardautotunnel.data.AppDatabase/35.json b/app/schemas/com.zaneschepke.wireguardautotunnel.data.AppDatabase/35.json new file mode 100644 index 000000000..ebccaa543 --- /dev/null +++ b/app/schemas/com.zaneschepke.wireguardautotunnel.data.AppDatabase/35.json @@ -0,0 +1,586 @@ +{ + "formatVersion": 1, + "database": { + "version": 35, + "identityHash": "3f5d07c1a895d1a3d28c03ce2c2877c0", + "entities": [ + { + "tableName": "tunnel_config", + "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 '[]', `is_metered` INTEGER NOT NULL DEFAULT false)", + "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": "'[]'" + }, + { + "fieldPath": "isMetered", + "columnName": "is_metered", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "false" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_tunnel_config_name", + "unique": true, + "columnNames": [ + "name" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_tunnel_config_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 0, `socks5_proxy_bind_address` TEXT, `http_proxy_enable` INTEGER NOT NULL DEFAULT 0, `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": "0" + }, + { + "fieldPath": "socks5ProxyBindAddress", + "columnName": "socks5_proxy_bind_address", + "affinity": "TEXT" + }, + { + "fieldPath": "httpProxyEnabled", + "columnName": "http_proxy_enable", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "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" + ] + } + }, + { + "tableName": "general_settings", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_shortcuts_enabled` INTEGER NOT NULL DEFAULT 0, `is_restore_on_boot_enabled` INTEGER NOT NULL DEFAULT 0, `is_multi_tunnel_enabled` INTEGER NOT NULL DEFAULT 0, `global_split_tunnel_enabled` INTEGER NOT NULL DEFAULT 0, `app_mode` INTEGER NOT NULL DEFAULT 0, `theme` TEXT NOT NULL DEFAULT 'AUTOMATIC', `locale` TEXT, `remote_key` TEXT, `is_remote_control_enabled` INTEGER NOT NULL DEFAULT 0, `is_pin_lock_enabled` INTEGER NOT NULL DEFAULT 0, `is_always_on_vpn_enabled` INTEGER NOT NULL DEFAULT 0, `already_donated` INTEGER NOT NULL DEFAULT 0)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isShortcutsEnabled", + "columnName": "is_shortcuts_enabled", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "isRestoreOnBootEnabled", + "columnName": "is_restore_on_boot_enabled", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "isMultiTunnelEnabled", + "columnName": "is_multi_tunnel_enabled", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "isGlobalSplitTunnelEnabled", + "columnName": "global_split_tunnel_enabled", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "appMode", + "columnName": "app_mode", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "theme", + "columnName": "theme", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "'AUTOMATIC'" + }, + { + "fieldPath": "locale", + "columnName": "locale", + "affinity": "TEXT" + }, + { + "fieldPath": "remoteKey", + "columnName": "remote_key", + "affinity": "TEXT" + }, + { + "fieldPath": "isRemoteControlEnabled", + "columnName": "is_remote_control_enabled", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "isPinLockEnabled", + "columnName": "is_pin_lock_enabled", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "isAlwaysOnVpnEnabled", + "columnName": "is_always_on_vpn_enabled", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "alreadyDonated", + "columnName": "already_donated", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + } + }, + { + "tableName": "auto_tunnel_settings", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_tunnel_enabled` INTEGER NOT NULL DEFAULT 0, `is_tunnel_on_mobile_data_enabled` INTEGER NOT NULL DEFAULT 0, `trusted_network_ssids` TEXT NOT NULL DEFAULT '', `is_tunnel_on_ethernet_enabled` INTEGER NOT NULL DEFAULT 0, `is_tunnel_on_wifi_enabled` INTEGER NOT NULL DEFAULT 0, `is_wildcards_enabled` INTEGER NOT NULL DEFAULT 0, `is_stop_on_no_internet_enabled` INTEGER NOT NULL DEFAULT 0, `debounce_delay_seconds` INTEGER NOT NULL DEFAULT 3, `is_tunnel_on_unsecure_enabled` INTEGER NOT NULL DEFAULT 0, `wifi_detection_method` INTEGER NOT NULL DEFAULT 0, `start_on_boot` INTEGER NOT NULL DEFAULT 0)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isAutoTunnelEnabled", + "columnName": "is_tunnel_enabled", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "isTunnelOnMobileDataEnabled", + "columnName": "is_tunnel_on_mobile_data_enabled", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "trustedNetworkSSIDs", + "columnName": "trusted_network_ssids", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "isTunnelOnEthernetEnabled", + "columnName": "is_tunnel_on_ethernet_enabled", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "isTunnelOnWifiEnabled", + "columnName": "is_tunnel_on_wifi_enabled", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "isWildcardsEnabled", + "columnName": "is_wildcards_enabled", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "isStopOnNoInternetEnabled", + "columnName": "is_stop_on_no_internet_enabled", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "debounceDelaySeconds", + "columnName": "debounce_delay_seconds", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "3" + }, + { + "fieldPath": "isTunnelOnUnsecureEnabled", + "columnName": "is_tunnel_on_unsecure_enabled", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "wifiDetectionMethod", + "columnName": "wifi_detection_method", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "startOnBoot", + "columnName": "start_on_boot", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + } + }, + { + "tableName": "monitoring_settings", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_ping_enabled` INTEGER NOT NULL DEFAULT 0, `is_ping_monitoring_enabled` INTEGER NOT NULL DEFAULT 1, `tunnel_ping_interval_sec` INTEGER NOT NULL DEFAULT 30, `tunnel_ping_attempts` INTEGER NOT NULL DEFAULT 3, `tunnel_ping_timeout_sec` INTEGER, `show_detailed_ping_stats` INTEGER NOT NULL DEFAULT 0, `is_local_logs_enabled` INTEGER NOT NULL DEFAULT 0, `is_restart_on_handshake_timeout_enabled` INTEGER NOT NULL DEFAULT 0, `max_handshake_restart_attempts` INTEGER NOT NULL DEFAULT 5, `restart_cooldown_seconds` INTEGER NOT NULL DEFAULT 30, `is_recovery_notification_enabled` INTEGER NOT NULL DEFAULT 1, `max_attempts_action` INTEGER NOT NULL DEFAULT 0, `ping_failures_before_restart` INTEGER NOT NULL DEFAULT 1, `is_backoff_enabled` INTEGER NOT NULL DEFAULT 0, `backoff_max_attempts` INTEGER NOT NULL DEFAULT 3, `startup_grace_seconds` INTEGER NOT NULL DEFAULT 30)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isPingEnabled", + "columnName": "is_ping_enabled", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "isPingMonitoringEnabled", + "columnName": "is_ping_monitoring_enabled", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "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": "showDetailedPingStats", + "columnName": "show_detailed_ping_stats", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "isLocalLogsEnabled", + "columnName": "is_local_logs_enabled", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "isRestartOnHandshakeTimeoutEnabled", + "columnName": "is_restart_on_handshake_timeout_enabled", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "maxHandshakeRestartAttempts", + "columnName": "max_handshake_restart_attempts", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "5" + }, + { + "fieldPath": "restartCooldownSeconds", + "columnName": "restart_cooldown_seconds", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "30" + }, + { + "fieldPath": "isRecoveryNotificationEnabled", + "columnName": "is_recovery_notification_enabled", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "maxAttemptsAction", + "columnName": "max_attempts_action", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "pingFailuresBeforeRestart", + "columnName": "ping_failures_before_restart", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "isBackoffEnabled", + "columnName": "is_backoff_enabled", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "backoffMaxAttempts", + "columnName": "backoff_max_attempts", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "3" + }, + { + "fieldPath": "startupGraceSeconds", + "columnName": "startup_grace_seconds", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "30" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + } + }, + { + "tableName": "dns_settings", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `dns_protocol` INTEGER NOT NULL DEFAULT 0, `dns_endpoint` TEXT, `global_tunnel_dns_enabled` INTEGER NOT NULL DEFAULT 0)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "dnsProtocol", + "columnName": "dns_protocol", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "dnsEndpoint", + "columnName": "dns_endpoint", + "affinity": "TEXT" + }, + { + "fieldPath": "isGlobalTunnelDnsEnabled", + "columnName": "global_tunnel_dns_enabled", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + } + }, + { + "tableName": "lockdown_settings", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `bypass_lan` INTEGER NOT NULL DEFAULT 0, `metered` INTEGER NOT NULL DEFAULT 0, `dual_stack` INTEGER NOT NULL DEFAULT 0)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "bypassLan", + "columnName": "bypass_lan", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "metered", + "columnName": "metered", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "dualStack", + "columnName": "dual_stack", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "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, '3f5d07c1a895d1a3d28c03ce2c2877c0')" + ] + } +} \ No newline at end of file diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/AppDatabase.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/AppDatabase.kt old mode 100644 new mode 100755 index 29413f5d4..18be1232e --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/AppDatabase.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/AppDatabase.kt @@ -17,7 +17,7 @@ import com.zaneschepke.wireguardautotunnel.data.entity.* DnsSettings::class, LockdownSettings::class, ], - version = 29, + version = 34, autoMigrations = [ AutoMigration(from = 1, to = 2), @@ -45,6 +45,11 @@ import com.zaneschepke.wireguardautotunnel.data.entity.* AutoMigration(from = 24, to = 25), AutoMigration(from = 26, to = 27, spec = GlobalsMigration::class), AutoMigration(from = 27, to = 28, spec = DonationMigration::class), + AutoMigration(from = 29, to = 30), + AutoMigration(from = 30, to = 31, spec = DropBackoffMaxAttemptsMigration::class), + AutoMigration(from = 31, to = 32, spec = RenameMaxRestartAttemptsMigration::class), + AutoMigration(from = 32, to = 33, spec = DropStartupGraceMigration::class), + AutoMigration(from = 33, to = 34, spec = DropRecoveryNotificationMigration::class), ], exportSchema = true, ) @@ -129,3 +134,19 @@ class GlobalsMigration : AutoMigrationSpec @DeleteColumn(tableName = "general_settings", columnName = "custom_split_packages") class DonationMigration : AutoMigrationSpec + +@DeleteColumn(tableName = "monitoring_settings", columnName = "backoff_max_attempts") +class DropBackoffMaxAttemptsMigration : AutoMigrationSpec + +@RenameColumn( + tableName = "monitoring_settings", + fromColumnName = "max_handshake_restart_attempts", + toColumnName = "max_restart_attempts", +) +class RenameMaxRestartAttemptsMigration : AutoMigrationSpec + +@DeleteColumn(tableName = "monitoring_settings", columnName = "startup_grace_seconds") +class DropStartupGraceMigration : AutoMigrationSpec + +@DeleteColumn(tableName = "monitoring_settings", columnName = "is_recovery_notification_enabled") +class DropRecoveryNotificationMigration : AutoMigrationSpec diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/DatabaseConverters.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/DatabaseConverters.kt old mode 100644 new mode 100755 index 5bb0ddeaf..7c87cff8e --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/DatabaseConverters.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/DatabaseConverters.kt @@ -3,6 +3,7 @@ package com.zaneschepke.wireguardautotunnel.data import androidx.room.TypeConverter import com.zaneschepke.wireguardautotunnel.data.model.AppMode import com.zaneschepke.wireguardautotunnel.data.model.DnsProtocol +import com.zaneschepke.wireguardautotunnel.data.model.MaxAttemptsAction import com.zaneschepke.wireguardautotunnel.data.model.WifiDetectionMethod import kotlinx.serialization.json.Json @@ -64,4 +65,9 @@ class DatabaseConverters { @TypeConverter fun toDnsProtocol(value: Int): DnsProtocol = DnsProtocol.fromValue(value) @TypeConverter fun fromDnsProtocol(mode: DnsProtocol): Int = mode.value + + @TypeConverter + fun toMaxAttemptsAction(value: Int): MaxAttemptsAction = MaxAttemptsAction.fromValue(value) + + @TypeConverter fun fromMaxAttemptsAction(action: MaxAttemptsAction): Int = action.value } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/entity/MonitoringSettings.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/entity/MonitoringSettings.kt old mode 100644 new mode 100755 index db97e3d7f..7df952b90 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/entity/MonitoringSettings.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/entity/MonitoringSettings.kt @@ -3,6 +3,7 @@ package com.zaneschepke.wireguardautotunnel.data.entity import androidx.room.ColumnInfo import androidx.room.Entity import androidx.room.PrimaryKey +import com.zaneschepke.wireguardautotunnel.data.model.MaxAttemptsAction @Entity(tableName = "monitoring_settings") data class MonitoringSettings( @@ -18,4 +19,15 @@ data class MonitoringSettings( val showDetailedPingStats: Boolean = false, @ColumnInfo(name = "is_local_logs_enabled", defaultValue = "0") val isLocalLogsEnabled: Boolean = false, + @ColumnInfo(name = "is_restart_on_handshake_timeout_enabled", defaultValue = "0") + val isRestartOnHandshakeTimeoutEnabled: Boolean = false, + @ColumnInfo(name = "max_restart_attempts", defaultValue = "5") val maxRestartAttempts: Int = 5, + @ColumnInfo(name = "restart_cooldown_seconds", defaultValue = "30") + val restartCooldownSeconds: Int = 30, + @ColumnInfo(name = "max_attempts_action", defaultValue = "0") + val maxAttemptsAction: MaxAttemptsAction = MaxAttemptsAction.DO_NOTHING, + @ColumnInfo(name = "ping_failures_before_restart", defaultValue = "1") + val pingFailuresBeforeRestart: Int = 1, + @ColumnInfo(name = "is_backoff_enabled", defaultValue = "0") + val isBackoffEnabled: Boolean = false, ) diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/mapper/MonitoringSettingsMapper.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/mapper/MonitoringSettingsMapper.kt old mode 100644 new mode 100755 index 35fc8f7e8..e16680e4f --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/mapper/MonitoringSettingsMapper.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/mapper/MonitoringSettingsMapper.kt @@ -13,6 +13,12 @@ fun Entity.toDomain(): Domain = tunnelPingTimeoutSeconds = tunnelPingTimeoutSeconds, showDetailedPingStats = showDetailedPingStats, isLocalLogsEnabled = isLocalLogsEnabled, + isRestartOnHandshakeTimeoutEnabled = isRestartOnHandshakeTimeoutEnabled, + maxRestartAttempts = maxRestartAttempts, + restartCooldownSeconds = restartCooldownSeconds, + maxAttemptsAction = maxAttemptsAction, + pingFailuresBeforeRestart = pingFailuresBeforeRestart, + isBackoffEnabled = isBackoffEnabled, ) fun Domain.toEntity(): Entity = @@ -25,4 +31,10 @@ fun Domain.toEntity(): Entity = tunnelPingTimeoutSeconds = tunnelPingTimeoutSeconds, showDetailedPingStats = showDetailedPingStats, isLocalLogsEnabled = isLocalLogsEnabled, + isRestartOnHandshakeTimeoutEnabled = isRestartOnHandshakeTimeoutEnabled, + maxRestartAttempts = maxRestartAttempts, + restartCooldownSeconds = restartCooldownSeconds, + maxAttemptsAction = maxAttemptsAction, + pingFailuresBeforeRestart = pingFailuresBeforeRestart, + isBackoffEnabled = isBackoffEnabled, ) diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/model/MaxAttemptsAction.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/model/MaxAttemptsAction.kt new file mode 100644 index 000000000..aa2e7abe8 --- /dev/null +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/model/MaxAttemptsAction.kt @@ -0,0 +1,11 @@ +package com.zaneschepke.wireguardautotunnel.data.model + +enum class MaxAttemptsAction(val value: Int) { + DO_NOTHING(0), + STOP_TUNNEL(1); + + companion object { + fun fromValue(value: Int): MaxAttemptsAction = + entries.find { it.value == value } ?: DO_NOTHING + } +} diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/domain/events/BackendMessage.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/domain/events/BackendMessage.kt old mode 100644 new mode 100755 index 92ee21b32..edfd5ab84 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/domain/events/BackendMessage.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/domain/events/BackendMessage.kt @@ -5,12 +5,50 @@ import com.zaneschepke.wireguardautotunnel.util.StringValue sealed class BackendMessage { + enum class RestartReason { + PING_FAILURE + } + data object DynamicDnsSuccess : BackendMessage() - fun toStringRes() = + data class ConnectionDegrading( + val reason: RestartReason, + val attempt: Int, + val maxAttempts: Int, + ) : BackendMessage() + + data object ConnectionRestored : BackendMessage() + + data class ConnectionPermanentlyLost( + val reason: RestartReason, + val totalAttempts: Int, + val isTunnelStopped: Boolean = false, + ) : BackendMessage() + + data object ConnectionCancelled : BackendMessage() + + fun toStringValue(): StringValue? = when (this) { - DynamicDnsSuccess -> R.string.ddns_success_message + DynamicDnsSuccess -> StringValue.StringResource(R.string.ddns_success_message) + is ConnectionDegrading -> + StringValue.StringResource( + R.string.snackbar_connection_degrading, + attempt.toString(), + maxAttempts.toString(), + ) + ConnectionRestored -> StringValue.StringResource(R.string.snackbar_connection_restored) + is ConnectionPermanentlyLost -> + if (isTunnelStopped) { + StringValue.StringResource( + R.string.snackbar_connection_lost_stopped, + totalAttempts.toString(), + ) + } else { + StringValue.StringResource( + R.string.snackbar_connection_lost, + totalAttempts.toString(), + ) + } + ConnectionCancelled -> null } - - fun toStringValue() = StringValue.StringResource(this.toStringRes()) } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/domain/model/MonitoringSettings.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/domain/model/MonitoringSettings.kt old mode 100644 new mode 100755 index 627df646b..fbac3784e --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/domain/model/MonitoringSettings.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/domain/model/MonitoringSettings.kt @@ -1,5 +1,7 @@ package com.zaneschepke.wireguardautotunnel.domain.model +import com.zaneschepke.wireguardautotunnel.data.model.MaxAttemptsAction + data class MonitoringSettings( val id: Int = 0, val isPingEnabled: Boolean = false, @@ -9,4 +11,10 @@ data class MonitoringSettings( val tunnelPingTimeoutSeconds: Int? = null, val showDetailedPingStats: Boolean = false, val isLocalLogsEnabled: Boolean = false, + val isRestartOnHandshakeTimeoutEnabled: Boolean = false, + val maxRestartAttempts: Int = 5, + val restartCooldownSeconds: Int = 30, + val maxAttemptsAction: MaxAttemptsAction = MaxAttemptsAction.DO_NOTHING, + val pingFailuresBeforeRestart: Int = 1, + val isBackoffEnabled: Boolean = false, ) diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/domain/state/TunnelRestartProgress.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/domain/state/TunnelRestartProgress.kt new file mode 100755 index 000000000..4349b7348 --- /dev/null +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/domain/state/TunnelRestartProgress.kt @@ -0,0 +1,17 @@ +package com.zaneschepke.wireguardautotunnel.domain.state + +import com.zaneschepke.wireguardautotunnel.domain.events.BackendMessage + +data class TunnelRestartProgress( + val isRestarting: Boolean = false, + val isVerifying: Boolean = false, + val attemptNumber: Int = 0, + val maxAttempts: Int = 0, + // Epoch ms when the post-restart cooldown ends; 0 = no pending cooldown + val nextRetryAtMillis: Long = 0L, + val reason: BackendMessage.RestartReason? = null, + // Non-empty when reason == PING_FAILURE, lists the unreachable targets + val failingPingTargets: List = emptyList(), + // Cumulative count of restarts since the handler started monitoring this tunnel + val totalRestarts: Int = 0, +) From da243a97cd348de038ebdeb3eead872cade43ad5 Mon Sep 17 00:00:00 2001 From: Fabien Date: Tue, 10 Mar 2026 14:16:43 +0100 Subject: [PATCH 2/8] feat(core): auto-restart tunnel on ping failure with backoff and verification MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implements HandshakeRestartHandler, a coroutine-based state machine that monitors ping health and automatically restarts the tunnel when consecutive ping failures exceed the configured threshold. Restart flow: 1. N consecutive ping failures → stop + restart tunnel (attempt 1/max) 2. 5 s verification ping after tunnel comes UP confirms recovery 3. On verification failure → exponential (or fixed) cooldown, then retry 4. Pings remain active during cooldown → early recovery skips next restart 5. After max attempts: emit ConnectionPermanentlyLost; if DO_NOTHING, suspend until natural ping recovery then re-arm automatically 6. On successful verification or natural recovery → emit ConnectionRestored, reset counter, re-arm monitor Edge cases handled: - Abort restart cycle when auto-tunnel switches to a different tunnel - Skip unnecessary restart when ping recovers during cooldown - Always poll WireGuard stats regardless of Doze mode (prerequisite fix) TunnelMonitoringHandler wires HandshakeRestartHandler alongside the existing ping/handshake monitors. TunnelManager exposes restart progress state. Co-Authored-By: Claude Sonnet 4.6 --- .../core/notification/NotificationMonitor.kt | 3 +- .../core/tunnel/TunnelLifecycleManager.kt | 3 + .../core/tunnel/TunnelManager.kt | 30 +- .../core/tunnel/TunnelProvider.kt | 2 + .../tunnel/handler/HandshakeRestartHandler.kt | 358 ++++++++++++++++++ .../tunnel/handler/TunnelMonitoringHandler.kt | 8 +- 6 files changed, 400 insertions(+), 4 deletions(-) create mode 100644 app/src/main/java/com/zaneschepke/wireguardautotunnel/core/tunnel/handler/HandshakeRestartHandler.kt diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/core/notification/NotificationMonitor.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/core/notification/NotificationMonitor.kt index 74e0cf4d1..47942ca33 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/core/notification/NotificationMonitor.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/core/notification/NotificationMonitor.kt @@ -42,6 +42,7 @@ class NotificationMonitor( private suspend fun handleTunnelMessages() = tunnelManager.messageEvents.collectLatest { (tunName, message) -> + val description = message.toStringValue() ?: return@collectLatest if (!WireGuardAutoTunnel.uiActive.value) { val notification = notificationManager.createNotification( @@ -49,7 +50,7 @@ class NotificationMonitor( title = tunName?.let { StringValue.DynamicString(it) } ?: StringValue.StringResource(R.string.tunnel), - description = message.toStringValue(), + description = description, groupKey = NotificationManager.VPN_GROUP_KEY, ) notificationManager.show( diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/core/tunnel/TunnelLifecycleManager.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/core/tunnel/TunnelLifecycleManager.kt index d2f18174d..268af776d 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/core/tunnel/TunnelLifecycleManager.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/core/tunnel/TunnelLifecycleManager.kt @@ -9,6 +9,7 @@ import com.zaneschepke.wireguardautotunnel.domain.events.UnknownError import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConfig import com.zaneschepke.wireguardautotunnel.domain.state.LogHealthState import com.zaneschepke.wireguardautotunnel.domain.state.PingState +import com.zaneschepke.wireguardautotunnel.domain.state.TunnelRestartProgress import com.zaneschepke.wireguardautotunnel.domain.state.TunnelState import com.zaneschepke.wireguardautotunnel.domain.state.TunnelStatistics import java.util.concurrent.ConcurrentHashMap @@ -39,6 +40,8 @@ class TunnelLifecycleManager( ) : TunnelProvider { override val activeTunnels: StateFlow> = sharedActiveTunnels.asStateFlow() + override val restartProgress: StateFlow> = + MutableStateFlow(emptyMap()) private val _errorEvents = MutableSharedFlow>() override val errorEvents: SharedFlow> = diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/core/tunnel/TunnelManager.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/core/tunnel/TunnelManager.kt index 50a717a0a..0e3626e09 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/core/tunnel/TunnelManager.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/core/tunnel/TunnelManager.kt @@ -6,6 +6,7 @@ import com.zaneschepke.networkmonitor.NetworkMonitor import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager import com.zaneschepke.wireguardautotunnel.core.tunnel.backend.TunnelBackend import com.zaneschepke.wireguardautotunnel.core.tunnel.handler.DynamicDnsHandler +import com.zaneschepke.wireguardautotunnel.core.tunnel.handler.HandshakeRestartHandler import com.zaneschepke.wireguardautotunnel.core.tunnel.handler.TunnelActiveStatePersister import com.zaneschepke.wireguardautotunnel.core.tunnel.handler.TunnelMonitorHandler import com.zaneschepke.wireguardautotunnel.core.tunnel.handler.TunnelServiceHandler @@ -25,6 +26,7 @@ import com.zaneschepke.wireguardautotunnel.domain.repository.MonitoringSettingsR import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository import com.zaneschepke.wireguardautotunnel.domain.state.LogHealthState import com.zaneschepke.wireguardautotunnel.domain.state.PingState +import com.zaneschepke.wireguardautotunnel.domain.state.TunnelRestartProgress import com.zaneschepke.wireguardautotunnel.domain.state.TunnelState import com.zaneschepke.wireguardautotunnel.domain.state.TunnelStatistics import com.zaneschepke.wireguardautotunnel.util.network.NetworkUtils @@ -47,6 +49,7 @@ import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.flow.merge import kotlinx.coroutines.flow.shareIn +import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import kotlinx.coroutines.plus import kotlinx.coroutines.supervisorScope @@ -59,10 +62,10 @@ class TunnelManager( userspaceBackend: TunnelBackend, proxyUserspaceBackend: TunnelBackend, networkMonitor: NetworkMonitor, - networkUtils: NetworkUtils, + private val networkUtils: NetworkUtils, powerManager: PowerManager, logReader: LogReader, - monitoringSettingsRepository: MonitoringSettingsRepository, + private val monitoringSettingsRepository: MonitoringSettingsRepository, private val serviceManager: ServiceManager, private val settingsRepository: GeneralSettingRepository, private val autoTunnelSettingsRepository: AutoTunnelSettingsRepository, @@ -75,6 +78,10 @@ class TunnelManager( private val _activeTunnels = MutableStateFlow>(emptyMap()) override val activeTunnels: StateFlow> = _activeTunnels.asStateFlow() + private val _restartProgress = MutableStateFlow>(emptyMap()) + override val restartProgress: StateFlow> = + _restartProgress.asStateFlow() + @OptIn(ExperimentalAtomicApi::class) val currentAppMode = AtomicReference(AppMode.VPN) private val defaultManager = @@ -201,6 +208,7 @@ class TunnelManager( networkUtils = networkUtils, powerManager = powerManager, logReader = logReader, + restartProgress = _restartProgress, getStatistics = { id -> getStatistics(id) }, updateTunnelStatus = { id, status, stats, pings, logHealth -> updateTunnelStatus(id, status, stats, pings, logHealth) @@ -209,6 +217,24 @@ class TunnelManager( ioDispatcher = ioDispatcher, ) + private val handshakeRestartHandler = + HandshakeRestartHandler( + activeTunnels = activeTunnels, + tunnelsRepository = tunnelsRepository, + monitoringSettingsRepository = monitoringSettingsRepository, + networkUtils = networkUtils, + stopTunnel = { id -> getProvider().stopTunnel(id) }, + startTunnel = { config -> startTunnel(config) }, + updateProgress = { id, progress -> + _restartProgress.update { current -> + if (progress == null) current - id else current + (id to progress) + } + }, + emitMessage = { name, msg -> localMessageEvents.emit(name to msg) }, + applicationScope = applicationScope, + ioDispatcher = ioDispatcher, + ) + init { applicationScope.launch(ioDispatcher) { val initialEmit = AtomicBoolean(true) diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/core/tunnel/TunnelProvider.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/core/tunnel/TunnelProvider.kt index 63a5dc4bc..8ac0113ba 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/core/tunnel/TunnelProvider.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/core/tunnel/TunnelProvider.kt @@ -7,6 +7,7 @@ import com.zaneschepke.wireguardautotunnel.domain.events.BackendMessage import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConfig import com.zaneschepke.wireguardautotunnel.domain.state.LogHealthState import com.zaneschepke.wireguardautotunnel.domain.state.PingState +import com.zaneschepke.wireguardautotunnel.domain.state.TunnelRestartProgress import com.zaneschepke.wireguardautotunnel.domain.state.TunnelState import com.zaneschepke.wireguardautotunnel.domain.state.TunnelStatistics import kotlinx.coroutines.flow.SharedFlow @@ -32,6 +33,7 @@ interface TunnelProvider { fun getStatistics(tunnelId: Int): TunnelStatistics? val activeTunnels: StateFlow> + val restartProgress: StateFlow> val errorEvents: SharedFlow> val messageEvents: SharedFlow> diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/core/tunnel/handler/HandshakeRestartHandler.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/core/tunnel/handler/HandshakeRestartHandler.kt new file mode 100644 index 000000000..fe9e33519 --- /dev/null +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/core/tunnel/handler/HandshakeRestartHandler.kt @@ -0,0 +1,358 @@ +package com.zaneschepke.wireguardautotunnel.core.tunnel.handler + +import com.zaneschepke.wireguardautotunnel.core.tunnel.handler.TunnelMonitorHandler.Companion.CLOUDFLARE_IPV4_IP +import com.zaneschepke.wireguardautotunnel.data.model.MaxAttemptsAction +import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelStatus +import com.zaneschepke.wireguardautotunnel.domain.events.BackendMessage +import com.zaneschepke.wireguardautotunnel.domain.model.MonitoringSettings +import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConfig +import com.zaneschepke.wireguardautotunnel.domain.repository.MonitoringSettingsRepository +import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository +import com.zaneschepke.wireguardautotunnel.domain.state.TunnelRestartProgress +import com.zaneschepke.wireguardautotunnel.domain.state.TunnelState +import com.zaneschepke.wireguardautotunnel.util.extensions.toMillis +import com.zaneschepke.wireguardautotunnel.util.network.NetworkUtils +import java.util.concurrent.ConcurrentHashMap +import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.mapNotNull +import kotlinx.coroutines.launch +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock +import kotlinx.coroutines.withTimeoutOrNull +import timber.log.Timber + +class HandshakeRestartHandler( + private val activeTunnels: StateFlow>, + private val tunnelsRepository: TunnelRepository, + private val monitoringSettingsRepository: MonitoringSettingsRepository, + private val networkUtils: NetworkUtils, + private val stopTunnel: suspend (Int) -> Unit, + private val startTunnel: suspend (TunnelConfig) -> Unit, + private val updateProgress: (Int, TunnelRestartProgress?) -> Unit, + private val emitMessage: suspend (String?, BackendMessage) -> Unit, + private val applicationScope: CoroutineScope, + private val ioDispatcher: CoroutineDispatcher, +) { + private val mutex = Mutex() + private val jobs = ConcurrentHashMap() + + // Tracks tunnels that we stopped ourselves (so the outer collector doesn't cancel their job) + private val restarting = ConcurrentHashMap() + + init { + applicationScope.launch(ioDispatcher) { + activeTunnels.collect { activeTuns -> + mutex.withLock { + val activeIds = activeTuns.keys.toSet() + + val tunnelConfigs = tunnelsRepository.flow.first() + val knownIds = tunnelConfigs.map { it.id }.toSet() + + // Cancel jobs for tunnels that left activeTunnels + // Skip tunnels we're actively restarting (they leave temporarily during + // stop/start) + // BUT always cancel if the tunnel was deleted from DB + (jobs.keys - activeIds).forEach { id -> + if (restarting[id] != true || id !in knownIds) { + Timber.d("HandshakeRestartHandler: tunnel $id gone, cancelling job") + jobs.remove(id)?.cancel() + restarting.remove(id) + updateProgress(id, null) + } + } + + // Start a job for each newly active tunnel not already tracked + activeIds.forEach { id -> + if (jobs.containsKey(id)) return@forEach + val config = tunnelConfigs.find { it.id == id } ?: return@forEach + jobs[id] = + applicationScope.launch(ioDispatcher) { runRestartLoop(id, config) } + } + } + } + } + } + + /** Top-level loop for a single tunnel. Restarts whenever monitoring settings change. */ + private suspend fun runRestartLoop(tunnelId: Int, config: TunnelConfig) { + monitoringSettingsRepository.flow.collectLatest { settings -> + if (!settings.isAutoRestartActive()) { + updateProgress(tunnelId, null) + return@collectLatest + } + Timber.d("HandshakeRestartHandler: starting monitor for tunnel $tunnelId") + try { + monitorTunnel(tunnelId, config, settings) + } catch (e: CancellationException) { + throw e + } catch (e: Exception) { + Timber.e(e, "HandshakeRestartHandler: error monitoring tunnel $tunnelId") + } + } + } + + /** + * Monitoring state machine for one tunnel with fixed settings snapshot. Exits when max attempts + * are reached or the coroutine is cancelled. + */ + private suspend fun monitorTunnel( + tunnelId: Int, + config: TunnelConfig, + settings: MonitoringSettings, + ) { + // Wait for tunnel to be UP before doing anything + activeTunnels.mapNotNull { it[tunnelId] }.first { it.status is TunnelStatus.Up } + + val maxAttempts = settings.maxRestartAttempts + var attempt = 0 + var totalRestarts = 0 + + // Wait for the initial ping failure that triggers the first restart + var pingTarget = awaitPingFailures(tunnelId, settings) + + while (true) { + attempt++ + + if (attempt > maxAttempts) { + Timber.d( + "HandshakeRestartHandler: tunnel $tunnelId gave up after $maxAttempts attempts" + ) + updateProgress( + tunnelId, + TunnelRestartProgress( + attemptNumber = maxAttempts, + maxAttempts = maxAttempts, + nextRetryAtMillis = 0L, + reason = BackendMessage.RestartReason.PING_FAILURE, + totalRestarts = totalRestarts, + ), + ) + val isStopped = settings.maxAttemptsAction == MaxAttemptsAction.STOP_TUNNEL + emitMessage( + config.name, + BackendMessage.ConnectionPermanentlyLost( + BackendMessage.RestartReason.PING_FAILURE, + maxAttempts, + isStopped, + ), + ) + if (isStopped) { + mutex.withLock { restarting[tunnelId] = false } + runCatching { stopTunnel(tunnelId) } + updateProgress(tunnelId, null) + return + } + // DO_NOTHING: keep progress ("awaiting recovery"), wait for natural ping recovery + activeTunnels + .mapNotNull { it[tunnelId]?.pingStates } + .distinctUntilChanged() + .first { pingStates -> + pingStates.values.isNotEmpty() && pingStates.values.all { it.isReachable } + } + updateProgress(tunnelId, TunnelRestartProgress(totalRestarts = totalRestarts)) + emitMessage(config.name, BackendMessage.ConnectionRestored) + attempt = 0 + pingTarget = awaitPingFailures(tunnelId, settings) + continue + } + + // RESTARTING + Timber.d( + "HandshakeRestartHandler: restarting tunnel $tunnelId (attempt $attempt/$maxAttempts)" + ) + updateProgress( + tunnelId, + TunnelRestartProgress( + isRestarting = true, + attemptNumber = attempt, + maxAttempts = maxAttempts, + reason = BackendMessage.RestartReason.PING_FAILURE, + failingPingTargets = listOf(pingTarget), + totalRestarts = totalRestarts, + ), + ) + mutex.withLock { restarting[tunnelId] = true } + runCatching { stopTunnel(tunnelId) } + .onFailure { + Timber.e(it, "HandshakeRestartHandler: stop failed for tunnel $tunnelId") + } + delay(RESTART_SETTLE_DELAY_MS) + + // Abort if another tunnel took over while we were stopped (e.g. auto-tunnel switched) + val currentActive = activeTunnels.value + if (currentActive.isNotEmpty() && !currentActive.containsKey(tunnelId)) { + Timber.d( + "HandshakeRestartHandler: tunnel $tunnelId superseded by another tunnel, aborting restart" + ) + mutex.withLock { restarting[tunnelId] = false } + updateProgress(tunnelId, null) + return + } + + // Fetch fresh config in case tunnel was modified since handler started + val freshConfig = tunnelsRepository.getById(tunnelId) ?: config + runCatching { startTunnel(freshConfig) } + .onFailure { + Timber.e(it, "HandshakeRestartHandler: start failed for tunnel $tunnelId") + } + + // Wait for tunnel to come back UP (30s safety timeout) + val cameUp = + withTimeoutOrNull(TUNNEL_UP_TIMEOUT_MS) { + activeTunnels.mapNotNull { it[tunnelId] }.first { it.status is TunnelStatus.Up } + } + mutex.withLock { restarting[tunnelId] = false } + totalRestarts++ + + if (cameUp == null) { + Timber.w("HandshakeRestartHandler: tunnel $tunnelId did not come UP within timeout") + // Count as failed verification, will retry on next loop iteration + continue + } + + // VERIFYING — short settle then direct ping, before any cooldown + delay(VERIFY_SETTLE_DELAY_MS) + Timber.d("HandshakeRestartHandler: verifying tunnel $tunnelId via ping to $pingTarget") + updateProgress( + tunnelId, + TunnelRestartProgress( + isVerifying = true, + attemptNumber = attempt, + maxAttempts = maxAttempts, + reason = BackendMessage.RestartReason.PING_FAILURE, + failingPingTargets = listOf(pingTarget), + totalRestarts = totalRestarts, + ), + ) + + val timeout = + settings.tunnelPingTimeoutSeconds?.toMillis() + ?: (settings.tunnelPingAttempts * 2000L) + val pingResult = runCatching { + networkUtils.pingWithStats(pingTarget, settings.tunnelPingAttempts, timeout) + } + val recovered = pingResult.getOrNull()?.isReachable == true + + if (recovered) { + Timber.d( + "HandshakeRestartHandler: tunnel $tunnelId recovered after attempt $attempt" + ) + emitMessage(config.name, BackendMessage.ConnectionRestored) + // Reset attempt counter but keep totalRestarts visible + attempt = 0 + updateProgress(tunnelId, TunnelRestartProgress(totalRestarts = totalRestarts)) + pingTarget = awaitPingFailures(tunnelId, settings) + continue + } + + // COOLDOWN — only if there are remaining attempts + if (attempt < maxAttempts) { + val cooldownMs = + computeCooldown( + settings.restartCooldownSeconds, + attempt, + settings.isBackoffEnabled, + ) * 1000L + val cooldownEnd = System.currentTimeMillis() + cooldownMs + Timber.d( + "HandshakeRestartHandler: cooldown ${cooldownMs / 1000}s before next restart for tunnel $tunnelId" + ) + updateProgress( + tunnelId, + TunnelRestartProgress( + attemptNumber = attempt, + maxAttempts = maxAttempts, + nextRetryAtMillis = cooldownEnd, + reason = BackendMessage.RestartReason.PING_FAILURE, + failingPingTargets = listOf(pingTarget), + totalRestarts = totalRestarts, + ), + ) + + // If cooldown is longer than ping interval, race against periodic ping recovery + val pingIntervalMs = settings.tunnelPingIntervalSeconds.toMillis() + if (cooldownMs > pingIntervalMs) { + val recoveredDuringCooldown = + withTimeoutOrNull(cooldownMs) { + activeTunnels + .mapNotNull { it[tunnelId]?.pingStates } + .distinctUntilChanged() + .first { pingStates -> + pingStates.values.isNotEmpty() && + pingStates.values.all { it.isReachable } + } + true + } != null + + if (recoveredDuringCooldown) { + Timber.d( + "HandshakeRestartHandler: tunnel $tunnelId recovered during cooldown" + ) + emitMessage(config.name, BackendMessage.ConnectionRestored) + attempt = 0 + updateProgress( + tunnelId, + TunnelRestartProgress(totalRestarts = totalRestarts), + ) + pingTarget = awaitPingFailures(tunnelId, settings) + continue + } + } else { + delay(cooldownMs) + } + } + // If not recovered: loop continues → next restart attempt + } + } + + /** + * Suspends until [MonitoringSettings.pingFailuresBeforeRestart] consecutive ping cycles all + * report unreachable. Returns the ping target of the failing peer. + */ + private suspend fun awaitPingFailures(tunnelId: Int, settings: MonitoringSettings): String { + var consecutive = 0 + var target = CLOUDFLARE_IPV4_IP + + activeTunnels + .mapNotNull { it[tunnelId]?.pingStates } + .distinctUntilChanged() + .first { pingStates -> + val allFailing = + pingStates.values.isNotEmpty() && pingStates.values.all { !it.isReachable } + if (allFailing) { + pingStates.values + .firstOrNull { !it.isReachable } + ?.pingTarget + ?.let { target = it } + consecutive++ + } else { + consecutive = 0 + } + consecutive >= settings.pingFailuresBeforeRestart + } + + return target + } + + private fun MonitoringSettings.isAutoRestartActive(): Boolean = + isRestartOnHandshakeTimeoutEnabled && isPingEnabled + + private fun computeCooldown(baseSec: Int, attempt: Int, isBackoff: Boolean): Long = + if (isBackoff) baseSec.toLong() * (1L shl (attempt - 1).coerceAtMost(MAX_BACKOFF_SHIFT)) + else baseSec.toLong() + + companion object { + private const val TUNNEL_UP_TIMEOUT_MS = 30_000L + private const val RESTART_SETTLE_DELAY_MS = 300L + private const val VERIFY_SETTLE_DELAY_MS = 5_000L + private const val MAX_BACKOFF_SHIFT = 20 // cap at 2^20 (~12 days with 1s base) + } +} diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/core/tunnel/handler/TunnelMonitoringHandler.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/core/tunnel/handler/TunnelMonitoringHandler.kt index a8e888a9d..d14895185 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/core/tunnel/handler/TunnelMonitoringHandler.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/core/tunnel/handler/TunnelMonitoringHandler.kt @@ -12,6 +12,7 @@ import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository import com.zaneschepke.wireguardautotunnel.domain.state.FailureReason import com.zaneschepke.wireguardautotunnel.domain.state.LogHealthState import com.zaneschepke.wireguardautotunnel.domain.state.PingState +import com.zaneschepke.wireguardautotunnel.domain.state.TunnelRestartProgress import com.zaneschepke.wireguardautotunnel.domain.state.TunnelState import com.zaneschepke.wireguardautotunnel.domain.state.TunnelStatistics import com.zaneschepke.wireguardautotunnel.util.extensions.toMillis @@ -58,6 +59,7 @@ class TunnelMonitorHandler( private val networkUtils: NetworkUtils, private val logReader: LogReader, private val powerManager: PowerManager, + private val restartProgress: StateFlow>, private val getStatistics: (Int) -> TunnelStatistics?, private val updateTunnelStatus: suspend ( @@ -323,7 +325,11 @@ class TunnelMonitorHandler( while (isActive) { ensureActive() - if (!powerManager.isDeviceIdleMode) { + val activeRestart = + restartProgress.value[tunnelConfig.id]?.let { + it.isRestarting || it.isVerifying + } ?: false + if (!powerManager.isDeviceIdleMode && !activeRestart) { if (isNetworkConnected.value) { performPing() } else { From b39a60dff3843103c2490bc7fbd5b800ad95a99b Mon Sep 17 00:00:00 2001 From: Fabien Date: Tue, 10 Mar 2026 14:17:01 +0100 Subject: [PATCH 3/8] feat(ui): auto-restart settings screen with real-time progress display MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit AutoRestartScreen: configures auto-restart (enable/disable, ping failures before restart, cooldown, max attempts, exponential backoff, on-max-attempts action). Accessible from Settings → Tunnel monitoring. TunnelList: inline restart progress label below tunnel name shows the current phase — "restarting 1/3…", "verifying 1/3…", "restart 1/3 · next in 28s", "awaiting ping recovery" — and total restart counter alongside uptime ("uptime: 4m · ↺ 3"). Dot color forced to UNHEALTHY during active restart. MonitoringViewModel bridges MonitoringSettings persistence and exposes restartProgress state from TunnelManager to the UI layer. Snackbar notifications emitted on ConnectionRestored and ConnectionPermanentlyLost (always active, no per-setting toggle). Co-Authored-By: Claude Sonnet 4.6 --- .../wireguardautotunnel/MainActivity.kt | 2 + .../tunnel/handler/HandshakeRestartHandler.kt | 6 +- .../common/dropdown/LabelledNumberDropdown.kt | 2 + .../ui/navigation/Route.kt | 3 + .../currentBackStackEntryAsNavbarState.kt | 13 ++ .../ui/screens/settings/SettingsScreen.kt | 28 +++ .../monitoring/TunnelMonitoringScreen.kt | 0 .../autorestart/AutoRestartScreen.kt | 161 ++++++++++++++++++ .../screens/tunnels/components/TunnelList.kt | 109 ++++++++++-- .../tunnels/components/TunnelStatisticsRow.kt | 4 +- .../ui/state/TunnelsUiState.kt | 2 + .../viewmodel/MonitoringViewModel.kt | 42 ++++- .../viewmodel/SettingsViewModel.kt | 4 + .../viewmodel/SharedAppViewModel.kt | 6 +- app/src/main/res/values/strings.xml | 23 +++ 15 files changed, 387 insertions(+), 18 deletions(-) mode change 100644 => 100755 app/src/main/java/com/zaneschepke/wireguardautotunnel/MainActivity.kt mode change 100644 => 100755 app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/dropdown/LabelledNumberDropdown.kt mode change 100644 => 100755 app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/navigation/Route.kt mode change 100644 => 100755 app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/navigation/components/currentBackStackEntryAsNavbarState.kt mode change 100644 => 100755 app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/SettingsScreen.kt mode change 100644 => 100755 app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/monitoring/TunnelMonitoringScreen.kt create mode 100755 app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/monitoring/autorestart/AutoRestartScreen.kt mode change 100644 => 100755 app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/tunnels/components/TunnelList.kt mode change 100644 => 100755 app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/state/TunnelsUiState.kt mode change 100644 => 100755 app/src/main/java/com/zaneschepke/wireguardautotunnel/viewmodel/MonitoringViewModel.kt mode change 100644 => 100755 app/src/main/java/com/zaneschepke/wireguardautotunnel/viewmodel/SettingsViewModel.kt mode change 100644 => 100755 app/src/main/java/com/zaneschepke/wireguardautotunnel/viewmodel/SharedAppViewModel.kt mode change 100644 => 100755 app/src/main/res/values/strings.xml diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/MainActivity.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/MainActivity.kt old mode 100644 new mode 100755 index 3165f059d..2b888d122 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/MainActivity.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/MainActivity.kt @@ -95,6 +95,7 @@ import com.zaneschepke.wireguardautotunnel.ui.screens.settings.dns.DnsSettingsSc import com.zaneschepke.wireguardautotunnel.ui.screens.settings.integrations.AndroidIntegrationsScreen import com.zaneschepke.wireguardautotunnel.ui.screens.settings.lockdown.LockdownSettingsScreen import com.zaneschepke.wireguardautotunnel.ui.screens.settings.monitoring.TunnelMonitoringScreen +import com.zaneschepke.wireguardautotunnel.ui.screens.settings.monitoring.autorestart.AutoRestartScreen import com.zaneschepke.wireguardautotunnel.ui.screens.settings.monitoring.logs.LogsScreen import com.zaneschepke.wireguardautotunnel.ui.screens.settings.monitoring.ping.PingTargetScreen import com.zaneschepke.wireguardautotunnel.ui.screens.support.SupportScreen @@ -503,6 +504,7 @@ class MainActivity : AppCompatActivity() { PreferredTunnelScreen(key.tunnelNetwork) } entry { PingTargetScreen() } + entry { AutoRestartScreen() } }, ) } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/core/tunnel/handler/HandshakeRestartHandler.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/core/tunnel/handler/HandshakeRestartHandler.kt index fe9e33519..f42e14a36 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/core/tunnel/handler/HandshakeRestartHandler.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/core/tunnel/handler/HandshakeRestartHandler.kt @@ -239,7 +239,8 @@ class HandshakeRestartHandler( val pingResult = runCatching { networkUtils.pingWithStats(pingTarget, settings.tunnelPingAttempts, timeout) } - val recovered = pingResult.getOrNull()?.isReachable == true + val pingStats = pingResult.getOrNull() + val recovered = pingStats?.isReachable == true || pingStats?.transmitted == 0 if (recovered) { Timber.d( @@ -326,7 +327,8 @@ class HandshakeRestartHandler( .distinctUntilChanged() .first { pingStates -> val allFailing = - pingStates.values.isNotEmpty() && pingStates.values.all { !it.isReachable } + pingStates.values.isNotEmpty() && + pingStates.values.all { !it.isReachable && it.transmitted > 0 } if (allFailing) { pingStates.values .firstOrNull { !it.isReachable } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/dropdown/LabelledNumberDropdown.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/dropdown/LabelledNumberDropdown.kt old mode 100644 new mode 100755 index 2d7adba55..f3d0122ce --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/dropdown/LabelledNumberDropdown.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/dropdown/LabelledNumberDropdown.kt @@ -8,6 +8,7 @@ fun LabelledDropdown( title: String, description: (@Composable () -> Unit)? = null, leading: @Composable () -> Unit, + enabled: Boolean = true, onSelected: (T?) -> Unit, options: List, currentValue: T?, @@ -19,6 +20,7 @@ fun LabelledDropdown( leading = leading, title = title, description = description, + enabled = enabled, onClick = { isDropDownExpanded = true }, trailing = { DropdownSelector( diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/navigation/Route.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/navigation/Route.kt old mode 100644 new mode 100755 index 95bf8aa85..e89323785 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/navigation/Route.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/navigation/Route.kt @@ -75,6 +75,8 @@ sealed class Route : NavKey { @Keep @Serializable data class PreferredTunnel(val tunnelNetwork: TunnelNetwork) : Route() @Keep @Serializable data object PingTarget : Route() + + @Keep @Serializable data object AutoRestart : Route() } @Serializable @@ -128,6 +130,7 @@ enum class Tab( Route.Language, Route.Display, Route.PingTarget, + Route.AutoRestart, is Route.ConfigGlobal, Route.Logs -> SETTINGS is Route.Support, diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/navigation/components/currentBackStackEntryAsNavbarState.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/navigation/components/currentBackStackEntryAsNavbarState.kt old mode 100644 new mode 100755 index f7eb298a1..e6bf8095c --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/navigation/components/currentBackStackEntryAsNavbarState.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/navigation/components/currentBackStackEntryAsNavbarState.kt @@ -554,6 +554,19 @@ fun currentRouteAsNavbarState( topTitle = context.getString(R.string.ping_target), showBottomItems = true, ) + AutoRestart -> + NavbarState( + topLeading = { + IconButton(onClick = { navController.pop() }) { + Icon( + Icons.AutoMirrored.Rounded.ArrowBack, + stringResource(R.string.back), + ) + } + }, + topTitle = context.getString(R.string.auto_restart), + showBottomItems = true, + ) null -> NavbarState() } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/SettingsScreen.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/SettingsScreen.kt old mode 100644 new mode 100755 index f225940fc..efc5a74ea --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/SettingsScreen.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/SettingsScreen.kt @@ -14,11 +14,13 @@ import androidx.compose.material.icons.outlined.Dns import androidx.compose.material.icons.outlined.ExpandMore import androidx.compose.material.icons.outlined.NetworkPing import androidx.compose.material.icons.outlined.Pin +import androidx.compose.material.icons.outlined.RestartAlt import androidx.compose.material.icons.outlined.SettingsBackupRestore import androidx.compose.material.icons.outlined.ViewHeadline import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue @@ -237,6 +239,32 @@ fun SettingsScreen( }, onClick = { navController.push(Route.TunnelMonitoring) }, ) + SurfaceRow( + enabled = uiState.monitoring.isPingEnabled, + leading = { Icon(Icons.Outlined.RestartAlt, contentDescription = null) }, + title = stringResource(R.string.auto_restart), + description = + if (!uiState.monitoring.isPingEnabled) { + { + Text( + text = stringResource(R.string.use_ping_for_detection_description), + style = + MaterialTheme.typography.bodySmall.copy( + color = MaterialTheme.colorScheme.outline + ), + ) + } + } else null, + trailing = { modifier -> + SwitchWithDivider( + checked = uiState.monitoring.isRestartOnHandshakeTimeoutEnabled, + onClick = { viewModel.setRestartOnHandshakeTimeout(it) }, + enabled = uiState.monitoring.isPingEnabled, + modifier = modifier, + ) + }, + onClick = { navController.push(Route.AutoRestart) }, + ) SurfaceRow( leading = { Icon(Icons.Outlined.ViewHeadline, contentDescription = null) }, title = stringResource(R.string.local_logging), diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/monitoring/TunnelMonitoringScreen.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/monitoring/TunnelMonitoringScreen.kt old mode 100644 new mode 100755 diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/monitoring/autorestart/AutoRestartScreen.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/monitoring/autorestart/AutoRestartScreen.kt new file mode 100755 index 000000000..92f3d15b0 --- /dev/null +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/monitoring/autorestart/AutoRestartScreen.kt @@ -0,0 +1,161 @@ +package com.zaneschepke.wireguardautotunnel.ui.screens.settings.monitoring.autorestart + +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.automirrored.outlined.TrendingUp +import androidx.compose.material.icons.outlined.FilterAlt +import androidx.compose.material.icons.outlined.PowerSettingsNew +import androidx.compose.material.icons.outlined.Replay +import androidx.compose.material.icons.outlined.Timer +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.zaneschepke.wireguardautotunnel.R +import com.zaneschepke.wireguardautotunnel.data.model.MaxAttemptsAction +import com.zaneschepke.wireguardautotunnel.ui.common.button.SurfaceRow +import com.zaneschepke.wireguardautotunnel.ui.common.button.ThemedSwitch +import com.zaneschepke.wireguardautotunnel.ui.common.dropdown.LabelledDropdown +import com.zaneschepke.wireguardautotunnel.ui.common.label.GroupLabel +import com.zaneschepke.wireguardautotunnel.viewmodel.MonitoringViewModel +import org.koin.androidx.compose.koinViewModel + +@Composable +fun AutoRestartScreen(viewModel: MonitoringViewModel = koinViewModel()) { + val uiState by viewModel.container.stateFlow.collectAsStateWithLifecycle() + + if (uiState.isLoading) return + + val pingEnabled = uiState.monitoringSettings.isPingEnabled + + Column( + horizontalAlignment = Alignment.Start, + verticalArrangement = Arrangement.spacedBy(12.dp, Alignment.Top), + modifier = Modifier.fillMaxSize().verticalScroll(rememberScrollState()), + ) { + Column { + GroupLabel( + stringResource(R.string.auto_restart), + modifier = Modifier.padding(horizontal = 16.dp), + ) + if (!pingEnabled) { + Text( + text = stringResource(R.string.use_ping_for_detection_description), + style = + MaterialTheme.typography.bodySmall.copy( + color = MaterialTheme.colorScheme.outline + ), + modifier = Modifier.padding(horizontal = 16.dp), + ) + } + LabelledDropdown( + enabled = pingEnabled, + title = stringResource(R.string.ping_failures_before_restart), + leading = { Icon(Icons.Outlined.FilterAlt, contentDescription = null) }, + currentValue = uiState.monitoringSettings.pingFailuresBeforeRestart, + onSelected = { selected -> + selected?.let { viewModel.setPingFailuresBeforeRestart(it) } + }, + options = listOf(1, 2, 3, 4, 5), + optionToString = { it?.toString() ?: stringResource(R.string._default) }, + ) + LabelledDropdown( + enabled = pingEnabled, + title = stringResource(R.string.restart_cooldown), + leading = { Icon(Icons.Outlined.Timer, contentDescription = null) }, + currentValue = uiState.monitoringSettings.restartCooldownSeconds, + onSelected = { selected -> + selected?.let { viewModel.setRestartCooldownSeconds(it) } + }, + options = listOf(5, 10, 15, 30, 60, 120, 300), + optionToString = { it?.let { "${it}s" } ?: stringResource(R.string._default) }, + ) + SurfaceRow( + enabled = pingEnabled, + leading = { + Icon(Icons.AutoMirrored.Outlined.TrendingUp, contentDescription = null) + }, + title = stringResource(R.string.exponential_backoff), + description = { + Text( + text = stringResource(R.string.exponential_backoff_description), + style = + MaterialTheme.typography.bodySmall.copy( + color = MaterialTheme.colorScheme.outline + ), + ) + }, + trailing = { + ThemedSwitch( + checked = uiState.monitoringSettings.isBackoffEnabled, + onClick = { viewModel.setBackoffEnabled(it) }, + enabled = pingEnabled, + ) + }, + onClick = { + viewModel.setBackoffEnabled(!uiState.monitoringSettings.isBackoffEnabled) + }, + ) + LabelledDropdown( + enabled = pingEnabled, + title = stringResource(R.string.max_restart_attempts), + leading = { Icon(Icons.Outlined.Replay, contentDescription = null) }, + currentValue = uiState.monitoringSettings.maxRestartAttempts, + onSelected = { selected -> selected?.let { viewModel.setMaxRestartAttempts(it) } }, + options = + if (uiState.monitoringSettings.isBackoffEnabled) listOf(3, 4, 5, 6, 7, 8) + else listOf(3, 5, 10, 20), + optionToString = { n -> + if (n == null) return@LabelledDropdown stringResource(R.string._default) + val baseSec = uiState.monitoringSettings.restartCooldownSeconds.toLong() + val totalSec = + if (uiState.monitoringSettings.isBackoffEnabled) baseSec * ((1L shl n) - 1) + else baseSec * n + val display = + when { + totalSec < 60 -> "${totalSec}s" + totalSec < 3600 -> { + val m = totalSec / 60 + val s = totalSec % 60 + if (s == 0L) "${m}m" else "${m}m${s}s" + } + else -> { + val h = totalSec / 3600 + val m = (totalSec % 3600) / 60 + if (m == 0L) "${h}h" else "${h}h${m}m" + } + } + "$n attempts (~$display)" + }, + ) + LabelledDropdown( + enabled = pingEnabled, + title = stringResource(R.string.max_attempts_action), + leading = { Icon(Icons.Outlined.PowerSettingsNew, contentDescription = null) }, + currentValue = uiState.monitoringSettings.maxAttemptsAction, + onSelected = { selected -> selected?.let { viewModel.setMaxAttemptsAction(it) } }, + options = MaxAttemptsAction.entries.toList(), + optionToString = { action -> + when (action) { + MaxAttemptsAction.DO_NOTHING -> + stringResource(R.string.max_attempts_action_do_nothing) + MaxAttemptsAction.STOP_TUNNEL -> + stringResource(R.string.max_attempts_action_stop_tunnel) + null -> stringResource(R.string._default) + } + }, + ) + } + } +} diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/tunnels/components/TunnelList.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/tunnels/components/TunnelList.kt old mode 100644 new mode 100755 index c2fd53498..61ec135cc --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/tunnels/components/TunnelList.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/tunnels/components/TunnelList.kt @@ -12,7 +12,10 @@ import androidx.compose.foundation.rememberOverscrollEffect import androidx.compose.material.icons.Icons import androidx.compose.material.icons.rounded.Circle import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -24,6 +27,7 @@ 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.domain.events.BackendMessage import com.zaneschepke.wireguardautotunnel.domain.state.TunnelState import com.zaneschepke.wireguardautotunnel.ui.LocalNavController import com.zaneschepke.wireguardautotunnel.ui.common.button.SurfaceRow @@ -33,6 +37,7 @@ import com.zaneschepke.wireguardautotunnel.ui.state.TunnelsUiState import com.zaneschepke.wireguardautotunnel.util.extensions.asColor import com.zaneschepke.wireguardautotunnel.util.extensions.openWebUrl import com.zaneschepke.wireguardautotunnel.viewmodel.SharedAppViewModel +import kotlinx.coroutines.delay @OptIn(ExperimentalFoundationApi::class) @Composable @@ -75,18 +80,18 @@ fun TunnelList( remember(uiState.activeTunnels) { uiState.activeTunnels[tunnel.id] ?: TunnelState() } + val restartProgress = + remember(uiState.restartProgress) { uiState.restartProgress[tunnel.id] } val selected = remember(uiState.selectedTunnels) { uiState.selectedTunnels.any { it.id == tunnel.id } } - var leadingIconColor by - remember( - tunnelState.status, - tunnelState.logHealthState, - tunnelState.pingStates, - tunnelState.statistics, - ) { - mutableStateOf(tunnelState.health().asColor()) + + val frozenHealthColor = + if (restartProgress != null && restartProgress.attemptNumber > 0) { + TunnelState.Health.UNHEALTHY.asColor() + } else { + tunnelState.health().asColor() } SurfaceRow( @@ -95,11 +100,92 @@ fun TunnelList( Icon( Icons.Rounded.Circle, contentDescription = stringResource(R.string.tunnel_monitoring), - tint = leadingIconColor, + tint = frozenHealthColor, modifier = Modifier.size(14.dp), ) }, title = tunnel.name, + description = + if (restartProgress != null) { + { + // Countdown towards next retry (only shown during cooldown) + var secondsRemaining by + remember(restartProgress.nextRetryAtMillis) { + val ms = + restartProgress.nextRetryAtMillis - + System.currentTimeMillis() + mutableStateOf(if (ms > 0) (ms / 1000).toInt() else 0) + } + LaunchedEffect(restartProgress.nextRetryAtMillis) { + while (secondsRemaining > 0) { + delay(1000) + val ms = + restartProgress.nextRetryAtMillis - + System.currentTimeMillis() + secondsRemaining = if (ms > 0) (ms / 1000).toInt() else 0 + } + } + + val reasonText = + when (restartProgress.reason) { + BackendMessage.RestartReason.PING_FAILURE -> { + val targets = restartProgress.failingPingTargets + if (targets.isNotEmpty()) { + stringResource( + R.string.restart_reason_ping_failure_targets, + targets.joinToString(", "), + ) + } else { + stringResource(R.string.restart_reason_ping_failure) + } + } + null -> null + } + + val statusText: String? = + when { + restartProgress.isVerifying -> + stringResource( + R.string.restart_verifying, + restartProgress.attemptNumber, + restartProgress.maxAttempts, + ) + restartProgress.isRestarting -> + stringResource( + R.string.restart_restarting, + restartProgress.attemptNumber, + restartProgress.maxAttempts, + ) + secondsRemaining > 0 -> + stringResource( + R.string.restart_cooldown_countdown, + restartProgress.attemptNumber, + restartProgress.maxAttempts, + secondsRemaining, + ) + restartProgress.attemptNumber > 0 && + restartProgress.attemptNumber >= + restartProgress.maxAttempts && + restartProgress.nextRetryAtMillis == 0L -> + stringResource(R.string.restart_awaiting_recovery) + else -> null + } + + val descStyle = + MaterialTheme.typography.bodySmall.copy( + color = MaterialTheme.colorScheme.outline + ) + val isAwaitingRecovery = + restartProgress.attemptNumber > 0 && + restartProgress.attemptNumber >= restartProgress.maxAttempts + if (reasonText != null && !isAwaitingRecovery) { + Text(text = reasonText, style = descStyle) + } + if (statusText != null) { + Text(text = statusText, style = descStyle) + } + } + } else null, onClick = { if (uiState.selectedTunnels.isNotEmpty()) { viewModel.toggleSelectedTunnel(tunnel.id) @@ -110,20 +196,21 @@ fun TunnelList( }, selected = selected, expandedContent = - if (!tunnelState.status.isDown()) { + if (!tunnelState.status.isDown() || restartProgress != null) { { TunnelStatisticsRow( tunnel, tunnelState, uiState.isPingEnabled, uiState.showPingStats, + totalRestarts = restartProgress?.totalRestarts ?: 0, ) } } else null, onLongClick = { viewModel.toggleSelectedTunnel(tunnel.id) }, trailing = { modifier -> SwitchWithDivider( - checked = tunnelState.status.isUpOrStarting(), + checked = tunnelState.status.isUpOrStarting() || restartProgress != null, onClick = { checked -> if (checked) viewModel.startTunnel(tunnel) else viewModel.stopTunnel(tunnel) diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/tunnels/components/TunnelStatisticsRow.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/tunnels/components/TunnelStatisticsRow.kt index a6614d6fe..daa9517bf 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/tunnels/components/TunnelStatisticsRow.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/tunnels/components/TunnelStatisticsRow.kt @@ -31,6 +31,7 @@ fun TunnelStatisticsRow( tunnelState: TunnelState, pingEnabled: Boolean, showDetailedStats: Boolean, + totalRestarts: Int = 0, ) { val context = LocalContext.current val textStyle = MaterialTheme.typography.bodySmall @@ -73,8 +74,9 @@ fun TunnelStatisticsRow( verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(16.dp), ) { + val restartSuffix = if (totalRestarts > 0) " · ↺ $totalRestarts" else "" Text( - "uptime: ${tunnelState.uptime().localizedDuration(locale)}", + "uptime: ${tunnelState.uptime().localizedDuration(locale)}$restartSuffix", style = textStyle, color = textColor, ) diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/state/TunnelsUiState.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/state/TunnelsUiState.kt old mode 100644 new mode 100755 index 1b6ead927..431c2a3e6 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/state/TunnelsUiState.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/state/TunnelsUiState.kt @@ -1,6 +1,7 @@ package com.zaneschepke.wireguardautotunnel.ui.state import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConfig +import com.zaneschepke.wireguardautotunnel.domain.state.TunnelRestartProgress import com.zaneschepke.wireguardautotunnel.domain.state.TunnelState data class TunnelsUiState( @@ -9,5 +10,6 @@ data class TunnelsUiState( val selectedTunnels: List = emptyList(), val isPingEnabled: Boolean = false, val showPingStats: Boolean = false, + val restartProgress: Map = emptyMap(), val isLoading: Boolean = true, ) diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/viewmodel/MonitoringViewModel.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/viewmodel/MonitoringViewModel.kt old mode 100644 new mode 100755 index a160685de..978cdb52d --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/viewmodel/MonitoringViewModel.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/viewmodel/MonitoringViewModel.kt @@ -1,6 +1,7 @@ package com.zaneschepke.wireguardautotunnel.viewmodel import androidx.lifecycle.ViewModel +import com.zaneschepke.wireguardautotunnel.data.model.MaxAttemptsAction import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConfig import com.zaneschepke.wireguardautotunnel.domain.repository.MonitoringSettingsRepository import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository @@ -11,7 +12,6 @@ import org.orbitmvi.orbit.viewmodel.container class MonitoringViewModel( private val monitoringSettingsRepository: MonitoringSettingsRepository, - private val tunnelRepository: TunnelRepository, private val tunnelsRepository: TunnelRepository, ) : ContainerHost, ViewModel() { @@ -20,7 +20,7 @@ class MonitoringViewModel( MonitoringUiState(), buildSettings = { repeatOnSubscribedStopTimeout = 5000L }, ) { - combine(monitoringSettingsRepository.flow, tunnelRepository.userTunnelsFlow) { + combine(monitoringSettingsRepository.flow, tunnelsRepository.userTunnelsFlow) { monitoringSettings, tunnels -> state.copy( @@ -57,4 +57,42 @@ class MonitoringViewModel( fun setPingTarget(tunnel: TunnelConfig, target: String?) = intent { tunnelsRepository.save(tunnel.copy(pingTarget = target?.ifBlank { null })) } + + fun setRestartOnHandshakeTimeout(to: Boolean) = intent { + monitoringSettingsRepository.upsert( + state.monitoringSettings.copy(isRestartOnHandshakeTimeoutEnabled = to) + ) + } + + fun setMaxRestartAttempts(to: Int) = intent { + monitoringSettingsRepository.upsert(state.monitoringSettings.copy(maxRestartAttempts = to)) + } + + fun setRestartCooldownSeconds(to: Int) = intent { + monitoringSettingsRepository.upsert( + state.monitoringSettings.copy(restartCooldownSeconds = to) + ) + } + + fun setMaxAttemptsAction(to: MaxAttemptsAction) = intent { + monitoringSettingsRepository.upsert(state.monitoringSettings.copy(maxAttemptsAction = to)) + } + + fun setPingFailuresBeforeRestart(to: Int) = intent { + monitoringSettingsRepository.upsert( + state.monitoringSettings.copy(pingFailuresBeforeRestart = to) + ) + } + + fun setBackoffEnabled(to: Boolean) = intent { + val clampedAttempts = + if (to) minOf(state.monitoringSettings.maxRestartAttempts, 8) + else state.monitoringSettings.maxRestartAttempts + monitoringSettingsRepository.upsert( + state.monitoringSettings.copy( + isBackoffEnabled = to, + maxRestartAttempts = clampedAttempts, + ) + ) + } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/viewmodel/SettingsViewModel.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/viewmodel/SettingsViewModel.kt old mode 100644 new mode 100755 index 17d7460ba..021e5f200 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/viewmodel/SettingsViewModel.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/viewmodel/SettingsViewModel.kt @@ -86,6 +86,10 @@ class SettingsViewModel( monitoringRepository.upsert(state.monitoring.copy(isPingEnabled = to)) } + fun setRestartOnHandshakeTimeout(to: Boolean) = intent { + monitoringRepository.upsert(state.monitoring.copy(isRestartOnHandshakeTimeoutEnabled = to)) + } + fun setRemoteEnabled(to: Boolean) = intent { settingsRepository.upsert( state.settings.copy( diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/viewmodel/SharedAppViewModel.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/viewmodel/SharedAppViewModel.kt old mode 100644 new mode 100755 index ce326db19..ffb6b8081 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/viewmodel/SharedAppViewModel.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/viewmodel/SharedAppViewModel.kt @@ -76,13 +76,15 @@ class SharedAppViewModel( monitoringSettingsRepository.flow, tunnelManager.activeTunnels, selectedTunnelsRepository.flow, - ) { tunnels, monitoringSettings, activeTuns, selectedTuns -> + tunnelManager.restartProgress, + ) { tunnels, monitoringSettings, activeTuns, selectedTuns, restartProgress -> TunnelsUiState( tunnels = tunnels, isPingEnabled = monitoringSettings.isPingEnabled, showPingStats = monitoringSettings.showDetailedPingStats, activeTunnels = activeTuns, selectedTunnels = selectedTuns, + restartProgress = restartProgress, isLoading = false, ) } @@ -132,7 +134,7 @@ class SharedAppViewModel( intent { tunnelManager.messageEvents.collect { (_, message) -> - postSideEffect(GlobalSideEffect.Snackbar(message.toStringValue())) + message.toStringValue()?.let { postSideEffect(GlobalSideEffect.Snackbar(it)) } } } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml old mode 100644 new mode 100755 index f2797ec25..ad7efc37f --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -466,4 +466,27 @@ https://apt.izzysoft.de/fdroid/index/apk/com.zaneschepke.wireguardautotunnel The URL must be secure and serve a .conf file. %1$s only + Auto-restart + Auto-restart on tunnel failure + Automatically restart the tunnel when ping monitoring detects a failure. + Give up after + Cooldown between restarts + Requires ping monitoring to be enabled. + Restarting… + ping unreachable + ping unreachable: %1$s + restarting %1$d/%2$d… + verifying %1$d/%2$d… + restart %1$d/%2$d · next in %3$ds + awaiting ping recovery + On max attempts reached + Keep waiting + Stop tunnel + Consecutive ping failures before first restart + Exponential backoff + Doubles the cooldown after each failed restart attempt + Connection degrading · restart %1$s/%2$s + Connection restored + Tunnel degraded: ping failure after %1$s restarts, awaiting recovery + Tunnel stopped after %1$s restarts (ping failure) From 69e109eaa9da3fb23fc5bdac70e8470db73c5f7a Mon Sep 17 00:00:00 2001 From: Fabien Date: Wed, 11 Mar 2026 14:45:19 +0100 Subject: [PATCH 4/8] fix(ping): always set transmitted count regardless of ping success When all pings fail (timeout -> Icmp.PingResult.Failed), rttList stays empty and stats.transmitted was never assigned, leaving it at 0. Move stats.transmitted = count before the rttList.isNotEmpty() check so it always reflects the number of attempted pings, matching the expected semantics of "packets transmitted". This unblocks HandshakeRestartHandler.awaitPingFailures() (introduced in #1182) which requires transmitted > 0 to distinguish a real failure from pings not routed through the tunnel. --- .../wireguardautotunnel/util/network/NetworkUtils.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/network/NetworkUtils.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/network/NetworkUtils.kt index 9164fee72..0a2019972 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/network/NetworkUtils.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/network/NetworkUtils.kt @@ -62,8 +62,8 @@ class NetworkUtils(private val ioDispatcher: CoroutineDispatcher) { } .collect() + stats.transmitted = count if (rttList.isNotEmpty()) { - stats.transmitted = count stats.received = received stats.packetLoss = ((count - received).toDouble().round(2) / count) * 100 stats.rttMin = rttList.minOrNull()?.round(2) ?: 0.0 From ca72d746b712838844aecb4e77cb8492a3647cdb Mon Sep 17 00:00:00 2001 From: Fabien Date: Wed, 11 Mar 2026 20:29:43 +0100 Subject: [PATCH 5/8] refactor(restart): always race for early recovery during cooldown MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remove the cooldownMs > pingIntervalMs guard. The withTimeoutOrNull block already handles both cases correctly — it expires after cooldownMs when no recovery is detected, and exits early if pings succeed. This enables early recovery detection even when cooldown <= pingInterval, at zero extra cost. Co-Authored-By: Claude Sonnet 4.6 --- .../tunnel/handler/HandshakeRestartHandler.kt | 48 ++++++++----------- 1 file changed, 19 insertions(+), 29 deletions(-) diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/core/tunnel/handler/HandshakeRestartHandler.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/core/tunnel/handler/HandshakeRestartHandler.kt index f42e14a36..1f67ca025 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/core/tunnel/handler/HandshakeRestartHandler.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/core/tunnel/handler/HandshakeRestartHandler.kt @@ -278,36 +278,26 @@ class HandshakeRestartHandler( ), ) - // If cooldown is longer than ping interval, race against periodic ping recovery - val pingIntervalMs = settings.tunnelPingIntervalSeconds.toMillis() - if (cooldownMs > pingIntervalMs) { - val recoveredDuringCooldown = - withTimeoutOrNull(cooldownMs) { - activeTunnels - .mapNotNull { it[tunnelId]?.pingStates } - .distinctUntilChanged() - .first { pingStates -> - pingStates.values.isNotEmpty() && - pingStates.values.all { it.isReachable } - } - true - } != null + // Race against periodic ping recovery during cooldown + val recoveredDuringCooldown = + withTimeoutOrNull(cooldownMs) { + activeTunnels + .mapNotNull { it[tunnelId]?.pingStates } + .distinctUntilChanged() + .first { pingStates -> + pingStates.values.isNotEmpty() && + pingStates.values.all { it.isReachable } + } + true + } != null - if (recoveredDuringCooldown) { - Timber.d( - "HandshakeRestartHandler: tunnel $tunnelId recovered during cooldown" - ) - emitMessage(config.name, BackendMessage.ConnectionRestored) - attempt = 0 - updateProgress( - tunnelId, - TunnelRestartProgress(totalRestarts = totalRestarts), - ) - pingTarget = awaitPingFailures(tunnelId, settings) - continue - } - } else { - delay(cooldownMs) + if (recoveredDuringCooldown) { + Timber.d("HandshakeRestartHandler: tunnel $tunnelId recovered during cooldown") + emitMessage(config.name, BackendMessage.ConnectionRestored) + attempt = 0 + updateProgress(tunnelId, TunnelRestartProgress(totalRestarts = totalRestarts)) + pingTarget = awaitPingFailures(tunnelId, settings) + continue } } // If not recovered: loop continues → next restart attempt From 34433139de82bce46bc27903839864f3079bc4fb Mon Sep 17 00:00:00 2001 From: Fabien Date: Wed, 18 Mar 2026 19:57:08 +0100 Subject: [PATCH 6/8] feat(ping): skip auto-restart when physical internet is unavailable Co-Authored-By: Claude Sonnet 4.6 --- .../tunnel/handler/HandshakeRestartHandler.kt | 7 ++++- .../tunnel/handler/TunnelMonitoringHandler.kt | 31 +++++++++++++++++-- .../networkmonitor/AndroidNetworkMonitor.kt | 18 +++++++++++ .../networkmonitor/ConnectivityState.kt | 3 ++ .../networkmonitor/NetworkMonitor.kt | 2 ++ 5 files changed, 58 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/core/tunnel/handler/HandshakeRestartHandler.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/core/tunnel/handler/HandshakeRestartHandler.kt index 1f67ca025..ebef1ef48 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/core/tunnel/handler/HandshakeRestartHandler.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/core/tunnel/handler/HandshakeRestartHandler.kt @@ -8,6 +8,7 @@ import com.zaneschepke.wireguardautotunnel.domain.model.MonitoringSettings import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConfig import com.zaneschepke.wireguardautotunnel.domain.repository.MonitoringSettingsRepository import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository +import com.zaneschepke.wireguardautotunnel.domain.state.FailureReason import com.zaneschepke.wireguardautotunnel.domain.state.TunnelRestartProgress import com.zaneschepke.wireguardautotunnel.domain.state.TunnelState import com.zaneschepke.wireguardautotunnel.util.extensions.toMillis @@ -318,7 +319,11 @@ class HandshakeRestartHandler( .first { pingStates -> val allFailing = pingStates.values.isNotEmpty() && - pingStates.values.all { !it.isReachable && it.transmitted > 0 } + pingStates.values.all { + !it.isReachable && + it.transmitted > 0 && + it.failureReason != FailureReason.NoConnectivity + } if (allFailing) { pingStates.values .firstOrNull { !it.isReachable } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/core/tunnel/handler/TunnelMonitoringHandler.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/core/tunnel/handler/TunnelMonitoringHandler.kt index d14895185..ecfa1c431 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/core/tunnel/handler/TunnelMonitoringHandler.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/core/tunnel/handler/TunnelMonitoringHandler.kt @@ -166,7 +166,8 @@ class TunnelMonitorHandler( val connectivityStateFlow = networkMonitor.connectivityStateFlow.stateIn(this) - val isNetworkConnected = connectivityStateFlow.map { it.hasInternet() }.stateIn(this) + val isNetworkConnected = + connectivityStateFlow.map { it.hasValidatedInternet() }.stateIn(this) combine( settingsRepository.flow.distinctUntilChangedBy { it.appMode }, @@ -330,8 +331,34 @@ class TunnelMonitorHandler( it.isRestarting || it.isVerifying } ?: false if (!powerManager.isDeviceIdleMode && !activeRestart) { - if (isNetworkConnected.value) { + val hasConnectivity = + isNetworkConnected.value && + networkMonitor.hasPhysicalInternetConnectivity() + if (hasConnectivity) { performPing() + // Race condition guard: connectivity may have been lost during the ping + if ( + !isNetworkConnected.value || + !networkMonitor.hasPhysicalInternetConnectivity() + ) { + pingStatsFlow.update { current -> + current.mapValues { entry -> + entry.value.copy( + isReachable = false, + failureReason = FailureReason.NoConnectivity, + lastPingAttemptMillis = System.currentTimeMillis(), + ) + } + } + ensureActive() + updateTunnelStatus( + tunnelConfig.id, + null, + null, + pingStatsFlow.value, + null, + ) + } } else { pingStatsFlow.update { current -> current.mapValues { entry -> diff --git a/networkmonitor/src/main/java/com/zaneschepke/networkmonitor/AndroidNetworkMonitor.kt b/networkmonitor/src/main/java/com/zaneschepke/networkmonitor/AndroidNetworkMonitor.kt index d4db0b539..e00afeaca 100644 --- a/networkmonitor/src/main/java/com/zaneschepke/networkmonitor/AndroidNetworkMonitor.kt +++ b/networkmonitor/src/main/java/com/zaneschepke/networkmonitor/AndroidNetworkMonitor.kt @@ -396,6 +396,8 @@ class AndroidNetworkMonitor( val wifiEvent = networkData.wifiNetworkEvent val cellularCaps = networkData.cellularCaps val ethernetCaps = networkData.ethernetCaps + val wifiCaps = + (wifiEvent as? TransportEvent.CapabilitiesChanged)?.networkCapabilities val permissions = when (defaultEvent) { @@ -505,11 +507,19 @@ class AndroidNetworkMonitor( } .also { network -> lastKnownActiveNetwork.value = network } + val isPhysicalNetworkValidated = + wifiCaps?.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED) == true || + cellularCaps?.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED) == + true || + ethernetCaps?.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED) == + true + ConnectivityState( activeNetwork = activeNetwork, locationPermissionsGranted = permissions.locationPermissionGranted, locationServicesEnabled = permissions.locationServicesEnabled, vpnState = vpnState, + isPhysicalNetworkValidated = isPhysicalNetworkValidated, ) } .distinctUntilChanged() @@ -608,6 +618,14 @@ class AndroidNetworkMonitor( airplaneModeState.update { appContext.isAirplaneModeOn() } } + override fun hasPhysicalInternetConnectivity(): Boolean { + return connectivityManager?.allNetworks?.any { network -> + val caps = connectivityManager.getNetworkCapabilities(network) ?: return@any false + !caps.hasTransport(NetworkCapabilities.TRANSPORT_VPN) && + caps.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED) + } ?: false + } + override fun destroy() { runCatching { permissionReceiver?.let { appContext.unregisterReceiver(it) } diff --git a/networkmonitor/src/main/java/com/zaneschepke/networkmonitor/ConnectivityState.kt b/networkmonitor/src/main/java/com/zaneschepke/networkmonitor/ConnectivityState.kt index 15b3d9d2b..5623727c1 100644 --- a/networkmonitor/src/main/java/com/zaneschepke/networkmonitor/ConnectivityState.kt +++ b/networkmonitor/src/main/java/com/zaneschepke/networkmonitor/ConnectivityState.kt @@ -7,9 +7,12 @@ data class ConnectivityState( val locationPermissionsGranted: Boolean, val locationServicesEnabled: Boolean, val vpnState: VpnState, + val isPhysicalNetworkValidated: Boolean = false, ) { fun hasInternet(): Boolean = activeNetwork !is ActiveNetwork.Disconnected + fun hasValidatedInternet(): Boolean = isPhysicalNetworkValidated + override fun toString(): String { val networkInfo = when (activeNetwork) { diff --git a/networkmonitor/src/main/java/com/zaneschepke/networkmonitor/NetworkMonitor.kt b/networkmonitor/src/main/java/com/zaneschepke/networkmonitor/NetworkMonitor.kt index 02f20286c..ffb189877 100644 --- a/networkmonitor/src/main/java/com/zaneschepke/networkmonitor/NetworkMonitor.kt +++ b/networkmonitor/src/main/java/com/zaneschepke/networkmonitor/NetworkMonitor.kt @@ -8,4 +8,6 @@ interface NetworkMonitor { fun checkPermissionsAndUpdateState() fun destroy() + + fun hasPhysicalInternetConnectivity(): Boolean } From 60e3b8180979ce8c2966e88f09cfb5885b2b5206 Mon Sep 17 00:00:00 2001 From: Fabien Date: Thu, 19 Mar 2026 09:47:03 +0100 Subject: [PATCH 7/8] feat(fallback): add fallback tunnel support for auto-restart - DB v35: add fallbackTunnelId to TunnelConfig, isFallbackEnabled and defaultFallbackTunnelId to MonitoringSettings - HandshakeRestartHandler: switch to fallback on max failures, emit SwitchedToFallback notification; fix race condition (keep restarting=true until after startTunnel); prevent self-reference fallback loop; clear "awaiting recovery" progress before fallback switch; only emit ConnectionPermanentlyLost when no fallback available - TunnelConfig.equals(): include fallbackTunnelId so StateFlow emits on fallback change and FallbackTunnelScreen recomposes correctly - FallbackTunnelScreen: per-tunnel fallback picker with SurfaceRow expandedContent pattern - AutoRestartScreen: global fallback toggle + default fallback dropdown, disabled state grays out dropdown - DropdownSelector: add enabled param with disabled color - Navigation: Route.FallbackTunnel + navbar state Co-Authored-By: Claude Sonnet 4.6 --- .../35.json | 38 ++++----- .../wireguardautotunnel/MainActivity.kt | 4 + .../tunnel/handler/HandshakeRestartHandler.kt | 26 ++++++ .../wireguardautotunnel/data/AppDatabase.kt | 3 +- .../data/entity/MonitoringSettings.kt | 3 + .../data/entity/TunnelConfig.kt | 1 + .../data/mapper/MonitoringSettingsMapper.kt | 4 + .../data/mapper/TunnelConfigMapper.kt | 2 + .../domain/events/BackendMessage.kt | 5 ++ .../domain/model/MonitoringSettings.kt | 2 + .../domain/model/TunnelConfig.kt | 5 +- .../ui/common/dropdown/DropdownSelector.kt | 11 ++- .../common/dropdown/LabelledNumberDropdown.kt | 1 + .../ui/navigation/Route.kt | 3 + .../currentBackStackEntryAsNavbarState.kt | 13 +++ .../autorestart/AutoRestartScreen.kt | 55 +++++++++++++ .../autorestart/FallbackTunnelScreen.kt | 81 +++++++++++++++++++ .../viewmodel/MonitoringViewModel.kt | 33 ++++++++ app/src/main/res/values/strings.xml | 7 ++ 19 files changed, 273 insertions(+), 24 deletions(-) create mode 100644 app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/monitoring/autorestart/FallbackTunnelScreen.kt diff --git a/app/schemas/com.zaneschepke.wireguardautotunnel.data.AppDatabase/35.json b/app/schemas/com.zaneschepke.wireguardautotunnel.data.AppDatabase/35.json index ebccaa543..73fd32b61 100644 --- a/app/schemas/com.zaneschepke.wireguardautotunnel.data.AppDatabase/35.json +++ b/app/schemas/com.zaneschepke.wireguardautotunnel.data.AppDatabase/35.json @@ -2,11 +2,11 @@ "formatVersion": 1, "database": { "version": 35, - "identityHash": "3f5d07c1a895d1a3d28c03ce2c2877c0", + "identityHash": "c5362e92a130332e3bb9a9949da3eaf1", "entities": [ { "tableName": "tunnel_config", - "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 '[]', `is_metered` INTEGER NOT NULL DEFAULT false)", + "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 '[]', `is_metered` INTEGER NOT NULL DEFAULT false, `fallback_tunnel_id` INTEGER)", "fields": [ { "fieldPath": "id", @@ -108,6 +108,11 @@ "affinity": "INTEGER", "notNull": true, "defaultValue": "false" + }, + { + "fieldPath": "fallbackTunnelId", + "columnName": "fallback_tunnel_id", + "affinity": "INTEGER" } ], "primaryKey": { @@ -375,7 +380,7 @@ }, { "tableName": "monitoring_settings", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_ping_enabled` INTEGER NOT NULL DEFAULT 0, `is_ping_monitoring_enabled` INTEGER NOT NULL DEFAULT 1, `tunnel_ping_interval_sec` INTEGER NOT NULL DEFAULT 30, `tunnel_ping_attempts` INTEGER NOT NULL DEFAULT 3, `tunnel_ping_timeout_sec` INTEGER, `show_detailed_ping_stats` INTEGER NOT NULL DEFAULT 0, `is_local_logs_enabled` INTEGER NOT NULL DEFAULT 0, `is_restart_on_handshake_timeout_enabled` INTEGER NOT NULL DEFAULT 0, `max_handshake_restart_attempts` INTEGER NOT NULL DEFAULT 5, `restart_cooldown_seconds` INTEGER NOT NULL DEFAULT 30, `is_recovery_notification_enabled` INTEGER NOT NULL DEFAULT 1, `max_attempts_action` INTEGER NOT NULL DEFAULT 0, `ping_failures_before_restart` INTEGER NOT NULL DEFAULT 1, `is_backoff_enabled` INTEGER NOT NULL DEFAULT 0, `backoff_max_attempts` INTEGER NOT NULL DEFAULT 3, `startup_grace_seconds` INTEGER NOT NULL DEFAULT 30)", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_ping_enabled` INTEGER NOT NULL DEFAULT 0, `is_ping_monitoring_enabled` INTEGER NOT NULL DEFAULT 1, `tunnel_ping_interval_sec` INTEGER NOT NULL DEFAULT 30, `tunnel_ping_attempts` INTEGER NOT NULL DEFAULT 3, `tunnel_ping_timeout_sec` INTEGER, `show_detailed_ping_stats` INTEGER NOT NULL DEFAULT 0, `is_local_logs_enabled` INTEGER NOT NULL DEFAULT 0, `is_restart_on_handshake_timeout_enabled` INTEGER NOT NULL DEFAULT 0, `max_restart_attempts` INTEGER NOT NULL DEFAULT 5, `restart_cooldown_seconds` INTEGER NOT NULL DEFAULT 30, `max_attempts_action` INTEGER NOT NULL DEFAULT 0, `ping_failures_before_restart` INTEGER NOT NULL DEFAULT 1, `is_backoff_enabled` INTEGER NOT NULL DEFAULT 0, `is_fallback_enabled` INTEGER NOT NULL DEFAULT 0, `default_fallback_tunnel_id` INTEGER)", "fields": [ { "fieldPath": "id", @@ -438,8 +443,8 @@ "defaultValue": "0" }, { - "fieldPath": "maxHandshakeRestartAttempts", - "columnName": "max_handshake_restart_attempts", + "fieldPath": "maxRestartAttempts", + "columnName": "max_restart_attempts", "affinity": "INTEGER", "notNull": true, "defaultValue": "5" @@ -451,13 +456,6 @@ "notNull": true, "defaultValue": "30" }, - { - "fieldPath": "isRecoveryNotificationEnabled", - "columnName": "is_recovery_notification_enabled", - "affinity": "INTEGER", - "notNull": true, - "defaultValue": "1" - }, { "fieldPath": "maxAttemptsAction", "columnName": "max_attempts_action", @@ -480,18 +478,16 @@ "defaultValue": "0" }, { - "fieldPath": "backoffMaxAttempts", - "columnName": "backoff_max_attempts", + "fieldPath": "isFallbackEnabled", + "columnName": "is_fallback_enabled", "affinity": "INTEGER", "notNull": true, - "defaultValue": "3" + "defaultValue": "0" }, { - "fieldPath": "startupGraceSeconds", - "columnName": "startup_grace_seconds", - "affinity": "INTEGER", - "notNull": true, - "defaultValue": "30" + "fieldPath": "defaultFallbackTunnelId", + "columnName": "default_fallback_tunnel_id", + "affinity": "INTEGER" } ], "primaryKey": { @@ -580,7 +576,7 @@ ], "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, '3f5d07c1a895d1a3d28c03ce2c2877c0')" + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'c5362e92a130332e3bb9a9949da3eaf1')" ] } } \ No newline at end of file diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/MainActivity.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/MainActivity.kt index 2b888d122..55baed0cf 100755 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/MainActivity.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/MainActivity.kt @@ -96,6 +96,7 @@ import com.zaneschepke.wireguardautotunnel.ui.screens.settings.integrations.Andr import com.zaneschepke.wireguardautotunnel.ui.screens.settings.lockdown.LockdownSettingsScreen import com.zaneschepke.wireguardautotunnel.ui.screens.settings.monitoring.TunnelMonitoringScreen import com.zaneschepke.wireguardautotunnel.ui.screens.settings.monitoring.autorestart.AutoRestartScreen +import com.zaneschepke.wireguardautotunnel.ui.screens.settings.monitoring.autorestart.FallbackTunnelScreen import com.zaneschepke.wireguardautotunnel.ui.screens.settings.monitoring.logs.LogsScreen import com.zaneschepke.wireguardautotunnel.ui.screens.settings.monitoring.ping.PingTargetScreen import com.zaneschepke.wireguardautotunnel.ui.screens.support.SupportScreen @@ -505,6 +506,9 @@ class MainActivity : AppCompatActivity() { } entry { PingTargetScreen() } entry { AutoRestartScreen() } + entry { + FallbackTunnelScreen() + } }, ) } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/core/tunnel/handler/HandshakeRestartHandler.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/core/tunnel/handler/HandshakeRestartHandler.kt index ebef1ef48..4be655012 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/core/tunnel/handler/HandshakeRestartHandler.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/core/tunnel/handler/HandshakeRestartHandler.kt @@ -136,6 +136,31 @@ class HandshakeRestartHandler( totalRestarts = totalRestarts, ), ) + // Try fallback before applying maxAttemptsAction + if (settings.isFallbackEnabled) { + val freshConfig = tunnelsRepository.getById(tunnelId) ?: config + val fallbackId = + freshConfig.fallbackTunnelId ?: settings.defaultFallbackTunnelId + val fallbackConfig = + fallbackId?.takeIf { it != tunnelId }?.let { tunnelsRepository.getById(it) } + if (fallbackConfig != null) { + updateProgress(tunnelId, null) + Timber.d( + "HandshakeRestartHandler: switching tunnel $tunnelId to fallback ${fallbackConfig.id}" + ) + emitMessage( + config.name, + BackendMessage.SwitchedToFallback(config.name, fallbackConfig.name), + ) + mutex.withLock { restarting[tunnelId] = true } + runCatching { stopTunnel(tunnelId) } + delay(RESTART_SETTLE_DELAY_MS) + runCatching { startTunnel(fallbackConfig) } + mutex.withLock { restarting[tunnelId] = false } + return + } + } + val isStopped = settings.maxAttemptsAction == MaxAttemptsAction.STOP_TUNNEL emitMessage( config.name, @@ -145,6 +170,7 @@ class HandshakeRestartHandler( isStopped, ), ) + if (isStopped) { mutex.withLock { restarting[tunnelId] = false } runCatching { stopTunnel(tunnelId) } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/AppDatabase.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/AppDatabase.kt index 18be1232e..200c5f15b 100755 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/AppDatabase.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/AppDatabase.kt @@ -17,7 +17,7 @@ import com.zaneschepke.wireguardautotunnel.data.entity.* DnsSettings::class, LockdownSettings::class, ], - version = 34, + version = 35, autoMigrations = [ AutoMigration(from = 1, to = 2), @@ -50,6 +50,7 @@ import com.zaneschepke.wireguardautotunnel.data.entity.* AutoMigration(from = 31, to = 32, spec = RenameMaxRestartAttemptsMigration::class), AutoMigration(from = 32, to = 33, spec = DropStartupGraceMigration::class), AutoMigration(from = 33, to = 34, spec = DropRecoveryNotificationMigration::class), + AutoMigration(from = 34, to = 35), ], exportSchema = true, ) diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/entity/MonitoringSettings.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/entity/MonitoringSettings.kt index 7df952b90..3f7aba23b 100755 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/entity/MonitoringSettings.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/entity/MonitoringSettings.kt @@ -30,4 +30,7 @@ data class MonitoringSettings( val pingFailuresBeforeRestart: Int = 1, @ColumnInfo(name = "is_backoff_enabled", defaultValue = "0") val isBackoffEnabled: Boolean = false, + @ColumnInfo(name = "is_fallback_enabled", defaultValue = "0") + val isFallbackEnabled: Boolean = false, + @ColumnInfo(name = "default_fallback_tunnel_id") val defaultFallbackTunnelId: Int? = null, ) diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/entity/TunnelConfig.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/entity/TunnelConfig.kt index cb1cdc8fa..3f2b994eb 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/entity/TunnelConfig.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/entity/TunnelConfig.kt @@ -29,6 +29,7 @@ data class TunnelConfig( @ColumnInfo(name = "auto_tunnel_apps", defaultValue = "[]") val autoTunnelApps: Set = emptySet(), @ColumnInfo(name = "is_metered", defaultValue = "false") val isMetered: Boolean = false, + @ColumnInfo(name = "fallback_tunnel_id") val fallbackTunnelId: Int? = null, ) { companion object { const val GLOBAL_CONFIG_NAME = "4675ab06-903a-438b-8485-6ea4187a9512" diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/mapper/MonitoringSettingsMapper.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/mapper/MonitoringSettingsMapper.kt index e16680e4f..42ec6f191 100755 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/mapper/MonitoringSettingsMapper.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/mapper/MonitoringSettingsMapper.kt @@ -19,6 +19,8 @@ fun Entity.toDomain(): Domain = maxAttemptsAction = maxAttemptsAction, pingFailuresBeforeRestart = pingFailuresBeforeRestart, isBackoffEnabled = isBackoffEnabled, + isFallbackEnabled = isFallbackEnabled, + defaultFallbackTunnelId = defaultFallbackTunnelId, ) fun Domain.toEntity(): Entity = @@ -37,4 +39,6 @@ fun Domain.toEntity(): Entity = maxAttemptsAction = maxAttemptsAction, pingFailuresBeforeRestart = pingFailuresBeforeRestart, isBackoffEnabled = isBackoffEnabled, + isFallbackEnabled = isFallbackEnabled, + defaultFallbackTunnelId = defaultFallbackTunnelId, ) diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/mapper/TunnelConfigMapper.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/mapper/TunnelConfigMapper.kt index 036c849a9..65e14ed9d 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/mapper/TunnelConfigMapper.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/mapper/TunnelConfigMapper.kt @@ -20,6 +20,7 @@ fun Entity.toDomain(): Domain = position = position, autoTunnelApps = autoTunnelApps, isMetered = isMetered, + fallbackTunnelId = fallbackTunnelId, ) fun Domain.toEntity(): Entity = @@ -39,4 +40,5 @@ fun Domain.toEntity(): Entity = position = position, autoTunnelApps = autoTunnelApps, isMetered = isMetered, + fallbackTunnelId = fallbackTunnelId, ) diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/domain/events/BackendMessage.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/domain/events/BackendMessage.kt index edfd5ab84..1621dffbb 100755 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/domain/events/BackendMessage.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/domain/events/BackendMessage.kt @@ -27,6 +27,9 @@ sealed class BackendMessage { data object ConnectionCancelled : BackendMessage() + data class SwitchedToFallback(val fromTunnelName: String, val toTunnelName: String) : + BackendMessage() + fun toStringValue(): StringValue? = when (this) { DynamicDnsSuccess -> StringValue.StringResource(R.string.ddns_success_message) @@ -49,6 +52,8 @@ sealed class BackendMessage { totalAttempts.toString(), ) } + is SwitchedToFallback -> + StringValue.StringResource(R.string.snackbar_switched_to_fallback, toTunnelName) ConnectionCancelled -> null } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/domain/model/MonitoringSettings.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/domain/model/MonitoringSettings.kt index fbac3784e..90d326231 100755 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/domain/model/MonitoringSettings.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/domain/model/MonitoringSettings.kt @@ -17,4 +17,6 @@ data class MonitoringSettings( val maxAttemptsAction: MaxAttemptsAction = MaxAttemptsAction.DO_NOTHING, val pingFailuresBeforeRestart: Int = 1, val isBackoffEnabled: Boolean = false, + val isFallbackEnabled: Boolean = false, + val defaultFallbackTunnelId: Int? = null, ) diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/domain/model/TunnelConfig.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/domain/model/TunnelConfig.kt index 7ab0f1f0d..e3a71a0b8 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/domain/model/TunnelConfig.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/domain/model/TunnelConfig.kt @@ -28,6 +28,7 @@ data class TunnelConfig( val position: Int = 0, val autoTunnelApps: Set = setOf(), val isMetered: Boolean = false, + val fallbackTunnelId: Int? = null, ) { override fun equals(other: Any?): Boolean { @@ -44,7 +45,8 @@ data class TunnelConfig( restartOnPingFailure == other.restartOnPingFailure && tunnelNetworks == other.tunnelNetworks && isIpv4Preferred == other.isIpv4Preferred && - isMetered == other.isMetered + isMetered == other.isMetered && + fallbackTunnelId == other.fallbackTunnelId } override fun hashCode(): Int { @@ -52,6 +54,7 @@ data class TunnelConfig( result = 31 * result + name.hashCode() result = 31 * result + wgQuick.hashCode() result = 31 * result + amQuick.hashCode() + result = 31 * result + (fallbackTunnelId ?: 0) return result } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/dropdown/DropdownSelector.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/dropdown/DropdownSelector.kt index 64a81e798..b23c81d90 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/dropdown/DropdownSelector.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/dropdown/DropdownSelector.kt @@ -23,21 +23,30 @@ fun DropdownSelector( modifier: Modifier = Modifier, label: @Composable (() -> Unit)? = null, isExpanded: Boolean = false, + enabled: Boolean = true, onDismiss: () -> Unit = {}, optionToString: @Composable (T?) -> String = { it?.toString() ?: stringResource(R.string._default) }, ) { + val contentColor = + if (enabled) MaterialTheme.colorScheme.onSurface + else MaterialTheme.colorScheme.onSurface.copy(alpha = 0.38f) Box(modifier = modifier) { Row( horizontalArrangement = Arrangement.spacedBy(5.dp), verticalAlignment = Alignment.CenterVertically, ) { if (label != null) label() - Text(text = optionToString(currentValue), style = MaterialTheme.typography.bodyMedium) + Text( + text = optionToString(currentValue), + style = MaterialTheme.typography.bodyMedium, + color = contentColor, + ) Icon( Icons.Default.ArrowDropDown, contentDescription = stringResource(R.string.dropdown), + tint = contentColor, ) } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/dropdown/LabelledNumberDropdown.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/dropdown/LabelledNumberDropdown.kt index f3d0122ce..e8a07547a 100755 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/dropdown/LabelledNumberDropdown.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/dropdown/LabelledNumberDropdown.kt @@ -28,6 +28,7 @@ fun LabelledDropdown( options = options, onValueSelected = { selected -> onSelected(selected) }, isExpanded = isDropDownExpanded, + enabled = enabled, onDismiss = { isDropDownExpanded = false }, optionToString = optionToString, ) diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/navigation/Route.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/navigation/Route.kt index e89323785..90eb0df60 100755 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/navigation/Route.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/navigation/Route.kt @@ -77,6 +77,8 @@ sealed class Route : NavKey { @Keep @Serializable data object PingTarget : Route() @Keep @Serializable data object AutoRestart : Route() + + @Keep @Serializable data object FallbackTunnel : Route() } @Serializable @@ -131,6 +133,7 @@ enum class Tab( Route.Display, Route.PingTarget, Route.AutoRestart, + Route.FallbackTunnel, is Route.ConfigGlobal, Route.Logs -> SETTINGS is Route.Support, diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/navigation/components/currentBackStackEntryAsNavbarState.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/navigation/components/currentBackStackEntryAsNavbarState.kt index e6bf8095c..b3df25579 100755 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/navigation/components/currentBackStackEntryAsNavbarState.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/navigation/components/currentBackStackEntryAsNavbarState.kt @@ -567,6 +567,19 @@ fun currentRouteAsNavbarState( topTitle = context.getString(R.string.auto_restart), showBottomItems = true, ) + FallbackTunnel -> + NavbarState( + topLeading = { + IconButton(onClick = { navController.pop() }) { + Icon( + Icons.AutoMirrored.Rounded.ArrowBack, + stringResource(R.string.back), + ) + } + }, + topTitle = context.getString(R.string.per_tunnel_fallback), + showBottomItems = true, + ) null -> NavbarState() } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/monitoring/autorestart/AutoRestartScreen.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/monitoring/autorestart/AutoRestartScreen.kt index 92f3d15b0..0533fecae 100755 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/monitoring/autorestart/AutoRestartScreen.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/monitoring/autorestart/AutoRestartScreen.kt @@ -8,6 +8,7 @@ import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.outlined.TrendingUp +import androidx.compose.material.icons.outlined.AltRoute import androidx.compose.material.icons.outlined.FilterAlt import androidx.compose.material.icons.outlined.PowerSettingsNew import androidx.compose.material.icons.outlined.Replay @@ -24,15 +25,18 @@ import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.zaneschepke.wireguardautotunnel.R import com.zaneschepke.wireguardautotunnel.data.model.MaxAttemptsAction +import com.zaneschepke.wireguardautotunnel.ui.LocalNavController import com.zaneschepke.wireguardautotunnel.ui.common.button.SurfaceRow import com.zaneschepke.wireguardautotunnel.ui.common.button.ThemedSwitch import com.zaneschepke.wireguardautotunnel.ui.common.dropdown.LabelledDropdown import com.zaneschepke.wireguardautotunnel.ui.common.label.GroupLabel +import com.zaneschepke.wireguardautotunnel.ui.navigation.Route import com.zaneschepke.wireguardautotunnel.viewmodel.MonitoringViewModel import org.koin.androidx.compose.koinViewModel @Composable fun AutoRestartScreen(viewModel: MonitoringViewModel = koinViewModel()) { + val navController = LocalNavController.current val uiState by viewModel.container.stateFlow.collectAsStateWithLifecycle() if (uiState.isLoading) return @@ -157,5 +161,56 @@ fun AutoRestartScreen(viewModel: MonitoringViewModel = koinViewModel()) { }, ) } + + val fallbackEnabled = uiState.monitoringSettings.isFallbackEnabled + + Column { + GroupLabel( + stringResource(R.string.enable_fallback_tunnel), + modifier = Modifier.padding(horizontal = 16.dp), + ) + SurfaceRow( + enabled = pingEnabled, + leading = { Icon(Icons.Outlined.AltRoute, contentDescription = null) }, + title = stringResource(R.string.enable_fallback_tunnel), + description = { + Text( + text = stringResource(R.string.enable_fallback_tunnel_description), + style = + MaterialTheme.typography.bodySmall.copy( + color = MaterialTheme.colorScheme.outline + ), + ) + }, + trailing = { + ThemedSwitch( + checked = fallbackEnabled, + onClick = { viewModel.setFallbackEnabled(it) }, + enabled = pingEnabled, + ) + }, + onClick = { viewModel.setFallbackEnabled(!fallbackEnabled) }, + ) + LabelledDropdown( + enabled = pingEnabled && fallbackEnabled, + title = stringResource(R.string.default_fallback_tunnel), + leading = { Icon(Icons.Outlined.AltRoute, contentDescription = null) }, + currentValue = uiState.monitoringSettings.defaultFallbackTunnelId, + onSelected = { viewModel.setDefaultFallbackTunnelId(it) }, + options = uiState.tunnels.map { it.id } + listOf(null), + optionToString = { id -> + if (id == null) stringResource(R.string.no_fallback) + else + uiState.tunnels.find { it.id == id }?.name + ?: stringResource(R.string.no_fallback) + }, + ) + SurfaceRow( + enabled = pingEnabled && fallbackEnabled, + leading = { Icon(Icons.Outlined.AltRoute, contentDescription = null) }, + title = stringResource(R.string.per_tunnel_fallback), + onClick = { navController.push(Route.FallbackTunnel) }, + ) + } } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/monitoring/autorestart/FallbackTunnelScreen.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/monitoring/autorestart/FallbackTunnelScreen.kt new file mode 100644 index 000000000..6a63dcc20 --- /dev/null +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/monitoring/autorestart/FallbackTunnelScreen.kt @@ -0,0 +1,81 @@ +package com.zaneschepke.wireguardautotunnel.ui.screens.settings.monitoring.autorestart + +import androidx.compose.foundation.layout.Column +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.AltRoute +import androidx.compose.material3.Icon +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.zaneschepke.wireguardautotunnel.R +import com.zaneschepke.wireguardautotunnel.ui.common.button.SurfaceRow +import com.zaneschepke.wireguardautotunnel.ui.common.label.GroupLabel +import com.zaneschepke.wireguardautotunnel.ui.common.text.DescriptionText +import com.zaneschepke.wireguardautotunnel.viewmodel.MonitoringViewModel +import org.koin.androidx.compose.koinViewModel + +@Composable +fun FallbackTunnelScreen(viewModel: MonitoringViewModel = koinViewModel()) { + val settingsState by viewModel.container.stateFlow.collectAsStateWithLifecycle() + + if (settingsState.isLoading) return + + Column(modifier = Modifier.verticalScroll(rememberScrollState())) { + DescriptionText( + stringResource(R.string.enable_fallback_tunnel_description), + modifier = Modifier.padding(horizontal = 16.dp).padding(bottom = 16.dp), + ) + GroupLabel( + stringResource(R.string.per_tunnel_fallback), + modifier = Modifier.padding(horizontal = 16.dp), + ) + settingsState.tunnels.forEach { tunnel -> + var expanded by remember(tunnel.id) { mutableStateOf(false) } + val otherTunnels = settingsState.tunnels.filter { it.id != tunnel.id } + val currentName = + tunnel.fallbackTunnelId?.let { id -> otherTunnels.find { it.id == id }?.name } + ?: stringResource(R.string.use_default_fallback) + + SurfaceRow( + title = tunnel.name, + leading = { Icon(Icons.Outlined.AltRoute, contentDescription = null) }, + description = { DescriptionText(currentName) }, + onClick = { expanded = !expanded }, + expandedContent = + if (expanded) + ({ + Column { + SurfaceRow( + title = stringResource(R.string.use_default_fallback), + selected = tunnel.fallbackTunnelId == null, + onClick = { + viewModel.setTunnelFallbackId(tunnel, null) + expanded = false + }, + ) + otherTunnels.forEach { other -> + SurfaceRow( + title = other.name, + selected = tunnel.fallbackTunnelId == other.id, + onClick = { + viewModel.setTunnelFallbackId(tunnel, other.id) + expanded = false + }, + ) + } + } + }) + else null, + ) + } + } +} diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/viewmodel/MonitoringViewModel.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/viewmodel/MonitoringViewModel.kt index 978cdb52d..2b1da6301 100755 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/viewmodel/MonitoringViewModel.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/viewmodel/MonitoringViewModel.kt @@ -95,4 +95,37 @@ class MonitoringViewModel( ) ) } + + fun setFallbackEnabled(to: Boolean) = intent { + monitoringSettingsRepository.upsert(state.monitoringSettings.copy(isFallbackEnabled = to)) + } + + fun setDefaultFallbackTunnelId(to: Int?) = intent { + monitoringSettingsRepository.upsert( + state.monitoringSettings.copy(defaultFallbackTunnelId = to) + ) + } + + fun setTunnelFallbackId(tunnel: TunnelConfig, fallbackId: Int?) = intent { + if (fallbackId != null) { + if (fallbackId == tunnel.id) return@intent + if (wouldCreateLoop(tunnel.id, fallbackId, state.tunnels)) return@intent + } + tunnelsRepository.save(tunnel.copy(fallbackTunnelId = fallbackId)) + } + + private fun wouldCreateLoop( + sourceId: Int, + fallbackId: Int, + tunnels: List, + ): Boolean { + var current: Int? = fallbackId + val visited = mutableSetOf() + while (current != null) { + if (current == sourceId) return true + if (!visited.add(current)) break + current = tunnels.find { it.id == current }?.fallbackTunnelId + } + return false + } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index ad7efc37f..5ab4a92d6 100755 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -489,4 +489,11 @@ Connection restored Tunnel degraded: ping failure after %1$s restarts, awaiting recovery Tunnel stopped after %1$s restarts (ping failure) + Switched to fallback tunnel: %1$s + Fallback tunnel + Switch to a fallback tunnel when all restart attempts are exhausted + Default fallback tunnel + Per-tunnel fallback + None + Use default From 1f4f3ef770a04bc5c043eac95ef7f6ea8d12c602 Mon Sep 17 00:00:00 2001 From: Fabien Date: Thu, 19 Mar 2026 10:20:38 +0100 Subject: [PATCH 8/8] fix(restart): abort restart loop when tunnel is stopped externally MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When restarting=true blocks job cancellation during a stop/start cycle, the activeTunnels collector skips cleanup. After restarting=false the collector won't re-fire since activeTunnels hasn't changed, leaving the job alive to restart the tunnel indefinitely even after a user toggle-off. Fix: after clearing restarting flag, check if the tunnel is still in activeTunnels. If absent, it was stopped externally during the protected window — clear progress and return to break the loop. Co-Authored-By: Claude Sonnet 4.6 --- .../core/tunnel/handler/HandshakeRestartHandler.kt | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/core/tunnel/handler/HandshakeRestartHandler.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/core/tunnel/handler/HandshakeRestartHandler.kt index 4be655012..ba73b8ed7 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/core/tunnel/handler/HandshakeRestartHandler.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/core/tunnel/handler/HandshakeRestartHandler.kt @@ -239,6 +239,17 @@ class HandshakeRestartHandler( mutex.withLock { restarting[tunnelId] = false } totalRestarts++ + // The collector skips cancellation when restarting=true, and won't re-fire if + // activeTunnels hasn't changed since. Re-check here to abort if the tunnel was + // stopped externally (e.g. user toggle) while we were protected by the flag. + if (!activeTunnels.value.containsKey(tunnelId)) { + Timber.d( + "HandshakeRestartHandler: tunnel $tunnelId was stopped externally during restart, aborting" + ) + updateProgress(tunnelId, null) + return + } + if (cameUp == null) { Timber.w("HandshakeRestartHandler: tunnel $tunnelId did not come UP within timeout") // Count as failed verification, will retry on next loop iteration