feat: zip file import export
Added support for importing zip files containing multiple config files. Closes #33 Added support for exporting all config files to downloads folder as a zip with biometric or security pin approval. Added support for editing or viewing private key with biometric or security pin approval. Fixed a bug where VPN status indicator functionality was unintentionally disabled. Other various enhancements and refactors.
This commit is contained in:
parent
77cd328a71
commit
37bae82700
|
@ -14,8 +14,8 @@ android {
|
||||||
applicationId = "com.zaneschepke.wireguardautotunnel"
|
applicationId = "com.zaneschepke.wireguardautotunnel"
|
||||||
minSdk = 26
|
minSdk = 26
|
||||||
targetSdk = 34
|
targetSdk = 34
|
||||||
versionCode = 31300
|
versionCode = 31400
|
||||||
versionName = "3.1.3"
|
versionName = "3.1.4"
|
||||||
|
|
||||||
multiDexEnabled = true
|
multiDexEnabled = true
|
||||||
|
|
||||||
|
@ -128,6 +128,9 @@ dependencies {
|
||||||
|
|
||||||
//lifecycle
|
//lifecycle
|
||||||
implementation(libs.lifecycle.runtime.compose)
|
implementation(libs.lifecycle.runtime.compose)
|
||||||
|
implementation(libs.androidx.lifecycle.runtime.ktx)
|
||||||
|
implementation(libs.androidx.lifecycle.process)
|
||||||
|
|
||||||
|
|
||||||
//icons
|
//icons
|
||||||
implementation(libs.material.icons.extended)
|
implementation(libs.material.icons.extended)
|
||||||
|
@ -142,4 +145,8 @@ dependencies {
|
||||||
//barcode scanning
|
//barcode scanning
|
||||||
implementation(libs.zxing.android.embedded)
|
implementation(libs.zxing.android.embedded)
|
||||||
implementation(libs.zxing.core)
|
implementation(libs.zxing.core)
|
||||||
|
|
||||||
|
//bio
|
||||||
|
implementation(libs.androidx.biometric.ktx)
|
||||||
|
|
||||||
}
|
}
|
|
@ -4,6 +4,10 @@
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"
|
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"
|
||||||
android:maxSdkVersion="32" />
|
android:maxSdkVersion="32" />
|
||||||
|
<uses-permission
|
||||||
|
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
|
||||||
|
android:maxSdkVersion="32"
|
||||||
|
tools:ignore="ScopedStorage" />
|
||||||
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE"
|
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE"
|
||||||
android:maxSdkVersion="30"
|
android:maxSdkVersion="30"
|
||||||
tools:ignore="LeanbackUsesWifi" />
|
tools:ignore="LeanbackUsesWifi" />
|
||||||
|
|
|
@ -8,7 +8,8 @@ object Constants {
|
||||||
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 CONF_FILE_EXTENSION = ".conf"
|
||||||
|
const val ZIP_FILE_EXTENSION = ".zip"
|
||||||
const val URI_CONTENT_SCHEME = "content"
|
const val URI_CONTENT_SCHEME = "content"
|
||||||
const val URI_PACKAGE_SCHEME = "package"
|
const val URI_PACKAGE_SCHEME = "package"
|
||||||
const val ALLOWED_FILE_TYPES = "*/*"
|
const val ALLOWED_FILE_TYPES = "*/*"
|
||||||
|
|
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -3,6 +3,8 @@ package com.zaneschepke.wireguardautotunnel
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
|
import androidx.lifecycle.ProcessLifecycleOwner
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
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 dagger.hilt.android.HiltAndroidApp
|
import dagger.hilt.android.HiltAndroidApp
|
||||||
|
@ -27,13 +29,17 @@ class WireGuardAutoTunnel : Application() {
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun initSettings() {
|
private fun initSettings() {
|
||||||
CoroutineScope(Dispatchers.IO).launch {
|
with(ProcessLifecycleOwner.get()) {
|
||||||
if(settingsRepo.getAll().isEmpty()) {
|
lifecycleScope.launch {
|
||||||
settingsRepo.save(Settings())
|
if(settingsRepo.getAll().isEmpty()) {
|
||||||
|
settingsRepo.save(Settings())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun isRunningOnAndroidTv(context : Context) : Boolean {
|
fun isRunningOnAndroidTv(context : Context) : Boolean {
|
||||||
return context.packageManager.hasSystemFeature(PackageManager.FEATURE_LEANBACK)
|
return context.packageManager.hasSystemFeature(PackageManager.FEATURE_LEANBACK)
|
||||||
|
|
|
@ -3,13 +3,11 @@ package com.zaneschepke.wireguardautotunnel.receiver
|
||||||
import android.content.BroadcastReceiver
|
import android.content.BroadcastReceiver
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import com.zaneschepke.wireguardautotunnel.goAsync
|
||||||
import com.zaneschepke.wireguardautotunnel.repository.SettingsDoa
|
import com.zaneschepke.wireguardautotunnel.repository.SettingsDoa
|
||||||
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
|
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.cancel
|
import kotlinx.coroutines.cancel
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
|
@ -18,20 +16,18 @@ class BootReceiver : BroadcastReceiver() {
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var settingsRepo : SettingsDoa
|
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) {
|
if (intent.action == Intent.ACTION_BOOT_COMPLETED) {
|
||||||
CoroutineScope(Dispatchers.IO).launch {
|
try {
|
||||||
try {
|
val settings = settingsRepo.getAll()
|
||||||
val settings = settingsRepo.getAll()
|
if (settings.isNotEmpty()) {
|
||||||
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!!)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} finally {
|
|
||||||
cancel()
|
|
||||||
}
|
}
|
||||||
|
} finally {
|
||||||
|
cancel()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,14 +4,12 @@ import android.content.BroadcastReceiver
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import com.zaneschepke.wireguardautotunnel.Constants
|
import com.zaneschepke.wireguardautotunnel.Constants
|
||||||
|
import com.zaneschepke.wireguardautotunnel.goAsync
|
||||||
import com.zaneschepke.wireguardautotunnel.repository.SettingsDoa
|
import com.zaneschepke.wireguardautotunnel.repository.SettingsDoa
|
||||||
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
|
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.cancel
|
import kotlinx.coroutines.cancel
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
|
@ -19,21 +17,19 @@ class NotificationActionReceiver : BroadcastReceiver() {
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var settingsRepo : SettingsDoa
|
lateinit var settingsRepo : SettingsDoa
|
||||||
override fun onReceive(context: Context, intent: Intent?) {
|
override fun onReceive(context: Context, intent: Intent?) = goAsync {
|
||||||
CoroutineScope(Dispatchers.IO).launch {
|
try {
|
||||||
try {
|
val settings = settingsRepo.getAll()
|
||||||
val settings = settingsRepo.getAll()
|
if (settings.isNotEmpty()) {
|
||||||
if (settings.isNotEmpty()) {
|
val setting = settings.first()
|
||||||
val setting = settings.first()
|
if (setting.defaultTunnel != null) {
|
||||||
if (setting.defaultTunnel != null) {
|
ServiceManager.stopVpnService(context)
|
||||||
ServiceManager.stopVpnService(context)
|
delay(Constants.TOGGLE_TUNNEL_DELAY)
|
||||||
delay(Constants.TOGGLE_TUNNEL_DELAY)
|
ServiceManager.startVpnService(context, setting.defaultTunnel.toString())
|
||||||
ServiceManager.startVpnService(context, setting.defaultTunnel.toString())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} finally {
|
|
||||||
cancel()
|
|
||||||
}
|
}
|
||||||
|
} finally {
|
||||||
|
cancel()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -3,6 +3,7 @@ package com.zaneschepke.wireguardautotunnel.service.foreground
|
||||||
import android.app.PendingIntent
|
import android.app.PendingIntent
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import com.zaneschepke.wireguardautotunnel.R
|
import com.zaneschepke.wireguardautotunnel.R
|
||||||
import com.zaneschepke.wireguardautotunnel.receiver.NotificationActionReceiver
|
import com.zaneschepke.wireguardautotunnel.receiver.NotificationActionReceiver
|
||||||
|
@ -50,24 +51,26 @@ class WireGuardTunnelService : ForegroundService() {
|
||||||
val tunnelConfigString = extras?.getString(getString(R.string.tunnel_extras_key))
|
val tunnelConfigString = extras?.getString(getString(R.string.tunnel_extras_key))
|
||||||
cancelJob()
|
cancelJob()
|
||||||
job = lifecycleScope.launch(Dispatchers.IO) {
|
job = lifecycleScope.launch(Dispatchers.IO) {
|
||||||
if(tunnelConfigString != null) {
|
launch {
|
||||||
try {
|
if(tunnelConfigString != null) {
|
||||||
val tunnelConfig = TunnelConfig.from(tunnelConfigString)
|
try {
|
||||||
tunnelName = tunnelConfig.name
|
val tunnelConfig = TunnelConfig.from(tunnelConfigString)
|
||||||
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
|
tunnelName = tunnelConfig.name
|
||||||
vpnService.startTunnel(tunnelConfig)
|
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(
|
val notification = notificationService.createNotification(
|
||||||
channelId = getString(R.string.vpn_channel_id),
|
channelId = getString(R.string.vpn_channel_id),
|
||||||
channelName = getString(R.string.vpn_channel_name),
|
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),
|
actionText = getString(R.string.restart),
|
||||||
title = getString(R.string.vpn_connection_failed),
|
title = getString(R.string.vpn_connection_failed),
|
||||||
onGoing = false,
|
onGoing = false,
|
||||||
|
|
|
@ -2,6 +2,7 @@ package com.zaneschepke.wireguardautotunnel.service.shortcut
|
||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import androidx.activity.ComponentActivity
|
import androidx.activity.ComponentActivity
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
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.repository.model.Settings
|
import com.zaneschepke.wireguardautotunnel.repository.model.Settings
|
||||||
|
@ -27,10 +28,9 @@ class ShortcutsActivity : ComponentActivity() {
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var tunnelConfigRepo : TunnelConfigDao
|
lateinit var tunnelConfigRepo : TunnelConfigDao
|
||||||
|
|
||||||
private val scope = CoroutineScope(Dispatchers.Main);
|
|
||||||
|
|
||||||
private fun attemptWatcherServiceToggle(tunnelConfig : String) {
|
private fun attemptWatcherServiceToggle(tunnelConfig : String) {
|
||||||
scope.launch {
|
lifecycleScope.launch(Dispatchers.Main) {
|
||||||
val settings = getSettings()
|
val settings = getSettings()
|
||||||
if(settings.isAutoTunnelEnabled) {
|
if(settings.isAutoTunnelEnabled) {
|
||||||
ServiceManager.toggleWatcherServiceForeground(this@ShortcutsActivity, tunnelConfig)
|
ServiceManager.toggleWatcherServiceForeground(this@ShortcutsActivity, tunnelConfig)
|
||||||
|
@ -42,7 +42,7 @@ class ShortcutsActivity : ComponentActivity() {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
if(intent.getStringExtra(CLASS_NAME_EXTRA_KEY)
|
if(intent.getStringExtra(CLASS_NAME_EXTRA_KEY)
|
||||||
.equals(WireGuardTunnelService::class.java.simpleName)) {
|
.equals(WireGuardTunnelService::class.java.simpleName)) {
|
||||||
scope.launch {
|
lifecycleScope.launch(Dispatchers.Main) {
|
||||||
try {
|
try {
|
||||||
val settings = getSettings()
|
val settings = getSettings()
|
||||||
val tunnelConfig = if(settings.defaultTunnel == null) {
|
val tunnelConfig = if(settings.defaultTunnel == null) {
|
||||||
|
@ -63,11 +63,6 @@ class ShortcutsActivity : ComponentActivity() {
|
||||||
finish()
|
finish()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDestroy() {
|
|
||||||
super.onDestroy()
|
|
||||||
scope.cancel()
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun getSettings() : Settings {
|
private suspend fun getSettings() : Settings {
|
||||||
val settings = settingsRepo.getAll()
|
val settings = settingsRepo.getAll()
|
||||||
return if (settings.isNotEmpty()) {
|
return if (settings.isNotEmpty()) {
|
||||||
|
|
|
@ -78,7 +78,6 @@ class WireGuardTunnel @Inject constructor(private val backend : Backend,
|
||||||
if(getState() == Tunnel.State.UP) {
|
if(getState() == Tunnel.State.UP) {
|
||||||
val state = backend.setState(this, Tunnel.State.DOWN, null)
|
val state = backend.setState(this, Tunnel.State.DOWN, null)
|
||||||
_state.emit(state)
|
_state.emit(state)
|
||||||
scope.cancel()
|
|
||||||
}
|
}
|
||||||
} catch (e : BackendException) {
|
} catch (e : BackendException) {
|
||||||
Timber.e("Failed to stop tunnel with error: ${e.message}")
|
Timber.e("Failed to stop tunnel with error: ${e.message}")
|
||||||
|
|
|
@ -11,6 +11,7 @@ import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
import androidx.activity.compose.setContent
|
import androidx.activity.compose.setContent
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import androidx.compose.animation.ExitTransition
|
||||||
import androidx.compose.animation.ExperimentalAnimationApi
|
import androidx.compose.animation.ExperimentalAnimationApi
|
||||||
import androidx.compose.animation.core.tween
|
import androidx.compose.animation.core.tween
|
||||||
import androidx.compose.animation.fadeIn
|
import androidx.compose.animation.fadeIn
|
||||||
|
@ -32,6 +33,7 @@ import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.focus.FocusRequester
|
import androidx.compose.ui.focus.FocusRequester
|
||||||
import androidx.compose.ui.input.key.onKeyEvent
|
import androidx.compose.ui.input.key.onKeyEvent
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
import com.google.accompanist.navigation.animation.AnimatedNavHost
|
import com.google.accompanist.navigation.animation.AnimatedNavHost
|
||||||
import com.google.accompanist.navigation.animation.composable
|
import com.google.accompanist.navigation.animation.composable
|
||||||
import com.google.accompanist.navigation.animation.rememberAnimatedNavController
|
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.wireguard.android.backend.GoBackend
|
||||||
import com.zaneschepke.wireguardautotunnel.Constants
|
import com.zaneschepke.wireguardautotunnel.Constants
|
||||||
import com.zaneschepke.wireguardautotunnel.R
|
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.PermissionRequestFailedScreen
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.BottomNavBar
|
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.config.ConfigScreen
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.screens.detail.DetailScreen
|
import com.zaneschepke.wireguardautotunnel.ui.screens.detail.DetailScreen
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.screens.main.MainScreen
|
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.TransparentSystemBars
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.theme.WireguardAutoTunnelTheme
|
import com.zaneschepke.wireguardautotunnel.ui.theme.WireguardAutoTunnelTheme
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
@ -99,10 +100,10 @@ class MainActivity : AppCompatActivity() {
|
||||||
}
|
}
|
||||||
|
|
||||||
fun showSnackBarMessage(message : String) {
|
fun showSnackBarMessage(message : String) {
|
||||||
CoroutineScope(Dispatchers.Main).launch {
|
lifecycleScope.launch(Dispatchers.Main) {
|
||||||
val result = snackbarHostState.showSnackbar(
|
val result = snackbarHostState.showSnackbar(
|
||||||
message = message,
|
message = message,
|
||||||
actionLabel = "Okay",
|
actionLabel = applicationContext.getString(R.string.okay),
|
||||||
duration = SnackbarDuration.Short,
|
duration = SnackbarDuration.Short,
|
||||||
)
|
)
|
||||||
when (result) {
|
when (result) {
|
||||||
|
@ -184,7 +185,10 @@ class MainActivity : AppCompatActivity() {
|
||||||
fadeIn(animationSpec = tween(Constants.FADE_IN_ANIMATION_DURATION))
|
fadeIn(animationSpec = tween(Constants.FADE_IN_ANIMATION_DURATION))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}) {
|
}, exitTransition = {
|
||||||
|
ExitTransition.None
|
||||||
|
}
|
||||||
|
) {
|
||||||
MainScreen(padding = padding, showSnackbarMessage = { message -> showSnackBarMessage(message) }, navController = navController)
|
MainScreen(padding = padding, showSnackbarMessage = { message -> showSnackBarMessage(message) }, navController = navController)
|
||||||
}
|
}
|
||||||
composable(Routes.Settings.name, enterTransition = {
|
composable(Routes.Settings.name, enterTransition = {
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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.Arrangement
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
|
@ -3,6 +3,7 @@ package com.zaneschepke.wireguardautotunnel.ui.screens.config
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||||
import androidx.compose.foundation.Image
|
import androidx.compose.foundation.Image
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.focusGroup
|
import androidx.compose.foundation.focusGroup
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Column
|
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.ImeAction
|
||||||
import androidx.compose.ui.text.input.KeyboardCapitalization
|
import androidx.compose.ui.text.input.KeyboardCapitalization
|
||||||
import androidx.compose.ui.text.input.PasswordVisualTransformation
|
import androidx.compose.ui.text.input.PasswordVisualTransformation
|
||||||
|
import androidx.compose.ui.text.input.VisualTransformation
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.hilt.navigation.compose.hiltViewModel
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
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.Routes
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.common.SearchBar
|
import com.zaneschepke.wireguardautotunnel.ui.common.SearchBar
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.common.config.ConfigurationTextBox
|
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 com.zaneschepke.wireguardautotunnel.ui.common.text.SectionTitle
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
@ -109,6 +112,8 @@ fun ConfigScreen(
|
||||||
val proxyPeers by viewModel.proxyPeers.collectAsStateWithLifecycle()
|
val proxyPeers by viewModel.proxyPeers.collectAsStateWithLifecycle()
|
||||||
val proxyInterface by viewModel.interfaceProxy.collectAsStateWithLifecycle()
|
val proxyInterface by viewModel.interfaceProxy.collectAsStateWithLifecycle()
|
||||||
var showApplicationsDialog by remember { mutableStateOf(false) }
|
var showApplicationsDialog by remember { mutableStateOf(false) }
|
||||||
|
var showAuthPrompt by remember { mutableStateOf(false) }
|
||||||
|
var isAuthenticated by remember { mutableStateOf(false) }
|
||||||
val baseTextBoxModifier = Modifier.onFocusChanged {
|
val baseTextBoxModifier = Modifier.onFocusChanged {
|
||||||
if(WireGuardAutoTunnel.isRunningOnAndroidTv(context)) {
|
if(WireGuardAutoTunnel.isRunningOnAndroidTv(context)) {
|
||||||
keyboardController?.hide()
|
keyboardController?.hide()
|
||||||
|
@ -117,18 +122,7 @@ fun ConfigScreen(
|
||||||
|
|
||||||
val keyboardActions = KeyboardActions(
|
val keyboardActions = KeyboardActions(
|
||||||
onDone = {
|
onDone = {
|
||||||
//focusManager.clearFocus()
|
|
||||||
keyboardController?.hide()
|
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")
|
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) {
|
if (showApplicationsDialog) {
|
||||||
val sortedPackages = remember(packages) {
|
val sortedPackages = remember(packages) {
|
||||||
packages.sortedBy { viewModel.getPackageLabel(it) }
|
packages.sortedBy { viewModel.getPackageLabel(it) }
|
||||||
|
@ -399,10 +408,12 @@ fun ConfigScreen(
|
||||||
modifier = baseTextBoxModifier.fillMaxWidth().focusRequester(focusRequester)
|
modifier = baseTextBoxModifier.fillMaxWidth().focusRequester(focusRequester)
|
||||||
)
|
)
|
||||||
OutlinedTextField(
|
OutlinedTextField(
|
||||||
modifier = baseTextBoxModifier.fillMaxWidth(),
|
modifier = baseTextBoxModifier.fillMaxWidth().clickable {
|
||||||
|
showAuthPrompt = true
|
||||||
|
},
|
||||||
value = proxyInterface.privateKey,
|
value = proxyInterface.privateKey,
|
||||||
visualTransformation = PasswordVisualTransformation(),
|
visualTransformation = if((id == Constants.MANUAL_TUNNEL_CONFIG_ID) || isAuthenticated) VisualTransformation.None else PasswordVisualTransformation(),
|
||||||
enabled = id == Constants.MANUAL_TUNNEL_CONFIG_ID,
|
enabled = (id == Constants.MANUAL_TUNNEL_CONFIG_ID) || isAuthenticated,
|
||||||
onValueChange = { value ->
|
onValueChange = { value ->
|
||||||
viewModel.onPrivateKeyChange(value)
|
viewModel.onPrivateKeyChange(value)
|
||||||
},
|
},
|
||||||
|
|
|
@ -285,7 +285,7 @@ fun MainScreen(
|
||||||
modifier = Modifier.padding(10.dp)
|
modifier = Modifier.padding(10.dp)
|
||||||
)
|
)
|
||||||
Text(
|
Text(
|
||||||
stringResource(id = R.string.add_from_file),
|
stringResource(id = R.string.add_tunnels_text),
|
||||||
modifier = Modifier.padding(10.dp)
|
modifier = Modifier.padding(10.dp)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,8 +7,6 @@ import android.net.Uri
|
||||||
import android.provider.OpenableColumns
|
import android.provider.OpenableColumns
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
|
||||||
import com.wireguard.config.BadConfigException
|
|
||||||
import com.wireguard.config.Config
|
import com.wireguard.config.Config
|
||||||
import com.zaneschepke.wireguardautotunnel.Constants
|
import com.zaneschepke.wireguardautotunnel.Constants
|
||||||
import com.zaneschepke.wireguardautotunnel.R
|
import com.zaneschepke.wireguardautotunnel.R
|
||||||
|
@ -31,6 +29,7 @@ import kotlinx.coroutines.flow.asStateFlow
|
||||||
import kotlinx.coroutines.flow.filter
|
import kotlinx.coroutines.flow.filter
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
|
import java.util.zip.ZipInputStream
|
||||||
import javax.inject.Inject
|
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) {
|
private fun saveTunnelConfigFromStream(stream: InputStream, fileName: String) {
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
val bufferReader = stream.bufferedReader(charset = Charsets.UTF_8)
|
val bufferReader = stream.bufferedReader(charset = Charsets.UTF_8)
|
||||||
|
@ -160,17 +152,41 @@ class MainViewModel @Inject constructor(
|
||||||
?: throw WgTunnelException(application.getString(R.string.stream_failed))
|
?: throw WgTunnelException(application.getString(R.string.stream_failed))
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onTunnelFileSelected(uri: Uri) {
|
suspend fun onTunnelFileSelected(uri: Uri) {
|
||||||
try {
|
try {
|
||||||
val fileName = getFileName(application.applicationContext, uri)
|
val fileName = getFileName(application.applicationContext, uri)
|
||||||
validateFileExtension(fileName)
|
val fileExtension = getFileExtensionFromFileName(fileName)
|
||||||
val stream = getInputStreamFromUri(uri)
|
when(fileExtension){
|
||||||
saveTunnelConfigFromStream(stream, fileName)
|
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) {
|
} catch (e: Exception) {
|
||||||
throw WgTunnelException(e.message ?: "Error importing file")
|
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) {
|
private suspend fun addTunnel(tunnelConfig: TunnelConfig) {
|
||||||
saveTunnel(tunnelConfig)
|
saveTunnel(tunnelConfig)
|
||||||
}
|
}
|
||||||
|
|
|
@ -69,8 +69,12 @@ import com.zaneschepke.wireguardautotunnel.R
|
||||||
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
|
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.common.ClickableIconButton
|
import com.zaneschepke.wireguardautotunnel.ui.common.ClickableIconButton
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.common.config.ConfigurationToggle
|
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.ui.common.text.SectionTitle
|
||||||
|
import com.zaneschepke.wireguardautotunnel.util.StorageUtil
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import java.io.File
|
||||||
|
import kotlin.math.exp
|
||||||
|
|
||||||
@OptIn(
|
@OptIn(
|
||||||
ExperimentalPermissionsApi::class,
|
ExperimentalPermissionsApi::class,
|
||||||
|
@ -98,11 +102,28 @@ fun SettingsScreen(
|
||||||
val scrollState = rememberScrollState()
|
val scrollState = rememberScrollState()
|
||||||
var didShowLocationDisclaimer by remember { mutableStateOf(false) }
|
var didShowLocationDisclaimer by remember { mutableStateOf(false) }
|
||||||
var isBackgroundLocationGranted by remember { mutableStateOf(true) }
|
var isBackgroundLocationGranted by remember { mutableStateOf(true) }
|
||||||
|
var showAuthPrompt by remember { mutableStateOf(false) }
|
||||||
|
var didExportFiles by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
val screenPadding = 5.dp
|
val screenPadding = 5.dp
|
||||||
val fillMaxHeight = .85f
|
|
||||||
val fillMaxWidth = .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() {
|
fun saveTrustedSSID() {
|
||||||
if (currentText.isNotEmpty()) {
|
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()) {
|
if (tunnels.isEmpty()) {
|
||||||
Column(
|
Column(
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
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")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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<File>) {
|
||||||
|
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) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -8,7 +8,7 @@
|
||||||
<string name="foreground_file">FOREGROUND_FILE</string>
|
<string name="foreground_file">FOREGROUND_FILE</string>
|
||||||
<string name="github_url">https://github.com/zaneschepke/wgtunnel</string>
|
<string name="github_url">https://github.com/zaneschepke/wgtunnel</string>
|
||||||
<string name="privacy_policy_url">https://zaneschepke.github.io/wgtunnel/</string>
|
<string name="privacy_policy_url">https://zaneschepke.github.io/wgtunnel/</string>
|
||||||
<string name="file_extension_message">File is not a .conf file</string>
|
<string name="file_extension_message">File is not a .conf or .zip</string>
|
||||||
<string name="turn_off_tunnel">Turn off tunnel before editing</string>
|
<string name="turn_off_tunnel">Turn off tunnel before editing</string>
|
||||||
<string name="no_tunnels">No tunnels added yet!</string>
|
<string name="no_tunnels">No tunnels added yet!</string>
|
||||||
<string name="tunnel_exists">Tunnel name already exists</string>
|
<string name="tunnel_exists">Tunnel name already exists</string>
|
||||||
|
@ -38,9 +38,9 @@
|
||||||
<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="add_from_file">Add tunnel from files</string>
|
<string name="add_tunnels_text">Add from file or zip</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 from QR code</string>
|
||||||
<string name="qr_scan">QR Scan</string>
|
<string name="qr_scan">QR Scan</string>
|
||||||
<string name="tunnel_edit">Tunnel Edit</string>
|
<string name="tunnel_edit">Tunnel Edit</string>
|
||||||
<string name="tunnel_name">Tunnel Name</string>
|
<string name="tunnel_name">Tunnel Name</string>
|
||||||
|
|
|
@ -3,6 +3,7 @@ accompanist = "0.31.2-alpha"
|
||||||
activityCompose = "1.8.0"
|
activityCompose = "1.8.0"
|
||||||
androidx-junit = "1.1.5"
|
androidx-junit = "1.1.5"
|
||||||
appcompat = "1.6.1"
|
appcompat = "1.6.1"
|
||||||
|
biometricKtx = "1.2.0-alpha05"
|
||||||
coreKtx = "1.12.0"
|
coreKtx = "1.12.0"
|
||||||
espressoCore = "3.5.1"
|
espressoCore = "3.5.1"
|
||||||
firebase-crashlytics-gradle = "2.9.9"
|
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" }
|
accompanist-systemuicontroller = { module = "com.google.accompanist:accompanist-systemuicontroller", version.ref = "accompanist" }
|
||||||
|
|
||||||
#room
|
#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-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-compiler = { module = "androidx.room:room-compiler", version.ref = "roomVersion" }
|
||||||
androidx-room-ktx = { module = "androidx.room:room-ktx", version.ref = "roomVersion" }
|
androidx-room-ktx = { module = "androidx.room:room-ktx", version.ref = "roomVersion" }
|
||||||
|
|
Loading…
Reference in New Issue