more changes

bump versions
This commit is contained in:
Zane Schepke 2024-11-03 01:32:42 -04:00
parent 0784c96011
commit d3ea75869a
71 changed files with 1419 additions and 1512 deletions

View File

@ -0,0 +1,232 @@
{
"formatVersion": 1,
"database": {
"version": 11,
"identityHash": "4c9418386f72dfac5d28ab96c1e5ea0b",
"entities": [
{
"tableName": "Settings",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_tunnel_enabled` INTEGER NOT NULL, `is_tunnel_on_mobile_data_enabled` INTEGER NOT NULL, `trusted_network_ssids` TEXT NOT NULL, `is_always_on_vpn_enabled` INTEGER NOT NULL, `is_tunnel_on_ethernet_enabled` INTEGER NOT NULL, `is_shortcuts_enabled` INTEGER NOT NULL DEFAULT false, `is_tunnel_on_wifi_enabled` INTEGER NOT NULL DEFAULT false, `is_kernel_enabled` INTEGER NOT NULL DEFAULT false, `is_restore_on_boot_enabled` INTEGER NOT NULL DEFAULT false, `is_multi_tunnel_enabled` INTEGER NOT NULL DEFAULT false, `is_ping_enabled` INTEGER NOT NULL DEFAULT false, `is_amnezia_enabled` INTEGER NOT NULL DEFAULT false, `is_wildcards_enabled` INTEGER NOT NULL DEFAULT false, `is_wifi_by_shell_enabled` INTEGER NOT NULL DEFAULT false)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isAutoTunnelEnabled",
"columnName": "is_tunnel_enabled",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isTunnelOnMobileDataEnabled",
"columnName": "is_tunnel_on_mobile_data_enabled",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "trustedNetworkSSIDs",
"columnName": "trusted_network_ssids",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "isAlwaysOnVpnEnabled",
"columnName": "is_always_on_vpn_enabled",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isTunnelOnEthernetEnabled",
"columnName": "is_tunnel_on_ethernet_enabled",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isShortcutsEnabled",
"columnName": "is_shortcuts_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isTunnelOnWifiEnabled",
"columnName": "is_tunnel_on_wifi_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isKernelEnabled",
"columnName": "is_kernel_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isRestoreOnBootEnabled",
"columnName": "is_restore_on_boot_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isMultiTunnelEnabled",
"columnName": "is_multi_tunnel_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isPingEnabled",
"columnName": "is_ping_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isAmneziaEnabled",
"columnName": "is_amnezia_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isWildcardsEnabled",
"columnName": "is_wildcards_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isWifiNameByShellEnabled",
"columnName": "is_wifi_by_shell_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
}
],
"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, '4c9418386f72dfac5d28ab96c1e5ea0b')"
]
}
}

View File

@ -186,10 +186,6 @@
<action android:name="com.htc.intent.action.QUICKBOOT_POWERON" /> <action android:name="com.htc.intent.action.QUICKBOOT_POWERON" />
</intent-filter> </intent-filter>
</receiver> </receiver>
<receiver
android:name=".receiver.BackgroundActionReceiver"
android:enabled="true"
android:exported="false"/>
<receiver <receiver
android:name=".receiver.AppUpdateReceiver" android:name=".receiver.AppUpdateReceiver"
android:exported="false"> android:exported="false">

View File

@ -11,7 +11,7 @@ import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
@Database( @Database(
entities = [Settings::class, TunnelConfig::class], entities = [Settings::class, TunnelConfig::class],
version = 10, version = 11,
autoMigrations = autoMigrations =
[ [
AutoMigration(from = 1, to = 2), AutoMigration(from = 1, to = 2),
@ -36,6 +36,11 @@ import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
AutoMigration(7, 8), AutoMigration(7, 8),
AutoMigration(8, 9), AutoMigration(8, 9),
AutoMigration(9, 10), AutoMigration(9, 10),
AutoMigration(
from = 10,
to = 11,
spec = RemoveTunnelPauseMigration::class,
),
], ],
exportSchema = true, exportSchema = true,
) )
@ -55,3 +60,9 @@ abstract class AppDatabase : RoomDatabase() {
columnName = "is_battery_saver_enabled", columnName = "is_battery_saver_enabled",
) )
class RemoveLegacySettingColumnsMigration : AutoMigrationSpec class RemoveLegacySettingColumnsMigration : AutoMigrationSpec
@DeleteColumn(
tableName = "Settings",
columnName = "is_auto_tunnel_paused",
)
class RemoveTunnelPauseMigration : AutoMigrationSpec

View File

@ -26,7 +26,6 @@ class DataStoreManager(
val currentSSID = stringPreferencesKey("CURRENT_SSID") val currentSSID = stringPreferencesKey("CURRENT_SSID")
val pinLockEnabled = booleanPreferencesKey("PIN_LOCK_ENABLED") val pinLockEnabled = booleanPreferencesKey("PIN_LOCK_ENABLED")
val tunnelStatsExpanded = booleanPreferencesKey("TUNNEL_STATS_EXPANDED") val tunnelStatsExpanded = booleanPreferencesKey("TUNNEL_STATS_EXPANDED")
val wildcardsEnabled = booleanPreferencesKey("WILDCARDS_ENABLED")
val theme = stringPreferencesKey("THEME") val theme = stringPreferencesKey("THEME")
} }

View File

@ -7,14 +7,12 @@ data class GeneralState(
val isBatteryOptimizationDisableShown: Boolean = BATTERY_OPTIMIZATION_DISABLE_SHOWN_DEFAULT, val isBatteryOptimizationDisableShown: Boolean = BATTERY_OPTIMIZATION_DISABLE_SHOWN_DEFAULT,
val isPinLockEnabled: Boolean = PIN_LOCK_ENABLED_DEFAULT, val isPinLockEnabled: Boolean = PIN_LOCK_ENABLED_DEFAULT,
val isTunnelStatsExpanded: Boolean = IS_TUNNEL_STATS_EXPANDED, val isTunnelStatsExpanded: Boolean = IS_TUNNEL_STATS_EXPANDED,
val isWildcardsEnabled: Boolean = IS_WILDCARDS_ENABLED, val theme: Theme = Theme.AUTOMATIC,
val theme: Theme = Theme.AUTOMATIC
) { ) {
companion object { companion object {
const val LOCATION_DISCLOSURE_SHOWN_DEFAULT = false const val LOCATION_DISCLOSURE_SHOWN_DEFAULT = false
const val BATTERY_OPTIMIZATION_DISABLE_SHOWN_DEFAULT = false const val BATTERY_OPTIMIZATION_DISABLE_SHOWN_DEFAULT = false
const val PIN_LOCK_ENABLED_DEFAULT = false const val PIN_LOCK_ENABLED_DEFAULT = false
const val IS_TUNNEL_STATS_EXPANDED = false const val IS_TUNNEL_STATS_EXPANDED = false
const val IS_WILDCARDS_ENABLED = false
} }
} }

View File

@ -40,11 +40,6 @@ data class Settings(
defaultValue = "false", defaultValue = "false",
) )
val isMultiTunnelEnabled: Boolean = false, val isMultiTunnelEnabled: Boolean = false,
@ColumnInfo(
name = "is_auto_tunnel_paused",
defaultValue = "false",
)
val isAutoTunnelPaused: Boolean = false,
@ColumnInfo( @ColumnInfo(
name = "is_ping_enabled", name = "is_ping_enabled",
defaultValue = "false", defaultValue = "false",
@ -55,4 +50,14 @@ data class Settings(
defaultValue = "false", defaultValue = "false",
) )
val isAmneziaEnabled: Boolean = false, val isAmneziaEnabled: Boolean = false,
@ColumnInfo(
name = "is_wildcards_enabled",
defaultValue = "false",
)
val isWildcardsEnabled: Boolean = false,
@ColumnInfo(
name = "is_wifi_by_shell_enabled",
defaultValue = "false",
)
val isWifiNameByShellEnabled: Boolean = false,
) )

View File

@ -13,10 +13,6 @@ interface AppStateRepository {
suspend fun setPinLockEnabled(enabled: Boolean) suspend fun setPinLockEnabled(enabled: Boolean)
suspend fun isWildcardsEnabled(): Boolean
suspend fun setWildcardsEnabled(enabled: Boolean)
suspend fun isBatteryOptimizationDisableShown(): Boolean suspend fun isBatteryOptimizationDisableShown(): Boolean
suspend fun setBatteryOptimizationDisableShown(shown: Boolean) suspend fun setBatteryOptimizationDisableShown(shown: Boolean)
@ -31,7 +27,7 @@ interface AppStateRepository {
suspend fun setTheme(theme: Theme) suspend fun setTheme(theme: Theme)
suspend fun getTheme() : Theme suspend fun getTheme(): Theme
val generalStateFlow: Flow<GeneralState> val generalStateFlow: Flow<GeneralState>
} }

View File

@ -29,14 +29,6 @@ class DataStoreAppStateRepository(
dataStoreManager.saveToDataStore(DataStoreManager.pinLockEnabled, enabled) dataStoreManager.saveToDataStore(DataStoreManager.pinLockEnabled, enabled)
} }
override suspend fun isWildcardsEnabled(): Boolean {
return dataStoreManager.getFromStore(DataStoreManager.wildcardsEnabled) ?: GeneralState.IS_WILDCARDS_ENABLED
}
override suspend fun setWildcardsEnabled(enabled: Boolean) {
dataStoreManager.saveToDataStore(DataStoreManager.wildcardsEnabled, enabled)
}
override suspend fun isBatteryOptimizationDisableShown(): Boolean { override suspend fun isBatteryOptimizationDisableShown(): Boolean {
return dataStoreManager.getFromStore(DataStoreManager.batteryDisableShown) return dataStoreManager.getFromStore(DataStoreManager.batteryDisableShown)
?: GeneralState.BATTERY_OPTIMIZATION_DISABLE_SHOWN_DEFAULT ?: GeneralState.BATTERY_OPTIMIZATION_DISABLE_SHOWN_DEFAULT
@ -71,7 +63,7 @@ class DataStoreAppStateRepository(
return dataStoreManager.getFromStore(DataStoreManager.theme)?.let { return dataStoreManager.getFromStore(DataStoreManager.theme)?.let {
try { try {
Theme.valueOf(it) Theme.valueOf(it)
} catch (_ : IllegalArgumentException) { } catch (_: IllegalArgumentException) {
Theme.AUTOMATIC Theme.AUTOMATIC
} }
} ?: Theme.AUTOMATIC } ?: Theme.AUTOMATIC
@ -92,8 +84,7 @@ class DataStoreAppStateRepository(
pref[DataStoreManager.pinLockEnabled] pref[DataStoreManager.pinLockEnabled]
?: GeneralState.PIN_LOCK_ENABLED_DEFAULT, ?: GeneralState.PIN_LOCK_ENABLED_DEFAULT,
isTunnelStatsExpanded = pref[DataStoreManager.tunnelStatsExpanded] ?: GeneralState.IS_TUNNEL_STATS_EXPANDED, isTunnelStatsExpanded = pref[DataStoreManager.tunnelStatsExpanded] ?: GeneralState.IS_TUNNEL_STATS_EXPANDED,
isWildcardsEnabled = pref[DataStoreManager.wildcardsEnabled] ?: GeneralState.IS_WILDCARDS_ENABLED, theme = getTheme(),
theme = getTheme()
) )
} catch (e: IllegalArgumentException) { } catch (e: IllegalArgumentException) {
Timber.e(e) Timber.e(e)

View File

@ -1,11 +1,9 @@
package com.zaneschepke.wireguardautotunnel.data.repository package com.zaneschepke.wireguardautotunnel.data.repository
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
import com.zaneschepke.wireguardautotunnel.data.TunnelConfigDao import com.zaneschepke.wireguardautotunnel.data.TunnelConfigDao
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
import com.zaneschepke.wireguardautotunnel.module.IoDispatcher import com.zaneschepke.wireguardautotunnel.module.IoDispatcher
import com.zaneschepke.wireguardautotunnel.util.extensions.TunnelConfigs import com.zaneschepke.wireguardautotunnel.util.extensions.TunnelConfigs
import com.zaneschepke.wireguardautotunnel.util.extensions.requestTunnelTileServiceStateUpdate
import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
@ -26,8 +24,6 @@ class RoomTunnelConfigRepository(
override suspend fun save(tunnelConfig: TunnelConfig) { override suspend fun save(tunnelConfig: TunnelConfig) {
withContext(ioDispatcher) { withContext(ioDispatcher) {
tunnelConfigDao.save(tunnelConfig) tunnelConfigDao.save(tunnelConfig)
}.also {
WireGuardAutoTunnel.instance.requestTunnelTileServiceStateUpdate()
} }
} }
@ -60,8 +56,6 @@ class RoomTunnelConfigRepository(
override suspend fun delete(tunnelConfig: TunnelConfig) { override suspend fun delete(tunnelConfig: TunnelConfig) {
withContext(ioDispatcher) { withContext(ioDispatcher) {
tunnelConfigDao.delete(tunnelConfig) tunnelConfigDao.delete(tunnelConfig)
}.also {
WireGuardAutoTunnel.instance.requestTunnelTileServiceStateUpdate()
} }
} }

View File

@ -9,6 +9,7 @@ import com.wireguard.android.util.RootShell
import com.wireguard.android.util.ToolsInstaller import com.wireguard.android.util.ToolsInstaller
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
import com.zaneschepke.wireguardautotunnel.data.repository.TunnelConfigRepository import com.zaneschepke.wireguardautotunnel.data.repository.TunnelConfigRepository
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
@ -65,6 +66,7 @@ class TunnelModule {
tunnelConfigRepository: TunnelConfigRepository, tunnelConfigRepository: TunnelConfigRepository,
@ApplicationScope applicationScope: CoroutineScope, @ApplicationScope applicationScope: CoroutineScope,
@IoDispatcher ioDispatcher: CoroutineDispatcher, @IoDispatcher ioDispatcher: CoroutineDispatcher,
serviceManager: ServiceManager,
): TunnelService { ): TunnelService {
return WireGuardTunnel( return WireGuardTunnel(
amneziaBackend, amneziaBackend,
@ -73,6 +75,13 @@ class TunnelModule {
appDataRepository, appDataRepository,
applicationScope, applicationScope,
ioDispatcher, ioDispatcher,
serviceManager,
) )
} }
@Singleton
@Provides
fun provideServiceManager(@ApplicationContext context: Context): ServiceManager {
return ServiceManager(context)
}
} }

View File

@ -7,12 +7,12 @@ 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 com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelService import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelService
import com.zaneschepke.wireguardautotunnel.util.extensions.startTunnelBackground
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
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Provider
@AndroidEntryPoint @AndroidEntryPoint
class AppUpdateReceiver : BroadcastReceiver() { class AppUpdateReceiver : BroadcastReceiver() {
@ -25,7 +25,10 @@ class AppUpdateReceiver : BroadcastReceiver() {
lateinit var appDataRepository: AppDataRepository lateinit var appDataRepository: AppDataRepository
@Inject @Inject
lateinit var tunnelService: TunnelService lateinit var tunnelService: Provider<TunnelService>
@Inject
lateinit var serviceManager: ServiceManager
override fun onReceive(context: Context, intent: Intent) { override fun onReceive(context: Context, intent: Intent) {
if (intent.action != Intent.ACTION_MY_PACKAGE_REPLACED) return if (intent.action != Intent.ACTION_MY_PACKAGE_REPLACED) return
@ -33,11 +36,11 @@ 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.startAutoTunnel(true)
} }
if (!settings.isAutoTunnelEnabled || settings.isAutoTunnelPaused) { if (!settings.isAutoTunnelEnabled) {
val tunnels = appDataRepository.tunnels.getAll().filter { it.isActive } val tunnels = appDataRepository.tunnels.getAll().filter { it.isActive }
if (tunnels.isNotEmpty()) context.startTunnelBackground(tunnels.first().id) if (tunnels.isNotEmpty()) tunnelService.get().startTunnel(tunnels.first(), true)
} }
} }
} }

View File

@ -1,61 +0,0 @@
package com.zaneschepke.wireguardautotunnel.receiver
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import com.zaneschepke.wireguardautotunnel.data.repository.TunnelConfigRepository
import com.zaneschepke.wireguardautotunnel.module.ApplicationScope
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelService
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject
import javax.inject.Provider
@AndroidEntryPoint
class BackgroundActionReceiver : BroadcastReceiver() {
@Inject
@ApplicationScope
lateinit var applicationScope: CoroutineScope
@Inject
lateinit var tunnelService: Provider<TunnelService>
@Inject
lateinit var tunnelConfigRepository: TunnelConfigRepository
override fun onReceive(context: Context, intent: Intent) {
val id = intent.getIntExtra(TUNNEL_ID_EXTRA_KEY, 0)
if (id == 0) return
when (intent.action) {
ACTION_CONNECT -> {
Timber.d("Connect actions")
applicationScope.launch {
val tunnel = tunnelConfigRepository.getById(id)
tunnel?.let {
ServiceManager.startTunnelBackgroundService(context)
tunnelService.get().startTunnel(it)
}
}
}
ACTION_DISCONNECT -> {
applicationScope.launch {
val tunnel = tunnelConfigRepository.getById(id)
tunnel?.let {
ServiceManager.stopTunnelBackgroundService(context)
tunnelService.get().stopTunnel(it)
}
}
}
}
}
companion object {
const val ACTION_CONNECT = "ACTION_CONNECT"
const val ACTION_DISCONNECT = "ACTION_DISCONNECT"
const val TUNNEL_ID_EXTRA_KEY = "tunnelId"
}
}

View File

@ -8,7 +8,6 @@ import com.zaneschepke.wireguardautotunnel.module.ApplicationScope
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.service.tunnel.TunnelService
import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelState import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelState
import com.zaneschepke.wireguardautotunnel.util.extensions.startTunnelBackground
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
@ -28,6 +27,9 @@ class BootReceiver : BroadcastReceiver() {
@ApplicationScope @ApplicationScope
lateinit var applicationScope: CoroutineScope lateinit var applicationScope: CoroutineScope
@Inject
lateinit var serviceManager: ServiceManager
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 {
@ -37,11 +39,11 @@ class BootReceiver : BroadcastReceiver() {
val tunState = tunnelService.get().vpnState.value.status val tunState = tunnelService.get().vpnState.value.status
if (activeTunnels.isNotEmpty() && tunState != TunnelState.UP) { if (activeTunnels.isNotEmpty() && tunState != TunnelState.UP) {
Timber.i("Starting previously active tunnel") Timber.i("Starting previously active tunnel")
context.startTunnelBackground(activeTunnels.first().id) tunnelService.get().startTunnel(activeTunnels.first(), true)
} }
if (isAutoTunnelEnabled) { if (isAutoTunnelEnabled) {
Timber.i("Starting watcher service from boot") Timber.i("Starting watcher service from boot")
ServiceManager.startWatcherServiceForeground(context) serviceManager.startAutoTunnel(true)
} }
} }
} }

View File

@ -26,10 +26,11 @@ 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.cancelWithMessage
import com.zaneschepke.wireguardautotunnel.util.extensions.getCurrentWifiName import com.zaneschepke.wireguardautotunnel.util.extensions.getCurrentWifiName
import com.zaneschepke.wireguardautotunnel.util.extensions.isMatchingToWildcardList import com.zaneschepke.wireguardautotunnel.util.extensions.isDown
import com.zaneschepke.wireguardautotunnel.util.extensions.isReachable import com.zaneschepke.wireguardautotunnel.util.extensions.isReachable
import com.zaneschepke.wireguardautotunnel.util.extensions.onNotRunning import com.zaneschepke.wireguardautotunnel.util.extensions.onNotRunning
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
@ -74,6 +75,9 @@ class AutoTunnelService : LifecycleService() {
@IoDispatcher @IoDispatcher
lateinit var ioDispatcher: CoroutineDispatcher lateinit var ioDispatcher: CoroutineDispatcher
@Inject
lateinit var serviceManager: ServiceManager
@Inject @Inject
@MainImmediateDispatcher @MainImmediateDispatcher
lateinit var mainImmediateDispatcher: CoroutineDispatcher lateinit var mainImmediateDispatcher: CoroutineDispatcher
@ -88,14 +92,11 @@ class AutoTunnelService : LifecycleService() {
private var pingJob: Job? = null private var pingJob: Job? = null
private var networkEventJob: Job? = null private var networkEventJob: Job? = null
@get:Synchronized @set:Synchronized
private var running: Boolean = false
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
lifecycleScope.launch(mainImmediateDispatcher) { lifecycleScope.launch(mainImmediateDispatcher) {
kotlin.runCatching { kotlin.runCatching {
launchNotification() launchWatcherNotification()
}.onFailure { }.onFailure {
Timber.e(it) Timber.e(it)
} }
@ -110,32 +111,14 @@ class AutoTunnelService : LifecycleService() {
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
Timber.d("onStartCommand executed with startId: $startId") Timber.d("onStartCommand executed with startId: $startId")
if (intent != null) { serviceManager.autoTunnelService.complete(this)
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) return super.onStartCommand(intent, flags, startId)
} }
private suspend fun launchNotification() { fun start() {
if (appDataRepository.settings.getSettings().isAutoTunnelPaused) {
launchWatcherPausedNotification()
} else {
launchWatcherNotification()
}
}
private fun startService() {
if (running) return
running = true
kotlin.runCatching { kotlin.runCatching {
lifecycleScope.launch(mainImmediateDispatcher) { lifecycleScope.launch(mainImmediateDispatcher) {
launchNotification() launchWatcherNotification()
initWakeLock() initWakeLock()
} }
startSettingsJob() startSettingsJob()
@ -145,7 +128,7 @@ class AutoTunnelService : LifecycleService() {
} }
} }
private fun stopService() { fun stop() {
wakeLock?.let { wakeLock?.let {
if (it.isHeld) { if (it.isHeld) {
it.release() it.release()
@ -157,6 +140,7 @@ class AutoTunnelService : LifecycleService() {
override fun onDestroy() { override fun onDestroy() {
cancelAndResetNetworkJobs() cancelAndResetNetworkJobs()
cancelAndResetPingJob() cancelAndResetPingJob()
serviceManager.autoTunnelService = CompletableDeferred()
super.onDestroy() super.onDestroy()
} }
@ -176,10 +160,6 @@ class AutoTunnelService : LifecycleService() {
) )
} }
private fun launchWatcherPausedNotification() {
launchWatcherNotification(getString(R.string.watcher_notification_text_paused))
}
private fun initWakeLock() { private fun initWakeLock() {
wakeLock = wakeLock =
(getSystemService(Context.POWER_SERVICE) as PowerManager).run { (getSystemService(Context.POWER_SERVICE) as PowerManager).run {
@ -265,8 +245,7 @@ class AutoTunnelService : LifecycleService() {
runCatching { runCatching {
do { do {
val vpnState = tunnelService.get().vpnState.value val vpnState = tunnelService.get().vpnState.value
val settings = appDataRepository.settings.getSettings() if (vpnState.status == TunnelState.UP) {
if (vpnState.status == TunnelState.UP && !settings.isAutoTunnelPaused) {
if (vpnState.tunnelConfig != null) { if (vpnState.tunnelConfig != null) {
val config = TunnelConfig.configFromWgQuick(vpnState.tunnelConfig.wgQuick) val config = TunnelConfig.configFromWgQuick(vpnState.tunnelConfig.wgQuick)
val results = if (vpnState.tunnelConfig.pingIp != null) { val results = if (vpnState.tunnelConfig.pingIp != null) {
@ -296,17 +275,6 @@ class AutoTunnelService : LifecycleService() {
} }
} }
private fun onAutoTunnelPause(paused: Boolean) {
if (autoTunnelStateFlow.value.settings.isAutoTunnelPaused
!= paused
) {
when (paused) {
true -> launchWatcherPausedNotification()
false -> launchWatcherNotification()
}
}
}
private suspend fun watchForSettingsChanges() { private suspend fun watchForSettingsChanges() {
Timber.i("Starting settings watcher") Timber.i("Starting settings watcher")
withContext(ioDispatcher) { withContext(ioDispatcher) {
@ -321,7 +289,7 @@ class AutoTunnelService : LifecycleService() {
tunnels = tunnels, tunnels = tunnels,
) )
}.collect { }.collect {
onAutoTunnelPause(it.settings.isAutoTunnelPaused) Timber.d("got new settings: ${it.settings}")
manageJobsBySettings(it.settings) manageJobsBySettings(it.settings)
autoTunnelStateFlow.emit(it) autoTunnelStateFlow.emit(it)
} }
@ -331,7 +299,12 @@ class AutoTunnelService : LifecycleService() {
private suspend fun watchForVpnStateChanges() { private suspend fun watchForVpnStateChanges() {
Timber.i("Starting vpn state watcher") Timber.i("Starting vpn state watcher")
withContext(ioDispatcher) { withContext(ioDispatcher) {
tunnelService.get().vpnState.collect { state -> tunnelService.get().vpnState.distinctUntilChanged { old, new ->
old.tunnelConfig == new.tunnelConfig && old.status == new.status
}.collect { state ->
autoTunnelStateFlow.update {
it.copy(vpnState = state)
}
state.tunnelConfig?.let { state.tunnelConfig?.let {
val settings = appDataRepository.settings.getSettings() val settings = appDataRepository.settings.getSettings()
if (it.isPingEnabled && !settings.isPingEnabled) { if (it.isPingEnabled && !settings.isPingEnabled) {
@ -475,9 +448,8 @@ class AutoTunnelService : LifecycleService() {
private suspend fun getWifiSSID(networkCapabilities: NetworkCapabilities): String? { private suspend fun getWifiSSID(networkCapabilities: NetworkCapabilities): String? {
return withContext(ioDispatcher) { return withContext(ioDispatcher) {
try { with(autoTunnelStateFlow.value.settings) {
rootShell.get().getCurrentWifiName() if (isWifiNameByShellEnabled) return@withContext rootShell.get().getCurrentWifiName()
} catch (_: Exception) {
wifiService.getNetworkName(networkCapabilities) wifiService.getNetworkName(networkCapabilities)
} }
} }
@ -487,100 +459,95 @@ class AutoTunnelService : LifecycleService() {
return appDataRepository.tunnels.findByMobileDataTunnel().firstOrNull() return appDataRepository.tunnels.findByMobileDataTunnel().firstOrNull()
} }
private fun isTunnelDown(): Boolean {
return tunnelService.get().vpnState.value.status == TunnelState.DOWN
}
private suspend fun handleNetworkEventChanges() { private suspend fun handleNetworkEventChanges() {
withContext(ioDispatcher) { withContext(ioDispatcher) {
Timber.i("Starting network event watcher") Timber.i("Starting network event watcher")
autoTunnelStateFlow.collectLatest { watcherState -> autoTunnelStateFlow.collectLatest { watcherState ->
val autoTunnel = "Auto-tunnel watcher" val autoTunnel = "Auto-tunnel watcher"
if (!watcherState.settings.isAutoTunnelPaused) { Timber.d("New watcher state!")
// delay for rapid network state changes and then collect latest // delay for rapid network state changes and then collect latest
delay(Constants.WATCHER_COLLECTION_DELAY) delay(Constants.WATCHER_COLLECTION_DELAY)
val activeTunnel = tunnelService.get().vpnState.value.tunnelConfig val activeTunnel = watcherState.vpnState.tunnelConfig
val defaultTunnel = appDataRepository.getPrimaryOrFirstTunnel() val defaultTunnel = appDataRepository.getPrimaryOrFirstTunnel()
when { when {
watcherState.isEthernetConditionMet() -> { watcherState.isEthernetConditionMet() -> {
Timber.i("$autoTunnel - tunnel on on ethernet condition met") Timber.i("$autoTunnel - tunnel on on ethernet condition met")
if (isTunnelDown()) { if (watcherState.vpnState.isDown()) {
defaultTunnel?.let { defaultTunnel?.let {
tunnelService.get().startTunnel(it)
}
}
}
watcherState.isMobileDataConditionMet() -> {
Timber.i("$autoTunnel - tunnel on mobile data condition met")
val mobileDataTunnel = getMobileDataTunnel()
val tunnel =
mobileDataTunnel ?: defaultTunnel
if (watcherState.vpnState.isDown() || activeTunnel?.isMobileDataTunnel == false) {
tunnel?.let {
tunnelService.get().startTunnel(it)
}
}
}
watcherState.isTunnelOffOnMobileDataConditionMet() -> {
Timber.i("$autoTunnel - tunnel off on mobile data met, turning vpn off")
if (!watcherState.vpnState.isDown()) {
activeTunnel?.let {
tunnelService.get().stopTunnel(it)
}
}
}
watcherState.isUntrustedWifiConditionMet() -> {
Timber.i("Untrusted wifi condition met")
if (activeTunnel == null || watcherState.isCurrentSSIDActiveTunnelNetwork() == false ||
watcherState.vpnState.isDown()
) {
Timber.i(
"$autoTunnel - tunnel on ssid not associated with current tunnel condition met",
)
watcherState.getTunnelWithMatchingTunnelNetwork()?.let {
Timber.i("Found tunnel associated with this SSID, bringing tunnel up: ${it.name}")
if (watcherState.vpnState.isDown() || activeTunnel?.id != it.id) {
tunnelService.get().startTunnel(it) tunnelService.get().startTunnel(it)
} }
} } ?: suspend {
} Timber.i("No tunnel associated with this SSID, using defaults")
val default = appDataRepository.getPrimaryOrFirstTunnel()
watcherState.isMobileDataConditionMet() -> { if (default?.name != tunnelService.get().name || watcherState.vpnState.isDown()) {
Timber.i("$autoTunnel - tunnel on mobile data condition met") default?.let {
val mobileDataTunnel = getMobileDataTunnel()
val tunnel =
mobileDataTunnel ?: defaultTunnel
if (isTunnelDown() || activeTunnel?.isMobileDataTunnel == false) {
tunnel?.let {
tunnelService.get().startTunnel(it)
}
}
}
watcherState.isTunnelOffOnMobileDataConditionMet() -> {
Timber.i("$autoTunnel - tunnel off on mobile data met, turning vpn off")
if (!isTunnelDown()) {
activeTunnel?.let {
tunnelService.get().stopTunnel(it)
}
}
}
watcherState.isUntrustedWifiConditionMet() -> {
Timber.i("Untrusted wifi condition met")
if (activeTunnel?.tunnelNetworks?.isMatchingToWildcardList(watcherState.currentNetworkSSID) == false ||
activeTunnel == null || isTunnelDown()
) {
Timber.i(
"$autoTunnel - tunnel on ssid not associated with current tunnel condition met",
)
watcherState.tunnels.firstOrNull { it.tunnelNetworks.isMatchingToWildcardList(watcherState.currentNetworkSSID) }?.let {
Timber.i("Found tunnel associated with this SSID, bringing tunnel up: ${it.name}")
if (isTunnelDown() || activeTunnel?.id != it.id) {
tunnelService.get().startTunnel(it) tunnelService.get().startTunnel(it)
} }
} ?: suspend { }
Timber.i("No tunnel associated with this SSID, using defaults") }.invoke()
val default = appDataRepository.getPrimaryOrFirstTunnel()
if (default?.name != tunnelService.get().name || isTunnelDown()) {
default?.let {
tunnelService.get().startTunnel(it)
}
}
}.invoke()
}
} }
}
watcherState.isTrustedWifiConditionMet() -> { watcherState.isTrustedWifiConditionMet() -> {
Timber.i( Timber.i(
"$autoTunnel - tunnel off on trusted wifi condition met, turning vpn off", "$autoTunnel - tunnel off on trusted wifi condition met, turning vpn off",
) )
if (!isTunnelDown()) activeTunnel?.let { tunnelService.get().stopTunnel(it) } if (!watcherState.vpnState.isDown()) activeTunnel?.let { tunnelService.get().stopTunnel(it) }
} }
watcherState.isTunnelOffOnWifiConditionMet() -> { watcherState.isTunnelOffOnWifiConditionMet() -> {
Timber.i( Timber.i(
"$autoTunnel - tunnel off on wifi condition met, turning vpn off", "$autoTunnel - tunnel off on wifi condition met, turning vpn off",
) )
if (!isTunnelDown()) activeTunnel?.let { tunnelService.get().stopTunnel(it) } if (!watcherState.vpnState.isDown()) activeTunnel?.let { tunnelService.get().stopTunnel(it) }
} }
watcherState.isTunnelOffOnNoConnectivityMet() -> { watcherState.isTunnelOffOnNoConnectivityMet() -> {
Timber.i( Timber.i(
"$autoTunnel - tunnel off on no connectivity met, turning vpn off", "$autoTunnel - tunnel off on no connectivity met, turning vpn off",
) )
if (!isTunnelDown()) activeTunnel?.let { tunnelService.get().stopTunnel(it) } if (!watcherState.vpnState.isDown()) activeTunnel?.let { tunnelService.get().stopTunnel(it) }
} }
else -> { else -> {
Timber.i("$autoTunnel - no condition met") Timber.i("$autoTunnel - no condition met")
}
} }
} }
} }

View File

@ -1,10 +1,13 @@
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.data.domain.TunnelConfig
import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnState
import com.zaneschepke.wireguardautotunnel.util.extensions.TunnelConfigs import com.zaneschepke.wireguardautotunnel.util.extensions.TunnelConfigs
import com.zaneschepke.wireguardautotunnel.util.extensions.isMatchingToWildcardList import com.zaneschepke.wireguardautotunnel.util.extensions.isMatchingToWildcardList
data class AutoTunnelState( data class AutoTunnelState(
val vpnState: VpnState = VpnState(),
val isWifiConnected: Boolean = false, val isWifiConnected: Boolean = false,
val isEthernetConnected: Boolean = false, val isEthernetConnected: Boolean = false,
val isMobileDataConnected: Boolean = false, val isMobileDataConnected: Boolean = false,
@ -41,7 +44,7 @@ data class AutoTunnelState(
return ( return (
!isEthernetConnected && !isEthernetConnected &&
isWifiConnected && isWifiConnected &&
!settings.trustedNetworkSSIDs.isMatchingToWildcardList(currentNetworkSSID) && !isCurrentSSIDTrusted() &&
settings.isTunnelOnWifiEnabled settings.isTunnelOnWifiEnabled
) )
} }
@ -51,7 +54,7 @@ data class AutoTunnelState(
!isEthernetConnected && !isEthernetConnected &&
( (
isWifiConnected && isWifiConnected &&
settings.trustedNetworkSSIDs.isMatchingToWildcardList(currentNetworkSSID) isCurrentSSIDTrusted()
) )
) )
} }
@ -73,4 +76,32 @@ data class AutoTunnelState(
!isMobileDataConnected !isMobileDataConnected
) )
} }
fun isCurrentSSIDTrusted(): Boolean {
return if (settings.isWildcardsEnabled) {
settings.trustedNetworkSSIDs.isMatchingToWildcardList(currentNetworkSSID)
} else {
settings.trustedNetworkSSIDs.contains(currentNetworkSSID)
}
}
fun isCurrentSSIDActiveTunnelNetwork(): Boolean {
val currentTunnelNetworks = vpnState.tunnelConfig?.tunnelNetworks
return (
if (settings.isWildcardsEnabled) {
currentTunnelNetworks?.isMatchingToWildcardList(currentNetworkSSID)
} else {
currentTunnelNetworks?.contains(currentNetworkSSID)
}
) == true
}
fun getTunnelWithMatchingTunnelNetwork(): TunnelConfig? {
return tunnels.firstOrNull {
if (settings.isWildcardsEnabled) {
it.tunnelNetworks.isMatchingToWildcardList(currentNetworkSSID)
} else {
it.tunnelNetworks.contains(currentNetworkSSID)
}
}
}
} }

View File

@ -3,69 +3,81 @@ 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 com.zaneschepke.wireguardautotunnel.util.SingletonHolder
import com.zaneschepke.wireguardautotunnel.util.extensions.requestTunnelTileServiceStateUpdate
import jakarta.inject.Inject
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import timber.log.Timber import timber.log.Timber
object ServiceManager { @OptIn(ExperimentalCoroutinesApi::class)
private fun <T : Service> actionOnService(action: Action, context: Context, cls: Class<T>, extras: Map<String, Int>? = null) { class ServiceManager
if (VpnService.prepare(context) != null) return @Inject constructor(private val context: Context) {
val intent =
Intent(context, cls).also {
it.action = action.name
extras?.forEach { (k, v) -> it.putExtra(k, v) }
}
intent.component?.javaClass
try {
when (action) {
Action.START_FOREGROUND, Action.STOP_FOREGROUND ->
context.startForegroundService(
intent,
)
Action.START, Action.STOP -> context.startService(intent) private val _autoTunnelActive = MutableStateFlow(false)
}
} catch (e: Exception) { val autoTunnelActive = _autoTunnelActive.asStateFlow()
Timber.e(e.message)
var autoTunnelService = CompletableDeferred<AutoTunnelService>()
var backgroundService = CompletableDeferred<TunnelBackgroundService>()
companion object : SingletonHolder<ServiceManager, Context>(::ServiceManager)
private fun <T : Service> startService(cls: Class<T>, background: Boolean) {
val intent = Intent(context, cls)
if (background) {
context.startForegroundService(intent)
} else {
context.startService(intent)
} }
} }
fun startWatcherServiceForeground(context: Context) { suspend fun startAutoTunnel(background: Boolean) {
actionOnService( if (autoTunnelService.isCompleted) return _autoTunnelActive.update { true }
Action.START_FOREGROUND, kotlin.runCatching {
context, startService(AutoTunnelService::class.java, background)
AutoTunnelService::class.java, autoTunnelService.await()
) autoTunnelService.getCompleted().start()
_autoTunnelActive.update { true }
}.onFailure {
Timber.e(it)
}
} }
fun startWatcherService(context: Context) { suspend fun startBackgroundService() {
actionOnService( if (backgroundService.isCompleted) return
Action.START, kotlin.runCatching {
context, startService(TunnelBackgroundService::class.java, true)
AutoTunnelService::class.java, backgroundService.await()
) backgroundService.getCompleted().start()
}.onFailure {
Timber.e(it)
}
} }
fun stopWatcherService(context: Context) { fun stopBackgroundService() {
actionOnService( if (!backgroundService.isCompleted) return
Action.STOP, runCatching {
context, backgroundService.getCompleted().stop()
AutoTunnelService::class.java, }.onFailure {
) Timber.e(it)
}
} }
fun startTunnelBackgroundService(context: Context) { fun stopAutoTunnel() {
actionOnService( if (!autoTunnelService.isCompleted) return
Action.START_FOREGROUND, runCatching {
context, autoTunnelService.getCompleted().stop()
TunnelBackgroundService::class.java, _autoTunnelActive.update { false }
) }.onFailure {
Timber.e(it)
}
} }
fun stopTunnelBackgroundService(context: Context) { fun requestTunnelTileUpdate() {
actionOnService( context.requestTunnelTileServiceStateUpdate()
Action.STOP,
context,
TunnelBackgroundService::class.java,
)
} }
} }

View File

@ -7,6 +7,7 @@ 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 kotlinx.coroutines.CompletableDeferred
import javax.inject.Inject import javax.inject.Inject
@AndroidEntryPoint @AndroidEntryPoint
@ -15,6 +16,9 @@ class TunnelBackgroundService : LifecycleService() {
@Inject @Inject
lateinit var notificationService: NotificationService lateinit var notificationService: NotificationService
@Inject
lateinit var serviceManager: ServiceManager
private val foregroundId = 123 private val foregroundId = 123
override fun onCreate() { override fun onCreate() {
@ -29,27 +33,24 @@ class TunnelBackgroundService : LifecycleService() {
} }
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
if (intent != null) { serviceManager.backgroundService.complete(this)
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) return super.onStartCommand(intent, flags, startId)
} }
private fun startService() { fun start() {
startForeground(foregroundId, createNotification()) startForeground(foregroundId, createNotification())
} }
private fun stopService() { fun stop() {
stopForeground(STOP_FOREGROUND_REMOVE) stopForeground(STOP_FOREGROUND_REMOVE)
stopSelf() stopSelf()
} }
override fun onDestroy() {
serviceManager.backgroundService = CompletableDeferred()
super.onDestroy()
}
private fun createNotification(): Notification { private fun createNotification(): Notification {
return notificationService.createNotification( return notificationService.createNotification(
getString(R.string.vpn_channel_id), getString(R.string.vpn_channel_id),

View File

@ -6,9 +6,8 @@ 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.Action import com.zaneschepke.wireguardautotunnel.service.foreground.Action
import com.zaneschepke.wireguardautotunnel.service.foreground.AutoTunnelService import com.zaneschepke.wireguardautotunnel.service.foreground.AutoTunnelService
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.util.extensions.startTunnelBackground
import com.zaneschepke.wireguardautotunnel.util.extensions.stopTunnelBackground
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,6 +23,9 @@ class ShortcutsActivity : ComponentActivity() {
@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
@ -44,26 +46,16 @@ class ShortcutsActivity : ComponentActivity() {
Timber.d("Shortcut action on name: ${tunnelConfig?.name}") Timber.d("Shortcut action on name: ${tunnelConfig?.name}")
tunnelConfig?.let { tunnelConfig?.let {
when (intent.action) { when (intent.action) {
Action.START.name -> this@ShortcutsActivity.startTunnelBackground(it.id) Action.START.name -> tunnelService.get().startTunnel(it, true)
Action.STOP.name -> this@ShortcutsActivity.stopTunnelBackground(it.id) Action.STOP.name -> tunnelService.get().stopTunnel(it)
else -> Unit else -> Unit
} }
} }
} }
AutoTunnelService::class.java.simpleName, LEGACY_AUTO_TUNNEL_SERVICE_NAME -> { AutoTunnelService::class.java.simpleName, LEGACY_AUTO_TUNNEL_SERVICE_NAME -> {
when (intent.action) { when (intent.action) {
Action.START.name -> Action.START.name -> serviceManager.startAutoTunnel(true)
appDataRepository.settings.save( Action.STOP.name -> serviceManager.stopAutoTunnel()
settings.copy(
isAutoTunnelPaused = false,
),
)
Action.STOP.name ->
appDataRepository.settings.save(
settings.copy(
isAutoTunnelPaused = true,
),
)
} }
} }
} }

View File

@ -1,7 +1,6 @@
package com.zaneschepke.wireguardautotunnel.service.tile package com.zaneschepke.wireguardautotunnel.service.tile
import android.content.Intent import android.content.Intent
import android.os.Build
import android.os.IBinder import android.os.IBinder
import android.service.quicksettings.Tile import android.service.quicksettings.Tile
import android.service.quicksettings.TileService import android.service.quicksettings.TileService
@ -9,9 +8,9 @@ import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.LifecycleRegistry import androidx.lifecycle.LifecycleRegistry
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
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
@ -23,51 +22,18 @@ 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
private val lifecycleRegistry: LifecycleRegistry = LifecycleRegistry(this) private val lifecycleRegistry: LifecycleRegistry = LifecycleRegistry(this)
/* This works around an annoying unsolved frameworks bug some people are hitting. */
override fun onBind(intent: Intent): IBinder? {
var ret: IBinder? = null
try {
ret = super.onBind(intent)
} catch (e: Throwable) {
Timber.e("Failed to bind to AutoTunnelTile")
}
return ret
}
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_CREATE) lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_CREATE)
applicationScope.launch {
appDataRepository.settings.getSettingsFlow().collect {
kotlin.runCatching {
when (it.isAutoTunnelEnabled) {
true -> {
if (it.isAutoTunnelPaused) {
setInactive()
setTileDescription(this@AutoTunnelControlTile.getString(R.string.paused))
} else {
setActive()
setTileDescription(this@AutoTunnelControlTile.getString(R.string.active))
}
}
false -> {
setTileDescription(this@AutoTunnelControlTile.getString(R.string.disabled))
setUnavailable()
}
}
}.onFailure {
Timber.e(it)
}
}
}
} }
override fun onStopListening() { override fun onStopListening() {
@ -82,26 +48,28 @@ class AutoTunnelControlTile : TileService(), LifecycleOwner {
override fun onStartListening() { override fun onStartListening() {
super.onStartListening() super.onStartListening()
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_START) lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_START)
lifecycleScope.launch {
if (appDataRepository.tunnels.getAll().isEmpty()) return@launch setUnavailable()
updateTileState()
}
}
private fun updateTileState() {
serviceManager.autoTunnelActive.value.let {
if (it) setActive() else setInactive()
}
} }
override fun onClick() { override fun onClick() {
super.onClick() super.onClick()
unlockAndRun { unlockAndRun {
lifecycleScope.launch { lifecycleScope.launch {
kotlin.runCatching { if (serviceManager.autoTunnelActive.value) {
val settings = appDataRepository.settings.getSettings() serviceManager.stopAutoTunnel()
if (settings.isAutoTunnelPaused) { setInactive()
return@launch appDataRepository.settings.save( } else {
settings.copy( serviceManager.startAutoTunnel(true)
isAutoTunnelPaused = false, setActive()
),
)
}
appDataRepository.settings.save(
settings.copy(
isAutoTunnelPaused = true,
),
)
} }
} }
} }
@ -128,16 +96,15 @@ class AutoTunnelControlTile : TileService(), LifecycleOwner {
} }
} }
private fun setTileDescription(description: String) { /* This works around an annoying unsolved frameworks bug some people are hitting. */
kotlin.runCatching { override fun onBind(intent: Intent): IBinder? {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { var ret: IBinder? = null
qsTile.subtitle = description try {
} ret = super.onBind(intent)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { } catch (_: Throwable) {
qsTile.stateDescription = description Timber.e("Failed to bind to TunnelControlTile")
}
qsTile.updateTile()
} }
return ret
} }
override val lifecycle: Lifecycle override val lifecycle: Lifecycle

View File

@ -1,6 +1,8 @@
package com.zaneschepke.wireguardautotunnel.service.tile package com.zaneschepke.wireguardautotunnel.service.tile
import android.content.Intent
import android.os.Build import android.os.Build
import android.os.IBinder
import android.service.quicksettings.Tile import android.service.quicksettings.Tile
import android.service.quicksettings.TileService import android.service.quicksettings.TileService
import androidx.lifecycle.Lifecycle import androidx.lifecycle.Lifecycle
@ -11,8 +13,6 @@ 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.ApplicationScope import com.zaneschepke.wireguardautotunnel.module.ApplicationScope
import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelService import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelService
import com.zaneschepke.wireguardautotunnel.util.extensions.startTunnelBackground
import com.zaneschepke.wireguardautotunnel.util.extensions.stopTunnelBackground
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
@ -36,7 +36,6 @@ class TunnelControlTile : TileService(), LifecycleOwner {
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
Timber.d("onCreate for tile service")
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_CREATE) lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_CREATE)
} }
@ -52,6 +51,7 @@ class TunnelControlTile : TileService(), LifecycleOwner {
override fun onStartListening() { override fun onStartListening() {
super.onStartListening() super.onStartListening()
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_START) lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_START)
Timber.d("Updating tile!")
lifecycleScope.launch { lifecycleScope.launch {
if (appDataRepository.tunnels.getAll().isEmpty()) return@launch setUnavailable() if (appDataRepository.tunnels.getAll().isEmpty()) return@launch setUnavailable()
updateTileState() updateTileState()
@ -60,6 +60,7 @@ class TunnelControlTile : TileService(), LifecycleOwner {
private suspend fun updateTileState() { private suspend fun updateTileState() {
val lastActive = appDataRepository.getStartTunnelConfig() val lastActive = appDataRepository.getStartTunnelConfig()
Timber.d("Got config $lastActive")
lastActive?.let { lastActive?.let {
updateTile(it) updateTile(it)
} }
@ -68,13 +69,15 @@ class TunnelControlTile : TileService(), LifecycleOwner {
override fun onClick() { override fun onClick() {
super.onClick() super.onClick()
unlockAndRun { unlockAndRun {
Timber.d("Click")
lifecycleScope.launch { lifecycleScope.launch {
val context = this@TunnelControlTile
val lastActive = appDataRepository.getStartTunnelConfig() val lastActive = appDataRepository.getStartTunnelConfig()
lastActive?.let { tunnel -> lastActive?.let { tunnel ->
if (tunnel.isActive) return@launch context.stopTunnelBackground(tunnel.id) if (tunnel.isActive) {
context.startTunnelBackground(tunnel.id) tunnelService.get().stopTunnel(tunnel)
} else {
tunnelService.get().startTunnel(tunnel, true)
}
updateTileState()
} }
} }
} }
@ -124,6 +127,17 @@ class TunnelControlTile : TileService(), LifecycleOwner {
} }
} }
/* This works around an annoying unsolved frameworks bug some people are hitting. */
override fun onBind(intent: Intent): IBinder? {
var ret: IBinder? = null
try {
ret = super.onBind(intent)
} catch (_: Throwable) {
Timber.e("Failed to bind to TunnelControlTile")
}
return ret
}
override val lifecycle: Lifecycle override val lifecycle: Lifecycle
get() = lifecycleRegistry get() = lifecycleRegistry
} }

View File

@ -5,7 +5,7 @@ import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
interface TunnelService : Tunnel, org.amnezia.awg.backend.Tunnel { interface TunnelService : Tunnel, org.amnezia.awg.backend.Tunnel {
suspend fun startTunnel(tunnelConfig: TunnelConfig): Result<TunnelState> suspend fun startTunnel(tunnelConfig: TunnelConfig, background: Boolean = false): Result<TunnelState>
suspend fun stopTunnel(tunnelConfig: TunnelConfig): Result<TunnelState> suspend fun stopTunnel(tunnelConfig: TunnelConfig): Result<TunnelState>
@ -18,5 +18,6 @@ interface TunnelService : Tunnel, org.amnezia.awg.backend.Tunnel {
suspend fun getState(): TunnelState suspend fun getState(): TunnelState
fun cancelStatsJob() fun cancelStatsJob()
fun startStatsJob() fun startStatsJob()
} }

View File

@ -8,6 +8,7 @@ import com.zaneschepke.wireguardautotunnel.data.repository.TunnelConfigRepositor
import com.zaneschepke.wireguardautotunnel.module.ApplicationScope import com.zaneschepke.wireguardautotunnel.module.ApplicationScope
import com.zaneschepke.wireguardautotunnel.module.IoDispatcher import com.zaneschepke.wireguardautotunnel.module.IoDispatcher
import com.zaneschepke.wireguardautotunnel.module.Kernel import com.zaneschepke.wireguardautotunnel.module.Kernel
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
import com.zaneschepke.wireguardautotunnel.service.tunnel.statistics.AmneziaStatistics import com.zaneschepke.wireguardautotunnel.service.tunnel.statistics.AmneziaStatistics
import com.zaneschepke.wireguardautotunnel.service.tunnel.statistics.TunnelStatistics import com.zaneschepke.wireguardautotunnel.service.tunnel.statistics.TunnelStatistics
import com.zaneschepke.wireguardautotunnel.service.tunnel.statistics.WireGuardStatistics import com.zaneschepke.wireguardautotunnel.service.tunnel.statistics.WireGuardStatistics
@ -36,6 +37,7 @@ constructor(
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,
private val serviceManager: ServiceManager,
) : TunnelService { ) : TunnelService {
private val _vpnState = MutableStateFlow(VpnState()) private val _vpnState = MutableStateFlow(VpnState())
@ -87,9 +89,9 @@ constructor(
} }
} }
override suspend fun startTunnel(tunnelConfig: TunnelConfig): Result<TunnelState> { override suspend fun startTunnel(tunnelConfig: TunnelConfig, background: Boolean): Result<TunnelState> {
return withContext(ioDispatcher) { return withContext(ioDispatcher) {
onBeforeStart(tunnelConfig) onBeforeStart(tunnelConfig, background)
setState(tunnelConfig, TunnelState.UP).onSuccess { setState(tunnelConfig, TunnelState.UP).onSuccess {
emitTunnelState(it) emitTunnelState(it)
}.onFailure { }.onFailure {
@ -143,18 +145,28 @@ constructor(
resetBackendStatistics() resetBackendStatistics()
} }
private suspend fun onBeforeStart(tunnelConfig: TunnelConfig) { private suspend fun onBeforeStart(tunnelConfig: TunnelConfig, background: Boolean) {
if (_vpnState.value.status == TunnelState.UP) vpnState.value.tunnelConfig?.let { stopTunnel(it) } if (_vpnState.value.status == TunnelState.UP &&
tunnelConfig != _vpnState.value.tunnelConfig
) {
vpnState.value.tunnelConfig?.let { stopTunnel(it) }
}
if (background) serviceManager.startBackgroundService()
resetBackendStatistics() resetBackendStatistics()
appDataRepository.tunnels.save(tunnelConfig.copy(isActive = true)) appDataRepository.tunnels.save(tunnelConfig.copy(isActive = true))
emitVpnStateConfig(tunnelConfig) emitVpnStateConfig(tunnelConfig)
startStatsJob() startStatsJob()
Timber.d("Updating start")
serviceManager.requestTunnelTileUpdate()
} }
private suspend fun onBeforeStop(tunnelConfig: TunnelConfig) { private suspend fun onBeforeStop(tunnelConfig: TunnelConfig) {
cancelStatsJob() cancelStatsJob()
resetBackendStatistics() resetBackendStatistics()
appDataRepository.tunnels.save(tunnelConfig.copy(isActive = false)) appDataRepository.tunnels.save(tunnelConfig.copy(isActive = false))
serviceManager.stopBackgroundService()
Timber.d("UPdating stop")
serviceManager.requestTunnelTileUpdate()
} }
private fun emitTunnelState(state: TunnelState) { private fun emitTunnelState(state: TunnelState) {

View File

@ -10,6 +10,5 @@ data class AppUiState(
val tunnels: List<TunnelConfig> = emptyList(), val tunnels: List<TunnelConfig> = emptyList(),
val vpnState: VpnState = VpnState(), val vpnState: VpnState = VpnState(),
val generalState: GeneralState = GeneralState(), val generalState: GeneralState = GeneralState(),
val isKernelAvailable: Boolean = false, val autoTunnelActive: Boolean = false,
val isRooted: Boolean = false,
) )

View File

@ -2,8 +2,6 @@ package com.zaneschepke.wireguardautotunnel.ui
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.wireguard.android.backend.WgQuickBackend
import com.wireguard.android.util.RootShell
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
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
@ -11,7 +9,6 @@ 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.Constants 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.CoroutineDispatcher import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
@ -20,13 +17,10 @@ import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.onCompletion import kotlinx.coroutines.flow.onCompletion
import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.takeWhile import kotlinx.coroutines.flow.takeWhile
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.plus import kotlinx.coroutines.plus
import kotlinx.coroutines.withContext
import xyz.teamgravity.pin_lock_compose.PinManager import xyz.teamgravity.pin_lock_compose.PinManager
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Provider import javax.inject.Provider
@ -38,6 +32,7 @@ constructor(
private val appDataRepository: AppDataRepository, private val appDataRepository: AppDataRepository,
private val tunnelService: Provider<TunnelService>, private val tunnelService: Provider<TunnelService>,
@IoDispatcher private val ioDispatcher: CoroutineDispatcher, @IoDispatcher private val ioDispatcher: CoroutineDispatcher,
private val serviceManager: ServiceManager,
) : ViewModel() { ) : ViewModel() {
val uiState = val uiState =
@ -46,12 +41,14 @@ constructor(
appDataRepository.tunnels.getTunnelConfigsFlow(), appDataRepository.tunnels.getTunnelConfigsFlow(),
tunnelService.get().vpnState, tunnelService.get().vpnState,
appDataRepository.appState.generalStateFlow, appDataRepository.appState.generalStateFlow,
) { settings, tunnels, tunnelState, generalState -> serviceManager.autoTunnelActive,
) { settings, tunnels, tunnelState, generalState, autoTunnel ->
AppUiState( AppUiState(
settings, settings,
tunnels, tunnels,
tunnelState, tunnelState,
generalState, generalState,
autoTunnel,
) )
}.stateIn( }.stateIn(
viewModelScope + ioDispatcher, viewModelScope + ioDispatcher,
@ -95,7 +92,7 @@ constructor(
private suspend fun initAutoTunnel() { private suspend fun initAutoTunnel() {
val settings = appDataRepository.settings.getSettings() val settings = appDataRepository.settings.getSettings()
if (settings.isAutoTunnelEnabled) ServiceManager.startWatcherService(WireGuardAutoTunnel.instance) if (settings.isAutoTunnelEnabled) serviceManager.startAutoTunnel(false)
} }
fun onPinLockDisabled() = viewModelScope.launch(ioDispatcher) { fun onPinLockDisabled() = viewModelScope.launch(ioDispatcher) {

View File

@ -39,9 +39,7 @@ import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
import com.zaneschepke.wireguardautotunnel.data.datastore.LocaleStorage import com.zaneschepke.wireguardautotunnel.data.datastore.LocaleStorage
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.navigation.BottomNavItem import com.zaneschepke.wireguardautotunnel.ui.common.navigation.BottomNavItem
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.LocalFocusRequester import com.zaneschepke.wireguardautotunnel.ui.common.navigation.LocalFocusRequester
@ -67,6 +65,7 @@ import com.zaneschepke.wireguardautotunnel.util.LocaleUtil
import com.zaneschepke.wireguardautotunnel.util.extensions.requestAutoTunnelTileServiceUpdate import com.zaneschepke.wireguardautotunnel.util.extensions.requestAutoTunnelTileServiceUpdate
import com.zaneschepke.wireguardautotunnel.util.extensions.requestTunnelTileServiceStateUpdate import com.zaneschepke.wireguardautotunnel.util.extensions.requestTunnelTileServiceStateUpdate
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
@AndroidEntryPoint @AndroidEntryPoint
@ -100,17 +99,17 @@ class MainActivity : AppCompatActivity() {
val navController = rememberNavController() val navController = rememberNavController()
val rootItemFocusRequester = remember { FocusRequester() } val rootItemFocusRequester = remember { FocusRequester() }
LaunchedEffect(appUiState.vpnState.status) { LaunchedEffect(appUiState.tunnels) {
val context = this@MainActivity Timber.d("Updating launched")
when (appUiState.vpnState.status) { requestTunnelTileServiceStateUpdate()
TunnelState.DOWN -> ServiceManager.stopTunnelBackgroundService(context) }
else -> Unit
} LaunchedEffect(appUiState.autoTunnelActive) {
context.requestTunnelTileServiceStateUpdate() requestAutoTunnelTileServiceUpdate()
} }
with(appUiState.settings) { with(appUiState.settings) {
LaunchedEffect(isAutoTunnelPaused, isAutoTunnelEnabled) { LaunchedEffect(isAutoTunnelEnabled) {
this@MainActivity.requestAutoTunnelTileServiceUpdate() this@MainActivity.requestAutoTunnelTileServiceUpdate()
} }
} }
@ -248,4 +247,3 @@ class MainActivity : AppCompatActivity() {
tunnelService.cancelStatsJob() tunnelService.cancelStatsJob()
} }
} }

View File

@ -31,7 +31,7 @@ fun ClickableIconButton(onClick: () -> Unit, onIconClick: () -> Unit, text: Stri
if (enabled) { if (enabled) {
onIconClick() onIconClick()
} }
}, },
) )
} }
} }

View File

@ -16,8 +16,6 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp

View File

@ -2,7 +2,6 @@ package com.zaneschepke.wireguardautotunnel.ui.common.button
import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.focusable
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
@ -31,21 +30,27 @@ import kotlin.let
@androidx.compose.runtime.Composable @androidx.compose.runtime.Composable
fun IconSurfaceButton(title: String, onClick: () -> Unit, selected: Boolean, leadingIcon: ImageVector? = null, description: String? = null) { fun IconSurfaceButton(title: String, onClick: () -> Unit, selected: Boolean, leadingIcon: ImageVector? = null, description: String? = null) {
val border: BorderStroke? = val border: BorderStroke? =
if (selected) BorderStroke( if (selected) {
1.dp, BorderStroke(
MaterialTheme.colorScheme.primary 1.dp,
) else null MaterialTheme.colorScheme.primary,
Card( )
modifier = } else {
Modifier null
.fillMaxWidth() }
.height(IntrinsicSize.Min), Card(
shape = RoundedCornerShape(8.dp), modifier =
border = border, Modifier
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface), .fillMaxWidth()
) { .height(IntrinsicSize.Min),
Box(modifier = Modifier.clickable { onClick() } shape = RoundedCornerShape(8.dp),
.fillMaxWidth()) { border = border,
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface),
) {
Box(
modifier = Modifier.clickable { onClick() }
.fillMaxWidth(),
) {
Column( Column(
modifier = modifier =
Modifier Modifier
@ -61,7 +66,7 @@ fun IconSurfaceButton(title: String, onClick: () -> Unit, selected: Boolean, lea
) { ) {
Row( Row(
horizontalArrangement = Arrangement.spacedBy( horizontalArrangement = Arrangement.spacedBy(
16.dp.scaledWidth() 16.dp.scaledWidth(),
), ),
verticalAlignment = Alignment.Companion.CenterVertically, verticalAlignment = Alignment.Companion.CenterVertically,
modifier = Modifier.padding(vertical = if (description == null) 10.dp.scaledHeight() else 0.dp), modifier = Modifier.padding(vertical = if (description == null) 10.dp.scaledHeight() else 0.dp),
@ -77,7 +82,7 @@ fun IconSurfaceButton(title: String, onClick: () -> Unit, selected: Boolean, lea
Column { Column {
Text( Text(
title, title,
style = MaterialTheme.typography.titleMedium style = MaterialTheme.typography.titleMedium,
) )
description?.let { description?.let {
Text( Text(
@ -91,5 +96,5 @@ fun IconSurfaceButton(title: String, onClick: () -> Unit, selected: Boolean, lea
} }
} }
} }
} }
} }

View File

@ -1,7 +1,6 @@
package com.zaneschepke.wireguardautotunnel.ui.common.button.surface package com.zaneschepke.wireguardautotunnel.ui.common.button.surface
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.ImageVector
data class SelectionItem( data class SelectionItem(

View File

@ -24,65 +24,63 @@ import com.zaneschepke.wireguardautotunnel.util.extensions.scaledWidth
@Composable @Composable
fun SurfaceSelectionGroupButton(items: List<SelectionItem>) { fun SurfaceSelectionGroupButton(items: List<SelectionItem>) {
Card(
Card( modifier = Modifier.fillMaxWidth(),
modifier = Modifier.fillMaxWidth(), shape = RoundedCornerShape(8.dp),
shape = RoundedCornerShape(8.dp), colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface),
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface), ) {
) { items.mapIndexed { index, item ->
items.mapIndexed { index, item -> Box(
Box( contentAlignment = Alignment.Center,
contentAlignment = Alignment.Center, modifier = Modifier
modifier = Modifier .then(item.onClick?.let { Modifier.clickable { it() } } ?: Modifier)
.then(item.onClick?.let { Modifier.clickable { it() }} ?: Modifier) .fillMaxWidth(),
.fillMaxWidth() ) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.fillMaxWidth().padding(vertical = 4.dp.scaledHeight()),
) { ) {
Row( Row(
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.fillMaxWidth().padding(vertical = 4.dp.scaledHeight()), modifier = Modifier
.padding(start = 16.dp.scaledWidth())
.weight(4f, false)
.fillMaxWidth(),
) { ) {
Row( item.leadingIcon?.let { icon ->
verticalAlignment = Alignment.CenterVertically, Icon(
modifier = Modifier icon,
.padding(start = 16.dp.scaledWidth()) icon.name,
.weight(4f, false) modifier = Modifier.size(iconSize),
.fillMaxWidth(), )
) {
item.leadingIcon?.let { icon ->
Icon(
icon,
icon.name,
modifier = Modifier.size(iconSize),
)
}
Column(
horizontalAlignment = Alignment.Start,
verticalArrangement = Arrangement.spacedBy(2.dp, Alignment.CenterVertically),
modifier = Modifier
.fillMaxWidth()
.padding(start = if (item.leadingIcon != null) 16.dp.scaledWidth() else 0.dp)
.padding(vertical = if (item.description == null) 16.dp.scaledHeight() else 6.dp.scaledHeight()),
) {
item.title()
item.description?.let {
it()
}
}
} }
item.trailing?.let { Column(
Box( horizontalAlignment = Alignment.Start,
contentAlignment = Alignment.CenterEnd, verticalArrangement = Arrangement.spacedBy(2.dp, Alignment.CenterVertically),
modifier = Modifier modifier = Modifier
.padding(end = 24.dp.scaledWidth(), start = 16.dp.scaledWidth()) .fillMaxWidth()
.weight(1f), .padding(start = if (item.leadingIcon != null) 16.dp.scaledWidth() else 0.dp)
) { .padding(vertical = if (item.description == null) 16.dp.scaledHeight() else 6.dp.scaledHeight()),
) {
item.title()
item.description?.let {
it() it()
} }
} }
} }
item.trailing?.let {
Box(
contentAlignment = Alignment.CenterEnd,
modifier = Modifier
.padding(end = 24.dp.scaledWidth(), start = 16.dp.scaledWidth())
.weight(1f),
) {
it()
}
}
} }
if (index + 1 != items.size) HorizontalDivider(color = MaterialTheme.colorScheme.outlineVariant)
} }
if (index + 1 != items.size) HorizontalDivider(color = MaterialTheme.colorScheme.outlineVariant)
} }
} }
}

View File

@ -18,8 +18,6 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier 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.LocalFocusManager
import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource

View File

@ -19,4 +19,3 @@ fun GroupLabel(title: String) {
) )
} }
} }

View File

@ -27,7 +27,7 @@ fun VersionLabel() {
color = MaterialTheme.colorScheme.outline, color = MaterialTheme.colorScheme.outline,
modifier = Modifier.clickable { modifier = Modifier.clickable {
clipboardManager.setText(AnnotatedString(BuildConfig.VERSION_NAME)) clipboardManager.setText(AnnotatedString(BuildConfig.VERSION_NAME))
} },
) )
} }
} }

View File

@ -8,21 +8,11 @@ import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
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.saveable.rememberSaveable import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.onFocusChanged
import androidx.compose.ui.input.key.Key
import androidx.compose.ui.input.key.KeyEventType
import androidx.compose.ui.input.key.key
import androidx.compose.ui.input.key.onKeyEvent
import androidx.compose.ui.input.key.type
import androidx.navigation.NavController import androidx.navigation.NavController
import androidx.navigation.NavGraph.Companion.findStartDestination import androidx.navigation.NavGraph.Companion.findStartDestination
import androidx.navigation.compose.currentBackStackEntryAsState import androidx.navigation.compose.currentBackStackEntryAsState
import timber.log.Timber
@Composable @Composable
fun BottomNavBar(navController: NavController, bottomNavItems: List<BottomNavItem>) { fun BottomNavBar(navController: NavController, bottomNavItems: List<BottomNavItem>) {
@ -34,7 +24,6 @@ fun BottomNavBar(navController: NavController, bottomNavItems: List<BottomNavIte
} }
if (showBottomBar) { if (showBottomBar) {
NavigationBar( NavigationBar(
containerColor = MaterialTheme.colorScheme.surface, containerColor = MaterialTheme.colorScheme.surface,
) { ) {

View File

@ -1,7 +1,6 @@
package com.zaneschepke.wireguardautotunnel.ui.common.navigation package com.zaneschepke.wireguardautotunnel.ui.common.navigation
import androidx.compose.runtime.compositionLocalOf import androidx.compose.runtime.compositionLocalOf
import androidx.compose.runtime.staticCompositionLocalOf
import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.FocusRequester
import androidx.navigation.NavHostController import androidx.navigation.NavHostController
@ -10,4 +9,3 @@ val LocalNavController = compositionLocalOf<NavHostController> {
} }
val LocalFocusRequester = compositionLocalOf<FocusRequester> { error("FocusRequester is not provided") } val LocalFocusRequester = compositionLocalOf<FocusRequester> { error("FocusRequester is not provided") }

View File

@ -18,12 +18,14 @@ fun TopNavBar(title: String, trailing: @Composable () -> Unit = {}, showBack: Bo
Text(title) Text(title)
}, },
navigationIcon = { navigationIcon = {
if(showBack) IconButton(onClick = { navController.popBackStack() }) { if (showBack) {
val icon = Icons.AutoMirrored.Outlined.ArrowBack IconButton(onClick = { navController.popBackStack() }) {
Icon( val icon = Icons.AutoMirrored.Outlined.ArrowBack
imageVector = icon, Icon(
contentDescription = icon.name, imageVector = icon,
) contentDescription = icon.name,
)
}
} }
}, },
actions = { actions = {

View File

@ -234,7 +234,7 @@ fun ConfigScreen(tunnelId: Int) {
hint = stringResource(R.string.tunnel_name).lowercase(), hint = stringResource(R.string.tunnel_name).lowercase(),
modifier = modifier =
Modifier Modifier
.fillMaxWidth() .fillMaxWidth(),
) )
OutlinedTextField( OutlinedTextField(
modifier = modifier =
@ -347,7 +347,7 @@ fun ConfigScreen(tunnelId: Int) {
hint = stringResource(R.string.junk_packet_count).lowercase(), hint = stringResource(R.string.junk_packet_count).lowercase(),
modifier = modifier =
Modifier Modifier
.fillMaxWidth() .fillMaxWidth(),
) )
ConfigurationTextBox( ConfigurationTextBox(
value = uiState.interfaceProxy.junkPacketMinSize, value = uiState.interfaceProxy.junkPacketMinSize,
@ -360,7 +360,7 @@ fun ConfigScreen(tunnelId: Int) {
).lowercase(), ).lowercase(),
modifier = modifier =
Modifier Modifier
.fillMaxWidth() .fillMaxWidth(),
) )
ConfigurationTextBox( ConfigurationTextBox(
value = uiState.interfaceProxy.junkPacketMaxSize, value = uiState.interfaceProxy.junkPacketMaxSize,
@ -373,7 +373,7 @@ fun ConfigScreen(tunnelId: Int) {
).lowercase(), ).lowercase(),
modifier = modifier =
Modifier Modifier
.fillMaxWidth() .fillMaxWidth(),
) )
ConfigurationTextBox( ConfigurationTextBox(
value = uiState.interfaceProxy.initPacketJunkSize, value = uiState.interfaceProxy.initPacketJunkSize,
@ -383,7 +383,7 @@ fun ConfigScreen(tunnelId: Int) {
hint = stringResource(R.string.init_packet_junk_size).lowercase(), hint = stringResource(R.string.init_packet_junk_size).lowercase(),
modifier = modifier =
Modifier Modifier
.fillMaxWidth() .fillMaxWidth(),
) )
ConfigurationTextBox( ConfigurationTextBox(
value = uiState.interfaceProxy.responsePacketJunkSize, value = uiState.interfaceProxy.responsePacketJunkSize,
@ -396,7 +396,7 @@ fun ConfigScreen(tunnelId: Int) {
).lowercase(), ).lowercase(),
modifier = modifier =
Modifier Modifier
.fillMaxWidth() .fillMaxWidth(),
) )
ConfigurationTextBox( ConfigurationTextBox(
value = uiState.interfaceProxy.initPacketMagicHeader, value = uiState.interfaceProxy.initPacketMagicHeader,
@ -409,7 +409,7 @@ fun ConfigScreen(tunnelId: Int) {
).lowercase(), ).lowercase(),
modifier = modifier =
Modifier Modifier
.fillMaxWidth() .fillMaxWidth(),
) )
ConfigurationTextBox( ConfigurationTextBox(
value = uiState.interfaceProxy.responsePacketMagicHeader, value = uiState.interfaceProxy.responsePacketMagicHeader,
@ -422,7 +422,7 @@ fun ConfigScreen(tunnelId: Int) {
).lowercase(), ).lowercase(),
modifier = modifier =
Modifier Modifier
.fillMaxWidth() .fillMaxWidth(),
) )
ConfigurationTextBox( ConfigurationTextBox(
value = uiState.interfaceProxy.underloadPacketMagicHeader, value = uiState.interfaceProxy.underloadPacketMagicHeader,
@ -435,7 +435,7 @@ fun ConfigScreen(tunnelId: Int) {
).lowercase(), ).lowercase(),
modifier = modifier =
Modifier Modifier
.fillMaxWidth() .fillMaxWidth(),
) )
ConfigurationTextBox( ConfigurationTextBox(
value = uiState.interfaceProxy.transportPacketMagicHeader, value = uiState.interfaceProxy.transportPacketMagicHeader,
@ -448,7 +448,7 @@ fun ConfigScreen(tunnelId: Int) {
).lowercase(), ).lowercase(),
modifier = modifier =
Modifier Modifier
.fillMaxWidth() .fillMaxWidth(),
) )
} }
Row( Row(

View File

@ -1,7 +1,11 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.main package com.zaneschepke.wireguardautotunnel.ui.screens.main
import android.content.Intent
import android.net.Uri
import android.net.VpnService import android.net.VpnService
import android.provider.Settings
import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.ActivityResult
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity.RESULT_OK import androidx.appcompat.app.AppCompatActivity.RESULT_OK
import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.ExperimentalFoundationApi
@ -53,9 +57,9 @@ import com.zaneschepke.wireguardautotunnel.ui.screens.main.components.TunnelImpo
import com.zaneschepke.wireguardautotunnel.ui.screens.main.components.TunnelRowItem import com.zaneschepke.wireguardautotunnel.ui.screens.main.components.TunnelRowItem
import com.zaneschepke.wireguardautotunnel.ui.screens.main.components.VpnDeniedDialog import com.zaneschepke.wireguardautotunnel.ui.screens.main.components.VpnDeniedDialog
import com.zaneschepke.wireguardautotunnel.util.Constants import com.zaneschepke.wireguardautotunnel.util.Constants
import com.zaneschepke.wireguardautotunnel.util.extensions.isBatteryOptimizationsDisabled
import com.zaneschepke.wireguardautotunnel.util.extensions.isRunningOnTv import com.zaneschepke.wireguardautotunnel.util.extensions.isRunningOnTv
import com.zaneschepke.wireguardautotunnel.util.extensions.openWebUrl import com.zaneschepke.wireguardautotunnel.util.extensions.openWebUrl
import com.zaneschepke.wireguardautotunnel.util.extensions.startTunnelBackground
@OptIn(ExperimentalFoundationApi::class) @OptIn(ExperimentalFoundationApi::class)
@Composable @Composable
@ -75,13 +79,19 @@ fun MainScreen(viewModel: MainViewModel = hiltViewModel(), uiState: AppUiState)
NestedScrollListener({ isFabVisible = false }, { isFabVisible = true }) NestedScrollListener({ isFabVisible = false }, { isFabVisible = true })
} }
val vpnActivityResultState = val vpnActivity =
rememberLauncherForActivityResult( rememberLauncherForActivityResult(
ActivityResultContracts.StartActivityForResult(), ActivityResultContracts.StartActivityForResult(),
onResult = { onResult = {
if (it.resultCode != RESULT_OK) showVpnPermissionDialog = true if (it.resultCode != RESULT_OK) showVpnPermissionDialog = true
}, },
) )
val batteryActivity =
rememberLauncherForActivityResult(
ActivityResultContracts.StartActivityForResult(),
) { result: ActivityResult ->
viewModel.setBatteryOptimizeDisableShown()
}
val tunnelFileImportResultLauncher = rememberFileImportLauncherForResult(onNoFileExplorer = { val tunnelFileImportResultLauncher = rememberFileImportLauncherForResult(onNoFileExplorer = {
snackbar.showMessage( snackbar.showMessage(
@ -104,7 +114,7 @@ fun MainScreen(viewModel: MainViewModel = hiltViewModel(), uiState: AppUiState)
InfoDialog( InfoDialog(
onDismiss = { showDeleteTunnelAlertDialog = false }, onDismiss = { showDeleteTunnelAlertDialog = false },
onAttest = { onAttest = {
selectedTunnel?.let { viewModel.onDelete(it, context) } selectedTunnel?.let { viewModel::onDelete }
showDeleteTunnelAlertDialog = false showDeleteTunnelAlertDialog = false
selectedTunnel = null selectedTunnel = null
}, },
@ -114,15 +124,35 @@ fun MainScreen(viewModel: MainViewModel = hiltViewModel(), uiState: AppUiState)
) )
} }
fun requestBatteryOptimizationsDisabled() {
val intent =
Intent().apply {
action = Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS
data = Uri.parse("package:${context.packageName}")
}
batteryActivity.launch(intent)
}
fun onAutoTunnelToggle() {
if (!uiState.generalState.isBatteryOptimizationDisableShown &&
!context.isBatteryOptimizationsDisabled() && !isRunningOnTv
) {
return requestBatteryOptimizationsDisabled()
}
val intent = if (!uiState.settings.isKernelEnabled) {
VpnService.prepare(context)
} else {
null
}
if (intent != null) return vpnActivity.launch(intent)
viewModel.onToggleAutoTunnel()
}
fun onTunnelToggle(checked: Boolean, tunnel: TunnelConfig) { fun onTunnelToggle(checked: Boolean, tunnel: TunnelConfig) {
val intent = if (uiState.settings.isKernelEnabled) null else VpnService.prepare(context) val intent = if (uiState.settings.isKernelEnabled) null else VpnService.prepare(context)
if (intent != null) return vpnActivityResultState.launch(intent) if (intent != null) return vpnActivity.launch(intent)
if (!checked) viewModel.onTunnelStop(tunnel).also { return } if (!checked) viewModel.onTunnelStop(tunnel).also { return }
if (uiState.settings.isKernelEnabled) { viewModel.onTunnelStart(tunnel, uiState.settings.isKernelEnabled)
context.startTunnelBackground(tunnel.id)
} else {
viewModel.onTunnelStart(tunnel)
}
} }
Scaffold( Scaffold(
@ -137,34 +167,38 @@ fun MainScreen(viewModel: MainViewModel = hiltViewModel(), uiState: AppUiState)
}, },
floatingActionButtonPosition = FabPosition.End, floatingActionButtonPosition = FabPosition.End,
floatingActionButton = { floatingActionButton = {
if(!isRunningOnTv) ScrollDismissFab({ if (!isRunningOnTv) {
val icon = Icons.Filled.Add ScrollDismissFab({
Icon( val icon = Icons.Filled.Add
imageVector = icon, Icon(
contentDescription = icon.name, imageVector = icon,
tint = MaterialTheme.colorScheme.onPrimary, contentDescription = icon.name,
) tint = MaterialTheme.colorScheme.onPrimary,
}, isVisible = isFabVisible, onClick = { )
showBottomSheet = true }, isVisible = isFabVisible, onClick = {
}) showBottomSheet = true
})
}
}, },
topBar = { topBar = {
if(isRunningOnTv) TopNavBar( if (isRunningOnTv) {
showBack = false, TopNavBar(
title = stringResource(R.string.app_name), showBack = false,
trailing = { title = stringResource(R.string.app_name),
IconButton(onClick = { trailing = {
showBottomSheet = true IconButton(onClick = {
}) { showBottomSheet = true
val icon = Icons.Outlined.Add }) {
Icon( val icon = Icons.Outlined.Add
imageVector = icon, Icon(
contentDescription = icon.name, imageVector = icon,
) contentDescription = icon.name,
} )
} }
) },
} )
}
},
) { ) {
TunnelImportSheet( TunnelImportSheet(
showBottomSheet, showBottomSheet,
@ -196,7 +230,9 @@ fun MainScreen(viewModel: MainViewModel = hiltViewModel(), uiState: AppUiState)
} }
} else { } else {
item { item {
AutoTunnelRowItem(uiState.settings, { viewModel.onToggleAutoTunnel(context) }) AutoTunnelRowItem(uiState, {
onAutoTunnelToggle()
})
} }
} }
items( items(

View File

@ -30,26 +30,24 @@ import timber.log.Timber
import java.io.InputStream import java.io.InputStream
import java.util.zip.ZipInputStream import java.util.zip.ZipInputStream
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Provider
@HiltViewModel @HiltViewModel
class MainViewModel class MainViewModel
@Inject @Inject
constructor( constructor(
private val appDataRepository: AppDataRepository, private val appDataRepository: AppDataRepository,
val tunnelService: TunnelService, private val tunnelService: Provider<TunnelService>,
@IoDispatcher private val ioDispatcher: CoroutineDispatcher, @IoDispatcher private val ioDispatcher: CoroutineDispatcher,
private val serviceManager: ServiceManager,
) : ViewModel() { ) : ViewModel() {
private fun stopWatcherService(context: Context) { fun onDelete(tunnel: TunnelConfig) {
ServiceManager.stopWatcherService(context)
}
fun onDelete(tunnel: TunnelConfig, context: Context) {
viewModelScope.launch { viewModelScope.launch {
val settings = appDataRepository.settings.getSettings() val settings = appDataRepository.settings.getSettings()
val isPrimary = tunnel.isPrimaryTunnel val isPrimary = tunnel.isPrimaryTunnel
if (appDataRepository.tunnels.count() == 1 || isPrimary) { if (appDataRepository.tunnels.count() == 1 || isPrimary) {
stopWatcherService(context) serviceManager.stopAutoTunnel()
resetTunnelSetting(settings) resetTunnelSetting(settings)
} }
appDataRepository.tunnels.delete(tunnel) appDataRepository.tunnels.delete(tunnel)
@ -69,14 +67,14 @@ constructor(
appDataRepository.appState.setTunnelStatsExpanded(expanded) appDataRepository.appState.setTunnelStatsExpanded(expanded)
} }
fun onTunnelStart(tunnelConfig: TunnelConfig) = viewModelScope.launch { fun onTunnelStart(tunnelConfig: TunnelConfig, background: Boolean) = viewModelScope.launch {
Timber.i("Starting tunnel ${tunnelConfig.name}") Timber.i("Starting tunnel ${tunnelConfig.name}")
tunnelService.startTunnel(tunnelConfig) tunnelService.get().startTunnel(tunnelConfig, background)
} }
fun onTunnelStop(tunnel: TunnelConfig) = viewModelScope.launch { fun onTunnelStop(tunnel: TunnelConfig) = viewModelScope.launch {
Timber.i("Stopping active tunnel") Timber.i("Stopping active tunnel")
tunnelService.stopTunnel(tunnel) tunnelService.get().stopTunnel(tunnel)
} }
private fun generateQrCodeDefaultName(config: String): String { private fun generateQrCodeDefaultName(config: String): String {
@ -160,16 +158,17 @@ constructor(
} }
} }
fun onToggleAutoTunnel(context: Context) = viewModelScope.launch { fun onToggleAutoTunnel() = viewModelScope.launch {
val settings = appDataRepository.settings.getSettings() val settings = appDataRepository.settings.getSettings()
if (settings.isAutoTunnelEnabled) { val toggled = !settings.isAutoTunnelEnabled
ServiceManager.stopWatcherService(context) if (toggled) {
serviceManager.startAutoTunnel(false)
} else { } else {
ServiceManager.startWatcherService(context) serviceManager.stopAutoTunnel()
} }
appDataRepository.settings.save( appDataRepository.settings.save(
settings.copy( settings.copy(
isAutoTunnelEnabled = !settings.isAutoTunnelEnabled, isAutoTunnelEnabled = toggled,
), ),
) )
} }
@ -195,6 +194,10 @@ constructor(
} }
} }
fun setBatteryOptimizeDisableShown() = viewModelScope.launch {
appDataRepository.appState.setBatteryOptimizationDisableShown(true)
}
private suspend fun saveTunnelFromConfUri(name: String, uri: Uri, context: Context) { private suspend fun saveTunnelFromConfUri(name: String, uri: Uri, context: Context) {
val stream = getInputStreamFromUri(uri, context) ?: throw FileReadException val stream = getInputStreamFromUri(uri, context) ?: throw FileReadException
saveTunnelConfigFromStream(stream, name) saveTunnelConfigFromStream(stream, name)

View File

@ -4,30 +4,25 @@ import androidx.compose.foundation.layout.size
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.Bolt import androidx.compose.material.icons.rounded.Bolt
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.scale import androidx.compose.ui.draw.scale
import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.zaneschepke.wireguardautotunnel.R import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.data.domain.Settings import com.zaneschepke.wireguardautotunnel.ui.AppUiState
import com.zaneschepke.wireguardautotunnel.ui.common.ExpandingRowListItem import com.zaneschepke.wireguardautotunnel.ui.common.ExpandingRowListItem
import com.zaneschepke.wireguardautotunnel.ui.common.button.ScaledSwitch import com.zaneschepke.wireguardautotunnel.ui.common.button.ScaledSwitch
import com.zaneschepke.wireguardautotunnel.ui.theme.SilverTree import com.zaneschepke.wireguardautotunnel.ui.theme.SilverTree
import com.zaneschepke.wireguardautotunnel.ui.theme.iconSize
import com.zaneschepke.wireguardautotunnel.util.extensions.isRunningOnTv import com.zaneschepke.wireguardautotunnel.util.extensions.isRunningOnTv
import com.zaneschepke.wireguardautotunnel.util.extensions.scaledHeight import com.zaneschepke.wireguardautotunnel.util.extensions.scaledHeight
@Composable @Composable
fun AutoTunnelRowItem(settings: Settings, onToggle: () -> Unit) { fun AutoTunnelRowItem(appUiState: AppUiState, onToggle: () -> Unit) {
val context = LocalContext.current val context = LocalContext.current
val itemFocusRequester = remember { FocusRequester() } val itemFocusRequester = remember { FocusRequester() }
ExpandingRowListItem( ExpandingRowListItem(
@ -40,7 +35,7 @@ fun AutoTunnelRowItem(settings: Settings, onToggle: () -> Unit) {
Modifier Modifier
.size(16.dp.scaledHeight()).scale(1.5f), .size(16.dp.scaledHeight()).scale(1.5f),
tint = tint =
if (!settings.isAutoTunnelEnabled) { if (!appUiState.autoTunnelActive) {
Color.Gray Color.Gray
} else { } else {
SilverTree SilverTree
@ -50,10 +45,10 @@ fun AutoTunnelRowItem(settings: Settings, onToggle: () -> Unit) {
text = stringResource(R.string.auto_tunneling), text = stringResource(R.string.auto_tunneling),
trailing = { trailing = {
ScaledSwitch( ScaledSwitch(
settings.isAutoTunnelEnabled, appUiState.settings.isAutoTunnelEnabled,
onClick = { onClick = {
onToggle() onToggle()
} },
) )
}, },
onClick = { onClick = {

View File

@ -9,8 +9,6 @@ import androidx.compose.material3.FloatingActionButton
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
@Composable @Composable

View File

@ -182,14 +182,14 @@ fun TunnelRowItem(
ScaledSwitch( ScaledSwitch(
modifier = Modifier.focusRequester(itemFocusRequester), modifier = Modifier.focusRequester(itemFocusRequester),
checked = isActive, checked = isActive,
onClick = onSwitchClick onClick = onSwitchClick,
) )
} }
} else { } else {
ScaledSwitch( ScaledSwitch(
modifier = Modifier.focusRequester(itemFocusRequester), modifier = Modifier.focusRequester(itemFocusRequester),
checked = isActive, checked = isActive,
onClick = onSwitchClick onClick = onSwitchClick,
) )
} }
} }

View File

@ -46,14 +46,18 @@ fun TunnelStatisticsRow(statistics: TunnelStatistics?, tunnelConfig: TunnelConfi
Column( Column(
verticalArrangement = Arrangement.spacedBy(10.dp), verticalArrangement = Arrangement.spacedBy(10.dp),
) { ) {
Text(stringResource(R.string.peer).lowercase() + ": $peerId", style = MaterialTheme.typography.bodySmall) Text(
Text("tx: $peerTxMB MB", style = MaterialTheme.typography.bodySmall) stringResource(R.string.peer).lowercase() + ": $peerId",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.outline,
)
Text("tx: $peerTxMB MB", style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.outline)
} }
Column( Column(
verticalArrangement = Arrangement.spacedBy(10.dp), verticalArrangement = Arrangement.spacedBy(10.dp),
) { ) {
Text(stringResource(R.string.handshake) + ": $handshake", style = MaterialTheme.typography.bodySmall) Text(stringResource(R.string.handshake) + ": $handshake", style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.outline)
Text("rx: $peerRxMB MB", style = MaterialTheme.typography.bodySmall) Text("rx: $peerRxMB MB", style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.outline)
} }
} }
} }

View File

@ -4,15 +4,10 @@ import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ExperimentalLayoutApi import androidx.compose.foundation.layout.ExperimentalLayoutApi
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.systemBars
import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Edit import androidx.compose.material.icons.outlined.Edit
import androidx.compose.material.icons.outlined.NetworkPing import androidx.compose.material.icons.outlined.NetworkPing
@ -27,18 +22,11 @@ import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
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
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.input.key.Key
import androidx.compose.ui.input.key.KeyEventType
import androidx.compose.ui.input.key.key
import androidx.compose.ui.input.key.onKeyEvent
import androidx.compose.ui.input.key.type
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
@ -56,7 +44,6 @@ import com.zaneschepke.wireguardautotunnel.ui.screens.settings.autotunnel.compon
import com.zaneschepke.wireguardautotunnel.ui.theme.iconSize import com.zaneschepke.wireguardautotunnel.ui.theme.iconSize
import com.zaneschepke.wireguardautotunnel.util.extensions.scaledHeight import com.zaneschepke.wireguardautotunnel.util.extensions.scaledHeight
import com.zaneschepke.wireguardautotunnel.util.extensions.scaledWidth import com.zaneschepke.wireguardautotunnel.util.extensions.scaledWidth
import kotlinx.coroutines.delay
@OptIn(ExperimentalLayoutApi::class) @OptIn(ExperimentalLayoutApi::class)
@Composable @Composable
@ -84,7 +71,7 @@ fun OptionsScreen(optionsViewModel: OptionsViewModel = hiltViewModel(), appUiSta
) )
} }
}) })
} },
) { ) {
Column( Column(
horizontalAlignment = Alignment.Start, horizontalAlignment = Alignment.Start,
@ -101,7 +88,12 @@ fun OptionsScreen(optionsViewModel: OptionsViewModel = hiltViewModel(), appUiSta
listOf( listOf(
SelectionItem( SelectionItem(
Icons.Outlined.Star, Icons.Outlined.Star,
title = { Text(stringResource(R.string.primary_tunnel), style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface)) }, title = {
Text(
stringResource(R.string.primary_tunnel),
style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface),
)
},
description = { description = {
Text( Text(
stringResource(R.string.set_primary_tunnel), stringResource(R.string.set_primary_tunnel),
@ -114,7 +106,7 @@ fun OptionsScreen(optionsViewModel: OptionsViewModel = hiltViewModel(), appUiSta
onClick = { optionsViewModel.onTogglePrimaryTunnel(config) }, onClick = { optionsViewModel.onTogglePrimaryTunnel(config) },
) )
}, },
onClick = { optionsViewModel.onTogglePrimaryTunnel(config) } onClick = { optionsViewModel.onTogglePrimaryTunnel(config) },
), ),
SelectionItem( SelectionItem(
Icons.Outlined.PhoneAndroid, Icons.Outlined.PhoneAndroid,
@ -131,7 +123,7 @@ fun OptionsScreen(optionsViewModel: OptionsViewModel = hiltViewModel(), appUiSta
onClick = { optionsViewModel.onToggleIsMobileDataTunnel(config) }, onClick = { optionsViewModel.onToggleIsMobileDataTunnel(config) },
) )
}, },
onClick = { optionsViewModel.onToggleIsMobileDataTunnel(config) } onClick = { optionsViewModel.onToggleIsMobileDataTunnel(config) },
), ),
SelectionItem( SelectionItem(
Icons.Outlined.NetworkPing, Icons.Outlined.NetworkPing,
@ -147,7 +139,7 @@ fun OptionsScreen(optionsViewModel: OptionsViewModel = hiltViewModel(), appUiSta
onClick = { optionsViewModel.onToggleRestartOnPing(config) }, onClick = { optionsViewModel.onToggleRestartOnPing(config) },
) )
}, },
onClick = { optionsViewModel.onToggleRestartOnPing(config) } onClick = { optionsViewModel.onToggleRestartOnPing(config) },
), ),
SelectionItem( SelectionItem(
title = { title = {
@ -180,24 +172,25 @@ fun OptionsScreen(optionsViewModel: OptionsViewModel = hiltViewModel(), appUiSta
style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface), style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface),
) )
} }
} }
} }
}, },
description = { description = {
TrustedNetworkTextBox( TrustedNetworkTextBox(
config.tunnelNetworks, onDelete = { optionsViewModel.onDeleteRunSSID(it, config) }, config.tunnelNetworks,
onDelete = { optionsViewModel.onDeleteRunSSID(it, config) },
currentText = currentText, currentText = currentText,
onSave = { optionsViewModel.onSaveRunSSID(it, config) }, onSave = { optionsViewModel.onSaveRunSSID(it, config) },
onValueChange = { currentText = it }, onValueChange = { currentText = it },
supporting = { if(appUiState.generalState.isWildcardsEnabled) { supporting = {
WildcardsLabel() if (appUiState.settings.isWildcardsEnabled) {
}} WildcardsLabel()
}
},
) )
}, },
) ),
) ),
) )
} }
} }

View File

@ -32,7 +32,7 @@ constructor(
} }
fun onSaveRunSSID(ssid: String, tunnelConfig: TunnelConfig) = viewModelScope.launch { fun onSaveRunSSID(ssid: String, tunnelConfig: TunnelConfig) = viewModelScope.launch {
if(ssid.isBlank()) return@launch if (ssid.isBlank()) return@launch
val trimmed = ssid.trim() val trimmed = ssid.trim()
val tunnelsWithName = appDataRepository.tunnels.findByTunnelNetworksName(trimmed) val tunnelsWithName = appDataRepository.tunnels.findByTunnelNetworksName(trimmed)

View File

@ -1,26 +1,13 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.settings package com.zaneschepke.wireguardautotunnel.ui.screens.settings
import android.content.Context.POWER_SERVICE
import android.content.Intent
import android.net.Uri
import android.net.VpnService
import android.os.PowerManager
import android.provider.Settings
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.ActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity.RESULT_OK
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.focusable import androidx.compose.foundation.focusable
import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ExperimentalLayoutApi import androidx.compose.foundation.layout.ExperimentalLayoutApi
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.systemBars
import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
@ -49,7 +36,6 @@ import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.google.accompanist.permissions.ExperimentalPermissionsApi import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.zaneschepke.wireguardautotunnel.R import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelState import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelState
@ -63,13 +49,9 @@ import com.zaneschepke.wireguardautotunnel.ui.common.navigation.LocalFocusReques
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.LocalNavController import com.zaneschepke.wireguardautotunnel.ui.common.navigation.LocalNavController
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.snackbar.SnackbarController
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.ForwardButton import com.zaneschepke.wireguardautotunnel.ui.screens.settings.components.ForwardButton
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.components.LocationServicesDialog
import com.zaneschepke.wireguardautotunnel.ui.theme.topPadding import com.zaneschepke.wireguardautotunnel.ui.theme.topPadding
import com.zaneschepke.wireguardautotunnel.util.extensions.isRunningOnTv import com.zaneschepke.wireguardautotunnel.util.extensions.isRunningOnTv
import com.zaneschepke.wireguardautotunnel.util.extensions.launchAppSettings
import com.zaneschepke.wireguardautotunnel.util.extensions.launchNotificationSettings import com.zaneschepke.wireguardautotunnel.util.extensions.launchNotificationSettings
import com.zaneschepke.wireguardautotunnel.util.extensions.launchVpnSettings import com.zaneschepke.wireguardautotunnel.util.extensions.launchVpnSettings
import com.zaneschepke.wireguardautotunnel.util.extensions.scaledHeight import com.zaneschepke.wireguardautotunnel.util.extensions.scaledHeight
@ -91,114 +73,7 @@ fun SettingsScreen(viewModel: SettingsViewModel = hiltViewModel(), appViewModel:
val isRunningOnTv = remember { context.isRunningOnTv() } val isRunningOnTv = remember { context.isRunningOnTv() }
val interactionSource = remember { MutableInteractionSource() } val interactionSource = remember { MutableInteractionSource() }
val settingsUiState by viewModel.uiState.collectAsStateWithLifecycle()
var showVpnPermissionDialog by remember { mutableStateOf(false) }
var showLocationServicesAlertDialog by remember { mutableStateOf(false) }
var showAuthPrompt by remember { mutableStateOf(false) } var showAuthPrompt by remember { mutableStateOf(false) }
var showLocationDialog by remember { mutableStateOf(false) }
val startForResult =
rememberLauncherForActivityResult(
ActivityResultContracts.StartActivityForResult(),
) { result: ActivityResult ->
if (result.resultCode == RESULT_OK) {
result.data
// Handle the Intent
}
viewModel.setBatteryOptimizeDisableShown()
}
val vpnActivityResultState =
rememberLauncherForActivityResult(
ActivityResultContracts.StartActivityForResult(),
onResult = {
val accepted = (it.resultCode == RESULT_OK)
if (!accepted) {
showVpnPermissionDialog = true
}
},
)
fun isBatteryOptimizationsDisabled(): Boolean {
val pm = context.getSystemService(POWER_SERVICE) as PowerManager
return pm.isIgnoringBatteryOptimizations(context.packageName)
}
fun requestBatteryOptimizationsDisabled() {
val intent =
Intent().apply {
action = Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS
data = Uri.parse("package:${context.packageName}")
}
startForResult.launch(intent)
}
// fun handleAutoTunnelToggle() {
// if (!uiState.generalState.isBatteryOptimizationDisableShown &&
// !isBatteryOptimizationsDisabled() && !isRunningOnTv
// ) {
// return requestBatteryOptimizationsDisabled()
// }
// val intent = if (!uiState.settings.isKernelEnabled) {
// VpnService.prepare(context)
// } else {
// null
// }
// if (intent != null) return vpnActivityResultState.launch(intent)
// viewModel.onToggleAutoTunnel(context)
// }
// fun checkFineLocationGranted() {
// isBackgroundLocationGranted =
// if (!fineLocationState.status.isGranted) {
// false
// } else {
// viewModel.setLocationDisclosureShown()
// true
// }
// }
// if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
// if (
// isRunningOnTv &&
// Build.VERSION.SDK_INT == Build.VERSION_CODES.Q
// ) {
// checkFineLocationGranted()
// } else {
// val backgroundLocationState =
// rememberPermissionState(Manifest.permission.ACCESS_BACKGROUND_LOCATION)
// isBackgroundLocationGranted =
// if (!backgroundLocationState.status.isGranted) {
// false
// } else {
// SideEffect { viewModel.setLocationDisclosureShown() }
// true
// }
// }
// }
//
// if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.P) {
// checkFineLocationGranted()
// }
BackgroundLocationDialog(
showLocationDialog,
onDismiss = { showLocationDialog = false },
onAttest = { showLocationDialog = false },
)
// LocationServicesDialog(
// showLocationServicesAlertDialog,
// onDismiss = { showVpnPermissionDialog = false },
// onAttest = { handleAutoTunnelToggle() },
// )
VpnDeniedDialog(showVpnPermissionDialog, onDismiss = { showVpnPermissionDialog = false })
if (showAuthPrompt) { if (showAuthPrompt) {
AuthorizationPrompt( AuthorizationPrompt(
@ -231,12 +106,18 @@ fun SettingsScreen(viewModel: SettingsViewModel = hiltViewModel(), appViewModel:
.padding(top = topPadding) .padding(top = topPadding)
.padding(bottom = 40.dp.scaledHeight()) .padding(bottom = 40.dp.scaledHeight())
.padding(horizontal = 24.dp.scaledWidth()) .padding(horizontal = 24.dp.scaledWidth())
.then(if(!isRunningOnTv) Modifier.clickable( .then(
indication = null, if (!isRunningOnTv) {
interactionSource = interactionSource, Modifier.clickable(
) { indication = null,
focusManager.clearFocus() interactionSource = interactionSource,
} else Modifier) ) {
focusManager.clearFocus()
}
} else {
Modifier
},
),
) { ) {
SurfaceSelectionGroupButton( SurfaceSelectionGroupButton(
listOf( listOf(
@ -250,73 +131,78 @@ fun SettingsScreen(viewModel: SettingsViewModel = hiltViewModel(), appViewModel:
) )
}, },
onClick = { onClick = {
if(!uiState.generalState.isLocationDisclosureShown) return@SelectionItem navController.navigate(Route.LocationDisclosure) if (!uiState.generalState.isLocationDisclosureShown) return@SelectionItem navController.navigate(Route.LocationDisclosure)
navController.navigate(Route.AutoTunnel) navController.navigate(Route.AutoTunnel)
}, },
trailing = { trailing = {
ForwardButton(Modifier.focusable().focusRequester(rootFocusRequester)) { navController.navigate(Route.AutoTunnel) } ForwardButton(Modifier.focusable().focusRequester(rootFocusRequester)) { navController.navigate(Route.AutoTunnel) }
}, },
) ),
) ),
) )
SurfaceSelectionGroupButton( SurfaceSelectionGroupButton(
buildList { buildList {
if (!isRunningOnTv) addAll( if (!isRunningOnTv) {
listOf( addAll(
SelectionItem( listOf(
Icons.Filled.AppShortcut, SelectionItem(
{ Icons.Filled.AppShortcut,
ScaledSwitch( {
uiState.settings.isShortcutsEnabled, ScaledSwitch(
onClick = { viewModel.onToggleShortcutsEnabled() }, uiState.settings.isShortcutsEnabled,
) onClick = { viewModel.onToggleShortcutsEnabled() },
}, )
title = { },
Text( title = {
stringResource(R.string.enabled_app_shortcuts), Text(
style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface)) stringResource(R.string.enabled_app_shortcuts),
}, style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface),
onClick = { viewModel.onToggleShortcutsEnabled() } )
},
onClick = { viewModel.onToggleShortcutsEnabled() },
),
SelectionItem(
Icons.Outlined.VpnLock,
{
ScaledSwitch(
enabled = !(
(
uiState.settings.isTunnelOnWifiEnabled ||
uiState.settings.isTunnelOnEthernetEnabled ||
uiState.settings.isTunnelOnMobileDataEnabled
) &&
uiState.settings.isAutoTunnelEnabled
),
onClick = { viewModel.onToggleAlwaysOnVPN() },
checked = uiState.settings.isAlwaysOnVpnEnabled,
)
},
title = {
Text(
stringResource(R.string.always_on_vpn_support),
style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface),
)
},
onClick = { viewModel.onToggleAlwaysOnVPN() },
),
SelectionItem(
Icons.Outlined.AdminPanelSettings,
title = {
Text(
stringResource(R.string.kill_switch),
style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface),
)
},
onClick = {
context.launchVpnSettings()
},
trailing = {
ForwardButton { context.launchVpnSettings() }
},
),
), ),
SelectionItem(
Icons.Outlined.VpnLock,
{
ScaledSwitch(
enabled = !(
(
uiState.settings.isTunnelOnWifiEnabled ||
uiState.settings.isTunnelOnEthernetEnabled ||
uiState.settings.isTunnelOnMobileDataEnabled
) &&
uiState.settings.isAutoTunnelEnabled
),
onClick = { viewModel.onToggleAlwaysOnVPN() },
checked = uiState.settings.isAlwaysOnVpnEnabled,
)
},
title = {
Text(
stringResource(R.string.always_on_vpn_support),
style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface))
},
onClick = { viewModel.onToggleAlwaysOnVPN() }
),
SelectionItem(
Icons.Outlined.AdminPanelSettings,
title = {
Text(
stringResource(R.string.kill_switch),
style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface))
},
onClick = {
context.launchVpnSettings()
},
trailing = {
ForwardButton { context.launchVpnSettings() }
},
)
) )
) }
add( add(
SelectionItem( SelectionItem(
Icons.Outlined.Restore, Icons.Outlined.Restore,
@ -329,25 +215,27 @@ fun SettingsScreen(viewModel: SettingsViewModel = hiltViewModel(), appViewModel:
title = { title = {
Text( Text(
stringResource(R.string.restart_at_boot), stringResource(R.string.restart_at_boot),
style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface)) style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface),
)
}, },
onClick = { viewModel.onToggleRestartAtBoot() } onClick = { viewModel.onToggleRestartAtBoot() },
) ),
) )
} },
) )
SurfaceSelectionGroupButton( SurfaceSelectionGroupButton(
listOf(SelectionItem( listOf(
Icons.AutoMirrored.Outlined.ViewQuilt, SelectionItem(
title = { Text(stringResource(R.string.appearance), style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface)) }, Icons.AutoMirrored.Outlined.ViewQuilt,
onClick = { title = { Text(stringResource(R.string.appearance), style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface)) },
navController.navigate(Route.Appearance) onClick = {
}, navController.navigate(Route.Appearance)
trailing = { },
ForwardButton { navController.navigate(Route.Appearance) } trailing = {
}, ForwardButton { navController.navigate(Route.Appearance) }
), },
),
SelectionItem( SelectionItem(
Icons.Outlined.Notifications, Icons.Outlined.Notifications,
title = { Text(stringResource(R.string.notifications), style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface)) }, title = { Text(stringResource(R.string.notifications), style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface)) },
@ -360,7 +248,12 @@ fun SettingsScreen(viewModel: SettingsViewModel = hiltViewModel(), appViewModel:
), ),
SelectionItem( SelectionItem(
Icons.Outlined.Pin, Icons.Outlined.Pin,
title = { Text(stringResource(R.string.enable_app_lock), style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface)) }, title = {
Text(
stringResource(R.string.enable_app_lock),
style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface),
)
},
trailing = { trailing = {
ScaledSwitch( ScaledSwitch(
uiState.generalState.isPinLockEnabled, uiState.generalState.isPinLockEnabled,
@ -374,370 +267,67 @@ fun SettingsScreen(viewModel: SettingsViewModel = hiltViewModel(), appViewModel:
}, },
) )
}, },
onClick = { if (uiState.generalState.isPinLockEnabled) {
appViewModel.onPinLockDisabled()
} else {
PinManager.initialize(context)
navController.navigate(Route.Lock)
} }
)
))
if(!isRunningOnTv) SurfaceSelectionGroupButton(listOf(
SelectionItem(
Icons.Outlined.Code,
title = { Text(stringResource(R.string.kernel), style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface)) },
description = {
Text(
stringResource(R.string.use_kernel),
style = MaterialTheme.typography.bodySmall.copy(MaterialTheme.colorScheme.outline),
)
},
trailing = {
ScaledSwitch(
uiState.settings.isKernelEnabled,
onClick = { viewModel.onToggleKernelMode() },
enabled = !(
uiState.settings.isAutoTunnelEnabled ||
uiState.settings.isAlwaysOnVpnEnabled ||
(uiState.vpnState.status == TunnelState.UP) ||
!settingsUiState.isKernelAvailable
),
)
},
onClick = {
viewModel.onToggleKernelMode()
}
),
))
if(!isRunningOnTv) SurfaceSelectionGroupButton(
listOf(
SelectionItem(
Icons.Outlined.FolderZip,
title = { Text(stringResource(R.string.export_configs), style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface)) },
onClick = { onClick = {
if (uiState.tunnels.isEmpty()) return@SelectionItem context.showToast(R.string.tunnel_required) if (uiState.generalState.isPinLockEnabled) {
showAuthPrompt = true appViewModel.onPinLockDisabled()
} else {
PinManager.initialize(context)
navController.navigate(Route.Lock)
}
}, },
), ),
) ),
) )
if (!isRunningOnTv) {
SurfaceSelectionGroupButton(
listOf(
SelectionItem(
Icons.Outlined.Code,
title = { Text(stringResource(R.string.kernel), style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface)) },
description = {
Text(
stringResource(R.string.use_kernel),
style = MaterialTheme.typography.bodySmall.copy(MaterialTheme.colorScheme.outline),
)
},
trailing = {
ScaledSwitch(
uiState.settings.isKernelEnabled,
onClick = { viewModel.onToggleKernelMode() },
enabled = !(
uiState.settings.isAutoTunnelEnabled ||
uiState.settings.isAlwaysOnVpnEnabled ||
(uiState.vpnState.status == TunnelState.UP)
),
)
},
onClick = {
viewModel.onToggleKernelMode()
},
),
),
)
}
if (!isRunningOnTv) {
SurfaceSelectionGroupButton(
listOf(
// Surface( SelectionItem(
// tonalElevation = 2.dp, Icons.Outlined.FolderZip,
// shadowElevation = 2.dp, title = {
// shape = RoundedCornerShape(12.dp), Text(
// color = MaterialTheme.colorScheme.surface, stringResource(R.string.export_configs),
// modifier = style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface),
// ( )
// if (isRunningOnTv) { },
// Modifier onClick = {
// .height(IntrinsicSize.Min) if (uiState.tunnels.isEmpty()) return@SelectionItem context.showToast(R.string.tunnel_required)
// .fillMaxWidth(fillMaxWidth) showAuthPrompt = true
// .padding(top = 10.dp) },
// } else { ),
// Modifier ),
// .fillMaxWidth(fillMaxWidth) )
// .padding(top = 20.dp) }
// }
// )
// .padding(bottom = 10.dp),
// ) {
// Column(
// horizontalAlignment = Alignment.Start,
// verticalArrangement = Arrangement.Top,
// modifier = Modifier.padding(15.dp),
// ) {
// SectionTitle(
// title = stringResource(id = R.string.auto_tunneling),
// padding = screenPadding,
// )
// ConfigurationToggle(
// stringResource(id = R.string.tunnel_on_wifi),
// enabled = !uiState.settings.isAlwaysOnVpnEnabled,
// checked = uiState.settings.isTunnelOnWifiEnabled,
// onCheckChanged = { checked ->
// if (!checked || settingsUiState.isRooted) viewModel.onToggleTunnelOnWifi().also { return@ConfigurationToggle }
// onAutoTunnelWifiChecked()
// },
// modifier =
// if (uiState.settings.isAutoTunnelEnabled) {
// Modifier
// } else {
// Modifier
// .focusRequester(focusRequester)
// },
// )
// if (uiState.settings.isTunnelOnWifiEnabled) {
// Column {
// FlowRow(
// modifier =
// Modifier
// .padding(screenPadding)
// .fillMaxWidth(),
// horizontalArrangement = Arrangement.spacedBy(5.dp),
// ) {
// uiState.settings.trustedNetworkSSIDs.forEach { ssid ->
// ClickableIconButton(
// onClick = {
// if (isRunningOnTv) {
// focusRequester.requestFocus()
// viewModel.onDeleteTrustedSSID(ssid)
// }
// },
// onIconClick = {
// if (isRunningOnTv) focusRequester.requestFocus()
// viewModel.onDeleteTrustedSSID(ssid)
// },
// text = ssid,
// icon = Icons.Filled.Close,
// )
// }
// if (uiState.settings.trustedNetworkSSIDs.isEmpty()) {
// Text(
// stringResource(R.string.none),
// fontStyle = FontStyle.Italic,
// style = MaterialTheme.typography.bodySmall,
// color = MaterialTheme.colorScheme.onSurface,
// )
// }
// }
// OutlinedTextField(
// value = currentText,
// onValueChange = { currentText = it },
// label = { Text(stringResource(R.string.add_trusted_ssid)) },
// modifier =
// Modifier
// .padding(
// start = screenPadding,
// top = 5.dp,
// bottom = 10.dp,
// ),
// supportingText = { WildcardSupportingLabel { context.openWebUrl(it) } },
// maxLines = 1,
// keyboardOptions =
// KeyboardOptions(
// capitalization = KeyboardCapitalization.None,
// imeAction = ImeAction.Done,
// ),
// keyboardActions = KeyboardActions(onDone = { saveTrustedSSID() }),
// trailingIcon = {
// if (currentText != "") {
// IconButton(onClick = { saveTrustedSSID() }) {
// Icon(
// imageVector = Icons.Outlined.Add,
// contentDescription =
// if (currentText == "") {
// stringResource(
// id =
// R.string
// .trusted_ssid_empty_description,
// )
// } else {
// stringResource(
// id =
// R.string
// .trusted_ssid_value_description,
// )
// },
// tint = MaterialTheme.colorScheme.primary,
// )
// }
// }
// },
// )
// }
// }
// ConfigurationToggle(
// stringResource(R.string.tunnel_mobile_data),
// enabled = !uiState.settings.isAlwaysOnVpnEnabled,
// checked = uiState.settings.isTunnelOnMobileDataEnabled,
// onCheckChanged = { viewModel.onToggleTunnelOnMobileData() },
// )
// ConfigurationToggle(
// stringResource(id = R.string.tunnel_on_ethernet),
// enabled = !uiState.settings.isAlwaysOnVpnEnabled,
// checked = uiState.settings.isTunnelOnEthernetEnabled,
// onCheckChanged = { viewModel.onToggleTunnelOnEthernet() },
// )
// ConfigurationToggle(
// stringResource(R.string.restart_on_ping),
// checked = uiState.settings.isPingEnabled,
// onCheckChanged = { viewModel.onToggleRestartOnPing() },
// )
// Row(
// verticalAlignment = Alignment.CenterVertically,
// modifier =
// (
// if (!uiState.settings.isAutoTunnelEnabled) {
// Modifier
// } else {
// Modifier.focusRequester(
// focusRequester,
// )
// }
// )
// .fillMaxSize()
// .padding(top = 5.dp),
// horizontalArrangement = Arrangement.Center,
// ) {
// TextButton(
// onClick = {
// if (uiState.tunnels.isEmpty()) return@TextButton context.showToast(R.string.tunnel_required)
// handleAutoTunnelToggle()
// },
// ) {
// val autoTunnelButtonText =
// if (uiState.settings.isAutoTunnelEnabled) {
// stringResource(R.string.disable_auto_tunnel)
// } else {
// stringResource(id = R.string.enable_auto_tunnel)
// }
// Text(autoTunnelButtonText)
// }
// }
// }
// }
// Surface(
// tonalElevation = 2.dp,
// shadowElevation = 2.dp,
// shape = RoundedCornerShape(12.dp),
// color = MaterialTheme.colorScheme.surface,
// modifier =
// Modifier
// .fillMaxWidth(fillMaxWidth)
// .padding(vertical = 10.dp),
// ) {
// Column(
// horizontalAlignment = Alignment.Start,
// verticalArrangement = Arrangement.Top,
// modifier = Modifier.padding(15.dp),
// ) {
// SectionTitle(
// title = stringResource(id = R.string.backend),
// padding = screenPadding,
// )
// ConfigurationToggle(
// stringResource(R.string.use_kernel),
// enabled =
// !(
// uiState.settings.isAutoTunnelEnabled ||
// uiState.settings.isAlwaysOnVpnEnabled ||
// (uiState.vpnState.status == TunnelState.UP) ||
// !settingsUiState.isKernelAvailable
// ),
// checked = uiState.settings.isKernelEnabled,
// onCheckChanged = {
// viewModel.onToggleKernelMode()
// },
// )
// Row(
// verticalAlignment = Alignment.CenterVertically,
// modifier =
// Modifier
// .fillMaxSize()
// .padding(top = 5.dp),
// horizontalArrangement = Arrangement.Center,
// ) {
// TextButton(
// onClick = {
// viewModel.onRequestRoot()
// },
// ) {
// Text(stringResource(R.string.request_root))
// }
// }
// }
// }
// Surface(
// tonalElevation = 2.dp,
// shadowElevation = 2.dp,
// shape = RoundedCornerShape(12.dp),
// color = MaterialTheme.colorScheme.surface,
// modifier =
// Modifier
// .fillMaxWidth(fillMaxWidth)
// .padding(vertical = 10.dp)
// .padding(bottom = 10.dp),
// ) {
// Column(
// horizontalAlignment = Alignment.Start,
// verticalArrangement = Arrangement.Top,
// modifier = Modifier.padding(15.dp),
// ) {
// SectionTitle(
// title = stringResource(id = R.string.other),
// padding = screenPadding,
// )
// if (!isRunningOnTv) {
// ConfigurationToggle(
// stringResource(R.string.always_on_vpn_support),
// enabled = !(
// (
// uiState.settings.isTunnelOnWifiEnabled ||
// uiState.settings.isTunnelOnEthernetEnabled ||
// uiState.settings.isTunnelOnMobileDataEnabled
// ) &&
// uiState.settings.isAutoTunnelEnabled
// ),
// checked = uiState.settings.isAlwaysOnVpnEnabled,
// onCheckChanged = { viewModel.onToggleAlwaysOnVPN() },
// )
// ConfigurationToggle(
// stringResource(R.string.enabled_app_shortcuts),
// enabled = true,
// checked = uiState.settings.isShortcutsEnabled,
// onCheckChanged = { viewModel.onToggleShortcutsEnabled() },
// )
// }
// ConfigurationToggle(
// stringResource(R.string.restart_at_boot),
// enabled = true,
// checked = uiState.settings.isRestoreOnBootEnabled,
// onCheckChanged = {
// viewModel.onToggleRestartAtBoot()
// },
// )
// ConfigurationToggle(
// stringResource(R.string.enable_app_lock),
// enabled = true,
// checked = uiState.generalState.isPinLockEnabled,
// onCheckChanged = {
// if (uiState.generalState.isPinLockEnabled) {
// appViewModel.onPinLockDisabled()
// } else {
// // TODO may want to show a dialog before proceeding in the future
// PinManager.initialize(WireGuardAutoTunnel.instance)
// navController.navigate(Route.Lock)
// }
// },
// )
// if (!isRunningOnTv) {
// Row(
// verticalAlignment = Alignment.CenterVertically,
// modifier =
// Modifier
// .fillMaxSize()
// .padding(top = 5.dp),
// horizontalArrangement = Arrangement.Center,
// ) {
// TextButton(
// enabled = !didExportFiles,
// onClick = {
// if (uiState.tunnels.isEmpty()) return@TextButton context.showToast(R.string.tunnel_required)
// showAuthPrompt = true
// },
// ) {
// Text(stringResource(R.string.export_configs))
// }
// }
// }
// }
// }
} }
} }

View File

@ -1,6 +0,0 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.settings
data class SettingsUiState(
val isRooted: Boolean = false,
val isKernelAvailable: Boolean = false,
)

View File

@ -10,19 +10,14 @@ 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.ui.common.snackbar.SnackbarController 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.StringValue import com.zaneschepke.wireguardautotunnel.util.StringValue
import com.zaneschepke.wireguardautotunnel.util.extensions.launchShareFile import com.zaneschepke.wireguardautotunnel.util.extensions.launchShareFile
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.SharingStarted import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import java.time.Instant import java.time.Instant
@ -39,16 +34,6 @@ constructor(
@IoDispatcher private val ioDispatcher: CoroutineDispatcher, @IoDispatcher private val ioDispatcher: CoroutineDispatcher,
) : ViewModel() { ) : ViewModel() {
private val _uiState = MutableStateFlow(SettingsUiState())
val uiState = _uiState.onStart {
_uiState.update {
it.copy(isKernelAvailable = isKernelSupported(), isRooted = isRooted())
}
}.stateIn(
viewModelScope,
SharingStarted.WhileSubscribed(Constants.SUBSCRIPTION_TIMEOUT),
SettingsUiState(),
)
private val settings = appDataRepository.settings.getSettingsFlow() private val settings = appDataRepository.settings.getSettingsFlow()
.stateIn(viewModelScope, SharingStarted.Eagerly, Settings()) .stateIn(viewModelScope, SharingStarted.Eagerly, Settings())
@ -56,10 +41,6 @@ constructor(
appDataRepository.appState.setLocationDisclosureShown(true) appDataRepository.appState.setLocationDisclosureShown(true)
} }
fun setBatteryOptimizeDisableShown() = viewModelScope.launch {
appDataRepository.appState.setBatteryOptimizationDisableShown(true)
}
fun onToggleAlwaysOnVPN() = viewModelScope.launch { fun onToggleAlwaysOnVPN() = viewModelScope.launch {
with(settings.value) { with(settings.value) {
appDataRepository.settings.save( appDataRepository.settings.save(
@ -90,23 +71,11 @@ constructor(
} }
} }
fun onToggleAmnezia() = viewModelScope.launch {
with(settings.value) {
if (isKernelEnabled) {
saveKernelMode(false)
}
appDataRepository.settings.save(
copy(
isAmneziaEnabled = !isAmneziaEnabled,
),
)
}
}
fun onToggleKernelMode() = viewModelScope.launch { fun onToggleKernelMode() = viewModelScope.launch {
with(settings.value) { with(settings.value) {
if (!isKernelEnabled) { if (!isKernelEnabled) {
requestRoot().onSuccess { requestRoot().onSuccess {
if (!isKernelSupported()) return@onSuccess SnackbarController.showMessage(StringValue.StringResource(R.string.kernel_not_supported))
appDataRepository.settings.save( appDataRepository.settings.save(
copy( copy(
isKernelEnabled = true, isKernelEnabled = true,
@ -136,17 +105,6 @@ constructor(
} }
} }
private suspend fun isRooted(): Boolean {
return try {
withContext(ioDispatcher) {
rootShell.get().start()
}
true
} catch (_: Exception) {
false
}
}
private suspend fun requestRoot(): Result<Unit> { private suspend fun requestRoot(): Result<Unit> {
return withContext(ioDispatcher) { return withContext(ioDispatcher) {
kotlin.runCatching { kotlin.runCatching {
@ -158,10 +116,6 @@ constructor(
} }
} }
fun onRequestRoot() = viewModelScope.launch {
requestRoot()
}
fun exportAllConfigs(context: Context) = viewModelScope.launch { fun exportAllConfigs(context: Context) = viewModelScope.launch {
kotlin.runCatching { kotlin.runCatching {
val shareFile = fileUtils.createNewShareFile("wg-export_${Instant.now().epochSecond}.zip") val shareFile = fileUtils.createNewShareFile("wg-export_${Instant.now().epochSecond}.zip")

View File

@ -32,8 +32,8 @@ fun AppearanceScreen() {
Scaffold( Scaffold(
topBar = { topBar = {
TopNavBar(stringResource(R.string.appearance)) TopNavBar(stringResource(R.string.appearance))
} },
){ ) {
Column( Column(
horizontalAlignment = Alignment.Start, horizontalAlignment = Alignment.Start,
verticalArrangement = Arrangement.spacedBy(24.dp.scaledHeight(), Alignment.Top), verticalArrangement = Arrangement.spacedBy(24.dp.scaledHeight(), Alignment.Top),
@ -51,7 +51,7 @@ fun AppearanceScreen() {
onClick = { navController.navigate(Route.Language) }, onClick = { navController.navigate(Route.Language) },
trailing = { trailing = {
ForwardButton { navController.navigate(Route.Language) } ForwardButton { navController.navigate(Route.Language) }
} },
), ),
), ),
) )
@ -63,7 +63,7 @@ fun AppearanceScreen() {
onClick = { navController.navigate(Route.Display) }, onClick = { navController.navigate(Route.Display) },
trailing = { trailing = {
ForwardButton { navController.navigate(Route.Display) } ForwardButton { navController.navigate(Route.Display) }
} },
), ),
), ),
) )

View File

@ -2,11 +2,8 @@ package com.zaneschepke.wireguardautotunnel.ui.screens.settings.appearance.displ
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.systemBars
import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
@ -24,11 +21,10 @@ import com.zaneschepke.wireguardautotunnel.util.extensions.scaledWidth
@Composable @Composable
fun DisplayScreen(appUiState: AppUiState, viewModel: DisplayViewModel = hiltViewModel()) { fun DisplayScreen(appUiState: AppUiState, viewModel: DisplayViewModel = hiltViewModel()) {
Scaffold( Scaffold(
topBar = { topBar = {
TopNavBar(stringResource(R.string.display_theme)) TopNavBar(stringResource(R.string.display_theme))
} },
) { ) {
Column( Column(
horizontalAlignment = Alignment.Start, horizontalAlignment = Alignment.Start,

View File

@ -12,7 +12,7 @@ import javax.inject.Inject
class DisplayViewModel class DisplayViewModel
@Inject @Inject
constructor( constructor(
private val appStateRepository: AppStateRepository private val appStateRepository: AppStateRepository,
) : ViewModel() { ) : ViewModel() {
fun onThemeChange(theme: Theme) = viewModelScope.launch { fun onThemeChange(theme: Theme) = viewModelScope.launch {

View File

@ -6,7 +6,6 @@ import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.navigationBars import androidx.compose.foundation.layout.navigationBars
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.systemBars
import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
@ -69,7 +68,7 @@ fun LanguageScreen(localeStorage: LocaleStorage) {
Scaffold( Scaffold(
topBar = { topBar = {
TopNavBar(stringResource(R.string.language)) TopNavBar(stringResource(R.string.language))
} },
) { ) {
LazyColumn( LazyColumn(
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,

View File

@ -1,6 +1,7 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.settings.autotunnel package com.zaneschepke.wireguardautotunnel.ui.screens.settings.autotunnel
import android.Manifest import android.Manifest
import android.os.Build
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ExperimentalLayoutApi import androidx.compose.foundation.layout.ExperimentalLayoutApi
@ -11,6 +12,7 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Code
import androidx.compose.material.icons.outlined.Filter1 import androidx.compose.material.icons.outlined.Filter1
import androidx.compose.material.icons.outlined.NetworkPing import androidx.compose.material.icons.outlined.NetworkPing
import androidx.compose.material.icons.outlined.Security import androidx.compose.material.icons.outlined.Security
@ -31,7 +33,6 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontStyle
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 com.google.accompanist.permissions.ExperimentalPermissionsApi import com.google.accompanist.permissions.ExperimentalPermissionsApi
@ -45,9 +46,12 @@ import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SurfaceSelec
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.TopNavBar import com.zaneschepke.wireguardautotunnel.ui.common.navigation.TopNavBar
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.autotunnel.components.TrustedNetworkTextBox import com.zaneschepke.wireguardautotunnel.ui.screens.settings.autotunnel.components.TrustedNetworkTextBox
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.autotunnel.components.WildcardsLabel import com.zaneschepke.wireguardautotunnel.ui.screens.settings.autotunnel.components.WildcardsLabel
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.components.BackgroundLocationDialog
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.components.LearnMoreLinkLabel import com.zaneschepke.wireguardautotunnel.ui.screens.settings.components.LearnMoreLinkLabel
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.components.LocationServicesDialog
import com.zaneschepke.wireguardautotunnel.ui.theme.iconSize import com.zaneschepke.wireguardautotunnel.ui.theme.iconSize
import com.zaneschepke.wireguardautotunnel.util.extensions.isLocationServicesEnabled import com.zaneschepke.wireguardautotunnel.util.extensions.isLocationServicesEnabled
import com.zaneschepke.wireguardautotunnel.util.extensions.isRunningOnTv
import com.zaneschepke.wireguardautotunnel.util.extensions.openWebUrl import com.zaneschepke.wireguardautotunnel.util.extensions.openWebUrl
import com.zaneschepke.wireguardautotunnel.util.extensions.scaledHeight import com.zaneschepke.wireguardautotunnel.util.extensions.scaledHeight
import com.zaneschepke.wireguardautotunnel.util.extensions.scaledWidth import com.zaneschepke.wireguardautotunnel.util.extensions.scaledWidth
@ -63,11 +67,12 @@ fun AutoTunnelScreen(uiState: AppUiState, viewModel: AutoTunnelViewModel = hiltV
var showLocationServicesAlertDialog by remember { mutableStateOf(false) } var showLocationServicesAlertDialog by remember { mutableStateOf(false) }
var showLocationDialog by remember { mutableStateOf(false) } var showLocationDialog by remember { mutableStateOf(false) }
LaunchedEffect(uiState.settings.trustedNetworkSSIDs) { fun checkFineLocationGranted() {
currentText = "" isBackgroundLocationGranted = fineLocationState.status.isGranted
} }
fun onAutoTunnelWifiChecked() { fun onAutoTunnelWifiChecked() {
if (uiState.settings.isTunnelOnWifiEnabled) viewModel.onToggleTunnelOnWifi().also { return }
when (false) { when (false) {
isBackgroundLocationGranted -> showLocationDialog = true isBackgroundLocationGranted -> showLocationDialog = true
fineLocationState.status.isGranted -> showLocationDialog = true fineLocationState.status.isGranted -> showLocationDialog = true
@ -79,11 +84,39 @@ fun AutoTunnelScreen(uiState: AppUiState, viewModel: AutoTunnelViewModel = hiltV
} }
} }
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.P) checkFineLocationGranted()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
if (context.isRunningOnTv() && Build.VERSION.SDK_INT == Build.VERSION_CODES.Q) {
checkFineLocationGranted()
} else {
val backgroundLocationState = rememberPermissionState(Manifest.permission.ACCESS_BACKGROUND_LOCATION)
isBackgroundLocationGranted = backgroundLocationState.status.isGranted
}
}
LaunchedEffect(uiState.settings.trustedNetworkSSIDs) {
currentText = ""
}
LocationServicesDialog(
showLocationServicesAlertDialog,
onDismiss = { showLocationServicesAlertDialog = false },
onAttest = {
viewModel.onToggleTunnelOnWifi()
},
)
BackgroundLocationDialog(
showLocationDialog,
onDismiss = { showLocationDialog = false },
onAttest = { showLocationDialog = false },
)
Scaffold( Scaffold(
contentWindowInsets = WindowInsets(0.dp), contentWindowInsets = WindowInsets(0.dp),
topBar = { topBar = {
TopNavBar(stringResource(R.string.auto_tunneling)) TopNavBar(stringResource(R.string.auto_tunneling))
} },
) { ) {
Column( Column(
horizontalAlignment = Alignment.Start, horizontalAlignment = Alignment.Start,
@ -97,34 +130,60 @@ fun AutoTunnelScreen(uiState: AppUiState, viewModel: AutoTunnelViewModel = hiltV
) { ) {
SurfaceSelectionGroupButton( SurfaceSelectionGroupButton(
buildList { buildList {
add( addAll(
SelectionItem( listOf(
Icons.Outlined.Wifi, SelectionItem(
title = { Icons.Outlined.Wifi,
Text( title = {
stringResource(R.string.tunnel_on_wifi), Text(
style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface) stringResource(R.string.tunnel_on_wifi),
) style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface),
}, )
description = { },
}, description = {
trailing = { },
ScaledSwitch( trailing = {
enabled = !uiState.settings.isAlwaysOnVpnEnabled, ScaledSwitch(
checked = uiState.settings.isTunnelOnWifiEnabled, enabled = !uiState.settings.isAlwaysOnVpnEnabled,
onClick = { checked = uiState.settings.isTunnelOnWifiEnabled,
if (!uiState.settings.isTunnelOnWifiEnabled || uiState.isRooted) viewModel.onToggleTunnelOnWifi() onClick = {
.also { return@ScaledSwitch } if (uiState.settings.isWifiNameByShellEnabled) viewModel.onToggleTunnelOnWifi().also { return@ScaledSwitch }
onAutoTunnelWifiChecked() onAutoTunnelWifiChecked()
}, },
) )
}, },
onClick = { onClick = {
if (!uiState.settings.isTunnelOnWifiEnabled || uiState.isRooted) viewModel.onToggleTunnelOnWifi() if (uiState.settings.isWifiNameByShellEnabled) viewModel.onToggleTunnelOnWifi().also { return@SelectionItem }
.also { return@SelectionItem } onAutoTunnelWifiChecked()
onAutoTunnelWifiChecked() },
} ),
) SelectionItem(
Icons.Outlined.Code,
title = {
Text(
stringResource(R.string.wifi_name_via_shell),
style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface),
)
},
description = {
Text(
stringResource(R.string.use_root_shell_for_wifi),
style = MaterialTheme.typography.bodySmall.copy(MaterialTheme.colorScheme.outline),
)
},
trailing = {
ScaledSwitch(
checked = uiState.settings.isWifiNameByShellEnabled,
onClick = {
viewModel.onRootShellWifiToggle()
},
)
},
onClick = {
viewModel.onRootShellWifiToggle()
},
),
),
) )
if (uiState.settings.isTunnelOnWifiEnabled) { if (uiState.settings.isTunnelOnWifiEnabled) {
addAll( addAll(
@ -134,15 +193,15 @@ fun AutoTunnelScreen(uiState: AppUiState, viewModel: AutoTunnelViewModel = hiltV
title = { title = {
Text( Text(
stringResource(R.string.use_wildcards), stringResource(R.string.use_wildcards),
style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface) style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface),
) )
}, },
description = { description = {
LearnMoreLinkLabel({context.openWebUrl(it)}, stringResource(id = R.string.docs_wildcards)) LearnMoreLinkLabel({ context.openWebUrl(it) }, stringResource(id = R.string.docs_wildcards))
}, },
trailing = { trailing = {
ScaledSwitch( ScaledSwitch(
checked = uiState.generalState.isWildcardsEnabled, checked = uiState.settings.isWildcardsEnabled,
onClick = { onClick = {
viewModel.onToggleWildcards() viewModel.onToggleWildcards()
}, },
@ -150,7 +209,7 @@ fun AutoTunnelScreen(uiState: AppUiState, viewModel: AutoTunnelViewModel = hiltV
}, },
onClick = { onClick = {
viewModel.onToggleWildcards() viewModel.onToggleWildcards()
} },
), ),
SelectionItem( SelectionItem(
title = { title = {
@ -183,87 +242,89 @@ fun AutoTunnelScreen(uiState: AppUiState, viewModel: AutoTunnelViewModel = hiltV
style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface), style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface),
) )
} }
} }
} }
}, },
description = { description = {
TrustedNetworkTextBox( TrustedNetworkTextBox(
uiState.settings.trustedNetworkSSIDs, onDelete = viewModel::onDeleteTrustedSSID, uiState.settings.trustedNetworkSSIDs,
onDelete = viewModel::onDeleteTrustedSSID,
currentText = currentText, currentText = currentText,
onSave = viewModel::onSaveTrustedSSID, onSave = viewModel::onSaveTrustedSSID,
onValueChange = { currentText = it }, onValueChange = { currentText = it },
supporting = { if(uiState.generalState.isWildcardsEnabled) { supporting = {
WildcardsLabel() if (uiState.settings.isWildcardsEnabled) {
}} WildcardsLabel()
}
},
) )
}, },
) ),
)) ),
)
} }
} },
) )
SurfaceSelectionGroupButton( SurfaceSelectionGroupButton(
listOf( listOf(
SelectionItem( SelectionItem(
Icons.Outlined.SignalCellular4Bar, Icons.Outlined.SignalCellular4Bar,
title = { title = {
Text( Text(
stringResource(R.string.tunnel_mobile_data), stringResource(R.string.tunnel_mobile_data),
style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface), style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface),
)
},
trailing = {
ScaledSwitch(
enabled = !uiState.settings.isAlwaysOnVpnEnabled,
checked = uiState.settings.isTunnelOnMobileDataEnabled,
onClick = { viewModel.onToggleTunnelOnMobileData() },
)
},
onClick = {
viewModel.onToggleTunnelOnMobileData()
}
),
SelectionItem(
Icons.Outlined.SettingsEthernet,
title = {
Text(
stringResource(R.string.tunnel_on_ethernet),
style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface),
)
},
trailing = {
ScaledSwitch(
enabled = !uiState.settings.isAlwaysOnVpnEnabled,
checked = uiState.settings.isTunnelOnEthernetEnabled,
onClick = { viewModel.onToggleTunnelOnEthernet() },
)
},
onClick = {
viewModel.onToggleTunnelOnEthernet()
}
),
SelectionItem(
Icons.Outlined.NetworkPing,
title = {
Text(
stringResource(R.string.restart_on_ping),
style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface),
)
},
trailing = {
ScaledSwitch(
checked = uiState.settings.isPingEnabled,
onClick = { viewModel.onToggleRestartOnPing() },
)
},
onClick = {
viewModel.onToggleRestartOnPing()
}
) )
) },
) trailing = {
ScaledSwitch(
enabled = !uiState.settings.isAlwaysOnVpnEnabled,
checked = uiState.settings.isTunnelOnMobileDataEnabled,
onClick = { viewModel.onToggleTunnelOnMobileData() },
)
},
onClick = {
viewModel.onToggleTunnelOnMobileData()
},
),
SelectionItem(
Icons.Outlined.SettingsEthernet,
title = {
Text(
stringResource(R.string.tunnel_on_ethernet),
style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface),
)
},
trailing = {
ScaledSwitch(
enabled = !uiState.settings.isAlwaysOnVpnEnabled,
checked = uiState.settings.isTunnelOnEthernetEnabled,
onClick = { viewModel.onToggleTunnelOnEthernet() },
)
},
onClick = {
viewModel.onToggleTunnelOnEthernet()
},
),
SelectionItem(
Icons.Outlined.NetworkPing,
title = {
Text(
stringResource(R.string.restart_on_ping),
style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface),
)
},
trailing = {
ScaledSwitch(
checked = uiState.settings.isPingEnabled,
onClick = { viewModel.onToggleRestartOnPing() },
)
},
onClick = {
viewModel.onToggleRestartOnPing()
},
),
),
)
} }
} }
} }

View File

@ -2,22 +2,29 @@ package com.zaneschepke.wireguardautotunnel.ui.screens.settings.autotunnel
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.wireguard.android.util.RootShell
import com.zaneschepke.wireguardautotunnel.R import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.data.domain.Settings import com.zaneschepke.wireguardautotunnel.data.domain.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.ui.common.snackbar.SnackbarController import com.zaneschepke.wireguardautotunnel.ui.common.snackbar.SnackbarController
import com.zaneschepke.wireguardautotunnel.util.StringValue 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.flow.SharingStarted import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Provider
@HiltViewModel @HiltViewModel
class AutoTunnelViewModel class AutoTunnelViewModel
@Inject @Inject
constructor( constructor(
private val appDataRepository: AppDataRepository, private val appDataRepository: AppDataRepository,
private val rootShell: Provider<RootShell>,
@IoDispatcher private val ioDispatcher: CoroutineDispatcher,
) : ViewModel() { ) : ViewModel() {
private val settings = appDataRepository.settings.getSettingsFlow() private val settings = appDataRepository.settings.getSettingsFlow()
@ -37,29 +44,53 @@ constructor(
with(settings.value) { with(settings.value) {
appDataRepository.settings.save( appDataRepository.settings.save(
copy( copy(
isTunnelOnMobileDataEnabled = !this.isTunnelOnMobileDataEnabled, isTunnelOnMobileDataEnabled = !isTunnelOnMobileDataEnabled,
), ),
) )
} }
} }
fun onToggleWildcards() = viewModelScope.launch { fun onToggleWildcards() = viewModelScope.launch {
val wildcards = appDataRepository.appState.isWildcardsEnabled() with(settings.value) {
appDataRepository.appState.setWildcardsEnabled( appDataRepository.settings.save(
!wildcards copy(
) isWildcardsEnabled = !isWildcardsEnabled,
),
)
}
} }
fun onDeleteTrustedSSID(ssid: String) = viewModelScope.launch { fun onDeleteTrustedSSID(ssid: String) = viewModelScope.launch {
with(settings.value) { with(settings.value) {
appDataRepository.settings.save( appDataRepository.settings.save(
copy( copy(
trustedNetworkSSIDs = (this.trustedNetworkSSIDs - ssid).toMutableList(), trustedNetworkSSIDs = (trustedNetworkSSIDs - ssid).toMutableList(),
), ),
) )
} }
} }
fun onRootShellWifiToggle() = viewModelScope.launch {
requestRoot().onSuccess {
with(settings.value) {
appDataRepository.settings.save(
copy(isWifiNameByShellEnabled = !isWifiNameByShellEnabled),
)
}
}
}
private suspend fun requestRoot(): Result<Unit> {
return withContext(ioDispatcher) {
kotlin.runCatching {
rootShell.get().start()
SnackbarController.showMessage(StringValue.StringResource(R.string.root_accepted))
}.onFailure {
SnackbarController.showMessage(StringValue.StringResource(R.string.error_root_denied))
}
}
}
fun onToggleTunnelOnEthernet() = viewModelScope.launch { fun onToggleTunnelOnEthernet() = viewModelScope.launch {
with(settings.value) { with(settings.value) {
appDataRepository.settings.save( appDataRepository.settings.save(

View File

@ -32,9 +32,16 @@ import com.zaneschepke.wireguardautotunnel.util.extensions.scaledWidth
@OptIn(ExperimentalLayoutApi::class) @OptIn(ExperimentalLayoutApi::class)
@Composable @Composable
fun TrustedNetworkTextBox(trustedNetworks: List<String>, onDelete: (ssid: String) -> Unit, currentText: String, onSave : (ssid: String) -> Unit, onValueChange: (network: String) -> Unit, supporting: @Composable () -> Unit) { fun TrustedNetworkTextBox(
trustedNetworks: List<String>,
onDelete: (ssid: String) -> Unit,
currentText: String,
onSave: (ssid: String) -> Unit,
onValueChange: (network: String) -> Unit,
supporting: @Composable () -> Unit,
) {
val context = LocalContext.current val context = LocalContext.current
Column(verticalArrangement = Arrangement.spacedBy(10.dp.scaledHeight())){ Column(verticalArrangement = Arrangement.spacedBy(10.dp.scaledHeight())) {
FlowRow( FlowRow(
modifier = modifier =
Modifier.fillMaxWidth(), Modifier.fillMaxWidth(),
@ -93,6 +100,5 @@ fun TrustedNetworkTextBox(trustedNetworks: List<String>, onDelete: (ssid: String
} }
}, },
) )
} }
} }

View File

@ -7,7 +7,6 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontStyle import androidx.compose.ui.text.font.FontStyle
import com.zaneschepke.wireguardautotunnel.R import com.zaneschepke.wireguardautotunnel.R
@Composable @Composable
fun WildcardsLabel() { fun WildcardsLabel() {
Text( Text(

View File

@ -14,7 +14,7 @@ import com.zaneschepke.wireguardautotunnel.ui.theme.iconSize
fun ForwardButton(modifier: Modifier = Modifier.focusable(), onClick: () -> Unit) { fun ForwardButton(modifier: Modifier = Modifier.focusable(), onClick: () -> Unit) {
IconButton( IconButton(
modifier = modifier, modifier = modifier,
onClick = onClick onClick = onClick,
) { ) {
val icon = Icons.AutoMirrored.Outlined.ArrowForward val icon = Icons.AutoMirrored.Outlined.ArrowForward
Icon(icon, icon.name, Modifier.size(iconSize)) Icon(icon, icon.name, Modifier.size(iconSize))

View File

@ -12,7 +12,7 @@ import androidx.compose.ui.text.withStyle
import com.zaneschepke.wireguardautotunnel.R import com.zaneschepke.wireguardautotunnel.R
@Composable @Composable
fun LearnMoreLinkLabel(onClick: (url: String) -> Unit, url : String) { fun LearnMoreLinkLabel(onClick: (url: String) -> Unit, url: String) {
// TODO update link when docs are fully updated // TODO update link when docs are fully updated
val gettingStarted = val gettingStarted =
buildAnnotatedString { buildAnnotatedString {

View File

@ -39,7 +39,7 @@ fun LocationDisclosureScreen(appViewModel: AppViewModel, appUiState: AppUiState)
val navController = LocalNavController.current val navController = LocalNavController.current
LaunchedEffect(Unit, appUiState) { LaunchedEffect(Unit, appUiState) {
if(appUiState.generalState.isLocationDisclosureShown) navController.goFromRoot(Route.AutoTunnel) if (appUiState.generalState.isLocationDisclosureShown) navController.goFromRoot(Route.AutoTunnel)
} }
Column( Column(
@ -56,40 +56,49 @@ fun LocationDisclosureScreen(appViewModel: AppViewModel, appUiState: AppUiState)
.padding(30.dp.scaledHeight()) .padding(30.dp.scaledHeight())
.size(128.dp.scaledHeight()), .size(128.dp.scaledHeight()),
) )
Text( Text(
stringResource(R.string.prominent_background_location_title), stringResource(R.string.prominent_background_location_title),
style = MaterialTheme.typography.titleLarge.copy(fontWeight = FontWeight.Bold), style = MaterialTheme.typography.titleLarge.copy(fontWeight = FontWeight.Bold),
) )
Text( Text(
stringResource(R.string.prominent_background_location_message), stringResource(R.string.prominent_background_location_message),
style = MaterialTheme.typography.bodyLarge style = MaterialTheme.typography.bodyLarge,
) )
SurfaceSelectionGroupButton( SurfaceSelectionGroupButton(
listOf( listOf(
SelectionItem( SelectionItem(
Icons.Outlined.LocationOn, Icons.Outlined.LocationOn,
title = { Text(stringResource(R.string.launch_app_settings), style = MaterialTheme.typography.bodyLarge.copy(MaterialTheme.colorScheme.onSurface)) }, title = {
onClick = { context.launchAppSettings().also { Text(
stringResource(R.string.launch_app_settings),
style = MaterialTheme.typography.bodyLarge.copy(MaterialTheme.colorScheme.onSurface),
)
},
onClick = {
context.launchAppSettings().also {
appViewModel.setLocationDisclosureShown() appViewModel.setLocationDisclosureShown()
} }, }
trailing = { },
ForwardButton { context.launchAppSettings().also { trailing = {
ForwardButton {
context.launchAppSettings().also {
appViewModel.setLocationDisclosureShown() appViewModel.setLocationDisclosureShown()
} } }
} }
), },
), ),
) ),
SurfaceSelectionGroupButton( )
listOf( SurfaceSelectionGroupButton(
SelectionItem( listOf(
title = { Text(stringResource(R.string.skip), style = MaterialTheme.typography.bodyLarge.copy(MaterialTheme.colorScheme.onSurface)) }, SelectionItem(
onClick = { appViewModel.setLocationDisclosureShown() }, title = { Text(stringResource(R.string.skip), style = MaterialTheme.typography.bodyLarge.copy(MaterialTheme.colorScheme.onSurface)) },
trailing = { onClick = { appViewModel.setLocationDisclosureShown() },
ForwardButton { appViewModel.setLocationDisclosureShown() } trailing = {
} ForwardButton { appViewModel.setLocationDisclosureShown() }
), },
), ),
) ),
} )
} }
}

View File

@ -38,94 +38,118 @@ fun SupportScreen() {
val context = LocalContext.current val context = LocalContext.current
val navController = LocalNavController.current val navController = LocalNavController.current
Column( Column(
horizontalAlignment = Alignment.Start, horizontalAlignment = Alignment.Start,
verticalArrangement = Arrangement.spacedBy(24.dp.scaledHeight(), Alignment.Top), verticalArrangement = Arrangement.spacedBy(24.dp.scaledHeight(), Alignment.Top),
modifier = modifier =
Modifier Modifier
.fillMaxSize() .fillMaxSize()
.padding(top = topPadding) .padding(top = topPadding)
.padding(horizontal = 24.dp.scaledWidth()), .padding(horizontal = 24.dp.scaledWidth()),
) { ) {
GroupLabel(stringResource(R.string.thank_you)) GroupLabel(stringResource(R.string.thank_you))
SurfaceSelectionGroupButton( SurfaceSelectionGroupButton(
listOf( listOf(
SelectionItem( SelectionItem(
Icons.Filled.Book, Icons.Filled.Book,
title = { Text(stringResource(R.string.docs_description), style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface)) }, title = {
trailing = { Text(
ForwardButton { context.openWebUrl(context.getString(R.string.docs_url)) } stringResource(R.string.docs_description),
}, style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface),
onClick = { )
context.openWebUrl(context.getString(R.string.docs_url)) },
} trailing = {
), ForwardButton { context.openWebUrl(context.getString(R.string.docs_url)) }
SelectionItem( },
Icons.Filled.LineStyle, onClick = {
title = { Text(stringResource(R.string.read_logs), context.openWebUrl(context.getString(R.string.docs_url))
style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface)) }, },
trailing = { ),
ForwardButton { SelectionItem(
navController.navigate(Route.Logs) Icons.Filled.LineStyle,
} title = {
}, Text(
onClick = { stringResource(R.string.read_logs),
style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface),
)
},
trailing = {
ForwardButton {
navController.navigate(Route.Logs) navController.navigate(Route.Logs)
} }
), },
SelectionItem( onClick = {
Icons.Filled.Policy, navController.navigate(Route.Logs)
title = { Text(stringResource(R.string.privacy_policy), style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface)) }, },
trailing = { ),
ForwardButton { context.openWebUrl(context.getString(R.string.privacy_policy_url)) } SelectionItem(
}, Icons.Filled.Policy,
onClick = { title = {
context.openWebUrl(context.getString(R.string.privacy_policy_url)) Text(
} stringResource(R.string.privacy_policy),
), style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface),
)
},
trailing = {
ForwardButton { context.openWebUrl(context.getString(R.string.privacy_policy_url)) }
},
onClick = {
context.openWebUrl(context.getString(R.string.privacy_policy_url))
},
),
) ),
) )
SurfaceSelectionGroupButton( SurfaceSelectionGroupButton(
listOf( listOf(
SelectionItem( SelectionItem(
ImageVector.vectorResource(R.drawable.telegram), ImageVector.vectorResource(R.drawable.telegram),
title = { Text(stringResource(R.string.chat_description), style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface)) }, title = {
trailing = { Text(
ForwardButton { stringResource(R.string.chat_description),
context.openWebUrl(context.getString(R.string.telegram_url)) style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface),
} )
}, },
onClick = { trailing = {
ForwardButton {
context.openWebUrl(context.getString(R.string.telegram_url)) context.openWebUrl(context.getString(R.string.telegram_url))
} }
), },
SelectionItem( onClick = {
ImageVector.vectorResource(R.drawable.github), context.openWebUrl(context.getString(R.string.telegram_url))
title = { Text(stringResource(R.string.open_issue), style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface)) }, },
trailing = { ),
ForwardButton { SelectionItem(
context.openWebUrl(context.getString(R.string.github_url)) ImageVector.vectorResource(R.drawable.github),
} title = { Text(stringResource(R.string.open_issue), style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface)) },
}, trailing = {
onClick = { ForwardButton {
context.openWebUrl(context.getString(R.string.github_url)) context.openWebUrl(context.getString(R.string.github_url))
} }
), },
SelectionItem( onClick = {
Icons.Filled.Mail, context.openWebUrl(context.getString(R.string.github_url))
title = { Text(stringResource(R.string.email_description), style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface)) }, },
trailing = { ),
ForwardButton { SelectionItem(
context.launchSupportEmail() Icons.Filled.Mail,
} title = {
}, Text(
onClick = { stringResource(R.string.email_description),
style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface),
)
},
trailing = {
ForwardButton {
context.launchSupportEmail() context.launchSupportEmail()
} }
), },
) onClick = {
) context.launchSupportEmail()
VersionLabel() },
} ),
),
)
VersionLabel()
} }
}

View File

@ -4,11 +4,8 @@ import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.systemBars
import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.lazy.rememberLazyListState

View File

@ -12,8 +12,6 @@ val BalticSea = Color(0xFF1C1B1F)
val Brick = Color(0xFFCE4257) val Brick = Color(0xFFCE4257)
val Straw = Color(0xFFD4C483) val Straw = Color(0xFFD4C483)
sealed class ThemeColors( sealed class ThemeColors(
val background: Color, val background: Color,
val surface: Color, val surface: Color,

View File

@ -40,18 +40,15 @@ enum class Theme {
AUTOMATIC, AUTOMATIC,
LIGHT, LIGHT,
DARK, DARK,
DYNAMIC DYNAMIC,
} }
@Composable @Composable
fun WireguardAutoTunnelTheme( fun WireguardAutoTunnelTheme(theme: Theme = Theme.AUTOMATIC, content: @Composable () -> Unit) {
theme: Theme = Theme.AUTOMATIC,
content: @Composable () -> Unit,
) {
val context = LocalContext.current val context = LocalContext.current
var isDark = isSystemInDarkTheme() var isDark = isSystemInDarkTheme()
val autoTheme = if(isDark) DarkColorScheme else LightColorScheme val autoTheme = if (isDark) DarkColorScheme else LightColorScheme
val colorScheme = when(theme) { val colorScheme = when (theme) {
Theme.AUTOMATIC -> autoTheme Theme.AUTOMATIC -> autoTheme
Theme.DARK -> { Theme.DARK -> {
isDark = true isDark = true
@ -68,7 +65,9 @@ fun WireguardAutoTunnelTheme(
} else { } else {
dynamicLightColorScheme(context) dynamicLightColorScheme(context)
} }
} else autoTheme } else {
autoTheme
}
} }
} }
val view = LocalView.current val view = LocalView.current

View File

@ -0,0 +1,26 @@
package com.zaneschepke.wireguardautotunnel.util
open class SingletonHolder<out T : Any, in A>(creator: (A) -> T) {
private var creator: ((A) -> T)? = creator
@Volatile private var instance: T? = null
fun getInstance(arg: A): T {
val i = instance
if (i != null) {
return i
}
return synchronized(this) {
val i2 = instance
if (i2 != null) {
i2
} else {
val created = creator!!(arg)
instance = created
creator = null
created
}
}
}
}

View File

@ -2,10 +2,12 @@ package com.zaneschepke.wireguardautotunnel.util.extensions
import android.content.ComponentName import android.content.ComponentName
import android.content.Context import android.content.Context
import android.content.Context.POWER_SERVICE
import android.content.Intent import android.content.Intent
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.location.LocationManager import android.location.LocationManager
import android.net.Uri import android.net.Uri
import android.os.PowerManager
import android.provider.Settings import android.provider.Settings
import android.service.quicksettings.TileService import android.service.quicksettings.TileService
import android.widget.Toast import android.widget.Toast
@ -13,7 +15,6 @@ import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.TextUnit import androidx.compose.ui.unit.TextUnit
import androidx.core.location.LocationManagerCompat import androidx.core.location.LocationManagerCompat
import com.zaneschepke.wireguardautotunnel.R import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.receiver.BackgroundActionReceiver
import com.zaneschepke.wireguardautotunnel.service.tile.AutoTunnelControlTile import com.zaneschepke.wireguardautotunnel.service.tile.AutoTunnelControlTile
import com.zaneschepke.wireguardautotunnel.service.tile.TunnelControlTile import com.zaneschepke.wireguardautotunnel.service.tile.TunnelControlTile
import com.zaneschepke.wireguardautotunnel.util.Constants import com.zaneschepke.wireguardautotunnel.util.Constants
@ -34,6 +35,11 @@ fun Context.openWebUrl(url: String): Result<Unit> {
} }
} }
fun Context.isBatteryOptimizationsDisabled(): Boolean {
val pm = getSystemService(POWER_SERVICE) as PowerManager
return pm.isIgnoringBatteryOptimizations(packageName)
}
val Context.actionBarSize val Context.actionBarSize
get() = theme.obtainStyledAttributes(intArrayOf(android.R.attr.actionBarSize)) get() = theme.obtainStyledAttributes(intArrayOf(android.R.attr.actionBarSize))
.let { attrs -> attrs.getDimension(0, 0F).toInt().also { attrs.recycle() } } .let { attrs -> attrs.getDimension(0, 0F).toInt().also { attrs.recycle() } }
@ -66,7 +72,7 @@ fun Context.resizeWidth(dp: Dp): Dp {
} }
fun Context.launchNotificationSettings() { fun Context.launchNotificationSettings() {
if(isRunningOnTv()) return launchAppSettings() if (isRunningOnTv()) return launchAppSettings()
val settingsIntent: Intent = Intent(Settings.ACTION_APP_NOTIFICATION_SETTINGS) val settingsIntent: Intent = Intent(Settings.ACTION_APP_NOTIFICATION_SETTINGS)
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
.putExtra(Settings.EXTRA_APP_PACKAGE, packageName) .putExtra(Settings.EXTRA_APP_PACKAGE, packageName)
@ -159,23 +165,23 @@ fun Context.launchAppSettings() {
} }
} }
fun Context.startTunnelBackground(tunnelId: Int) { // fun Context.startTunnelBackground(tunnelId: Int) {
sendBroadcast( // sendBroadcast(
Intent(this, BackgroundActionReceiver::class.java).apply { // Intent(this, BackgroundActionReceiver::class.java).apply {
action = BackgroundActionReceiver.ACTION_CONNECT // action = BackgroundActionReceiver.ACTION_CONNECT
putExtra(BackgroundActionReceiver.TUNNEL_ID_EXTRA_KEY, tunnelId) // putExtra(BackgroundActionReceiver.TUNNEL_ID_EXTRA_KEY, tunnelId)
}, // },
) // )
} // }
//
fun Context.stopTunnelBackground(tunnelId: Int) { // fun Context.stopTunnelBackground(tunnelId: Int) {
sendBroadcast( // sendBroadcast(
Intent(this, BackgroundActionReceiver::class.java).apply { // Intent(this, BackgroundActionReceiver::class.java).apply {
action = BackgroundActionReceiver.ACTION_DISCONNECT // action = BackgroundActionReceiver.ACTION_DISCONNECT
putExtra(BackgroundActionReceiver.TUNNEL_ID_EXTRA_KEY, tunnelId) // putExtra(BackgroundActionReceiver.TUNNEL_ID_EXTRA_KEY, tunnelId)
}, // },
) // )
} // }
fun Context.requestTunnelTileServiceStateUpdate() { fun Context.requestTunnelTileServiceStateUpdate() {
TileService.requestListeningState( TileService.requestListeningState(

View File

@ -28,9 +28,9 @@ fun String.extractNameAndNumber(): Pair<String, Int>? {
} }
fun List<String>.isMatchingToWildcardList(value: String): Boolean { fun List<String>.isMatchingToWildcardList(value: String): Boolean {
val excludeValues = this.filter { it.startsWith("!") }.map { it.removePrefix("!").toRegexWithWildcards() } val excludeValues = this.filter { it.startsWith("!") }.map { it.removePrefix("!").transformWildcardsToRegex() }
Timber.d("Excluded values: $excludeValues") Timber.d("Excluded values: $excludeValues")
val includedValues = this.filter { !it.startsWith("!") }.map { it.toRegexWithWildcards() } val includedValues = this.filter { !it.startsWith("!") }.map { it.transformWildcardsToRegex() }
Timber.d("Included values: $includedValues") Timber.d("Included values: $includedValues")
val matches = includedValues.filter { it.matches(value) } val matches = includedValues.filter { it.matches(value) }
val excludedMatches = excludeValues.filter { it.matches(value) } val excludedMatches = excludeValues.filter { it.matches(value) }
@ -39,6 +39,32 @@ fun List<String>.isMatchingToWildcardList(value: String): Boolean {
return matches.isNotEmpty() && excludedMatches.isEmpty() return matches.isNotEmpty() && excludedMatches.isEmpty()
} }
fun String.toRegexWithWildcards(): Regex { fun String.transformWildcardsToRegex(): Regex {
return this.replace("*", ".*").replace("?", ".").toRegex() return this.replaceUnescapedChar("*", ".*").replaceUnescapedChar("?", ".").toRegex()
}
fun String.replaceUnescapedChar(charToReplace: String, replacement: String): String {
val escapedChar = Regex.escape(charToReplace)
val regex = "(?<!\\\\)(?<!(?<!\\\\)\\\\)($escapedChar)".toRegex()
return regex.replace(this) { matchResult ->
if (matchResult.range.first == 0 ||
this[matchResult.range.first - 1] != '\\' ||
(matchResult.range.first > 1 && this[matchResult.range.first - 2] == '\\')
) {
replacement.toString()
} else {
matchResult.value
}
}
}
fun String.isCharacterEscaped(index: Int): Boolean {
if (index <= 0) return false
var backslashCount = 0
var currentIndex = index - 1
while (currentIndex >= 0 && this[currentIndex] == '\\') {
backslashCount++
currentIndex--
}
return backslashCount % 2 != 0
} }

View File

@ -4,9 +4,11 @@ import androidx.compose.ui.graphics.Color
import com.wireguard.android.util.RootShell import com.wireguard.android.util.RootShell
import com.wireguard.config.Peer import com.wireguard.config.Peer
import com.zaneschepke.wireguardautotunnel.service.tunnel.HandshakeStatus import com.zaneschepke.wireguardautotunnel.service.tunnel.HandshakeStatus
import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelState
import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnState
import com.zaneschepke.wireguardautotunnel.service.tunnel.statistics.TunnelStatistics import com.zaneschepke.wireguardautotunnel.service.tunnel.statistics.TunnelStatistics
import com.zaneschepke.wireguardautotunnel.ui.theme.Straw
import com.zaneschepke.wireguardautotunnel.ui.theme.SilverTree import com.zaneschepke.wireguardautotunnel.ui.theme.SilverTree
import com.zaneschepke.wireguardautotunnel.ui.theme.Straw
import com.zaneschepke.wireguardautotunnel.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
@ -21,6 +23,10 @@ fun TunnelStatistics.PeerStats.latestHandshakeSeconds(): Long? {
return NumberUtils.getSecondsBetweenTimestampAndNow(this.latestHandshakeEpochMillis) return NumberUtils.getSecondsBetweenTimestampAndNow(this.latestHandshakeEpochMillis)
} }
fun VpnState.isDown(): Boolean {
return this.status == TunnelState.DOWN
}
fun TunnelStatistics.PeerStats.handshakeStatus(): HandshakeStatus { fun TunnelStatistics.PeerStats.handshakeStatus(): HandshakeStatus {
// TODO add never connected status after duration // TODO add never connected status after duration
return this.latestHandshakeSeconds().let { return this.latestHandshakeSeconds().let {

View File

@ -220,4 +220,9 @@
<string name="use_wildcards">Use name wildcards</string> <string name="use_wildcards">Use name wildcards</string>
<string name="learn_more">Learn more</string> <string name="learn_more">Learn more</string>
<string name="wildcards_active">Wildcards active</string> <string name="wildcards_active">Wildcards active</string>
<string name="wifi_name_via_shell">Wifi name via shell</string>
<string name="use_root_shell_for_wifi">Use root shell to get wifi name</string>
<string name="kernel_not_supported">Kernel not supported</string>
<string name="start_auto">Start auto-tunnel</string>
<string name="stop_auto">Stop auto-tunnel</string>
</resources> </resources>

View File

@ -37,8 +37,8 @@
android:enabled="true" android:enabled="true"
android:icon="@drawable/auto_play" android:icon="@drawable/auto_play"
android:shortcutId="autoOn1" android:shortcutId="autoOn1"
android:shortcutLongLabel="@string/auto_on" android:shortcutLongLabel="@string/start_auto"
android:shortcutShortLabel="@string/auto_tun_on"> android:shortcutShortLabel="@string/start_auto">
<intent <intent
android:action="START" android:action="START"
android:targetClass="com.zaneschepke.wireguardautotunnel.service.shortcut.ShortcutsActivity" android:targetClass="com.zaneschepke.wireguardautotunnel.service.shortcut.ShortcutsActivity"
@ -53,8 +53,8 @@
android:enabled="true" android:enabled="true"
android:icon="@drawable/auto_pause" android:icon="@drawable/auto_pause"
android:shortcutId="autoOff1" android:shortcutId="autoOff1"
android:shortcutLongLabel="@string/auto_off" android:shortcutLongLabel="@string/stop_auto"
android:shortcutShortLabel="@string/auto_tun_off"> android:shortcutShortLabel="@string/stop_auto">
<intent <intent
android:action="STOP" android:action="STOP"
android:targetClass="com.zaneschepke.wireguardautotunnel.service.shortcut.ShortcutsActivity" android:targetClass="com.zaneschepke.wireguardautotunnel.service.shortcut.ShortcutsActivity"

View File

@ -1,7 +1,7 @@
object Constants { object Constants {
const val VERSION_NAME = "3.5.3" const val VERSION_NAME = "3.6.0"
const val JVM_TARGET = "17" const val JVM_TARGET = "17"
const val VERSION_CODE = 35300 const val VERSION_CODE = 36000
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"

View File

@ -6,7 +6,7 @@ androidx-junit = "1.2.1"
appcompat = "1.7.0" appcompat = "1.7.0"
biometricKtx = "1.2.0-alpha05" biometricKtx = "1.2.0-alpha05"
coreGoogleShortcuts = "1.1.0" coreGoogleShortcuts = "1.1.0"
coreKtx = "1.13.1" coreKtx = "1.15.0"
datastorePreferences = "1.1.1" datastorePreferences = "1.1.1"
desugar_jdk_libs = "2.1.2" desugar_jdk_libs = "2.1.2"
espressoCore = "3.6.1" espressoCore = "3.6.1"
@ -14,18 +14,18 @@ hiltAndroid = "2.52"
hiltNavigationCompose = "1.2.0" hiltNavigationCompose = "1.2.0"
junit = "4.13.2" junit = "4.13.2"
kotlinx-serialization-json = "1.7.3" kotlinx-serialization-json = "1.7.3"
lifecycle-runtime-compose = "2.8.6" lifecycle-runtime-compose = "2.8.7"
material3 = "1.3.0" material3 = "1.3.1"
navigationCompose = "2.8.3" navigationCompose = "2.8.3"
pinLockCompose = "1.0.4" pinLockCompose = "1.0.4"
roomVersion = "2.6.1" roomVersion = "2.6.1"
timber = "5.0.1" timber = "5.0.1"
tunnel = "1.2.1" tunnel = "1.2.1"
androidGradlePlugin = "8.7.1" androidGradlePlugin = "8.7.2"
kotlin = "2.0.21" kotlin = "2.0.21"
ksp = "2.0.21-1.0.25" ksp = "2.0.21-1.0.25"
composeBom = "2024.10.00" composeBom = "2024.10.01"
compose = "1.7.4" compose = "1.7.5"
zxingAndroidEmbedded = "4.3.0" zxingAndroidEmbedded = "4.3.0"
coreSplashscreen = "1.0.1" coreSplashscreen = "1.0.1"
gradlePlugins-grgit = "5.3.0" gradlePlugins-grgit = "5.3.0"