diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 74a44ee..3858032 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -14,8 +14,8 @@ android {
applicationId = "com.zaneschepke.wireguardautotunnel"
minSdk = 26
targetSdk = 34
- versionCode = 31300
- versionName = "3.1.3"
+ versionCode = 31400
+ versionName = "3.1.4"
multiDexEnabled = true
@@ -128,6 +128,9 @@ dependencies {
//lifecycle
implementation(libs.lifecycle.runtime.compose)
+ implementation(libs.androidx.lifecycle.runtime.ktx)
+ implementation(libs.androidx.lifecycle.process)
+
//icons
implementation(libs.material.icons.extended)
@@ -142,4 +145,8 @@ dependencies {
//barcode scanning
implementation(libs.zxing.android.embedded)
implementation(libs.zxing.core)
+
+ //bio
+ implementation(libs.androidx.biometric.ktx)
+
}
\ No newline at end of file
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 03404ee..bd35721 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -4,6 +4,10 @@
+
diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/Constants.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/Constants.kt
index 509b25c..264130a 100644
--- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/Constants.kt
+++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/Constants.kt
@@ -8,7 +8,8 @@ object Constants {
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 CONF_FILE_EXTENSION = ".conf"
+ const val ZIP_FILE_EXTENSION = ".zip"
const val URI_CONTENT_SCHEME = "content"
const val URI_PACKAGE_SCHEME = "package"
const val ALLOWED_FILE_TYPES = "*/*"
diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/Extensions.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/Extensions.kt
new file mode 100644
index 0000000..9e7efee
--- /dev/null
+++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/Extensions.kt
@@ -0,0 +1,24 @@
+package com.zaneschepke.wireguardautotunnel
+
+import android.content.BroadcastReceiver
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.DelicateCoroutinesApi
+import kotlinx.coroutines.GlobalScope
+import kotlinx.coroutines.launch
+import kotlin.coroutines.CoroutineContext
+import kotlin.coroutines.EmptyCoroutineContext
+
+fun BroadcastReceiver.goAsync(
+ context: CoroutineContext = EmptyCoroutineContext,
+ block: suspend CoroutineScope.() -> Unit
+) {
+ val pendingResult = goAsync()
+ @OptIn(DelicateCoroutinesApi::class) // Must run globally; there's no teardown callback.
+ GlobalScope.launch(context) {
+ try {
+ block()
+ } finally {
+ pendingResult.finish()
+ }
+ }
+}
diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/WireGuardAutoTunnel.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/WireGuardAutoTunnel.kt
index f9e140a..3d40cdd 100644
--- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/WireGuardAutoTunnel.kt
+++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/WireGuardAutoTunnel.kt
@@ -3,6 +3,8 @@ package com.zaneschepke.wireguardautotunnel
import android.app.Application
import android.content.Context
import android.content.pm.PackageManager
+import androidx.lifecycle.ProcessLifecycleOwner
+import androidx.lifecycle.lifecycleScope
import com.zaneschepke.wireguardautotunnel.repository.SettingsDoa
import com.zaneschepke.wireguardautotunnel.repository.model.Settings
import dagger.hilt.android.HiltAndroidApp
@@ -27,13 +29,17 @@ class WireGuardAutoTunnel : Application() {
}
private fun initSettings() {
- CoroutineScope(Dispatchers.IO).launch {
- if(settingsRepo.getAll().isEmpty()) {
- settingsRepo.save(Settings())
+ with(ProcessLifecycleOwner.get()) {
+ lifecycleScope.launch {
+ if(settingsRepo.getAll().isEmpty()) {
+ settingsRepo.save(Settings())
+ }
}
}
}
+
+
companion object {
fun isRunningOnAndroidTv(context : Context) : Boolean {
return context.packageManager.hasSystemFeature(PackageManager.FEATURE_LEANBACK)
diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/receiver/BootReceiver.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/receiver/BootReceiver.kt
index 8d85636..9748e7b 100644
--- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/receiver/BootReceiver.kt
+++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/receiver/BootReceiver.kt
@@ -3,13 +3,11 @@ package com.zaneschepke.wireguardautotunnel.receiver
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
+import com.zaneschepke.wireguardautotunnel.goAsync
import com.zaneschepke.wireguardautotunnel.repository.SettingsDoa
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
import dagger.hilt.android.AndroidEntryPoint
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.cancel
-import kotlinx.coroutines.launch
import javax.inject.Inject
@AndroidEntryPoint
@@ -18,20 +16,18 @@ class BootReceiver : BroadcastReceiver() {
@Inject
lateinit var settingsRepo : SettingsDoa
- override fun onReceive(context: Context, intent: Intent) {
+ override fun onReceive(context: Context, intent: Intent) = goAsync {
if (intent.action == Intent.ACTION_BOOT_COMPLETED) {
- CoroutineScope(Dispatchers.IO).launch {
- try {
- val settings = settingsRepo.getAll()
- if (settings.isNotEmpty()) {
- val setting = settings.first()
- if (setting.isAutoTunnelEnabled && setting.defaultTunnel != null) {
- ServiceManager.startWatcherService(context, setting.defaultTunnel!!)
- }
+ try {
+ val settings = settingsRepo.getAll()
+ if (settings.isNotEmpty()) {
+ val setting = settings.first()
+ if (setting.isAutoTunnelEnabled && setting.defaultTunnel != null) {
+ ServiceManager.startWatcherService(context, setting.defaultTunnel!!)
}
- } finally {
- cancel()
}
+ } finally {
+ cancel()
}
}
}
diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/receiver/NotificationActionReceiver.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/receiver/NotificationActionReceiver.kt
index ebe4120..f9bb3e3 100644
--- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/receiver/NotificationActionReceiver.kt
+++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/receiver/NotificationActionReceiver.kt
@@ -4,14 +4,12 @@ import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import com.zaneschepke.wireguardautotunnel.Constants
+import com.zaneschepke.wireguardautotunnel.goAsync
import com.zaneschepke.wireguardautotunnel.repository.SettingsDoa
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
import dagger.hilt.android.AndroidEntryPoint
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.cancel
import kotlinx.coroutines.delay
-import kotlinx.coroutines.launch
import javax.inject.Inject
@AndroidEntryPoint
@@ -19,21 +17,19 @@ class NotificationActionReceiver : BroadcastReceiver() {
@Inject
lateinit var settingsRepo : SettingsDoa
- override fun onReceive(context: Context, intent: Intent?) {
- CoroutineScope(Dispatchers.IO).launch {
- try {
- val settings = settingsRepo.getAll()
- if (settings.isNotEmpty()) {
- val setting = settings.first()
- if (setting.defaultTunnel != null) {
- ServiceManager.stopVpnService(context)
- delay(Constants.TOGGLE_TUNNEL_DELAY)
- ServiceManager.startVpnService(context, setting.defaultTunnel.toString())
- }
+ override fun onReceive(context: Context, intent: Intent?) = goAsync {
+ try {
+ val settings = settingsRepo.getAll()
+ if (settings.isNotEmpty()) {
+ val setting = settings.first()
+ if (setting.defaultTunnel != null) {
+ ServiceManager.stopVpnService(context)
+ delay(Constants.TOGGLE_TUNNEL_DELAY)
+ ServiceManager.startVpnService(context, setting.defaultTunnel.toString())
}
- } finally {
- cancel()
}
+ } finally {
+ cancel()
}
}
}
\ No newline at end of file
diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/WireGuardTunnelService.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/WireGuardTunnelService.kt
index 918bc5e..44ca2fc 100644
--- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/WireGuardTunnelService.kt
+++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/WireGuardTunnelService.kt
@@ -3,6 +3,7 @@ package com.zaneschepke.wireguardautotunnel.service.foreground
import android.app.PendingIntent
import android.content.Intent
import android.os.Bundle
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.lifecycleScope
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.receiver.NotificationActionReceiver
@@ -50,24 +51,26 @@ class WireGuardTunnelService : ForegroundService() {
val tunnelConfigString = extras?.getString(getString(R.string.tunnel_extras_key))
cancelJob()
job = lifecycleScope.launch(Dispatchers.IO) {
- if(tunnelConfigString != null) {
- try {
- val tunnelConfig = TunnelConfig.from(tunnelConfigString)
- tunnelName = tunnelConfig.name
- vpnService.startTunnel(tunnelConfig)
- } catch (e : Exception) {
- Timber.e("Problem starting tunnel: ${e.message}")
- stopService(extras)
- }
- } else {
- Timber.d("Tunnel config null, starting default tunnel")
- val settings = settingsRepo.getAll();
- if(settings.isNotEmpty()) {
- val setting = settings[0]
- if(setting.defaultTunnel != null && setting.isAlwaysOnVpnEnabled) {
- val tunnelConfig = TunnelConfig.from(setting.defaultTunnel!!)
+ launch {
+ if(tunnelConfigString != null) {
+ try {
+ val tunnelConfig = TunnelConfig.from(tunnelConfigString)
tunnelName = tunnelConfig.name
vpnService.startTunnel(tunnelConfig)
+ } catch (e : Exception) {
+ Timber.e("Problem starting tunnel: ${e.message}")
+ stopService(extras)
+ }
+ } else {
+ Timber.d("Tunnel config null, starting default tunnel")
+ val settings = settingsRepo.getAll();
+ if(settings.isNotEmpty()) {
+ val setting = settings[0]
+ if(setting.defaultTunnel != null && setting.isAlwaysOnVpnEnabled) {
+ val tunnelConfig = TunnelConfig.from(setting.defaultTunnel!!)
+ tunnelName = tunnelConfig.name
+ vpnService.startTunnel(tunnelConfig)
+ }
}
}
}
@@ -141,7 +144,8 @@ class WireGuardTunnelService : ForegroundService() {
val notification = notificationService.createNotification(
channelId = getString(R.string.vpn_channel_id),
channelName = getString(R.string.vpn_channel_name),
- action = PendingIntent.getBroadcast(this,0,Intent(this, NotificationActionReceiver::class.java),PendingIntent.FLAG_IMMUTABLE),
+ action = PendingIntent.getBroadcast(this,0,
+ Intent(this, NotificationActionReceiver::class.java),PendingIntent.FLAG_IMMUTABLE),
actionText = getString(R.string.restart),
title = getString(R.string.vpn_connection_failed),
onGoing = false,
diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/shortcut/ShortcutsActivity.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/shortcut/ShortcutsActivity.kt
index adc687e..872084d 100644
--- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/shortcut/ShortcutsActivity.kt
+++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/shortcut/ShortcutsActivity.kt
@@ -2,6 +2,7 @@ package com.zaneschepke.wireguardautotunnel.service.shortcut
import android.os.Bundle
import androidx.activity.ComponentActivity
+import androidx.lifecycle.lifecycleScope
import com.zaneschepke.wireguardautotunnel.repository.SettingsDoa
import com.zaneschepke.wireguardautotunnel.repository.TunnelConfigDao
import com.zaneschepke.wireguardautotunnel.repository.model.Settings
@@ -27,10 +28,9 @@ class ShortcutsActivity : ComponentActivity() {
@Inject
lateinit var tunnelConfigRepo : TunnelConfigDao
- private val scope = CoroutineScope(Dispatchers.Main);
private fun attemptWatcherServiceToggle(tunnelConfig : String) {
- scope.launch {
+ lifecycleScope.launch(Dispatchers.Main) {
val settings = getSettings()
if(settings.isAutoTunnelEnabled) {
ServiceManager.toggleWatcherServiceForeground(this@ShortcutsActivity, tunnelConfig)
@@ -42,7 +42,7 @@ class ShortcutsActivity : ComponentActivity() {
super.onCreate(savedInstanceState)
if(intent.getStringExtra(CLASS_NAME_EXTRA_KEY)
.equals(WireGuardTunnelService::class.java.simpleName)) {
- scope.launch {
+ lifecycleScope.launch(Dispatchers.Main) {
try {
val settings = getSettings()
val tunnelConfig = if(settings.defaultTunnel == null) {
@@ -63,11 +63,6 @@ class ShortcutsActivity : ComponentActivity() {
finish()
}
- override fun onDestroy() {
- super.onDestroy()
- scope.cancel()
- }
-
private suspend fun getSettings() : Settings {
val settings = settingsRepo.getAll()
return if (settings.isNotEmpty()) {
diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tunnel/WireGuardTunnel.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tunnel/WireGuardTunnel.kt
index e7e7085..32bac9c 100644
--- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tunnel/WireGuardTunnel.kt
+++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tunnel/WireGuardTunnel.kt
@@ -78,7 +78,6 @@ class WireGuardTunnel @Inject constructor(private val backend : Backend,
if(getState() == Tunnel.State.UP) {
val state = backend.setState(this, Tunnel.State.DOWN, null)
_state.emit(state)
- scope.cancel()
}
} catch (e : BackendException) {
Timber.e("Failed to stop tunnel with error: ${e.message}")
diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/MainActivity.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/MainActivity.kt
index ff9d8f8..2b9e0c5 100644
--- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/MainActivity.kt
+++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/MainActivity.kt
@@ -11,6 +11,7 @@ import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.compose.setContent
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity
+import androidx.compose.animation.ExitTransition
import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn
@@ -32,6 +33,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.input.key.onKeyEvent
import androidx.compose.ui.unit.dp
+import androidx.lifecycle.lifecycleScope
import com.google.accompanist.navigation.animation.AnimatedNavHost
import com.google.accompanist.navigation.animation.composable
import com.google.accompanist.navigation.animation.rememberAnimatedNavController
@@ -41,9 +43,9 @@ import com.google.accompanist.permissions.rememberPermissionState
import com.wireguard.android.backend.GoBackend
import com.zaneschepke.wireguardautotunnel.Constants
import com.zaneschepke.wireguardautotunnel.R
-import com.zaneschepke.wireguardautotunnel.ui.common.CustomSnackBar
import com.zaneschepke.wireguardautotunnel.ui.common.PermissionRequestFailedScreen
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.BottomNavBar
+import com.zaneschepke.wireguardautotunnel.ui.common.prompt.CustomSnackBar
import com.zaneschepke.wireguardautotunnel.ui.screens.config.ConfigScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.detail.DetailScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.main.MainScreen
@@ -52,7 +54,6 @@ import com.zaneschepke.wireguardautotunnel.ui.screens.support.SupportScreen
import com.zaneschepke.wireguardautotunnel.ui.theme.TransparentSystemBars
import com.zaneschepke.wireguardautotunnel.ui.theme.WireguardAutoTunnelTheme
import dagger.hilt.android.AndroidEntryPoint
-import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import timber.log.Timber
@@ -99,10 +100,10 @@ class MainActivity : AppCompatActivity() {
}
fun showSnackBarMessage(message : String) {
- CoroutineScope(Dispatchers.Main).launch {
+ lifecycleScope.launch(Dispatchers.Main) {
val result = snackbarHostState.showSnackbar(
message = message,
- actionLabel = "Okay",
+ actionLabel = applicationContext.getString(R.string.okay),
duration = SnackbarDuration.Short,
)
when (result) {
@@ -184,7 +185,10 @@ class MainActivity : AppCompatActivity() {
fadeIn(animationSpec = tween(Constants.FADE_IN_ANIMATION_DURATION))
}
}
- }) {
+ }, exitTransition = {
+ ExitTransition.None
+ }
+ ) {
MainScreen(padding = padding, showSnackbarMessage = { message -> showSnackBarMessage(message) }, navController = navController)
}
composable(Routes.Settings.name, enterTransition = {
diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/prompt/AuthorizationPrompt.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/prompt/AuthorizationPrompt.kt
new file mode 100644
index 0000000..b90d0aa
--- /dev/null
+++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/prompt/AuthorizationPrompt.kt
@@ -0,0 +1,79 @@
+package com.zaneschepke.wireguardautotunnel.ui.common.prompt
+
+import androidx.biometric.BiometricManager
+import androidx.biometric.BiometricManager.Authenticators.BIOMETRIC_WEAK
+import androidx.biometric.BiometricManager.Authenticators.DEVICE_CREDENTIAL
+import androidx.biometric.BiometricPrompt
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
+import androidx.compose.ui.platform.LocalContext
+import androidx.core.content.ContextCompat
+import androidx.fragment.app.FragmentActivity
+
+@Composable
+fun AuthorizationPrompt(onSuccess : () -> Unit, onFailure : () -> Unit, onError : (String) -> Unit) {
+ val context = LocalContext.current
+ val biometricManager = BiometricManager.from(context)
+ val bio = biometricManager.canAuthenticate(BIOMETRIC_WEAK or DEVICE_CREDENTIAL)
+ val isBiometricAvailable = remember {
+ when(bio){
+ BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE -> {
+ onError("Biometrics not available")
+ false
+ }
+ BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED -> {
+ onError("Biometrics not created")
+ false
+ }
+ BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE -> {
+ onError("Biometric hardware not found")
+ false
+ }
+ BiometricManager.BIOMETRIC_ERROR_SECURITY_UPDATE_REQUIRED -> {
+ onError("Biometric security update required")
+ false
+ }
+ BiometricManager.BIOMETRIC_ERROR_UNSUPPORTED -> {
+ onError("Biometrics not supported")
+ false
+ }
+ BiometricManager.BIOMETRIC_STATUS_UNKNOWN -> {
+ onError("Biometrics status unknown")
+ false
+ }
+ BiometricManager.BIOMETRIC_SUCCESS -> true
+ else -> false
+ }
+ }
+ if(isBiometricAvailable) {
+ val executor = remember { ContextCompat.getMainExecutor(context) }
+
+ val promptInfo = BiometricPrompt.PromptInfo.Builder()
+ .setAllowedAuthenticators(BIOMETRIC_WEAK or DEVICE_CREDENTIAL)
+ .setTitle("Biometric Authentication")
+ .setSubtitle("Log in using your biometric credential")
+ .build()
+
+ val biometricPrompt = BiometricPrompt(
+ context as FragmentActivity,
+ executor,
+ object : BiometricPrompt.AuthenticationCallback() {
+ override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
+ super.onAuthenticationError(errorCode, errString)
+ onFailure()
+ }
+
+ override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
+ super.onAuthenticationSucceeded(result)
+ onSuccess()
+ }
+
+ override fun onAuthenticationFailed() {
+ super.onAuthenticationFailed()
+ onFailure()
+ }
+ }
+ )
+ biometricPrompt.authenticate(promptInfo)
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/CustomSnackbar.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/prompt/CustomSnackbar.kt
similarity index 97%
rename from app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/CustomSnackbar.kt
rename to app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/prompt/CustomSnackbar.kt
index 24cdd83..ea95962 100644
--- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/CustomSnackbar.kt
+++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/prompt/CustomSnackbar.kt
@@ -1,4 +1,4 @@
-package com.zaneschepke.wireguardautotunnel.ui.common
+package com.zaneschepke.wireguardautotunnel.ui.common.prompt
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/config/ConfigScreen.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/config/ConfigScreen.kt
index ff25946..713bab6 100644
--- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/config/ConfigScreen.kt
+++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/config/ConfigScreen.kt
@@ -3,6 +3,7 @@ package com.zaneschepke.wireguardautotunnel.ui.screens.config
import android.annotation.SuppressLint
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.Image
+import androidx.compose.foundation.clickable
import androidx.compose.foundation.focusGroup
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
@@ -66,6 +67,7 @@ import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardCapitalization
import androidx.compose.ui.text.input.PasswordVisualTransformation
+import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
@@ -77,6 +79,7 @@ import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
import com.zaneschepke.wireguardautotunnel.ui.Routes
import com.zaneschepke.wireguardautotunnel.ui.common.SearchBar
import com.zaneschepke.wireguardautotunnel.ui.common.config.ConfigurationTextBox
+import com.zaneschepke.wireguardautotunnel.ui.common.prompt.AuthorizationPrompt
import com.zaneschepke.wireguardautotunnel.ui.common.text.SectionTitle
import kotlinx.coroutines.launch
import timber.log.Timber
@@ -109,6 +112,8 @@ fun ConfigScreen(
val proxyPeers by viewModel.proxyPeers.collectAsStateWithLifecycle()
val proxyInterface by viewModel.interfaceProxy.collectAsStateWithLifecycle()
var showApplicationsDialog by remember { mutableStateOf(false) }
+ var showAuthPrompt by remember { mutableStateOf(false) }
+ var isAuthenticated by remember { mutableStateOf(false) }
val baseTextBoxModifier = Modifier.onFocusChanged {
if(WireGuardAutoTunnel.isRunningOnAndroidTv(context)) {
keyboardController?.hide()
@@ -117,18 +122,7 @@ fun ConfigScreen(
val keyboardActions = KeyboardActions(
onDone = {
- //focusManager.clearFocus()
keyboardController?.hide()
- },
- onNext = {
- keyboardController?.hide()
- },
- onPrevious = {
- keyboardController?.hide()
- },
- onGo = {
- keyboardController?.hide(
- )
}
)
@@ -156,6 +150,21 @@ fun ConfigScreen(
else "${checkedPackages.size} " + (if (include) "included" else "excluded")
}
+
+ if(showAuthPrompt) {
+ AuthorizationPrompt(onSuccess = {
+ showAuthPrompt = false
+ isAuthenticated = true },
+ onError = { error ->
+ showSnackbarMessage(error)
+ showAuthPrompt = false
+ },
+ onFailure = {
+ showAuthPrompt = false
+ showSnackbarMessage("Authentication failed")
+ })
+ }
+
if (showApplicationsDialog) {
val sortedPackages = remember(packages) {
packages.sortedBy { viewModel.getPackageLabel(it) }
@@ -399,10 +408,12 @@ fun ConfigScreen(
modifier = baseTextBoxModifier.fillMaxWidth().focusRequester(focusRequester)
)
OutlinedTextField(
- modifier = baseTextBoxModifier.fillMaxWidth(),
+ modifier = baseTextBoxModifier.fillMaxWidth().clickable {
+ showAuthPrompt = true
+ },
value = proxyInterface.privateKey,
- visualTransformation = PasswordVisualTransformation(),
- enabled = id == Constants.MANUAL_TUNNEL_CONFIG_ID,
+ visualTransformation = if((id == Constants.MANUAL_TUNNEL_CONFIG_ID) || isAuthenticated) VisualTransformation.None else PasswordVisualTransformation(),
+ enabled = (id == Constants.MANUAL_TUNNEL_CONFIG_ID) || isAuthenticated,
onValueChange = { value ->
viewModel.onPrivateKeyChange(value)
},
diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/MainScreen.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/MainScreen.kt
index a41d9a5..593f842 100644
--- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/MainScreen.kt
+++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/MainScreen.kt
@@ -285,7 +285,7 @@ fun MainScreen(
modifier = Modifier.padding(10.dp)
)
Text(
- stringResource(id = R.string.add_from_file),
+ stringResource(id = R.string.add_tunnels_text),
modifier = Modifier.padding(10.dp)
)
}
diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/MainViewModel.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/MainViewModel.kt
index d718d07..577154c 100644
--- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/MainViewModel.kt
+++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/MainViewModel.kt
@@ -7,8 +7,6 @@ import android.net.Uri
import android.provider.OpenableColumns
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
-import androidx.lifecycle.viewmodel.compose.viewModel
-import com.wireguard.config.BadConfigException
import com.wireguard.config.Config
import com.zaneschepke.wireguardautotunnel.Constants
import com.zaneschepke.wireguardautotunnel.R
@@ -31,6 +29,7 @@ import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.launch
import java.io.InputStream
+import java.util.zip.ZipInputStream
import javax.inject.Inject
@@ -138,13 +137,6 @@ class MainViewModel @Inject constructor(
}
}
- 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)
@@ -160,17 +152,41 @@ class MainViewModel @Inject constructor(
?: throw WgTunnelException(application.getString(R.string.stream_failed))
}
- fun onTunnelFileSelected(uri: Uri) {
+ suspend fun onTunnelFileSelected(uri: Uri) {
try {
val fileName = getFileName(application.applicationContext, uri)
- validateFileExtension(fileName)
- val stream = getInputStreamFromUri(uri)
- saveTunnelConfigFromStream(stream, fileName)
+ val fileExtension = getFileExtensionFromFileName(fileName)
+ when(fileExtension){
+ Constants.CONF_FILE_EXTENSION -> saveTunnelFromConfUri(fileName, uri)
+ Constants.ZIP_FILE_EXTENSION -> saveTunnelsFromZipUri(uri)
+ else -> throw WgTunnelException(application.getString(R.string.file_extension_message))
+ }
+
} catch (e: Exception) {
throw WgTunnelException(e.message ?: "Error importing file")
}
}
+ private suspend fun saveTunnelsFromZipUri(uri: Uri) {
+ ZipInputStream(getInputStreamFromUri(uri)).use { zip ->
+ generateSequence { zip.nextEntry }
+ .filterNot { it.isDirectory ||
+ getFileExtensionFromFileName(it.name) != Constants.CONF_FILE_EXTENSION }
+ .forEach {
+ val name = getNameFromFileName(it.name)
+ val config = Config.parse(zip)
+ viewModelScope.launch(Dispatchers.IO) {
+ addTunnel(TunnelConfig(name = name, wgQuick = config.toWgQuickString()))
+ }
+ }
+ }
+ }
+
+ private fun saveTunnelFromConfUri(name : String, uri: Uri) {
+ val stream = getInputStreamFromUri(uri)
+ saveTunnelConfigFromStream(stream, name)
+ }
+
private suspend fun addTunnel(tunnelConfig: TunnelConfig) {
saveTunnel(tunnelConfig)
}
diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/SettingsScreen.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/SettingsScreen.kt
index c106cba..bb0f2ec 100644
--- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/SettingsScreen.kt
+++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/SettingsScreen.kt
@@ -69,8 +69,12 @@ import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
import com.zaneschepke.wireguardautotunnel.ui.common.ClickableIconButton
import com.zaneschepke.wireguardautotunnel.ui.common.config.ConfigurationToggle
+import com.zaneschepke.wireguardautotunnel.ui.common.prompt.AuthorizationPrompt
import com.zaneschepke.wireguardautotunnel.ui.common.text.SectionTitle
+import com.zaneschepke.wireguardautotunnel.util.StorageUtil
import kotlinx.coroutines.launch
+import java.io.File
+import kotlin.math.exp
@OptIn(
ExperimentalPermissionsApi::class,
@@ -98,11 +102,28 @@ fun SettingsScreen(
val scrollState = rememberScrollState()
var didShowLocationDisclaimer by remember { mutableStateOf(false) }
var isBackgroundLocationGranted by remember { mutableStateOf(true) }
+ var showAuthPrompt by remember { mutableStateOf(false) }
+ var didExportFiles by remember { mutableStateOf(false) }
val screenPadding = 5.dp
- val fillMaxHeight = .85f
val fillMaxWidth = .85f
+ fun exportAllConfigs() {
+ try {
+ val files = tunnels.map { File(context.cacheDir, "${it.name}.conf") }
+ files.forEachIndexed() { index, file ->
+ file.outputStream().use {
+ it.write(tunnels[index].wgQuick.toByteArray())
+ }
+ }
+ StorageUtil.saveFilesToZip(context, files)
+ didExportFiles = true
+ showSnackbarMessage("Exported configs to downloads")
+ } catch (e : Exception) {
+ showSnackbarMessage(e.message!!)
+ }
+ }
+
fun saveTrustedSSID() {
if (currentText.isNotEmpty()) {
@@ -192,6 +213,20 @@ fun SettingsScreen(
}
}
+ if(showAuthPrompt) {
+ AuthorizationPrompt(onSuccess = {
+ showAuthPrompt = false
+ exportAllConfigs() },
+ onError = { error ->
+ showSnackbarMessage(error)
+ showAuthPrompt = false
+ },
+ onFailure = {
+ showAuthPrompt = false
+ showSnackbarMessage("Authentication failed")
+ })
+ }
+
if (tunnels.isEmpty()) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
@@ -363,6 +398,21 @@ fun SettingsScreen(
}
}
)
+ Row(
+ verticalAlignment = Alignment.CenterVertically,
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(top = 5.dp),
+ horizontalArrangement = Arrangement.Center
+ ) {
+ TextButton(
+ enabled = !didExportFiles,
+ onClick = {
+ showAuthPrompt = true
+ }) {
+ Text("Export configs")
+ }
+ }
}
}
}
diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/StorageUtil.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/StorageUtil.kt
new file mode 100644
index 0000000..f01e3cc
--- /dev/null
+++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/StorageUtil.kt
@@ -0,0 +1,53 @@
+package com.zaneschepke.wireguardautotunnel.util
+
+import android.content.ContentValues
+import android.content.Context
+import android.os.Build
+import android.os.Environment
+import android.provider.MediaStore
+import android.provider.MediaStore.MediaColumns
+import com.zaneschepke.wireguardautotunnel.Constants
+import java.io.File
+import java.io.OutputStream
+import java.time.Instant
+import java.util.zip.ZipEntry
+import java.util.zip.ZipOutputStream
+
+object StorageUtil {
+ private const val ZIP_FILE_MIME_TYPE = "application/zip"
+ private fun createDownloadsFileOutputStream(context: Context, fileName: String, mimeType : String = Constants.ALLOWED_FILE_TYPES) : OutputStream? {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
+ val resolver = context.contentResolver
+ val contentValues = ContentValues().apply {
+ put(MediaColumns.DISPLAY_NAME, fileName)
+ put(MediaColumns.MIME_TYPE, mimeType)
+ put(MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_DOWNLOADS)
+ }
+ val uri = resolver.insert(MediaStore.Downloads.EXTERNAL_CONTENT_URI, contentValues)
+ if (uri != null) {
+
+ return resolver.openOutputStream(uri)
+ }
+ } else {
+ val target = File(
+ Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS),
+ fileName
+ )
+ return target.outputStream()
+ }
+ return null
+ }
+
+ fun saveFilesToZip(context: Context, files : List) {
+ val zipOutputStream = createDownloadsFileOutputStream(context, "wg-export_${Instant.now().epochSecond}.zip", ZIP_FILE_MIME_TYPE)
+ ZipOutputStream(zipOutputStream).use { zos ->
+ files.forEach { file ->
+ val entry = ZipEntry( file.name)
+ zos.putNextEntry(entry)
+ if (file.isFile) {
+ file.inputStream().use { fis -> fis.copyTo(zos) }
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 9029933..34c4694 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -8,7 +8,7 @@
FOREGROUND_FILE
https://github.com/zaneschepke/wgtunnel
https://zaneschepke.github.io/wgtunnel/
- File is not a .conf file
+ File is not a .conf or .zip
Turn off tunnel before editing
No tunnels added yet!
Tunnel name already exists
@@ -38,9 +38,9 @@
Enter SSID
Submit SSID
[Interface]
- Add tunnel from files
+ Add from file or zip
File Open
- Add tunnel from QR code
+ Add from QR code
QR Scan
Tunnel Edit
Tunnel Name
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 3a04087..0f9a6ff 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -3,6 +3,7 @@ accompanist = "0.31.2-alpha"
activityCompose = "1.8.0"
androidx-junit = "1.1.5"
appcompat = "1.6.1"
+biometricKtx = "1.2.0-alpha05"
coreKtx = "1.12.0"
espressoCore = "3.5.1"
firebase-crashlytics-gradle = "2.9.9"
@@ -40,6 +41,8 @@ accompanist-permissions = { module = "com.google.accompanist:accompanist-permiss
accompanist-systemuicontroller = { module = "com.google.accompanist:accompanist-systemuicontroller", version.ref = "accompanist" }
#room
+androidx-biometric-ktx = { module = "androidx.biometric:biometric-ktx", version.ref = "biometricKtx" }
+androidx-lifecycle-process = { module = "androidx.lifecycle:lifecycle-process", version.ref = "lifecycle-runtime-compose" }
androidx-lifecycle-service = { module = "androidx.lifecycle:lifecycle-service", version.ref = "lifecycle-runtime-compose" }
androidx-room-compiler = { module = "androidx.room:room-compiler", version.ref = "roomVersion" }
androidx-room-ktx = { module = "androidx.room:room-ktx", version.ref = "roomVersion" }