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