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" />
</intent-filter>
</receiver>
<receiver
android:name=".receiver.BackgroundActionReceiver"
android:enabled="true"
android:exported="false"/>
<receiver
android:name=".receiver.AppUpdateReceiver"
android:exported="false">

View File

@ -11,7 +11,7 @@ import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
@Database(
entities = [Settings::class, TunnelConfig::class],
version = 10,
version = 11,
autoMigrations =
[
AutoMigration(from = 1, to = 2),
@ -36,6 +36,11 @@ import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
AutoMigration(7, 8),
AutoMigration(8, 9),
AutoMigration(9, 10),
AutoMigration(
from = 10,
to = 11,
spec = RemoveTunnelPauseMigration::class,
),
],
exportSchema = true,
)
@ -55,3 +60,9 @@ abstract class AppDatabase : RoomDatabase() {
columnName = "is_battery_saver_enabled",
)
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 pinLockEnabled = booleanPreferencesKey("PIN_LOCK_ENABLED")
val tunnelStatsExpanded = booleanPreferencesKey("TUNNEL_STATS_EXPANDED")
val wildcardsEnabled = booleanPreferencesKey("WILDCARDS_ENABLED")
val theme = stringPreferencesKey("THEME")
}

View File

@ -7,14 +7,12 @@ data class GeneralState(
val isBatteryOptimizationDisableShown: Boolean = BATTERY_OPTIMIZATION_DISABLE_SHOWN_DEFAULT,
val isPinLockEnabled: Boolean = PIN_LOCK_ENABLED_DEFAULT,
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 {
const val LOCATION_DISCLOSURE_SHOWN_DEFAULT = false
const val BATTERY_OPTIMIZATION_DISABLE_SHOWN_DEFAULT = false
const val PIN_LOCK_ENABLED_DEFAULT = 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",
)
val isMultiTunnelEnabled: Boolean = false,
@ColumnInfo(
name = "is_auto_tunnel_paused",
defaultValue = "false",
)
val isAutoTunnelPaused: Boolean = false,
@ColumnInfo(
name = "is_ping_enabled",
defaultValue = "false",
@ -55,4 +50,14 @@ data class Settings(
defaultValue = "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 isWildcardsEnabled(): Boolean
suspend fun setWildcardsEnabled(enabled: Boolean)
suspend fun isBatteryOptimizationDisableShown(): Boolean
suspend fun setBatteryOptimizationDisableShown(shown: Boolean)
@ -31,7 +27,7 @@ interface AppStateRepository {
suspend fun setTheme(theme: Theme)
suspend fun getTheme() : Theme
suspend fun getTheme(): Theme
val generalStateFlow: Flow<GeneralState>
}

View File

@ -29,14 +29,6 @@ class DataStoreAppStateRepository(
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 {
return dataStoreManager.getFromStore(DataStoreManager.batteryDisableShown)
?: GeneralState.BATTERY_OPTIMIZATION_DISABLE_SHOWN_DEFAULT
@ -71,7 +63,7 @@ class DataStoreAppStateRepository(
return dataStoreManager.getFromStore(DataStoreManager.theme)?.let {
try {
Theme.valueOf(it)
} catch (_ : IllegalArgumentException) {
} catch (_: IllegalArgumentException) {
Theme.AUTOMATIC
}
} ?: Theme.AUTOMATIC
@ -92,8 +84,7 @@ class DataStoreAppStateRepository(
pref[DataStoreManager.pinLockEnabled]
?: GeneralState.PIN_LOCK_ENABLED_DEFAULT,
isTunnelStatsExpanded = pref[DataStoreManager.tunnelStatsExpanded] ?: GeneralState.IS_TUNNEL_STATS_EXPANDED,
isWildcardsEnabled = pref[DataStoreManager.wildcardsEnabled] ?: GeneralState.IS_WILDCARDS_ENABLED,
theme = getTheme()
theme = getTheme(),
)
} catch (e: IllegalArgumentException) {
Timber.e(e)

View File

@ -1,11 +1,9 @@
package com.zaneschepke.wireguardautotunnel.data.repository
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
import com.zaneschepke.wireguardautotunnel.data.TunnelConfigDao
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
import com.zaneschepke.wireguardautotunnel.module.IoDispatcher
import com.zaneschepke.wireguardautotunnel.util.extensions.TunnelConfigs
import com.zaneschepke.wireguardautotunnel.util.extensions.requestTunnelTileServiceStateUpdate
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.withContext
@ -26,8 +24,6 @@ class RoomTunnelConfigRepository(
override suspend fun save(tunnelConfig: TunnelConfig) {
withContext(ioDispatcher) {
tunnelConfigDao.save(tunnelConfig)
}.also {
WireGuardAutoTunnel.instance.requestTunnelTileServiceStateUpdate()
}
}
@ -60,8 +56,6 @@ class RoomTunnelConfigRepository(
override suspend fun delete(tunnelConfig: TunnelConfig) {
withContext(ioDispatcher) {
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.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
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.WireGuardTunnel
import dagger.Module
@ -65,6 +66,7 @@ class TunnelModule {
tunnelConfigRepository: TunnelConfigRepository,
@ApplicationScope applicationScope: CoroutineScope,
@IoDispatcher ioDispatcher: CoroutineDispatcher,
serviceManager: ServiceManager,
): TunnelService {
return WireGuardTunnel(
amneziaBackend,
@ -73,6 +75,13 @@ class TunnelModule {
appDataRepository,
applicationScope,
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.service.foreground.ServiceManager
import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelService
import com.zaneschepke.wireguardautotunnel.util.extensions.startTunnelBackground
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 AppUpdateReceiver : BroadcastReceiver() {
@ -25,7 +25,10 @@ class AppUpdateReceiver : BroadcastReceiver() {
lateinit var appDataRepository: AppDataRepository
@Inject
lateinit var tunnelService: TunnelService
lateinit var tunnelService: Provider<TunnelService>
@Inject
lateinit var serviceManager: ServiceManager
override fun onReceive(context: Context, intent: Intent) {
if (intent.action != Intent.ACTION_MY_PACKAGE_REPLACED) return
@ -33,11 +36,11 @@ class AppUpdateReceiver : BroadcastReceiver() {
val settings = appDataRepository.settings.getSettings()
if (settings.isAutoTunnelEnabled) {
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 }
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.tunnel.TunnelService
import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelState
import com.zaneschepke.wireguardautotunnel.util.extensions.startTunnelBackground
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
@ -28,6 +27,9 @@ class BootReceiver : BroadcastReceiver() {
@ApplicationScope
lateinit var applicationScope: CoroutineScope
@Inject
lateinit var serviceManager: ServiceManager
override fun onReceive(context: Context, intent: Intent) {
if (Intent.ACTION_BOOT_COMPLETED != intent.action) return
applicationScope.launch {
@ -37,11 +39,11 @@ class BootReceiver : BroadcastReceiver() {
val tunState = tunnelService.get().vpnState.value.status
if (activeTunnels.isNotEmpty() && tunState != TunnelState.UP) {
Timber.i("Starting previously active tunnel")
context.startTunnelBackground(activeTunnels.first().id)
tunnelService.get().startTunnel(activeTunnels.first(), true)
}
if (isAutoTunnelEnabled) {
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.extensions.cancelWithMessage
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.onNotRunning
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
@ -74,6 +75,9 @@ class AutoTunnelService : LifecycleService() {
@IoDispatcher
lateinit var ioDispatcher: CoroutineDispatcher
@Inject
lateinit var serviceManager: ServiceManager
@Inject
@MainImmediateDispatcher
lateinit var mainImmediateDispatcher: CoroutineDispatcher
@ -88,14 +92,11 @@ class AutoTunnelService : LifecycleService() {
private var pingJob: Job? = null
private var networkEventJob: Job? = null
@get:Synchronized @set:Synchronized
private var running: Boolean = false
override fun onCreate() {
super.onCreate()
lifecycleScope.launch(mainImmediateDispatcher) {
kotlin.runCatching {
launchNotification()
launchWatcherNotification()
}.onFailure {
Timber.e(it)
}
@ -110,32 +111,14 @@ class AutoTunnelService : LifecycleService() {
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
Timber.d("onStartCommand executed with startId: $startId")
if (intent != null) {
val action = intent.action
when (action) {
Action.START.name,
Action.START_FOREGROUND.name,
-> startService()
Action.STOP.name, Action.STOP_FOREGROUND.name -> stopService()
}
}
serviceManager.autoTunnelService.complete(this)
return super.onStartCommand(intent, flags, startId)
}
private suspend fun launchNotification() {
if (appDataRepository.settings.getSettings().isAutoTunnelPaused) {
launchWatcherPausedNotification()
} else {
launchWatcherNotification()
}
}
private fun startService() {
if (running) return
running = true
fun start() {
kotlin.runCatching {
lifecycleScope.launch(mainImmediateDispatcher) {
launchNotification()
launchWatcherNotification()
initWakeLock()
}
startSettingsJob()
@ -145,7 +128,7 @@ class AutoTunnelService : LifecycleService() {
}
}
private fun stopService() {
fun stop() {
wakeLock?.let {
if (it.isHeld) {
it.release()
@ -157,6 +140,7 @@ class AutoTunnelService : LifecycleService() {
override fun onDestroy() {
cancelAndResetNetworkJobs()
cancelAndResetPingJob()
serviceManager.autoTunnelService = CompletableDeferred()
super.onDestroy()
}
@ -176,10 +160,6 @@ class AutoTunnelService : LifecycleService() {
)
}
private fun launchWatcherPausedNotification() {
launchWatcherNotification(getString(R.string.watcher_notification_text_paused))
}
private fun initWakeLock() {
wakeLock =
(getSystemService(Context.POWER_SERVICE) as PowerManager).run {
@ -265,8 +245,7 @@ class AutoTunnelService : LifecycleService() {
runCatching {
do {
val vpnState = tunnelService.get().vpnState.value
val settings = appDataRepository.settings.getSettings()
if (vpnState.status == TunnelState.UP && !settings.isAutoTunnelPaused) {
if (vpnState.status == TunnelState.UP) {
if (vpnState.tunnelConfig != null) {
val config = TunnelConfig.configFromWgQuick(vpnState.tunnelConfig.wgQuick)
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() {
Timber.i("Starting settings watcher")
withContext(ioDispatcher) {
@ -321,7 +289,7 @@ class AutoTunnelService : LifecycleService() {
tunnels = tunnels,
)
}.collect {
onAutoTunnelPause(it.settings.isAutoTunnelPaused)
Timber.d("got new settings: ${it.settings}")
manageJobsBySettings(it.settings)
autoTunnelStateFlow.emit(it)
}
@ -331,7 +299,12 @@ class AutoTunnelService : LifecycleService() {
private suspend fun watchForVpnStateChanges() {
Timber.i("Starting vpn state watcher")
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 {
val settings = appDataRepository.settings.getSettings()
if (it.isPingEnabled && !settings.isPingEnabled) {
@ -475,9 +448,8 @@ class AutoTunnelService : LifecycleService() {
private suspend fun getWifiSSID(networkCapabilities: NetworkCapabilities): String? {
return withContext(ioDispatcher) {
try {
rootShell.get().getCurrentWifiName()
} catch (_: Exception) {
with(autoTunnelStateFlow.value.settings) {
if (isWifiNameByShellEnabled) return@withContext rootShell.get().getCurrentWifiName()
wifiService.getNetworkName(networkCapabilities)
}
}
@ -487,100 +459,95 @@ class AutoTunnelService : LifecycleService() {
return appDataRepository.tunnels.findByMobileDataTunnel().firstOrNull()
}
private fun isTunnelDown(): Boolean {
return tunnelService.get().vpnState.value.status == TunnelState.DOWN
}
private suspend fun handleNetworkEventChanges() {
withContext(ioDispatcher) {
Timber.i("Starting network event watcher")
autoTunnelStateFlow.collectLatest { watcherState ->
val autoTunnel = "Auto-tunnel watcher"
if (!watcherState.settings.isAutoTunnelPaused) {
// delay for rapid network state changes and then collect latest
delay(Constants.WATCHER_COLLECTION_DELAY)
val activeTunnel = tunnelService.get().vpnState.value.tunnelConfig
val defaultTunnel = appDataRepository.getPrimaryOrFirstTunnel()
when {
watcherState.isEthernetConditionMet() -> {
Timber.i("$autoTunnel - tunnel on on ethernet condition met")
if (isTunnelDown()) {
defaultTunnel?.let {
Timber.d("New watcher state!")
// delay for rapid network state changes and then collect latest
delay(Constants.WATCHER_COLLECTION_DELAY)
val activeTunnel = watcherState.vpnState.tunnelConfig
val defaultTunnel = appDataRepository.getPrimaryOrFirstTunnel()
when {
watcherState.isEthernetConditionMet() -> {
Timber.i("$autoTunnel - tunnel on on ethernet condition met")
if (watcherState.vpnState.isDown()) {
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)
}
}
}
watcherState.isMobileDataConditionMet() -> {
Timber.i("$autoTunnel - tunnel on mobile data condition met")
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) {
} ?: suspend {
Timber.i("No tunnel associated with this SSID, using defaults")
val default = appDataRepository.getPrimaryOrFirstTunnel()
if (default?.name != tunnelService.get().name || watcherState.vpnState.isDown()) {
default?.let {
tunnelService.get().startTunnel(it)
}
} ?: suspend {
Timber.i("No tunnel associated with this SSID, using defaults")
val default = appDataRepository.getPrimaryOrFirstTunnel()
if (default?.name != tunnelService.get().name || isTunnelDown()) {
default?.let {
tunnelService.get().startTunnel(it)
}
}
}.invoke()
}
}
}.invoke()
}
}
watcherState.isTrustedWifiConditionMet() -> {
Timber.i(
"$autoTunnel - tunnel off on trusted wifi condition met, turning vpn off",
)
if (!isTunnelDown()) activeTunnel?.let { tunnelService.get().stopTunnel(it) }
}
watcherState.isTrustedWifiConditionMet() -> {
Timber.i(
"$autoTunnel - tunnel off on trusted wifi condition met, turning vpn off",
)
if (!watcherState.vpnState.isDown()) activeTunnel?.let { tunnelService.get().stopTunnel(it) }
}
watcherState.isTunnelOffOnWifiConditionMet() -> {
Timber.i(
"$autoTunnel - tunnel off on wifi condition met, turning vpn off",
)
if (!isTunnelDown()) activeTunnel?.let { tunnelService.get().stopTunnel(it) }
}
watcherState.isTunnelOffOnWifiConditionMet() -> {
Timber.i(
"$autoTunnel - tunnel off on wifi condition met, turning vpn off",
)
if (!watcherState.vpnState.isDown()) activeTunnel?.let { tunnelService.get().stopTunnel(it) }
}
watcherState.isTunnelOffOnNoConnectivityMet() -> {
Timber.i(
"$autoTunnel - tunnel off on no connectivity met, turning vpn off",
)
if (!isTunnelDown()) activeTunnel?.let { tunnelService.get().stopTunnel(it) }
}
watcherState.isTunnelOffOnNoConnectivityMet() -> {
Timber.i(
"$autoTunnel - tunnel off on no connectivity met, turning vpn off",
)
if (!watcherState.vpnState.isDown()) activeTunnel?.let { tunnelService.get().stopTunnel(it) }
}
else -> {
Timber.i("$autoTunnel - no condition met")
}
else -> {
Timber.i("$autoTunnel - no condition met")
}
}
}

View File

@ -1,10 +1,13 @@
package com.zaneschepke.wireguardautotunnel.service.foreground
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.isMatchingToWildcardList
data class AutoTunnelState(
val vpnState: VpnState = VpnState(),
val isWifiConnected: Boolean = false,
val isEthernetConnected: Boolean = false,
val isMobileDataConnected: Boolean = false,
@ -41,7 +44,7 @@ data class AutoTunnelState(
return (
!isEthernetConnected &&
isWifiConnected &&
!settings.trustedNetworkSSIDs.isMatchingToWildcardList(currentNetworkSSID) &&
!isCurrentSSIDTrusted() &&
settings.isTunnelOnWifiEnabled
)
}
@ -51,7 +54,7 @@ data class AutoTunnelState(
!isEthernetConnected &&
(
isWifiConnected &&
settings.trustedNetworkSSIDs.isMatchingToWildcardList(currentNetworkSSID)
isCurrentSSIDTrusted()
)
)
}
@ -73,4 +76,32 @@ data class AutoTunnelState(
!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.content.Context
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
object ServiceManager {
private fun <T : Service> actionOnService(action: Action, context: Context, cls: Class<T>, extras: Map<String, Int>? = null) {
if (VpnService.prepare(context) != null) return
val intent =
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,
)
@OptIn(ExperimentalCoroutinesApi::class)
class ServiceManager
@Inject constructor(private val context: Context) {
Action.START, Action.STOP -> context.startService(intent)
}
} catch (e: Exception) {
Timber.e(e.message)
private val _autoTunnelActive = MutableStateFlow(false)
val autoTunnelActive = _autoTunnelActive.asStateFlow()
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) {
actionOnService(
Action.START_FOREGROUND,
context,
AutoTunnelService::class.java,
)
suspend fun startAutoTunnel(background: Boolean) {
if (autoTunnelService.isCompleted) return _autoTunnelActive.update { true }
kotlin.runCatching {
startService(AutoTunnelService::class.java, background)
autoTunnelService.await()
autoTunnelService.getCompleted().start()
_autoTunnelActive.update { true }
}.onFailure {
Timber.e(it)
}
}
fun startWatcherService(context: Context) {
actionOnService(
Action.START,
context,
AutoTunnelService::class.java,
)
suspend fun startBackgroundService() {
if (backgroundService.isCompleted) return
kotlin.runCatching {
startService(TunnelBackgroundService::class.java, true)
backgroundService.await()
backgroundService.getCompleted().start()
}.onFailure {
Timber.e(it)
}
}
fun stopWatcherService(context: Context) {
actionOnService(
Action.STOP,
context,
AutoTunnelService::class.java,
)
fun stopBackgroundService() {
if (!backgroundService.isCompleted) return
runCatching {
backgroundService.getCompleted().stop()
}.onFailure {
Timber.e(it)
}
}
fun startTunnelBackgroundService(context: Context) {
actionOnService(
Action.START_FOREGROUND,
context,
TunnelBackgroundService::class.java,
)
fun stopAutoTunnel() {
if (!autoTunnelService.isCompleted) return
runCatching {
autoTunnelService.getCompleted().stop()
_autoTunnelActive.update { false }
}.onFailure {
Timber.e(it)
}
}
fun stopTunnelBackgroundService(context: Context) {
actionOnService(
Action.STOP,
context,
TunnelBackgroundService::class.java,
)
fun requestTunnelTileUpdate() {
context.requestTunnelTileServiceStateUpdate()
}
}

View File

@ -7,6 +7,7 @@ import androidx.lifecycle.LifecycleService
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.service.notification.NotificationService
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.CompletableDeferred
import javax.inject.Inject
@AndroidEntryPoint
@ -15,6 +16,9 @@ class TunnelBackgroundService : LifecycleService() {
@Inject
lateinit var notificationService: NotificationService
@Inject
lateinit var serviceManager: ServiceManager
private val foregroundId = 123
override fun onCreate() {
@ -29,27 +33,24 @@ class TunnelBackgroundService : LifecycleService() {
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
if (intent != null) {
val action = intent.action
when (action) {
Action.START.name,
Action.START_FOREGROUND.name,
-> startService()
Action.STOP.name, Action.STOP_FOREGROUND.name -> stopService()
}
}
serviceManager.backgroundService.complete(this)
return super.onStartCommand(intent, flags, startId)
}
private fun startService() {
fun start() {
startForeground(foregroundId, createNotification())
}
private fun stopService() {
fun stop() {
stopForeground(STOP_FOREGROUND_REMOVE)
stopSelf()
}
override fun onDestroy() {
serviceManager.backgroundService = CompletableDeferred()
super.onDestroy()
}
private fun createNotification(): Notification {
return notificationService.createNotification(
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.service.foreground.Action
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.util.extensions.startTunnelBackground
import com.zaneschepke.wireguardautotunnel.util.extensions.stopTunnelBackground
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
@ -24,6 +23,9 @@ class ShortcutsActivity : ComponentActivity() {
@Inject
lateinit var tunnelService: Provider<TunnelService>
@Inject
lateinit var serviceManager: ServiceManager
@Inject
@ApplicationScope
lateinit var applicationScope: CoroutineScope
@ -44,26 +46,16 @@ class ShortcutsActivity : ComponentActivity() {
Timber.d("Shortcut action on name: ${tunnelConfig?.name}")
tunnelConfig?.let {
when (intent.action) {
Action.START.name -> this@ShortcutsActivity.startTunnelBackground(it.id)
Action.STOP.name -> this@ShortcutsActivity.stopTunnelBackground(it.id)
Action.START.name -> tunnelService.get().startTunnel(it, true)
Action.STOP.name -> tunnelService.get().stopTunnel(it)
else -> Unit
}
}
}
AutoTunnelService::class.java.simpleName, LEGACY_AUTO_TUNNEL_SERVICE_NAME -> {
when (intent.action) {
Action.START.name ->
appDataRepository.settings.save(
settings.copy(
isAutoTunnelPaused = false,
),
)
Action.STOP.name ->
appDataRepository.settings.save(
settings.copy(
isAutoTunnelPaused = true,
),
)
Action.START.name -> serviceManager.startAutoTunnel(true)
Action.STOP.name -> serviceManager.stopAutoTunnel()
}
}
}

View File

@ -1,7 +1,6 @@
package com.zaneschepke.wireguardautotunnel.service.tile
import android.content.Intent
import android.os.Build
import android.os.IBinder
import android.service.quicksettings.Tile
import android.service.quicksettings.TileService
@ -9,9 +8,9 @@ import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.LifecycleRegistry
import androidx.lifecycle.lifecycleScope
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
import com.zaneschepke.wireguardautotunnel.module.ApplicationScope
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
@ -23,51 +22,18 @@ class AutoTunnelControlTile : TileService(), LifecycleOwner {
@Inject
lateinit var appDataRepository: AppDataRepository
@Inject
lateinit var serviceManager: ServiceManager
@Inject
@ApplicationScope
lateinit var applicationScope: CoroutineScope
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() {
super.onCreate()
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() {
@ -82,26 +48,28 @@ class AutoTunnelControlTile : TileService(), LifecycleOwner {
override fun onStartListening() {
super.onStartListening()
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() {
super.onClick()
unlockAndRun {
lifecycleScope.launch {
kotlin.runCatching {
val settings = appDataRepository.settings.getSettings()
if (settings.isAutoTunnelPaused) {
return@launch appDataRepository.settings.save(
settings.copy(
isAutoTunnelPaused = false,
),
)
}
appDataRepository.settings.save(
settings.copy(
isAutoTunnelPaused = true,
),
)
if (serviceManager.autoTunnelActive.value) {
serviceManager.stopAutoTunnel()
setInactive()
} else {
serviceManager.startAutoTunnel(true)
setActive()
}
}
}
@ -128,16 +96,15 @@ class AutoTunnelControlTile : TileService(), LifecycleOwner {
}
}
private fun setTileDescription(description: String) {
kotlin.runCatching {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
qsTile.subtitle = description
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
qsTile.stateDescription = description
}
qsTile.updateTile()
/* 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

View File

@ -1,6 +1,8 @@
package com.zaneschepke.wireguardautotunnel.service.tile
import android.content.Intent
import android.os.Build
import android.os.IBinder
import android.service.quicksettings.Tile
import android.service.quicksettings.TileService
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.module.ApplicationScope
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 kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
@ -36,7 +36,6 @@ class TunnelControlTile : TileService(), LifecycleOwner {
override fun onCreate() {
super.onCreate()
Timber.d("onCreate for tile service")
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_CREATE)
}
@ -52,6 +51,7 @@ class TunnelControlTile : TileService(), LifecycleOwner {
override fun onStartListening() {
super.onStartListening()
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_START)
Timber.d("Updating tile!")
lifecycleScope.launch {
if (appDataRepository.tunnels.getAll().isEmpty()) return@launch setUnavailable()
updateTileState()
@ -60,6 +60,7 @@ class TunnelControlTile : TileService(), LifecycleOwner {
private suspend fun updateTileState() {
val lastActive = appDataRepository.getStartTunnelConfig()
Timber.d("Got config $lastActive")
lastActive?.let {
updateTile(it)
}
@ -68,13 +69,15 @@ class TunnelControlTile : TileService(), LifecycleOwner {
override fun onClick() {
super.onClick()
unlockAndRun {
Timber.d("Click")
lifecycleScope.launch {
val context = this@TunnelControlTile
val lastActive = appDataRepository.getStartTunnelConfig()
lastActive?.let { tunnel ->
if (tunnel.isActive) return@launch context.stopTunnelBackground(tunnel.id)
context.startTunnelBackground(tunnel.id)
if (tunnel.isActive) {
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
get() = lifecycleRegistry
}

View File

@ -5,7 +5,7 @@ import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
import kotlinx.coroutines.flow.StateFlow
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>
@ -18,5 +18,6 @@ interface TunnelService : Tunnel, org.amnezia.awg.backend.Tunnel {
suspend fun getState(): TunnelState
fun cancelStatsJob()
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.IoDispatcher
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.TunnelStatistics
import com.zaneschepke.wireguardautotunnel.service.tunnel.statistics.WireGuardStatistics
@ -36,6 +37,7 @@ constructor(
private val appDataRepository: AppDataRepository,
@ApplicationScope private val applicationScope: CoroutineScope,
@IoDispatcher private val ioDispatcher: CoroutineDispatcher,
private val serviceManager: ServiceManager,
) : TunnelService {
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) {
onBeforeStart(tunnelConfig)
onBeforeStart(tunnelConfig, background)
setState(tunnelConfig, TunnelState.UP).onSuccess {
emitTunnelState(it)
}.onFailure {
@ -143,18 +145,28 @@ constructor(
resetBackendStatistics()
}
private suspend fun onBeforeStart(tunnelConfig: TunnelConfig) {
if (_vpnState.value.status == TunnelState.UP) vpnState.value.tunnelConfig?.let { stopTunnel(it) }
private suspend fun onBeforeStart(tunnelConfig: TunnelConfig, background: Boolean) {
if (_vpnState.value.status == TunnelState.UP &&
tunnelConfig != _vpnState.value.tunnelConfig
) {
vpnState.value.tunnelConfig?.let { stopTunnel(it) }
}
if (background) serviceManager.startBackgroundService()
resetBackendStatistics()
appDataRepository.tunnels.save(tunnelConfig.copy(isActive = true))
emitVpnStateConfig(tunnelConfig)
startStatsJob()
Timber.d("Updating start")
serviceManager.requestTunnelTileUpdate()
}
private suspend fun onBeforeStop(tunnelConfig: TunnelConfig) {
cancelStatsJob()
resetBackendStatistics()
appDataRepository.tunnels.save(tunnelConfig.copy(isActive = false))
serviceManager.stopBackgroundService()
Timber.d("UPdating stop")
serviceManager.requestTunnelTileUpdate()
}
private fun emitTunnelState(state: TunnelState) {

View File

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

View File

@ -2,8 +2,6 @@ package com.zaneschepke.wireguardautotunnel.ui
import androidx.lifecycle.ViewModel
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.data.repository.AppDataRepository
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.TunnelState
import com.zaneschepke.wireguardautotunnel.util.Constants
import com.zaneschepke.wireguardautotunnel.util.extensions.TunnelConfigs
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.flow.MutableStateFlow
@ -20,13 +17,10 @@ import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.onCompletion
import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.takeWhile
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.coroutines.plus
import kotlinx.coroutines.withContext
import xyz.teamgravity.pin_lock_compose.PinManager
import javax.inject.Inject
import javax.inject.Provider
@ -38,6 +32,7 @@ constructor(
private val appDataRepository: AppDataRepository,
private val tunnelService: Provider<TunnelService>,
@IoDispatcher private val ioDispatcher: CoroutineDispatcher,
private val serviceManager: ServiceManager,
) : ViewModel() {
val uiState =
@ -46,12 +41,14 @@ constructor(
appDataRepository.tunnels.getTunnelConfigsFlow(),
tunnelService.get().vpnState,
appDataRepository.appState.generalStateFlow,
) { settings, tunnels, tunnelState, generalState ->
serviceManager.autoTunnelActive,
) { settings, tunnels, tunnelState, generalState, autoTunnel ->
AppUiState(
settings,
tunnels,
tunnelState,
generalState,
autoTunnel,
)
}.stateIn(
viewModelScope + ioDispatcher,
@ -95,7 +92,7 @@ constructor(
private suspend fun initAutoTunnel() {
val settings = appDataRepository.settings.getSettings()
if (settings.isAutoTunnelEnabled) ServiceManager.startWatcherService(WireGuardAutoTunnel.instance)
if (settings.isAutoTunnelEnabled) serviceManager.startAutoTunnel(false)
}
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.data.datastore.LocaleStorage
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.TunnelState
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.BottomNavBar
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.BottomNavItem
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.requestTunnelTileServiceStateUpdate
import dagger.hilt.android.AndroidEntryPoint
import timber.log.Timber
import javax.inject.Inject
@AndroidEntryPoint
@ -100,17 +99,17 @@ class MainActivity : AppCompatActivity() {
val navController = rememberNavController()
val rootItemFocusRequester = remember { FocusRequester() }
LaunchedEffect(appUiState.vpnState.status) {
val context = this@MainActivity
when (appUiState.vpnState.status) {
TunnelState.DOWN -> ServiceManager.stopTunnelBackgroundService(context)
else -> Unit
}
context.requestTunnelTileServiceStateUpdate()
LaunchedEffect(appUiState.tunnels) {
Timber.d("Updating launched")
requestTunnelTileServiceStateUpdate()
}
LaunchedEffect(appUiState.autoTunnelActive) {
requestAutoTunnelTileServiceUpdate()
}
with(appUiState.settings) {
LaunchedEffect(isAutoTunnelPaused, isAutoTunnelEnabled) {
LaunchedEffect(isAutoTunnelEnabled) {
this@MainActivity.requestAutoTunnelTileServiceUpdate()
}
}
@ -248,4 +247,3 @@ class MainActivity : AppCompatActivity() {
tunnelService.cancelStatsJob()
}
}

View File

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

View File

@ -16,8 +16,6 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
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.unit.dp

View File

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

View File

@ -24,65 +24,63 @@ import com.zaneschepke.wireguardautotunnel.util.extensions.scaledWidth
@Composable
fun SurfaceSelectionGroupButton(items: List<SelectionItem>) {
Card(
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(8.dp),
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface),
) {
items.mapIndexed { index, item ->
Box(
contentAlignment = Alignment.Center,
modifier = Modifier
.then(item.onClick?.let { Modifier.clickable { it() }} ?: Modifier)
.fillMaxWidth()
Card(
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(8.dp),
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface),
) {
items.mapIndexed { index, item ->
Box(
contentAlignment = Alignment.Center,
modifier = Modifier
.then(item.onClick?.let { Modifier.clickable { it() } } ?: Modifier)
.fillMaxWidth(),
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.fillMaxWidth().padding(vertical = 4.dp.scaledHeight()),
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.fillMaxWidth().padding(vertical = 4.dp.scaledHeight()),
modifier = Modifier
.padding(start = 16.dp.scaledWidth())
.weight(4f, false)
.fillMaxWidth(),
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.padding(start = 16.dp.scaledWidth())
.weight(4f, false)
.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.leadingIcon?.let { icon ->
Icon(
icon,
icon.name,
modifier = Modifier.size(iconSize),
)
}
item.trailing?.let {
Box(
contentAlignment = Alignment.CenterEnd,
modifier = Modifier
.padding(end = 24.dp.scaledWidth(), start = 16.dp.scaledWidth())
.weight(1f),
) {
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 {
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.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.res.stringResource

View File

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

View File

@ -27,7 +27,7 @@ fun VersionLabel() {
color = MaterialTheme.colorScheme.outline,
modifier = Modifier.clickable {
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.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
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.NavGraph.Companion.findStartDestination
import androidx.navigation.compose.currentBackStackEntryAsState
import timber.log.Timber
@Composable
fun BottomNavBar(navController: NavController, bottomNavItems: List<BottomNavItem>) {
@ -34,7 +24,6 @@ fun BottomNavBar(navController: NavController, bottomNavItems: List<BottomNavIte
}
if (showBottomBar) {
NavigationBar(
containerColor = MaterialTheme.colorScheme.surface,
) {

View File

@ -1,7 +1,6 @@
package com.zaneschepke.wireguardautotunnel.ui.common.navigation
import androidx.compose.runtime.compositionLocalOf
import androidx.compose.runtime.staticCompositionLocalOf
import androidx.compose.ui.focus.FocusRequester
import androidx.navigation.NavHostController
@ -10,4 +9,3 @@ val LocalNavController = compositionLocalOf<NavHostController> {
}
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)
},
navigationIcon = {
if(showBack) IconButton(onClick = { navController.popBackStack() }) {
val icon = Icons.AutoMirrored.Outlined.ArrowBack
Icon(
imageVector = icon,
contentDescription = icon.name,
)
if (showBack) {
IconButton(onClick = { navController.popBackStack() }) {
val icon = Icons.AutoMirrored.Outlined.ArrowBack
Icon(
imageVector = icon,
contentDescription = icon.name,
)
}
}
},
actions = {

View File

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

View File

@ -1,7 +1,11 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.main
import android.content.Intent
import android.net.Uri
import android.net.VpnService
import android.provider.Settings
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.ActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity.RESULT_OK
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.VpnDeniedDialog
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.openWebUrl
import com.zaneschepke.wireguardautotunnel.util.extensions.startTunnelBackground
@OptIn(ExperimentalFoundationApi::class)
@Composable
@ -75,13 +79,19 @@ fun MainScreen(viewModel: MainViewModel = hiltViewModel(), uiState: AppUiState)
NestedScrollListener({ isFabVisible = false }, { isFabVisible = true })
}
val vpnActivityResultState =
val vpnActivity =
rememberLauncherForActivityResult(
ActivityResultContracts.StartActivityForResult(),
onResult = {
if (it.resultCode != RESULT_OK) showVpnPermissionDialog = true
},
)
val batteryActivity =
rememberLauncherForActivityResult(
ActivityResultContracts.StartActivityForResult(),
) { result: ActivityResult ->
viewModel.setBatteryOptimizeDisableShown()
}
val tunnelFileImportResultLauncher = rememberFileImportLauncherForResult(onNoFileExplorer = {
snackbar.showMessage(
@ -104,7 +114,7 @@ fun MainScreen(viewModel: MainViewModel = hiltViewModel(), uiState: AppUiState)
InfoDialog(
onDismiss = { showDeleteTunnelAlertDialog = false },
onAttest = {
selectedTunnel?.let { viewModel.onDelete(it, context) }
selectedTunnel?.let { viewModel::onDelete }
showDeleteTunnelAlertDialog = false
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) {
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 (uiState.settings.isKernelEnabled) {
context.startTunnelBackground(tunnel.id)
} else {
viewModel.onTunnelStart(tunnel)
}
viewModel.onTunnelStart(tunnel, uiState.settings.isKernelEnabled)
}
Scaffold(
@ -137,34 +167,38 @@ fun MainScreen(viewModel: MainViewModel = hiltViewModel(), uiState: AppUiState)
},
floatingActionButtonPosition = FabPosition.End,
floatingActionButton = {
if(!isRunningOnTv) ScrollDismissFab({
val icon = Icons.Filled.Add
Icon(
imageVector = icon,
contentDescription = icon.name,
tint = MaterialTheme.colorScheme.onPrimary,
)
}, isVisible = isFabVisible, onClick = {
showBottomSheet = true
})
if (!isRunningOnTv) {
ScrollDismissFab({
val icon = Icons.Filled.Add
Icon(
imageVector = icon,
contentDescription = icon.name,
tint = MaterialTheme.colorScheme.onPrimary,
)
}, isVisible = isFabVisible, onClick = {
showBottomSheet = true
})
}
},
topBar = {
if(isRunningOnTv) TopNavBar(
showBack = false,
title = stringResource(R.string.app_name),
trailing = {
IconButton(onClick = {
showBottomSheet = true
}) {
val icon = Icons.Outlined.Add
Icon(
imageVector = icon,
contentDescription = icon.name,
)
}
}
)
}
if (isRunningOnTv) {
TopNavBar(
showBack = false,
title = stringResource(R.string.app_name),
trailing = {
IconButton(onClick = {
showBottomSheet = true
}) {
val icon = Icons.Outlined.Add
Icon(
imageVector = icon,
contentDescription = icon.name,
)
}
},
)
}
},
) {
TunnelImportSheet(
showBottomSheet,
@ -196,7 +230,9 @@ fun MainScreen(viewModel: MainViewModel = hiltViewModel(), uiState: AppUiState)
}
} else {
item {
AutoTunnelRowItem(uiState.settings, { viewModel.onToggleAutoTunnel(context) })
AutoTunnelRowItem(uiState, {
onAutoTunnelToggle()
})
}
}
items(

View File

@ -30,26 +30,24 @@ import timber.log.Timber
import java.io.InputStream
import java.util.zip.ZipInputStream
import javax.inject.Inject
import javax.inject.Provider
@HiltViewModel
class MainViewModel
@Inject
constructor(
private val appDataRepository: AppDataRepository,
val tunnelService: TunnelService,
private val tunnelService: Provider<TunnelService>,
@IoDispatcher private val ioDispatcher: CoroutineDispatcher,
private val serviceManager: ServiceManager,
) : ViewModel() {
private fun stopWatcherService(context: Context) {
ServiceManager.stopWatcherService(context)
}
fun onDelete(tunnel: TunnelConfig, context: Context) {
fun onDelete(tunnel: TunnelConfig) {
viewModelScope.launch {
val settings = appDataRepository.settings.getSettings()
val isPrimary = tunnel.isPrimaryTunnel
if (appDataRepository.tunnels.count() == 1 || isPrimary) {
stopWatcherService(context)
serviceManager.stopAutoTunnel()
resetTunnelSetting(settings)
}
appDataRepository.tunnels.delete(tunnel)
@ -69,14 +67,14 @@ constructor(
appDataRepository.appState.setTunnelStatsExpanded(expanded)
}
fun onTunnelStart(tunnelConfig: TunnelConfig) = viewModelScope.launch {
fun onTunnelStart(tunnelConfig: TunnelConfig, background: Boolean) = viewModelScope.launch {
Timber.i("Starting tunnel ${tunnelConfig.name}")
tunnelService.startTunnel(tunnelConfig)
tunnelService.get().startTunnel(tunnelConfig, background)
}
fun onTunnelStop(tunnel: TunnelConfig) = viewModelScope.launch {
Timber.i("Stopping active tunnel")
tunnelService.stopTunnel(tunnel)
tunnelService.get().stopTunnel(tunnel)
}
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()
if (settings.isAutoTunnelEnabled) {
ServiceManager.stopWatcherService(context)
val toggled = !settings.isAutoTunnelEnabled
if (toggled) {
serviceManager.startAutoTunnel(false)
} else {
ServiceManager.startWatcherService(context)
serviceManager.stopAutoTunnel()
}
appDataRepository.settings.save(
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) {
val stream = getInputStreamFromUri(uri, context) ?: throw FileReadException
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.rounded.Bolt
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.scale
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.unit.dp
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.button.ScaledSwitch
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.scaledHeight
@Composable
fun AutoTunnelRowItem(settings: Settings, onToggle: () -> Unit) {
fun AutoTunnelRowItem(appUiState: AppUiState, onToggle: () -> Unit) {
val context = LocalContext.current
val itemFocusRequester = remember { FocusRequester() }
ExpandingRowListItem(
@ -40,7 +35,7 @@ fun AutoTunnelRowItem(settings: Settings, onToggle: () -> Unit) {
Modifier
.size(16.dp.scaledHeight()).scale(1.5f),
tint =
if (!settings.isAutoTunnelEnabled) {
if (!appUiState.autoTunnelActive) {
Color.Gray
} else {
SilverTree
@ -50,10 +45,10 @@ fun AutoTunnelRowItem(settings: Settings, onToggle: () -> Unit) {
text = stringResource(R.string.auto_tunneling),
trailing = {
ScaledSwitch(
settings.isAutoTunnelEnabled,
appUiState.settings.isAutoTunnelEnabled,
onClick = {
onToggle()
}
},
)
},
onClick = {

View File

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

View File

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

View File

@ -46,14 +46,18 @@ fun TunnelStatisticsRow(statistics: TunnelStatistics?, tunnelConfig: TunnelConfi
Column(
verticalArrangement = Arrangement.spacedBy(10.dp),
) {
Text(stringResource(R.string.peer).lowercase() + ": $peerId", style = MaterialTheme.typography.bodySmall)
Text("tx: $peerTxMB MB", style = MaterialTheme.typography.bodySmall)
Text(
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(
verticalArrangement = Arrangement.spacedBy(10.dp),
) {
Text(stringResource(R.string.handshake) + ": $handshake", style = MaterialTheme.typography.bodySmall)
Text("rx: $peerRxMB MB", 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, 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.ExperimentalLayoutApi
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
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.outlined.Edit
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.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
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.unit.dp
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.util.extensions.scaledHeight
import com.zaneschepke.wireguardautotunnel.util.extensions.scaledWidth
import kotlinx.coroutines.delay
@OptIn(ExperimentalLayoutApi::class)
@Composable
@ -84,7 +71,7 @@ fun OptionsScreen(optionsViewModel: OptionsViewModel = hiltViewModel(), appUiSta
)
}
})
}
},
) {
Column(
horizontalAlignment = Alignment.Start,
@ -101,7 +88,12 @@ fun OptionsScreen(optionsViewModel: OptionsViewModel = hiltViewModel(), appUiSta
listOf(
SelectionItem(
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 = {
Text(
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) },
),
SelectionItem(
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) },
),
SelectionItem(
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) },
),
SelectionItem(
title = {
@ -180,24 +172,25 @@ fun OptionsScreen(optionsViewModel: OptionsViewModel = hiltViewModel(), appUiSta
style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface),
)
}
}
}
},
description = {
TrustedNetworkTextBox(
config.tunnelNetworks, onDelete = { optionsViewModel.onDeleteRunSSID(it, config) },
config.tunnelNetworks,
onDelete = { optionsViewModel.onDeleteRunSSID(it, config) },
currentText = currentText,
onSave = { optionsViewModel.onSaveRunSSID(it, config) },
onValueChange = { currentText = it },
supporting = { if(appUiState.generalState.isWildcardsEnabled) {
WildcardsLabel()
}}
supporting = {
if (appUiState.settings.isWildcardsEnabled) {
WildcardsLabel()
}
},
)
},
)
)
),
),
)
}
}

View File

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

View File

@ -1,26 +1,13 @@
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.focusable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ExperimentalLayoutApi
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxSize
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.verticalScroll
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.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.zaneschepke.wireguardautotunnel.R
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.prompt.AuthorizationPrompt
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.LocationServicesDialog
import com.zaneschepke.wireguardautotunnel.ui.theme.topPadding
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.launchVpnSettings
import com.zaneschepke.wireguardautotunnel.util.extensions.scaledHeight
@ -91,114 +73,7 @@ fun SettingsScreen(viewModel: SettingsViewModel = hiltViewModel(), appViewModel:
val isRunningOnTv = remember { context.isRunningOnTv() }
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 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) {
AuthorizationPrompt(
@ -231,12 +106,18 @@ fun SettingsScreen(viewModel: SettingsViewModel = hiltViewModel(), appViewModel:
.padding(top = topPadding)
.padding(bottom = 40.dp.scaledHeight())
.padding(horizontal = 24.dp.scaledWidth())
.then(if(!isRunningOnTv) Modifier.clickable(
indication = null,
interactionSource = interactionSource,
) {
focusManager.clearFocus()
} else Modifier)
.then(
if (!isRunningOnTv) {
Modifier.clickable(
indication = null,
interactionSource = interactionSource,
) {
focusManager.clearFocus()
}
} else {
Modifier
},
),
) {
SurfaceSelectionGroupButton(
listOf(
@ -250,73 +131,78 @@ fun SettingsScreen(viewModel: SettingsViewModel = hiltViewModel(), appViewModel:
)
},
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)
},
trailing = {
ForwardButton(Modifier.focusable().focusRequester(rootFocusRequester)) { navController.navigate(Route.AutoTunnel) }
},
)
)
),
),
)
SurfaceSelectionGroupButton(
buildList {
if (!isRunningOnTv) addAll(
listOf(
SelectionItem(
Icons.Filled.AppShortcut,
{
ScaledSwitch(
uiState.settings.isShortcutsEnabled,
onClick = { viewModel.onToggleShortcutsEnabled() },
)
},
title = {
Text(
stringResource(R.string.enabled_app_shortcuts),
style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface))
},
onClick = { viewModel.onToggleShortcutsEnabled() }
if (!isRunningOnTv) {
addAll(
listOf(
SelectionItem(
Icons.Filled.AppShortcut,
{
ScaledSwitch(
uiState.settings.isShortcutsEnabled,
onClick = { viewModel.onToggleShortcutsEnabled() },
)
},
title = {
Text(
stringResource(R.string.enabled_app_shortcuts),
style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface),
)
},
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(
SelectionItem(
Icons.Outlined.Restore,
@ -329,25 +215,27 @@ fun SettingsScreen(viewModel: SettingsViewModel = hiltViewModel(), appViewModel:
title = {
Text(
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(
listOf(SelectionItem(
Icons.AutoMirrored.Outlined.ViewQuilt,
title = { Text(stringResource(R.string.appearance), style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface)) },
onClick = {
navController.navigate(Route.Appearance)
},
trailing = {
ForwardButton { navController.navigate(Route.Appearance) }
},
),
listOf(
SelectionItem(
Icons.AutoMirrored.Outlined.ViewQuilt,
title = { Text(stringResource(R.string.appearance), style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface)) },
onClick = {
navController.navigate(Route.Appearance)
},
trailing = {
ForwardButton { navController.navigate(Route.Appearance) }
},
),
SelectionItem(
Icons.Outlined.Notifications,
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(
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 = {
ScaledSwitch(
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 = {
if (uiState.tunnels.isEmpty()) return@SelectionItem context.showToast(R.string.tunnel_required)
showAuthPrompt = true
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)
),
)
},
onClick = {
viewModel.onToggleKernelMode()
},
),
),
)
}
// Surface(
// tonalElevation = 2.dp,
// shadowElevation = 2.dp,
// shape = RoundedCornerShape(12.dp),
// color = MaterialTheme.colorScheme.surface,
// modifier =
// (
// if (isRunningOnTv) {
// Modifier
// .height(IntrinsicSize.Min)
// .fillMaxWidth(fillMaxWidth)
// .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))
// }
// }
// }
// }
// }
if (!isRunningOnTv) {
SurfaceSelectionGroupButton(
listOf(
SelectionItem(
Icons.Outlined.FolderZip,
title = {
Text(
stringResource(R.string.export_configs),
style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface),
)
},
onClick = {
if (uiState.tunnels.isEmpty()) return@SelectionItem context.showToast(R.string.tunnel_required)
showAuthPrompt = true
},
),
),
)
}
}
}

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.repository.AppDataRepository
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.util.Constants
import com.zaneschepke.wireguardautotunnel.util.FileUtils
import com.zaneschepke.wireguardautotunnel.util.StringValue
import com.zaneschepke.wireguardautotunnel.util.extensions.launchShareFile
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.time.Instant
@ -39,16 +34,6 @@ constructor(
@IoDispatcher private val ioDispatcher: CoroutineDispatcher,
) : 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()
.stateIn(viewModelScope, SharingStarted.Eagerly, Settings())
@ -56,10 +41,6 @@ constructor(
appDataRepository.appState.setLocationDisclosureShown(true)
}
fun setBatteryOptimizeDisableShown() = viewModelScope.launch {
appDataRepository.appState.setBatteryOptimizationDisableShown(true)
}
fun onToggleAlwaysOnVPN() = viewModelScope.launch {
with(settings.value) {
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 {
with(settings.value) {
if (!isKernelEnabled) {
requestRoot().onSuccess {
if (!isKernelSupported()) return@onSuccess SnackbarController.showMessage(StringValue.StringResource(R.string.kernel_not_supported))
appDataRepository.settings.save(
copy(
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> {
return withContext(ioDispatcher) {
kotlin.runCatching {
@ -158,10 +116,6 @@ constructor(
}
}
fun onRequestRoot() = viewModelScope.launch {
requestRoot()
}
fun exportAllConfigs(context: Context) = viewModelScope.launch {
kotlin.runCatching {
val shareFile = fileUtils.createNewShareFile("wg-export_${Instant.now().epochSecond}.zip")

View File

@ -32,8 +32,8 @@ fun AppearanceScreen() {
Scaffold(
topBar = {
TopNavBar(stringResource(R.string.appearance))
}
){
},
) {
Column(
horizontalAlignment = Alignment.Start,
verticalArrangement = Arrangement.spacedBy(24.dp.scaledHeight(), Alignment.Top),
@ -51,7 +51,7 @@ fun AppearanceScreen() {
onClick = { navController.navigate(Route.Language) },
trailing = {
ForwardButton { navController.navigate(Route.Language) }
}
},
),
),
)
@ -63,7 +63,7 @@ fun AppearanceScreen() {
onClick = { navController.navigate(Route.Display) },
trailing = {
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.Column
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxSize
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.runtime.Composable
import androidx.compose.ui.Alignment
@ -24,11 +21,10 @@ import com.zaneschepke.wireguardautotunnel.util.extensions.scaledWidth
@Composable
fun DisplayScreen(appUiState: AppUiState, viewModel: DisplayViewModel = hiltViewModel()) {
Scaffold(
topBar = {
TopNavBar(stringResource(R.string.display_theme))
}
},
) {
Column(
horizontalAlignment = Alignment.Start,

View File

@ -12,7 +12,7 @@ import javax.inject.Inject
class DisplayViewModel
@Inject
constructor(
private val appStateRepository: AppStateRepository
private val appStateRepository: AppStateRepository,
) : ViewModel() {
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.navigationBars
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.items
@ -69,7 +68,7 @@ fun LanguageScreen(localeStorage: LocaleStorage) {
Scaffold(
topBar = {
TopNavBar(stringResource(R.string.language))
}
},
) {
LazyColumn(
horizontalAlignment = Alignment.CenterHorizontally,

View File

@ -1,6 +1,7 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.settings.autotunnel
import android.Manifest
import android.os.Build
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
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.size
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.NetworkPing
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.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
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.screens.settings.autotunnel.components.TrustedNetworkTextBox
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.LocationServicesDialog
import com.zaneschepke.wireguardautotunnel.ui.theme.iconSize
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.scaledHeight
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 showLocationDialog by remember { mutableStateOf(false) }
LaunchedEffect(uiState.settings.trustedNetworkSSIDs) {
currentText = ""
fun checkFineLocationGranted() {
isBackgroundLocationGranted = fineLocationState.status.isGranted
}
fun onAutoTunnelWifiChecked() {
if (uiState.settings.isTunnelOnWifiEnabled) viewModel.onToggleTunnelOnWifi().also { return }
when (false) {
isBackgroundLocationGranted -> 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(
contentWindowInsets = WindowInsets(0.dp),
topBar = {
TopNavBar(stringResource(R.string.auto_tunneling))
}
},
) {
Column(
horizontalAlignment = Alignment.Start,
@ -97,34 +130,60 @@ fun AutoTunnelScreen(uiState: AppUiState, viewModel: AutoTunnelViewModel = hiltV
) {
SurfaceSelectionGroupButton(
buildList {
add(
SelectionItem(
Icons.Outlined.Wifi,
title = {
Text(
stringResource(R.string.tunnel_on_wifi),
style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface)
)
},
description = {
},
trailing = {
ScaledSwitch(
enabled = !uiState.settings.isAlwaysOnVpnEnabled,
checked = uiState.settings.isTunnelOnWifiEnabled,
onClick = {
if (!uiState.settings.isTunnelOnWifiEnabled || uiState.isRooted) viewModel.onToggleTunnelOnWifi()
.also { return@ScaledSwitch }
onAutoTunnelWifiChecked()
},
)
},
onClick = {
if (!uiState.settings.isTunnelOnWifiEnabled || uiState.isRooted) viewModel.onToggleTunnelOnWifi()
.also { return@SelectionItem }
onAutoTunnelWifiChecked()
}
)
addAll(
listOf(
SelectionItem(
Icons.Outlined.Wifi,
title = {
Text(
stringResource(R.string.tunnel_on_wifi),
style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface),
)
},
description = {
},
trailing = {
ScaledSwitch(
enabled = !uiState.settings.isAlwaysOnVpnEnabled,
checked = uiState.settings.isTunnelOnWifiEnabled,
onClick = {
if (uiState.settings.isWifiNameByShellEnabled) viewModel.onToggleTunnelOnWifi().also { return@ScaledSwitch }
onAutoTunnelWifiChecked()
},
)
},
onClick = {
if (uiState.settings.isWifiNameByShellEnabled) viewModel.onToggleTunnelOnWifi().also { return@SelectionItem }
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) {
addAll(
@ -134,15 +193,15 @@ fun AutoTunnelScreen(uiState: AppUiState, viewModel: AutoTunnelViewModel = hiltV
title = {
Text(
stringResource(R.string.use_wildcards),
style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface)
style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface),
)
},
description = {
LearnMoreLinkLabel({context.openWebUrl(it)}, stringResource(id = R.string.docs_wildcards))
LearnMoreLinkLabel({ context.openWebUrl(it) }, stringResource(id = R.string.docs_wildcards))
},
trailing = {
ScaledSwitch(
checked = uiState.generalState.isWildcardsEnabled,
checked = uiState.settings.isWildcardsEnabled,
onClick = {
viewModel.onToggleWildcards()
},
@ -150,7 +209,7 @@ fun AutoTunnelScreen(uiState: AppUiState, viewModel: AutoTunnelViewModel = hiltV
},
onClick = {
viewModel.onToggleWildcards()
}
},
),
SelectionItem(
title = {
@ -183,87 +242,89 @@ fun AutoTunnelScreen(uiState: AppUiState, viewModel: AutoTunnelViewModel = hiltV
style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface),
)
}
}
}
},
description = {
TrustedNetworkTextBox(
uiState.settings.trustedNetworkSSIDs, onDelete = viewModel::onDeleteTrustedSSID,
uiState.settings.trustedNetworkSSIDs,
onDelete = viewModel::onDeleteTrustedSSID,
currentText = currentText,
onSave = viewModel::onSaveTrustedSSID,
onValueChange = { currentText = it },
supporting = { if(uiState.generalState.isWildcardsEnabled) {
WildcardsLabel()
}}
supporting = {
if (uiState.settings.isWildcardsEnabled) {
WildcardsLabel()
}
},
)
},
)
))
),
),
)
}
}
},
)
SurfaceSelectionGroupButton(
listOf(
SelectionItem(
Icons.Outlined.SignalCellular4Bar,
title = {
Text(
stringResource(R.string.tunnel_mobile_data),
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()
}
listOf(
SelectionItem(
Icons.Outlined.SignalCellular4Bar,
title = {
Text(
stringResource(R.string.tunnel_mobile_data),
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()
},
),
),
)
}
}
}

View File

@ -2,22 +2,29 @@ package com.zaneschepke.wireguardautotunnel.ui.screens.settings.autotunnel
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.wireguard.android.util.RootShell
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.data.domain.Settings
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.util.StringValue
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import javax.inject.Inject
import javax.inject.Provider
@HiltViewModel
class AutoTunnelViewModel
@Inject
constructor(
private val appDataRepository: AppDataRepository,
private val rootShell: Provider<RootShell>,
@IoDispatcher private val ioDispatcher: CoroutineDispatcher,
) : ViewModel() {
private val settings = appDataRepository.settings.getSettingsFlow()
@ -37,29 +44,53 @@ constructor(
with(settings.value) {
appDataRepository.settings.save(
copy(
isTunnelOnMobileDataEnabled = !this.isTunnelOnMobileDataEnabled,
isTunnelOnMobileDataEnabled = !isTunnelOnMobileDataEnabled,
),
)
}
}
fun onToggleWildcards() = viewModelScope.launch {
val wildcards = appDataRepository.appState.isWildcardsEnabled()
appDataRepository.appState.setWildcardsEnabled(
!wildcards
)
with(settings.value) {
appDataRepository.settings.save(
copy(
isWildcardsEnabled = !isWildcardsEnabled,
),
)
}
}
fun onDeleteTrustedSSID(ssid: String) = viewModelScope.launch {
with(settings.value) {
appDataRepository.settings.save(
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 {
with(settings.value) {
appDataRepository.settings.save(

View File

@ -32,9 +32,16 @@ import com.zaneschepke.wireguardautotunnel.util.extensions.scaledWidth
@OptIn(ExperimentalLayoutApi::class)
@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
Column(verticalArrangement = Arrangement.spacedBy(10.dp.scaledHeight())){
Column(verticalArrangement = Arrangement.spacedBy(10.dp.scaledHeight())) {
FlowRow(
modifier =
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 com.zaneschepke.wireguardautotunnel.R
@Composable
fun WildcardsLabel() {
Text(

View File

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

View File

@ -12,7 +12,7 @@ import androidx.compose.ui.text.withStyle
import com.zaneschepke.wireguardautotunnel.R
@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
val gettingStarted =
buildAnnotatedString {

View File

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

View File

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

View File

@ -4,11 +4,8 @@ import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxSize
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.itemsIndexed
import androidx.compose.foundation.lazy.rememberLazyListState

View File

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

View File

@ -40,18 +40,15 @@ enum class Theme {
AUTOMATIC,
LIGHT,
DARK,
DYNAMIC
DYNAMIC,
}
@Composable
fun WireguardAutoTunnelTheme(
theme: Theme = Theme.AUTOMATIC,
content: @Composable () -> Unit,
) {
fun WireguardAutoTunnelTheme(theme: Theme = Theme.AUTOMATIC, content: @Composable () -> Unit) {
val context = LocalContext.current
var isDark = isSystemInDarkTheme()
val autoTheme = if(isDark) DarkColorScheme else LightColorScheme
val colorScheme = when(theme) {
val autoTheme = if (isDark) DarkColorScheme else LightColorScheme
val colorScheme = when (theme) {
Theme.AUTOMATIC -> autoTheme
Theme.DARK -> {
isDark = true
@ -68,7 +65,9 @@ fun WireguardAutoTunnelTheme(
} else {
dynamicLightColorScheme(context)
}
} else autoTheme
} else {
autoTheme
}
}
}
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.Context
import android.content.Context.POWER_SERVICE
import android.content.Intent
import android.content.pm.PackageManager
import android.location.LocationManager
import android.net.Uri
import android.os.PowerManager
import android.provider.Settings
import android.service.quicksettings.TileService
import android.widget.Toast
@ -13,7 +15,6 @@ import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.TextUnit
import androidx.core.location.LocationManagerCompat
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.receiver.BackgroundActionReceiver
import com.zaneschepke.wireguardautotunnel.service.tile.AutoTunnelControlTile
import com.zaneschepke.wireguardautotunnel.service.tile.TunnelControlTile
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
get() = theme.obtainStyledAttributes(intArrayOf(android.R.attr.actionBarSize))
.let { attrs -> attrs.getDimension(0, 0F).toInt().also { attrs.recycle() } }
@ -66,7 +72,7 @@ fun Context.resizeWidth(dp: Dp): Dp {
}
fun Context.launchNotificationSettings() {
if(isRunningOnTv()) return launchAppSettings()
if (isRunningOnTv()) return launchAppSettings()
val settingsIntent: Intent = Intent(Settings.ACTION_APP_NOTIFICATION_SETTINGS)
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
.putExtra(Settings.EXTRA_APP_PACKAGE, packageName)
@ -159,23 +165,23 @@ fun Context.launchAppSettings() {
}
}
fun Context.startTunnelBackground(tunnelId: Int) {
sendBroadcast(
Intent(this, BackgroundActionReceiver::class.java).apply {
action = BackgroundActionReceiver.ACTION_CONNECT
putExtra(BackgroundActionReceiver.TUNNEL_ID_EXTRA_KEY, tunnelId)
},
)
}
fun Context.stopTunnelBackground(tunnelId: Int) {
sendBroadcast(
Intent(this, BackgroundActionReceiver::class.java).apply {
action = BackgroundActionReceiver.ACTION_DISCONNECT
putExtra(BackgroundActionReceiver.TUNNEL_ID_EXTRA_KEY, tunnelId)
},
)
}
// fun Context.startTunnelBackground(tunnelId: Int) {
// sendBroadcast(
// Intent(this, BackgroundActionReceiver::class.java).apply {
// action = BackgroundActionReceiver.ACTION_CONNECT
// putExtra(BackgroundActionReceiver.TUNNEL_ID_EXTRA_KEY, tunnelId)
// },
// )
// }
//
// fun Context.stopTunnelBackground(tunnelId: Int) {
// sendBroadcast(
// Intent(this, BackgroundActionReceiver::class.java).apply {
// action = BackgroundActionReceiver.ACTION_DISCONNECT
// putExtra(BackgroundActionReceiver.TUNNEL_ID_EXTRA_KEY, tunnelId)
// },
// )
// }
fun Context.requestTunnelTileServiceStateUpdate() {
TileService.requestListeningState(

View File

@ -28,9 +28,9 @@ fun String.extractNameAndNumber(): Pair<String, Int>? {
}
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")
val includedValues = this.filter { !it.startsWith("!") }.map { it.toRegexWithWildcards() }
val includedValues = this.filter { !it.startsWith("!") }.map { it.transformWildcardsToRegex() }
Timber.d("Included values: $includedValues")
val matches = includedValues.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()
}
fun String.toRegexWithWildcards(): Regex {
return this.replace("*", ".*").replace("?", ".").toRegex()
fun String.transformWildcardsToRegex(): Regex {
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.config.Peer
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.ui.theme.Straw
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.NumberUtils
import org.amnezia.awg.config.Config
@ -21,6 +23,10 @@ fun TunnelStatistics.PeerStats.latestHandshakeSeconds(): Long? {
return NumberUtils.getSecondsBetweenTimestampAndNow(this.latestHandshakeEpochMillis)
}
fun VpnState.isDown(): Boolean {
return this.status == TunnelState.DOWN
}
fun TunnelStatistics.PeerStats.handshakeStatus(): HandshakeStatus {
// TODO add never connected status after duration
return this.latestHandshakeSeconds().let {

View File

@ -220,4 +220,9 @@
<string name="use_wildcards">Use name wildcards</string>
<string name="learn_more">Learn more</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>

View File

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

View File

@ -1,7 +1,7 @@
object Constants {
const val VERSION_NAME = "3.5.3"
const val VERSION_NAME = "3.6.0"
const val JVM_TARGET = "17"
const val VERSION_CODE = 35300
const val VERSION_CODE = 36000
const val TARGET_SDK = 34
const val MIN_SDK = 26
const val APP_ID = "com.zaneschepke.wireguardautotunnel"

View File

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