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

View File

@ -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"
} }

View File

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

View File

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

View File

@ -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,8 +71,7 @@ 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 {
@ -102,10 +102,11 @@ 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()

View File

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

View File

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

View File

@ -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
@ -75,6 +78,7 @@ 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}")
@ -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())
} }
} }
} }

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 @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()
}
),
) )
} }

View File

@ -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 = {

View File

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

View File

@ -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,15 +135,35 @@ 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)
/* 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 { try {
viewModel.onTunnelFileSelected(result) viewModel.onTunnelFileSelected(data)
} catch (e : Exception) { } catch (e : Exception) {
showSnackbarMessage(e.message ?: "Unknown error occurred") showSnackbarMessage(e.message ?: "Unknown error occurred")
} }
} }
}
val scanLauncher = rememberLauncherForActivityResult( val scanLauncher = rememberLauncherForActivityResult(
contract = ScanContract(), contract = ScanContract(),
@ -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,6 +289,7 @@ fun MainScreen(
modifier = Modifier.padding(10.dp) modifier = Modifier.padding(10.dp)
) )
} }
if(!WireGuardAutoTunnel.isRunningOnAndroidTv(context)) {
Divider() Divider()
Row(modifier = Modifier Row(modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
@ -290,6 +317,7 @@ fun MainScreen(
modifier = Modifier.padding(10.dp) modifier = Modifier.padding(10.dp)
) )
} }
}
Divider() Divider()
Row( Row(
modifier = Modifier modifier = Modifier
@ -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)

View File

@ -35,7 +35,8 @@ import javax.inject.Inject
@HiltViewModel @HiltViewModel
class MainViewModel @Inject constructor(private val application : Application, class MainViewModel @Inject constructor(
private val application: Application,
private val tunnelRepo: TunnelConfigDao, private val tunnelRepo: TunnelConfigDao,
private val settingsRepo: SettingsDoa, private val settingsRepo: SettingsDoa,
private val vpnService: VpnService private val vpnService: VpnService
@ -60,9 +61,15 @@ 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(
application.applicationContext,
WireGuardConnectivityWatcherService::class.java
)
if (settings.isAutoTunnelEnabled && watcherState == ServiceState.STOPPED && settings.defaultTunnel != null) { if (settings.isAutoTunnelEnabled && watcherState == ServiceState.STOPPED && settings.defaultTunnel != null) {
ServiceManager.startWatcherService(application.applicationContext, settings.defaultTunnel!!) ServiceManager.startWatcherService(
application.applicationContext,
settings.defaultTunnel!!
)
} }
} }
@ -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)
} }
@ -117,10 +127,13 @@ class MainViewModel @Inject constructor(private val application : Application,
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)
)
} }
} }
} }
@ -149,12 +162,10 @@ class MainViewModel @Inject constructor(private val application : Application,
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")
} }
@ -226,9 +237,11 @@ class MainViewModel @Inject constructor(private val application : Application,
suspend fun onDefaultTunnelChange(selectedTunnel: TunnelConfig?) { suspend fun onDefaultTunnelChange(selectedTunnel: TunnelConfig?) {
if (selectedTunnel != null) { if (selectedTunnel != null) {
_settings.emit(_settings.value.copy( _settings.emit(
_settings.value.copy(
defaultTunnel = selectedTunnel.toString() defaultTunnel = selectedTunnel.toString()
)) )
)
settingsRepo.save(_settings.value) settingsRepo.save(_settings.value)
} }
} }

View File

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

View File

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

View File

@ -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" }

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