From a5e60c3fbe729a1c989bbcd137031adbf7092e78 Mon Sep 17 00:00:00 2001 From: Zane Schepke Date: Fri, 10 May 2024 21:42:59 -0400 Subject: [PATCH] add amnezia import/export --- app/build.gradle.kts | 3 + .../data/domain/TunnelConfig.kt | 3 +- .../WireGuardConnectivityWatcherService.kt | 3 +- .../service/tile/TunnelControlTile.kt | 1 - .../wireguardautotunnel/ui/MainActivity.kt | 19 +- .../ui/screens/config/ConfigScreen.kt | 8 +- .../ui/screens/config/ConfigViewModel.kt | 16 +- .../ui/screens/main/ConfigType.kt | 6 + .../ui/screens/main/MainScreen.kt | 85 ++-- .../ui/screens/main/MainViewModel.kt | 69 ++- .../ui/screens/options/OptionsScreen.kt | 399 ++++++++++-------- .../ui/screens/settings/SettingsScreen.kt | 19 +- .../wireguardautotunnel/util/Constants.kt | 4 +- .../wireguardautotunnel/util/Event.kt | 2 +- .../wireguardautotunnel/util/Extensions.kt | 18 + app/src/main/res/drawable/add.xml | 9 + app/src/main/res/drawable/close.xml | 9 + app/src/main/res/drawable/edit.xml | 9 + app/src/main/res/values/strings.xml | 5 +- gradle/libs.versions.toml | 4 +- 20 files changed, 449 insertions(+), 242 deletions(-) create mode 100644 app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/ConfigType.kt create mode 100644 app/src/main/res/drawable/add.xml create mode 100644 app/src/main/res/drawable/close.xml create mode 100644 app/src/main/res/drawable/edit.xml diff --git a/app/build.gradle.kts b/app/build.gradle.kts index b08a8b0..46d7a3b 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -139,6 +139,7 @@ dependencies { implementation(libs.androidx.core.ktx) implementation(libs.androidx.lifecycle.runtime.ktx) + // helpers for implementing LifecycleOwner in a Service implementation(libs.androidx.lifecycle.service) implementation(libs.androidx.activity.compose) @@ -173,6 +174,8 @@ dependencies { implementation(libs.androidx.navigation.compose) implementation(libs.androidx.hilt.navigation.compose) + implementation(libs.zaneschepke.multifab) + // hilt implementation(libs.hilt.android) ksp(libs.hilt.android.compiler) diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/domain/TunnelConfig.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/domain/TunnelConfig.kt index 724d842..a35bb15 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/domain/TunnelConfig.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/domain/TunnelConfig.kt @@ -31,7 +31,7 @@ data class TunnelConfig( name = "am_quick", defaultValue = "", ) - val amQuick: String = "", + val amQuick: String = AM_QUICK_DEFAULT, ) { companion object { fun configFromWgQuick(wgQuick: String): Config { @@ -46,5 +46,6 @@ data class TunnelConfig( org.amnezia.awg.config.Config.parse(it) } } + const val AM_QUICK_DEFAULT = "" } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/WireGuardConnectivityWatcherService.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/WireGuardConnectivityWatcherService.kt index 36bfb91..3749c44 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/WireGuardConnectivityWatcherService.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/WireGuardConnectivityWatcherService.kt @@ -25,6 +25,7 @@ import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import timber.log.Timber import java.net.InetAddress import javax.inject.Inject @@ -225,7 +226,7 @@ class WireGuardConnectivityWatcherService : ForegroundService() { val host = if (peer.endpoint.isPresent && peer.endpoint.get().resolved.isPresent) peer.endpoint.get().resolved.get().host - else Constants.BACKUP_PING_HOST + else Constants.DEFAULT_PING_IP Timber.i("Checking reachability of: $host") val reachable = InetAddress.getByName(host) .isReachable(Constants.PING_TIMEOUT.toInt()) diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tile/TunnelControlTile.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tile/TunnelControlTile.kt index 1ea4f8a..15b3c66 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tile/TunnelControlTile.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tile/TunnelControlTile.kt @@ -52,7 +52,6 @@ class TunnelControlTile : TileService() { setTileDescription(it.name) } ?: setUnavailable() } - else -> setInactive() } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/MainActivity.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/MainActivity.kt index b08e579..dba17e8 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/MainActivity.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/MainActivity.kt @@ -32,10 +32,12 @@ import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.lifecycleScope +import androidx.navigation.NavType import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.currentBackStackEntryAsState import androidx.navigation.compose.rememberNavController +import androidx.navigation.navArgument import com.google.accompanist.permissions.ExperimentalPermissionsApi import com.google.accompanist.permissions.isGranted import com.google.accompanist.permissions.rememberPermissionState @@ -47,6 +49,7 @@ import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager import com.zaneschepke.wireguardautotunnel.ui.common.navigation.BottomNavBar import com.zaneschepke.wireguardautotunnel.ui.common.prompt.CustomSnackBar import com.zaneschepke.wireguardautotunnel.ui.screens.config.ConfigScreen +import com.zaneschepke.wireguardautotunnel.ui.screens.main.ConfigType import com.zaneschepke.wireguardautotunnel.ui.screens.main.MainScreen import com.zaneschepke.wireguardautotunnel.ui.screens.options.OptionsScreen import com.zaneschepke.wireguardautotunnel.ui.screens.pinlock.PinLockScreen @@ -236,14 +239,28 @@ class MainActivity : AppCompatActivity() { composable(Screen.Support.Logs.route) { LogsScreen(appViewModel) } - composable("${Screen.Config.route}/{id}") { + //TODO fix navigation for amnezia + composable("${Screen.Config.route}/{id}?configType={configType}", arguments = + listOf( + navArgument("id") { + type = NavType.StringType + defaultValue = "0" + }, + navArgument("configType") { + type = NavType.StringType + defaultValue = ConfigType.WIREGUARD.name + } + ) + ) { val id = it.arguments?.getString("id") + val configType = ConfigType.valueOf( it.arguments?.getString("configType") ?: ConfigType.WIREGUARD.name) if (!id.isNullOrBlank()) { ConfigScreen( navController = navController, tunnelId = id, appViewModel = appViewModel, focusRequester = focusRequester, + configType = configType ) } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/config/ConfigScreen.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/config/ConfigScreen.kt index 619519b..074c469 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/config/ConfigScreen.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/config/ConfigScreen.kt @@ -79,6 +79,7 @@ import com.zaneschepke.wireguardautotunnel.ui.common.config.ConfigurationTextBox import com.zaneschepke.wireguardautotunnel.ui.common.prompt.AuthorizationPrompt import com.zaneschepke.wireguardautotunnel.ui.common.screen.LoadingScreen import com.zaneschepke.wireguardautotunnel.ui.common.text.SectionTitle +import com.zaneschepke.wireguardautotunnel.ui.screens.main.ConfigType import com.zaneschepke.wireguardautotunnel.util.Constants import com.zaneschepke.wireguardautotunnel.util.Event import com.zaneschepke.wireguardautotunnel.util.Result @@ -94,7 +95,8 @@ fun ConfigScreen( focusRequester: FocusRequester, navController: NavController, appViewModel: AppViewModel, - tunnelId: String + tunnelId: String, + configType: ConfigType ) { val context = LocalContext.current val clipboardManager: ClipboardManager = LocalClipboardManager.current @@ -319,7 +321,7 @@ fun ConfigScreen( } }, onClick = { - viewModel.onSaveAllChanges().let { + viewModel.onSaveAllChanges(configType).let { when (it) { is Result.Success -> { appViewModel.showSnackbarMessage(it.data.message) @@ -486,7 +488,7 @@ fun ConfigScreen( modifier = Modifier.width(IntrinsicSize.Min), ) } - if(uiState.isAmneziaEnabled) { + if(configType == ConfigType.AMNEZIA) { ConfigurationTextBox( value = uiState.interfaceProxy.junkPacketCount, onValueChange = { value -> viewModel.onJunkPacketCountChanged(value) }, diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/config/ConfigViewModel.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/config/ConfigViewModel.kt index 4625813..27f6521 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/config/ConfigViewModel.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/config/ConfigViewModel.kt @@ -17,6 +17,7 @@ import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository import com.zaneschepke.wireguardautotunnel.data.repository.SettingsRepository import com.zaneschepke.wireguardautotunnel.ui.screens.config.model.PeerProxy +import com.zaneschepke.wireguardautotunnel.ui.screens.main.ConfigType import com.zaneschepke.wireguardautotunnel.util.Constants import com.zaneschepke.wireguardautotunnel.util.Event import com.zaneschepke.wireguardautotunnel.util.NumberUtils @@ -248,19 +249,22 @@ constructor( return org.amnezia.awg.config.Config.Builder().addPeers(peerList).setInterface(amInterface).build() } - fun onSaveAllChanges(): Result { + fun onSaveAllChanges(configType: ConfigType): Result { return try { - val config = buildConfig() + val wgQuick = buildConfig().toWgQuickString() + val amQuick = if(configType == ConfigType.AMNEZIA) { + buildAmConfig().toAwgQuickString() + } else TunnelConfig.AM_QUICK_DEFAULT val tunnelConfig = when (uiState.value.tunnel) { null -> TunnelConfig( name = _uiState.value.tunnelName, - wgQuick = config.toWgQuickString(), + wgQuick = wgQuick, + amQuick = amQuick ) else -> uiState.value.tunnel!!.copy( name = _uiState.value.tunnelName, - wgQuick = config.toWgQuickString(), - amQuick = if(uiState.value.isAmneziaEnabled) buildAmConfig().toAwgQuickString() - else _uiState.value.tunnel?.amQuick ?: "" + wgQuick = wgQuick, + amQuick = amQuick ) } updateTunnelConfig(tunnelConfig) diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/ConfigType.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/ConfigType.kt new file mode 100644 index 0000000..67593b5 --- /dev/null +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/ConfigType.kt @@ -0,0 +1,6 @@ +package com.zaneschepke.wireguardautotunnel.ui.screens.main + +enum class ConfigType { + AMNEZIA, + WIREGUARD +} diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/MainScreen.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/MainScreen.kt index e6a7bf9..6b117a8 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/MainScreen.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/MainScreen.kt @@ -34,7 +34,6 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Create import androidx.compose.material.icons.filled.FileOpen import androidx.compose.material.icons.filled.QrCode -import androidx.compose.material.icons.rounded.Add import androidx.compose.material.icons.rounded.Bolt import androidx.compose.material.icons.rounded.Circle import androidx.compose.material.icons.rounded.CopyAll @@ -46,7 +45,6 @@ import androidx.compose.material.icons.rounded.Star import androidx.compose.material3.AlertDialog import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.FabPosition -import androidx.compose.material3.FloatingActionButton import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.IconButton @@ -91,6 +89,10 @@ import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation.NavController +import com.iamageo.multifablibrary.FabIcon +import com.iamageo.multifablibrary.FabOption +import com.iamageo.multifablibrary.MultiFabItem +import com.iamageo.multifablibrary.MultiFloatingActionButton import com.journeyapps.barcodescanner.ScanContract import com.journeyapps.barcodescanner.ScanOptions import com.zaneschepke.wireguardautotunnel.R @@ -114,8 +116,6 @@ import com.zaneschepke.wireguardautotunnel.util.truncateWithEllipsis import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.launch -import timber.log.Timber -import java.util.Timer @SuppressLint("UnusedMaterial3ScaffoldPaddingParameter") @OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class) @@ -133,6 +133,7 @@ fun MainScreen( val sheetState = rememberModalBottomSheetState() var showBottomSheet by remember { mutableStateOf(false) } + var configType by remember { mutableStateOf(ConfigType.WIREGUARD) } // Nested scroll for control FAB val nestedScrollConnection = remember { @@ -201,7 +202,7 @@ fun MainScreen( ) { data -> if (data == null) return@rememberLauncherForActivityResult scope.launch { - viewModel.onTunnelFileSelected(data).let { + viewModel.onTunnelFileSelected(data, configType).let { when (it) { is Result.Error -> appViewModel.showSnackbarMessage(it.error.message) is Result.Success -> {} @@ -215,7 +216,7 @@ fun MainScreen( onResult = { if (it.contents != null) { scope.launch { - viewModel.onTunnelQrResult(it.contents).let { result -> + viewModel.onTunnelQrResult(it.contents, configType).let { result -> when (result) { is Result.Success -> {} is Result.Error -> appViewModel.showSnackbarMessage(result.error.message) @@ -260,6 +261,19 @@ fun MainScreen( return LoadingScreen() } + fun launchQrScanner() { + 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) + } + Scaffold( modifier = Modifier.pointerInput(Unit) { @@ -282,7 +296,7 @@ fun MainScreen( val secondaryColor = MaterialTheme.colorScheme.secondary val hoverColor = MaterialTheme.colorScheme.surfaceColorAtElevation(2.dp) var fobColor by remember { mutableStateOf(secondaryColor) } - FloatingActionButton( + MultiFloatingActionButton( modifier = (if ( WireGuardAutoTunnel.isRunningOnAndroidTv() && @@ -295,16 +309,42 @@ fun MainScreen( fobColor = if (it.isFocused) hoverColor else secondaryColor } }, - onClick = { showBottomSheet = true }, - containerColor = fobColor, + fabIcon = FabIcon( + iconRes = R.drawable.add, + iconResAfterRotate = R.drawable.close, + iconRotate = 180f + ), + fabOption = FabOption( + iconTint = MaterialTheme.colorScheme.background, + backgroundTint = MaterialTheme.colorScheme.primary, + ), + itemsMultiFab = listOf( + MultiFabItem( + label = { + Text( + stringResource(id = R.string.amnezia), + color = Color.White, + textAlign = TextAlign.Center, + modifier = Modifier.padding(end = 10.dp) + ) + }, + icon = R.drawable.add, + value = ConfigType.AMNEZIA.name, + ), + MultiFabItem( + label = { + Text(stringResource(id = R.string.wireguard), color = Color.White, textAlign = TextAlign.Center, modifier = Modifier.padding(end = 10.dp)) + }, + icon = R.drawable.add, + value = ConfigType.WIREGUARD.name + ), + ), + onFabItemClicked = { + showBottomSheet = true + configType = ConfigType.valueOf(it.value) + }, shape = RoundedCornerShape(16.dp), - ) { - Icon( - imageVector = Icons.Rounded.Add, - contentDescription = stringResource(id = R.string.add_tunnel), - tint = Color.DarkGray, - ) - } + ) } }, ) { @@ -343,16 +383,7 @@ fun MainScreen( .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) + launchQrScanner() } } .padding(10.dp), @@ -376,7 +407,7 @@ fun MainScreen( .clickable { showBottomSheet = false navController.navigate( - "${Screen.Config.route}/${Constants.MANUAL_TUNNEL_CONFIG_ID}", + "${Screen.Config.route}/${Constants.MANUAL_TUNNEL_CONFIG_ID}?configType=${configType}", ) } .padding(10.dp), diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/MainViewModel.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/MainViewModel.kt index b4fd09a..a541d4b 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/MainViewModel.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/MainViewModel.kt @@ -18,13 +18,13 @@ import com.zaneschepke.wireguardautotunnel.util.Constants import com.zaneschepke.wireguardautotunnel.util.Event import com.zaneschepke.wireguardautotunnel.util.NumberUtils import com.zaneschepke.wireguardautotunnel.util.Result +import com.zaneschepke.wireguardautotunnel.util.toWgQuickString import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext import timber.log.Timber import java.io.InputStream import java.util.zip.ZipInputStream @@ -98,15 +98,23 @@ constructor( serviceManager.stopVpnService(application.applicationContext, isManualStop = true) } - private fun validateConfigString(config: String) { - TunnelConfig.configFromWgQuick(config) + private fun validateConfigString(config: String, configType: ConfigType) { + when(configType) { + ConfigType.AMNEZIA -> TunnelConfig.configFromAmQuick(config) + ConfigType.WIREGUARD -> TunnelConfig.configFromWgQuick(config) + } } - suspend fun onTunnelQrResult(result: String): Result { + suspend fun onTunnelQrResult(result: String, configType: ConfigType): Result { return try { - validateConfigString(result) - val tunnelConfig = - TunnelConfig(name = NumberUtils.generateRandomTunnelName(), wgQuick = result) + validateConfigString(result, configType) + val tunnelConfig = when(configType) { + ConfigType.AMNEZIA ->{ + TunnelConfig(name = NumberUtils.generateRandomTunnelName(), amQuick = result, + wgQuick = TunnelConfig.configFromAmQuick(result).toWgQuickString()) + } + ConfigType.WIREGUARD -> TunnelConfig(name = NumberUtils.generateRandomTunnelName(), wgQuick = result) + } addTunnel(tunnelConfig) Result.Success(Unit) } catch (e: Exception) { @@ -115,32 +123,42 @@ constructor( } } - private suspend fun saveTunnelConfigFromStream(stream: InputStream, fileName: String) { - val bufferReader = stream.bufferedReader(charset = Charsets.UTF_8) - val config = Config.parse(bufferReader) + private suspend fun saveTunnelConfigFromStream(stream: InputStream, fileName: String, type: ConfigType) { + var amQuick : String? = null + val wgQuick = stream.use { + when(type) { + ConfigType.AMNEZIA -> { + val config = org.amnezia.awg.config.Config.parse(it) + amQuick = config.toAwgQuickString() + config.toWgQuickString() + } + ConfigType.WIREGUARD -> { + Config.parse(it).toWgQuickString() + } + } + } val tunnelName = getNameFromFileName(fileName) - addTunnel(TunnelConfig(name = tunnelName, wgQuick = config.toWgQuickString())) - withContext(Dispatchers.IO) { stream.close() } + addTunnel(TunnelConfig(name = tunnelName, wgQuick = wgQuick, amQuick = amQuick ?: TunnelConfig.AM_QUICK_DEFAULT)) } private fun getInputStreamFromUri(uri: Uri): InputStream? { return application.applicationContext.contentResolver.openInputStream(uri) } - suspend fun onTunnelFileSelected(uri: Uri): Result { + suspend fun onTunnelFileSelected(uri: Uri, configType: ConfigType): Result { try { if (isValidUriContentScheme(uri)) { val fileName = getFileName(application.applicationContext, uri) when (getFileExtensionFromFileName(fileName)) { Constants.CONF_FILE_EXTENSION -> - saveTunnelFromConfUri(fileName, uri).let { + saveTunnelFromConfUri(fileName, uri, configType).let { when (it) { is Result.Error -> return Result.Error(Event.Error.FileReadFailed) is Result.Success -> return it } } - Constants.ZIP_FILE_EXTENSION -> saveTunnelsFromZipUri(uri) + Constants.ZIP_FILE_EXTENSION -> saveTunnelsFromZipUri(uri, configType) else -> return Result.Error(Event.Error.InvalidFileExtension) } return Result.Success(Unit) @@ -153,7 +171,7 @@ constructor( } } - private suspend fun saveTunnelsFromZipUri(uri: Uri) { + private suspend fun saveTunnelsFromZipUri(uri: Uri, configType: ConfigType) { ZipInputStream(getInputStreamFromUri(uri)).use { zip -> generateSequence { zip.nextEntry } .filterNot { @@ -162,18 +180,29 @@ constructor( } .forEach { val name = getNameFromFileName(it.name) - val config = Config.parse(zip) viewModelScope.launch(Dispatchers.IO) { - addTunnel(TunnelConfig(name = name, wgQuick = config.toWgQuickString())) + var amQuick : String? = null + val wgQuick = + when(configType) { + ConfigType.AMNEZIA -> { + val config = org.amnezia.awg.config.Config.parse(zip) + amQuick = config.toAwgQuickString() + config.toWgQuickString() + } + ConfigType.WIREGUARD -> { + Config.parse(zip).toWgQuickString() + } + } + addTunnel(TunnelConfig(name = name, wgQuick = wgQuick, amQuick = amQuick ?: TunnelConfig.AM_QUICK_DEFAULT)) } } } } - private suspend fun saveTunnelFromConfUri(name: String, uri: Uri): Result { + private suspend fun saveTunnelFromConfUri(name: String, uri: Uri, configType: ConfigType): Result { val stream = getInputStreamFromUri(uri) return if (stream != null) { - saveTunnelConfigFromStream(stream, name) + saveTunnelConfigFromStream(stream, name, configType) Result.Success(Unit) } else { Result.Error(Event.Error.FileReadFailed) diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/options/OptionsScreen.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/options/OptionsScreen.kt index bfcd161..7e1893a 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/options/OptionsScreen.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/options/OptionsScreen.kt @@ -1,5 +1,6 @@ package com.zaneschepke.wireguardautotunnel.ui.screens.options +import android.annotation.SuppressLint import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Arrangement @@ -7,7 +8,6 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.ExperimentalLayoutApi import androidx.compose.foundation.layout.FlowRow import androidx.compose.foundation.layout.IntrinsicSize -import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height @@ -24,9 +24,11 @@ import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Scaffold import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.material3.TextButton +import androidx.compose.material3.surfaceColorAtElevation import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue @@ -38,16 +40,22 @@ import androidx.compose.ui.Alignment 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.LocalFocusManager import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontStyle import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardCapitalization +import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation.NavController +import com.iamageo.multifablibrary.FabIcon +import com.iamageo.multifablibrary.FabOption +import com.iamageo.multifablibrary.MultiFabItem +import com.iamageo.multifablibrary.MultiFloatingActionButton import com.zaneschepke.wireguardautotunnel.R import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel import com.zaneschepke.wireguardautotunnel.ui.AppViewModel @@ -55,11 +63,13 @@ import com.zaneschepke.wireguardautotunnel.ui.Screen import com.zaneschepke.wireguardautotunnel.ui.common.ClickableIconButton import com.zaneschepke.wireguardautotunnel.ui.common.config.ConfigurationToggle import com.zaneschepke.wireguardautotunnel.ui.common.text.SectionTitle +import com.zaneschepke.wireguardautotunnel.ui.screens.main.ConfigType import com.zaneschepke.wireguardautotunnel.util.Constants import com.zaneschepke.wireguardautotunnel.util.Result import kotlinx.coroutines.delay import kotlinx.coroutines.launch +@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter") @OptIn(ExperimentalLayoutApi::class) @Composable fun OptionsScreen( @@ -100,186 +110,227 @@ fun OptionsScreen( } } - Column( - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Top, - modifier = - Modifier - .fillMaxSize() - .verticalScroll(scrollState) - .clickable( - indication = null, - interactionSource = interactionSource, - ) { - focusManager.clearFocus() - }, - ) { - Surface( - tonalElevation = 2.dp, - shadowElevation = 2.dp, - shape = RoundedCornerShape(12.dp), - color = MaterialTheme.colorScheme.surface, - modifier = - (if (WireGuardAutoTunnel.isRunningOnAndroidTv()) { - Modifier - .height(IntrinsicSize.Min) - .fillMaxWidth(fillMaxWidth) - .padding(top = 10.dp) - } else { - Modifier - .fillMaxWidth(fillMaxWidth) - .padding(top = 20.dp) - }) - .padding(bottom = 10.dp), - ) { - Column( - horizontalAlignment = Alignment.Start, - verticalArrangement = Arrangement.Top, - modifier = Modifier.padding(15.dp), - ) { - SectionTitle( - title = stringResource(id = R.string.general), - padding = screenPadding, + Scaffold( + floatingActionButton = { + val secondaryColor = MaterialTheme.colorScheme.secondary + val hoverColor = MaterialTheme.colorScheme.surfaceColorAtElevation(2.dp) + var fobColor by remember { mutableStateOf(secondaryColor) } + MultiFloatingActionButton( + modifier = + (if ( + WireGuardAutoTunnel.isRunningOnAndroidTv() ) - ConfigurationToggle( - stringResource(R.string.set_primary_tunnel), - enabled = true, - checked = uiState.isDefaultTunnel, - modifier = Modifier - .focusRequester(focusRequester), - padding = screenPadding, - onCheckChanged = { optionsViewModel.onTogglePrimaryTunnel() }, - ) - Row( - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier - .fillMaxSize() - .padding(top = 5.dp), - horizontalArrangement = Arrangement.Center, - ) { - TextButton( - onClick = { - navController.navigate( - "${Screen.Config.route}/${tunnelId}", + Modifier.focusRequester(focusRequester) + else Modifier) + .onFocusChanged { + if (WireGuardAutoTunnel.isRunningOnAndroidTv()) { + fobColor = if (it.isFocused) hoverColor else secondaryColor + } + }, + fabIcon = FabIcon( + iconRes = R.drawable.edit, + iconResAfterRotate = R.drawable.close, + iconRotate = 180f + ), + fabOption = FabOption( + iconTint = MaterialTheme.colorScheme.background, + backgroundTint = MaterialTheme.colorScheme.primary, + ), + itemsMultiFab = listOf( + MultiFabItem( + label = { + Text( + stringResource(id = R.string.amnezia), + color = Color.White, + textAlign = TextAlign.Center, + modifier = Modifier.padding(end = 10.dp) ) }, - ) { - Text(stringResource(R.string.edit_tunnel)) - } + icon = R.drawable.edit, + value = ConfigType.AMNEZIA.name, + ), + MultiFabItem( + label = { + Text(stringResource(id = R.string.wireguard), color = Color.White, textAlign = TextAlign.Center, modifier = Modifier.padding(end = 10.dp)) + }, + icon = R.drawable.edit, + value = ConfigType.WIREGUARD.name + ), + ), + onFabItemClicked = { + val configType = ConfigType.valueOf(it.value) + navController.navigate( + "${Screen.Config.route}/${tunnelId}?configType=${configType.name}", + ) + }, + shape = RoundedCornerShape(16.dp), + ) + } + ) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Top, + modifier = + Modifier + .fillMaxSize() + .verticalScroll(scrollState) + .clickable( + indication = null, + interactionSource = interactionSource, + ) { + focusManager.clearFocus() + }, + ) { + Surface( + tonalElevation = 2.dp, + shadowElevation = 2.dp, + shape = RoundedCornerShape(12.dp), + color = MaterialTheme.colorScheme.surface, + modifier = + (if (WireGuardAutoTunnel.isRunningOnAndroidTv()) { + Modifier + .height(IntrinsicSize.Min) + .fillMaxWidth(fillMaxWidth) + .padding(top = 10.dp) + } else { + Modifier + .fillMaxWidth(fillMaxWidth) + .padding(top = 20.dp) + }) + .padding(bottom = 10.dp), + ) { + Column( + horizontalAlignment = Alignment.Start, + verticalArrangement = Arrangement.Top, + modifier = Modifier.padding(15.dp), + ) { + SectionTitle( + title = stringResource(id = R.string.general), + padding = screenPadding, + ) + ConfigurationToggle( + stringResource(R.string.set_primary_tunnel), + enabled = true, + checked = uiState.isDefaultTunnel, + modifier = Modifier + .focusRequester(focusRequester), + padding = screenPadding, + onCheckChanged = { optionsViewModel.onTogglePrimaryTunnel() }, + ) } } - } - Surface( - tonalElevation = 2.dp, - shadowElevation = 2.dp, - shape = RoundedCornerShape(12.dp), - color = MaterialTheme.colorScheme.surface, - modifier = - (if (WireGuardAutoTunnel.isRunningOnAndroidTv()) { - Modifier - .height(IntrinsicSize.Min) - .fillMaxWidth(fillMaxWidth) - .padding(top = 10.dp) - } else { - Modifier - .fillMaxWidth(fillMaxWidth) - .padding(top = 20.dp) - }) - .padding(bottom = 10.dp), - ) { - Column( - horizontalAlignment = Alignment.Start, - verticalArrangement = Arrangement.Top, - modifier = Modifier.padding(15.dp), + Surface( + tonalElevation = 2.dp, + shadowElevation = 2.dp, + shape = RoundedCornerShape(12.dp), + color = MaterialTheme.colorScheme.surface, + modifier = + (if (WireGuardAutoTunnel.isRunningOnAndroidTv()) { + Modifier + .height(IntrinsicSize.Min) + .fillMaxWidth(fillMaxWidth) + .padding(top = 10.dp) + } else { + Modifier + .fillMaxWidth(fillMaxWidth) + .padding(top = 20.dp) + }) + .padding(bottom = 10.dp), ) { - SectionTitle( - title = stringResource(id = R.string.auto_tunneling), - padding = screenPadding, - ) - ConfigurationToggle( - stringResource(R.string.mobile_data_tunnel), - enabled = true, - checked = uiState.tunnel?.isMobileDataTunnel == true, - padding = screenPadding, - onCheckChanged = { optionsViewModel.onToggleIsMobileDataTunnel() }, - ) - Column { - FlowRow( - modifier = Modifier - .padding(screenPadding) - .fillMaxWidth(), - horizontalArrangement = Arrangement.spacedBy(5.dp), - ) { - uiState.tunnel?.tunnelNetworks?.forEach { ssid -> - ClickableIconButton( - onClick = { - if (WireGuardAutoTunnel.isRunningOnAndroidTv()) { - focusRequester.requestFocus() - optionsViewModel.onDeleteRunSSID(ssid) - } - }, - onIconClick = { - if (WireGuardAutoTunnel.isRunningOnAndroidTv()) focusRequester.requestFocus() - optionsViewModel.onDeleteRunSSID(ssid) - - }, - text = ssid, - icon = Icons.Filled.Close, - enabled = true, - ) - } - if (uiState.tunnel == null || uiState.tunnel?.tunnelNetworks?.isEmpty() == true) { - Text( - stringResource(R.string.no_wifi_names_configured), - fontStyle = FontStyle.Italic, - color = Color.Gray, - ) - } - } - OutlinedTextField( - enabled = true, - value = currentText, - onValueChange = { currentText = it }, - label = { Text(stringResource(id = R.string.use_tunnel_on_wifi_name)) }, - modifier = - Modifier - .padding( - start = screenPadding, - top = 5.dp, - bottom = 10.dp, - ), - maxLines = 1, - keyboardOptions = - KeyboardOptions( - capitalization = KeyboardCapitalization.None, - imeAction = ImeAction.Done, - ), - keyboardActions = KeyboardActions(onDone = { saveTrustedSSID() }), - trailingIcon = { - if (currentText != "") { - IconButton(onClick = { saveTrustedSSID() }) { - Icon( - imageVector = Icons.Outlined.Add, - contentDescription = - if (currentText == "") { - stringResource( - id = - R.string - .trusted_ssid_empty_description, - ) - } else { - stringResource( - id = - R.string - .trusted_ssid_value_description, - ) - }, - tint = MaterialTheme.colorScheme.primary, - ) - } - } - }, + Column( + horizontalAlignment = Alignment.Start, + verticalArrangement = Arrangement.Top, + modifier = Modifier.padding(15.dp), + ) { + SectionTitle( + title = stringResource(id = R.string.auto_tunneling), + padding = screenPadding, ) + ConfigurationToggle( + stringResource(R.string.mobile_data_tunnel), + enabled = true, + checked = uiState.tunnel?.isMobileDataTunnel == true, + padding = screenPadding, + onCheckChanged = { optionsViewModel.onToggleIsMobileDataTunnel() }, + ) + Column { + FlowRow( + modifier = Modifier + .padding(screenPadding) + .fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(5.dp), + ) { + uiState.tunnel?.tunnelNetworks?.forEach { ssid -> + ClickableIconButton( + onClick = { + if (WireGuardAutoTunnel.isRunningOnAndroidTv()) { + focusRequester.requestFocus() + optionsViewModel.onDeleteRunSSID(ssid) + } + }, + onIconClick = { + if (WireGuardAutoTunnel.isRunningOnAndroidTv()) focusRequester.requestFocus() + optionsViewModel.onDeleteRunSSID(ssid) + + }, + text = ssid, + icon = Icons.Filled.Close, + enabled = true, + ) + } + if (uiState.tunnel == null || uiState.tunnel?.tunnelNetworks?.isEmpty() == true) { + Text( + stringResource(R.string.no_wifi_names_configured), + fontStyle = FontStyle.Italic, + color = Color.Gray, + ) + } + } + OutlinedTextField( + enabled = true, + value = currentText, + onValueChange = { currentText = it }, + label = { Text(stringResource(id = R.string.use_tunnel_on_wifi_name)) }, + modifier = + Modifier + .padding( + start = screenPadding, + top = 5.dp, + bottom = 10.dp, + ), + maxLines = 1, + keyboardOptions = + KeyboardOptions( + capitalization = KeyboardCapitalization.None, + imeAction = ImeAction.Done, + ), + keyboardActions = KeyboardActions(onDone = { saveTrustedSSID() }), + trailingIcon = { + if (currentText != "") { + IconButton(onClick = { saveTrustedSSID() }) { + Icon( + imageVector = Icons.Outlined.Add, + contentDescription = + if (currentText == "") { + stringResource( + id = + R.string + .trusted_ssid_empty_description, + ) + } else { + stringResource( + id = + R.string + .trusted_ssid_value_description, + ) + }, + tint = MaterialTheme.colorScheme.primary, + ) + } + } + }, + ) + } } } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/SettingsScreen.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/SettingsScreen.kt index 66a7338..5147ef9 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/SettingsScreen.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/SettingsScreen.kt @@ -74,6 +74,7 @@ import com.wireguard.android.backend.Tunnel import com.wireguard.android.backend.WgQuickBackend import com.zaneschepke.wireguardautotunnel.R import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel +import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelState import com.zaneschepke.wireguardautotunnel.ui.AppViewModel import com.zaneschepke.wireguardautotunnel.ui.Screen @@ -131,11 +132,21 @@ fun SettingsScreen( fun exportAllConfigs() { try { - val files = uiState.tunnels.map { File(context.cacheDir, "${it.name}.conf") } - files.forEachIndexed { index, file -> - file.outputStream().use { it.write(uiState.tunnels[index].wgQuick.toByteArray()) } + val wgFiles = uiState.tunnels.map { config -> + val file = File(context.cacheDir, "${config.name}-wg.conf") + file.outputStream().use { + it.write(config.wgQuick.toByteArray()) + } + file } - FileUtils.saveFilesToZip(context, files) + val amFiles = uiState.tunnels.mapNotNull { config -> if(config.amQuick != TunnelConfig.AM_QUICK_DEFAULT) { + val file = File(context.cacheDir, "${config.name}-am.conf") + file.outputStream().use { + it.write(config.amQuick.toByteArray()) + } + file + } else null } + FileUtils.saveFilesToZip(context, wgFiles + amFiles) didExportFiles = true appViewModel.showSnackbarMessage(Event.Message.ConfigsExported.message) } catch (e: Exception) { diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/Constants.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/Constants.kt index e0086b0..0cb1bd5 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/Constants.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/Constants.kt @@ -25,7 +25,7 @@ object Constants { const val SUBSCRIPTION_TIMEOUT = 5_000L const val FOCUS_REQUEST_DELAY = 500L - const val BACKUP_PING_HOST = "1.1.1.1" + const val DEFAULT_PING_IP = "1.1.1.1" const val PING_TIMEOUT = 5_000L const val VPN_RESTART_DELAY = 1_000L const val PING_INTERVAL = 60_000L @@ -37,4 +37,6 @@ object Constants { const val UNREADABLE_SSID = "" + val amneziaProperties = listOf("Jc", "Jmin", "Jmax", "S1", "S2", "H1", "H2", "H3", "H4") + } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/Event.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/Event.kt index d4019c6..22639ea 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/Event.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/Event.kt @@ -53,7 +53,7 @@ sealed class Event { data object FileReadFailed : Error() { override val message: String - get() = WireGuardAutoTunnel.instance.getString(R.string.error_file_extension) + get() = WireGuardAutoTunnel.instance.getString(R.string.error_file_format) } data object AuthenticationFailed : Error() { diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/Extensions.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/Extensions.kt index 7b27335..6d9b59a 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/Extensions.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/Extensions.kt @@ -2,6 +2,7 @@ package com.zaneschepke.wireguardautotunnel.util import android.content.BroadcastReceiver import android.content.pm.PackageInfo +import com.wireguard.config.Peer import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig import com.zaneschepke.wireguardautotunnel.service.tunnel.HandshakeStatus import com.zaneschepke.wireguardautotunnel.service.tunnel.statistics.TunnelStatistics @@ -9,6 +10,8 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch +import org.amnezia.awg.config.Config +import timber.log.Timber import java.math.BigDecimal import java.text.DecimalFormat import kotlin.coroutines.CoroutineContext @@ -69,3 +72,18 @@ fun TunnelStatistics.PeerStats.handshakeStatus(): HandshakeStatus { } } } + +fun Config.toWgQuickString() : String { + val amQuick = toAwgQuickString() + val lines = amQuick.lines().toMutableList() + val linesIterator = lines.iterator() + while(linesIterator.hasNext()) { + val next = linesIterator.next() + Constants.amneziaProperties.forEach { + if(next.startsWith(it, ignoreCase = true)) { + linesIterator.remove() + } + } + } + return lines.joinToString(System.lineSeparator()) +} diff --git a/app/src/main/res/drawable/add.xml b/app/src/main/res/drawable/add.xml new file mode 100644 index 0000000..e4bb397 --- /dev/null +++ b/app/src/main/res/drawable/add.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/close.xml b/app/src/main/res/drawable/close.xml new file mode 100644 index 0000000..0efa44f --- /dev/null +++ b/app/src/main/res/drawable/close.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/edit.xml b/app/src/main/res/drawable/edit.xml new file mode 100644 index 0000000..a2f714d --- /dev/null +++ b/app/src/main/res/drawable/edit.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 08f6d40..eab41c1 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -173,5 +173,8 @@ if you are unsure how to proceed See the https://zaneschepke.com/wgtunnel-docs/getting-started.html - Getting started guide + getting started guide + Amnezia + WireGuard + Invalid tunnel config format \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 82882c9..aec302e 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -16,12 +16,13 @@ junit = "4.13.2" kotlinx-serialization-json = "1.6.3" lifecycle-runtime-compose = "2.7.0" material3 = "1.2.1" +multifabVersion = "1.0.9" navigationCompose = "2.7.7" pinLockCompose = "1.0.3" roomVersion = "2.6.1" timber = "5.0.1" tunnel = "1.0.20230706" -androidGradlePlugin = "8.4.0-rc02" +androidGradlePlugin = "8.4.0" kotlin = "1.9.23" ksp = "1.9.23-1.0.19" composeBom = "2024.05.00" @@ -87,6 +88,7 @@ pin-lock-compose = { module = "com.zaneschepke:pin_lock_compose", version.ref = timber = { module = "com.jakewharton.timber:timber", version.ref = "timber" } tunnel = { module = "com.wireguard.android:tunnel", version.ref = "tunnel" } +zaneschepke-multifab = { module = "com.zaneschepke:multifab", version.ref = "multifabVersion" } zxing-core = { module = "com.google.zxing:core", version.ref = "zxingCore" } zxing-android-embedded = { module = "com.journeyapps:zxing-android-embedded", version.ref = "zxingAndroidEmbedded" } material = { group = "com.google.android.material", name = "material", version.ref = "material" }