feat: add vpn kill switch (#476)
This commit is contained in:
parent
c3a2e05eb2
commit
cda747deee
|
@ -0,0 +1,267 @@
|
||||||
|
{
|
||||||
|
"formatVersion": 1,
|
||||||
|
"database": {
|
||||||
|
"version": 13,
|
||||||
|
"identityHash": "ff209157b98a641c424f5086818ec585",
|
||||||
|
"entities": [
|
||||||
|
{
|
||||||
|
"tableName": "Settings",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_tunnel_enabled` INTEGER NOT NULL, `is_tunnel_on_mobile_data_enabled` INTEGER NOT NULL, `trusted_network_ssids` TEXT NOT NULL, `is_always_on_vpn_enabled` INTEGER NOT NULL, `is_tunnel_on_ethernet_enabled` INTEGER NOT NULL, `is_shortcuts_enabled` INTEGER NOT NULL DEFAULT false, `is_tunnel_on_wifi_enabled` INTEGER NOT NULL DEFAULT false, `is_kernel_enabled` INTEGER NOT NULL DEFAULT false, `is_restore_on_boot_enabled` INTEGER NOT NULL DEFAULT false, `is_multi_tunnel_enabled` INTEGER NOT NULL DEFAULT false, `is_ping_enabled` INTEGER NOT NULL DEFAULT false, `is_amnezia_enabled` INTEGER NOT NULL DEFAULT false, `is_wildcards_enabled` INTEGER NOT NULL DEFAULT false, `is_wifi_by_shell_enabled` INTEGER NOT NULL DEFAULT false, `is_stop_on_no_internet_enabled` INTEGER NOT NULL DEFAULT false, `is_vpn_kill_switch_enabled` INTEGER NOT NULL DEFAULT false, `is_kernel_kill_switch_enabled` INTEGER NOT NULL DEFAULT false, `is_lan_on_kill_switch_enabled` INTEGER NOT NULL DEFAULT false)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "id",
|
||||||
|
"columnName": "id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "isAutoTunnelEnabled",
|
||||||
|
"columnName": "is_tunnel_enabled",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "isTunnelOnMobileDataEnabled",
|
||||||
|
"columnName": "is_tunnel_on_mobile_data_enabled",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "trustedNetworkSSIDs",
|
||||||
|
"columnName": "trusted_network_ssids",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "isAlwaysOnVpnEnabled",
|
||||||
|
"columnName": "is_always_on_vpn_enabled",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "isTunnelOnEthernetEnabled",
|
||||||
|
"columnName": "is_tunnel_on_ethernet_enabled",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "isShortcutsEnabled",
|
||||||
|
"columnName": "is_shortcuts_enabled",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true,
|
||||||
|
"defaultValue": "false"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "isTunnelOnWifiEnabled",
|
||||||
|
"columnName": "is_tunnel_on_wifi_enabled",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true,
|
||||||
|
"defaultValue": "false"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "isKernelEnabled",
|
||||||
|
"columnName": "is_kernel_enabled",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true,
|
||||||
|
"defaultValue": "false"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "isRestoreOnBootEnabled",
|
||||||
|
"columnName": "is_restore_on_boot_enabled",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true,
|
||||||
|
"defaultValue": "false"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "isMultiTunnelEnabled",
|
||||||
|
"columnName": "is_multi_tunnel_enabled",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true,
|
||||||
|
"defaultValue": "false"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "isPingEnabled",
|
||||||
|
"columnName": "is_ping_enabled",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true,
|
||||||
|
"defaultValue": "false"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "isAmneziaEnabled",
|
||||||
|
"columnName": "is_amnezia_enabled",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true,
|
||||||
|
"defaultValue": "false"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "isWildcardsEnabled",
|
||||||
|
"columnName": "is_wildcards_enabled",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true,
|
||||||
|
"defaultValue": "false"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "isWifiNameByShellEnabled",
|
||||||
|
"columnName": "is_wifi_by_shell_enabled",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true,
|
||||||
|
"defaultValue": "false"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "isStopOnNoInternetEnabled",
|
||||||
|
"columnName": "is_stop_on_no_internet_enabled",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true,
|
||||||
|
"defaultValue": "false"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "isVpnKillSwitchEnabled",
|
||||||
|
"columnName": "is_vpn_kill_switch_enabled",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true,
|
||||||
|
"defaultValue": "false"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "isKernelKillSwitchEnabled",
|
||||||
|
"columnName": "is_kernel_kill_switch_enabled",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true,
|
||||||
|
"defaultValue": "false"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "isLanOnKillSwitchEnabled",
|
||||||
|
"columnName": "is_lan_on_kill_switch_enabled",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true,
|
||||||
|
"defaultValue": "false"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": true,
|
||||||
|
"columnNames": [
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "TunnelConfig",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `wg_quick` TEXT NOT NULL, `tunnel_networks` TEXT NOT NULL DEFAULT '', `is_mobile_data_tunnel` INTEGER NOT NULL DEFAULT false, `is_primary_tunnel` INTEGER NOT NULL DEFAULT false, `am_quick` TEXT NOT NULL DEFAULT '', `is_Active` INTEGER NOT NULL DEFAULT false, `is_ping_enabled` INTEGER NOT NULL DEFAULT false, `ping_interval` INTEGER DEFAULT null, `ping_cooldown` INTEGER DEFAULT null, `ping_ip` TEXT DEFAULT null, `is_ethernet_tunnel` 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": "isPingEnabled",
|
||||||
|
"columnName": "is_ping_enabled",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true,
|
||||||
|
"defaultValue": "false"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "pingInterval",
|
||||||
|
"columnName": "ping_interval",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false,
|
||||||
|
"defaultValue": "null"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "pingCooldown",
|
||||||
|
"columnName": "ping_cooldown",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false,
|
||||||
|
"defaultValue": "null"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "pingIp",
|
||||||
|
"columnName": "ping_ip",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false,
|
||||||
|
"defaultValue": "null"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "isEthernetTunnel",
|
||||||
|
"columnName": "is_ethernet_tunnel",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true,
|
||||||
|
"defaultValue": "false"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": true,
|
||||||
|
"columnNames": [
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_TunnelConfig_name",
|
||||||
|
"unique": true,
|
||||||
|
"columnNames": [
|
||||||
|
"name"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_TunnelConfig_name` ON `${TABLE_NAME}` (`name`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"views": [],
|
||||||
|
"setupQueries": [
|
||||||
|
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
||||||
|
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'ff209157b98a641c424f5086818ec585')"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
|
@ -7,8 +7,11 @@ import androidx.appcompat.app.AppCompatDelegate
|
||||||
import androidx.core.os.LocaleListCompat
|
import androidx.core.os.LocaleListCompat
|
||||||
import com.zaneschepke.logcatter.LogReader
|
import com.zaneschepke.logcatter.LogReader
|
||||||
import com.zaneschepke.wireguardautotunnel.data.repository.AppStateRepository
|
import com.zaneschepke.wireguardautotunnel.data.repository.AppStateRepository
|
||||||
|
import com.zaneschepke.wireguardautotunnel.data.repository.SettingsRepository
|
||||||
import com.zaneschepke.wireguardautotunnel.module.ApplicationScope
|
import com.zaneschepke.wireguardautotunnel.module.ApplicationScope
|
||||||
import com.zaneschepke.wireguardautotunnel.module.IoDispatcher
|
import com.zaneschepke.wireguardautotunnel.module.IoDispatcher
|
||||||
|
import com.zaneschepke.wireguardautotunnel.service.tunnel.BackendState
|
||||||
|
import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelService
|
||||||
import com.zaneschepke.wireguardautotunnel.util.LocaleUtil
|
import com.zaneschepke.wireguardautotunnel.util.LocaleUtil
|
||||||
import com.zaneschepke.wireguardautotunnel.util.ReleaseTree
|
import com.zaneschepke.wireguardautotunnel.util.ReleaseTree
|
||||||
import com.zaneschepke.wireguardautotunnel.util.extensions.isRunningOnTv
|
import com.zaneschepke.wireguardautotunnel.util.extensions.isRunningOnTv
|
||||||
|
@ -32,6 +35,12 @@ class WireGuardAutoTunnel : Application() {
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var appStateRepository: AppStateRepository
|
lateinit var appStateRepository: AppStateRepository
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var settingsRepository: SettingsRepository
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var tunnelService: TunnelService
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
@IoDispatcher
|
@IoDispatcher
|
||||||
lateinit var ioDispatcher: CoroutineDispatcher
|
lateinit var ioDispatcher: CoroutineDispatcher
|
||||||
|
@ -53,6 +62,10 @@ class WireGuardAutoTunnel : Application() {
|
||||||
Timber.plant(ReleaseTree())
|
Timber.plant(ReleaseTree())
|
||||||
}
|
}
|
||||||
applicationScope.launch {
|
applicationScope.launch {
|
||||||
|
if (!settingsRepository.getSettings().isKernelEnabled) {
|
||||||
|
tunnelService.setBackendState(BackendState.SERVICE_ACTIVE, emptyList())
|
||||||
|
}
|
||||||
|
|
||||||
appStateRepository.getLocale()?.let {
|
appStateRepository.getLocale()?.let {
|
||||||
val locale = LocaleUtil.getLocaleFromPrefCode(it)
|
val locale = LocaleUtil.getLocaleFromPrefCode(it)
|
||||||
val appLocale: LocaleListCompat = LocaleListCompat.forLanguageTags(locale)
|
val appLocale: LocaleListCompat = LocaleListCompat.forLanguageTags(locale)
|
||||||
|
@ -69,6 +82,13 @@ class WireGuardAutoTunnel : Application() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onTerminate() {
|
||||||
|
applicationScope.launch {
|
||||||
|
tunnelService.setBackendState(BackendState.INACTIVE, emptyList())
|
||||||
|
}
|
||||||
|
super.onTerminate()
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
lateinit var instance: WireGuardAutoTunnel
|
lateinit var instance: WireGuardAutoTunnel
|
||||||
private set
|
private set
|
||||||
|
|
|
@ -11,7 +11,7 @@ import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
|
||||||
|
|
||||||
@Database(
|
@Database(
|
||||||
entities = [Settings::class, TunnelConfig::class],
|
entities = [Settings::class, TunnelConfig::class],
|
||||||
version = 12,
|
version = 13,
|
||||||
autoMigrations =
|
autoMigrations =
|
||||||
[
|
[
|
||||||
AutoMigration(from = 1, to = 2),
|
AutoMigration(from = 1, to = 2),
|
||||||
|
@ -45,6 +45,10 @@ import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
|
||||||
from = 11,
|
from = 11,
|
||||||
to = 12,
|
to = 12,
|
||||||
),
|
),
|
||||||
|
AutoMigration(
|
||||||
|
from = 12,
|
||||||
|
to = 13,
|
||||||
|
),
|
||||||
],
|
],
|
||||||
exportSchema = true,
|
exportSchema = true,
|
||||||
)
|
)
|
||||||
|
|
|
@ -16,7 +16,7 @@ object Queries {
|
||||||
VALUES
|
VALUES
|
||||||
('false',
|
('false',
|
||||||
'false',
|
'false',
|
||||||
'sampleSSID1,sampleSSID2',
|
'',
|
||||||
'false',
|
'false',
|
||||||
'false',
|
'false',
|
||||||
'false',
|
'false',
|
||||||
|
|
|
@ -65,4 +65,19 @@ data class Settings(
|
||||||
defaultValue = "false",
|
defaultValue = "false",
|
||||||
)
|
)
|
||||||
val isStopOnNoInternetEnabled: Boolean = false,
|
val isStopOnNoInternetEnabled: Boolean = false,
|
||||||
|
@ColumnInfo(
|
||||||
|
name = "is_vpn_kill_switch_enabled",
|
||||||
|
defaultValue = "false",
|
||||||
|
)
|
||||||
|
val isVpnKillSwitchEnabled: Boolean = false,
|
||||||
|
@ColumnInfo(
|
||||||
|
name = "is_kernel_kill_switch_enabled",
|
||||||
|
defaultValue = "false",
|
||||||
|
)
|
||||||
|
val isKernelKillSwitchEnabled: Boolean = false,
|
||||||
|
@ColumnInfo(
|
||||||
|
name = "is_lan_on_kill_switch_enabled",
|
||||||
|
defaultValue = "false",
|
||||||
|
)
|
||||||
|
val isLanOnKillSwitchEnabled: Boolean = false,
|
||||||
)
|
)
|
||||||
|
|
|
@ -92,5 +92,14 @@ data class TunnelConfig(
|
||||||
}
|
}
|
||||||
|
|
||||||
const val AM_QUICK_DEFAULT = ""
|
const val AM_QUICK_DEFAULT = ""
|
||||||
|
|
||||||
|
val IPV4_PUBLIC_NETWORKS = setOf(
|
||||||
|
"0.0.0.0/5", "8.0.0.0/7", "11.0.0.0/8", "12.0.0.0/6", "16.0.0.0/4", "32.0.0.0/3",
|
||||||
|
"64.0.0.0/2", "128.0.0.0/3", "160.0.0.0/5", "168.0.0.0/6", "172.0.0.0/12",
|
||||||
|
"172.32.0.0/11", "172.64.0.0/10", "172.128.0.0/9", "173.0.0.0/8", "174.0.0.0/7",
|
||||||
|
"176.0.0.0/4", "192.0.0.0/9", "192.128.0.0/11", "192.160.0.0/13", "192.169.0.0/16",
|
||||||
|
"192.170.0.0/15", "192.172.0.0/14", "192.176.0.0/12", "192.192.0.0/10",
|
||||||
|
"193.0.0.0/8", "194.0.0.0/7", "196.0.0.0/6", "200.0.0.0/5", "208.0.0.0/4",
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,10 +17,6 @@ interface AppStateRepository {
|
||||||
|
|
||||||
suspend fun setBatteryOptimizationDisableShown(shown: Boolean)
|
suspend fun setBatteryOptimizationDisableShown(shown: Boolean)
|
||||||
|
|
||||||
suspend fun getCurrentSsid(): String?
|
|
||||||
|
|
||||||
suspend fun setCurrentSsid(ssid: String)
|
|
||||||
|
|
||||||
suspend fun isTunnelStatsExpanded(): Boolean
|
suspend fun isTunnelStatsExpanded(): Boolean
|
||||||
|
|
||||||
suspend fun setTunnelStatsExpanded(expanded: Boolean)
|
suspend fun setTunnelStatsExpanded(expanded: Boolean)
|
||||||
|
|
|
@ -38,14 +38,6 @@ class DataStoreAppStateRepository(
|
||||||
dataStoreManager.saveToDataStore(DataStoreManager.batteryDisableShown, shown)
|
dataStoreManager.saveToDataStore(DataStoreManager.batteryDisableShown, shown)
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun getCurrentSsid(): String? {
|
|
||||||
return dataStoreManager.getFromStore(DataStoreManager.currentSSID)
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun setCurrentSsid(ssid: String) {
|
|
||||||
dataStoreManager.saveToDataStore(DataStoreManager.currentSSID, ssid)
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun isTunnelStatsExpanded(): Boolean {
|
override suspend fun isTunnelStatsExpanded(): Boolean {
|
||||||
return dataStoreManager.getFromStore(DataStoreManager.tunnelStatsExpanded)
|
return dataStoreManager.getFromStore(DataStoreManager.tunnelStatsExpanded)
|
||||||
?: GeneralState.IS_TUNNEL_STATS_EXPANDED
|
?: GeneralState.IS_TUNNEL_STATS_EXPANDED
|
||||||
|
|
|
@ -9,12 +9,16 @@ import androidx.lifecycle.LifecycleService
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import com.wireguard.android.util.RootShell
|
import com.wireguard.android.util.RootShell
|
||||||
import com.zaneschepke.wireguardautotunnel.R
|
import com.zaneschepke.wireguardautotunnel.R
|
||||||
|
import com.zaneschepke.wireguardautotunnel.data.domain.Settings
|
||||||
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
|
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
|
||||||
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
|
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
|
||||||
import com.zaneschepke.wireguardautotunnel.module.AppShell
|
import com.zaneschepke.wireguardautotunnel.module.AppShell
|
||||||
import com.zaneschepke.wireguardautotunnel.module.IoDispatcher
|
import com.zaneschepke.wireguardautotunnel.module.IoDispatcher
|
||||||
import com.zaneschepke.wireguardautotunnel.module.MainImmediateDispatcher
|
import com.zaneschepke.wireguardautotunnel.module.MainImmediateDispatcher
|
||||||
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
|
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
|
||||||
|
import com.zaneschepke.wireguardautotunnel.service.foreground.autotunnel.model.AutoTunnelEvent
|
||||||
|
import com.zaneschepke.wireguardautotunnel.service.foreground.autotunnel.model.AutoTunnelState
|
||||||
|
import com.zaneschepke.wireguardautotunnel.service.foreground.autotunnel.model.NetworkState
|
||||||
import com.zaneschepke.wireguardautotunnel.service.network.EthernetService
|
import com.zaneschepke.wireguardautotunnel.service.network.EthernetService
|
||||||
import com.zaneschepke.wireguardautotunnel.service.network.MobileDataService
|
import com.zaneschepke.wireguardautotunnel.service.network.MobileDataService
|
||||||
import com.zaneschepke.wireguardautotunnel.service.network.NetworkService
|
import com.zaneschepke.wireguardautotunnel.service.network.NetworkService
|
||||||
|
@ -23,6 +27,7 @@ import com.zaneschepke.wireguardautotunnel.service.network.WifiService
|
||||||
import com.zaneschepke.wireguardautotunnel.service.notification.NotificationService
|
import com.zaneschepke.wireguardautotunnel.service.notification.NotificationService
|
||||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelService
|
import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelService
|
||||||
import com.zaneschepke.wireguardautotunnel.util.Constants
|
import com.zaneschepke.wireguardautotunnel.util.Constants
|
||||||
|
import com.zaneschepke.wireguardautotunnel.util.extensions.TunnelConfigs
|
||||||
import com.zaneschepke.wireguardautotunnel.util.extensions.cancelWithMessage
|
import com.zaneschepke.wireguardautotunnel.util.extensions.cancelWithMessage
|
||||||
import com.zaneschepke.wireguardautotunnel.util.extensions.getCurrentWifiName
|
import com.zaneschepke.wireguardautotunnel.util.extensions.getCurrentWifiName
|
||||||
import com.zaneschepke.wireguardautotunnel.util.extensions.isReachable
|
import com.zaneschepke.wireguardautotunnel.util.extensions.isReachable
|
||||||
|
@ -32,8 +37,8 @@ import kotlinx.coroutines.CompletableDeferred
|
||||||
import kotlinx.coroutines.CoroutineDispatcher
|
import kotlinx.coroutines.CoroutineDispatcher
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.collect
|
|
||||||
import kotlinx.coroutines.flow.combine
|
import kotlinx.coroutines.flow.combine
|
||||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
import kotlinx.coroutines.flow.update
|
import kotlinx.coroutines.flow.update
|
||||||
|
@ -63,7 +68,7 @@ class AutoTunnelService : LifecycleService() {
|
||||||
lateinit var ethernetService: NetworkService<EthernetService>
|
lateinit var ethernetService: NetworkService<EthernetService>
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var appDataRepository: AppDataRepository
|
lateinit var appDataRepository: Provider<AppDataRepository>
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var notificationService: NotificationService
|
lateinit var notificationService: NotificationService
|
||||||
|
@ -92,6 +97,7 @@ class AutoTunnelService : LifecycleService() {
|
||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
|
serviceManager.autoTunnelService.complete(this)
|
||||||
lifecycleScope.launch(mainImmediateDispatcher) {
|
lifecycleScope.launch(mainImmediateDispatcher) {
|
||||||
kotlin.runCatching {
|
kotlin.runCatching {
|
||||||
launchWatcherNotification()
|
launchWatcherNotification()
|
||||||
|
@ -103,7 +109,6 @@ class AutoTunnelService : LifecycleService() {
|
||||||
|
|
||||||
override fun onBind(intent: Intent): IBinder? {
|
override fun onBind(intent: Intent): IBinder? {
|
||||||
super.onBind(intent)
|
super.onBind(intent)
|
||||||
// We don't provide binding, so return null
|
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -119,9 +124,8 @@ class AutoTunnelService : LifecycleService() {
|
||||||
launchWatcherNotification()
|
launchWatcherNotification()
|
||||||
initWakeLock()
|
initWakeLock()
|
||||||
}
|
}
|
||||||
startSettingsJob()
|
startAutoTunnelJob()
|
||||||
startVpnStateJob()
|
startAutoTunnelStateJob()
|
||||||
startNetworkJobs()
|
|
||||||
startPingStateJob()
|
startPingStateJob()
|
||||||
}.onFailure {
|
}.onFailure {
|
||||||
Timber.e(it)
|
Timber.e(it)
|
||||||
|
@ -129,11 +133,7 @@ class AutoTunnelService : LifecycleService() {
|
||||||
}
|
}
|
||||||
|
|
||||||
fun stop() {
|
fun stop() {
|
||||||
wakeLock?.let {
|
wakeLock?.let { if (it.isHeld) it.release() }
|
||||||
if (it.isHeld) {
|
|
||||||
it.release()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
stopSelf()
|
stopSelf()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -160,48 +160,23 @@ class AutoTunnelService : LifecycleService() {
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun initWakeLock() {
|
private fun initWakeLock() {
|
||||||
wakeLock =
|
wakeLock = (getSystemService(POWER_SERVICE) as PowerManager).run {
|
||||||
(getSystemService(POWER_SERVICE) as PowerManager).run {
|
val tag = this.javaClass.name
|
||||||
val tag = this.javaClass.name
|
newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "$tag::lock").apply {
|
||||||
newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "$tag::lock").apply {
|
try {
|
||||||
try {
|
Timber.i("Initiating wakelock with 10 min timeout")
|
||||||
Timber.i("Initiating wakelock with 10 min timeout")
|
acquire(Constants.BATTERY_SAVER_WATCHER_WAKE_LOCK_TIMEOUT)
|
||||||
acquire(Constants.BATTERY_SAVER_WATCHER_WAKE_LOCK_TIMEOUT)
|
} finally {
|
||||||
} finally {
|
release()
|
||||||
release()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun startSettingsJob() = lifecycleScope.launch {
|
|
||||||
watchForSettingsChanges()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun startVpnStateJob() = lifecycleScope.launch {
|
|
||||||
watchForVpnStateChanges()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun startWifiJob() = lifecycleScope.launch {
|
|
||||||
watchForWifiConnectivityChanges()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun startMobileDataJob() = lifecycleScope.launch {
|
|
||||||
watchForMobileDataConnectivityChanges()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun startEthernetJob() = lifecycleScope.launch {
|
|
||||||
watchForEthernetConnectivityChanges()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun startPingJob() = lifecycleScope.launch {
|
private fun startPingJob() = lifecycleScope.launch {
|
||||||
watchForPingFailure()
|
watchForPingFailure()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun startNetworkEventJob() = lifecycleScope.launch {
|
|
||||||
handleNetworkEventChanges()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun startPingStateJob() = lifecycleScope.launch {
|
private fun startPingStateJob() = lifecycleScope.launch {
|
||||||
autoTunnelStateFlow.collect {
|
autoTunnelStateFlow.collect {
|
||||||
if (it.isPingEnabled()) {
|
if (it.isPingEnabled()) {
|
||||||
|
@ -212,30 +187,6 @@ class AutoTunnelService : LifecycleService() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun watchForMobileDataConnectivityChanges() {
|
|
||||||
withContext(ioDispatcher) {
|
|
||||||
Timber.i("Starting mobile data watcher")
|
|
||||||
mobileDataService.networkStatus.collect { status ->
|
|
||||||
when (status) {
|
|
||||||
is NetworkStatus.Available -> {
|
|
||||||
Timber.i("Gained Mobile data connection")
|
|
||||||
emitMobileDataConnected(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
is NetworkStatus.CapabilitiesChanged -> {
|
|
||||||
emitMobileDataConnected(true)
|
|
||||||
Timber.i("Mobile data capabilities changed")
|
|
||||||
}
|
|
||||||
|
|
||||||
is NetworkStatus.Unavailable -> {
|
|
||||||
emitMobileDataConnected(false)
|
|
||||||
Timber.i("Lost mobile data connection")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun watchForPingFailure() {
|
private suspend fun watchForPingFailure() {
|
||||||
withContext(ioDispatcher) {
|
withContext(ioDispatcher) {
|
||||||
Timber.i("Starting ping watcher")
|
Timber.i("Starting ping watcher")
|
||||||
|
@ -274,136 +225,52 @@ class AutoTunnelService : LifecycleService() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun watchForSettingsChanges() {
|
private fun startAutoTunnelStateJob() = lifecycleScope.launch(ioDispatcher) {
|
||||||
Timber.i("Starting settings watcher")
|
combine(
|
||||||
withContext(ioDispatcher) {
|
combineSettings(),
|
||||||
appDataRepository.settings.getSettingsFlow().combine(
|
combineNetworkEventsJob(),
|
||||||
appDataRepository.tunnels.getTunnelConfigsFlow(),
|
) { double, networkState ->
|
||||||
) { settings, tunnels ->
|
AutoTunnelState(tunnelService.get().vpnState.value, networkState, double.first, double.second)
|
||||||
Pair(settings, tunnels)
|
}.collect { state ->
|
||||||
}.collect { pair ->
|
autoTunnelStateFlow.update {
|
||||||
autoTunnelStateFlow.update {
|
it.copy(state.vpnState, state.networkState, state.settings, state.tunnels)
|
||||||
it.copy(
|
|
||||||
settings = pair.first,
|
|
||||||
tunnels = pair.second,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun watchForVpnStateChanges() {
|
|
||||||
Timber.i("Starting vpn state watcher")
|
|
||||||
withContext(ioDispatcher) {
|
|
||||||
tunnelService.get().vpnState.collect { state ->
|
|
||||||
autoTunnelStateFlow.update {
|
|
||||||
it.copy(vpnState = state)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun startNetworkJobs() {
|
|
||||||
Timber.i("Starting all network state jobs..")
|
|
||||||
startWifiJob()
|
|
||||||
startEthernetJob()
|
|
||||||
startMobileDataJob()
|
|
||||||
startNetworkEventJob()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun cancelAndResetPingJob() {
|
private fun cancelAndResetPingJob() {
|
||||||
pingJob?.cancelWithMessage("Ping job canceled")
|
pingJob?.cancelWithMessage("Ping job canceled")
|
||||||
pingJob = null
|
pingJob = null
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun emitEthernetConnected(connected: Boolean) {
|
private fun combineNetworkEventsJob(): Flow<NetworkState> {
|
||||||
autoTunnelStateFlow.update {
|
return combine(
|
||||||
it.copy(
|
wifiService.networkStatus,
|
||||||
isEthernetConnected = connected,
|
mobileDataService.networkStatus,
|
||||||
|
ethernetService.networkStatus,
|
||||||
|
) { wifi, mobileData, ethernet ->
|
||||||
|
NetworkState(
|
||||||
|
wifi.isConnected,
|
||||||
|
mobileData.isConnected,
|
||||||
|
ethernet.isConnected,
|
||||||
|
when (wifi) {
|
||||||
|
is NetworkStatus.CapabilitiesChanged -> getWifiSSID(wifi.networkCapabilities)
|
||||||
|
is NetworkStatus.Available -> autoTunnelStateFlow.value.networkState.wifiName
|
||||||
|
is NetworkStatus.Unavailable -> null
|
||||||
|
},
|
||||||
)
|
)
|
||||||
}
|
}.distinctUntilChanged()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun emitWifiConnected(connected: Boolean) {
|
private fun combineSettings(): Flow<Pair<Settings, TunnelConfigs>> {
|
||||||
autoTunnelStateFlow.update {
|
return combine(
|
||||||
it.copy(
|
appDataRepository.get().settings.getSettingsFlow(),
|
||||||
isWifiConnected = connected,
|
appDataRepository.get().tunnels.getTunnelConfigsFlow().distinctUntilChanged { old, new ->
|
||||||
)
|
old.map { it.isActive } != new.map { it.isActive }
|
||||||
}
|
},
|
||||||
}
|
) { settings, tunnels ->
|
||||||
|
Pair(settings, tunnels)
|
||||||
private fun emitWifiSSID(ssid: String) {
|
}.distinctUntilChanged()
|
||||||
autoTunnelStateFlow.update {
|
|
||||||
it.copy(
|
|
||||||
currentNetworkSSID = ssid,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun emitMobileDataConnected(connected: Boolean) {
|
|
||||||
autoTunnelStateFlow.update {
|
|
||||||
it.copy(
|
|
||||||
isMobileDataConnected = connected,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun watchForEthernetConnectivityChanges() {
|
|
||||||
withContext(ioDispatcher) {
|
|
||||||
Timber.i("Starting ethernet data watcher")
|
|
||||||
ethernetService.networkStatus.collect { status ->
|
|
||||||
when (status) {
|
|
||||||
is NetworkStatus.Available -> {
|
|
||||||
Timber.i("Gained Ethernet connection")
|
|
||||||
emitEthernetConnected(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
is NetworkStatus.CapabilitiesChanged -> {
|
|
||||||
Timber.i("Ethernet capabilities changed")
|
|
||||||
emitEthernetConnected(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
is NetworkStatus.Unavailable -> {
|
|
||||||
emitEthernetConnected(false)
|
|
||||||
Timber.i("Lost Ethernet connection")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun watchForWifiConnectivityChanges() {
|
|
||||||
withContext(ioDispatcher) {
|
|
||||||
Timber.i("Starting wifi watcher")
|
|
||||||
wifiService.networkStatus.collect { status ->
|
|
||||||
when (status) {
|
|
||||||
is NetworkStatus.Available -> {
|
|
||||||
Timber.i("Gained Wi-Fi connection")
|
|
||||||
emitWifiConnected(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
is NetworkStatus.CapabilitiesChanged -> {
|
|
||||||
Timber.i("Wifi capabilities changed")
|
|
||||||
emitWifiConnected(true)
|
|
||||||
val ssid = getWifiSSID(status.networkCapabilities)
|
|
||||||
ssid?.let { name ->
|
|
||||||
if (name.contains(Constants.UNREADABLE_SSID)) {
|
|
||||||
Timber.w("SSID unreadable: missing permissions")
|
|
||||||
} else {
|
|
||||||
Timber.i("Detected valid SSID")
|
|
||||||
}
|
|
||||||
appDataRepository.appState.setCurrentSsid(name)
|
|
||||||
emitWifiSSID(name)
|
|
||||||
} ?: Timber.w("Failed to read ssid")
|
|
||||||
}
|
|
||||||
|
|
||||||
is NetworkStatus.Unavailable -> {
|
|
||||||
emitWifiConnected(false)
|
|
||||||
Timber.i("Lost Wi-Fi connection")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun getWifiSSID(networkCapabilities: NetworkCapabilities): String? {
|
private suspend fun getWifiSSID(networkCapabilities: NetworkCapabilities): String? {
|
||||||
|
@ -411,26 +278,28 @@ class AutoTunnelService : LifecycleService() {
|
||||||
with(autoTunnelStateFlow.value.settings) {
|
with(autoTunnelStateFlow.value.settings) {
|
||||||
if (isWifiNameByShellEnabled) return@withContext rootShell.get().getCurrentWifiName()
|
if (isWifiNameByShellEnabled) return@withContext rootShell.get().getCurrentWifiName()
|
||||||
wifiService.getNetworkName(networkCapabilities)
|
wifiService.getNetworkName(networkCapabilities)
|
||||||
}
|
}.also {
|
||||||
}
|
if (it?.contains(Constants.UNREADABLE_SSID) == true) {
|
||||||
}
|
Timber.w("SSID unreadable: missing permissions")
|
||||||
|
} else {
|
||||||
private suspend fun handleNetworkEventChanges() {
|
Timber.i("Detected valid SSID")
|
||||||
withContext(ioDispatcher) {
|
|
||||||
Timber.i("Starting auto-tunnel network event watcher")
|
|
||||||
// ignore vpnState emits to allow manual overrides
|
|
||||||
autoTunnelStateFlow.distinctUntilChanged { old, new ->
|
|
||||||
old.copy(vpnState = new.vpnState) == new || old.tunnels.map { it.isActive } != new.tunnels.map { it.isActive }
|
|
||||||
}.collect { watcherState ->
|
|
||||||
when (val event = watcherState.asAutoTunnelEvent()) {
|
|
||||||
is AutoTunnelEvent.Start -> tunnelService.get().startTunnel(
|
|
||||||
event.tunnelConfig
|
|
||||||
?: appDataRepository.getPrimaryOrFirstTunnel(),
|
|
||||||
)
|
|
||||||
is AutoTunnelEvent.Stop -> tunnelService.get().stopTunnel()
|
|
||||||
AutoTunnelEvent.DoNothing -> Timber.i("Auto-tunneling: no condition met")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun startAutoTunnelJob() = lifecycleScope.launch(ioDispatcher) {
|
||||||
|
Timber.i("Starting auto-tunnel network event watcher")
|
||||||
|
autoTunnelStateFlow.collect { watcherState ->
|
||||||
|
Timber.d("New auto tunnel state emitted")
|
||||||
|
when (val event = watcherState.asAutoTunnelEvent()) {
|
||||||
|
is AutoTunnelEvent.Start -> tunnelService.get().startTunnel(
|
||||||
|
event.tunnelConfig
|
||||||
|
?: appDataRepository.get().getPrimaryOrFirstTunnel(),
|
||||||
|
)
|
||||||
|
is AutoTunnelEvent.Stop -> tunnelService.get().stopTunnel()
|
||||||
|
AutoTunnelEvent.DoNothing -> Timber.i("Auto-tunneling: no condition met")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
package com.zaneschepke.wireguardautotunnel.service.foreground.autotunnel
|
package com.zaneschepke.wireguardautotunnel.service.foreground.autotunnel.model
|
||||||
|
|
||||||
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
|
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
|
||||||
|
|
|
@ -1,23 +1,21 @@
|
||||||
package com.zaneschepke.wireguardautotunnel.service.foreground.autotunnel
|
package com.zaneschepke.wireguardautotunnel.service.foreground.autotunnel.model
|
||||||
|
|
||||||
import com.zaneschepke.wireguardautotunnel.data.domain.Settings
|
import com.zaneschepke.wireguardautotunnel.data.domain.Settings
|
||||||
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
|
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
|
||||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnState
|
import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnState
|
||||||
import com.zaneschepke.wireguardautotunnel.util.extensions.TunnelConfigs
|
import com.zaneschepke.wireguardautotunnel.util.extensions.TunnelConfigs
|
||||||
import com.zaneschepke.wireguardautotunnel.util.extensions.isMatchingToWildcardList
|
import com.zaneschepke.wireguardautotunnel.util.extensions.isMatchingToWildcardList
|
||||||
|
import timber.log.Timber
|
||||||
|
|
||||||
data class AutoTunnelState(
|
data class AutoTunnelState(
|
||||||
val vpnState: VpnState = VpnState(),
|
val vpnState: VpnState = VpnState(),
|
||||||
val isWifiConnected: Boolean = false,
|
val networkState: NetworkState = NetworkState(),
|
||||||
val isEthernetConnected: Boolean = false,
|
|
||||||
val isMobileDataConnected: Boolean = false,
|
|
||||||
val currentNetworkSSID: String = "",
|
|
||||||
val settings: Settings = Settings(),
|
val settings: Settings = Settings(),
|
||||||
val tunnels: TunnelConfigs = emptyList(),
|
val tunnels: TunnelConfigs = emptyList(),
|
||||||
) {
|
) {
|
||||||
|
|
||||||
private fun isMobileDataActive(): Boolean {
|
private fun isMobileDataActive(): Boolean {
|
||||||
return !isEthernetConnected && !isWifiConnected && isMobileDataConnected
|
return !networkState.isEthernetConnected && !networkState.isWifiConnected && networkState.isMobileDataConnected
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun isMobileTunnelDataChangeNeeded(): Boolean {
|
private fun isMobileTunnelDataChangeNeeded(): Boolean {
|
||||||
|
@ -44,19 +42,19 @@ data class AutoTunnelState(
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun isWifiActive(): Boolean {
|
private fun isWifiActive(): Boolean {
|
||||||
return !isEthernetConnected && isWifiConnected
|
return !networkState.isEthernetConnected && networkState.isWifiConnected
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun startOnEthernet(): Boolean {
|
private fun startOnEthernet(): Boolean {
|
||||||
return isEthernetConnected && settings.isTunnelOnEthernetEnabled && vpnState.status.isDown()
|
return networkState.isEthernetConnected && settings.isTunnelOnEthernetEnabled && vpnState.status.isDown()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun stopOnEthernet(): Boolean {
|
private fun stopOnEthernet(): Boolean {
|
||||||
return isEthernetConnected && !settings.isTunnelOnEthernetEnabled && vpnState.status.isUp()
|
return networkState.isEthernetConnected && !settings.isTunnelOnEthernetEnabled && vpnState.status.isUp()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun isNoConnectivity(): Boolean {
|
fun isNoConnectivity(): Boolean {
|
||||||
return !isEthernetConnected && !isWifiConnected && !isMobileDataConnected
|
return !networkState.isEthernetConnected && !networkState.isWifiConnected && !networkState.isMobileDataConnected
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun stopOnMobileData(): Boolean {
|
private fun stopOnMobileData(): Boolean {
|
||||||
|
@ -72,7 +70,7 @@ data class AutoTunnelState(
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun changeOnEthernet(): Boolean {
|
private fun changeOnEthernet(): Boolean {
|
||||||
return isEthernetConnected && settings.isTunnelOnEthernetEnabled && isEthernetTunnelChangeNeeded()
|
return networkState.isEthernetConnected && settings.isTunnelOnEthernetEnabled && isEthernetTunnelChangeNeeded()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun stopOnWifi(): Boolean {
|
private fun stopOnWifi(): Boolean {
|
||||||
|
@ -84,6 +82,7 @@ data class AutoTunnelState(
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun startOnUntrustedWifi(): Boolean {
|
private fun startOnUntrustedWifi(): Boolean {
|
||||||
|
Timber.d("Is tunnel on wifi enabled ${settings.isTunnelOnWifiEnabled}")
|
||||||
return isWifiActive() && settings.isTunnelOnWifiEnabled && vpnState.status.isDown() && !isCurrentSSIDTrusted()
|
return isWifiActive() && settings.isTunnelOnWifiEnabled && vpnState.status.isDown() && !isCurrentSSIDTrusted()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -120,19 +119,23 @@ data class AutoTunnelState(
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun isCurrentSSIDTrusted(): Boolean {
|
private fun isCurrentSSIDTrusted(): Boolean {
|
||||||
|
return networkState.wifiName?.let {
|
||||||
|
hasTrustedWifiName(it)
|
||||||
|
} == true
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun hasTrustedWifiName(wifiName: String, wifiNames: List<String> = settings.trustedNetworkSSIDs): Boolean {
|
||||||
return if (settings.isWildcardsEnabled) {
|
return if (settings.isWildcardsEnabled) {
|
||||||
settings.trustedNetworkSSIDs.isMatchingToWildcardList(currentNetworkSSID)
|
wifiNames.isMatchingToWildcardList(wifiName)
|
||||||
} else {
|
} else {
|
||||||
settings.trustedNetworkSSIDs.contains(currentNetworkSSID)
|
wifiNames.contains(wifiName)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getTunnelWithMatchingTunnelNetwork(): TunnelConfig? {
|
private fun getTunnelWithMatchingTunnelNetwork(): TunnelConfig? {
|
||||||
return tunnels.firstOrNull {
|
return networkState.wifiName?.let { wifiName ->
|
||||||
if (settings.isWildcardsEnabled) {
|
tunnels.firstOrNull {
|
||||||
it.tunnelNetworks.isMatchingToWildcardList(currentNetworkSSID)
|
hasTrustedWifiName(wifiName, it.tunnelNetworks)
|
||||||
} else {
|
|
||||||
it.tunnelNetworks.contains(currentNetworkSSID)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -0,0 +1,8 @@
|
||||||
|
package com.zaneschepke.wireguardautotunnel.service.foreground.autotunnel.model
|
||||||
|
|
||||||
|
data class NetworkState(
|
||||||
|
val isWifiConnected: Boolean = false,
|
||||||
|
val isMobileDataConnected: Boolean = false,
|
||||||
|
val isEthernetConnected: Boolean = false,
|
||||||
|
val wifiName: String? = null,
|
||||||
|
)
|
|
@ -10,7 +10,10 @@ import android.os.Build
|
||||||
import kotlinx.coroutines.channels.awaitClose
|
import kotlinx.coroutines.channels.awaitClose
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.callbackFlow
|
import kotlinx.coroutines.flow.callbackFlow
|
||||||
|
import kotlinx.coroutines.flow.catch
|
||||||
|
import kotlinx.coroutines.flow.conflate
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
|
import timber.log.Timber
|
||||||
|
|
||||||
abstract class BaseNetworkService<T : BaseNetworkService<T>>(
|
abstract class BaseNetworkService<T : BaseNetworkService<T>>(
|
||||||
val context: Context,
|
val context: Context,
|
||||||
|
@ -22,8 +25,17 @@ abstract class BaseNetworkService<T : BaseNetworkService<T>>(
|
||||||
val wifiManager =
|
val wifiManager =
|
||||||
context.applicationContext.getSystemService(Context.WIFI_SERVICE) as WifiManager
|
context.applicationContext.getSystemService(Context.WIFI_SERVICE) as WifiManager
|
||||||
|
|
||||||
|
fun checkHasCapability(networkCapability: Int): Boolean {
|
||||||
|
val network = connectivityManager.activeNetwork
|
||||||
|
val networkCapabilities = connectivityManager.getNetworkCapabilities(network)
|
||||||
|
return networkCapabilities?.hasTransport(networkCapability) == true
|
||||||
|
}
|
||||||
|
|
||||||
override val networkStatus =
|
override val networkStatus =
|
||||||
callbackFlow {
|
callbackFlow {
|
||||||
|
if (!checkHasCapability(networkCapability)) {
|
||||||
|
trySend(NetworkStatus.Unavailable())
|
||||||
|
}
|
||||||
val networkStatusCallback =
|
val networkStatusCallback =
|
||||||
when (Build.VERSION.SDK_INT) {
|
when (Build.VERSION.SDK_INT) {
|
||||||
in Build.VERSION_CODES.S..Int.MAX_VALUE -> {
|
in Build.VERSION_CODES.S..Int.MAX_VALUE -> {
|
||||||
|
@ -36,7 +48,7 @@ abstract class BaseNetworkService<T : BaseNetworkService<T>>(
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onLost(network: Network) {
|
override fun onLost(network: Network) {
|
||||||
trySend(NetworkStatus.Unavailable(network))
|
trySend(NetworkStatus.Unavailable())
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCapabilitiesChanged(network: Network, networkCapabilities: NetworkCapabilities) {
|
override fun onCapabilitiesChanged(network: Network, networkCapabilities: NetworkCapabilities) {
|
||||||
|
@ -57,7 +69,7 @@ abstract class BaseNetworkService<T : BaseNetworkService<T>>(
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onLost(network: Network) {
|
override fun onLost(network: Network) {
|
||||||
trySend(NetworkStatus.Unavailable(network))
|
trySend(NetworkStatus.Unavailable())
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCapabilitiesChanged(network: Network, networkCapabilities: NetworkCapabilities) {
|
override fun onCapabilitiesChanged(network: Network, networkCapabilities: NetworkCapabilities) {
|
||||||
|
@ -80,17 +92,20 @@ abstract class BaseNetworkService<T : BaseNetworkService<T>>(
|
||||||
connectivityManager.registerNetworkCallback(request, networkStatusCallback)
|
connectivityManager.registerNetworkCallback(request, networkStatusCallback)
|
||||||
|
|
||||||
awaitClose { connectivityManager.unregisterNetworkCallback(networkStatusCallback) }
|
awaitClose { connectivityManager.unregisterNetworkCallback(networkStatusCallback) }
|
||||||
}
|
}.catch {
|
||||||
|
Timber.e(it)
|
||||||
|
// conflate for backpressure
|
||||||
|
}.conflate()
|
||||||
}
|
}
|
||||||
|
|
||||||
inline fun <Result> Flow<NetworkStatus>.map(
|
inline fun <Result> Flow<NetworkStatus>.map(
|
||||||
crossinline onUnavailable: suspend (network: Network) -> Result,
|
crossinline onUnavailable: suspend () -> Result,
|
||||||
crossinline onAvailable: suspend (network: Network) -> Result,
|
crossinline onAvailable: suspend (network: Network) -> Result,
|
||||||
crossinline onCapabilitiesChanged:
|
crossinline onCapabilitiesChanged:
|
||||||
suspend (network: Network, networkCapabilities: NetworkCapabilities) -> Result,
|
suspend (network: Network, networkCapabilities: NetworkCapabilities) -> Result,
|
||||||
): Flow<Result> = map { status ->
|
): Flow<Result> = map { status ->
|
||||||
when (status) {
|
when (status) {
|
||||||
is NetworkStatus.Unavailable -> onUnavailable(status.network)
|
is NetworkStatus.Unavailable -> onUnavailable()
|
||||||
is NetworkStatus.Available -> onAvailable(status.network)
|
is NetworkStatus.Available -> onAvailable(status.network)
|
||||||
is NetworkStatus.CapabilitiesChanged ->
|
is NetworkStatus.CapabilitiesChanged ->
|
||||||
onCapabilitiesChanged(
|
onCapabilitiesChanged(
|
||||||
|
|
|
@ -4,10 +4,11 @@ import android.net.Network
|
||||||
import android.net.NetworkCapabilities
|
import android.net.NetworkCapabilities
|
||||||
|
|
||||||
sealed class NetworkStatus {
|
sealed class NetworkStatus {
|
||||||
class Available(val network: Network) : NetworkStatus()
|
abstract val isConnected: Boolean
|
||||||
|
class Available(val network: Network, override val isConnected: Boolean = true) : NetworkStatus()
|
||||||
|
|
||||||
class Unavailable(val network: Network) : NetworkStatus()
|
class Unavailable(override val isConnected: Boolean = false) : NetworkStatus()
|
||||||
|
|
||||||
class CapabilitiesChanged(val network: Network, val networkCapabilities: NetworkCapabilities) :
|
class CapabilitiesChanged(val network: Network, val networkCapabilities: NetworkCapabilities, override val isConnected: Boolean = true) :
|
||||||
NetworkStatus()
|
NetworkStatus()
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,7 @@
|
||||||
|
package com.zaneschepke.wireguardautotunnel.service.tunnel
|
||||||
|
|
||||||
|
enum class BackendState {
|
||||||
|
KILL_SWITCH_ACTIVE,
|
||||||
|
SERVICE_ACTIVE,
|
||||||
|
INACTIVE,
|
||||||
|
}
|
|
@ -12,13 +12,17 @@ interface TunnelService : Tunnel, org.amnezia.awg.backend.Tunnel {
|
||||||
|
|
||||||
suspend fun bounceTunnel()
|
suspend fun bounceTunnel()
|
||||||
|
|
||||||
|
suspend fun getBackendState(): BackendState
|
||||||
|
|
||||||
|
suspend fun setBackendState(backendState: BackendState, allowedIps: Collection<String>)
|
||||||
|
|
||||||
val vpnState: StateFlow<VpnState>
|
val vpnState: StateFlow<VpnState>
|
||||||
|
|
||||||
suspend fun runningTunnelNames(): Set<String>
|
suspend fun runningTunnelNames(): Set<String>
|
||||||
|
|
||||||
suspend fun getState(): TunnelState
|
suspend fun getState(): TunnelState
|
||||||
|
|
||||||
fun cancelStatsJob()
|
fun cancelActiveTunnelJobs()
|
||||||
|
|
||||||
fun startStatsJob()
|
fun startActiveTunnelJobs()
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,15 +12,15 @@ import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
|
||||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.statistics.AmneziaStatistics
|
import com.zaneschepke.wireguardautotunnel.service.tunnel.statistics.AmneziaStatistics
|
||||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.statistics.TunnelStatistics
|
import com.zaneschepke.wireguardautotunnel.service.tunnel.statistics.TunnelStatistics
|
||||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.statistics.WireGuardStatistics
|
import com.zaneschepke.wireguardautotunnel.service.tunnel.statistics.WireGuardStatistics
|
||||||
|
import com.zaneschepke.wireguardautotunnel.util.extensions.asAmBackendState
|
||||||
|
import com.zaneschepke.wireguardautotunnel.util.extensions.asBackendState
|
||||||
import kotlinx.coroutines.CoroutineDispatcher
|
import kotlinx.coroutines.CoroutineDispatcher
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.SharingStarted
|
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.combine
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
import kotlinx.coroutines.flow.stateIn
|
|
||||||
import kotlinx.coroutines.flow.update
|
import kotlinx.coroutines.flow.update
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.sync.Mutex
|
import kotlinx.coroutines.sync.Mutex
|
||||||
|
@ -35,7 +35,7 @@ class WireGuardTunnel
|
||||||
@Inject
|
@Inject
|
||||||
constructor(
|
constructor(
|
||||||
private val amneziaBackend: Provider<org.amnezia.awg.backend.Backend>,
|
private val amneziaBackend: Provider<org.amnezia.awg.backend.Backend>,
|
||||||
tunnelConfigRepository: TunnelConfigRepository,
|
private val tunnelConfigRepository: TunnelConfigRepository,
|
||||||
@Kernel private val kernelBackend: Provider<Backend>,
|
@Kernel private val kernelBackend: Provider<Backend>,
|
||||||
private val appDataRepository: AppDataRepository,
|
private val appDataRepository: AppDataRepository,
|
||||||
@ApplicationScope private val applicationScope: CoroutineScope,
|
@ApplicationScope private val applicationScope: CoroutineScope,
|
||||||
|
@ -44,22 +44,28 @@ constructor(
|
||||||
) : TunnelService {
|
) : TunnelService {
|
||||||
|
|
||||||
private val _vpnState = MutableStateFlow(VpnState())
|
private val _vpnState = MutableStateFlow(VpnState())
|
||||||
override val vpnState: StateFlow<VpnState> = _vpnState.combine(
|
override val vpnState: StateFlow<VpnState> = _vpnState.asStateFlow()
|
||||||
tunnelConfigRepository.getTunnelConfigsFlow(),
|
|
||||||
) {
|
|
||||||
vpnState, tunnels ->
|
|
||||||
vpnState.copy(
|
|
||||||
tunnelConfig = tunnels.firstOrNull { it.id == vpnState.tunnelConfig?.id },
|
|
||||||
)
|
|
||||||
}.stateIn(applicationScope, SharingStarted.Eagerly, VpnState())
|
|
||||||
|
|
||||||
private var statsJob: Job? = null
|
private var statsJob: Job? = null
|
||||||
|
private var tunnelChangesJob: Job? = null
|
||||||
|
|
||||||
private val mutex = Mutex()
|
@get:Synchronized @set:Synchronized
|
||||||
|
private var isKernelBackend: Boolean? = null
|
||||||
|
|
||||||
|
private val tunnelControlMutex = Mutex()
|
||||||
|
|
||||||
|
init {
|
||||||
|
applicationScope.launch(ioDispatcher) {
|
||||||
|
appDataRepository.settings.getSettingsFlow().collect {
|
||||||
|
isKernelBackend = it.isKernelEnabled
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private suspend fun backend(): Any {
|
private suspend fun backend(): Any {
|
||||||
val settings = appDataRepository.settings.getSettings()
|
val isKernelEnabled = isKernelBackend
|
||||||
if (settings.isKernelEnabled) return kernelBackend.get()
|
?: appDataRepository.settings.getSettings().isKernelEnabled
|
||||||
|
if (isKernelEnabled) return kernelBackend.get()
|
||||||
return amneziaBackend.get()
|
return amneziaBackend.get()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -103,13 +109,15 @@ constructor(
|
||||||
override suspend fun startTunnel(tunnelConfig: TunnelConfig?, background: Boolean) {
|
override suspend fun startTunnel(tunnelConfig: TunnelConfig?, background: Boolean) {
|
||||||
if (tunnelConfig == null) return
|
if (tunnelConfig == null) return
|
||||||
withContext(ioDispatcher) {
|
withContext(ioDispatcher) {
|
||||||
mutex.withLock {
|
if (isTunnelAlreadyRunning(tunnelConfig)) return@withContext
|
||||||
if (isTunnelAlreadyRunning(tunnelConfig)) return@withContext
|
withServiceActive {
|
||||||
onBeforeStart(background)
|
onBeforeStart(background)
|
||||||
setState(tunnelConfig, TunnelState.UP).onSuccess {
|
tunnelControlMutex.withLock {
|
||||||
startStatsJob()
|
setState(tunnelConfig, TunnelState.UP).onSuccess {
|
||||||
if (it.isUp()) appDataRepository.tunnels.save(tunnelConfig.copy(isActive = true))
|
startActiveTunnelJobs()
|
||||||
updateTunnelState(it, tunnelConfig)
|
if (it.isUp()) appDataRepository.tunnels.save(tunnelConfig.copy(isActive = true))
|
||||||
|
updateTunnelState(it, tunnelConfig)
|
||||||
|
}
|
||||||
}.onFailure {
|
}.onFailure {
|
||||||
Timber.e(it)
|
Timber.e(it)
|
||||||
}
|
}
|
||||||
|
@ -119,10 +127,10 @@ constructor(
|
||||||
|
|
||||||
override suspend fun stopTunnel() {
|
override suspend fun stopTunnel() {
|
||||||
withContext(ioDispatcher) {
|
withContext(ioDispatcher) {
|
||||||
mutex.withLock {
|
if (_vpnState.value.status.isDown()) return@withContext
|
||||||
if (_vpnState.value.status.isDown()) return@withContext
|
with(_vpnState.value) {
|
||||||
with(_vpnState.value) {
|
if (tunnelConfig == null) return@withContext
|
||||||
if (tunnelConfig == null) return@withContext
|
tunnelControlMutex.withLock {
|
||||||
setState(tunnelConfig, TunnelState.DOWN).onSuccess {
|
setState(tunnelConfig, TunnelState.DOWN).onSuccess {
|
||||||
updateTunnelState(it, null)
|
updateTunnelState(it, null)
|
||||||
onStop(tunnelConfig)
|
onStop(tunnelConfig)
|
||||||
|
@ -135,11 +143,64 @@ constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private suspend fun toggleTunnel(tunnelConfig: TunnelConfig) {
|
||||||
|
withContext(ioDispatcher) {
|
||||||
|
tunnelControlMutex.withLock {
|
||||||
|
setState(tunnelConfig, TunnelState.TOGGLE)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// utility to keep vpnService alive during rapid changes to prevent bad states
|
||||||
|
private suspend fun withServiceActive(callback: suspend () -> Unit) {
|
||||||
|
when (val backend = backend()) {
|
||||||
|
is org.amnezia.awg.backend.Backend -> {
|
||||||
|
val backendState = backend.backendState
|
||||||
|
if (backendState == org.amnezia.awg.backend.Backend.BackendState.INACTIVE) {
|
||||||
|
backend.setBackendState(org.amnezia.awg.backend.Backend.BackendState.SERVICE_ACTIVE, emptyList())
|
||||||
|
}
|
||||||
|
callback()
|
||||||
|
}
|
||||||
|
is Backend -> {
|
||||||
|
callback()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override suspend fun bounceTunnel() {
|
override suspend fun bounceTunnel() {
|
||||||
if (_vpnState.value.tunnelConfig == null) return
|
_vpnState.value.tunnelConfig?.let {
|
||||||
val config = _vpnState.value.tunnelConfig
|
withServiceActive {
|
||||||
stopTunnel()
|
toggleTunnel(it)
|
||||||
startTunnel(config)
|
toggleTunnel(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun getBackendState(): BackendState {
|
||||||
|
return when (val backend = backend()) {
|
||||||
|
is org.amnezia.awg.backend.Backend -> {
|
||||||
|
backend.backendState.asBackendState()
|
||||||
|
}
|
||||||
|
is Backend -> {
|
||||||
|
BackendState.SERVICE_ACTIVE
|
||||||
|
}
|
||||||
|
else -> BackendState.INACTIVE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun setBackendState(backendState: BackendState, allowedIps: Collection<String>) {
|
||||||
|
kotlin.runCatching {
|
||||||
|
when (val backend = backend()) {
|
||||||
|
is org.amnezia.awg.backend.Backend -> {
|
||||||
|
backend.setBackendState(backendState.asAmBackendState(), allowedIps)
|
||||||
|
}
|
||||||
|
is Backend -> {
|
||||||
|
// TODO not yet implemented
|
||||||
|
Timber.d("Kernel backend state not yet implemented")
|
||||||
|
}
|
||||||
|
else -> Unit
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun shutDownActiveTunnel() {
|
private suspend fun shutDownActiveTunnel() {
|
||||||
|
@ -169,7 +230,7 @@ constructor(
|
||||||
|
|
||||||
private suspend fun onStop(tunnelConfig: TunnelConfig) {
|
private suspend fun onStop(tunnelConfig: TunnelConfig) {
|
||||||
appDataRepository.tunnels.save(tunnelConfig.copy(isActive = false))
|
appDataRepository.tunnels.save(tunnelConfig.copy(isActive = false))
|
||||||
cancelStatsJob()
|
cancelActiveTunnelJobs()
|
||||||
resetBackendStatistics()
|
resetBackendStatistics()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -179,7 +240,13 @@ constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun emitBackendStatistics(statistics: TunnelStatistics) {
|
private fun updateTunnelConfig(tunnelConfig: TunnelConfig?) {
|
||||||
|
_vpnState.update {
|
||||||
|
it.copy(tunnelConfig = tunnelConfig)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateBackendStatistics(statistics: TunnelStatistics) {
|
||||||
_vpnState.update {
|
_vpnState.update {
|
||||||
it.copy(statistics = statistics)
|
it.copy(statistics = statistics)
|
||||||
}
|
}
|
||||||
|
@ -199,12 +266,14 @@ constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun cancelStatsJob() {
|
override fun cancelActiveTunnelJobs() {
|
||||||
statsJob?.cancel()
|
statsJob?.cancel()
|
||||||
|
tunnelChangesJob?.cancel()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun startStatsJob() {
|
override fun startActiveTunnelJobs() {
|
||||||
statsJob = startTunnelStatisticsJob()
|
statsJob = startTunnelStatisticsJob()
|
||||||
|
tunnelChangesJob = startTunnelConfigChangesJob()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getName(): String {
|
override fun getName(): String {
|
||||||
|
@ -216,11 +285,11 @@ constructor(
|
||||||
delay(STATS_START_DELAY)
|
delay(STATS_START_DELAY)
|
||||||
while (true) {
|
while (true) {
|
||||||
when (backend) {
|
when (backend) {
|
||||||
is Backend -> emitBackendStatistics(
|
is Backend -> updateBackendStatistics(
|
||||||
WireGuardStatistics(backend.getStatistics(this@WireGuardTunnel)),
|
WireGuardStatistics(backend.getStatistics(this@WireGuardTunnel)),
|
||||||
)
|
)
|
||||||
is org.amnezia.awg.backend.Backend -> {
|
is org.amnezia.awg.backend.Backend -> {
|
||||||
emitBackendStatistics(
|
updateBackendStatistics(
|
||||||
AmneziaStatistics(
|
AmneziaStatistics(
|
||||||
backend.getStatistics(this@WireGuardTunnel),
|
backend.getStatistics(this@WireGuardTunnel),
|
||||||
),
|
),
|
||||||
|
@ -231,6 +300,22 @@ constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun startTunnelConfigChangesJob() = applicationScope.launch(ioDispatcher) {
|
||||||
|
tunnelConfigRepository.getTunnelConfigsFlow().collect {
|
||||||
|
with(_vpnState.value) {
|
||||||
|
if (status.isDown() || tunnelConfig == null) return@collect
|
||||||
|
val vpnConfigFromStorage = it.first { it.id == tunnelConfig.id }
|
||||||
|
val isRestartNeeded = vpnConfigFromStorage.wgQuick != tunnelConfig.wgQuick ||
|
||||||
|
vpnConfigFromStorage.amQuick != tunnelConfig.amQuick
|
||||||
|
updateTunnelConfig(vpnConfigFromStorage)
|
||||||
|
if (isRestartNeeded) {
|
||||||
|
Timber.d("Bouncing tunnel on config change")
|
||||||
|
bounceTunnel()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun onStateChange(newState: Tunnel.State) {
|
override fun onStateChange(newState: Tunnel.State) {
|
||||||
_vpnState.update {
|
_vpnState.update {
|
||||||
it.copy(status = TunnelState.from(newState))
|
it.copy(status = TunnelState.from(newState))
|
||||||
|
|
|
@ -9,10 +9,12 @@ import com.wireguard.android.util.RootShell
|
||||||
import com.zaneschepke.logcatter.LogReader
|
import com.zaneschepke.logcatter.LogReader
|
||||||
import com.zaneschepke.wireguardautotunnel.R
|
import com.zaneschepke.wireguardautotunnel.R
|
||||||
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
|
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
|
||||||
|
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
|
||||||
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
|
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
|
||||||
import com.zaneschepke.wireguardautotunnel.module.AppShell
|
import com.zaneschepke.wireguardautotunnel.module.AppShell
|
||||||
import com.zaneschepke.wireguardautotunnel.module.IoDispatcher
|
import com.zaneschepke.wireguardautotunnel.module.IoDispatcher
|
||||||
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
|
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
|
||||||
|
import com.zaneschepke.wireguardautotunnel.service.tunnel.BackendState
|
||||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelService
|
import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelService
|
||||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelState
|
import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelState
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.common.snackbar.SnackbarController
|
import com.zaneschepke.wireguardautotunnel.ui.common.snackbar.SnackbarController
|
||||||
|
@ -34,6 +36,7 @@ import kotlinx.coroutines.flow.update
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.plus
|
import kotlinx.coroutines.plus
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
import timber.log.Timber
|
||||||
import xyz.teamgravity.pin_lock_compose.PinManager
|
import xyz.teamgravity.pin_lock_compose.PinManager
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import javax.inject.Provider
|
import javax.inject.Provider
|
||||||
|
@ -80,7 +83,7 @@ constructor(
|
||||||
init {
|
init {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
initPin()
|
initPin()
|
||||||
initAutoTunnel()
|
initServices()
|
||||||
initTunnel()
|
initTunnel()
|
||||||
appReadyCheck()
|
appReadyCheck()
|
||||||
}
|
}
|
||||||
|
@ -94,7 +97,7 @@ constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun initTunnel() {
|
private suspend fun initTunnel() {
|
||||||
if (tunnelService.get().getState() == TunnelState.UP) tunnelService.get().startStatsJob()
|
if (tunnelService.get().getState() == TunnelState.UP) tunnelService.get().startActiveTunnelJobs()
|
||||||
val activeTunnels = appDataRepository.tunnels.getActive()
|
val activeTunnels = appDataRepository.tunnels.getActive()
|
||||||
if (activeTunnels.isNotEmpty() &&
|
if (activeTunnels.isNotEmpty() &&
|
||||||
tunnelService.get().getState() == TunnelState.DOWN
|
tunnelService.get().getState() == TunnelState.DOWN
|
||||||
|
@ -108,9 +111,12 @@ constructor(
|
||||||
if (isPinEnabled) PinManager.initialize(WireGuardAutoTunnel.instance)
|
if (isPinEnabled) PinManager.initialize(WireGuardAutoTunnel.instance)
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun initAutoTunnel() {
|
private suspend fun initServices() {
|
||||||
val settings = appDataRepository.settings.getSettings()
|
withContext(ioDispatcher) {
|
||||||
if (settings.isAutoTunnelEnabled) serviceManager.startAutoTunnel(false)
|
val settings = appDataRepository.settings.getSettings()
|
||||||
|
handleVpnKillSwitchChange(settings.isVpnKillSwitchEnabled)
|
||||||
|
if (settings.isAutoTunnelEnabled) serviceManager.startAutoTunnel(false)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onPinLockDisabled() = viewModelScope.launch(ioDispatcher) {
|
fun onPinLockDisabled() = viewModelScope.launch(ioDispatcher) {
|
||||||
|
@ -170,10 +176,50 @@ constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun onToggleVpnKillSwitch(enabled: Boolean) = viewModelScope.launch {
|
||||||
|
with(uiState.value.settings) {
|
||||||
|
appDataRepository.settings.save(
|
||||||
|
copy(
|
||||||
|
isVpnKillSwitchEnabled = enabled,
|
||||||
|
isLanOnKillSwitchEnabled = if (enabled) isLanOnKillSwitchEnabled else false,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
handleVpnKillSwitchChange(enabled)
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun handleVpnKillSwitchChange(enabled: Boolean) {
|
||||||
|
withContext(ioDispatcher) {
|
||||||
|
if (enabled) {
|
||||||
|
Timber.d("Starting kill switch")
|
||||||
|
val allowedIps = if (appDataRepository.settings.getSettings().isLanOnKillSwitchEnabled) {
|
||||||
|
TunnelConfig.IPV4_PUBLIC_NETWORKS
|
||||||
|
} else {
|
||||||
|
emptySet()
|
||||||
|
}
|
||||||
|
tunnelService.get().setBackendState(BackendState.KILL_SWITCH_ACTIVE, allowedIps)
|
||||||
|
} else {
|
||||||
|
Timber.d("Sending shutdown of kill switch")
|
||||||
|
tunnelService.get().setBackendState(BackendState.SERVICE_ACTIVE, emptySet())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onToggleLanOnKillSwitch(enabled: Boolean) = viewModelScope.launch(ioDispatcher) {
|
||||||
|
appDataRepository.settings.save(
|
||||||
|
uiState.value.settings.copy(
|
||||||
|
isLanOnKillSwitchEnabled = enabled,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
val allowedIps = if (enabled) TunnelConfig.IPV4_PUBLIC_NETWORKS else emptySet()
|
||||||
|
Timber.d("Setting allowedIps $allowedIps")
|
||||||
|
tunnelService.get().setBackendState(BackendState.KILL_SWITCH_ACTIVE, allowedIps)
|
||||||
|
}
|
||||||
|
|
||||||
fun onToggleShortcutsEnabled() = viewModelScope.launch {
|
fun onToggleShortcutsEnabled() = viewModelScope.launch {
|
||||||
with(uiState.value.settings) {
|
with(uiState.value.settings) {
|
||||||
appDataRepository.settings.save(
|
appDataRepository.settings.save(
|
||||||
this.copy(
|
copy(
|
||||||
isShortcutsEnabled = !isShortcutsEnabled,
|
isShortcutsEnabled = !isShortcutsEnabled,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
|
@ -52,6 +52,7 @@ import com.zaneschepke.wireguardautotunnel.ui.screens.settings.appearance.displa
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.appearance.language.LanguageScreen
|
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.appearance.language.LanguageScreen
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.autotunnel.AutoTunnelScreen
|
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.autotunnel.AutoTunnelScreen
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.disclosure.LocationDisclosureScreen
|
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.disclosure.LocationDisclosureScreen
|
||||||
|
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.killswitch.KillSwitchScreen
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.screens.support.SupportScreen
|
import com.zaneschepke.wireguardautotunnel.ui.screens.support.SupportScreen
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.screens.support.logs.LogsScreen
|
import com.zaneschepke.wireguardautotunnel.ui.screens.support.logs.LogsScreen
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.theme.WireguardAutoTunnelTheme
|
import com.zaneschepke.wireguardautotunnel.ui.theme.WireguardAutoTunnelTheme
|
||||||
|
@ -144,8 +145,8 @@ class MainActivity : AppCompatActivity() {
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
) {
|
) { padding ->
|
||||||
Box(modifier = Modifier.fillMaxSize().padding(it)) {
|
Box(modifier = Modifier.fillMaxSize().padding(padding)) {
|
||||||
NavHost(
|
NavHost(
|
||||||
navController,
|
navController,
|
||||||
enterTransition = { fadeIn(tween(Constants.TRANSITION_ANIMATION_TIME)) },
|
enterTransition = { fadeIn(tween(Constants.TRANSITION_ANIMATION_TIME)) },
|
||||||
|
@ -207,6 +208,9 @@ class MainActivity : AppCompatActivity() {
|
||||||
composable<Route.Scanner> {
|
composable<Route.Scanner> {
|
||||||
ScannerScreen()
|
ScannerScreen()
|
||||||
}
|
}
|
||||||
|
composable<Route.KillSwitch> {
|
||||||
|
KillSwitchScreen(appUiState, viewModel)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -218,6 +222,6 @@ class MainActivity : AppCompatActivity() {
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
// save battery by not polling stats while app is closed
|
// save battery by not polling stats while app is closed
|
||||||
tunnelService.cancelStatsJob()
|
tunnelService.cancelActiveTunnelJobs()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,6 +21,9 @@ sealed class Route {
|
||||||
@Serializable
|
@Serializable
|
||||||
data object Display : Route()
|
data object Display : Route()
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data object KillSwitch : Route()
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data object Language : Route()
|
data object Language : Route()
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
package com.zaneschepke.wireguardautotunnel.ui.screens.main.components
|
package com.zaneschepke.wireguardautotunnel.ui.common.permission.vpn
|
||||||
|
|
||||||
import androidx.compose.foundation.text.ClickableText
|
import androidx.compose.foundation.text.ClickableText
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
|
@ -0,0 +1,38 @@
|
||||||
|
package com.zaneschepke.wireguardautotunnel.ui.common.permission.vpn
|
||||||
|
|
||||||
|
import android.net.VpnService
|
||||||
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
|
import androidx.appcompat.app.AppCompatActivity.RESULT_OK
|
||||||
|
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.platform.LocalContext
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
inline fun <T> withVpnPermission(crossinline onSuccess: (t: T) -> Unit): (t: T) -> Unit {
|
||||||
|
val context = LocalContext.current
|
||||||
|
|
||||||
|
var showVpnPermissionDialog by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
val vpnActivity =
|
||||||
|
rememberLauncherForActivityResult(
|
||||||
|
ActivityResultContracts.StartActivityForResult(),
|
||||||
|
onResult = {
|
||||||
|
if (it.resultCode != RESULT_OK) showVpnPermissionDialog = true
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
VpnDeniedDialog(showVpnPermissionDialog, onDismiss = { showVpnPermissionDialog = false })
|
||||||
|
|
||||||
|
return {
|
||||||
|
val intent = VpnService.prepare(context)
|
||||||
|
if (intent != null) {
|
||||||
|
vpnActivity.launch(intent)
|
||||||
|
} else {
|
||||||
|
onSuccess(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,35 @@
|
||||||
|
package com.zaneschepke.wireguardautotunnel.ui.common.permission
|
||||||
|
|
||||||
|
import android.content.Intent
|
||||||
|
import android.net.Uri
|
||||||
|
import android.provider.Settings
|
||||||
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
|
import androidx.activity.result.ActivityResult
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import com.zaneschepke.wireguardautotunnel.util.extensions.isBatteryOptimizationsDisabled
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
inline fun withIgnoreBatteryOpt(ignore: Boolean, crossinline callback: () -> Unit): () -> Unit {
|
||||||
|
val context = LocalContext.current
|
||||||
|
val batteryActivity =
|
||||||
|
rememberLauncherForActivityResult(
|
||||||
|
ActivityResultContracts.StartActivityForResult(),
|
||||||
|
) { result: ActivityResult ->
|
||||||
|
// we only ask once
|
||||||
|
callback()
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
if (ignore || context.isBatteryOptimizationsDisabled()) {
|
||||||
|
callback()
|
||||||
|
} else {
|
||||||
|
batteryActivity.launch(
|
||||||
|
Intent().apply {
|
||||||
|
action = Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS
|
||||||
|
data = Uri.parse("package:${context.packageName}")
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,13 +1,7 @@
|
||||||
package com.zaneschepke.wireguardautotunnel.ui.screens.main
|
package com.zaneschepke.wireguardautotunnel.ui.screens.main
|
||||||
|
|
||||||
import android.content.Intent
|
|
||||||
import android.net.Uri
|
|
||||||
import android.net.VpnService
|
|
||||||
import android.provider.Settings
|
|
||||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
import androidx.activity.result.ActivityResult
|
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.appcompat.app.AppCompatActivity.RESULT_OK
|
|
||||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||||
import androidx.compose.foundation.gestures.ScrollableDefaults
|
import androidx.compose.foundation.gestures.ScrollableDefaults
|
||||||
import androidx.compose.foundation.gestures.detectTapGestures
|
import androidx.compose.foundation.gestures.detectTapGestures
|
||||||
|
@ -51,15 +45,15 @@ import com.zaneschepke.wireguardautotunnel.ui.common.dialog.InfoDialog
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.common.functions.rememberFileImportLauncherForResult
|
import com.zaneschepke.wireguardautotunnel.ui.common.functions.rememberFileImportLauncherForResult
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.LocalNavController
|
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.LocalNavController
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.TopNavBar
|
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.TopNavBar
|
||||||
|
import com.zaneschepke.wireguardautotunnel.ui.common.permission.vpn.withVpnPermission
|
||||||
|
import com.zaneschepke.wireguardautotunnel.ui.common.permission.withIgnoreBatteryOpt
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.common.snackbar.SnackbarController
|
import com.zaneschepke.wireguardautotunnel.ui.common.snackbar.SnackbarController
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.screens.main.components.AutoTunnelRowItem
|
import com.zaneschepke.wireguardautotunnel.ui.screens.main.components.AutoTunnelRowItem
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.screens.main.components.GettingStartedLabel
|
import com.zaneschepke.wireguardautotunnel.ui.screens.main.components.GettingStartedLabel
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.screens.main.components.ScrollDismissFab
|
import com.zaneschepke.wireguardautotunnel.ui.screens.main.components.ScrollDismissFab
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.screens.main.components.TunnelImportSheet
|
import com.zaneschepke.wireguardautotunnel.ui.screens.main.components.TunnelImportSheet
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.screens.main.components.TunnelRowItem
|
import com.zaneschepke.wireguardautotunnel.ui.screens.main.components.TunnelRowItem
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.screens.main.components.VpnDeniedDialog
|
|
||||||
import com.zaneschepke.wireguardautotunnel.util.Constants
|
import com.zaneschepke.wireguardautotunnel.util.Constants
|
||||||
import com.zaneschepke.wireguardautotunnel.util.extensions.isBatteryOptimizationsDisabled
|
|
||||||
import com.zaneschepke.wireguardautotunnel.util.extensions.isRunningOnTv
|
import com.zaneschepke.wireguardautotunnel.util.extensions.isRunningOnTv
|
||||||
import com.zaneschepke.wireguardautotunnel.util.extensions.openWebUrl
|
import com.zaneschepke.wireguardautotunnel.util.extensions.openWebUrl
|
||||||
import com.zaneschepke.wireguardautotunnel.util.extensions.scaledHeight
|
import com.zaneschepke.wireguardautotunnel.util.extensions.scaledHeight
|
||||||
|
@ -73,30 +67,28 @@ fun MainScreen(viewModel: MainViewModel = hiltViewModel(), uiState: AppUiState)
|
||||||
val snackbar = SnackbarController.current
|
val snackbar = SnackbarController.current
|
||||||
|
|
||||||
var showBottomSheet by remember { mutableStateOf(false) }
|
var showBottomSheet by remember { mutableStateOf(false) }
|
||||||
var showVpnPermissionDialog by remember { mutableStateOf(false) }
|
|
||||||
var isFabVisible by rememberSaveable { mutableStateOf(true) }
|
var isFabVisible by rememberSaveable { mutableStateOf(true) }
|
||||||
var showDeleteTunnelAlertDialog by remember { mutableStateOf(false) }
|
var showDeleteTunnelAlertDialog by remember { mutableStateOf(false) }
|
||||||
var selectedTunnel by remember { mutableStateOf<TunnelConfig?>(null) }
|
var selectedTunnel by remember { mutableStateOf<TunnelConfig?>(null) }
|
||||||
val isRunningOnTv = remember { context.isRunningOnTv() }
|
val isRunningOnTv = remember { context.isRunningOnTv() }
|
||||||
|
|
||||||
|
val startAutoTunnel = withVpnPermission<Unit> { viewModel.onToggleAutoTunnel() }
|
||||||
|
val startTunnel = withVpnPermission<TunnelConfig> {
|
||||||
|
viewModel.onTunnelStart(it, uiState.settings.isKernelEnabled)
|
||||||
|
}
|
||||||
|
val autoTunnelToggleBattery = withIgnoreBatteryOpt(uiState.generalState.isBatteryOptimizationDisableShown) {
|
||||||
|
if (!uiState.generalState.isBatteryOptimizationDisableShown) viewModel.setBatteryOptimizeDisableShown()
|
||||||
|
if (uiState.settings.isKernelEnabled) {
|
||||||
|
viewModel.onToggleAutoTunnel()
|
||||||
|
} else {
|
||||||
|
startAutoTunnel.invoke(Unit)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
val nestedScrollConnection = remember {
|
val nestedScrollConnection = remember {
|
||||||
NestedScrollListener({ isFabVisible = false }, { isFabVisible = true })
|
NestedScrollListener({ isFabVisible = false }, { isFabVisible = true })
|
||||||
}
|
}
|
||||||
|
|
||||||
val vpnActivity =
|
|
||||||
rememberLauncherForActivityResult(
|
|
||||||
ActivityResultContracts.StartActivityForResult(),
|
|
||||||
onResult = {
|
|
||||||
if (it.resultCode != RESULT_OK) showVpnPermissionDialog = true
|
|
||||||
},
|
|
||||||
)
|
|
||||||
val batteryActivity =
|
|
||||||
rememberLauncherForActivityResult(
|
|
||||||
ActivityResultContracts.StartActivityForResult(),
|
|
||||||
) { result: ActivityResult ->
|
|
||||||
viewModel.setBatteryOptimizeDisableShown()
|
|
||||||
}
|
|
||||||
|
|
||||||
val tunnelFileImportResultLauncher = rememberFileImportLauncherForResult(onNoFileExplorer = {
|
val tunnelFileImportResultLauncher = rememberFileImportLauncherForResult(onNoFileExplorer = {
|
||||||
snackbar.showMessage(
|
snackbar.showMessage(
|
||||||
context.getString(R.string.error_no_file_explorer),
|
context.getString(R.string.error_no_file_explorer),
|
||||||
|
@ -112,8 +104,6 @@ fun MainScreen(viewModel: MainViewModel = hiltViewModel(), uiState: AppUiState)
|
||||||
navController.navigate(Route.Scanner)
|
navController.navigate(Route.Scanner)
|
||||||
}
|
}
|
||||||
|
|
||||||
VpnDeniedDialog(showVpnPermissionDialog, onDismiss = { showVpnPermissionDialog = false })
|
|
||||||
|
|
||||||
if (showDeleteTunnelAlertDialog) {
|
if (showDeleteTunnelAlertDialog) {
|
||||||
InfoDialog(
|
InfoDialog(
|
||||||
onDismiss = { showDeleteTunnelAlertDialog = false },
|
onDismiss = { showDeleteTunnelAlertDialog = false },
|
||||||
|
@ -128,35 +118,13 @@ fun MainScreen(viewModel: MainViewModel = hiltViewModel(), uiState: AppUiState)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun requestBatteryOptimizationsDisabled() {
|
|
||||||
val intent =
|
|
||||||
Intent().apply {
|
|
||||||
action = Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS
|
|
||||||
data = Uri.parse("package:${context.packageName}")
|
|
||||||
}
|
|
||||||
batteryActivity.launch(intent)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onAutoTunnelToggle() {
|
|
||||||
if (!uiState.generalState.isBatteryOptimizationDisableShown &&
|
|
||||||
!context.isBatteryOptimizationsDisabled() && !isRunningOnTv
|
|
||||||
) {
|
|
||||||
return requestBatteryOptimizationsDisabled()
|
|
||||||
}
|
|
||||||
val intent = if (!uiState.settings.isKernelEnabled) {
|
|
||||||
VpnService.prepare(context)
|
|
||||||
} else {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
if (intent != null) return vpnActivity.launch(intent)
|
|
||||||
viewModel.onToggleAutoTunnel()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onTunnelToggle(checked: Boolean, tunnel: TunnelConfig) {
|
fun onTunnelToggle(checked: Boolean, tunnel: TunnelConfig) {
|
||||||
val intent = if (uiState.settings.isKernelEnabled) null else VpnService.prepare(context)
|
|
||||||
if (intent != null) return vpnActivity.launch(intent)
|
|
||||||
if (!checked) viewModel.onTunnelStop().also { return }
|
if (!checked) viewModel.onTunnelStop().also { return }
|
||||||
viewModel.onTunnelStart(tunnel, uiState.settings.isKernelEnabled)
|
if (uiState.settings.isKernelEnabled) {
|
||||||
|
viewModel.onTunnelStart(tunnel, uiState.settings.isKernelEnabled)
|
||||||
|
} else {
|
||||||
|
startTunnel.invoke(tunnel)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
|
@ -239,9 +207,9 @@ fun MainScreen(viewModel: MainViewModel = hiltViewModel(), uiState: AppUiState)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
item {
|
item {
|
||||||
AutoTunnelRowItem(uiState, {
|
AutoTunnelRowItem(uiState) {
|
||||||
onAutoTunnelToggle()
|
autoTunnelToggleBattery.invoke()
|
||||||
})
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
items(
|
items(
|
||||||
|
|
|
@ -12,13 +12,13 @@ import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.automirrored.outlined.ViewQuilt
|
import androidx.compose.material.icons.automirrored.outlined.ViewQuilt
|
||||||
import androidx.compose.material.icons.filled.AppShortcut
|
import androidx.compose.material.icons.filled.AppShortcut
|
||||||
import androidx.compose.material.icons.outlined.AdminPanelSettings
|
|
||||||
import androidx.compose.material.icons.outlined.Bolt
|
import androidx.compose.material.icons.outlined.Bolt
|
||||||
import androidx.compose.material.icons.outlined.Code
|
import androidx.compose.material.icons.outlined.Code
|
||||||
import androidx.compose.material.icons.outlined.FolderZip
|
import androidx.compose.material.icons.outlined.FolderZip
|
||||||
import androidx.compose.material.icons.outlined.Notifications
|
import androidx.compose.material.icons.outlined.Notifications
|
||||||
import androidx.compose.material.icons.outlined.Pin
|
import androidx.compose.material.icons.outlined.Pin
|
||||||
import androidx.compose.material.icons.outlined.Restore
|
import androidx.compose.material.icons.outlined.Restore
|
||||||
|
import androidx.compose.material.icons.outlined.VpnKeyOff
|
||||||
import androidx.compose.material.icons.outlined.VpnLock
|
import androidx.compose.material.icons.outlined.VpnLock
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
|
@ -49,7 +49,6 @@ import com.zaneschepke.wireguardautotunnel.ui.screens.settings.components.Forwar
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.theme.topPadding
|
import com.zaneschepke.wireguardautotunnel.ui.theme.topPadding
|
||||||
import com.zaneschepke.wireguardautotunnel.util.extensions.isRunningOnTv
|
import com.zaneschepke.wireguardautotunnel.util.extensions.isRunningOnTv
|
||||||
import com.zaneschepke.wireguardautotunnel.util.extensions.launchNotificationSettings
|
import com.zaneschepke.wireguardautotunnel.util.extensions.launchNotificationSettings
|
||||||
import com.zaneschepke.wireguardautotunnel.util.extensions.launchVpnSettings
|
|
||||||
import com.zaneschepke.wireguardautotunnel.util.extensions.scaledHeight
|
import com.zaneschepke.wireguardautotunnel.util.extensions.scaledHeight
|
||||||
import com.zaneschepke.wireguardautotunnel.util.extensions.scaledWidth
|
import com.zaneschepke.wireguardautotunnel.util.extensions.scaledWidth
|
||||||
import com.zaneschepke.wireguardautotunnel.util.extensions.showToast
|
import com.zaneschepke.wireguardautotunnel.util.extensions.showToast
|
||||||
|
@ -179,18 +178,18 @@ fun SettingsScreen(viewModel: SettingsViewModel = hiltViewModel(), appViewModel:
|
||||||
onClick = { appViewModel.onToggleAlwaysOnVPN() },
|
onClick = { appViewModel.onToggleAlwaysOnVPN() },
|
||||||
),
|
),
|
||||||
SelectionItem(
|
SelectionItem(
|
||||||
Icons.Outlined.AdminPanelSettings,
|
Icons.Outlined.VpnKeyOff,
|
||||||
title = {
|
title = {
|
||||||
Text(
|
Text(
|
||||||
stringResource(R.string.kill_switch),
|
stringResource(R.string.kill_switch_options),
|
||||||
style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface),
|
style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface),
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
onClick = {
|
onClick = {
|
||||||
context.launchVpnSettings()
|
navController.navigate(Route.KillSwitch)
|
||||||
},
|
},
|
||||||
trailing = {
|
trailing = {
|
||||||
ForwardButton { context.launchVpnSettings() }
|
ForwardButton { navController.navigate(Route.KillSwitch) }
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
@ -0,0 +1,142 @@
|
||||||
|
package com.zaneschepke.wireguardautotunnel.ui.screens.settings.killswitch
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.outlined.AdminPanelSettings
|
||||||
|
import androidx.compose.material.icons.outlined.Lan
|
||||||
|
import androidx.compose.material.icons.outlined.VpnKey
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Scaffold
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.zaneschepke.wireguardautotunnel.R
|
||||||
|
import com.zaneschepke.wireguardautotunnel.ui.AppUiState
|
||||||
|
import com.zaneschepke.wireguardautotunnel.ui.AppViewModel
|
||||||
|
import com.zaneschepke.wireguardautotunnel.ui.common.button.ScaledSwitch
|
||||||
|
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SelectionItem
|
||||||
|
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SurfaceSelectionGroupButton
|
||||||
|
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.TopNavBar
|
||||||
|
import com.zaneschepke.wireguardautotunnel.ui.common.permission.vpn.withVpnPermission
|
||||||
|
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.components.ForwardButton
|
||||||
|
import com.zaneschepke.wireguardautotunnel.util.extensions.launchVpnSettings
|
||||||
|
import com.zaneschepke.wireguardautotunnel.util.extensions.scaledHeight
|
||||||
|
import com.zaneschepke.wireguardautotunnel.util.extensions.scaledWidth
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun KillSwitchScreen(uiState: AppUiState, appViewModel: AppViewModel) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
|
||||||
|
val toggleVpnSwitch = withVpnPermission<Boolean> { appViewModel.onToggleVpnKillSwitch(it) }
|
||||||
|
|
||||||
|
fun toggleVpnKillSwitch() {
|
||||||
|
with(uiState.settings) {
|
||||||
|
if (isVpnKillSwitchEnabled) {
|
||||||
|
appViewModel.onToggleVpnKillSwitch(false)
|
||||||
|
} else {
|
||||||
|
toggleVpnSwitch.invoke(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun toggleLanOnKillSwitch() {
|
||||||
|
with(uiState.settings) {
|
||||||
|
appViewModel.onToggleLanOnKillSwitch(!isLanOnKillSwitchEnabled)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Scaffold(
|
||||||
|
topBar = {
|
||||||
|
TopNavBar(stringResource(R.string.kill_switch))
|
||||||
|
},
|
||||||
|
) { padding ->
|
||||||
|
Column(
|
||||||
|
horizontalAlignment = Alignment.Start,
|
||||||
|
verticalArrangement = Arrangement.spacedBy(24.dp.scaledHeight(), Alignment.Top),
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.fillMaxSize().padding(padding)
|
||||||
|
.padding(top = 24.dp.scaledHeight())
|
||||||
|
.padding(horizontal = 24.dp.scaledWidth()),
|
||||||
|
) {
|
||||||
|
SurfaceSelectionGroupButton(
|
||||||
|
listOf(
|
||||||
|
SelectionItem(
|
||||||
|
Icons.Outlined.AdminPanelSettings,
|
||||||
|
title = {
|
||||||
|
Text(
|
||||||
|
stringResource(R.string.native_kill_switch),
|
||||||
|
style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
onClick = { context.launchVpnSettings() },
|
||||||
|
trailing = {
|
||||||
|
ForwardButton { context.launchVpnSettings() }
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
SurfaceSelectionGroupButton(
|
||||||
|
buildList {
|
||||||
|
add(
|
||||||
|
SelectionItem(
|
||||||
|
Icons.Outlined.VpnKey,
|
||||||
|
title = {
|
||||||
|
Text(
|
||||||
|
stringResource(R.string.vpn_kill_switch),
|
||||||
|
style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
onClick = {
|
||||||
|
toggleVpnKillSwitch()
|
||||||
|
},
|
||||||
|
trailing = {
|
||||||
|
ScaledSwitch(
|
||||||
|
uiState.settings.isVpnKillSwitchEnabled,
|
||||||
|
onClick = {
|
||||||
|
toggleVpnKillSwitch()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
if (uiState.settings.isVpnKillSwitchEnabled) {
|
||||||
|
add(
|
||||||
|
SelectionItem(
|
||||||
|
Icons.Outlined.Lan,
|
||||||
|
title = {
|
||||||
|
Text(
|
||||||
|
stringResource(R.string.allow_lan_traffic),
|
||||||
|
style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
onClick = { toggleLanOnKillSwitch() },
|
||||||
|
description = {
|
||||||
|
Text(
|
||||||
|
stringResource(R.string.bypass_lan_for_kill_switch),
|
||||||
|
style = MaterialTheme.typography.bodySmall.copy(MaterialTheme.colorScheme.outline),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
trailing = {
|
||||||
|
ScaledSwitch(
|
||||||
|
uiState.settings.isLanOnKillSwitchEnabled,
|
||||||
|
onClick = {
|
||||||
|
toggleLanOnKillSwitch()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -3,12 +3,14 @@ package com.zaneschepke.wireguardautotunnel.util.extensions
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import com.wireguard.android.util.RootShell
|
import com.wireguard.android.util.RootShell
|
||||||
import com.wireguard.config.Peer
|
import com.wireguard.config.Peer
|
||||||
|
import com.zaneschepke.wireguardautotunnel.service.tunnel.BackendState
|
||||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.HandshakeStatus
|
import com.zaneschepke.wireguardautotunnel.service.tunnel.HandshakeStatus
|
||||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.statistics.TunnelStatistics
|
import com.zaneschepke.wireguardautotunnel.service.tunnel.statistics.TunnelStatistics
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.theme.SilverTree
|
import com.zaneschepke.wireguardautotunnel.ui.theme.SilverTree
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.theme.Straw
|
import com.zaneschepke.wireguardautotunnel.ui.theme.Straw
|
||||||
import com.zaneschepke.wireguardautotunnel.util.Constants
|
import com.zaneschepke.wireguardautotunnel.util.Constants
|
||||||
import com.zaneschepke.wireguardautotunnel.util.NumberUtils
|
import com.zaneschepke.wireguardautotunnel.util.NumberUtils
|
||||||
|
import org.amnezia.awg.backend.Backend
|
||||||
import org.amnezia.awg.config.Config
|
import org.amnezia.awg.config.Config
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import java.net.InetAddress
|
import java.net.InetAddress
|
||||||
|
@ -85,3 +87,11 @@ fun RootShell.getCurrentWifiName(): String? {
|
||||||
this.run(response, "dumpsys wifi | grep -o \"SSID: [^,]*\" | cut -d ' ' -f2- | tr -d '\"'")
|
this.run(response, "dumpsys wifi | grep -o \"SSID: [^,]*\" | cut -d ' ' -f2- | tr -d '\"'")
|
||||||
return response.lastOrNull()
|
return response.lastOrNull()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun Backend.BackendState.asBackendState(): BackendState {
|
||||||
|
return BackendState.valueOf(this.name)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun BackendState.asAmBackendState(): Backend.BackendState {
|
||||||
|
return Backend.BackendState.valueOf(this.name)
|
||||||
|
}
|
||||||
|
|
|
@ -182,4 +182,9 @@
|
||||||
<string name="stop_on_internet_loss">Stop tunnel on internet loss</string>
|
<string name="stop_on_internet_loss">Stop tunnel on internet loss</string>
|
||||||
<string name="ethernet_tunnel">Ethernet tunnel</string>
|
<string name="ethernet_tunnel">Ethernet tunnel</string>
|
||||||
<string name="set_ethernet_tunnel">Set as ethernet tunnel</string>
|
<string name="set_ethernet_tunnel">Set as ethernet tunnel</string>
|
||||||
|
<string name="native_kill_switch">Native kill switch</string>
|
||||||
|
<string name="vpn_kill_switch">VPN kill switch</string>
|
||||||
|
<string name="kill_switch_options">Kill switch options</string>
|
||||||
|
<string name="allow_lan_traffic">Allow LAN traffic</string>
|
||||||
|
<string name="bypass_lan_for_kill_switch">Bypass LAN for kill switch</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
[versions]
|
[versions]
|
||||||
accompanist = "0.36.0"
|
accompanist = "0.36.0"
|
||||||
activityCompose = "1.9.3"
|
activityCompose = "1.9.3"
|
||||||
amneziawgAndroid = "1.2.2"
|
amneziawgAndroid = "1.2.3"
|
||||||
androidx-junit = "1.2.1"
|
androidx-junit = "1.2.1"
|
||||||
appcompat = "1.7.0"
|
appcompat = "1.7.0"
|
||||||
biometricKtx = "1.2.0-alpha05"
|
biometricKtx = "1.2.0-alpha05"
|
||||||
|
@ -10,7 +10,7 @@ coreKtx = "1.15.0"
|
||||||
datastorePreferences = "1.1.1"
|
datastorePreferences = "1.1.1"
|
||||||
desugar_jdk_libs = "2.1.3"
|
desugar_jdk_libs = "2.1.3"
|
||||||
espressoCore = "3.6.1"
|
espressoCore = "3.6.1"
|
||||||
hiltAndroid = "2.52"
|
hiltAndroid = "2.53"
|
||||||
hiltNavigationCompose = "1.2.0"
|
hiltNavigationCompose = "1.2.0"
|
||||||
junit = "4.13.2"
|
junit = "4.13.2"
|
||||||
kotlinx-serialization-json = "1.7.3"
|
kotlinx-serialization-json = "1.7.3"
|
||||||
|
@ -21,9 +21,9 @@ pinLockCompose = "1.0.4"
|
||||||
roomVersion = "2.6.1"
|
roomVersion = "2.6.1"
|
||||||
timber = "5.0.1"
|
timber = "5.0.1"
|
||||||
tunnel = "1.2.1"
|
tunnel = "1.2.1"
|
||||||
androidGradlePlugin = "8.7.2"
|
androidGradlePlugin = "8.8.0-rc01"
|
||||||
kotlin = "2.0.21"
|
kotlin = "2.1.0"
|
||||||
ksp = "2.0.21-1.0.28"
|
ksp = "2.1.0-1.0.29"
|
||||||
composeBom = "2024.11.00"
|
composeBom = "2024.11.00"
|
||||||
compose = "1.7.5"
|
compose = "1.7.5"
|
||||||
zxingAndroidEmbedded = "4.3.0"
|
zxingAndroidEmbedded = "4.3.0"
|
||||||
|
@ -32,7 +32,7 @@ gradlePlugins-grgit = "5.3.0"
|
||||||
|
|
||||||
#plugins
|
#plugins
|
||||||
material = "1.12.0"
|
material = "1.12.0"
|
||||||
gradlePlugins-ktlint="12.1.1"
|
gradlePlugins-ktlint="12.1.2"
|
||||||
|
|
||||||
|
|
||||||
[libraries]
|
[libraries]
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
#Wed Oct 11 22:39:21 EDT 2023
|
#Wed Oct 11 22:39:21 EDT 2023
|
||||||
distributionBase=GRADLE_USER_HOME
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip
|
||||||
distributionSha256Sum=d725d707bfabd4dfdc958c624003b3c80accc03f7037b5122c4b1d0ef15cecab
|
distributionSha256Sum=31c55713e40233a8303827ceb42ca48a47267a0ad4bab9177123121e71524c26
|
||||||
validateDistributionUrl=true
|
validateDistributionUrl=true
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
zipStorePath=wrapper/dists
|
zipStorePath=wrapper/dists
|
||||||
|
|
Loading…
Reference in New Issue