fix: auto-tunnel screen not loading without wifi

Fixes auto tunnel screen failing to load if you haven't connected to wifi once.

Fixes import via url.

Closes #1108
Closes #1105
This commit is contained in:
Zane Schepke
2025-12-19 11:30:39 -05:00
parent 394188b55f
commit eac674c996
4 changed files with 51 additions and 35 deletions
@@ -1,42 +1,47 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.tunnels.components package com.zaneschepke.wireguardautotunnel.ui.screens.tunnels.components
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.material3.MaterialTheme
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.zaneschepke.wireguardautotunnel.R import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.ui.common.dialog.InfoDialog
import com.zaneschepke.wireguardautotunnel.ui.common.textbox.ConfigurationTextBox
@Composable @Composable
fun UrlImportDialog(onDismiss: () -> Unit, onConfirm: (String) -> Unit) { fun UrlImportDialog(onDismiss: () -> Unit, onConfirm: (String) -> Unit) {
var url by remember { mutableStateOf("") } var url by remember { mutableStateOf("") }
var isError by remember { mutableStateOf(false) }
AlertDialog( LaunchedEffect(url) { isError = false }
onDismissRequest = onDismiss,
title = { Text(stringResource(R.string.add_from_url)) }, InfoDialog(
text = { onDismiss = onDismiss,
Column(modifier = Modifier.fillMaxWidth().padding(vertical = 8.dp)) { title = stringResource(R.string.add_from_url),
OutlinedTextField( body = {
Column(verticalArrangement = Arrangement.spacedBy(24.dp)) {
Text(
stringResource(R.string.import_url_description),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurface,
)
ConfigurationTextBox(
value = url, value = url,
label = stringResource(R.string.enter_config_url),
hint = stringResource(R.string.example_import_url),
onValueChange = { url = it }, onValueChange = { url = it },
label = { Text(stringResource(R.string.enter_config_url)) }, isError = isError,
modifier = Modifier.fillMaxWidth(),
) )
} }
}, },
confirmButton = { confirmText = stringResource(R.string.okay),
TextButton(onClick = { onConfirm(url) }, enabled = url.isNotBlank()) { onAttest = {
Text(stringResource(R.string.okay)) if (url.isNotBlank() && url.startsWith("https://")) {
} onConfirm(url)
}, } else isError = true
dismissButton = {
TextButton(onClick = onDismiss) { Text(stringResource(R.string.cancel)) }
}, },
) )
} }
@@ -1,7 +1,6 @@
package com.zaneschepke.wireguardautotunnel.viewmodel package com.zaneschepke.wireguardautotunnel.viewmodel
import android.net.Uri import android.net.Uri
import androidx.core.net.toUri
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import com.wireguard.android.backend.WgQuickBackend import com.wireguard.android.backend.WgQuickBackend
import com.zaneschepke.wireguardautotunnel.R import com.zaneschepke.wireguardautotunnel.R
@@ -24,9 +23,11 @@ import com.zaneschepke.wireguardautotunnel.util.extensions.TunnelName
import com.zaneschepke.wireguardautotunnel.util.extensions.asStringValue import com.zaneschepke.wireguardautotunnel.util.extensions.asStringValue
import com.zaneschepke.wireguardautotunnel.util.extensions.saveTunnelsUniquely import com.zaneschepke.wireguardautotunnel.util.extensions.saveTunnelsUniquely
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import io.ktor.client.HttpClient
import io.ktor.client.request.prepareGet
import io.ktor.client.statement.bodyAsText
import java.io.File import java.io.File
import java.io.IOException import java.io.IOException
import java.net.URL
import java.time.Instant import java.time.Instant
import javax.inject.Inject import javax.inject.Inject
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
@@ -50,6 +51,7 @@ constructor(
private val settingsRepository: GeneralSettingRepository, private val settingsRepository: GeneralSettingRepository,
private val monitoringSettingsRepository: MonitoringSettingsRepository, private val monitoringSettingsRepository: MonitoringSettingsRepository,
private val rootShellUtils: RootShellUtils, private val rootShellUtils: RootShellUtils,
private val httpClient: HttpClient,
private val fileUtils: FileUtils, private val fileUtils: FileUtils,
) : ContainerHost<SharedAppUiState, LocalSideEffect>, ViewModel() { ) : ContainerHost<SharedAppUiState, LocalSideEffect>, ViewModel() {
@@ -239,18 +241,23 @@ constructor(
fun importFromQr(conf: String) = intent { importFromClipboard(conf) } fun importFromQr(conf: String) = intent { importFromClipboard(conf) }
fun importFromUrl(url: String) = intent { fun importFromUrl(url: String) = intent {
runCatching { try {
val url = URL(url) httpClient.prepareGet(url).execute { response ->
val uri = url.toURI().toString().toUri() if (response.status.value in 200..299) {
importFromUri(uri) val body = response.bodyAsText()
} importFromClipboard(body)
.onFailure { } else {
postSideEffect( throw IOException(
GlobalSideEffect.Toast( "Failed to download file with error status: ${response.status.value}"
StringValue.StringResource(R.string.error_download_failed)
) )
) }
} }
} catch (e: Exception) {
Timber.e(e)
postSideEffect(
GlobalSideEffect.Toast(StringValue.StringResource(R.string.error_download_failed))
)
}
} }
fun importFromUri(uri: Uri) = intent { fun importFromUri(uri: Uri) = intent {
+2
View File
@@ -459,4 +459,6 @@
<string name="copy_from">Copy from</string> <string name="copy_from">Copy from</string>
<string name="mode">Mode</string> <string name="mode">Mode</string>
<string name="app_selection">App selection</string> <string name="app_selection">App selection</string>
<string name="example_import_url" translatable="false">https://123.com/tun.conf</string>
<string name="import_url_description">The URL must be secure and serve a .conf file.</string>
</resources> </resources>
@@ -15,8 +15,6 @@ import com.wireguard.android.util.RootShell
import com.zaneschepke.networkmonitor.AndroidNetworkMonitor.WifiDetectionMethod.* import com.zaneschepke.networkmonitor.AndroidNetworkMonitor.WifiDetectionMethod.*
import com.zaneschepke.networkmonitor.shizuku.ShizukuShell import com.zaneschepke.networkmonitor.shizuku.ShizukuShell
import com.zaneschepke.networkmonitor.util.* import com.zaneschepke.networkmonitor.util.*
import kotlin.concurrent.atomics.AtomicReference
import kotlin.concurrent.atomics.ExperimentalAtomicApi
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.FlowPreview
@@ -24,6 +22,8 @@ import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.* import kotlinx.coroutines.flow.*
import kotlinx.coroutines.withTimeoutOrNull import kotlinx.coroutines.withTimeoutOrNull
import timber.log.Timber import timber.log.Timber
import kotlin.concurrent.atomics.AtomicReference
import kotlin.concurrent.atomics.ExperimentalAtomicApi
class AndroidNetworkMonitor( class AndroidNetworkMonitor(
private val appContext: Context, private val appContext: Context,
@@ -212,6 +212,8 @@ class AndroidNetworkMonitor(
connectivityManager?.registerNetworkCallback(request, wifiCallback!!) connectivityManager?.registerNetworkCallback(request, wifiCallback!!)
trySend(TransportEvent.Unknown)
awaitClose { awaitClose {
runCatching { connectivityManager?.unregisterNetworkCallback(wifiCallback!!) } runCatching { connectivityManager?.unregisterNetworkCallback(wifiCallback!!) }
.onFailure { Timber.e(it, "Error unregistering WiFi network callback") } .onFailure { Timber.e(it, "Error unregistering WiFi network callback") }