Compare commits

..

12 Commits

Author SHA1 Message Date
Zane Schepke e77966d70a ci: fix notification workflow on release 2025-04-08 21:39:41 -04:00
Zane Schepke dcf213b63c fix: signing 2025-04-08 21:30:11 -04:00
Zane Schepke ca10586604 chore: bump version with notes 2025-04-08 21:14:39 -04:00
Zane Schepke 53480b0233 fix: back gesture issues on some devices 2025-04-08 21:04:22 -04:00
Zane Schepke 84de3a3991 fix: default to phone preferred dns server
closes #663
2025-04-08 20:51:10 -04:00
Zane Schepke 820ff8a9ad ci: add matrix, fix release notifications 2025-04-08 20:45:57 -04:00
Zane Schepke 1c0b54a8e4 feat: copy wifi name to clipboard
closes #65
2025-04-08 19:11:24 -04:00
Zane Schepke 75364f323c fix: tv navigation bug
closes #666
2025-04-08 18:57:09 -04:00
Zane Schepke b87aa75bf0 feat: add custom intent app control 2025-04-08 18:49:27 -04:00
Zane Schepke c59e7d7637 fix: service shutdown on abrupt shutdown 2025-04-07 13:56:40 -04:00
Zane Schepke 28ef1a7683 fix: tun start bug after bad shutdown 2025-04-07 05:18:26 -04:00
Zane Schepke a5aadb42ed fix: ipv6 static regex bug 2025-04-06 18:33:09 -04:00
44 changed files with 435 additions and 107 deletions
+15 -1
View File
@@ -17,4 +17,18 @@ jobs:
status: ${{ github.event.issue.state }} - #${{ github.event.issue.number }} ${{ github.event.issue.title }}
https://github.com/zaneschepke/wgtunnel/issues/${{ github.event.issue.number }}'
curl -s -X POST 'https://api.telegram.org/bot${{ secrets.TELEGRAM_TOKEN }}/sendMessage' \
-d "chat_id=${{ secrets.TELEGRAM_TO }}&text=${msg_text}&message_thread_id=${{ secrets.TELEGRAM_TOPIC }}"
-d "chat_id=${{ secrets.TELEGRAM_TO }}&text=${msg_text}&message_thread_id=${{ vars.TELEGRAM_ACTIVITY_TOPIC }}"
- name: Send Matrix Message
run: |
msg_text='${{ github.actor }} updated an issue:
status: ${{ github.event.issue.state }} - #${{ github.event.issue.number }} ${{ github.event.issue.title }}
https://github.com/zaneschepke/wgtunnel/issues/${{ github.event.issue.number }}'
curl -s -X POST \
-H "Authorization: Bearer ${{ secrets.MATRIX_TOKEN }}" \
-H "Content-Type: application/json" \
-d '{
"msgtype": "m.text",
"body": "'"$msg_text"'"
}' \
"https://matrix.yourserver.com/_matrix/client/v3/rooms/${{ vars.MATRIX_ACTIVITY_TOPIC }}/send/m.room.message/$(date +%s)"
+18 -5
View File
@@ -1,12 +1,10 @@
name: on-publish
on:
release:
types: [ published ]
repository_dispatch:
types: [ publish-release ]
jobs:
on-publish:
name: On publish
runs-on: ubuntu-latest
@@ -18,4 +16,19 @@ jobs:
${{ github.event.release.body }}
https://github.com/zaneschepke/wgtunnel/releases/tag/${{ github.event.release.tag_name }}'
curl -s -X POST 'https://api.telegram.org/bot${{ secrets.TELEGRAM_TOKEN }}/sendMessage' \
-d "chat_id=${{ secrets.TELEGRAM_TO }}&text=${msg_text}&message_thread_id=${{ secrets.TELEGRAM_TOPIC }}"
-d "chat_id=${{ secrets.TELEGRAM_TO }}&text=${msg_text}&message_thread_id=${{ vars.TELEGRAM_RELEASE_TOPIC }}"
- name: Send Matrix Message
run: |
msg_text='${{ github.actor }} published a new release:
Release: ${{ github.event.release.tag_name }}
${{ github.event.release.body }}
https://github.com/zaneschepke/wgtunnel/releases/tag/${{ github.event.release.tag_name }}'
curl -s -X POST \
-H "Authorization: Bearer ${{ secrets.MATRIX_TOKEN }}" \
-H "Content-Type: application/json" \
-d '{
"msgtype": "m.text",
"body": "'"$msg_text"'"
}' \
"https://matrix.yourserver.com/_matrix/client/v3/rooms/${{ vars.MATRIX_RELEASE_TOPIC }}/send/m.room.message/$(date +%s)"
+8
View File
@@ -193,6 +193,14 @@ jobs:
files: |
${{ github.workspace }}/temp/*
# notify socials
- name: Trigger on-publish workflow
if: ${{ inputs.release_type == 'release' }}
uses: peter-evans/repository-dispatch@v3
with:
token: ${{ secrets.PAT }}
event-type: publish-release
publish-fdroid:
runs-on: ubuntu-latest
needs:
+21 -3
View File
@@ -20,6 +20,8 @@
<permission
android:name="${applicationId}.permission.CONTROL_TUNNELS"
android:label="@string/app_permission_title"
android:description="@string/app_permission_description"
android:icon="@mipmap/ic_launcher"
android:protectionLevel="dangerous" />
@@ -45,6 +47,7 @@
<queries>
<intent>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent>
</queries>
<application
@@ -84,7 +87,7 @@
<activity
android:name=".core.shortcut.ShortcutsActivity"
android:enabled="true"
android:exported="true"
android:exported="false"
android:noHistory="true"
android:excludeFromRecents="true"
android:finishOnTaskLaunch="true"
@@ -161,16 +164,17 @@
<action android:name="android.net.VpnService" />
</intent-filter>
</service>
<receiver
android:name=".core.broadcast.RestartReceiver"
android:enabled="true"
android:exported="true">
android:exported="false"
android:directBootAware="true">
<intent-filter>
<category android:name="android.intent.category.DEFAULT" />
<action android:name="android.intent.action.BOOT_COMPLETED" />
<action android:name="android.intent.action.QUICKBOOT_POWERON" />
<action android:name="com.htc.intent.action.QUICKBOOT_POWERON" />
<action android:name="android.intent.action.LOCKED_BOOT_COMPLETED" />
<action android:name="android.intent.action.MY_PACKAGE_REPLACED" />
</intent-filter>
</receiver>
@@ -182,6 +186,20 @@
<action android:name="com.wireguard.android.action.REFRESH_TUNNEL_STATES" />
</intent-filter>
</receiver>
<!--custom security solution for easier user integration-->
<receiver
android:name=".core.broadcast.RemoteControlReceiver"
android:enabled="true"
android:exported="true" tools:ignore="ExportedReceiver">
<intent-filter>
<action android:name="com.zaneschepke.wireguardautotunnel.START_TUNNEL" />
<action android:name="com.zaneschepke.wireguardautotunnel.STOP_TUNNEL" />
<action android:name="com.zaneschepke.wireguardautotunnel.START_AUTO_TUNNEL" />
<action android:name="com.zaneschepke.wireguardautotunnel.STOP_AUTO_TUNNEL" />
<action android:name="com.zaneschepke.wireguardautotunnel.START_KILL_SWITCH" />
<action android:name="com.zaneschepke.wireguardautotunnel.STOP_KILL_SWITCH" />
</intent-filter>
</receiver>
<receiver
android:name=".core.broadcast.NotificationActionReceiver"
android:exported="false"
@@ -81,7 +81,8 @@ import com.zaneschepke.wireguardautotunnel.ui.screens.settings.appearance.Appear
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.appearance.display.DisplayScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.appearance.language.LanguageScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.autotunnel.AutoTunnelScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.autotunnel.advanced.AdvancedScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.autotunnel.advanced.AutoTunnelAdvancedScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.advanced.SettingsAdvancedScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.disclosure.LocationDisclosureScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.killswitch.KillSwitchScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.support.SupportScreen
@@ -220,7 +221,7 @@ class MainActivity : AppCompatActivity() {
}
CompositionLocalProvider(LocalNavController provides navController) {
WireguardAutoTunnelTheme(theme = appUiState.generalState.theme) {
WireguardAutoTunnelTheme(theme = appUiState.appState.theme) {
VpnDeniedDialog(showVpnPermissionDialog, onDismiss = { showVpnPermissionDialog = false })
Scaffold(
@@ -263,7 +264,7 @@ class MainActivity : AppCompatActivity() {
route = Route.AutoTunnel,
icon = Icons.Rounded.Bolt,
onClick = {
val route = if (appUiState.generalState.isLocationDisclosureShown) Route.AutoTunnel else Route.LocationDisclosure
val route = if (appUiState.appState.isLocationDisclosureShown) Route.AutoTunnel else Route.LocationDisclosure
navController.goFromRoot(route)
},
active = appUiState.isAutoTunnelActive,
@@ -294,7 +295,7 @@ class MainActivity : AppCompatActivity() {
) {
NavHost(
navController,
startDestination = (if (appUiState.generalState.isPinLockEnabled) Route.Lock else Route.Main),
startDestination = (if (appUiState.appState.isPinLockEnabled) Route.Lock else Route.Main),
) {
composable<Route.Main> {
MainScreen(appUiState, appViewState, viewModel)
@@ -302,6 +303,9 @@ class MainActivity : AppCompatActivity() {
composable<Route.Settings> {
SettingsScreen(appUiState, appViewState, viewModel)
}
composable<Route.SettingsAdvanced> {
SettingsAdvancedScreen(appUiState, viewModel)
}
composable<Route.LocationDisclosure> {
LocationDisclosureScreen(appUiState, viewModel)
}
@@ -321,7 +325,7 @@ class MainActivity : AppCompatActivity() {
SupportScreen()
}
composable<Route.AutoTunnelAdvanced> {
AdvancedScreen(appUiState)
AutoTunnelAdvancedScreen(appUiState, viewModel)
}
composable<Route.Logs> {
LogsScreen(appViewState, viewModel)
@@ -356,13 +360,6 @@ class MainActivity : AppCompatActivity() {
}
}
}
BackHandler {
if (navController.currentDestination?.route != Route.Main::class.qualifiedName) {
navController.popBackStack()
} else {
this@MainActivity.finish()
}
}
}
}
}
@@ -0,0 +1,91 @@
package com.zaneschepke.wireguardautotunnel.core.broadcast
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager
import com.zaneschepke.wireguardautotunnel.di.ApplicationScope
import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository
import com.zaneschepke.wireguardautotunnel.util.Constants
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject
@AndroidEntryPoint
class RemoteControlReceiver : BroadcastReceiver() {
@Inject
@ApplicationScope
lateinit var applicationScope: CoroutineScope
@Inject
lateinit var appDataRepository: AppDataRepository
@Inject
lateinit var serviceManager: ServiceManager
@Inject
lateinit var tunnelManager: TunnelManager
enum class Action(private val suffix: String) {
START_TUNNEL("START_TUNNEL"),
STOP_TUNNEL("STOP_TUNNEL"),
START_AUTO_TUNNEL("START_AUTO_TUNNEL"),
STOP_AUTO_TUNNEL("STOP_AUTO_TUNNEL"),
;
fun getFullAction(): String {
return "${Constants.BASE_PACKAGE}.$suffix"
}
companion object {
fun fromAction(action: String): Action? {
for (a in entries) {
if (a.getFullAction() == action) {
return a
}
}
return null
}
}
}
override fun onReceive(context: Context, intent: Intent) {
Timber.i("onReceive")
val action = intent.action ?: return
val appAction = Action.fromAction(action) ?: return Timber.w("Unknown action $action")
applicationScope.launch {
if (!appDataRepository.appState.isRemoteControlEnabled()) return@launch Timber.w("Remote control disabled")
val key = appDataRepository.appState.getRemoteKey() ?: return@launch Timber.w("Remote control key missing")
if (key != intent.getStringExtra(EXTRA_KEY)?.trim()) return@launch Timber.w("Invalid remote control key")
when (appAction) {
Action.START_TUNNEL -> {
val tunnelName = intent.getStringExtra(EXTRA_TUN_NAME) ?: return@launch startDefaultTunnel()
val tunnel = appDataRepository.tunnels.findByTunnelName(tunnelName) ?: return@launch startDefaultTunnel()
tunnelManager.startTunnel(tunnel)
}
Action.STOP_TUNNEL -> {
val tunnelName = intent.getStringExtra(EXTRA_TUN_NAME) ?: return@launch tunnelManager.stopTunnel()
val tunnel = appDataRepository.tunnels.findByTunnelName(tunnelName) ?: return@launch tunnelManager.stopTunnel()
tunnelManager.stopTunnel(tunnel)
}
Action.START_AUTO_TUNNEL -> serviceManager.startAutoTunnel()
Action.STOP_AUTO_TUNNEL -> serviceManager.stopAutoTunnel()
}
}
}
private suspend fun startDefaultTunnel() {
appDataRepository.getPrimaryOrFirstTunnel()?.let { tunnel ->
tunnelManager.startTunnel(tunnel)
}
}
companion object {
const val EXTRA_TUN_NAME = "tunnelName"
const val EXTRA_KEY = "key"
}
}
@@ -35,14 +35,6 @@ class RestartReceiver : BroadcastReceiver() {
lateinit var ioDispatcher: CoroutineDispatcher
override fun onReceive(context: Context, intent: Intent) {
val action = intent.action ?: return
if (action != Intent.ACTION_BOOT_COMPLETED &&
action != Intent.ACTION_MY_PACKAGE_REPLACED &&
action != "com.htc.intent.action.QUICKBOOT_POWERON"
) {
return
}
Timber.d("RestartReceiver triggered with action: ${intent.action}")
serviceManager.updateTunnelTile()
serviceManager.updateAutoTunnelTile()
@@ -3,7 +3,6 @@ package com.zaneschepke.wireguardautotunnel.core.tunnel
import com.wireguard.android.backend.Tunnel
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
import com.zaneschepke.wireguardautotunnel.di.ApplicationScope
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
import com.zaneschepke.wireguardautotunnel.domain.entity.TunnelConf
import com.zaneschepke.wireguardautotunnel.domain.enums.BackendError
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelStatus
@@ -11,9 +10,7 @@ import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelState
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelStatistics
import com.zaneschepke.wireguardautotunnel.util.extensions.asTunnelState
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
@@ -27,7 +24,6 @@ import java.util.concurrent.atomic.AtomicBoolean
import kotlin.concurrent.thread
abstract class BaseTunnel(
@IoDispatcher private val ioDispatcher: CoroutineDispatcher,
@ApplicationScope private val applicationScope: CoroutineScope,
private val appDataRepository: AppDataRepository,
private val serviceManager: ServiceManager,
@@ -60,6 +56,7 @@ abstract class BaseTunnel(
val newState = state ?: existingState.status
if (newState == TunnelStatus.Down) {
Timber.d("Removing tunnel ${tunnelConf.id} from activeTunnels as state is DOWN")
cleanUpTunThread(tunnelConf)
current - originalConf
} else if (existingState.status == newState && stats == null) {
Timber.d("Skipping redundant state update for ${tunnelConf.id}: $newState")
@@ -79,19 +76,20 @@ abstract class BaseTunnel(
activeTunnels.value.forEach { (config, state) ->
if (state.status.isUpOrStarting()) {
stopTunnel(config)
delay(300)
}
}
}
private fun configureTunnelCallbacks(tunnelConf: TunnelConf) {
Timber.d("Configuring TunnelConf instance: ${tunnelConf.hashCode()}")
tunnelConf.setStateChangeCallback { state ->
Timber.d("State change callback triggered for tunnel ${tunnelConf.id}: ${tunnelConf.tunName} with state $state at ${System.currentTimeMillis()}")
when (state) {
is Tunnel.State -> applicationScope.launch { updateTunnelStatus(tunnelConf, state.asTunnelState()) }
is org.amnezia.awg.backend.Tunnel.State -> applicationScope.launch { updateTunnelStatus(tunnelConf, state.asTunnelState()) }
applicationScope.launch {
Timber.d("State change callback triggered for tunnel ${tunnelConf.id}: ${tunnelConf.tunName} with state $state at ${System.currentTimeMillis()}")
when (state) {
is Tunnel.State -> updateTunnelStatus(tunnelConf, state.asTunnelState())
is org.amnezia.awg.backend.Tunnel.State -> updateTunnelStatus(tunnelConf, state.asTunnelState())
}
handleServiceChangesOnStop()
}
serviceManager.updateTunnelTile()
}
@@ -144,9 +142,7 @@ abstract class BaseTunnel(
if (tunnelConf == null) return stopActiveTunnels()
tunMutex.withLock {
try {
val stuckStarting = activeTuns.isStarting(tunnelConf.id)
handleTunnelThreadCleanup(tunnelConf)
if (stuckStarting) return Timber.d("Stuck in starting, so just shutting down tunnel thread")
if (activeTuns.isStarting(tunnelConf.id)) return handleStuckStartingTunnelShutdown(tunnelConf)
updateTunnelStatus(tunnelConf, TunnelStatus.Stopping(reason))
stopTunnelInner(tunnelConf)
} catch (e: BackendError) {
@@ -161,15 +157,14 @@ abstract class BaseTunnel(
stopBackend(tunnel)
saveTunnelActiveState(tunnelConf, false)
removeActiveTunnel(tunnel)
handleServiceChangesOnStop()
}
private suspend fun handleServiceChangesOnStop() {
if (activeTuns.value.isEmpty() && !isBouncing.get()) return serviceManager.stopTunnelForegroundService()
}
private suspend fun handleTunnelThreadCleanup(tunnel: TunnelConf) {
Timber.d("Cleaning up thread for ${tunnel.name}")
private suspend fun handleStuckStartingTunnelShutdown(tunnel: TunnelConf) {
Timber.d("Stuck in starting state so shutting down tunnel thread for tunnel ${tunnel.name}")
try {
tunThreads[tunnel.id]?.let {
if (it.state != Thread.State.TERMINATED) {
@@ -182,6 +177,10 @@ abstract class BaseTunnel(
} catch (e: Exception) {
Timber.e(e, "Failed to stop tunnel thread for ${tunnel.name}")
}
cleanUpTunThread(tunnel)
}
private fun cleanUpTunThread(tunnel: TunnelConf) {
Timber.d("Removing thread for ${tunnel.name}")
tunThreads -= tunnel.id
}
@@ -5,7 +5,6 @@ import com.wireguard.android.backend.BackendException
import com.wireguard.android.backend.Tunnel
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
import com.zaneschepke.wireguardautotunnel.di.ApplicationScope
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
import com.zaneschepke.wireguardautotunnel.domain.entity.TunnelConf
import com.zaneschepke.wireguardautotunnel.domain.enums.BackendState
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelStatus
@@ -13,18 +12,16 @@ import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelStatistics
import com.zaneschepke.wireguardautotunnel.domain.state.WireGuardStatistics
import com.zaneschepke.wireguardautotunnel.util.extensions.toBackendError
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import timber.log.Timber
import javax.inject.Inject
class KernelTunnel @Inject constructor(
@IoDispatcher private val ioDispatcher: CoroutineDispatcher,
@ApplicationScope private val applicationScope: CoroutineScope,
serviceManager: ServiceManager,
appDataRepository: AppDataRepository,
private val backend: Backend,
) : BaseTunnel(ioDispatcher, applicationScope, appDataRepository, serviceManager) {
) : BaseTunnel(applicationScope, appDataRepository, serviceManager) {
override fun getStatistics(tunnelConf: TunnelConf): TunnelStatistics? {
return try {
@@ -2,7 +2,6 @@ package com.zaneschepke.wireguardautotunnel.core.tunnel
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
import com.zaneschepke.wireguardautotunnel.di.ApplicationScope
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
import com.zaneschepke.wireguardautotunnel.domain.entity.TunnelConf
import com.zaneschepke.wireguardautotunnel.domain.enums.BackendState
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelStatus
@@ -12,7 +11,6 @@ import com.zaneschepke.wireguardautotunnel.domain.state.TunnelStatistics
import com.zaneschepke.wireguardautotunnel.util.extensions.asAmBackendState
import com.zaneschepke.wireguardautotunnel.util.extensions.asBackendState
import com.zaneschepke.wireguardautotunnel.util.extensions.toBackendError
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import org.amnezia.awg.backend.Backend
import org.amnezia.awg.backend.BackendException
@@ -23,12 +21,11 @@ import javax.inject.Inject
import kotlin.jvm.optionals.getOrNull
class UserspaceTunnel @Inject constructor(
@IoDispatcher private val ioDispatcher: CoroutineDispatcher,
@ApplicationScope private val applicationScope: CoroutineScope,
val serviceManager: ServiceManager,
val appDataRepository: AppDataRepository,
private val backend: Backend,
) : BaseTunnel(ioDispatcher, applicationScope, appDataRepository, serviceManager) {
) : BaseTunnel(applicationScope, appDataRepository, serviceManager) {
private var previousBackendState: Pair<BackendState, Boolean>? = null
@@ -30,6 +30,8 @@ class DataStoreManager(
val isLocalLogsEnabled = booleanPreferencesKey("LOCAL_LOGS_ENABLED")
val locale = stringPreferencesKey("LOCALE")
val theme = stringPreferencesKey("THEME")
val isRemoteControlEnabled = booleanPreferencesKey("IS_REMOTE_CONTROL_ENABLED")
val remoteKey = stringPreferencesKey("REMOTE_KEY")
}
// preferences
@@ -9,6 +9,8 @@ data class GeneralState(
val isPinLockEnabled: Boolean = PIN_LOCK_ENABLED_DEFAULT,
val isTunnelStatsExpanded: Boolean = IS_TUNNEL_STATS_EXPANDED,
val isLocalLogsEnabled: Boolean = IS_LOGS_ENABLED_DEFAULT,
val isRemoteControlEnabled: Boolean = IS_REMOTE_CONTROL_ENABLED,
val remoteKey: String? = null,
val locale: String? = null,
val theme: Theme = Theme.AUTOMATIC,
) {
@@ -19,6 +21,8 @@ data class GeneralState(
isPinLockEnabled,
isTunnelStatsExpanded,
isLocationDisclosureShown,
isRemoteControlEnabled,
remoteKey,
locale,
theme,
)
@@ -31,7 +35,9 @@ data class GeneralState(
isBatteryOptimizationDisableShown,
isPinLockEnabled,
isTunnelStatsExpanded,
isLocationDisclosureShown,
isLocalLogsEnabled,
isRemoteControlEnabled,
remoteKey,
locale,
theme,
)
@@ -42,5 +48,6 @@ data class GeneralState(
const val PIN_LOCK_ENABLED_DEFAULT = false
const val IS_TUNNEL_STATS_EXPANDED = false
const val IS_LOGS_ENABLED_DEFAULT = false
const val IS_REMOTE_CONTROL_ENABLED = false
}
}
@@ -2,6 +2,7 @@ package com.zaneschepke.wireguardautotunnel.data.repository
import com.zaneschepke.wireguardautotunnel.data.DataStoreManager
import com.zaneschepke.wireguardautotunnel.data.model.GeneralState
import com.zaneschepke.wireguardautotunnel.domain.entity.AppState
import com.zaneschepke.wireguardautotunnel.domain.repository.AppStateRepository
import com.zaneschepke.wireguardautotunnel.ui.theme.Theme
import kotlinx.coroutines.flow.Flow
@@ -78,7 +79,23 @@ class DataStoreAppStateRepository(
return dataStoreManager.getFromStore(DataStoreManager.locale)
}
override val flow: Flow<GeneralState> =
override suspend fun setIsRemoteControlEnabled(enabled: Boolean) {
dataStoreManager.saveToDataStore(DataStoreManager.isRemoteControlEnabled, enabled)
}
override suspend fun isRemoteControlEnabled(): Boolean {
return dataStoreManager.getFromStore(DataStoreManager.isRemoteControlEnabled) ?: GeneralState.IS_REMOTE_CONTROL_ENABLED
}
override suspend fun setRemoteKey(key: String) {
dataStoreManager.saveToDataStore(DataStoreManager.remoteKey, key)
}
override suspend fun getRemoteKey(): String? {
return dataStoreManager.getFromStore(DataStoreManager.remoteKey)
}
override val flow: Flow<AppState> =
dataStoreManager.preferencesFlow.map { prefs ->
prefs?.let { pref ->
try {
@@ -94,6 +111,8 @@ class DataStoreAppStateRepository(
?: GeneralState.PIN_LOCK_ENABLED_DEFAULT,
isTunnelStatsExpanded = pref[DataStoreManager.tunnelStatsExpanded] ?: GeneralState.IS_TUNNEL_STATS_EXPANDED,
isLocalLogsEnabled = pref[DataStoreManager.isLocalLogsEnabled] ?: GeneralState.IS_LOGS_ENABLED_DEFAULT,
isRemoteControlEnabled = pref[DataStoreManager.isRemoteControlEnabled] ?: GeneralState.IS_REMOTE_CONTROL_ENABLED,
remoteKey = pref[DataStoreManager.remoteKey],
locale = pref[DataStoreManager.locale],
theme = getTheme(),
)
@@ -102,5 +121,5 @@ class DataStoreAppStateRepository(
GeneralState()
}
} ?: GeneralState()
}
}.map { it.toAppState() }
}
@@ -30,9 +30,9 @@ class RoomTunnelRepository(
}
}
override suspend fun saveAll(tunnelConfs: List<TunnelConf>) {
override suspend fun saveAll(tunnelConfList: List<TunnelConf>) {
withContext(ioDispatcher) {
tunnelConfigDao.saveAll(tunnelConfs.map(TunnelConfig::from))
tunnelConfigDao.saveAll(tunnelConfList.map(TunnelConfig::from))
}
}
@@ -62,26 +62,24 @@ class TunnelModule {
@Singleton
@Kernel
fun provideKernelProvider(
@IoDispatcher ioDispatcher: CoroutineDispatcher,
@ApplicationScope applicationScope: CoroutineScope,
serviceManager: ServiceManager,
appDataRepository: AppDataRepository,
backend: com.wireguard.android.backend.Backend,
): TunnelProvider {
return KernelTunnel(ioDispatcher, applicationScope, serviceManager, appDataRepository, backend)
return KernelTunnel(applicationScope, serviceManager, appDataRepository, backend)
}
@Provides
@Singleton
@Userspace
fun provideUserspaceProvider(
@IoDispatcher ioDispatcher: CoroutineDispatcher,
@ApplicationScope applicationScope: CoroutineScope,
serviceManager: ServiceManager,
appDataRepository: AppDataRepository,
backend: Backend,
): TunnelProvider {
return UserspaceTunnel(ioDispatcher, applicationScope, serviceManager, appDataRepository, backend)
return UserspaceTunnel(applicationScope, serviceManager, appDataRepository, backend)
}
@Provides
@@ -8,6 +8,8 @@ data class AppState(
val isPinLockEnabled: Boolean,
val isTunnelStatsExpanded: Boolean,
val isLocalLogsEnabled: Boolean,
val isRemoteControlEnabled: Boolean,
val remoteKey: String?,
val locale: String?,
val theme: Theme,
)
@@ -1,6 +1,6 @@
package com.zaneschepke.wireguardautotunnel.domain.repository
import com.zaneschepke.wireguardautotunnel.data.model.GeneralState
import com.zaneschepke.wireguardautotunnel.domain.entity.AppState
import com.zaneschepke.wireguardautotunnel.ui.theme.Theme
import kotlinx.coroutines.flow.Flow
@@ -33,5 +33,13 @@ interface AppStateRepository {
suspend fun getLocale(): String?
val flow: Flow<GeneralState>
suspend fun setIsRemoteControlEnabled(enabled: Boolean)
suspend fun isRemoteControlEnabled(): Boolean
suspend fun setRemoteKey(key: String)
suspend fun getRemoteKey(): String?
val flow: Flow<AppState>
}
@@ -9,6 +9,9 @@ sealed class Route {
@Serializable
data object Settings : Route()
@Serializable
data object SettingsAdvanced : Route()
@Serializable
data object AutoTunnel : Route()
@@ -41,7 +41,6 @@ fun SurfaceSelectionGroupButton(items: List<SelectionItem>) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.padding(start = 16.dp)
.weight(4f, false)
.fillMaxWidth(),
) {
@@ -71,7 +70,7 @@ fun SurfaceSelectionGroupButton(items: List<SelectionItem>) {
Box(
contentAlignment = Alignment.CenterEnd,
modifier = Modifier
.padding(end = 24.dp, start = 16.dp)
.padding(start = 16.dp)
.weight(1f),
) {
it()
@@ -4,6 +4,7 @@ import androidx.compose.animation.animateColorAsState
import androidx.compose.animation.core.Spring
import androidx.compose.animation.core.spring
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
@@ -20,15 +21,21 @@ import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import com.zaneschepke.wireguardautotunnel.ui.theme.SilverTree
import com.zaneschepke.wireguardautotunnel.util.extensions.isRunningOnTv
@Composable
fun BottomBarTabs(tabs: List<BottomNavItem>, selectedTabIndex: Int, isChildRoute: Boolean, onTabSelected: (BottomNavItem) -> Unit) {
val context = LocalContext.current
val isRunningOnTv = remember { context.isRunningOnTv() }
Row(
modifier = Modifier
.fillMaxWidth()
@@ -44,6 +51,17 @@ fun BottomBarTabs(tabs: List<BottomNavItem>, selectedTabIndex: Int, isChildRoute
.weight(1f)
.fillMaxHeight()
.background(Color.Transparent)
.then(
if (isRunningOnTv) {
Modifier.clickable {
if (index == selectedTabIndex && !isChildRoute) return@clickable
tab.onClick.invoke()
onTabSelected(tab)
}
} else {
Modifier
},
)
.pointerInput(Unit) {
detectTapGestures {
if (index == selectedTabIndex && !isChildRoute) return@detectTapGestures
@@ -79,7 +79,7 @@ fun currentNavBackStackEntryAsNavBarState(
route = Route.AutoTunnel,
)
}
backStackEntry.isCurrentRoute(Route.AutoTunnelAdvanced::class) -> {
backStackEntry.isCurrentRoute(Route.AutoTunnelAdvanced::class) || backStackEntry.isCurrentRoute(Route.SettingsAdvanced::class) -> {
NavBarState(
showTop = true, showBottom = true,
{ Text(stringResource(R.string.advanced_settings)) },
@@ -21,7 +21,9 @@ import androidx.compose.ui.unit.dp
import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.google.accompanist.permissions.isGranted
import com.google.accompanist.permissions.rememberPermissionState
import com.zaneschepke.wireguardautotunnel.ui.Route
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SurfaceSelectionGroupButton
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.LocalNavController
import com.zaneschepke.wireguardautotunnel.ui.screens.autotunnel.components.AdvancedSettingsItem
import com.zaneschepke.wireguardautotunnel.ui.screens.autotunnel.components.NetworkTunnelingItems
import com.zaneschepke.wireguardautotunnel.ui.screens.autotunnel.components.WifiTunnelingItems
@@ -37,6 +39,7 @@ import com.zaneschepke.wireguardautotunnel.viewmodel.AppViewModel
@Composable
fun AutoTunnelScreen(uiState: AppUiState, viewModel: AppViewModel) {
val context = LocalContext.current
val navController = LocalNavController.current
val fineLocationState = rememberPermissionState(Manifest.permission.ACCESS_FINE_LOCATION)
var currentText by remember { mutableStateOf("") }
var isBackgroundLocationGranted by remember { mutableStateOf(true) }
@@ -103,7 +106,7 @@ fun AutoTunnelScreen(uiState: AppUiState, viewModel: AppViewModel) {
items = NetworkTunnelingItems(uiState, viewModel),
)
SurfaceSelectionGroupButton(
items = listOf(AdvancedSettingsItem()),
items = listOf(AdvancedSettingsItem(onClick = { navController.navigate(Route.AutoTunnelAdvanced) })),
)
}
}
@@ -10,16 +10,13 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import com.zaneschepke.wireguardautotunnel.ui.screens.autotunnel.advanced.components.DebounceDelaySelector
import com.zaneschepke.wireguardautotunnel.ui.state.AppUiState
import com.zaneschepke.wireguardautotunnel.viewmodel.AppViewModel
@Composable
fun AdvancedScreen(appUiState: AppUiState) {
val appViewModel: AppViewModel = hiltViewModel()
fun AutoTunnelAdvancedScreen(appUiState: AppUiState, viewModel: AppViewModel) {
Column(
horizontalAlignment = Alignment.Start,
verticalArrangement = Arrangement.spacedBy(24.dp, Alignment.Top),
@@ -31,7 +28,7 @@ fun AdvancedScreen(appUiState: AppUiState) {
) {
DebounceDelaySelector(
currentDelay = appUiState.appSettings.debounceDelaySeconds,
onEvent = appViewModel::handleEvent,
onEvent = viewModel::handleEvent,
)
}
}
@@ -7,14 +7,11 @@ import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.res.stringResource
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.ui.Route
import com.zaneschepke.wireguardautotunnel.ui.common.button.ForwardButton
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SelectionItem
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.LocalNavController
@Composable
fun AdvancedSettingsItem(): SelectionItem {
val navController = LocalNavController.current
fun AdvancedSettingsItem(onClick: () -> Unit): SelectionItem {
return SelectionItem(
leadingIcon = Icons.Outlined.Settings,
title = {
@@ -24,8 +21,8 @@ fun AdvancedSettingsItem(): SelectionItem {
)
},
trailing = {
ForwardButton { navController.navigate(Route.AutoTunnelAdvanced) }
ForwardButton { onClick() }
},
onClick = { navController.navigate(Route.AutoTunnelAdvanced) },
onClick = { onClick() },
)
}
@@ -1,5 +1,6 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.autotunnel.components
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
@@ -21,8 +22,10 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalClipboardManager
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import com.zaneschepke.networkmonitor.NetworkStatus
@@ -46,6 +49,7 @@ fun WifiTunnelingItems(
isWifiNameReadable: () -> Boolean,
): List<SelectionItem> {
val context = LocalContext.current
val clipboard = LocalClipboardManager.current
val baseItems = listOf(
SelectionItem(
@@ -76,6 +80,9 @@ fun WifiTunnelingItems(
style = MaterialTheme.typography.bodySmall.copy(color = MaterialTheme.colorScheme.outline),
maxLines = 1,
overflow = TextOverflow.Ellipsis,
modifier = Modifier.clickable {
wifiName?.let { clipboard.setText(AnnotatedString(it)) }
},
)
},
onClick = { viewModel.handleEvent(AppEvent.ToggleAutoTunnelOnWifi) },
@@ -64,7 +64,7 @@ fun TunnelList(
val tunnelState = activeTunnels.getValueById(tunnel.id) ?: TunnelState()
TunnelRowItem(
isActive = tunnelState.status.isUpOrStarting(),
expanded = appUiState.generalState.isTunnelStatsExpanded,
expanded = appUiState.appState.isTunnelStatsExpanded,
isSelected = selectedTunnel?.id == tunnel.id,
tunnel = tunnel,
tunnelState = tunnelState,
@@ -15,7 +15,10 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.unit.dp
import com.zaneschepke.wireguardautotunnel.ui.Route
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SurfaceSelectionGroupButton
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.LocalNavController
import com.zaneschepke.wireguardautotunnel.ui.screens.autotunnel.components.AdvancedSettingsItem
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.components.AlwaysOnVpnItem
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.components.AppShortcutsItem
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.components.AppearanceItem
@@ -36,6 +39,7 @@ import com.zaneschepke.wireguardautotunnel.viewmodel.AppViewModel
fun SettingsScreen(uiState: AppUiState, appViewState: AppViewState, viewModel: AppViewModel) {
val context = LocalContext.current
val focusManager = LocalFocusManager.current
val navController = LocalNavController.current
val isRunningOnTv = remember { context.isRunningOnTv() }
val interactionSource = remember { MutableInteractionSource() }
@@ -76,7 +80,7 @@ fun SettingsScreen(uiState: AppUiState, appViewState: AppViewState, viewModel: A
items = buildList {
add(AppearanceItem())
add(LocalLoggingItem(uiState, viewModel))
if (uiState.generalState.isLocalLogsEnabled) add(ReadLogsItem())
if (uiState.appState.isLocalLogsEnabled) add(ReadLogsItem())
add(PinLockItem(uiState, viewModel))
},
)
@@ -85,5 +89,8 @@ fun SettingsScreen(uiState: AppUiState, appViewState: AppViewState, viewModel: A
items = listOf(KernelModeItem(uiState, viewModel)),
)
}
SurfaceSelectionGroupButton(
items = listOf(AdvancedSettingsItem(onClick = { navController.navigate(Route.SettingsAdvanced) })),
)
}
}
@@ -0,0 +1,54 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.settings.advanced
import android.view.WindowManager
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import com.zaneschepke.wireguardautotunnel.MainActivity
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SurfaceSelectionGroupButton
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.advanced.components.RemoteControlItem
import com.zaneschepke.wireguardautotunnel.ui.state.AppUiState
import com.zaneschepke.wireguardautotunnel.viewmodel.AppViewModel
@Composable
fun SettingsAdvancedScreen(appUiState: AppUiState, viewModel: AppViewModel) {
val context = LocalContext.current
val activity = context as? MainActivity
// Secure screen due to sensitive information
DisposableEffect(Unit) {
activity?.window?.setFlags(
WindowManager.LayoutParams.FLAG_SECURE,
WindowManager.LayoutParams.FLAG_SECURE,
)
onDispose {
activity?.window?.clearFlags(WindowManager.LayoutParams.FLAG_SECURE)
}
}
Column(
horizontalAlignment = Alignment.Start,
verticalArrangement = Arrangement.spacedBy(24.dp, Alignment.Top),
modifier = Modifier
.fillMaxSize()
.verticalScroll(rememberScrollState())
.padding(top = 24.dp)
.padding(horizontal = 24.dp),
) {
SurfaceSelectionGroupButton(
listOf(
RemoteControlItem(appUiState, viewModel),
),
)
}
}
@@ -0,0 +1,59 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.settings.advanced.components
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.clickable
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.SmartToy
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalClipboardManager
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.style.TextOverflow
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.ui.common.button.ScaledSwitch
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SelectionItem
import com.zaneschepke.wireguardautotunnel.ui.state.AppUiState
import com.zaneschepke.wireguardautotunnel.viewmodel.AppViewModel
import com.zaneschepke.wireguardautotunnel.viewmodel.event.AppEvent
@Composable
fun RemoteControlItem(uiState: AppUiState, viewModel: AppViewModel): SelectionItem {
val clipboardManager = LocalClipboardManager.current
return SelectionItem(
leadingIcon = Icons.Filled.SmartToy,
trailing = {
ScaledSwitch(
checked = uiState.appState.isRemoteControlEnabled,
onClick = { viewModel.handleEvent(AppEvent.ToggleRemoteControl) },
)
},
description = {
uiState.appState.remoteKey?.let { key ->
AnimatedVisibility(visible = uiState.appState.isRemoteControlEnabled) {
Text(
text = stringResource(R.string.remote_key_template, key),
style = MaterialTheme.typography.bodySmall.copy(
color = MaterialTheme.colorScheme.outline,
),
maxLines = 1,
overflow = TextOverflow.Ellipsis,
modifier = Modifier.clickable {
clipboardManager.setText(AnnotatedString(key))
},
)
}
}
},
title = {
Text(
text = stringResource(R.string.enable_remote_app_control),
style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface),
)
},
onClick = { viewModel.handleEvent(AppEvent.ToggleRemoteControl) },
)
}
@@ -33,22 +33,22 @@ fun DisplayScreen(appUiState: AppUiState, viewModel: AppViewModel) {
onClick = {
viewModel.handleEvent(AppEvent.SetTheme(Theme.AUTOMATIC))
},
selected = appUiState.generalState.theme == Theme.AUTOMATIC,
selected = appUiState.appState.theme == Theme.AUTOMATIC,
)
IconSurfaceButton(
title = stringResource(R.string.light),
onClick = { viewModel.handleEvent(AppEvent.SetTheme(Theme.LIGHT)) },
selected = appUiState.generalState.theme == Theme.LIGHT,
selected = appUiState.appState.theme == Theme.LIGHT,
)
IconSurfaceButton(
title = stringResource(R.string.dark),
onClick = { viewModel.handleEvent(AppEvent.SetTheme(Theme.DARK)) },
selected = appUiState.generalState.theme == Theme.DARK,
selected = appUiState.appState.theme == Theme.DARK,
)
IconSurfaceButton(
title = stringResource(R.string.dynamic),
onClick = { viewModel.handleEvent(AppEvent.SetTheme(Theme.DYNAMIC)) },
selected = appUiState.generalState.theme == Theme.DYNAMIC,
selected = appUiState.appState.theme == Theme.DYNAMIC,
)
}
}
@@ -23,7 +23,7 @@ fun AutomaticLanguageItem(appUiState: AppUiState, viewModel: AppViewModel) {
viewModel.handleEvent(AppEvent.SetLocale(LocaleUtil.OPTION_PHONE_LANGUAGE))
},
trailing = {
with(appUiState.generalState.locale) {
with(appUiState.appState.locale) {
if (this == LocaleUtil.OPTION_PHONE_LANGUAGE || this == null) {
SelectedLabel()
}
@@ -24,7 +24,7 @@ fun LanguageItem(locale: Locale, appUiState: AppUiState, viewModel: AppViewModel
viewModel.handleEvent(AppEvent.SetLocale(locale.toLanguageTag()))
},
trailing = {
if (locale.toLanguageTag() == appUiState.generalState.locale) {
if (locale.toLanguageTag() == appUiState.appState.locale) {
SelectedLabel()
}
},
@@ -19,7 +19,7 @@ fun LocalLoggingItem(uiState: AppUiState, viewModel: AppViewModel): SelectionIte
description = { SelectionItemLabel(R.string.enable_local_logging, isDescription = true) },
trailing = {
ScaledSwitch(
checked = uiState.generalState.isLocalLogsEnabled,
checked = uiState.appState.isLocalLogsEnabled,
onClick = { viewModel.handleEvent(AppEvent.ToggleLocalLogging) },
)
},
@@ -23,7 +23,7 @@ fun PinLockItem(uiState: AppUiState, viewModel: AppViewModel): SelectionItem {
val context = LocalContext.current
fun onPinLockToggle() {
if (uiState.generalState.isPinLockEnabled) {
if (uiState.appState.isPinLockEnabled) {
viewModel.handleEvent(AppEvent.TogglePinLock)
} else {
PinManager.initialize(context)
@@ -41,7 +41,7 @@ fun PinLockItem(uiState: AppUiState, viewModel: AppViewModel): SelectionItem {
},
trailing = {
ScaledSwitch(
checked = uiState.generalState.isPinLockEnabled,
checked = uiState.appState.isPinLockEnabled,
onClick = { onPinLockToggle() },
)
},
@@ -25,7 +25,7 @@ fun LocationDisclosureScreen(appUiState: AppUiState, viewModel: AppViewModel) {
val navController = LocalNavController.current
LaunchedEffect(Unit, appUiState) {
if (appUiState.generalState.isLocationDisclosureShown) navController.goFromRoot(Route.AutoTunnel)
if (appUiState.appState.isLocationDisclosureShown) navController.goFromRoot(Route.AutoTunnel)
}
Column(
@@ -3,6 +3,7 @@ package com.zaneschepke.wireguardautotunnel.ui.state
import com.zaneschepke.networkmonitor.NetworkStatus
import com.zaneschepke.wireguardautotunnel.data.model.GeneralState
import com.zaneschepke.wireguardautotunnel.domain.entity.AppSettings
import com.zaneschepke.wireguardautotunnel.domain.entity.AppState
import com.zaneschepke.wireguardautotunnel.domain.entity.TunnelConf
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelState
@@ -10,7 +11,7 @@ data class AppUiState(
val appSettings: AppSettings = AppSettings(),
val tunnels: List<TunnelConf> = emptyList(),
val activeTunnels: Map<TunnelConf, TunnelState> = emptyMap(),
val generalState: GeneralState = GeneralState(),
val appState: AppState = GeneralState().toAppState(),
val isAutoTunnelActive: Boolean = false,
val appConfigurationChange: Boolean = false,
val isAppLoaded: Boolean = false,
@@ -1,8 +1,10 @@
package com.zaneschepke.wireguardautotunnel.util
object Constants {
const val BASE_PACKAGE = "com.zaneschepke.wireguardautotunnel"
const val BASE_LOG_FILE_NAME = "wg_tunnel_logs"
const val LOG_BUFFER_SIZE = 10_000L
const val MANUAL_TUNNEL_CONFIG_ID = 0
const val BATTERY_SAVER_WATCHER_WAKE_LOCK_TIMEOUT = 10 * 60 * 1_000L // 10 minutes
@@ -14,11 +14,11 @@ import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
import com.zaneschepke.wireguardautotunnel.core.shortcut.ShortcutManager
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager
import com.zaneschepke.wireguardautotunnel.data.model.GeneralState
import com.zaneschepke.wireguardautotunnel.di.AppShell
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
import com.zaneschepke.wireguardautotunnel.di.MainDispatcher
import com.zaneschepke.wireguardautotunnel.domain.entity.AppSettings
import com.zaneschepke.wireguardautotunnel.domain.entity.AppState
import com.zaneschepke.wireguardautotunnel.domain.entity.TunnelConf
import com.zaneschepke.wireguardautotunnel.domain.enums.BackendState
import com.zaneschepke.wireguardautotunnel.domain.enums.ConfigType
@@ -56,6 +56,7 @@ import timber.log.Timber
import xyz.teamgravity.pin_lock_compose.PinManager
import java.net.URL
import java.time.Instant
import java.util.*
import javax.inject.Inject
import javax.inject.Provider
@@ -99,7 +100,7 @@ constructor(
) { array ->
val settings = array[0] as AppSettings
val tunnels = array[1] as List<TunnelConf>
val generalState = array[2] as GeneralState
val generalState = array[2] as AppState
val activeTunnels = array[3] as Map<TunnelConf, TunnelState>
val autoTunnel = array[4] as Boolean
val network = array[5] as NetworkStatus
@@ -108,7 +109,7 @@ constructor(
appSettings = settings,
tunnels = tunnels,
activeTunnels = activeTunnels,
generalState = generalState,
appState = generalState,
isAutoTunnelActive = autoTunnel,
isAppLoaded = true,
networkStatus = network,
@@ -122,10 +123,10 @@ constructor(
init {
viewModelScope.launch(ioDispatcher) {
uiState.withFirstState { state ->
initPin(state.generalState.isPinLockEnabled)
initPin(state.appState.isPinLockEnabled)
handleKillSwitchChange(state.appSettings)
initServicesFromSavedState(state)
if (state.generalState.isLocalLogsEnabled) startCollectingLogs()
if (state.appState.isLocalLogsEnabled) startCollectingLogs()
}
}
}
@@ -133,7 +134,7 @@ constructor(
fun handleEvent(event: AppEvent) = viewModelScope.launch(ioDispatcher) {
uiState.withFirstState { state ->
when (event) {
AppEvent.ToggleLocalLogging -> handleToggleLocalLogging(state.generalState.isLocalLogsEnabled)
AppEvent.ToggleLocalLogging -> handleToggleLocalLogging(state.appState.isLocalLogsEnabled)
is AppEvent.SetDebounceDelay -> handleSetDebounceDelay(state.appSettings, event.delay)
is AppEvent.CopyTunnel -> handleCopyTunnel(event.tunnel, state.tunnels)
is AppEvent.DeleteTunnel -> handleDeleteTunnel(event.tunnel, state)
@@ -145,9 +146,9 @@ constructor(
is AppEvent.StartTunnel -> handleStartTunnel(event.tunnel, state.appSettings)
is AppEvent.StopTunnel -> handleStopTunnel(event.tunnel)
AppEvent.ToggleAutoTunnel -> handleToggleAutoTunnel(state)
AppEvent.ToggleTunnelStatsExpanded -> handleToggleExpandTunnelStats(state.generalState.isTunnelStatsExpanded)
AppEvent.ToggleTunnelStatsExpanded -> handleToggleExpandTunnelStats(state.appState.isTunnelStatsExpanded)
AppEvent.ToggleAlwaysOn -> handleToggleAlwaysOnVPN(state.appSettings)
AppEvent.TogglePinLock -> handlePinLockToggled(state.generalState.isPinLockEnabled)
AppEvent.TogglePinLock -> handlePinLockToggled(state.appState.isPinLockEnabled)
AppEvent.SetLocationDisclosureShown -> setLocationDisclosureShown()
is AppEvent.SetLocale -> handleLocaleChange(event.localeTag)
AppEvent.ToggleRestartAtBoot -> handleToggleRestartAtBoot(state.appSettings)
@@ -188,10 +189,17 @@ constructor(
is AppEvent.ShowMessage -> handleShowMessage(event.message)
is AppEvent.PopBackStack -> _appViewState.update { it.copy(popBackStack = event.pop) }
is AppEvent.ClearTunnelError -> tunnelManager.clearError(event.tunnel)
AppEvent.ToggleRemoteControl -> handleToggleRemoteControl(state.appState)
}
}
}
private suspend fun handleToggleRemoteControl(appState: AppState) {
val enabled = !appState.isRemoteControlEnabled
if (enabled) appDataRepository.appState.setRemoteKey(UUID.randomUUID().toString())
appDataRepository.appState.setIsRemoteControlEnabled(enabled)
}
private fun startCollectingLogs() {
viewModelScope.launch {
logReader.bufferedLogs
@@ -290,7 +298,7 @@ constructor(
true,
)
}
if (!state.generalState.isBatteryOptimizationDisableShown) return@withLock requestBatteryPermission(true)
if (!state.appState.isBatteryOptimizationDisableShown) return@withLock requestBatteryPermission(true)
serviceManager.toggleAutoTunnel()
}
}
@@ -62,4 +62,5 @@ sealed class AppEvent {
data class SetSelectedTunnel(val tunnel: TunnelConf?) : AppEvent()
data object VpnPermissionRequested : AppEvent()
data class AppReadyCheck(val tunnels: List<TunnelConf>) : AppEvent()
data object ToggleRemoteControl : AppEvent()
}
+4
View File
@@ -1,5 +1,7 @@
<resources>
<string name="app_name">WG Tunnel</string>
<string name="app_permission_title">WG Tunnel Control Bridge</string>
<string name="app_permission_description">Control tunnels and auto-tunnel features.</string>
<string name="vpn_channel_id" translatable="false">VPN Channel</string>
<string name="vpn_channel_name">VPN Notification Channel</string>
<string name="github_url" translatable="false">https://github.com/zaneschepke/wgtunnel/issues</string>
@@ -238,6 +240,7 @@
<string name="export_failed">Export failed</string>
<string name="tunnel_error_template">Tunnel failed with: %1$s</string>
<string name="wifi_name_template">Active: %1$s</string>
<string name="remote_key_template">Key: %1$s</string>
<string name="config_error">config error</string>
<string name="dns_resolve_error">dns resolution error</string>
<string name="invalid_config_error">invalid_config_error</string>
@@ -253,4 +256,5 @@
<string name="bio_not_created">Biometrics not created</string>
<string name="bio_update_required">Biometric security update required</string>
<string name="tunnel_starting">Tunnel starting</string>
<string name="enable_remote_app_control">Enable remote app control</string>
</resources>
+2 -2
View File
@@ -1,7 +1,7 @@
object Constants {
const val VERSION_NAME = "3.8.0"
const val VERSION_NAME = "3.8.1"
const val JVM_TARGET = "17"
const val VERSION_CODE = 38000
const val VERSION_CODE = 38100
const val TARGET_SDK = 35
const val MIN_SDK = 26
const val APP_ID = "com.zaneschepke.wireguardautotunnel"
@@ -0,0 +1,6 @@
What's new:
- New remote app intent integration
- Copy active wifi name to clipboard
- Fix for tunnel shutdown bug
- Fix for DNS default server bug, preferring phone DNS
- Fix for Ipv6 static peer host configs
+2 -2
View File
@@ -1,7 +1,7 @@
[versions]
accompanist = "0.37.2"
activityCompose = "1.10.1"
amneziawgAndroid = "1.3.2"
amneziawgAndroid = "1.3.4"
androidx-junit = "1.2.1"
appcompat = "1.7.0"
biometricKtx = "1.2.0-alpha05"
@@ -19,7 +19,7 @@ navigationCompose = "2.8.9"
pinLockCompose = "1.0.4"
roomVersion = "2.6.1"
timber = "5.0.1"
tunnel = "1.2.8"
tunnel = "1.2.10"
androidGradlePlugin = "8.8.0-alpha05"
kotlin = "2.1.20"
ksp = "2.1.20-1.0.32"
+1 -1
View File
@@ -1 +1 @@
4
0