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..73fd32b61 --- /dev/null +++ b/app/schemas/com.zaneschepke.wireguardautotunnel.data.AppDatabase/35.json @@ -0,0 +1,582 @@ +{ + "formatVersion": 1, + "database": { + "version": 35, + "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, `fallback_tunnel_id` INTEGER)", + "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" + }, + { + "fieldPath": "fallbackTunnelId", + "columnName": "fallback_tunnel_id", + "affinity": "INTEGER" + } + ], + "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, `is_fallback_enabled` INTEGER NOT NULL DEFAULT 0, `default_fallback_tunnel_id` INTEGER)", + "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" + }, + { + "fieldPath": "isFallbackEnabled", + "columnName": "is_fallback_enabled", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "defaultFallbackTunnelId", + "columnName": "default_fallback_tunnel_id", + "affinity": "INTEGER" + } + ], + "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, '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 old mode 100644 new mode 100755 index 3165f059d..55baed0cf --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/MainActivity.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/MainActivity.kt @@ -95,6 +95,8 @@ 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.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 @@ -503,6 +505,10 @@ class MainActivity : AppCompatActivity() { PreferredTunnelScreen(key.tunnelNetwork) } entry { PingTargetScreen() } + entry { AutoRestartScreen() } + entry { + FallbackTunnelScreen() + } }, ) } 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..ba73b8ed7 --- /dev/null +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/core/tunnel/handler/HandshakeRestartHandler.kt @@ -0,0 +1,392 @@ +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.FailureReason +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, + ), + ) + // 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, + 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++ + + // 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 + 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 pingStats = pingResult.getOrNull() + val recovered = pingStats?.isReachable == true || pingStats?.transmitted == 0 + + 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, + ), + ) + + // 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 + } + } + // 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 && + it.transmitted > 0 && + it.failureReason != FailureReason.NoConnectivity + } + 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..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 @@ -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 ( @@ -164,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 }, @@ -323,9 +326,39 @@ class TunnelMonitorHandler( while (isActive) { ensureActive() - if (!powerManager.isDeviceIdleMode) { - if (isNetworkConnected.value) { + val activeRestart = + restartProgress.value[tunnelConfig.id]?.let { + it.isRestarting || it.isVerifying + } ?: false + if (!powerManager.isDeviceIdleMode && !activeRestart) { + 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/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..200c5f15b --- 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 = 35, autoMigrations = [ AutoMigration(from = 1, to = 2), @@ -45,6 +45,12 @@ 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), + AutoMigration(from = 34, to = 35), ], exportSchema = true, ) @@ -129,3 +135,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..3f7aba23b --- 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,18 @@ 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, + @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 old mode 100644 new mode 100755 index 35fc8f7e8..42ec6f191 --- 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,14 @@ fun Entity.toDomain(): Domain = tunnelPingTimeoutSeconds = tunnelPingTimeoutSeconds, showDetailedPingStats = showDetailedPingStats, isLocalLogsEnabled = isLocalLogsEnabled, + isRestartOnHandshakeTimeoutEnabled = isRestartOnHandshakeTimeoutEnabled, + maxRestartAttempts = maxRestartAttempts, + restartCooldownSeconds = restartCooldownSeconds, + maxAttemptsAction = maxAttemptsAction, + pingFailuresBeforeRestart = pingFailuresBeforeRestart, + isBackoffEnabled = isBackoffEnabled, + isFallbackEnabled = isFallbackEnabled, + defaultFallbackTunnelId = defaultFallbackTunnelId, ) fun Domain.toEntity(): Entity = @@ -25,4 +33,12 @@ fun Domain.toEntity(): Entity = tunnelPingTimeoutSeconds = tunnelPingTimeoutSeconds, showDetailedPingStats = showDetailedPingStats, isLocalLogsEnabled = isLocalLogsEnabled, + isRestartOnHandshakeTimeoutEnabled = isRestartOnHandshakeTimeoutEnabled, + maxRestartAttempts = maxRestartAttempts, + restartCooldownSeconds = restartCooldownSeconds, + 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/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..1621dffbb --- 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,55 @@ 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() + + data class SwitchedToFallback(val fromTunnelName: String, val toTunnelName: String) : + 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(), + ) + } + is SwitchedToFallback -> + StringValue.StringResource(R.string.snackbar_switched_to_fallback, toTunnelName) + 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..90d326231 --- 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,12 @@ 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, + 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/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, +) 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 old mode 100644 new mode 100755 index 2d7adba55..e8a07547a --- 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( @@ -26,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 old mode 100644 new mode 100755 index 95bf8aa85..90eb0df60 --- 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,10 @@ 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() + + @Keep @Serializable data object FallbackTunnel : Route() } @Serializable @@ -128,6 +132,8 @@ enum class Tab( Route.Language, 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 old mode 100644 new mode 100755 index f7eb298a1..b3df25579 --- 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,32 @@ 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, + ) + 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/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..0533fecae --- /dev/null +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/monitoring/autorestart/AutoRestartScreen.kt @@ -0,0 +1,216 @@ +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.AltRoute +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.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 + + 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) + } + }, + ) + } + + 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/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/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 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..2b1da6301 --- 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,75 @@ 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, + ) + ) + } + + 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/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..5ab4a92d6 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -466,4 +466,34 @@ 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) + 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 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 }