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"
|
namespace = "com.zaneschepke.wireguardautotunnel"
|
||||||
compileSdk = 34
|
compileSdk = 34
|
||||||
|
|
||||||
val versionMajor = 2
|
|
||||||
val versionMinor = 5
|
|
||||||
val versionPatch = 0
|
|
||||||
val versionBuild = 0
|
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId = "com.zaneschepke.wireguardautotunnel"
|
applicationId = "com.zaneschepke.wireguardautotunnel"
|
||||||
minSdk = 26
|
minSdk = 26
|
||||||
targetSdk = 34
|
targetSdk = 34
|
||||||
versionCode = versionMajor * 10000 + versionMinor * 1000 + versionPatch * 100 + versionBuild
|
versionCode = 30002
|
||||||
versionName = "${versionMajor}.${versionMinor}.${versionPatch}"
|
versionName = "3.0.2"
|
||||||
|
|
||||||
multiDexEnabled = true
|
multiDexEnabled = true
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -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 = "*/*"
|
||||||
}
|
}
|
||||||
@@ -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>
|
||||||
}
|
}
|
||||||
@@ -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!!)
|
||||||
|
|||||||
+1
-1
@@ -5,7 +5,7 @@ import androidx.room.TypeConverter
|
|||||||
class DatabaseListConverters {
|
class DatabaseListConverters {
|
||||||
@TypeConverter
|
@TypeConverter
|
||||||
fun listToString(value: MutableList<String>): String {
|
fun listToString(value: MutableList<String>): String {
|
||||||
return value.joinToString()
|
return value.joinToString(",")
|
||||||
}
|
}
|
||||||
@TypeConverter
|
@TypeConverter
|
||||||
fun <T> stringToList(value: String): MutableList<String> {
|
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 = "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,
|
||||||
)
|
)
|
||||||
+36
-4
@@ -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)
|
||||||
|
|||||||
+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) {
|
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 {
|
||||||
|
|||||||
@@ -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.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)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+8
-7
@@ -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(
|
||||||
|
|||||||
+146
-56
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
+10
-6
@@ -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")
|
||||||
|
|||||||
+15
-15
@@ -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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
+18
-7
@@ -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,9 +212,13 @@ 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
|
||||||
|
}
|
||||||
|
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)
|
.padding(10.dp)
|
||||||
) {
|
) {
|
||||||
@@ -232,7 +243,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 +275,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 = remember { 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 +292,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)) {
|
||||||
|
|||||||
+136
-58
@@ -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,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())
|
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 {
|
||||||
val tunnelConfig =
|
validateConfigString(result)
|
||||||
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() {
|
||||||
|
|||||||
+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(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
|
|||||||
+12
@@ -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
|
||||||
|
|||||||
@@ -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_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,11 @@
|
|||||||
<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>
|
||||||
|
<string name="no_file_app">No file app installed.</string>
|
||||||
</resources>
|
</resources>
|
||||||
Reference in New Issue
Block a user