feat: tunnel on ethernet

Adds settings feature to enable auto tunneling on ethernet connections.

Add alphabetical sorting for split tunneling packages

Fix bug where search was not searching by displayed label but by package name

Other refactoring and improvements.

Closes #29 Closes #27 Closes #28
This commit is contained in:
Zane Schepke 2023-09-24 07:40:17 -04:00
parent 1714618f0c
commit 7fbc51af4c
22 changed files with 452 additions and 174 deletions

View File

@ -14,8 +14,8 @@ android {
applicationId = "com.zaneschepke.wireguardautotunnel" applicationId = "com.zaneschepke.wireguardautotunnel"
minSdk = 26 minSdk = 26
targetSdk = 34 targetSdk = 34
versionCode = 30000 versionCode = 30001
versionName = "3.0.0" versionName = "3.0.1"
multiDexEnabled = true multiDexEnabled = true

View File

@ -56,6 +56,7 @@
<action android:name="android.intent.action.MAIN" /> <action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" /> <category android:name="android.intent.category.LAUNCHER" />
<category android:name="android.intent.category.LEANBACK_LAUNCHER" /> <category android:name="android.intent.category.LEANBACK_LAUNCHER" />
<action android:name="android.service.quicksettings.action.QS_TILE_PREFERENCES"/>
</intent-filter> </intent-filter>
</activity> </activity>
<activity <activity
@ -117,8 +118,5 @@
</intent-filter> </intent-filter>
</receiver> </receiver>
<receiver android:exported="false" android:name=".receiver.NotificationActionReceiver"/> <receiver android:exported="false" android:name=".receiver.NotificationActionReceiver"/>
<meta-data
android:name="com.google.mlkit.vision.DEPENDENCIES"
android:value="barcode_ui"/>
</application> </application>
</manifest> </manifest>

View File

@ -4,8 +4,12 @@ object Constants {
const val VPN_CONNECTIVITY_CHECK_INTERVAL = 3000L const val VPN_CONNECTIVITY_CHECK_INTERVAL = 3000L
const val VPN_STATISTIC_CHECK_INTERVAL = 10000L const val VPN_STATISTIC_CHECK_INTERVAL = 10000L
const val SNACKBAR_DELAY = 3000L const val SNACKBAR_DELAY = 3000L
const val TOGGLE_TUNNEL_DELAY = 1000L const val TOGGLE_TUNNEL_DELAY = 500L
const val FADE_IN_ANIMATION_DURATION = 1000 const val FADE_IN_ANIMATION_DURATION = 1000
const val SLIDE_IN_ANIMATION_DURATION = 500 const val SLIDE_IN_ANIMATION_DURATION = 500
const val SLIDE_IN_TRANSITION_OFFSET = 1000 const val SLIDE_IN_TRANSITION_OFFSET = 1000
const val VALID_FILE_EXTENSION = ".conf"
const val URI_CONTENT_SCHEME = "content"
const val URI_PACKAGE_SCHEME = "package"
const val ALLOWED_FILE_TYPES = "*/*"
} }

View File

@ -1,5 +1,6 @@
package com.zaneschepke.wireguardautotunnel.module package com.zaneschepke.wireguardautotunnel.module
import com.zaneschepke.wireguardautotunnel.service.network.EthernetService
import com.zaneschepke.wireguardautotunnel.service.network.MobileDataService import com.zaneschepke.wireguardautotunnel.service.network.MobileDataService
import com.zaneschepke.wireguardautotunnel.service.network.NetworkService import com.zaneschepke.wireguardautotunnel.service.network.NetworkService
import com.zaneschepke.wireguardautotunnel.service.network.WifiService import com.zaneschepke.wireguardautotunnel.service.network.WifiService
@ -26,4 +27,8 @@ abstract class ServiceModule {
@Binds @Binds
@ServiceScoped @ServiceScoped
abstract fun provideMobileDataService(mobileDataService : MobileDataService) : NetworkService<MobileDataService> abstract fun provideMobileDataService(mobileDataService : MobileDataService) : NetworkService<MobileDataService>
@Binds
@ServiceScoped
abstract fun provideEthernetService(ethernetService: EthernetService) : NetworkService<EthernetService>
} }

View File

@ -23,7 +23,7 @@ class BootReceiver : BroadcastReceiver() {
CoroutineScope(Dispatchers.IO).launch { CoroutineScope(Dispatchers.IO).launch {
try { try {
val settings = settingsRepo.getAll() val settings = settingsRepo.getAll()
if (!settings.isNullOrEmpty()) { if (settings.isNotEmpty()) {
val setting = settings.first() val setting = settings.first()
if (setting.isAutoTunnelEnabled && setting.defaultTunnel != null) { if (setting.isAutoTunnelEnabled && setting.defaultTunnel != null) {
ServiceManager.startWatcherService(context, setting.defaultTunnel!!) ServiceManager.startWatcherService(context, setting.defaultTunnel!!)

View File

@ -12,4 +12,5 @@ data class Settings(
@ColumnInfo(name = "trusted_network_ssids") var trustedNetworkSSIDs : MutableList<String> = mutableListOf(), @ColumnInfo(name = "trusted_network_ssids") var trustedNetworkSSIDs : MutableList<String> = mutableListOf(),
@ColumnInfo(name = "default_tunnel") var defaultTunnel : String? = null, @ColumnInfo(name = "default_tunnel") var defaultTunnel : String? = null,
@ColumnInfo(name = "is_always_on_vpn_enabled") var isAlwaysOnVpnEnabled : Boolean = false, @ColumnInfo(name = "is_always_on_vpn_enabled") var isAlwaysOnVpnEnabled : Boolean = false,
@ColumnInfo(name = "is_tunnel_on_ethernet_enabled") var isTunnelOnEthernetEnabled : Boolean = false,
) )

View File

@ -12,6 +12,7 @@ import com.zaneschepke.wireguardautotunnel.Constants
import com.zaneschepke.wireguardautotunnel.R import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.repository.SettingsDoa import com.zaneschepke.wireguardautotunnel.repository.SettingsDoa
import com.zaneschepke.wireguardautotunnel.repository.model.Settings import com.zaneschepke.wireguardautotunnel.repository.model.Settings
import com.zaneschepke.wireguardautotunnel.service.network.EthernetService
import com.zaneschepke.wireguardautotunnel.service.network.MobileDataService import com.zaneschepke.wireguardautotunnel.service.network.MobileDataService
import com.zaneschepke.wireguardautotunnel.service.network.NetworkService import com.zaneschepke.wireguardautotunnel.service.network.NetworkService
import com.zaneschepke.wireguardautotunnel.service.network.NetworkStatus import com.zaneschepke.wireguardautotunnel.service.network.NetworkStatus
@ -38,6 +39,9 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
@Inject @Inject
lateinit var mobileDataService : NetworkService<MobileDataService> lateinit var mobileDataService : NetworkService<MobileDataService>
@Inject
lateinit var ethernetService: NetworkService<EthernetService>
@Inject @Inject
lateinit var settingsRepo: SettingsDoa lateinit var settingsRepo: SettingsDoa
@ -48,6 +52,7 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
lateinit var vpnService : VpnService lateinit var vpnService : VpnService
private var isWifiConnected = false; private var isWifiConnected = false;
private var isEthernetConnected = false;
private var isMobileDataConnected = false; private var isMobileDataConnected = false;
private var currentNetworkSSID = ""; private var currentNetworkSSID = "";
@ -142,6 +147,11 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
watchForMobileDataConnectivityChanges() watchForMobileDataConnectivityChanges()
} }
} }
if(setting.isTunnelOnEthernetEnabled) {
launch {
watchForEthernetConnectivityChanges()
}
}
launch { launch {
manageVpn() manageVpn()
} }
@ -167,6 +177,25 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
} }
} }
private suspend fun watchForEthernetConnectivityChanges() {
ethernetService.networkStatus.collect {
when (it) {
is NetworkStatus.Available -> {
Timber.d("Gained Ethernet connection")
isEthernetConnected = true
}
is NetworkStatus.CapabilitiesChanged -> {
Timber.d("Ethernet capabilities changed")
isEthernetConnected = true
}
is NetworkStatus.Unavailable -> {
isEthernetConnected = false
Timber.d("Lost Ethernet connection")
}
}
}
}
private suspend fun watchForWifiConnectivityChanges() { private suspend fun watchForWifiConnectivityChanges() {
wifiService.networkStatus.collect { wifiService.networkStatus.collect {
when (it) { when (it) {
@ -189,20 +218,23 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
private suspend fun manageVpn() { private suspend fun manageVpn() {
while(true) { while(true) {
if(setting.isTunnelOnMobileDataEnabled && if(isEthernetConnected && setting.isTunnelOnEthernetEnabled && vpnService.getState() == Tunnel.State.DOWN) {
ServiceManager.startVpnService(this, tunnelConfig)
}
if(!isEthernetConnected && setting.isTunnelOnMobileDataEnabled &&
!isWifiConnected && !isWifiConnected &&
isMobileDataConnected isMobileDataConnected
&& vpnService.getState() == Tunnel.State.DOWN) { && vpnService.getState() == Tunnel.State.DOWN) {
ServiceManager.startVpnService(this, tunnelConfig) ServiceManager.startVpnService(this, tunnelConfig)
} else if(!setting.isTunnelOnMobileDataEnabled && } else if(!isEthernetConnected && !setting.isTunnelOnMobileDataEnabled &&
!isWifiConnected && !isWifiConnected &&
vpnService.getState() == Tunnel.State.UP) { vpnService.getState() == Tunnel.State.UP) {
ServiceManager.stopVpnService(this) ServiceManager.stopVpnService(this)
} else if(isWifiConnected && } else if(!isEthernetConnected && isWifiConnected &&
!setting.trustedNetworkSSIDs.contains(currentNetworkSSID) && !setting.trustedNetworkSSIDs.contains(currentNetworkSSID) &&
(vpnService.getState() != Tunnel.State.UP)) { (vpnService.getState() != Tunnel.State.UP)) {
ServiceManager.startVpnService(this, tunnelConfig) ServiceManager.startVpnService(this, tunnelConfig)
} else if((isWifiConnected && } else if(!isEthernetConnected && (isWifiConnected &&
setting.trustedNetworkSSIDs.contains(currentNetworkSSID)) && setting.trustedNetworkSSIDs.contains(currentNetworkSSID)) &&
(vpnService.getState() == Tunnel.State.UP)) { (vpnService.getState() == Tunnel.State.UP)) {
ServiceManager.stopVpnService(this) ServiceManager.stopVpnService(this)

View File

@ -0,0 +1,10 @@
package com.zaneschepke.wireguardautotunnel.service.network
import android.content.Context
import android.net.NetworkCapabilities
import dagger.hilt.android.qualifiers.ApplicationContext
import javax.inject.Inject
class EthernetService @Inject constructor(@ApplicationContext context: Context) :
BaseNetworkService<EthernetService>(context, NetworkCapabilities.TRANSPORT_ETHERNET) {
}

View File

@ -46,9 +46,11 @@ object ShortcutsManager {
) )
} }
fun removeTunnelShortcuts(context : Context, tunnelConfig : TunnelConfig) { fun removeTunnelShortcuts(context : Context, tunnelConfig : TunnelConfig?) {
ShortcutManagerCompat.removeDynamicShortcuts(context, listOf(tunnelConfig.id.toString() + APPEND_ON, if(tunnelConfig != null) {
tunnelConfig.id.toString() + APPEND_OFF )) ShortcutManagerCompat.removeDynamicShortcuts(context, listOf(tunnelConfig.id.toString() + APPEND_ON,
tunnelConfig.id.toString() + APPEND_OFF ))
}
} }
private fun createTunnelOnIntent(context: Context, extras : Map<String,String>) : Intent { private fun createTunnelOnIntent(context: Context, extras : Map<String,String>) : Intent {

View File

@ -1,6 +0,0 @@
package com.zaneschepke.wireguardautotunnel.ui;
import com.journeyapps.barcodescanner.CaptureActivity;
public class CaptureActivityPortrait extends CaptureActivity {
}

View File

@ -0,0 +1,5 @@
package com.zaneschepke.wireguardautotunnel.ui
import com.journeyapps.barcodescanner.CaptureActivity
class CaptureActivityPortrait : CaptureActivity()

View File

@ -44,6 +44,7 @@ import com.zaneschepke.wireguardautotunnel.ui.screens.settings.SettingsScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.support.SupportScreen import com.zaneschepke.wireguardautotunnel.ui.screens.support.SupportScreen
import com.zaneschepke.wireguardautotunnel.ui.theme.TransparentSystemBars import com.zaneschepke.wireguardautotunnel.ui.theme.TransparentSystemBars
import com.zaneschepke.wireguardautotunnel.ui.theme.WireguardAutoTunnelTheme import com.zaneschepke.wireguardautotunnel.ui.theme.WireguardAutoTunnelTheme
import com.zaneschepke.wireguardautotunnel.util.WgTunnelException
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import timber.log.Timber import timber.log.Timber
import java.lang.IllegalStateException import java.lang.IllegalStateException
@ -101,7 +102,7 @@ class MainActivity : AppCompatActivity() {
} }
false false
} else -> { } else -> {
false; false
} }
} }
} else { } else {
@ -131,8 +132,8 @@ class MainActivity : AppCompatActivity() {
val intentSettings = val intentSettings =
Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS) Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
intentSettings.data = intentSettings.data =
Uri.fromParts("package", this.packageName, null) Uri.fromParts(Constants.URI_PACKAGE_SCHEME, this.packageName, null)
startActivity(intentSettings); startActivity(intentSettings)
}, },
message = getString(R.string.notification_permission_required), message = getString(R.string.notification_permission_required),
getString(R.string.open_settings) getString(R.string.open_settings)
@ -190,10 +191,19 @@ class MainActivity : AppCompatActivity() {
}) { SupportScreen(padding = padding, focusRequester) } }) { SupportScreen(padding = padding, focusRequester) }
composable("${Routes.Config.name}/{id}", enterTransition = { composable("${Routes.Config.name}/{id}", enterTransition = {
fadeIn(animationSpec = tween(Constants.FADE_IN_ANIMATION_DURATION)) fadeIn(animationSpec = tween(Constants.FADE_IN_ANIMATION_DURATION))
}) { ConfigScreen(padding = padding, navController = navController, id = it.arguments?.getString("id"), focusRequester = focusRequester)} }) {
val id = it.arguments?.getString("id")
if(!id.isNullOrBlank()) {
ConfigScreen(padding = padding, navController = navController, id = id, focusRequester = focusRequester)}
}
composable("${Routes.Detail.name}/{id}", enterTransition = { composable("${Routes.Detail.name}/{id}", enterTransition = {
fadeIn(animationSpec = tween(Constants.FADE_IN_ANIMATION_DURATION)) fadeIn(animationSpec = tween(Constants.FADE_IN_ANIMATION_DURATION))
}) { DetailScreen(padding = padding, id = it.arguments?.getString("id")) } }) {
val id = it.arguments?.getString("id")
if(!id.isNullOrBlank()) {
DetailScreen(padding = padding, id = id)
}
}
} }
} }
} }

View File

@ -24,6 +24,7 @@ import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.ExperimentalComposeUiApi
@ -53,7 +54,7 @@ fun ConfigScreen(
padding: PaddingValues, padding: PaddingValues,
focusRequester: FocusRequester, focusRequester: FocusRequester,
navController: NavController, navController: NavController,
id : String? id : String
) { ) {
val context = LocalContext.current val context = LocalContext.current
@ -67,11 +68,12 @@ fun ConfigScreen(
val checkedPackages by viewModel.checkedPackages.collectAsStateWithLifecycle() val checkedPackages by viewModel.checkedPackages.collectAsStateWithLifecycle()
val include by viewModel.include.collectAsStateWithLifecycle() val include by viewModel.include.collectAsStateWithLifecycle()
val allApplications by viewModel.allApplications.collectAsStateWithLifecycle() val allApplications by viewModel.allApplications.collectAsStateWithLifecycle()
val sortedPackages = remember(packages) {
packages.sortedBy { viewModel.getPackageLabel(it) }
}
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
viewModel.getTunnelById(id) viewModel.emitScreenData(id)
viewModel.emitQueriedPackages("")
viewModel.emitCurrentPackageConfigurations(id)
} }
if(tunnel != null) { if(tunnel != null) {
@ -174,7 +176,7 @@ fun ConfigScreen(
SearchBar(viewModel::emitQueriedPackages); SearchBar(viewModel::emitQueriedPackages);
} }
} }
items(packages) { pack -> items(sortedPackages, key = { it.packageName }) { pack ->
Row( Row(
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween horizontalArrangement = Arrangement.SpaceBetween
@ -200,8 +202,7 @@ fun ConfigScreen(
) )
} }
Text( Text(
pack.applicationInfo.loadLabel(context.packageManager) viewModel.getPackageLabel(pack), modifier = Modifier.padding(5.dp)
.toString(), modifier = Modifier.padding(5.dp)
) )
} }
Checkbox( Checkbox(

View File

@ -9,11 +9,14 @@ import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.toMutableStateList import androidx.compose.runtime.toMutableStateList
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.wireguard.config.Config
import com.zaneschepke.wireguardautotunnel.repository.SettingsDoa import com.zaneschepke.wireguardautotunnel.repository.SettingsDoa
import com.zaneschepke.wireguardautotunnel.repository.TunnelConfigDao import com.zaneschepke.wireguardautotunnel.repository.TunnelConfigDao
import com.zaneschepke.wireguardautotunnel.service.shortcut.ShortcutsManager import com.zaneschepke.wireguardautotunnel.service.shortcut.ShortcutsManager
import com.zaneschepke.wireguardautotunnel.repository.model.TunnelConfig import com.zaneschepke.wireguardautotunnel.repository.model.TunnelConfig
import com.zaneschepke.wireguardautotunnel.util.WgTunnelException
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -41,24 +44,37 @@ class ConfigViewModel @Inject constructor(private val application : Application,
private val _allApplications = MutableStateFlow(true) private val _allApplications = MutableStateFlow(true)
val allApplications get() = _allApplications.asStateFlow() val allApplications get() = _allApplications.asStateFlow()
suspend fun getTunnelById(id : String?) : TunnelConfig? { fun emitScreenData(id : String) {
return try { viewModelScope.launch(Dispatchers.IO) {
if(id != null) { val tunnelConfig = getTunnelConfigById(id);
val config = tunnelRepo.getById(id.toLong()) emitTunnelConfig(tunnelConfig);
if (config != null) { emitTunnelConfigName(tunnelConfig?.name)
_tunnel.emit(config) emitQueriedPackages("")
_tunnelName.emit(config.name) emitCurrentPackageConfigurations(id)
}
}
} private suspend fun getTunnelConfigById(id : String) : TunnelConfig? {
return config return try {
} tunnelRepo.getById(id.toLong())
return null
} catch (e : Exception) { } catch (e : Exception) {
Timber.e(e.message) Timber.e(e.message)
null null
} }
} }
private suspend fun emitTunnelConfig(tunnelConfig: TunnelConfig?) {
if(tunnelConfig != null) {
_tunnel.emit(tunnelConfig)
}
}
private suspend fun emitTunnelConfigName(name : String?) {
if(name != null) {
_tunnelName.emit(name)
}
}
fun onTunnelNameChange(name : String) { fun onTunnelNameChange(name : String) {
_tunnelName.value = name _tunnelName.value = name
} }
@ -78,35 +94,71 @@ class ConfigViewModel @Inject constructor(private val application : Application,
_checkedPackages.value.remove(packageName) _checkedPackages.value.remove(packageName)
} }
suspend fun emitCurrentPackageConfigurations(id : String?) { private suspend fun emitSplitTunnelConfiguration(config : Config) {
val tunnelConfig = getTunnelById(id) val excludedApps = config.`interface`.excludedApplications
if(tunnelConfig != null) { val includedApps = config.`interface`.includedApplications
val config = TunnelConfig.configFromQuick(tunnelConfig.wgQuick) if (excludedApps.isNotEmpty() || includedApps.isNotEmpty()) {
val excludedApps = config.`interface`.excludedApplications emitTunnelAllApplicationsDisabled()
val includedApps = config.`interface`.includedApplications determineAppInclusionState(excludedApps, includedApps)
if(excludedApps.isNullOrEmpty() && includedApps.isNullOrEmpty()) { } else {
_allApplications.emit(true) emitTunnelAllApplicationsEnabled()
return }
}
private suspend fun determineAppInclusionState(excludedApps : Set<String>, includedApps : Set<String>) {
if (excludedApps.isEmpty()) {
emitIncludedAppsExist()
emitCheckedApps(includedApps)
} else {
emitExcludedAppsExist()
emitCheckedApps(excludedApps)
}
}
private suspend fun emitIncludedAppsExist() {
_include.emit(true)
}
private suspend fun emitExcludedAppsExist() {
_include.emit(false)
}
private suspend fun emitCheckedApps(apps : Set<String>) {
_checkedPackages.emit(apps.toMutableStateList())
}
private suspend fun emitTunnelAllApplicationsEnabled() {
_allApplications.emit(true)
}
private suspend fun emitTunnelAllApplicationsDisabled() {
_allApplications.emit(false)
}
private fun emitCurrentPackageConfigurations(id : String) {
viewModelScope.launch(Dispatchers.IO) {
val tunnelConfig = getTunnelConfigById(id)
if (tunnelConfig != null) {
val config = TunnelConfig.configFromQuick(tunnelConfig.wgQuick)
emitSplitTunnelConfiguration(config)
} }
if(excludedApps.isEmpty()) {
_include.emit(true)
_checkedPackages.emit(includedApps.toMutableStateList())
} else {
_include.emit(false)
_checkedPackages.emit(excludedApps.toMutableStateList())
}
_allApplications.emit(false)
} }
} }
fun emitQueriedPackages(query : String) { fun emitQueriedPackages(query : String) {
viewModelScope.launch { viewModelScope.launch(Dispatchers.IO) {
_packages.emit(getAllInternetCapablePackages().filter { val packages = getAllInternetCapablePackages().filter {
it.packageName.contains(query) getPackageLabel(it).lowercase().contains(query.lowercase())
}) }
_packages.emit(packages)
} }
} }
fun getPackageLabel(packageInfo : PackageInfo) : String {
return packageInfo.applicationInfo.loadLabel(application.packageManager).toString()
}
private fun getAllInternetCapablePackages() : List<PackageInfo> { private fun getAllInternetCapablePackages() : List<PackageInfo> {
return getPackagesHoldingPermissions(arrayOf(Manifest.permission.INTERNET)) return getPackagesHoldingPermissions(arrayOf(Manifest.permission.INTERNET))
} }
@ -119,39 +171,77 @@ class ConfigViewModel @Inject constructor(private val application : Application,
} }
} }
suspend fun onSaveAllChanges() { private fun removeTunnelShortcuts(tunnelConfig: TunnelConfig?) {
if(_tunnel.value != null) { if(tunnelConfig != null) {
ShortcutsManager.removeTunnelShortcuts(application, _tunnel.value!!) ShortcutsManager.removeTunnelShortcuts(application, tunnelConfig)
} }
}
private fun isAllApplicationsEnabled() : Boolean {
return _allApplications.value
}
private fun isIncludeApplicationsEnabled() : Boolean {
return _include.value
}
private fun updateQuickStringWithSelectedPackages() : String {
var wgQuick = _tunnel.value?.wgQuick var wgQuick = _tunnel.value?.wgQuick
if(wgQuick != null) { if(wgQuick != null) {
wgQuick = if(_include.value) { wgQuick = if(isAllApplicationsEnabled()) {
TunnelConfig.clearAllApplicationsFromConfig(wgQuick)
} else if(isIncludeApplicationsEnabled()) {
TunnelConfig.setIncludedApplicationsOnQuick(_checkedPackages.value, wgQuick) TunnelConfig.setIncludedApplicationsOnQuick(_checkedPackages.value, wgQuick)
} else { } else {
TunnelConfig.setExcludedApplicationsOnQuick(_checkedPackages.value, wgQuick) TunnelConfig.setExcludedApplicationsOnQuick(_checkedPackages.value, wgQuick)
} }
if(_allApplications.value) { } else {
wgQuick = TunnelConfig.clearAllApplicationsFromConfig(wgQuick) throw WgTunnelException("Wg quick string is null")
} }
_tunnel.value?.copy( return wgQuick;
name = _tunnelName.value, }
wgQuick = wgQuick
)?.let { private suspend fun saveConfig(tunnelConfig: TunnelConfig) {
tunnelRepo.save(it) tunnelRepo.save(tunnelConfig)
ShortcutsManager.createTunnelShortcuts(application, it) }
val settings = settingsRepo.getAll() private suspend fun updateTunnelConfig(tunnelConfig: TunnelConfig?) {
if(settings.isEmpty()) { if(tunnelConfig != null) {
return saveConfig(tunnelConfig)
} addTunnelShortcuts(tunnelConfig)
val setting = settings[0] updateSettingsDefaultTunnel(tunnelConfig)
if(setting.defaultTunnel != null) { }
if(it.id == TunnelConfig.from(setting.defaultTunnel!!).id) { }
settingsRepo.save(setting.copy(
defaultTunnel = it.toString() private suspend fun updateSettingsDefaultTunnel(tunnelConfig: TunnelConfig) {
)) val settings = settingsRepo.getAll()
} if(settings.isNotEmpty()) {
val setting = settings[0]
if(setting.defaultTunnel != null) {
if(tunnelConfig.id == TunnelConfig.from(setting.defaultTunnel!!).id) {
settingsRepo.save(setting.copy(
defaultTunnel = tunnelConfig.toString()
))
} }
} }
} }
} }
private fun addTunnelShortcuts(tunnelConfig: TunnelConfig) {
ShortcutsManager.createTunnelShortcuts(application, tunnelConfig)
}
suspend fun onSaveAllChanges() {
try {
removeTunnelShortcuts(_tunnel.value)
val wgQuick = updateQuickStringWithSelectedPackages()
val tunnelConfig = _tunnel.value?.copy(
name = _tunnelName.value,
wgQuick = wgQuick
)
updateTunnelConfig(tunnelConfig)
} catch (e : Exception) {
Timber.e(e.message)
}
}
} }

View File

@ -36,7 +36,7 @@ import java.time.Instant
fun DetailScreen( fun DetailScreen(
viewModel: DetailViewModel = hiltViewModel(), viewModel: DetailViewModel = hiltViewModel(),
padding: PaddingValues, padding: PaddingValues,
id : String? id : String
) { ) {
val clipboardManager: ClipboardManager = LocalClipboardManager.current val clipboardManager: ClipboardManager = LocalClipboardManager.current
@ -47,15 +47,17 @@ fun DetailScreen(
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
viewModel.getTunnelById(id) viewModel.emitConfig(id)
} }
if(tunnel != null) { if(null != tunnel) {
val interfaceKey = tunnel?.`interface`?.keyPair?.publicKey?.toBase64().toString() val interfaceKey = tunnel?.`interface`?.keyPair?.publicKey?.toBase64().toString()
val addresses = tunnel?.`interface`?.addresses!!.joinToString() val addresses = tunnel?.`interface`?.addresses!!.joinToString()
val dnsServers = tunnel?.`interface`?.dnsServers!!.joinToString() val dnsServers = tunnel?.`interface`?.dnsServers!!.joinToString()
val optionalMtu = tunnel?.`interface`?.mtu val optionalMtu = tunnel?.`interface`?.mtu
val mtu = if(optionalMtu?.isPresent == true) optionalMtu.get().toString() else "None" val mtu = if(optionalMtu?.isPresent == true) optionalMtu.get().toString() else stringResource(
id = R.string.none
)
Column( Column(
horizontalAlignment = Alignment.Start, horizontalAlignment = Alignment.Start,
verticalArrangement = Arrangement.Top, verticalArrangement = Arrangement.Top,
@ -97,7 +99,9 @@ fun DetailScreen(
tunnel?.peers?.forEach{ tunnel?.peers?.forEach{
val peerKey = it.publicKey.toBase64().toString() val peerKey = it.publicKey.toBase64().toString()
val allowedIps = it.allowedIps.joinToString() val allowedIps = it.allowedIps.joinToString()
val endpoint = if(it.endpoint.isPresent) it.endpoint.get().toString() else "None" val endpoint = if(it.endpoint.isPresent) it.endpoint.get().toString() else stringResource(
id = R.string.none
)
Text(stringResource(R.string.peer), fontWeight = FontWeight.Bold, fontSize = 20.sp) Text(stringResource(R.string.peer), fontWeight = FontWeight.Bold, fontSize = 20.sp)
Text(stringResource(R.string.public_key), fontStyle = FontStyle.Italic) Text(stringResource(R.string.public_key), fontStyle = FontStyle.Italic)
Text(text = peerKey, modifier = Modifier.clickable { Text(text = peerKey, modifier = Modifier.clickable {
@ -123,7 +127,7 @@ fun DetailScreen(
val handshakeEpoch = lastHandshake[it.publicKey] val handshakeEpoch = lastHandshake[it.publicKey]
if(handshakeEpoch != null) { if(handshakeEpoch != null) {
if(handshakeEpoch == 0L) { if(handshakeEpoch == 0L) {
Text("Never") Text(stringResource(id = R.string.never))
} else { } else {
val time = Instant.ofEpochMilli(handshakeEpoch) val time = Instant.ofEpochMilli(handshakeEpoch)
Text("${Duration.between(time, Instant.now()).seconds} seconds ago") Text("${Duration.between(time, Instant.now()).seconds} seconds ago")

View File

@ -1,45 +1,45 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.detail package com.zaneschepke.wireguardautotunnel.ui.screens.detail
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.wireguard.config.Config import com.wireguard.config.Config
import com.zaneschepke.wireguardautotunnel.repository.TunnelConfigDao import com.zaneschepke.wireguardautotunnel.repository.TunnelConfigDao
import com.zaneschepke.wireguardautotunnel.repository.model.TunnelConfig import com.zaneschepke.wireguardautotunnel.repository.model.TunnelConfig
import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnService import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnService
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
@HiltViewModel @HiltViewModel
class DetailViewModel @Inject constructor(private val tunnelRepo : TunnelConfigDao, private val vpnService : VpnService class DetailViewModel @Inject constructor(private val tunnelRepo : TunnelConfigDao, private val vpnService : VpnService
) : ViewModel() { ) : ViewModel() {
private val _tunnel = MutableStateFlow<Config?>(null) private val _tunnel = MutableStateFlow<Config?>(null)
val tunnel get() = _tunnel.asStateFlow() val tunnel get() = _tunnel.asStateFlow()
private val _tunnelName = MutableStateFlow<String>("") private val _tunnelName = MutableStateFlow("")
val tunnelName = _tunnelName.asStateFlow() val tunnelName = _tunnelName.asStateFlow()
val tunnelStats get() = vpnService.statistics val tunnelStats get() = vpnService.statistics
val lastHandshake get() = vpnService.lastHandshake val lastHandshake get() = vpnService.lastHandshake
private var config : TunnelConfig? = null private suspend fun getTunnelConfigById(id: String): TunnelConfig? {
suspend fun getTunnelById(id : String?) : TunnelConfig? {
return try { return try {
if(id != null) { tunnelRepo.getById(id.toLong())
config = tunnelRepo.getById(id.toLong()) } catch (e: Exception) {
if (config != null) {
_tunnel.emit(TunnelConfig.configFromQuick(config!!.wgQuick))
_tunnelName.emit(config!!.name)
}
return config
}
return null
} catch (e : Exception) {
Timber.e(e.message) Timber.e(e.message)
null null
} }
} }
fun emitConfig(id: String) {
viewModelScope.launch(Dispatchers.IO) {
val tunnelConfig = getTunnelConfigById(id)
if(tunnelConfig != null) {
_tunnel.emit(TunnelConfig.configFromQuick(tunnelConfig.wgQuick))
}
}
}
} }

View File

@ -73,11 +73,12 @@ import androidx.navigation.NavController
import com.journeyapps.barcodescanner.ScanContract import com.journeyapps.barcodescanner.ScanContract
import com.journeyapps.barcodescanner.ScanOptions import com.journeyapps.barcodescanner.ScanOptions
import com.wireguard.android.backend.Tunnel import com.wireguard.android.backend.Tunnel
import com.zaneschepke.wireguardautotunnel.ui.CaptureActivityPortrait import com.zaneschepke.wireguardautotunnel.Constants
import com.zaneschepke.wireguardautotunnel.R import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
import com.zaneschepke.wireguardautotunnel.repository.model.TunnelConfig import com.zaneschepke.wireguardautotunnel.repository.model.TunnelConfig
import com.zaneschepke.wireguardautotunnel.service.tunnel.HandshakeStatus import com.zaneschepke.wireguardautotunnel.service.tunnel.HandshakeStatus
import com.zaneschepke.wireguardautotunnel.ui.CaptureActivityPortrait
import com.zaneschepke.wireguardautotunnel.ui.Routes import com.zaneschepke.wireguardautotunnel.ui.Routes
import com.zaneschepke.wireguardautotunnel.ui.common.RowListItem import com.zaneschepke.wireguardautotunnel.ui.common.RowListItem
import com.zaneschepke.wireguardautotunnel.ui.theme.brickRed import com.zaneschepke.wireguardautotunnel.ui.theme.brickRed
@ -146,7 +147,13 @@ fun MainScreen(
val scanLauncher = rememberLauncherForActivityResult( val scanLauncher = rememberLauncherForActivityResult(
contract = ScanContract(), contract = ScanContract(),
onResult = { result -> viewModel.onTunnelQrResult(result.contents) } onResult = {
try {
viewModel.onTunnelQrResult(it.contents)
} catch (e : Exception) {
viewModel.showSnackBarMessage(context.getString(R.string.qr_result_failed))
}
}
) )
Scaffold( Scaffold(
@ -205,7 +212,7 @@ fun MainScreen(
showBottomSheet = false showBottomSheet = false
val fileSelectionIntent = Intent(Intent.ACTION_OPEN_DOCUMENT).apply { val fileSelectionIntent = Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
addCategory(Intent.CATEGORY_OPENABLE) addCategory(Intent.CATEGORY_OPENABLE)
type = "*/*" type = Constants.ALLOWED_FILE_TYPES
} }
pickFileLauncher.launch(fileSelectionIntent) pickFileLauncher.launch(fileSelectionIntent)
} }
@ -232,7 +239,7 @@ fun MainScreen(
scanOptions.setOrientationLocked(true) scanOptions.setOrientationLocked(true)
scanOptions.setPrompt(context.getString(R.string.scanning_qr)) scanOptions.setPrompt(context.getString(R.string.scanning_qr))
scanOptions.setBeepEnabled(false) scanOptions.setBeepEnabled(false)
scanOptions.captureActivity = CaptureActivityPortrait().javaClass scanOptions.captureActivity = CaptureActivityPortrait::class.java
scanLauncher.launch(scanOptions) scanLauncher.launch(scanOptions)
} }
} }
@ -264,7 +271,7 @@ fun MainScreen(
.nestedScroll(nestedScrollConnection), .nestedScroll(nestedScrollConnection),
) { ) {
items(tunnels, key = { tunnel -> tunnel.id }) {tunnel -> items(tunnels, key = { tunnel -> tunnel.id }) {tunnel ->
val focusRequester = FocusRequester(); val focusRequester = FocusRequester()
RowListItem(leadingIcon = Icons.Rounded.Circle, RowListItem(leadingIcon = Icons.Rounded.Circle,
leadingIconColor = if (tunnelName == tunnel.name) when (handshakeStatus) { leadingIconColor = if (tunnelName == tunnel.name) when (handshakeStatus) {
HandshakeStatus.HEALTHY -> mint HandshakeStatus.HEALTHY -> mint
@ -281,7 +288,7 @@ fun MainScreen(
return@RowListItem return@RowListItem
} }
haptic.performHapticFeedback(HapticFeedbackType.LongPress) haptic.performHapticFeedback(HapticFeedbackType.LongPress)
selectedTunnel = tunnel; selectedTunnel = tunnel
}, },
onClick = { onClick = {
if (!WireGuardAutoTunnel.isRunningOnAndroidTv(context)) { if (!WireGuardAutoTunnel.isRunningOnAndroidTv(context)) {

View File

@ -1,8 +1,8 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.main package com.zaneschepke.wireguardautotunnel.ui.screens.main
import android.annotation.SuppressLint
import android.app.Application import android.app.Application
import android.content.Context import android.content.Context
import android.database.Cursor
import android.net.Uri import android.net.Uri
import android.provider.OpenableColumns import android.provider.OpenableColumns
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
@ -18,18 +18,21 @@ import com.zaneschepke.wireguardautotunnel.repository.model.TunnelConfig
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceState import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceState
import com.zaneschepke.wireguardautotunnel.service.foreground.WireGuardConnectivityWatcherService import com.zaneschepke.wireguardautotunnel.service.foreground.WireGuardConnectivityWatcherService
import com.zaneschepke.wireguardautotunnel.service.foreground.WireGuardTunnelService
import com.zaneschepke.wireguardautotunnel.service.shortcut.ShortcutsManager import com.zaneschepke.wireguardautotunnel.service.shortcut.ShortcutsManager
import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnService import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnService
import com.zaneschepke.wireguardautotunnel.ui.ViewState import com.zaneschepke.wireguardautotunnel.ui.ViewState
import com.zaneschepke.wireguardautotunnel.util.NumberUtils import com.zaneschepke.wireguardautotunnel.util.NumberUtils
import com.zaneschepke.wireguardautotunnel.util.WgTunnelException
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import timber.log.Timber import java.io.InputStream
import javax.inject.Inject import javax.inject.Inject
@ -86,88 +89,164 @@ class MainViewModel @Inject constructor(private val application : Application,
} }
} }
fun onTunnelStart(tunnelConfig : TunnelConfig) = viewModelScope.launch { fun onTunnelStart(tunnelConfig : TunnelConfig) {
viewModelScope.launch {
stopActiveTunnel()
startTunnel(tunnelConfig)
}
}
private fun startTunnel(tunnelConfig: TunnelConfig) {
ServiceManager.startVpnService(application.applicationContext, tunnelConfig.toString()) ServiceManager.startVpnService(application.applicationContext, tunnelConfig.toString())
} }
private suspend fun stopActiveTunnel() {
if(ServiceManager.getServiceState(application.applicationContext,
WireGuardTunnelService::class.java, ) == ServiceState.STARTED) {
onTunnelStop()
delay(Constants.TOGGLE_TUNNEL_DELAY)
}
}
fun onTunnelStop() { fun onTunnelStop() {
ServiceManager.stopVpnService(application.applicationContext) ServiceManager.stopVpnService(application.applicationContext)
} }
private fun validateConfigString(config : String) {
if(!config.contains(application.getString(R.string.config_validation))) {
throw WgTunnelException(application.getString(R.string.config_validation))
}
}
fun onTunnelQrResult(result : String) { fun onTunnelQrResult(result : String) {
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
if(result.contains(application.resources.getString(R.string.config_validation))) { try {
validateConfigString(result)
val tunnelConfig = TunnelConfig(name = NumberUtils.generateRandomTunnelName(), wgQuick = result) val tunnelConfig = TunnelConfig(name = NumberUtils.generateRandomTunnelName(), wgQuick = result)
saveTunnel(tunnelConfig) addTunnel(tunnelConfig)
} else { } catch (e : WgTunnelException) {
showSnackBarMessage(application.resources.getString(R.string.barcode_error)) showSnackBarMessage(e.message ?: application.getString(R.string.unknown_error_message))
} }
} }
} }
fun onTunnelFileSelected(uri : Uri) { private fun validateFileExtension(fileName : String) {
viewModelScope.launch(Dispatchers.IO) { val extension = getFileExtensionFromFileName(fileName)
try { if(extension != Constants.VALID_FILE_EXTENSION) {
val fileName = getFileName(application.applicationContext, uri) throw WgTunnelException(application.getString(R.string.file_extension_message))
val extension = getFileExtensionFromFileName(fileName)
if (extension != ".conf") {
launch {
showSnackBarMessage(application.resources.getString(R.string.file_extension_message))
}
return@launch
}
val stream = application.applicationContext.contentResolver.openInputStream(uri)
stream ?: return@launch
val bufferReader = stream.bufferedReader(charset = Charsets.UTF_8)
val config = Config.parse(bufferReader)
val tunnelName = getNameFromFileName(fileName)
saveTunnel(TunnelConfig(name = tunnelName, wgQuick = config.toWgQuickString()))
stream.close()
} catch (_: BadConfigException) {
launch {
showSnackBarMessage(application.applicationContext.getString(R.string.bad_config))
}
}
} }
} }
private fun saveTunnelConfigFromStream(stream : InputStream, fileName : String) {
viewModelScope.launch(Dispatchers.IO) {
val bufferReader = stream.bufferedReader(charset = Charsets.UTF_8)
val config = Config.parse(bufferReader)
val tunnelName = getNameFromFileName(fileName)
addTunnel(TunnelConfig(name = tunnelName, wgQuick = config.toWgQuickString()))
stream.close()
}
}
private fun getInputStreamFromUri(uri: Uri): InputStream {
return application.applicationContext.contentResolver.openInputStream(uri)
?: throw WgTunnelException(application.getString(R.string.stream_failed))
}
fun onTunnelFileSelected(uri : Uri) {
try {
val fileName = getFileName(application.applicationContext, uri)
validateFileExtension(fileName)
val stream = getInputStreamFromUri(uri)
saveTunnelConfigFromStream(stream, fileName)
} catch (e : Exception) {
showExceptionMessage(e)
}
}
private fun showExceptionMessage(e : Exception) {
when(e) {
is BadConfigException -> {
showSnackBarMessage(application.getString(R.string.bad_config))
}
is WgTunnelException -> {
showSnackBarMessage(e.message ?: application.getString(R.string.unknown_error_message))
}
else -> showSnackBarMessage(application.getString(R.string.unknown_error_message))
}
}
private suspend fun addTunnel(tunnelConfig: TunnelConfig) {
saveTunnel(tunnelConfig)
createTunnelAppShortcuts(tunnelConfig)
}
private suspend fun saveTunnel(tunnelConfig : TunnelConfig) { private suspend fun saveTunnel(tunnelConfig : TunnelConfig) {
tunnelRepo.save(tunnelConfig) tunnelRepo.save(tunnelConfig)
}
private fun createTunnelAppShortcuts(tunnelConfig: TunnelConfig) {
ShortcutsManager.createTunnelShortcuts(application.applicationContext, tunnelConfig) ShortcutsManager.createTunnelShortcuts(application.applicationContext, tunnelConfig)
} }
@SuppressLint("Range") private fun getFileNameByCursor(context: Context, uri: Uri) : String {
private fun getFileName(context: Context, uri: Uri): String { val cursor = context.contentResolver.query(uri, null, null, null, null)
if (uri.scheme == "content") { if(cursor != null) {
val cursor = try {
context.contentResolver.query(uri, null, null, null, null)
} catch (e : Exception) {
Timber.d("Exception getting config name")
null
}
cursor ?: return NumberUtils.generateRandomTunnelName()
cursor.use { cursor.use {
if(cursor.moveToFirst()) { return getDisplayNameByCursor(it)
return cursor.getString(cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME))
}
} }
} else {
throw WgTunnelException("Failed to initialize cursor")
} }
return NumberUtils.generateRandomTunnelName()
} }
suspend fun showSnackBarMessage(message : String) { private fun getDisplayNameColumnIndex(cursor: Cursor) : Int {
_viewState.emit(_viewState.value.copy( val columnIndex = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME)
showSnackbarMessage = true, if(columnIndex == -1) {
snackbarMessage = message, throw WgTunnelException("Cursor out of bounds")
snackbarActionText = "Okay", }
onSnackbarActionClick = { return columnIndex
viewModelScope.launch { }
dismissSnackBar()
private fun getDisplayNameByCursor(cursor: Cursor) : String {
if(cursor.moveToFirst()) {
val index = getDisplayNameColumnIndex(cursor)
return cursor.getString(index)
} else {
throw WgTunnelException("Cursor failed to move to first")
}
}
private fun validateUriContentScheme(uri : Uri) {
if (uri.scheme != Constants.URI_CONTENT_SCHEME) {
throw WgTunnelException(application.getString(R.string.file_extension_message))
}
}
private fun getFileName(context: Context, uri: Uri): String {
validateUriContentScheme(uri)
return try {
getFileNameByCursor(context, uri)
} catch (_: Exception) {
NumberUtils.generateRandomTunnelName()
}
}
fun showSnackBarMessage(message : String) {
CoroutineScope(Dispatchers.IO).launch {
_viewState.emit(_viewState.value.copy(
showSnackbarMessage = true,
snackbarMessage = message,
snackbarActionText = application.getString(R.string.okay),
onSnackbarActionClick = {
viewModelScope.launch {
dismissSnackBar()
}
} }
} ))
)) delay(Constants.SNACKBAR_DELAY)
delay(Constants.SNACKBAR_DELAY) dismissSnackBar()
dismissSnackBar() }
} }
private suspend fun dismissSnackBar() { private suspend fun dismissSnackBar() {

View File

@ -392,6 +392,24 @@ fun SettingsScreen(
} }
) )
} }
Row(
modifier = Modifier
.fillMaxWidth()
.padding(screenPadding),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween
) {
Text("Tunnel on Ethernet")
Switch(
enabled = !settings.isAutoTunnelEnabled,
checked = settings.isTunnelOnEthernetEnabled,
onCheckedChange = {
scope.launch {
viewModel.onToggleTunnelOnEthernet()
}
}
)
}
Row( Row(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()

View File

@ -122,6 +122,18 @@ class SettingsViewModel @Inject constructor(private val application : Applicatio
showSnackBarMessage(application.getString(R.string.select_tunnel_message)) showSnackBarMessage(application.getString(R.string.select_tunnel_message))
} }
} }
suspend fun onToggleTunnelOnEthernet() {
if(_settings.value.defaultTunnel != null) {
_settings.emit(
_settings.value.copy(isTunnelOnEthernetEnabled = !_settings.value.isTunnelOnEthernetEnabled)
)
settingsRepo.save(_settings.value)
} else {
showSnackBarMessage(application.getString(R.string.select_tunnel_message))
}
}
fun checkLocationServicesEnabled() : Boolean { fun checkLocationServicesEnabled() : Boolean {
val locationManager = val locationManager =
application.getSystemService(Context.LOCATION_SERVICE) as LocationManager application.getSystemService(Context.LOCATION_SERVICE) as LocationManager

View File

@ -0,0 +1,3 @@
package com.zaneschepke.wireguardautotunnel.util
class WgTunnelException(message: String) : Exception(message)

View File

@ -38,7 +38,6 @@
<string name="trusted_ssid_empty_description">Enter SSID</string> <string name="trusted_ssid_empty_description">Enter SSID</string>
<string name="trusted_ssid_value_description">Submit SSID</string> <string name="trusted_ssid_value_description">Submit SSID</string>
<string name="config_validation">[Interface]</string> <string name="config_validation">[Interface]</string>
<string name="invalid_qr">Invalid QR code.</string>
<string name="add_from_file">Add tunnel from files</string> <string name="add_from_file">Add tunnel from files</string>
<string name="open_file">File Open</string> <string name="open_file">File Open</string>
<string name="add_from_qr">Add tunnel from QR code</string> <string name="add_from_qr">Add tunnel from QR code</string>
@ -62,7 +61,6 @@
<string name="public_key">Public key</string> <string name="public_key">Public key</string>
<string name="barcode_downloading">Waiting for the Barcode UI module to be downloaded.</string> <string name="barcode_downloading">Waiting for the Barcode UI module to be downloaded.</string>
<string name="barcode_downloading_message">Barcode module downloading. Try again.</string> <string name="barcode_downloading_message">Barcode module downloading. Try again.</string>
<string name="barcode_error">Invalid QR code. Try again.</string>
<string name="addresses">Addresses</string> <string name="addresses">Addresses</string>
<string name="dns_servers">DNS servers</string> <string name="dns_servers">DNS servers</string>
<string name="mtu">MTU</string> <string name="mtu">MTU</string>
@ -92,5 +90,10 @@
<string name="attempt_connection">Attempting connection..</string> <string name="attempt_connection">Attempting connection..</string>
<string name="vpn_starting">VPN Starting</string> <string name="vpn_starting">VPN Starting</string>
<string name="db_name">wg-tunnel-db</string> <string name="db_name">wg-tunnel-db</string>
<string name="scanning_qr">Reading QR code</string> <string name="scanning_qr">Scanning for QR</string>
<string name="qr_result_failed">QR scan failed</string>
<string name="none">None</string>
<string name="never">Never</string>
<string name="stream_failed">Failed to open file stream.</string>
<string name="unknown_error_message">An unknown error occurred.</string>
</resources> </resources>