From 7fbc51af4c10debc457abdf3f2d94eb0f27c67f8 Mon Sep 17 00:00:00 2001 From: Zane Schepke Date: Sun, 24 Sep 2023 07:40:17 -0400 Subject: [PATCH] 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 --- app/build.gradle.kts | 4 +- app/src/main/AndroidManifest.xml | 4 +- .../wireguardautotunnel/Constants.kt | 6 +- .../module/ServiceModule.kt | 5 + .../receiver/BootReceiver.kt | 2 +- .../repository/model/Settings.kt | 1 + .../WireGuardConnectivityWatcherService.kt | 40 +++- .../service/network/EthernetService.kt | 10 + .../service/shortcut/ShortcutsManager.kt | 8 +- .../ui/CaptureActivityPortrait.java | 6 - .../ui/CaptureActivityPortrait.kt | 5 + .../wireguardautotunnel/ui/MainActivity.kt | 20 +- .../ui/screens/config/ConfigScreen.kt | 15 +- .../ui/screens/config/ConfigViewModel.kt | 202 +++++++++++++----- .../ui/screens/detail/DetailScreen.kt | 16 +- .../ui/screens/detail/DetailViewModel.kt | 30 +-- .../ui/screens/main/MainScreen.kt | 19 +- .../ui/screens/main/MainViewModel.kt | 191 ++++++++++++----- .../ui/screens/settings/SettingsScreen.kt | 18 ++ .../ui/screens/settings/SettingsViewModel.kt | 12 ++ .../util/WgTunnelException.kt | 3 + app/src/main/res/values/strings.xml | 9 +- 22 files changed, 452 insertions(+), 174 deletions(-) create mode 100644 app/src/main/java/com/zaneschepke/wireguardautotunnel/service/network/EthernetService.kt delete mode 100644 app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/CaptureActivityPortrait.java create mode 100644 app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/CaptureActivityPortrait.kt create mode 100644 app/src/main/java/com/zaneschepke/wireguardautotunnel/util/WgTunnelException.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 044109e..dce8bc1 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -14,8 +14,8 @@ android { applicationId = "com.zaneschepke.wireguardautotunnel" minSdk = 26 targetSdk = 34 - versionCode = 30000 - versionName = "3.0.0" + versionCode = 30001 + versionName = "3.0.1" multiDexEnabled = true diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 3d9332b..be059cb 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -56,6 +56,7 @@ + - \ No newline at end of file diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/Constants.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/Constants.kt index 2cf4afe..d4283bc 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/Constants.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/Constants.kt @@ -4,8 +4,12 @@ object Constants { const val VPN_CONNECTIVITY_CHECK_INTERVAL = 3000L const val VPN_STATISTIC_CHECK_INTERVAL = 10000L 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 SLIDE_IN_ANIMATION_DURATION = 500 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 = "*/*" } \ No newline at end of file diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/module/ServiceModule.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/module/ServiceModule.kt index 59f6644..addf983 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/module/ServiceModule.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/module/ServiceModule.kt @@ -1,5 +1,6 @@ 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.NetworkService import com.zaneschepke.wireguardautotunnel.service.network.WifiService @@ -26,4 +27,8 @@ abstract class ServiceModule { @Binds @ServiceScoped abstract fun provideMobileDataService(mobileDataService : MobileDataService) : NetworkService + + @Binds + @ServiceScoped + abstract fun provideEthernetService(ethernetService: EthernetService) : NetworkService } \ No newline at end of file diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/receiver/BootReceiver.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/receiver/BootReceiver.kt index ae743f0..8d85636 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/receiver/BootReceiver.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/receiver/BootReceiver.kt @@ -23,7 +23,7 @@ class BootReceiver : BroadcastReceiver() { CoroutineScope(Dispatchers.IO).launch { try { val settings = settingsRepo.getAll() - if (!settings.isNullOrEmpty()) { + if (settings.isNotEmpty()) { val setting = settings.first() if (setting.isAutoTunnelEnabled && setting.defaultTunnel != null) { ServiceManager.startWatcherService(context, setting.defaultTunnel!!) diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/repository/model/Settings.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/repository/model/Settings.kt index dd67942..07879dc 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/repository/model/Settings.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/repository/model/Settings.kt @@ -12,4 +12,5 @@ data class Settings( @ColumnInfo(name = "trusted_network_ssids") var trustedNetworkSSIDs : MutableList = mutableListOf(), @ColumnInfo(name = "default_tunnel") var defaultTunnel : String? = null, @ColumnInfo(name = "is_always_on_vpn_enabled") var isAlwaysOnVpnEnabled : Boolean = false, + @ColumnInfo(name = "is_tunnel_on_ethernet_enabled") var isTunnelOnEthernetEnabled : Boolean = false, ) \ No newline at end of file diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/WireGuardConnectivityWatcherService.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/WireGuardConnectivityWatcherService.kt index 37e24d4..e6dcf4e 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/WireGuardConnectivityWatcherService.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/WireGuardConnectivityWatcherService.kt @@ -12,6 +12,7 @@ import com.zaneschepke.wireguardautotunnel.Constants import com.zaneschepke.wireguardautotunnel.R import com.zaneschepke.wireguardautotunnel.repository.SettingsDoa 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.NetworkService import com.zaneschepke.wireguardautotunnel.service.network.NetworkStatus @@ -38,6 +39,9 @@ class WireGuardConnectivityWatcherService : ForegroundService() { @Inject lateinit var mobileDataService : NetworkService + @Inject + lateinit var ethernetService: NetworkService + @Inject lateinit var settingsRepo: SettingsDoa @@ -48,6 +52,7 @@ class WireGuardConnectivityWatcherService : ForegroundService() { lateinit var vpnService : VpnService private var isWifiConnected = false; + private var isEthernetConnected = false; private var isMobileDataConnected = false; private var currentNetworkSSID = ""; @@ -142,6 +147,11 @@ class WireGuardConnectivityWatcherService : ForegroundService() { watchForMobileDataConnectivityChanges() } } + if(setting.isTunnelOnEthernetEnabled) { + launch { + watchForEthernetConnectivityChanges() + } + } launch { 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() { wifiService.networkStatus.collect { when (it) { @@ -189,20 +218,23 @@ class WireGuardConnectivityWatcherService : ForegroundService() { private suspend fun manageVpn() { while(true) { - if(setting.isTunnelOnMobileDataEnabled && + if(isEthernetConnected && setting.isTunnelOnEthernetEnabled && vpnService.getState() == Tunnel.State.DOWN) { + ServiceManager.startVpnService(this, tunnelConfig) + } + if(!isEthernetConnected && setting.isTunnelOnMobileDataEnabled && !isWifiConnected && isMobileDataConnected && vpnService.getState() == Tunnel.State.DOWN) { ServiceManager.startVpnService(this, tunnelConfig) - } else if(!setting.isTunnelOnMobileDataEnabled && + } else if(!isEthernetConnected && !setting.isTunnelOnMobileDataEnabled && !isWifiConnected && vpnService.getState() == Tunnel.State.UP) { ServiceManager.stopVpnService(this) - } else if(isWifiConnected && + } else if(!isEthernetConnected && isWifiConnected && !setting.trustedNetworkSSIDs.contains(currentNetworkSSID) && (vpnService.getState() != Tunnel.State.UP)) { ServiceManager.startVpnService(this, tunnelConfig) - } else if((isWifiConnected && + } else if(!isEthernetConnected && (isWifiConnected && setting.trustedNetworkSSIDs.contains(currentNetworkSSID)) && (vpnService.getState() == Tunnel.State.UP)) { ServiceManager.stopVpnService(this) diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/network/EthernetService.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/network/EthernetService.kt new file mode 100644 index 0000000..5450ca3 --- /dev/null +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/network/EthernetService.kt @@ -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(context, NetworkCapabilities.TRANSPORT_ETHERNET) { +} \ No newline at end of file diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/shortcut/ShortcutsManager.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/shortcut/ShortcutsManager.kt index 8798319..3b02d19 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/shortcut/ShortcutsManager.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/shortcut/ShortcutsManager.kt @@ -46,9 +46,11 @@ object ShortcutsManager { ) } - fun removeTunnelShortcuts(context : Context, tunnelConfig : TunnelConfig) { - ShortcutManagerCompat.removeDynamicShortcuts(context, listOf(tunnelConfig.id.toString() + APPEND_ON, - tunnelConfig.id.toString() + APPEND_OFF )) + fun removeTunnelShortcuts(context : Context, tunnelConfig : TunnelConfig?) { + if(tunnelConfig != null) { + ShortcutManagerCompat.removeDynamicShortcuts(context, listOf(tunnelConfig.id.toString() + APPEND_ON, + tunnelConfig.id.toString() + APPEND_OFF )) + } } private fun createTunnelOnIntent(context: Context, extras : Map) : Intent { diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/CaptureActivityPortrait.java b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/CaptureActivityPortrait.java deleted file mode 100644 index f9770eb..0000000 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/CaptureActivityPortrait.java +++ /dev/null @@ -1,6 +0,0 @@ -package com.zaneschepke.wireguardautotunnel.ui; - -import com.journeyapps.barcodescanner.CaptureActivity; - -public class CaptureActivityPortrait extends CaptureActivity { -} diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/CaptureActivityPortrait.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/CaptureActivityPortrait.kt new file mode 100644 index 0000000..9972857 --- /dev/null +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/CaptureActivityPortrait.kt @@ -0,0 +1,5 @@ +package com.zaneschepke.wireguardautotunnel.ui + +import com.journeyapps.barcodescanner.CaptureActivity + +class CaptureActivityPortrait : CaptureActivity() \ No newline at end of file diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/MainActivity.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/MainActivity.kt index b72f801..548355a 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/MainActivity.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/MainActivity.kt @@ -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.theme.TransparentSystemBars import com.zaneschepke.wireguardautotunnel.ui.theme.WireguardAutoTunnelTheme +import com.zaneschepke.wireguardautotunnel.util.WgTunnelException import dagger.hilt.android.AndroidEntryPoint import timber.log.Timber import java.lang.IllegalStateException @@ -101,7 +102,7 @@ class MainActivity : AppCompatActivity() { } false } else -> { - false; + false } } } else { @@ -131,8 +132,8 @@ class MainActivity : AppCompatActivity() { val intentSettings = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS) intentSettings.data = - Uri.fromParts("package", this.packageName, null) - startActivity(intentSettings); + Uri.fromParts(Constants.URI_PACKAGE_SCHEME, this.packageName, null) + startActivity(intentSettings) }, message = getString(R.string.notification_permission_required), getString(R.string.open_settings) @@ -190,10 +191,19 @@ class MainActivity : AppCompatActivity() { }) { SupportScreen(padding = padding, focusRequester) } composable("${Routes.Config.name}/{id}", enterTransition = { 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 = { 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) + } + } } } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/config/ConfigScreen.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/config/ConfigScreen.kt index bc38ed4..ccb75b6 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/config/ConfigScreen.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/config/ConfigScreen.kt @@ -24,6 +24,7 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment import androidx.compose.ui.ExperimentalComposeUiApi @@ -53,7 +54,7 @@ fun ConfigScreen( padding: PaddingValues, focusRequester: FocusRequester, navController: NavController, - id : String? + id : String ) { val context = LocalContext.current @@ -67,11 +68,12 @@ fun ConfigScreen( val checkedPackages by viewModel.checkedPackages.collectAsStateWithLifecycle() val include by viewModel.include.collectAsStateWithLifecycle() val allApplications by viewModel.allApplications.collectAsStateWithLifecycle() + val sortedPackages = remember(packages) { + packages.sortedBy { viewModel.getPackageLabel(it) } + } LaunchedEffect(Unit) { - viewModel.getTunnelById(id) - viewModel.emitQueriedPackages("") - viewModel.emitCurrentPackageConfigurations(id) + viewModel.emitScreenData(id) } if(tunnel != null) { @@ -174,7 +176,7 @@ fun ConfigScreen( SearchBar(viewModel::emitQueriedPackages); } } - items(packages) { pack -> + items(sortedPackages, key = { it.packageName }) { pack -> Row( verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.SpaceBetween @@ -200,8 +202,7 @@ fun ConfigScreen( ) } Text( - pack.applicationInfo.loadLabel(context.packageManager) - .toString(), modifier = Modifier.padding(5.dp) + viewModel.getPackageLabel(pack), modifier = Modifier.padding(5.dp) ) } Checkbox( diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/config/ConfigViewModel.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/config/ConfigViewModel.kt index 6afe299..770b47e 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/config/ConfigViewModel.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/config/ConfigViewModel.kt @@ -9,11 +9,14 @@ import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.toMutableStateList import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.wireguard.config.Config import com.zaneschepke.wireguardautotunnel.repository.SettingsDoa import com.zaneschepke.wireguardautotunnel.repository.TunnelConfigDao import com.zaneschepke.wireguardautotunnel.service.shortcut.ShortcutsManager import com.zaneschepke.wireguardautotunnel.repository.model.TunnelConfig +import com.zaneschepke.wireguardautotunnel.util.WgTunnelException import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch @@ -41,24 +44,37 @@ class ConfigViewModel @Inject constructor(private val application : Application, private val _allApplications = MutableStateFlow(true) val allApplications get() = _allApplications.asStateFlow() - suspend fun getTunnelById(id : String?) : TunnelConfig? { - return try { - if(id != null) { - val config = tunnelRepo.getById(id.toLong()) - if (config != null) { - _tunnel.emit(config) - _tunnelName.emit(config.name) + fun emitScreenData(id : String) { + viewModelScope.launch(Dispatchers.IO) { + val tunnelConfig = getTunnelConfigById(id); + emitTunnelConfig(tunnelConfig); + emitTunnelConfigName(tunnelConfig?.name) + emitQueriedPackages("") + emitCurrentPackageConfigurations(id) + } + } - } - return config - } - return null + private suspend fun getTunnelConfigById(id : String) : TunnelConfig? { + return try { + tunnelRepo.getById(id.toLong()) } catch (e : Exception) { Timber.e(e.message) 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) { _tunnelName.value = name } @@ -78,35 +94,71 @@ class ConfigViewModel @Inject constructor(private val application : Application, _checkedPackages.value.remove(packageName) } - suspend fun emitCurrentPackageConfigurations(id : String?) { - val tunnelConfig = getTunnelById(id) - if(tunnelConfig != null) { - val config = TunnelConfig.configFromQuick(tunnelConfig.wgQuick) - val excludedApps = config.`interface`.excludedApplications - val includedApps = config.`interface`.includedApplications - if(excludedApps.isNullOrEmpty() && includedApps.isNullOrEmpty()) { - _allApplications.emit(true) - return + private suspend fun emitSplitTunnelConfiguration(config : Config) { + val excludedApps = config.`interface`.excludedApplications + val includedApps = config.`interface`.includedApplications + if (excludedApps.isNotEmpty() || includedApps.isNotEmpty()) { + emitTunnelAllApplicationsDisabled() + determineAppInclusionState(excludedApps, includedApps) + } else { + emitTunnelAllApplicationsEnabled() + } + } + + private suspend fun determineAppInclusionState(excludedApps : Set, includedApps : Set) { + 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) { + _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) { - viewModelScope.launch { - _packages.emit(getAllInternetCapablePackages().filter { - it.packageName.contains(query) - }) + viewModelScope.launch(Dispatchers.IO) { + val packages = getAllInternetCapablePackages().filter { + 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 { return getPackagesHoldingPermissions(arrayOf(Manifest.permission.INTERNET)) } @@ -119,39 +171,77 @@ class ConfigViewModel @Inject constructor(private val application : Application, } } - suspend fun onSaveAllChanges() { - if(_tunnel.value != null) { - ShortcutsManager.removeTunnelShortcuts(application, _tunnel.value!!) + private fun removeTunnelShortcuts(tunnelConfig: TunnelConfig?) { + if(tunnelConfig != null) { + 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 if(wgQuick != null) { - wgQuick = if(_include.value) { + wgQuick = if(isAllApplicationsEnabled()) { + TunnelConfig.clearAllApplicationsFromConfig(wgQuick) + } else if(isIncludeApplicationsEnabled()) { TunnelConfig.setIncludedApplicationsOnQuick(_checkedPackages.value, wgQuick) } else { TunnelConfig.setExcludedApplicationsOnQuick(_checkedPackages.value, wgQuick) } - if(_allApplications.value) { - wgQuick = TunnelConfig.clearAllApplicationsFromConfig(wgQuick) - } - _tunnel.value?.copy( - name = _tunnelName.value, - wgQuick = wgQuick - )?.let { - tunnelRepo.save(it) - ShortcutsManager.createTunnelShortcuts(application, it) - val settings = settingsRepo.getAll() - if(settings.isEmpty()) { - return - } - val setting = settings[0] - if(setting.defaultTunnel != null) { - if(it.id == TunnelConfig.from(setting.defaultTunnel!!).id) { - settingsRepo.save(setting.copy( - defaultTunnel = it.toString() - )) - } + } else { + throw WgTunnelException("Wg quick string is null") + } + return wgQuick; + } + + private suspend fun saveConfig(tunnelConfig: TunnelConfig) { + tunnelRepo.save(tunnelConfig) + } + private suspend fun updateTunnelConfig(tunnelConfig: TunnelConfig?) { + if(tunnelConfig != null) { + saveConfig(tunnelConfig) + addTunnelShortcuts(tunnelConfig) + updateSettingsDefaultTunnel(tunnelConfig) + } + } + + 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) + } + } } \ No newline at end of file diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/detail/DetailScreen.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/detail/DetailScreen.kt index c663ebd..bf19e36 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/detail/DetailScreen.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/detail/DetailScreen.kt @@ -36,7 +36,7 @@ import java.time.Instant fun DetailScreen( viewModel: DetailViewModel = hiltViewModel(), padding: PaddingValues, - id : String? + id : String ) { val clipboardManager: ClipboardManager = LocalClipboardManager.current @@ -47,15 +47,17 @@ fun DetailScreen( LaunchedEffect(Unit) { - viewModel.getTunnelById(id) + viewModel.emitConfig(id) } - if(tunnel != null) { + if(null != tunnel) { val interfaceKey = tunnel?.`interface`?.keyPair?.publicKey?.toBase64().toString() val addresses = tunnel?.`interface`?.addresses!!.joinToString() val dnsServers = tunnel?.`interface`?.dnsServers!!.joinToString() 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( horizontalAlignment = Alignment.Start, verticalArrangement = Arrangement.Top, @@ -97,7 +99,9 @@ fun DetailScreen( tunnel?.peers?.forEach{ val peerKey = it.publicKey.toBase64().toString() 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.public_key), fontStyle = FontStyle.Italic) Text(text = peerKey, modifier = Modifier.clickable { @@ -123,7 +127,7 @@ fun DetailScreen( val handshakeEpoch = lastHandshake[it.publicKey] if(handshakeEpoch != null) { if(handshakeEpoch == 0L) { - Text("Never") + Text(stringResource(id = R.string.never)) } else { val time = Instant.ofEpochMilli(handshakeEpoch) Text("${Duration.between(time, Instant.now()).seconds} seconds ago") diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/detail/DetailViewModel.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/detail/DetailViewModel.kt index c6f39ac..ec48f74 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/detail/DetailViewModel.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/detail/DetailViewModel.kt @@ -1,45 +1,45 @@ package com.zaneschepke.wireguardautotunnel.ui.screens.detail import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope import com.wireguard.config.Config import com.zaneschepke.wireguardautotunnel.repository.TunnelConfigDao import com.zaneschepke.wireguardautotunnel.repository.model.TunnelConfig import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnService import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch import timber.log.Timber import javax.inject.Inject @HiltViewModel class DetailViewModel @Inject constructor(private val tunnelRepo : TunnelConfigDao, private val vpnService : VpnService - ) : ViewModel() { private val _tunnel = MutableStateFlow(null) val tunnel get() = _tunnel.asStateFlow() - private val _tunnelName = MutableStateFlow("") + private val _tunnelName = MutableStateFlow("") val tunnelName = _tunnelName.asStateFlow() val tunnelStats get() = vpnService.statistics val lastHandshake get() = vpnService.lastHandshake - private var config : TunnelConfig? = null - - suspend fun getTunnelById(id : String?) : TunnelConfig? { + private suspend fun getTunnelConfigById(id: String): TunnelConfig? { return try { - if(id != null) { - config = tunnelRepo.getById(id.toLong()) - if (config != null) { - _tunnel.emit(TunnelConfig.configFromQuick(config!!.wgQuick)) - _tunnelName.emit(config!!.name) - } - return config - } - return null - } catch (e : Exception) { + tunnelRepo.getById(id.toLong()) + } catch (e: Exception) { Timber.e(e.message) null } } + fun emitConfig(id: String) { + viewModelScope.launch(Dispatchers.IO) { + val tunnelConfig = getTunnelConfigById(id) + if(tunnelConfig != null) { + _tunnel.emit(TunnelConfig.configFromQuick(tunnelConfig.wgQuick)) + } + } + } } \ No newline at end of file diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/MainScreen.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/MainScreen.kt index 58a8ae4..3a8195f 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/MainScreen.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/MainScreen.kt @@ -73,11 +73,12 @@ import androidx.navigation.NavController import com.journeyapps.barcodescanner.ScanContract import com.journeyapps.barcodescanner.ScanOptions 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.WireGuardAutoTunnel import com.zaneschepke.wireguardautotunnel.repository.model.TunnelConfig import com.zaneschepke.wireguardautotunnel.service.tunnel.HandshakeStatus +import com.zaneschepke.wireguardautotunnel.ui.CaptureActivityPortrait import com.zaneschepke.wireguardautotunnel.ui.Routes import com.zaneschepke.wireguardautotunnel.ui.common.RowListItem import com.zaneschepke.wireguardautotunnel.ui.theme.brickRed @@ -146,7 +147,13 @@ fun MainScreen( val scanLauncher = rememberLauncherForActivityResult( 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( @@ -205,7 +212,7 @@ fun MainScreen( showBottomSheet = false val fileSelectionIntent = Intent(Intent.ACTION_OPEN_DOCUMENT).apply { addCategory(Intent.CATEGORY_OPENABLE) - type = "*/*" + type = Constants.ALLOWED_FILE_TYPES } pickFileLauncher.launch(fileSelectionIntent) } @@ -232,7 +239,7 @@ fun MainScreen( scanOptions.setOrientationLocked(true) scanOptions.setPrompt(context.getString(R.string.scanning_qr)) scanOptions.setBeepEnabled(false) - scanOptions.captureActivity = CaptureActivityPortrait().javaClass + scanOptions.captureActivity = CaptureActivityPortrait::class.java scanLauncher.launch(scanOptions) } } @@ -264,7 +271,7 @@ fun MainScreen( .nestedScroll(nestedScrollConnection), ) { items(tunnels, key = { tunnel -> tunnel.id }) {tunnel -> - val focusRequester = FocusRequester(); + val focusRequester = FocusRequester() RowListItem(leadingIcon = Icons.Rounded.Circle, leadingIconColor = if (tunnelName == tunnel.name) when (handshakeStatus) { HandshakeStatus.HEALTHY -> mint @@ -281,7 +288,7 @@ fun MainScreen( return@RowListItem } haptic.performHapticFeedback(HapticFeedbackType.LongPress) - selectedTunnel = tunnel; + selectedTunnel = tunnel }, onClick = { if (!WireGuardAutoTunnel.isRunningOnAndroidTv(context)) { diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/MainViewModel.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/MainViewModel.kt index aab12fe..31625af 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/MainViewModel.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/MainViewModel.kt @@ -1,8 +1,8 @@ package com.zaneschepke.wireguardautotunnel.ui.screens.main -import android.annotation.SuppressLint import android.app.Application import android.content.Context +import android.database.Cursor import android.net.Uri import android.provider.OpenableColumns 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.ServiceState 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.tunnel.VpnService import com.zaneschepke.wireguardautotunnel.ui.ViewState import com.zaneschepke.wireguardautotunnel.util.NumberUtils +import com.zaneschepke.wireguardautotunnel.util.WgTunnelException import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.filter import kotlinx.coroutines.launch -import timber.log.Timber +import java.io.InputStream 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()) } + private suspend fun stopActiveTunnel() { + if(ServiceManager.getServiceState(application.applicationContext, + WireGuardTunnelService::class.java, ) == ServiceState.STARTED) { + onTunnelStop() + delay(Constants.TOGGLE_TUNNEL_DELAY) + } + } + fun onTunnelStop() { 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) { 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) - saveTunnel(tunnelConfig) - } else { - showSnackBarMessage(application.resources.getString(R.string.barcode_error)) + addTunnel(tunnelConfig) + } catch (e : WgTunnelException) { + showSnackBarMessage(e.message ?: application.getString(R.string.unknown_error_message)) } } } - fun onTunnelFileSelected(uri : Uri) { - viewModelScope.launch(Dispatchers.IO) { - try { - val fileName = getFileName(application.applicationContext, uri) - 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 validateFileExtension(fileName : String) { + val extension = getFileExtensionFromFileName(fileName) + if(extension != Constants.VALID_FILE_EXTENSION) { + throw WgTunnelException(application.getString(R.string.file_extension_message)) } } + 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) { tunnelRepo.save(tunnelConfig) + } + + private fun createTunnelAppShortcuts(tunnelConfig: TunnelConfig) { ShortcutsManager.createTunnelShortcuts(application.applicationContext, tunnelConfig) } - @SuppressLint("Range") - private fun getFileName(context: Context, uri: Uri): String { - if (uri.scheme == "content") { - 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() + private fun getFileNameByCursor(context: Context, uri: Uri) : String { + val cursor = context.contentResolver.query(uri, null, null, null, null) + if(cursor != null) { cursor.use { - if(cursor.moveToFirst()) { - return cursor.getString(cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME)) - } + return getDisplayNameByCursor(it) } + } else { + throw WgTunnelException("Failed to initialize cursor") } - return NumberUtils.generateRandomTunnelName() } - suspend fun showSnackBarMessage(message : String) { - _viewState.emit(_viewState.value.copy( - showSnackbarMessage = true, - snackbarMessage = message, - snackbarActionText = "Okay", - onSnackbarActionClick = { - viewModelScope.launch { - dismissSnackBar() + private fun getDisplayNameColumnIndex(cursor: Cursor) : Int { + val columnIndex = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME) + if(columnIndex == -1) { + throw WgTunnelException("Cursor out of bounds") + } + return columnIndex + } + + 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) - dismissSnackBar() + )) + delay(Constants.SNACKBAR_DELAY) + dismissSnackBar() + } } private suspend fun dismissSnackBar() { diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/SettingsScreen.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/SettingsScreen.kt index 3275815..d187479 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/SettingsScreen.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/SettingsScreen.kt @@ -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( modifier = Modifier .fillMaxWidth() diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/SettingsViewModel.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/SettingsViewModel.kt index 0a4ecde..4d9bd35 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/SettingsViewModel.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/SettingsViewModel.kt @@ -122,6 +122,18 @@ class SettingsViewModel @Inject constructor(private val application : Applicatio 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 { val locationManager = application.getSystemService(Context.LOCATION_SERVICE) as LocationManager diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/WgTunnelException.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/WgTunnelException.kt new file mode 100644 index 0000000..8a34abe --- /dev/null +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/WgTunnelException.kt @@ -0,0 +1,3 @@ +package com.zaneschepke.wireguardautotunnel.util + +class WgTunnelException(message: String) : Exception(message) \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index da5c9e0..cfbec0b 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -38,7 +38,6 @@ Enter SSID Submit SSID [Interface] - Invalid QR code. Add tunnel from files File Open Add tunnel from QR code @@ -62,7 +61,6 @@ Public key Waiting for the Barcode UI module to be downloaded. Barcode module downloading. Try again. - Invalid QR code. Try again. Addresses DNS servers MTU @@ -92,5 +90,10 @@ Attempting connection.. VPN Starting wg-tunnel-db - Reading QR code + Scanning for QR + QR scan failed + None + Never + Failed to open file stream. + An unknown error occurred. \ No newline at end of file