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:
Zane Schepke 2023-10-12 20:20:53 -04:00
parent 2912238f27
commit 321730536d
18 changed files with 246 additions and 193 deletions

View File

@ -14,8 +14,8 @@ android {
applicationId = "com.zaneschepke.wireguardautotunnel"
minSdk = 26
targetSdk = 34
versionCode = 31000
versionName = "3.1.0"
versionCode = 31100
versionName = "3.1.1"
multiDexEnabled = true
@ -81,6 +81,8 @@ val generalImplementation by configurations
dependencies {
implementation(libs.androidx.core.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(platform(libs.androidx.compose.bom))
implementation(libs.androidx.compose.ui)

View File

@ -12,5 +12,6 @@ object Constants {
const val URI_CONTENT_SCHEME = "content"
const val URI_PACKAGE_SCHEME = "package"
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"
}

View File

@ -1,22 +1,24 @@
package com.zaneschepke.wireguardautotunnel.service.foreground
import android.app.Service
import android.content.Intent
import android.os.Bundle
import android.os.IBinder
import androidx.lifecycle.LifecycleService
import timber.log.Timber
open class ForegroundService : Service() {
open class ForegroundService : LifecycleService() {
private var isServiceStarted = false
override fun onBind(intent: Intent): IBinder? {
super.onBind(intent)
// We don't provide binding, so return null
return null
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
super.onStartCommand(intent, flags, startId)
Timber.d("onStartCommand executed with startId: $startId")
if (intent != null) {
val action = intent.action

View File

@ -7,6 +7,7 @@ import android.content.Intent
import android.os.Bundle
import android.os.PowerManager
import android.os.SystemClock
import androidx.lifecycle.lifecycleScope
import com.wireguard.android.backend.Tunnel
import com.zaneschepke.wireguardautotunnel.Constants
import com.zaneschepke.wireguardautotunnel.R
@ -66,7 +67,7 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
override fun onCreate() {
super.onCreate()
CoroutineScope(Dispatchers.Main).launch {
lifecycleScope.launch(Dispatchers.Main) {
launchWatcherNotification()
}
}
@ -122,6 +123,9 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
wakeLock =
(getSystemService(Context.POWER_SERVICE) as PowerManager).run {
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()
}
}
@ -134,7 +138,7 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
}
private fun startWatcherJob() {
watcherJob = CoroutineScope(Dispatchers.IO).launch {
watcherJob = lifecycleScope.launch(Dispatchers.IO) {
val settings = settingsRepo.getAll();
if(settings.isNotEmpty()) {
setting = settings[0]

View File

@ -3,6 +3,7 @@ package com.zaneschepke.wireguardautotunnel.service.foreground
import android.app.PendingIntent
import android.content.Intent
import android.os.Bundle
import androidx.lifecycle.lifecycleScope
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.receiver.NotificationActionReceiver
import com.zaneschepke.wireguardautotunnel.repository.SettingsDoa
@ -38,7 +39,7 @@ class WireGuardTunnelService : ForegroundService() {
override fun onCreate() {
super.onCreate()
CoroutineScope(Dispatchers.Main).launch {
lifecycleScope.launch(Dispatchers.Main) {
launchVpnStartingNotification()
}
}
@ -48,7 +49,7 @@ class WireGuardTunnelService : ForegroundService() {
launchVpnStartingNotification()
val tunnelConfigString = extras?.getString(getString(R.string.tunnel_extras_key))
cancelJob()
job = CoroutineScope(Dispatchers.IO).launch {
job = lifecycleScope.launch(Dispatchers.IO) {
if(tunnelConfigString != null) {
try {
val tunnelConfig = TunnelConfig.from(tunnelConfigString)
@ -70,32 +71,32 @@ class WireGuardTunnelService : ForegroundService() {
}
}
}
}
CoroutineScope(job).launch {
var didShowConnected = false
var didShowFailedHandshakeNotification = false
vpnService.handshakeStatus.collect {
when(it) {
HandshakeStatus.NOT_STARTED -> {
}
HandshakeStatus.NEVER_CONNECTED -> {
if(!didShowFailedHandshakeNotification) {
launchVpnConnectionFailedNotification(getString(R.string.initial_connection_failure_message))
didShowFailedHandshakeNotification = true
didShowConnected = false
launch {
var didShowConnected = false
var didShowFailedHandshakeNotification = false
vpnService.handshakeStatus.collect {
when(it) {
HandshakeStatus.NOT_STARTED -> {
}
}
HandshakeStatus.HEALTHY -> {
if(!didShowConnected) {
launchVpnConnectedNotification()
didShowConnected = true
HandshakeStatus.NEVER_CONNECTED -> {
if(!didShowFailedHandshakeNotification) {
launchVpnConnectionFailedNotification(getString(R.string.initial_connection_failure_message))
didShowFailedHandshakeNotification = true
didShowConnected = false
}
}
}
HandshakeStatus.UNHEALTHY -> {
if(!didShowFailedHandshakeNotification) {
launchVpnConnectionFailedNotification(getString(R.string.lost_connection_failure_message))
didShowFailedHandshakeNotification = true
didShowConnected = false
HandshakeStatus.HEALTHY -> {
if(!didShowConnected) {
launchVpnConnectedNotification()
didShowConnected = true
}
}
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?) {
super.stopService(extras)
CoroutineScope(Dispatchers.IO).launch {
lifecycleScope.launch(Dispatchers.IO) {
vpnService.stopTunnel()
}
cancelJob()

View File

@ -13,6 +13,7 @@ import com.zaneschepke.wireguardautotunnel.util.WgTunnelException
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject
@ -62,6 +63,11 @@ class ShortcutsActivity : ComponentActivity() {
finish()
}
override fun onDestroy() {
super.onDestroy()
scope.cancel()
}
private suspend fun getSettings() : Settings {
val settings = settingsRepo.getAll()
return if (settings.isNotEmpty()) {

View File

@ -55,6 +55,11 @@ class TunnelControlTile : TileService() {
cancelJob()
}
override fun onDestroy() {
super.onDestroy()
scope.cancel()
}
override fun onClick() {
super.onClick()
unlockAndRun {

View File

@ -11,6 +11,7 @@ import com.zaneschepke.wireguardautotunnel.util.NumberUtils
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.cancel
import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableSharedFlow
@ -46,6 +47,8 @@ class WireGuardTunnel @Inject constructor(private val backend : Backend,
override val handshakeStatus: SharedFlow<HandshakeStatus>
get() = _handshakeStatus.asSharedFlow()
private val scope = CoroutineScope(Dispatchers.IO);
private lateinit var statsJob : Job
@ -70,11 +73,12 @@ class WireGuardTunnel @Inject constructor(private val backend : Backend,
return _tunnelName.value
}
override suspend fun stopTunnel() {
override suspend fun stopTunnel() {
try {
if(getState() == Tunnel.State.UP) {
val state = backend.setState(this, Tunnel.State.DOWN, null)
_state.emit(state)
scope.cancel()
}
} catch (e : BackendException) {
Timber.e("Failed to stop tunnel with error: ${e.message}")
@ -89,7 +93,7 @@ class WireGuardTunnel @Inject constructor(private val backend : Backend,
val tunnel = this;
_state.tryEmit(state)
if(state == Tunnel.State.UP) {
statsJob = CoroutineScope(Dispatchers.IO).launch {
statsJob = scope.launch {
val handshakeMap = HashMap<Key, Long>()
var neverHadHandshakeCounter = 0
while (true) {
@ -128,4 +132,6 @@ class WireGuardTunnel @Inject constructor(private val backend : Backend,
_lastHandshake.tryEmit(emptyMap())
}
}
}

View File

@ -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
)

View File

@ -12,7 +12,7 @@ import androidx.compose.ui.text.input.KeyboardCapitalization
@Composable
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(
modifier = modifier,
value = value,
@ -29,10 +29,6 @@ fun
capitalization = KeyboardCapitalization.None,
imeAction = ImeAction.Done
),
keyboardActions = KeyboardActions(
onDone = {
onDone()
}
),
keyboardActions = keyboardActions,
)
}

View File

@ -1,7 +1,6 @@
package com.zaneschepke.wireguardautotunnel.ui.common.config
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
@ -14,7 +13,7 @@ import androidx.compose.ui.unit.Dp
@Composable
fun ConfigurationToggle(label : String, enabled : Boolean, checked : Boolean, padding : Dp,
onCheckChanged : () -> Unit) {
onCheckChanged : () -> Unit, modifier : Modifier = Modifier) {
Row(
modifier = Modifier
.fillMaxWidth()
@ -24,6 +23,7 @@ fun ConfigurationToggle(label : String, enabled : Boolean, checked : Boolean, pa
) {
Text(label)
Switch(
modifier = modifier,
enabled = enabled,
checked = checked,
onCheckedChange = {

View File

@ -60,7 +60,6 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.ClipboardManager
import androidx.compose.ui.platform.LocalClipboardManager
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.AnnotatedString
@ -96,7 +95,6 @@ fun ConfigScreen(
) {
val context = LocalContext.current
val focusManager = LocalFocusManager.current
val scope = rememberCoroutineScope()
val clipboardManager: ClipboardManager = LocalClipboardManager.current
val keyboardController = LocalSoftwareKeyboardController.current
@ -111,11 +109,24 @@ fun ConfigScreen(
val proxyPeers by viewModel.proxyPeers.collectAsStateWithLifecycle()
val proxyInterface by viewModel.interfaceProxy.collectAsStateWithLifecycle()
var showApplicationsDialog by remember { mutableStateOf(false) }
val baseTextBoxModifier = Modifier.onFocusChanged {
keyboardController?.hide()
}
val keyboardActions = KeyboardActions(
onDone = {
focusManager.clearFocus()
//focusManager.clearFocus()
keyboardController?.hide()
},
onNext = {
keyboardController?.hide()
},
onPrevious = {
keyboardController?.hide()
},
onGo = {
keyboardController?.hide(
)
}
)
@ -380,16 +391,13 @@ fun ConfigScreen(
onValueChange = { value ->
viewModel.onTunnelNameChange(value)
},
onDone = {
focusManager.clearFocus()
keyboardController?.hide()
},
keyboardActions = keyboardActions,
label = stringResource(R.string.name),
hint = stringResource(R.string.tunnel_name).lowercase(),
modifier = Modifier.fillMaxWidth().focusRequester(focusRequester)
modifier = baseTextBoxModifier.fillMaxWidth().focusRequester(focusRequester)
)
OutlinedTextField(
modifier = Modifier.fillMaxWidth(),
modifier = baseTextBoxModifier.fillMaxWidth(),
value = proxyInterface.privateKey,
visualTransformation = PasswordVisualTransformation(),
enabled = id == Constants.MANUAL_TUNNEL_CONFIG_ID,
@ -416,7 +424,7 @@ fun ConfigScreen(
keyboardActions = keyboardActions
)
OutlinedTextField(
modifier = Modifier.fillMaxWidth().focusRequester(FocusRequester.Default),
modifier = baseTextBoxModifier.fillMaxWidth().focusRequester(FocusRequester.Default),
value = proxyInterface.publicKey,
enabled = false,
onValueChange = {},
@ -445,52 +453,40 @@ fun ConfigScreen(
onValueChange = { value ->
viewModel.onAddressesChanged(value)
},
onDone = {
focusManager.clearFocus()
keyboardController?.hide()
},
keyboardActions = keyboardActions,
label = stringResource(R.string.addresses),
hint = stringResource(R.string.comma_separated_list),
modifier = Modifier
modifier = baseTextBoxModifier
.fillMaxWidth(3 / 5f)
.padding(end = 5.dp)
)
ConfigurationTextBox(
value = proxyInterface.listenPort,
onValueChange = { value -> viewModel.onListenPortChanged(value) },
onDone = {
focusManager.clearFocus()
keyboardController?.hide()
},
keyboardActions = keyboardActions,
label = stringResource(R.string.listen_port),
hint = stringResource(R.string.random),
modifier = Modifier.width(IntrinsicSize.Min)
modifier = baseTextBoxModifier.width(IntrinsicSize.Min)
)
}
Row(modifier = Modifier.fillMaxWidth()) {
ConfigurationTextBox(
value = proxyInterface.dnsServers,
onValueChange = { value -> viewModel.onDnsServersChanged(value) },
onDone = {
focusManager.clearFocus()
keyboardController?.hide()
},
keyboardActions = keyboardActions,
label = stringResource(R.string.dns_servers),
hint = stringResource(R.string.comma_separated_list),
modifier = Modifier
modifier = baseTextBoxModifier
.fillMaxWidth(3 / 5f)
.padding(end = 5.dp)
)
ConfigurationTextBox(
value = proxyInterface.mtu,
onValueChange = { value -> viewModel.onMtuChanged(value) },
onDone = {
focusManager.clearFocus()
keyboardController?.hide()
},
keyboardActions = keyboardActions,
label = stringResource(R.string.mtu),
hint = stringResource(R.string.auto),
modifier = Modifier.width(IntrinsicSize.Min)
modifier = baseTextBoxModifier.width(IntrinsicSize.Min)
)
}
Row(
@ -556,13 +552,10 @@ fun ConfigScreen(
value
)
},
onDone = {
focusManager.clearFocus()
keyboardController?.hide()
},
keyboardActions = keyboardActions,
label = stringResource(R.string.public_key),
hint = stringResource(R.string.base64_key),
modifier = Modifier.fillMaxWidth()
modifier = baseTextBoxModifier.fillMaxWidth()
)
ConfigurationTextBox(
value = peer.preSharedKey,
@ -572,16 +565,13 @@ fun ConfigScreen(
value
)
},
onDone = {
focusManager.clearFocus()
keyboardController?.hide()
},
keyboardActions = keyboardActions,
label = stringResource(R.string.preshared_key),
hint = stringResource(R.string.optional),
modifier = Modifier.fillMaxWidth()
modifier = baseTextBoxModifier.fillMaxWidth()
)
OutlinedTextField(
modifier = Modifier.fillMaxWidth(),
modifier = baseTextBoxModifier.fillMaxWidth(),
value = peer.persistentKeepalive,
enabled = true,
onValueChange = { value ->
@ -602,16 +592,13 @@ fun ConfigScreen(
value
)
},
onDone = {
focusManager.clearFocus()
keyboardController?.hide()
},
keyboardActions = keyboardActions,
label = stringResource(R.string.endpoint),
hint = stringResource(R.string.endpoint).lowercase(),
modifier = Modifier.fillMaxWidth()
modifier = baseTextBoxModifier.fillMaxWidth()
)
OutlinedTextField(
modifier = Modifier.fillMaxWidth(),
modifier = baseTextBoxModifier.fillMaxWidth(),
value = peer.allowedIps,
enabled = true,
onValueChange = { value ->

View File

@ -1,6 +1,10 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.main
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.result.contract.ActivityResultContracts
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.theme.brickRed
import com.zaneschepke.wireguardautotunnel.ui.theme.mint
import com.zaneschepke.wireguardautotunnel.util.WgTunnelException
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter")
@ -129,13 +135,33 @@ fun MainScreen(
}
}
val pickFileLauncher = rememberLauncherForActivityResult(
ActivityResultContracts.GetContent()
) { result -> if (result != null)
try {
viewModel.onTunnelFileSelected(result)
} catch (e : Exception) {
showSnackbarMessage(e.message ?: "Unknown error occurred")
val tunnelFileImportResultLauncher = rememberLauncherForActivityResult(object : ActivityResultContracts.GetContent() {
override fun createIntent(context: Context, input: String): Intent {
val intent = super.createIntent(context, input)
/* AndroidTV now comes with stubs that do nothing but display a Toast less helpful than
* what we can do, so detect this and throw an exception that we can catch later. */
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
}
})
{ Text(text = "Okay") }
{ Text(text = stringResource(R.string.okay)) }
},
dismissButton = {
TextButton(onClick = {
showPrimaryChangeAlertDialog = false
})
{ Text(text = "Cancel") }
{ Text(text = stringResource(R.string.cancel)) }
},
title = { Text(text = "Primary tunnel change") },
text = { Text(text = "Would you like to make this your primary tunnel?") }
title = { Text(text = stringResource(R.string.primary_tunnel_change)) },
text = { Text(text = stringResource(R.string.primary_tunnnel_change_question)) }
)
}
@ -246,9 +272,9 @@ fun MainScreen(
.clickable {
showBottomSheet = false
try {
pickFileLauncher.launch(Constants.ALLOWED_FILE_TYPES)
} catch (_: Exception) {
showSnackbarMessage("No file explorer")
tunnelFileImportResultLauncher.launch(Constants.ALLOWED_FILE_TYPES)
} catch (e : Exception) {
showSnackbarMessage(e.message!!)
}
}
.padding(10.dp)
@ -263,32 +289,34 @@ fun MainScreen(
modifier = Modifier.padding(10.dp)
)
}
Divider()
Row(modifier = Modifier
.fillMaxWidth()
.clickable {
scope.launch {
showBottomSheet = false
val scanOptions = ScanOptions()
scanOptions.setDesiredBarcodeFormats(ScanOptions.QR_CODE)
scanOptions.setOrientationLocked(true)
scanOptions.setPrompt(context.getString(R.string.scanning_qr))
scanOptions.setBeepEnabled(false)
scanOptions.captureActivity = CaptureActivityPortrait::class.java
scanLauncher.launch(scanOptions)
if(!WireGuardAutoTunnel.isRunningOnAndroidTv(context)) {
Divider()
Row(modifier = Modifier
.fillMaxWidth()
.clickable {
scope.launch {
showBottomSheet = false
val scanOptions = ScanOptions()
scanOptions.setDesiredBarcodeFormats(ScanOptions.QR_CODE)
scanOptions.setOrientationLocked(true)
scanOptions.setPrompt(context.getString(R.string.scanning_qr))
scanOptions.setBeepEnabled(false)
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()
Row(
@ -440,6 +468,7 @@ fun MainScreen(
)
}
Switch(
modifier = Modifier.focusRequester(focusRequester),
checked = (state == Tunnel.State.UP && tunnel.name == tunnelName),
onCheckedChange = { checked ->
onTunnelToggle(checked, tunnel)

View File

@ -35,10 +35,11 @@ import javax.inject.Inject
@HiltViewModel
class MainViewModel @Inject constructor(private val application : Application,
private val tunnelRepo : TunnelConfigDao,
private val settingsRepo : SettingsDoa,
private val vpnService: VpnService
class MainViewModel @Inject constructor(
private val application: Application,
private val tunnelRepo: TunnelConfigDao,
private val settingsRepo: SettingsDoa,
private val vpnService: VpnService
) : ViewModel() {
val tunnels get() = tunnelRepo.getAllFlow()
@ -60,19 +61,25 @@ class MainViewModel @Inject constructor(private val application : Application,
}
private fun validateWatcherServiceState(settings: Settings) {
val watcherState = ServiceManager.getServiceState(application.applicationContext, WireGuardConnectivityWatcherService::class.java)
if(settings.isAutoTunnelEnabled && watcherState == ServiceState.STOPPED && settings.defaultTunnel != null) {
ServiceManager.startWatcherService(application.applicationContext, settings.defaultTunnel!!)
val watcherState = ServiceManager.getServiceState(
application.applicationContext,
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 {
if(tunnelRepo.count() == 1L) {
if (tunnelRepo.count() == 1L) {
ServiceManager.stopWatcherService(application.applicationContext)
val settings = settingsRepo.getAll()
if(settings.isNotEmpty()) {
if (settings.isNotEmpty()) {
val setting = settings[0]
setting.defaultTunnel = null
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 {
stopActiveTunnel()
startTunnel(tunnelConfig)
@ -96,8 +103,11 @@ class MainViewModel @Inject constructor(private val application : Application,
}
private suspend fun stopActiveTunnel() {
if(ServiceManager.getServiceState(application.applicationContext,
WireGuardTunnelService::class.java, ) == ServiceState.STARTED) {
if (ServiceManager.getServiceState(
application.applicationContext,
WireGuardTunnelService::class.java,
) == ServiceState.STARTED
) {
onTunnelStop()
delay(Constants.TOGGLE_TUNNEL_DELAY)
}
@ -107,32 +117,35 @@ class MainViewModel @Inject constructor(private val application : Application,
ServiceManager.stopVpnService(application.applicationContext)
}
private fun validateConfigString(config : String) {
if(!config.contains(application.getString(R.string.config_validation))) {
private fun validateConfigString(config: String) {
if (!config.contains(application.getString(R.string.config_validation))) {
throw WgTunnelException(application.getString(R.string.config_validation))
}
}
fun onTunnelQrResult(result : String) {
fun onTunnelQrResult(result: String) {
viewModelScope.launch(Dispatchers.IO) {
try {
validateConfigString(result)
val tunnelConfig = TunnelConfig(name = NumberUtils.generateRandomTunnelName(), wgQuick = result)
val tunnelConfig =
TunnelConfig(name = NumberUtils.generateRandomTunnelName(), wgQuick = result)
addTunnel(tunnelConfig)
} catch (e : WgTunnelException) {
throw WgTunnelException(e.message ?: application.getString(R.string.unknown_error_message))
} catch (e: WgTunnelException) {
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)
if(extension != Constants.VALID_FILE_EXTENSION) {
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) {
val bufferReader = stream.bufferedReader(charset = Charsets.UTF_8)
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))
}
fun onTunnelFileSelected(uri : Uri) {
fun onTunnelFileSelected(uri: Uri) {
try {
viewModelScope.launch(Dispatchers.IO) {
val fileName = getFileName(application.applicationContext, uri)
validateFileExtension(fileName)
val stream = getInputStreamFromUri(uri)
saveTunnelConfigFromStream(stream, fileName)
}
} catch (e : Exception) {
val fileName = getFileName(application.applicationContext, uri)
validateFileExtension(fileName)
val stream = getInputStreamFromUri(uri)
saveTunnelConfigFromStream(stream, fileName)
} catch (e: Exception) {
throw WgTunnelException(e.message ?: "Error importing file")
}
}
private suspend fun addTunnel(tunnelConfig: TunnelConfig) {
private suspend fun addTunnel(tunnelConfig: TunnelConfig) {
saveTunnel(tunnelConfig)
}
private suspend fun saveTunnel(tunnelConfig : TunnelConfig) {
private suspend fun saveTunnel(tunnelConfig: 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)
if(cursor != null) {
if (cursor != null) {
cursor.use {
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)
if(columnIndex == -1) {
if (columnIndex == -1) {
throw WgTunnelException("Cursor out of bounds")
}
return columnIndex
}
private fun getDisplayNameByCursor(cursor: Cursor) : String {
if(cursor.moveToFirst()) {
private fun getDisplayNameByCursor(cursor: Cursor): String {
if (cursor.moveToFirst()) {
val index = getDisplayNameColumnIndex(cursor)
return cursor.getString(index)
} 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) {
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 {
return fileName.substring(0 , fileName.lastIndexOf('.') )
private fun getNameFromFileName(fileName: String): String {
return fileName.substring(0, fileName.lastIndexOf('.'))
}
private fun getFileExtensionFromFileName(fileName : String) : String {
private fun getFileExtensionFromFileName(fileName: String): String {
return try {
fileName.substring(fileName.lastIndexOf('.'))
} catch (e : Exception) {
} catch (e: Exception) {
""
}
}
suspend fun onDefaultTunnelChange(selectedTunnel: TunnelConfig?) {
if(selectedTunnel != null) {
_settings.emit(_settings.value.copy(
defaultTunnel = selectedTunnel.toString()
))
if (selectedTunnel != null) {
_settings.emit(
_settings.value.copy(
defaultTunnel = selectedTunnel.toString()
)
)
settingsRepo.save(_settings.value)
}
}

View File

@ -44,12 +44,15 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
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.platform.LocalContext
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.input.ImeAction
@ -71,7 +74,7 @@ import kotlinx.coroutines.launch
@OptIn(
ExperimentalPermissionsApi::class,
ExperimentalLayoutApi::class
ExperimentalLayoutApi::class, ExperimentalComposeUiApi::class
)
@Composable
fun SettingsScreen(
@ -84,6 +87,7 @@ fun SettingsScreen(
val scope = rememberCoroutineScope()
val context = LocalContext.current
val focusManager = LocalFocusManager.current
val keyboardController = LocalSoftwareKeyboardController.current
val interactionSource = remember { MutableInteractionSource() }
val settings by viewModel.settings.collectAsStateWithLifecycle()
@ -264,7 +268,9 @@ fun SettingsScreen(
value = currentText,
onValueChange = { currentText = it },
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,
keyboardOptions = KeyboardOptions(
capitalization = KeyboardCapitalization.None,

View File

@ -124,4 +124,7 @@
<string name="preshared_key">Pre-shared key</string>
<string name="seconds">seconds</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>

View File

@ -1,6 +1,6 @@
[versions]
accompanist = "0.31.2-alpha"
activityCompose = "1.7.2"
activityCompose = "1.8.0"
androidx-junit = "1.1.5"
appcompat = "1.6.1"
coreKtx = "1.12.0"
@ -12,18 +12,18 @@ hiltNavigationCompose = "1.0.0"
junit = "4.13.2"
kotlinx-serialization-json = "1.5.1"
lifecycle-runtime-compose = "2.6.2"
material-icons-extended = "1.5.2"
material-icons-extended = "1.5.3"
material3 = "1.1.2"
navigationCompose = "2.7.3"
navigationCompose = "2.7.4"
roomVersion = "2.6.0-rc01"
timber = "5.0.1"
tunnel = "1.0.20230706"
androidGradlePlugin = "8.3.0-alpha06"
kotlin="1.9.10"
ksp="1.9.10-1.0.13"
composeBom="2023.09.02"
composeBom="2023.10.00"
firebaseBom="32.3.1"
compose="1.5.2"
compose="1.5.3"
crashlytics="18.4.3"
analytics="21.3.0"
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" }
#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-ktx = { module = "androidx.room:room-ktx", version.ref = "roomVersion" }
androidx-room-runtime = { module = "androidx.room:room-runtime", version.ref = "roomVersion" }

View File

@ -1,4 +1,4 @@
#Mon Apr 24 22:46:45 EDT 2023
#Wed Oct 11 22:39:21 EDT 2023
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.3-bin.zip