fix: file import handling
Fixes bug causing crashes when importing a file that is not a .conf file. Fixes file import on AndroidTV and FireTV Closes #39 Fixes usability issue on AndroidTV where textboxes immediately open the keyboard on hover. Allows users two keypress access to turning on tunnels from app load. Closes #36 Improves service efficiencies by making coroutines lifecycle aware.
This commit is contained in:
parent
2912238f27
commit
321730536d
|
@ -14,8 +14,8 @@ android {
|
||||||
applicationId = "com.zaneschepke.wireguardautotunnel"
|
applicationId = "com.zaneschepke.wireguardautotunnel"
|
||||||
minSdk = 26
|
minSdk = 26
|
||||||
targetSdk = 34
|
targetSdk = 34
|
||||||
versionCode = 31000
|
versionCode = 31100
|
||||||
versionName = "3.1.0"
|
versionName = "3.1.1"
|
||||||
|
|
||||||
multiDexEnabled = true
|
multiDexEnabled = true
|
||||||
|
|
||||||
|
@ -81,6 +81,8 @@ val generalImplementation by configurations
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation(libs.androidx.core.ktx)
|
implementation(libs.androidx.core.ktx)
|
||||||
implementation(libs.androidx.lifecycle.runtime.ktx)
|
implementation(libs.androidx.lifecycle.runtime.ktx)
|
||||||
|
// optional - helpers for implementing LifecycleOwner in a Service
|
||||||
|
implementation(libs.androidx.lifecycle.service)
|
||||||
implementation(libs.androidx.activity.compose)
|
implementation(libs.androidx.activity.compose)
|
||||||
implementation(platform(libs.androidx.compose.bom))
|
implementation(platform(libs.androidx.compose.bom))
|
||||||
implementation(libs.androidx.compose.ui)
|
implementation(libs.androidx.compose.ui)
|
||||||
|
|
|
@ -12,5 +12,6 @@ object Constants {
|
||||||
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 = "*/*"
|
||||||
const val ANDROID_TV_STUBS = "com.google.android.tv.frameworkpackagestubs"
|
const val GOOGLE_TV_EXPLORER_STUB = "com.google.android.tv.frameworkpackagestubs"
|
||||||
|
const val ANDROID_TV_EXPLORER_STUB = "com.android.tv.frameworkpackagestubs"
|
||||||
}
|
}
|
|
@ -1,22 +1,24 @@
|
||||||
package com.zaneschepke.wireguardautotunnel.service.foreground
|
package com.zaneschepke.wireguardautotunnel.service.foreground
|
||||||
|
|
||||||
import android.app.Service
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.IBinder
|
import android.os.IBinder
|
||||||
|
import androidx.lifecycle.LifecycleService
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
|
||||||
|
|
||||||
open class ForegroundService : Service() {
|
open class ForegroundService : LifecycleService() {
|
||||||
|
|
||||||
private var isServiceStarted = false
|
private var isServiceStarted = false
|
||||||
|
|
||||||
override fun onBind(intent: Intent): IBinder? {
|
override fun onBind(intent: Intent): IBinder? {
|
||||||
|
super.onBind(intent)
|
||||||
// We don't provide binding, so return null
|
// We don't provide binding, so return null
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||||
|
super.onStartCommand(intent, flags, startId)
|
||||||
Timber.d("onStartCommand executed with startId: $startId")
|
Timber.d("onStartCommand executed with startId: $startId")
|
||||||
if (intent != null) {
|
if (intent != null) {
|
||||||
val action = intent.action
|
val action = intent.action
|
||||||
|
|
|
@ -7,6 +7,7 @@ import android.content.Intent
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.PowerManager
|
import android.os.PowerManager
|
||||||
import android.os.SystemClock
|
import android.os.SystemClock
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
import com.wireguard.android.backend.Tunnel
|
import com.wireguard.android.backend.Tunnel
|
||||||
import com.zaneschepke.wireguardautotunnel.Constants
|
import com.zaneschepke.wireguardautotunnel.Constants
|
||||||
import com.zaneschepke.wireguardautotunnel.R
|
import com.zaneschepke.wireguardautotunnel.R
|
||||||
|
@ -66,7 +67,7 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
|
||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
CoroutineScope(Dispatchers.Main).launch {
|
lifecycleScope.launch(Dispatchers.Main) {
|
||||||
launchWatcherNotification()
|
launchWatcherNotification()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -122,6 +123,9 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
|
||||||
wakeLock =
|
wakeLock =
|
||||||
(getSystemService(Context.POWER_SERVICE) as PowerManager).run {
|
(getSystemService(Context.POWER_SERVICE) as PowerManager).run {
|
||||||
newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "$tag::lock").apply {
|
newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "$tag::lock").apply {
|
||||||
|
//TODO decide what to do here with the wakelock
|
||||||
|
//this is draining battery. Perhaps users only care for VPN to connect when their screen is on
|
||||||
|
//and they are actively using apps
|
||||||
acquire()
|
acquire()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -134,7 +138,7 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun startWatcherJob() {
|
private fun startWatcherJob() {
|
||||||
watcherJob = CoroutineScope(Dispatchers.IO).launch {
|
watcherJob = lifecycleScope.launch(Dispatchers.IO) {
|
||||||
val settings = settingsRepo.getAll();
|
val settings = settingsRepo.getAll();
|
||||||
if(settings.isNotEmpty()) {
|
if(settings.isNotEmpty()) {
|
||||||
setting = settings[0]
|
setting = settings[0]
|
||||||
|
|
|
@ -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.lifecycleScope
|
||||||
import com.zaneschepke.wireguardautotunnel.R
|
import com.zaneschepke.wireguardautotunnel.R
|
||||||
import com.zaneschepke.wireguardautotunnel.receiver.NotificationActionReceiver
|
import com.zaneschepke.wireguardautotunnel.receiver.NotificationActionReceiver
|
||||||
import com.zaneschepke.wireguardautotunnel.repository.SettingsDoa
|
import com.zaneschepke.wireguardautotunnel.repository.SettingsDoa
|
||||||
|
@ -38,7 +39,7 @@ class WireGuardTunnelService : ForegroundService() {
|
||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
CoroutineScope(Dispatchers.Main).launch {
|
lifecycleScope.launch(Dispatchers.Main) {
|
||||||
launchVpnStartingNotification()
|
launchVpnStartingNotification()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -48,7 +49,7 @@ class WireGuardTunnelService : ForegroundService() {
|
||||||
launchVpnStartingNotification()
|
launchVpnStartingNotification()
|
||||||
val tunnelConfigString = extras?.getString(getString(R.string.tunnel_extras_key))
|
val tunnelConfigString = extras?.getString(getString(R.string.tunnel_extras_key))
|
||||||
cancelJob()
|
cancelJob()
|
||||||
job = CoroutineScope(Dispatchers.IO).launch {
|
job = lifecycleScope.launch(Dispatchers.IO) {
|
||||||
if(tunnelConfigString != null) {
|
if(tunnelConfigString != null) {
|
||||||
try {
|
try {
|
||||||
val tunnelConfig = TunnelConfig.from(tunnelConfigString)
|
val tunnelConfig = TunnelConfig.from(tunnelConfigString)
|
||||||
|
@ -70,32 +71,32 @@ class WireGuardTunnelService : ForegroundService() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
launch {
|
||||||
CoroutineScope(job).launch {
|
var didShowConnected = false
|
||||||
var didShowConnected = false
|
var didShowFailedHandshakeNotification = false
|
||||||
var didShowFailedHandshakeNotification = false
|
vpnService.handshakeStatus.collect {
|
||||||
vpnService.handshakeStatus.collect {
|
when(it) {
|
||||||
when(it) {
|
HandshakeStatus.NOT_STARTED -> {
|
||||||
HandshakeStatus.NOT_STARTED -> {
|
|
||||||
}
|
|
||||||
HandshakeStatus.NEVER_CONNECTED -> {
|
|
||||||
if(!didShowFailedHandshakeNotification) {
|
|
||||||
launchVpnConnectionFailedNotification(getString(R.string.initial_connection_failure_message))
|
|
||||||
didShowFailedHandshakeNotification = true
|
|
||||||
didShowConnected = false
|
|
||||||
}
|
}
|
||||||
}
|
HandshakeStatus.NEVER_CONNECTED -> {
|
||||||
HandshakeStatus.HEALTHY -> {
|
if(!didShowFailedHandshakeNotification) {
|
||||||
if(!didShowConnected) {
|
launchVpnConnectionFailedNotification(getString(R.string.initial_connection_failure_message))
|
||||||
launchVpnConnectedNotification()
|
didShowFailedHandshakeNotification = true
|
||||||
didShowConnected = true
|
didShowConnected = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
HandshakeStatus.HEALTHY -> {
|
||||||
HandshakeStatus.UNHEALTHY -> {
|
if(!didShowConnected) {
|
||||||
if(!didShowFailedHandshakeNotification) {
|
launchVpnConnectedNotification()
|
||||||
launchVpnConnectionFailedNotification(getString(R.string.lost_connection_failure_message))
|
didShowConnected = true
|
||||||
didShowFailedHandshakeNotification = true
|
}
|
||||||
didShowConnected = false
|
}
|
||||||
|
HandshakeStatus.UNHEALTHY -> {
|
||||||
|
if(!didShowFailedHandshakeNotification) {
|
||||||
|
launchVpnConnectionFailedNotification(getString(R.string.lost_connection_failure_message))
|
||||||
|
didShowFailedHandshakeNotification = true
|
||||||
|
didShowConnected = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -105,7 +106,7 @@ class WireGuardTunnelService : ForegroundService() {
|
||||||
|
|
||||||
override fun stopService(extras : Bundle?) {
|
override fun stopService(extras : Bundle?) {
|
||||||
super.stopService(extras)
|
super.stopService(extras)
|
||||||
CoroutineScope(Dispatchers.IO).launch {
|
lifecycleScope.launch(Dispatchers.IO) {
|
||||||
vpnService.stopTunnel()
|
vpnService.stopTunnel()
|
||||||
}
|
}
|
||||||
cancelJob()
|
cancelJob()
|
||||||
|
|
|
@ -13,6 +13,7 @@ import com.zaneschepke.wireguardautotunnel.util.WgTunnelException
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.cancel
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
@ -62,6 +63,11 @@ 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()) {
|
||||||
|
|
|
@ -55,6 +55,11 @@ class TunnelControlTile : TileService() {
|
||||||
cancelJob()
|
cancelJob()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onDestroy() {
|
||||||
|
super.onDestroy()
|
||||||
|
scope.cancel()
|
||||||
|
}
|
||||||
|
|
||||||
override fun onClick() {
|
override fun onClick() {
|
||||||
super.onClick()
|
super.onClick()
|
||||||
unlockAndRun {
|
unlockAndRun {
|
||||||
|
|
|
@ -11,6 +11,7 @@ import com.zaneschepke.wireguardautotunnel.util.NumberUtils
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.cancel
|
||||||
import kotlinx.coroutines.channels.BufferOverflow
|
import kotlinx.coroutines.channels.BufferOverflow
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||||
|
@ -46,6 +47,8 @@ class WireGuardTunnel @Inject constructor(private val backend : Backend,
|
||||||
override val handshakeStatus: SharedFlow<HandshakeStatus>
|
override val handshakeStatus: SharedFlow<HandshakeStatus>
|
||||||
get() = _handshakeStatus.asSharedFlow()
|
get() = _handshakeStatus.asSharedFlow()
|
||||||
|
|
||||||
|
private val scope = CoroutineScope(Dispatchers.IO);
|
||||||
|
|
||||||
private lateinit var statsJob : Job
|
private lateinit var statsJob : Job
|
||||||
|
|
||||||
|
|
||||||
|
@ -70,11 +73,12 @@ class WireGuardTunnel @Inject constructor(private val backend : Backend,
|
||||||
return _tunnelName.value
|
return _tunnelName.value
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun stopTunnel() {
|
override suspend fun stopTunnel() {
|
||||||
try {
|
try {
|
||||||
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}")
|
||||||
|
@ -89,7 +93,7 @@ class WireGuardTunnel @Inject constructor(private val backend : Backend,
|
||||||
val tunnel = this;
|
val tunnel = this;
|
||||||
_state.tryEmit(state)
|
_state.tryEmit(state)
|
||||||
if(state == Tunnel.State.UP) {
|
if(state == Tunnel.State.UP) {
|
||||||
statsJob = CoroutineScope(Dispatchers.IO).launch {
|
statsJob = scope.launch {
|
||||||
val handshakeMap = HashMap<Key, Long>()
|
val handshakeMap = HashMap<Key, Long>()
|
||||||
var neverHadHandshakeCounter = 0
|
var neverHadHandshakeCounter = 0
|
||||||
while (true) {
|
while (true) {
|
||||||
|
@ -128,4 +132,6 @@ class WireGuardTunnel @Inject constructor(private val backend : Backend,
|
||||||
_lastHandshake.tryEmit(emptyMap())
|
_lastHandshake.tryEmit(emptyMap())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
|
@ -1,9 +0,0 @@
|
||||||
package com.zaneschepke.wireguardautotunnel.ui
|
|
||||||
|
|
||||||
data class ViewState(
|
|
||||||
val showSnackbarMessage : Boolean = false,
|
|
||||||
val snackbarMessage : String = "",
|
|
||||||
val snackbarActionText : String = "",
|
|
||||||
val onSnackbarActionClick : () -> Unit = {},
|
|
||||||
val isLoading : Boolean = false
|
|
||||||
)
|
|
|
@ -12,7 +12,7 @@ import androidx.compose.ui.text.input.KeyboardCapitalization
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun
|
fun
|
||||||
ConfigurationTextBox(value : String, hint : String, onValueChange : (String) -> Unit, label : String, onDone : () -> Unit, modifier: Modifier) {
|
ConfigurationTextBox(value : String, hint : String, onValueChange : (String) -> Unit, keyboardActions : KeyboardActions, label : String, modifier: Modifier) {
|
||||||
OutlinedTextField(
|
OutlinedTextField(
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
value = value,
|
value = value,
|
||||||
|
@ -29,10 +29,6 @@ fun
|
||||||
capitalization = KeyboardCapitalization.None,
|
capitalization = KeyboardCapitalization.None,
|
||||||
imeAction = ImeAction.Done
|
imeAction = ImeAction.Done
|
||||||
),
|
),
|
||||||
keyboardActions = KeyboardActions(
|
keyboardActions = keyboardActions,
|
||||||
onDone = {
|
|
||||||
onDone()
|
|
||||||
}
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
}
|
}
|
|
@ -1,7 +1,6 @@
|
||||||
package com.zaneschepke.wireguardautotunnel.ui.common.config
|
package com.zaneschepke.wireguardautotunnel.ui.common.config
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
|
@ -14,7 +13,7 @@ import androidx.compose.ui.unit.Dp
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ConfigurationToggle(label : String, enabled : Boolean, checked : Boolean, padding : Dp,
|
fun ConfigurationToggle(label : String, enabled : Boolean, checked : Boolean, padding : Dp,
|
||||||
onCheckChanged : () -> Unit) {
|
onCheckChanged : () -> Unit, modifier : Modifier = Modifier) {
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
|
@ -24,6 +23,7 @@ fun ConfigurationToggle(label : String, enabled : Boolean, checked : Boolean, pa
|
||||||
) {
|
) {
|
||||||
Text(label)
|
Text(label)
|
||||||
Switch(
|
Switch(
|
||||||
|
modifier = modifier,
|
||||||
enabled = enabled,
|
enabled = enabled,
|
||||||
checked = checked,
|
checked = checked,
|
||||||
onCheckedChange = {
|
onCheckedChange = {
|
||||||
|
|
|
@ -60,7 +60,6 @@ import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.platform.ClipboardManager
|
import androidx.compose.ui.platform.ClipboardManager
|
||||||
import androidx.compose.ui.platform.LocalClipboardManager
|
import androidx.compose.ui.platform.LocalClipboardManager
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.platform.LocalFocusManager
|
|
||||||
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.AnnotatedString
|
import androidx.compose.ui.text.AnnotatedString
|
||||||
|
@ -96,7 +95,6 @@ fun ConfigScreen(
|
||||||
) {
|
) {
|
||||||
|
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val focusManager = LocalFocusManager.current
|
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
val clipboardManager: ClipboardManager = LocalClipboardManager.current
|
val clipboardManager: ClipboardManager = LocalClipboardManager.current
|
||||||
val keyboardController = LocalSoftwareKeyboardController.current
|
val keyboardController = LocalSoftwareKeyboardController.current
|
||||||
|
@ -111,11 +109,24 @@ 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) }
|
||||||
|
val baseTextBoxModifier = Modifier.onFocusChanged {
|
||||||
|
keyboardController?.hide()
|
||||||
|
}
|
||||||
|
|
||||||
val keyboardActions = KeyboardActions(
|
val keyboardActions = KeyboardActions(
|
||||||
onDone = {
|
onDone = {
|
||||||
focusManager.clearFocus()
|
//focusManager.clearFocus()
|
||||||
keyboardController?.hide()
|
keyboardController?.hide()
|
||||||
|
},
|
||||||
|
onNext = {
|
||||||
|
keyboardController?.hide()
|
||||||
|
},
|
||||||
|
onPrevious = {
|
||||||
|
keyboardController?.hide()
|
||||||
|
},
|
||||||
|
onGo = {
|
||||||
|
keyboardController?.hide(
|
||||||
|
)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -380,16 +391,13 @@ fun ConfigScreen(
|
||||||
onValueChange = { value ->
|
onValueChange = { value ->
|
||||||
viewModel.onTunnelNameChange(value)
|
viewModel.onTunnelNameChange(value)
|
||||||
},
|
},
|
||||||
onDone = {
|
keyboardActions = keyboardActions,
|
||||||
focusManager.clearFocus()
|
|
||||||
keyboardController?.hide()
|
|
||||||
},
|
|
||||||
label = stringResource(R.string.name),
|
label = stringResource(R.string.name),
|
||||||
hint = stringResource(R.string.tunnel_name).lowercase(),
|
hint = stringResource(R.string.tunnel_name).lowercase(),
|
||||||
modifier = Modifier.fillMaxWidth().focusRequester(focusRequester)
|
modifier = baseTextBoxModifier.fillMaxWidth().focusRequester(focusRequester)
|
||||||
)
|
)
|
||||||
OutlinedTextField(
|
OutlinedTextField(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = baseTextBoxModifier.fillMaxWidth(),
|
||||||
value = proxyInterface.privateKey,
|
value = proxyInterface.privateKey,
|
||||||
visualTransformation = PasswordVisualTransformation(),
|
visualTransformation = PasswordVisualTransformation(),
|
||||||
enabled = id == Constants.MANUAL_TUNNEL_CONFIG_ID,
|
enabled = id == Constants.MANUAL_TUNNEL_CONFIG_ID,
|
||||||
|
@ -416,7 +424,7 @@ fun ConfigScreen(
|
||||||
keyboardActions = keyboardActions
|
keyboardActions = keyboardActions
|
||||||
)
|
)
|
||||||
OutlinedTextField(
|
OutlinedTextField(
|
||||||
modifier = Modifier.fillMaxWidth().focusRequester(FocusRequester.Default),
|
modifier = baseTextBoxModifier.fillMaxWidth().focusRequester(FocusRequester.Default),
|
||||||
value = proxyInterface.publicKey,
|
value = proxyInterface.publicKey,
|
||||||
enabled = false,
|
enabled = false,
|
||||||
onValueChange = {},
|
onValueChange = {},
|
||||||
|
@ -445,52 +453,40 @@ fun ConfigScreen(
|
||||||
onValueChange = { value ->
|
onValueChange = { value ->
|
||||||
viewModel.onAddressesChanged(value)
|
viewModel.onAddressesChanged(value)
|
||||||
},
|
},
|
||||||
onDone = {
|
keyboardActions = keyboardActions,
|
||||||
focusManager.clearFocus()
|
|
||||||
keyboardController?.hide()
|
|
||||||
},
|
|
||||||
label = stringResource(R.string.addresses),
|
label = stringResource(R.string.addresses),
|
||||||
hint = stringResource(R.string.comma_separated_list),
|
hint = stringResource(R.string.comma_separated_list),
|
||||||
modifier = Modifier
|
modifier = baseTextBoxModifier
|
||||||
.fillMaxWidth(3 / 5f)
|
.fillMaxWidth(3 / 5f)
|
||||||
.padding(end = 5.dp)
|
.padding(end = 5.dp)
|
||||||
)
|
)
|
||||||
ConfigurationTextBox(
|
ConfigurationTextBox(
|
||||||
value = proxyInterface.listenPort,
|
value = proxyInterface.listenPort,
|
||||||
onValueChange = { value -> viewModel.onListenPortChanged(value) },
|
onValueChange = { value -> viewModel.onListenPortChanged(value) },
|
||||||
onDone = {
|
keyboardActions = keyboardActions,
|
||||||
focusManager.clearFocus()
|
|
||||||
keyboardController?.hide()
|
|
||||||
},
|
|
||||||
label = stringResource(R.string.listen_port),
|
label = stringResource(R.string.listen_port),
|
||||||
hint = stringResource(R.string.random),
|
hint = stringResource(R.string.random),
|
||||||
modifier = Modifier.width(IntrinsicSize.Min)
|
modifier = baseTextBoxModifier.width(IntrinsicSize.Min)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
Row(modifier = Modifier.fillMaxWidth()) {
|
Row(modifier = Modifier.fillMaxWidth()) {
|
||||||
ConfigurationTextBox(
|
ConfigurationTextBox(
|
||||||
value = proxyInterface.dnsServers,
|
value = proxyInterface.dnsServers,
|
||||||
onValueChange = { value -> viewModel.onDnsServersChanged(value) },
|
onValueChange = { value -> viewModel.onDnsServersChanged(value) },
|
||||||
onDone = {
|
keyboardActions = keyboardActions,
|
||||||
focusManager.clearFocus()
|
|
||||||
keyboardController?.hide()
|
|
||||||
},
|
|
||||||
label = stringResource(R.string.dns_servers),
|
label = stringResource(R.string.dns_servers),
|
||||||
hint = stringResource(R.string.comma_separated_list),
|
hint = stringResource(R.string.comma_separated_list),
|
||||||
modifier = Modifier
|
modifier = baseTextBoxModifier
|
||||||
.fillMaxWidth(3 / 5f)
|
.fillMaxWidth(3 / 5f)
|
||||||
.padding(end = 5.dp)
|
.padding(end = 5.dp)
|
||||||
)
|
)
|
||||||
ConfigurationTextBox(
|
ConfigurationTextBox(
|
||||||
value = proxyInterface.mtu,
|
value = proxyInterface.mtu,
|
||||||
onValueChange = { value -> viewModel.onMtuChanged(value) },
|
onValueChange = { value -> viewModel.onMtuChanged(value) },
|
||||||
onDone = {
|
keyboardActions = keyboardActions,
|
||||||
focusManager.clearFocus()
|
|
||||||
keyboardController?.hide()
|
|
||||||
},
|
|
||||||
label = stringResource(R.string.mtu),
|
label = stringResource(R.string.mtu),
|
||||||
hint = stringResource(R.string.auto),
|
hint = stringResource(R.string.auto),
|
||||||
modifier = Modifier.width(IntrinsicSize.Min)
|
modifier = baseTextBoxModifier.width(IntrinsicSize.Min)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
Row(
|
Row(
|
||||||
|
@ -556,13 +552,10 @@ fun ConfigScreen(
|
||||||
value
|
value
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
onDone = {
|
keyboardActions = keyboardActions,
|
||||||
focusManager.clearFocus()
|
|
||||||
keyboardController?.hide()
|
|
||||||
},
|
|
||||||
label = stringResource(R.string.public_key),
|
label = stringResource(R.string.public_key),
|
||||||
hint = stringResource(R.string.base64_key),
|
hint = stringResource(R.string.base64_key),
|
||||||
modifier = Modifier.fillMaxWidth()
|
modifier = baseTextBoxModifier.fillMaxWidth()
|
||||||
)
|
)
|
||||||
ConfigurationTextBox(
|
ConfigurationTextBox(
|
||||||
value = peer.preSharedKey,
|
value = peer.preSharedKey,
|
||||||
|
@ -572,16 +565,13 @@ fun ConfigScreen(
|
||||||
value
|
value
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
onDone = {
|
keyboardActions = keyboardActions,
|
||||||
focusManager.clearFocus()
|
|
||||||
keyboardController?.hide()
|
|
||||||
},
|
|
||||||
label = stringResource(R.string.preshared_key),
|
label = stringResource(R.string.preshared_key),
|
||||||
hint = stringResource(R.string.optional),
|
hint = stringResource(R.string.optional),
|
||||||
modifier = Modifier.fillMaxWidth()
|
modifier = baseTextBoxModifier.fillMaxWidth()
|
||||||
)
|
)
|
||||||
OutlinedTextField(
|
OutlinedTextField(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = baseTextBoxModifier.fillMaxWidth(),
|
||||||
value = peer.persistentKeepalive,
|
value = peer.persistentKeepalive,
|
||||||
enabled = true,
|
enabled = true,
|
||||||
onValueChange = { value ->
|
onValueChange = { value ->
|
||||||
|
@ -602,16 +592,13 @@ fun ConfigScreen(
|
||||||
value
|
value
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
onDone = {
|
keyboardActions = keyboardActions,
|
||||||
focusManager.clearFocus()
|
|
||||||
keyboardController?.hide()
|
|
||||||
},
|
|
||||||
label = stringResource(R.string.endpoint),
|
label = stringResource(R.string.endpoint),
|
||||||
hint = stringResource(R.string.endpoint).lowercase(),
|
hint = stringResource(R.string.endpoint).lowercase(),
|
||||||
modifier = Modifier.fillMaxWidth()
|
modifier = baseTextBoxModifier.fillMaxWidth()
|
||||||
)
|
)
|
||||||
OutlinedTextField(
|
OutlinedTextField(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = baseTextBoxModifier.fillMaxWidth(),
|
||||||
value = peer.allowedIps,
|
value = peer.allowedIps,
|
||||||
enabled = true,
|
enabled = true,
|
||||||
onValueChange = { value ->
|
onValueChange = { value ->
|
||||||
|
|
|
@ -1,6 +1,10 @@
|
||||||
package com.zaneschepke.wireguardautotunnel.ui.screens.main
|
package com.zaneschepke.wireguardautotunnel.ui.screens.main
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.pm.PackageManager
|
||||||
|
import android.os.Build
|
||||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.compose.animation.AnimatedVisibility
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
|
@ -85,6 +89,8 @@ 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
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.theme.mint
|
import com.zaneschepke.wireguardautotunnel.ui.theme.mint
|
||||||
|
import com.zaneschepke.wireguardautotunnel.util.WgTunnelException
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter")
|
@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter")
|
||||||
|
@ -129,13 +135,33 @@ fun MainScreen(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val pickFileLauncher = rememberLauncherForActivityResult(
|
val tunnelFileImportResultLauncher = rememberLauncherForActivityResult(object : ActivityResultContracts.GetContent() {
|
||||||
ActivityResultContracts.GetContent()
|
override fun createIntent(context: Context, input: String): Intent {
|
||||||
) { result -> if (result != null)
|
val intent = super.createIntent(context, input)
|
||||||
try {
|
|
||||||
viewModel.onTunnelFileSelected(result)
|
/* AndroidTV now comes with stubs that do nothing but display a Toast less helpful than
|
||||||
} catch (e : Exception) {
|
* what we can do, so detect this and throw an exception that we can catch later. */
|
||||||
showSnackbarMessage(e.message ?: "Unknown error occurred")
|
val activitiesToResolveIntent = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||||
|
context.packageManager.queryIntentActivities(intent, PackageManager.ResolveInfoFlags.of(PackageManager.MATCH_DEFAULT_ONLY.toLong()))
|
||||||
|
} else {
|
||||||
|
context.packageManager.queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY)
|
||||||
|
}
|
||||||
|
if (activitiesToResolveIntent.all {
|
||||||
|
val name = it.activityInfo.packageName
|
||||||
|
name.startsWith(Constants.GOOGLE_TV_EXPLORER_STUB) || name.startsWith(Constants.ANDROID_TV_EXPLORER_STUB)
|
||||||
|
}) {
|
||||||
|
throw WgTunnelException("No file explorer installed")
|
||||||
|
}
|
||||||
|
return intent
|
||||||
|
}
|
||||||
|
}) { data ->
|
||||||
|
if (data == null) return@rememberLauncherForActivityResult
|
||||||
|
scope.launch(Dispatchers.IO) {
|
||||||
|
try {
|
||||||
|
viewModel.onTunnelFileSelected(data)
|
||||||
|
} catch (e : Exception) {
|
||||||
|
showSnackbarMessage(e.message ?: "Unknown error occurred")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -163,16 +189,16 @@ fun MainScreen(
|
||||||
selectedTunnel = null
|
selectedTunnel = null
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
{ Text(text = "Okay") }
|
{ Text(text = stringResource(R.string.okay)) }
|
||||||
},
|
},
|
||||||
dismissButton = {
|
dismissButton = {
|
||||||
TextButton(onClick = {
|
TextButton(onClick = {
|
||||||
showPrimaryChangeAlertDialog = false
|
showPrimaryChangeAlertDialog = false
|
||||||
})
|
})
|
||||||
{ Text(text = "Cancel") }
|
{ Text(text = stringResource(R.string.cancel)) }
|
||||||
},
|
},
|
||||||
title = { Text(text = "Primary tunnel change") },
|
title = { Text(text = stringResource(R.string.primary_tunnel_change)) },
|
||||||
text = { Text(text = "Would you like to make this your primary tunnel?") }
|
text = { Text(text = stringResource(R.string.primary_tunnnel_change_question)) }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -246,9 +272,9 @@ fun MainScreen(
|
||||||
.clickable {
|
.clickable {
|
||||||
showBottomSheet = false
|
showBottomSheet = false
|
||||||
try {
|
try {
|
||||||
pickFileLauncher.launch(Constants.ALLOWED_FILE_TYPES)
|
tunnelFileImportResultLauncher.launch(Constants.ALLOWED_FILE_TYPES)
|
||||||
} catch (_: Exception) {
|
} catch (e : Exception) {
|
||||||
showSnackbarMessage("No file explorer")
|
showSnackbarMessage(e.message!!)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.padding(10.dp)
|
.padding(10.dp)
|
||||||
|
@ -263,32 +289,34 @@ fun MainScreen(
|
||||||
modifier = Modifier.padding(10.dp)
|
modifier = Modifier.padding(10.dp)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
Divider()
|
if(!WireGuardAutoTunnel.isRunningOnAndroidTv(context)) {
|
||||||
Row(modifier = Modifier
|
Divider()
|
||||||
.fillMaxWidth()
|
Row(modifier = Modifier
|
||||||
.clickable {
|
.fillMaxWidth()
|
||||||
scope.launch {
|
.clickable {
|
||||||
showBottomSheet = false
|
scope.launch {
|
||||||
val scanOptions = ScanOptions()
|
showBottomSheet = false
|
||||||
scanOptions.setDesiredBarcodeFormats(ScanOptions.QR_CODE)
|
val scanOptions = ScanOptions()
|
||||||
scanOptions.setOrientationLocked(true)
|
scanOptions.setDesiredBarcodeFormats(ScanOptions.QR_CODE)
|
||||||
scanOptions.setPrompt(context.getString(R.string.scanning_qr))
|
scanOptions.setOrientationLocked(true)
|
||||||
scanOptions.setBeepEnabled(false)
|
scanOptions.setPrompt(context.getString(R.string.scanning_qr))
|
||||||
scanOptions.captureActivity = CaptureActivityPortrait::class.java
|
scanOptions.setBeepEnabled(false)
|
||||||
scanLauncher.launch(scanOptions)
|
scanOptions.captureActivity = CaptureActivityPortrait::class.java
|
||||||
|
scanLauncher.launch(scanOptions)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
.padding(10.dp)
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
Icons.Filled.QrCode,
|
||||||
|
contentDescription = stringResource(id = R.string.qr_scan),
|
||||||
|
modifier = Modifier.padding(10.dp)
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
stringResource(id = R.string.add_from_qr),
|
||||||
|
modifier = Modifier.padding(10.dp)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
.padding(10.dp)
|
|
||||||
) {
|
|
||||||
Icon(
|
|
||||||
Icons.Filled.QrCode,
|
|
||||||
contentDescription = stringResource(id = R.string.qr_scan),
|
|
||||||
modifier = Modifier.padding(10.dp)
|
|
||||||
)
|
|
||||||
Text(
|
|
||||||
stringResource(id = R.string.add_from_qr),
|
|
||||||
modifier = Modifier.padding(10.dp)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
Divider()
|
Divider()
|
||||||
Row(
|
Row(
|
||||||
|
@ -440,6 +468,7 @@ fun MainScreen(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
Switch(
|
Switch(
|
||||||
|
modifier = Modifier.focusRequester(focusRequester),
|
||||||
checked = (state == Tunnel.State.UP && tunnel.name == tunnelName),
|
checked = (state == Tunnel.State.UP && tunnel.name == tunnelName),
|
||||||
onCheckedChange = { checked ->
|
onCheckedChange = { checked ->
|
||||||
onTunnelToggle(checked, tunnel)
|
onTunnelToggle(checked, tunnel)
|
||||||
|
|
|
@ -35,10 +35,11 @@ import javax.inject.Inject
|
||||||
|
|
||||||
|
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
class MainViewModel @Inject constructor(private val application : Application,
|
class MainViewModel @Inject constructor(
|
||||||
private val tunnelRepo : TunnelConfigDao,
|
private val application: Application,
|
||||||
private val settingsRepo : SettingsDoa,
|
private val tunnelRepo: TunnelConfigDao,
|
||||||
private val vpnService: VpnService
|
private val settingsRepo: SettingsDoa,
|
||||||
|
private val vpnService: VpnService
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
|
|
||||||
val tunnels get() = tunnelRepo.getAllFlow()
|
val tunnels get() = tunnelRepo.getAllFlow()
|
||||||
|
@ -60,19 +61,25 @@ class MainViewModel @Inject constructor(private val application : Application,
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun validateWatcherServiceState(settings: Settings) {
|
private fun validateWatcherServiceState(settings: Settings) {
|
||||||
val watcherState = ServiceManager.getServiceState(application.applicationContext, WireGuardConnectivityWatcherService::class.java)
|
val watcherState = ServiceManager.getServiceState(
|
||||||
if(settings.isAutoTunnelEnabled && watcherState == ServiceState.STOPPED && settings.defaultTunnel != null) {
|
application.applicationContext,
|
||||||
ServiceManager.startWatcherService(application.applicationContext, settings.defaultTunnel!!)
|
WireGuardConnectivityWatcherService::class.java
|
||||||
|
)
|
||||||
|
if (settings.isAutoTunnelEnabled && watcherState == ServiceState.STOPPED && settings.defaultTunnel != null) {
|
||||||
|
ServiceManager.startWatcherService(
|
||||||
|
application.applicationContext,
|
||||||
|
settings.defaultTunnel!!
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
fun onDelete(tunnel : TunnelConfig) {
|
fun onDelete(tunnel: TunnelConfig) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
if(tunnelRepo.count() == 1L) {
|
if (tunnelRepo.count() == 1L) {
|
||||||
ServiceManager.stopWatcherService(application.applicationContext)
|
ServiceManager.stopWatcherService(application.applicationContext)
|
||||||
val settings = settingsRepo.getAll()
|
val settings = settingsRepo.getAll()
|
||||||
if(settings.isNotEmpty()) {
|
if (settings.isNotEmpty()) {
|
||||||
val setting = settings[0]
|
val setting = settings[0]
|
||||||
setting.defaultTunnel = null
|
setting.defaultTunnel = null
|
||||||
setting.isAutoTunnelEnabled = false
|
setting.isAutoTunnelEnabled = false
|
||||||
|
@ -84,7 +91,7 @@ class MainViewModel @Inject constructor(private val application : Application,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onTunnelStart(tunnelConfig : TunnelConfig) {
|
fun onTunnelStart(tunnelConfig: TunnelConfig) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
stopActiveTunnel()
|
stopActiveTunnel()
|
||||||
startTunnel(tunnelConfig)
|
startTunnel(tunnelConfig)
|
||||||
|
@ -96,8 +103,11 @@ class MainViewModel @Inject constructor(private val application : Application,
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun stopActiveTunnel() {
|
private suspend fun stopActiveTunnel() {
|
||||||
if(ServiceManager.getServiceState(application.applicationContext,
|
if (ServiceManager.getServiceState(
|
||||||
WireGuardTunnelService::class.java, ) == ServiceState.STARTED) {
|
application.applicationContext,
|
||||||
|
WireGuardTunnelService::class.java,
|
||||||
|
) == ServiceState.STARTED
|
||||||
|
) {
|
||||||
onTunnelStop()
|
onTunnelStop()
|
||||||
delay(Constants.TOGGLE_TUNNEL_DELAY)
|
delay(Constants.TOGGLE_TUNNEL_DELAY)
|
||||||
}
|
}
|
||||||
|
@ -107,32 +117,35 @@ class MainViewModel @Inject constructor(private val application : Application,
|
||||||
ServiceManager.stopVpnService(application.applicationContext)
|
ServiceManager.stopVpnService(application.applicationContext)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun validateConfigString(config : String) {
|
private fun validateConfigString(config: String) {
|
||||||
if(!config.contains(application.getString(R.string.config_validation))) {
|
if (!config.contains(application.getString(R.string.config_validation))) {
|
||||||
throw WgTunnelException(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) {
|
||||||
try {
|
try {
|
||||||
validateConfigString(result)
|
validateConfigString(result)
|
||||||
val tunnelConfig = TunnelConfig(name = NumberUtils.generateRandomTunnelName(), wgQuick = result)
|
val tunnelConfig =
|
||||||
|
TunnelConfig(name = NumberUtils.generateRandomTunnelName(), wgQuick = result)
|
||||||
addTunnel(tunnelConfig)
|
addTunnel(tunnelConfig)
|
||||||
} catch (e : WgTunnelException) {
|
} catch (e: WgTunnelException) {
|
||||||
throw WgTunnelException(e.message ?: application.getString(R.string.unknown_error_message))
|
throw WgTunnelException(
|
||||||
|
e.message ?: application.getString(R.string.unknown_error_message)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun validateFileExtension(fileName : String) {
|
private fun validateFileExtension(fileName: String) {
|
||||||
val extension = getFileExtensionFromFileName(fileName)
|
val extension = getFileExtensionFromFileName(fileName)
|
||||||
if(extension != Constants.VALID_FILE_EXTENSION) {
|
if (extension != Constants.VALID_FILE_EXTENSION) {
|
||||||
throw WgTunnelException(application.getString(R.string.file_extension_message))
|
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)
|
||||||
val config = Config.parse(bufferReader)
|
val config = Config.parse(bufferReader)
|
||||||
|
@ -147,30 +160,28 @@ class MainViewModel @Inject constructor(private val application : Application,
|
||||||
?: throw WgTunnelException(application.getString(R.string.stream_failed))
|
?: throw WgTunnelException(application.getString(R.string.stream_failed))
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onTunnelFileSelected(uri : Uri) {
|
fun onTunnelFileSelected(uri: Uri) {
|
||||||
try {
|
try {
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
val fileName = getFileName(application.applicationContext, uri)
|
||||||
val fileName = getFileName(application.applicationContext, uri)
|
validateFileExtension(fileName)
|
||||||
validateFileExtension(fileName)
|
val stream = getInputStreamFromUri(uri)
|
||||||
val stream = getInputStreamFromUri(uri)
|
saveTunnelConfigFromStream(stream, fileName)
|
||||||
saveTunnelConfigFromStream(stream, fileName)
|
} catch (e: Exception) {
|
||||||
}
|
|
||||||
} catch (e : Exception) {
|
|
||||||
throw WgTunnelException(e.message ?: "Error importing file")
|
throw WgTunnelException(e.message ?: "Error importing file")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun addTunnel(tunnelConfig: TunnelConfig) {
|
private suspend fun addTunnel(tunnelConfig: TunnelConfig) {
|
||||||
saveTunnel(tunnelConfig)
|
saveTunnel(tunnelConfig)
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun saveTunnel(tunnelConfig : TunnelConfig) {
|
private suspend fun saveTunnel(tunnelConfig: TunnelConfig) {
|
||||||
tunnelRepo.save(tunnelConfig)
|
tunnelRepo.save(tunnelConfig)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getFileNameByCursor(context: Context, uri: Uri) : String {
|
private fun getFileNameByCursor(context: Context, uri: Uri): String {
|
||||||
val cursor = context.contentResolver.query(uri, null, null, null, null)
|
val cursor = context.contentResolver.query(uri, null, null, null, null)
|
||||||
if(cursor != null) {
|
if (cursor != null) {
|
||||||
cursor.use {
|
cursor.use {
|
||||||
return getDisplayNameByCursor(it)
|
return getDisplayNameByCursor(it)
|
||||||
}
|
}
|
||||||
|
@ -179,16 +190,16 @@ class MainViewModel @Inject constructor(private val application : Application,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getDisplayNameColumnIndex(cursor: Cursor) : Int {
|
private fun getDisplayNameColumnIndex(cursor: Cursor): Int {
|
||||||
val columnIndex = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME)
|
val columnIndex = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME)
|
||||||
if(columnIndex == -1) {
|
if (columnIndex == -1) {
|
||||||
throw WgTunnelException("Cursor out of bounds")
|
throw WgTunnelException("Cursor out of bounds")
|
||||||
}
|
}
|
||||||
return columnIndex
|
return columnIndex
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getDisplayNameByCursor(cursor: Cursor) : String {
|
private fun getDisplayNameByCursor(cursor: Cursor): String {
|
||||||
if(cursor.moveToFirst()) {
|
if (cursor.moveToFirst()) {
|
||||||
val index = getDisplayNameColumnIndex(cursor)
|
val index = getDisplayNameColumnIndex(cursor)
|
||||||
return cursor.getString(index)
|
return cursor.getString(index)
|
||||||
} else {
|
} else {
|
||||||
|
@ -196,7 +207,7 @@ class MainViewModel @Inject constructor(private val application : Application,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun validateUriContentScheme(uri : Uri) {
|
private fun validateUriContentScheme(uri: Uri) {
|
||||||
if (uri.scheme != Constants.URI_CONTENT_SCHEME) {
|
if (uri.scheme != Constants.URI_CONTENT_SCHEME) {
|
||||||
throw WgTunnelException(application.getString(R.string.file_extension_message))
|
throw WgTunnelException(application.getString(R.string.file_extension_message))
|
||||||
}
|
}
|
||||||
|
@ -212,23 +223,25 @@ class MainViewModel @Inject constructor(private val application : Application,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getNameFromFileName(fileName : String) : String {
|
private fun getNameFromFileName(fileName: String): String {
|
||||||
return fileName.substring(0 , fileName.lastIndexOf('.') )
|
return fileName.substring(0, fileName.lastIndexOf('.'))
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getFileExtensionFromFileName(fileName : String) : String {
|
private fun getFileExtensionFromFileName(fileName: String): String {
|
||||||
return try {
|
return try {
|
||||||
fileName.substring(fileName.lastIndexOf('.'))
|
fileName.substring(fileName.lastIndexOf('.'))
|
||||||
} catch (e : Exception) {
|
} catch (e: Exception) {
|
||||||
""
|
""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun onDefaultTunnelChange(selectedTunnel: TunnelConfig?) {
|
suspend fun onDefaultTunnelChange(selectedTunnel: TunnelConfig?) {
|
||||||
if(selectedTunnel != null) {
|
if (selectedTunnel != null) {
|
||||||
_settings.emit(_settings.value.copy(
|
_settings.emit(
|
||||||
defaultTunnel = selectedTunnel.toString()
|
_settings.value.copy(
|
||||||
))
|
defaultTunnel = selectedTunnel.toString()
|
||||||
|
)
|
||||||
|
)
|
||||||
settingsRepo.save(_settings.value)
|
settingsRepo.save(_settings.value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -44,12 +44,15 @@ import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.ExperimentalComposeUiApi
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.focus.FocusRequester
|
import androidx.compose.ui.focus.FocusRequester
|
||||||
import androidx.compose.ui.focus.focusRequester
|
import androidx.compose.ui.focus.focusRequester
|
||||||
|
import androidx.compose.ui.focus.onFocusChanged
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.platform.LocalFocusManager
|
import androidx.compose.ui.platform.LocalFocusManager
|
||||||
|
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.font.FontStyle
|
import androidx.compose.ui.text.font.FontStyle
|
||||||
import androidx.compose.ui.text.input.ImeAction
|
import androidx.compose.ui.text.input.ImeAction
|
||||||
|
@ -71,7 +74,7 @@ import kotlinx.coroutines.launch
|
||||||
|
|
||||||
@OptIn(
|
@OptIn(
|
||||||
ExperimentalPermissionsApi::class,
|
ExperimentalPermissionsApi::class,
|
||||||
ExperimentalLayoutApi::class
|
ExperimentalLayoutApi::class, ExperimentalComposeUiApi::class
|
||||||
)
|
)
|
||||||
@Composable
|
@Composable
|
||||||
fun SettingsScreen(
|
fun SettingsScreen(
|
||||||
|
@ -84,6 +87,7 @@ fun SettingsScreen(
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val focusManager = LocalFocusManager.current
|
val focusManager = LocalFocusManager.current
|
||||||
|
val keyboardController = LocalSoftwareKeyboardController.current
|
||||||
val interactionSource = remember { MutableInteractionSource() }
|
val interactionSource = remember { MutableInteractionSource() }
|
||||||
|
|
||||||
val settings by viewModel.settings.collectAsStateWithLifecycle()
|
val settings by viewModel.settings.collectAsStateWithLifecycle()
|
||||||
|
@ -264,7 +268,9 @@ fun SettingsScreen(
|
||||||
value = currentText,
|
value = currentText,
|
||||||
onValueChange = { currentText = it },
|
onValueChange = { currentText = it },
|
||||||
label = { Text(stringResource(R.string.add_trusted_ssid)) },
|
label = { Text(stringResource(R.string.add_trusted_ssid)) },
|
||||||
modifier = Modifier.padding(start = screenPadding, top = 5.dp).focusRequester(focusRequester),
|
modifier = Modifier.padding(start = screenPadding, top = 5.dp).focusRequester(focusRequester).onFocusChanged {
|
||||||
|
keyboardController?.hide()
|
||||||
|
},
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
keyboardOptions = KeyboardOptions(
|
keyboardOptions = KeyboardOptions(
|
||||||
capitalization = KeyboardCapitalization.None,
|
capitalization = KeyboardCapitalization.None,
|
||||||
|
|
|
@ -124,4 +124,7 @@
|
||||||
<string name="preshared_key">Pre-shared key</string>
|
<string name="preshared_key">Pre-shared key</string>
|
||||||
<string name="seconds">seconds</string>
|
<string name="seconds">seconds</string>
|
||||||
<string name="persistent_keepalive">Persistent keepalive</string>
|
<string name="persistent_keepalive">Persistent keepalive</string>
|
||||||
|
<string name="cancel">Cancel</string>
|
||||||
|
<string name="primary_tunnel_change">Primary tunnel change</string>
|
||||||
|
<string name="primary_tunnnel_change_question">Would you like to make this your primary tunnel?</string>
|
||||||
</resources>
|
</resources>
|
|
@ -1,6 +1,6 @@
|
||||||
[versions]
|
[versions]
|
||||||
accompanist = "0.31.2-alpha"
|
accompanist = "0.31.2-alpha"
|
||||||
activityCompose = "1.7.2"
|
activityCompose = "1.8.0"
|
||||||
androidx-junit = "1.1.5"
|
androidx-junit = "1.1.5"
|
||||||
appcompat = "1.6.1"
|
appcompat = "1.6.1"
|
||||||
coreKtx = "1.12.0"
|
coreKtx = "1.12.0"
|
||||||
|
@ -12,18 +12,18 @@ hiltNavigationCompose = "1.0.0"
|
||||||
junit = "4.13.2"
|
junit = "4.13.2"
|
||||||
kotlinx-serialization-json = "1.5.1"
|
kotlinx-serialization-json = "1.5.1"
|
||||||
lifecycle-runtime-compose = "2.6.2"
|
lifecycle-runtime-compose = "2.6.2"
|
||||||
material-icons-extended = "1.5.2"
|
material-icons-extended = "1.5.3"
|
||||||
material3 = "1.1.2"
|
material3 = "1.1.2"
|
||||||
navigationCompose = "2.7.3"
|
navigationCompose = "2.7.4"
|
||||||
roomVersion = "2.6.0-rc01"
|
roomVersion = "2.6.0-rc01"
|
||||||
timber = "5.0.1"
|
timber = "5.0.1"
|
||||||
tunnel = "1.0.20230706"
|
tunnel = "1.0.20230706"
|
||||||
androidGradlePlugin = "8.3.0-alpha06"
|
androidGradlePlugin = "8.3.0-alpha06"
|
||||||
kotlin="1.9.10"
|
kotlin="1.9.10"
|
||||||
ksp="1.9.10-1.0.13"
|
ksp="1.9.10-1.0.13"
|
||||||
composeBom="2023.09.02"
|
composeBom="2023.10.00"
|
||||||
firebaseBom="32.3.1"
|
firebaseBom="32.3.1"
|
||||||
compose="1.5.2"
|
compose="1.5.3"
|
||||||
crashlytics="18.4.3"
|
crashlytics="18.4.3"
|
||||||
analytics="21.3.0"
|
analytics="21.3.0"
|
||||||
composeCompiler="1.5.3"
|
composeCompiler="1.5.3"
|
||||||
|
@ -40,6 +40,7 @@ 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-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" }
|
||||||
androidx-room-runtime = { module = "androidx.room:room-runtime", version.ref = "roomVersion" }
|
androidx-room-runtime = { module = "androidx.room:room-runtime", version.ref = "roomVersion" }
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
#Mon Apr 24 22:46:45 EDT 2023
|
#Wed Oct 11 22:39:21 EDT 2023
|
||||||
distributionBase=GRADLE_USER_HOME
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.3-bin.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-8.3-bin.zip
|
||||||
|
|
Loading…
Reference in New Issue