mirror of
https://github.com/wgtunnel/android.git
synced 2026-07-03 14:07:49 +02:00
Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 11aea3f1c4 | |||
| 7fbc51af4c | |||
| 1714618f0c | |||
| 7cb798a111 |
@@ -10,17 +10,12 @@ android {
|
||||
namespace = "com.zaneschepke.wireguardautotunnel"
|
||||
compileSdk = 34
|
||||
|
||||
val versionMajor = 2
|
||||
val versionMinor = 5
|
||||
val versionPatch = 0
|
||||
val versionBuild = 0
|
||||
|
||||
defaultConfig {
|
||||
applicationId = "com.zaneschepke.wireguardautotunnel"
|
||||
minSdk = 26
|
||||
targetSdk = 34
|
||||
versionCode = versionMajor * 10000 + versionMinor * 1000 + versionPatch * 100 + versionBuild
|
||||
versionName = "${versionMajor}.${versionMinor}.${versionPatch}"
|
||||
versionCode = 30002
|
||||
versionName = "3.0.2"
|
||||
|
||||
multiDexEnabled = true
|
||||
|
||||
|
||||
@@ -56,6 +56,7 @@
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
|
||||
<action android:name="android.service.quicksettings.action.QS_TILE_PREFERENCES"/>
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity
|
||||
@@ -117,8 +118,5 @@
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
<receiver android:exported="false" android:name=".receiver.NotificationActionReceiver"/>
|
||||
<meta-data
|
||||
android:name="com.google.mlkit.vision.DEPENDENCIES"
|
||||
android:value="barcode_ui"/>
|
||||
</application>
|
||||
</manifest>
|
||||
@@ -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 = "*/*"
|
||||
}
|
||||
@@ -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<MobileDataService>
|
||||
|
||||
@Binds
|
||||
@ServiceScoped
|
||||
abstract fun provideEthernetService(ethernetService: EthernetService) : NetworkService<EthernetService>
|
||||
}
|
||||
@@ -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!!)
|
||||
|
||||
+1
-1
@@ -5,7 +5,7 @@ import androidx.room.TypeConverter
|
||||
class DatabaseListConverters {
|
||||
@TypeConverter
|
||||
fun listToString(value: MutableList<String>): String {
|
||||
return value.joinToString()
|
||||
return value.joinToString(",")
|
||||
}
|
||||
@TypeConverter
|
||||
fun <T> stringToList(value: String): MutableList<String> {
|
||||
|
||||
@@ -12,4 +12,5 @@ data class Settings(
|
||||
@ColumnInfo(name = "trusted_network_ssids") var trustedNetworkSSIDs : MutableList<String> = 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,
|
||||
)
|
||||
+36
-4
@@ -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<MobileDataService>
|
||||
|
||||
@Inject
|
||||
lateinit var ethernetService: NetworkService<EthernetService>
|
||||
|
||||
@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)
|
||||
|
||||
+10
@@ -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) {
|
||||
}
|
||||
+5
-3
@@ -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<String,String>) : Intent {
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
package com.zaneschepke.wireguardautotunnel.ui;
|
||||
|
||||
import com.journeyapps.barcodescanner.CaptureActivity;
|
||||
|
||||
public class CaptureActivityPortrait extends CaptureActivity {
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
package com.zaneschepke.wireguardautotunnel.ui
|
||||
|
||||
import com.journeyapps.barcodescanner.CaptureActivity
|
||||
|
||||
class CaptureActivityPortrait : CaptureActivity()
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+8
-7
@@ -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(
|
||||
|
||||
+146
-56
@@ -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<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) {
|
||||
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<PackageInfo> {
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
+10
-6
@@ -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")
|
||||
|
||||
+15
-15
@@ -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<Config?>(null)
|
||||
val tunnel get() = _tunnel.asStateFlow()
|
||||
|
||||
private val _tunnelName = MutableStateFlow<String>("")
|
||||
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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+18
-7
@@ -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,9 +212,13 @@ fun MainScreen(
|
||||
showBottomSheet = false
|
||||
val fileSelectionIntent = Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
|
||||
addCategory(Intent.CATEGORY_OPENABLE)
|
||||
type = "*/*"
|
||||
type = Constants.ALLOWED_FILE_TYPES
|
||||
}
|
||||
if (fileSelectionIntent.resolveActivity(context.packageManager) != null) {
|
||||
pickFileLauncher.launch(fileSelectionIntent)
|
||||
} else {
|
||||
viewModel.showSnackBarMessage(context.getString(R.string.no_file_app))
|
||||
}
|
||||
pickFileLauncher.launch(fileSelectionIntent)
|
||||
}
|
||||
.padding(10.dp)
|
||||
) {
|
||||
@@ -232,7 +243,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 +275,7 @@ fun MainScreen(
|
||||
.nestedScroll(nestedScrollConnection),
|
||||
) {
|
||||
items(tunnels, key = { tunnel -> tunnel.id }) {tunnel ->
|
||||
val focusRequester = FocusRequester();
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
RowListItem(leadingIcon = Icons.Rounded.Circle,
|
||||
leadingIconColor = if (tunnelName == tunnel.name) when (handshakeStatus) {
|
||||
HandshakeStatus.HEALTHY -> mint
|
||||
@@ -281,7 +292,7 @@ fun MainScreen(
|
||||
return@RowListItem
|
||||
}
|
||||
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
|
||||
selectedTunnel = tunnel;
|
||||
selectedTunnel = tunnel
|
||||
},
|
||||
onClick = {
|
||||
if (!WireGuardAutoTunnel.isRunningOnAndroidTv(context)) {
|
||||
|
||||
+136
-58
@@ -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,89 +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))) {
|
||||
val tunnelConfig =
|
||||
TunnelConfig(name = NumberUtils.generateRandomTunnelName(), wgQuick = result)
|
||||
saveTunnel(tunnelConfig)
|
||||
} else {
|
||||
showSnackBarMessage(application.resources.getString(R.string.barcode_error))
|
||||
try {
|
||||
validateConfigString(result)
|
||||
val tunnelConfig = TunnelConfig(name = NumberUtils.generateRandomTunnelName(), wgQuick = result)
|
||||
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() {
|
||||
|
||||
+18
@@ -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()
|
||||
|
||||
+12
@@ -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
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
package com.zaneschepke.wireguardautotunnel.util
|
||||
|
||||
class WgTunnelException(message: String) : Exception(message)
|
||||
@@ -38,7 +38,6 @@
|
||||
<string name="trusted_ssid_empty_description">Enter SSID</string>
|
||||
<string name="trusted_ssid_value_description">Submit SSID</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="open_file">File Open</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="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_error">Invalid QR code. Try again.</string>
|
||||
<string name="addresses">Addresses</string>
|
||||
<string name="dns_servers">DNS servers</string>
|
||||
<string name="mtu">MTU</string>
|
||||
@@ -92,5 +90,11 @@
|
||||
<string name="attempt_connection">Attempting connection..</string>
|
||||
<string name="vpn_starting">VPN Starting</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>
|
||||
<string name="no_file_app">No file app installed.</string>
|
||||
</resources>
|
||||
Reference in New Issue
Block a user