diff --git a/README.md b/README.md index 448aea7..92ae521 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,7 @@ This is an alternative Android Application for [WireGuard](https://www.wireguard

+

diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 110dbc3..b26326f 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -15,8 +15,8 @@ android { namespace = "com.zaneschepke.wireguardautotunnel" compileSdk = 33 - val versionMajor = 1 - val versionMinor = 2 + val versionMajor = 2 + val versionMinor = 0 val versionPatch = 0 val versionBuild = 0 @@ -101,6 +101,7 @@ dependencies { implementation("com.google.accompanist:accompanist-permissions:${rExtra.get("accompanistVersion")}") implementation("com.google.accompanist:accompanist-flowlayout:${rExtra.get("accompanistVersion")}") implementation("com.google.accompanist:accompanist-navigation-animation:${rExtra.get("accompanistVersion")}") + implementation("com.google.accompanist:accompanist-drawablepainter:${rExtra.get("accompanistVersion")}") //db implementation("io.objectbox:objectbox-kotlin:${rExtra.get("objectBoxVersion")}") diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index b38fe4a..7e594a4 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -17,6 +17,11 @@ + + + + + , wgQuick: String) : String { + if(packages.isEmpty()) { + return wgQuick + } + val clearedWgQuick = clearAllApplicationsFromConfig(wgQuick) + val excludeConfig = buildExcludedApplicationsString(packages) + return addApplicationsToConfig(excludeConfig, clearedWgQuick) + } + + fun setIncludedApplicationsOnQuick(packages : List, wgQuick: String) : String { + if(packages.isEmpty()) { + return wgQuick + } + val clearedWgQuick = clearAllApplicationsFromConfig(wgQuick) + val includeConfig = buildIncludedApplicationsString(packages) + return addApplicationsToConfig(includeConfig, clearedWgQuick) + } + + private fun buildExcludedApplicationsString(packages : List) : String { + return EXCLUDED_APPLICATIONS + packages.joinToString(APP_CONFIG_SEPARATOR) + } + + private fun buildIncludedApplicationsString(packages : List) : String { + return INCLUDED_APPLICATIONS + packages.joinToString(APP_CONFIG_SEPARATOR) + } fun from(string : String) : TunnelConfig { return Json.decodeFromString(string) } 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 61c21cf..5e59c86 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/MainActivity.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/MainActivity.kt @@ -33,6 +33,7 @@ import com.wireguard.android.backend.GoBackend import com.zaneschepke.wireguardautotunnel.R import com.zaneschepke.wireguardautotunnel.ui.common.PermissionRequestFailedScreen import com.zaneschepke.wireguardautotunnel.ui.common.navigation.BottomNavBar +import com.zaneschepke.wireguardautotunnel.ui.screens.config.ConfigScreen import com.zaneschepke.wireguardautotunnel.ui.screens.main.MainScreen import com.zaneschepke.wireguardautotunnel.ui.screens.settings.SettingsScreen import com.zaneschepke.wireguardautotunnel.ui.screens.support.SupportScreen @@ -121,11 +122,11 @@ class MainActivity : AppCompatActivity() { ) else -> { - fadeIn(animationSpec = tween(2000)) + fadeIn(animationSpec = tween(1000)) } } }) { - MainScreen(padding = padding, snackbarHostState = snackbarHostState) + MainScreen(padding = padding, snackbarHostState = snackbarHostState, navController = navController) } composable(Routes.Settings.name, enterTransition = { when (initialState.destination.route) { @@ -143,7 +144,7 @@ class MainActivity : AppCompatActivity() { } else -> { - fadeIn(animationSpec = tween(2000)) + fadeIn(animationSpec = tween(1000)) } } }) { SettingsScreen(padding = padding, snackbarHostState = snackbarHostState, navController = navController) } @@ -156,10 +157,13 @@ class MainActivity : AppCompatActivity() { ) else -> { - fadeIn(animationSpec = tween(2000)) + fadeIn(animationSpec = tween(1000)) } } }) { SupportScreen(padding = padding) } + composable("${Routes.Config.name}/{id}", enterTransition = { + fadeIn(animationSpec = tween(1000)) + }) { ConfigScreen(padding = padding, navController = navController, id = it.arguments?.getString("id"))} } } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/Routes.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/Routes.kt index 6a66bfb..a78fedd 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/Routes.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/Routes.kt @@ -9,7 +9,8 @@ import com.zaneschepke.wireguardautotunnel.ui.common.navigation.BottomNavItem enum class Routes { Main, Settings, - Support; + Support, + Config; companion object { 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 new file mode 100644 index 0000000..53e3cae --- /dev/null +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/config/ConfigScreen.kt @@ -0,0 +1,200 @@ +package com.zaneschepke.wireguardautotunnel.ui.screens.config + +import android.widget.Toast +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.Android +import androidx.compose.material3.Button +import androidx.compose.material3.Checkbox +import androidx.compose.material3.Icon +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Switch +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Alignment +import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.ui.Modifier +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.input.ImeAction +import androidx.compose.ui.text.input.KeyboardCapitalization +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.navigation.NavController +import com.google.accompanist.drawablepainter.DrawablePainter +import com.zaneschepke.wireguardautotunnel.R +import com.zaneschepke.wireguardautotunnel.ui.Routes +import kotlinx.coroutines.launch + +@OptIn(ExperimentalComposeUiApi::class) +@Composable +fun ConfigScreen( + viewModel: ConfigViewModel = hiltViewModel(), + padding: PaddingValues, + navController: NavController, + id : String? +) { + + val context = LocalContext.current + val focusManager = LocalFocusManager.current + val keyboardController = LocalSoftwareKeyboardController.current + val scope = rememberCoroutineScope() + val tunnel by viewModel.tunnel.collectAsStateWithLifecycle(null) + val tunnelName = viewModel.tunnelName.collectAsStateWithLifecycle() + val packages by viewModel.packages.collectAsStateWithLifecycle() + val checkedPackages by viewModel.checkedPackages.collectAsStateWithLifecycle() + val include by viewModel.include.collectAsStateWithLifecycle() + val allApplications by viewModel.allApplications.collectAsStateWithLifecycle() + + LaunchedEffect(Unit) { + viewModel.getTunnelById(id) + viewModel.emitAllInternetCapablePackages() + viewModel.emitCurrentPackageConfigurations(id) + } + + if(tunnel != null) { + Column( + horizontalAlignment = Alignment.Start, + verticalArrangement = Arrangement.Top, + modifier = Modifier + .fillMaxSize() + .padding(padding) + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 20.dp, vertical = 7.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween + ) { + OutlinedTextField( + value = tunnelName.value, + onValueChange = { + viewModel.onTunnelNameChange(it) + }, + label = { Text(stringResource(id = R.string.tunnel_name)) }, + maxLines = 1, + keyboardOptions = KeyboardOptions( + capitalization = KeyboardCapitalization.None, + imeAction = ImeAction.Done + ), + keyboardActions = KeyboardActions( + onDone = { + focusManager.clearFocus() + keyboardController?.hide() + viewModel.onTunnelNameChange(tunnelName.value) + } + ), + ) + } + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 20.dp, vertical = 7.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween + ) { + Text(stringResource(id = R.string.tunnel_all)) + Switch( + checked = allApplications, + onCheckedChange = { + viewModel.onAllApplicationsChange(!allApplications) + } + ) + } + if(!allApplications) { + Row(modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 20.dp, vertical = 7.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween) { + Row(verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween){ + Text(stringResource(id = R.string.include)) + Checkbox( + checked = include, + onCheckedChange = { + viewModel.onIncludeChange(!include) + } + ) + } + Row(verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween){ + Text(stringResource(id = R.string.exclude)) + Checkbox( + checked = !include, + onCheckedChange = { + viewModel.onIncludeChange(!include) + } + ) + } + } + LazyColumn(modifier = Modifier + .fillMaxWidth() + .fillMaxHeight(.75f) + .padding(horizontal = 14.dp, vertical = 7.dp), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.Start) { + items(packages) { pack -> + Row(verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween) { + Row( + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.padding(5.dp) + ) { + val drawable = pack.applicationInfo?.loadIcon(context.packageManager) + if(drawable != null) { + Image(painter = DrawablePainter(drawable), stringResource(id = R.string.icon), modifier = Modifier.size(50.dp, 50.dp)) + } else { + Icon(Icons.Rounded.Android, stringResource(id = R.string.edit), modifier = Modifier.size(50.dp, 50.dp)) + } + Text(pack.applicationInfo.loadLabel(context.packageManager).toString(), modifier = Modifier.padding(5.dp)) + } + Checkbox( + checked = (checkedPackages.contains(pack.packageName)), + onCheckedChange = { + if(it) viewModel.onAddCheckedPackage(pack.packageName) else viewModel.onRemoveCheckedPackage(pack.packageName) + } + ) + } + } + } + } + Row( + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.fillMaxWidth() + ) { + Button(onClick = { + scope.launch { + viewModel.onSaveAllChanges() + Toast.makeText(context, context.resources.getString(R.string.config_changes_saved), Toast.LENGTH_LONG).show() + navController.navigate(Routes.Main.name) + } + }, Modifier.padding(25.dp)) { + Text(stringResource(id = R.string.save_changes)) + } + } + } + } +} \ No newline at end of file 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 new file mode 100644 index 0000000..caae8b9 --- /dev/null +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/config/ConfigViewModel.kt @@ -0,0 +1,133 @@ +package com.zaneschepke.wireguardautotunnel.ui.screens.config + +import android.Manifest +import android.app.Application +import android.content.pm.PackageInfo +import android.content.pm.PackageManager +import android.os.Build +import androidx.compose.runtime.mutableStateListOf +import androidx.compose.runtime.toMutableStateList +import androidx.lifecycle.ViewModel +import com.zaneschepke.wireguardautotunnel.repository.Repository +import com.zaneschepke.wireguardautotunnel.service.tunnel.model.TunnelConfig +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import timber.log.Timber +import javax.inject.Inject + +@HiltViewModel +class ConfigViewModel @Inject constructor(private val application : Application, + private val tunnelRepo : Repository) : ViewModel() { + + private val _tunnel = MutableStateFlow(null) + private val _tunnelName = MutableStateFlow("") + val tunnelName get() = _tunnelName.asStateFlow() + val tunnel get() = _tunnel.asStateFlow() + private val _packages = MutableStateFlow(emptyList()) + val packages get() = _packages.asStateFlow() + private val packageManager = application.packageManager + + private val _checkedPackages = MutableStateFlow(mutableStateListOf()) + val checkedPackages get() = _checkedPackages.asStateFlow() + private val _include = MutableStateFlow(true) + val include get() = _include.asStateFlow() + + private val _allApplications = MutableStateFlow(true) + val allApplications get() = _allApplications.asStateFlow() + + suspend fun getTunnelById(id : String?) : TunnelConfig? { + return try { + if(id != null) { + val config = tunnelRepo.getById(id.toLong()) + if (config != null) { + _tunnel.emit(config) + _tunnelName.emit(config.name) + + } + return config + } + return null + } catch (e : Exception) { + Timber.e(e.message) + null + } + } + + fun onTunnelNameChange(name : String) { + _tunnelName.value = name + } + + fun onIncludeChange(include : Boolean) { + _include.value = include + } + fun onAddCheckedPackage(packageName : String) { + _checkedPackages.value.add(packageName) + } + + fun onAllApplicationsChange(allApplications : Boolean) { + _allApplications.value = allApplications + } + + fun onRemoveCheckedPackage(packageName : String) { + _checkedPackages.value.remove(packageName) + } + + suspend fun emitCurrentPackageConfigurations(id : String?) { + val tunnelConfig = getTunnelById(id) + if(tunnelConfig != null) { + val config = TunnelConfig.configFromQuick(tunnelConfig.wgQuick) + val excludedApps = config.`interface`.excludedApplications + val includedApps = config.`interface`.includedApplications + if(excludedApps.isNullOrEmpty() && includedApps.isNullOrEmpty()) { + _allApplications.emit(true) + return + } + if(excludedApps.isEmpty()) { + _include.emit(true) + _checkedPackages.emit(includedApps.toMutableStateList()) + } else { + _include.emit(false) + _checkedPackages.emit(excludedApps.toMutableStateList()) + } + _allApplications.emit(false) + } + } + + suspend fun emitAllInternetCapablePackages() { + _packages.emit(getAllInternetCapablePackages()) + } + + private fun getAllInternetCapablePackages() : List { + return getPackagesHoldingPermissions(arrayOf(Manifest.permission.INTERNET)) + } + + private fun getPackagesHoldingPermissions(permissions: Array): List { + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + packageManager.getPackagesHoldingPermissions(permissions, PackageManager.PackageInfoFlags.of(0L)) + } else { + @Suppress("DEPRECATION") + packageManager.getPackagesHoldingPermissions(permissions, 0) + } + } + + suspend fun onSaveAllChanges() { + var wgQuick = _tunnel.value?.wgQuick + if(wgQuick != null) { + wgQuick = if(_include.value) { + TunnelConfig.setIncludedApplicationsOnQuick(_checkedPackages.value, wgQuick) + } else { + TunnelConfig.setExcludedApplicationsOnQuick(_checkedPackages.value, wgQuick) + } + if(_allApplications.value) { + wgQuick = TunnelConfig.clearAllApplicationsFromConfig(wgQuick) + } + _tunnel.value?.copy( + name = _tunnelName.value, + wgQuick = wgQuick + )?.let { + tunnelRepo.save(it) + } + } + } +} \ No newline at end of file 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 b45b62a..8ed00be 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 @@ -1,7 +1,6 @@ package com.zaneschepke.wireguardautotunnel.ui.screens.main import android.annotation.SuppressLint -import android.widget.Toast import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.foundation.clickable @@ -22,10 +21,7 @@ import androidx.compose.material.icons.filled.QrCode import androidx.compose.material.icons.rounded.Add import androidx.compose.material.icons.rounded.Delete import androidx.compose.material.icons.rounded.Edit -import androidx.compose.material3.AlertDialog -import androidx.compose.material3.Button import androidx.compose.material3.Divider -import androidx.compose.material3.DrawerValue import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.FabPosition import androidx.compose.material3.FloatingActionButton @@ -33,17 +29,12 @@ import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.ModalBottomSheet -import androidx.compose.material3.ModalDrawerSheet -import androidx.compose.material3.ModalNavigationDrawer -import androidx.compose.material3.NavigationDrawerItem -import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Scaffold import androidx.compose.material3.SnackbarDuration import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.SnackbarResult import androidx.compose.material3.Switch import androidx.compose.material3.Text -import androidx.compose.material3.rememberDrawerState import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect @@ -57,7 +48,6 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.hapticfeedback.HapticFeedbackType import androidx.compose.ui.input.pointer.pointerInput -import androidx.compose.ui.modifier.modifierLocalConsumer import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalHapticFeedback import androidx.compose.ui.res.stringResource @@ -65,9 +55,11 @@ import androidx.compose.ui.text.font.FontStyle import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.navigation.NavController import com.wireguard.android.backend.Tunnel import com.zaneschepke.wireguardautotunnel.R import com.zaneschepke.wireguardautotunnel.service.tunnel.model.TunnelConfig +import com.zaneschepke.wireguardautotunnel.ui.Routes import com.zaneschepke.wireguardautotunnel.ui.common.RowListItem import kotlinx.coroutines.launch @@ -75,7 +67,7 @@ import kotlinx.coroutines.launch @OptIn(ExperimentalMaterial3Api::class) @Composable fun MainScreen(viewModel: MainViewModel = hiltViewModel(), padding : PaddingValues, - snackbarHostState : SnackbarHostState) { + snackbarHostState : SnackbarHostState, navController: NavController) { val haptic = LocalHapticFeedback.current val context = LocalContext.current @@ -85,7 +77,6 @@ fun MainScreen(viewModel: MainViewModel = hiltViewModel(), padding : PaddingValu var showBottomSheet by remember { mutableStateOf(false) } val tunnels by viewModel.tunnels.collectAsStateWithLifecycle(mutableListOf()) val viewState = viewModel.viewState.collectAsStateWithLifecycle() - var showAlertDialog by remember { mutableStateOf(false) } var selectedTunnel by remember { mutableStateOf(null) } val state by viewModel.state.collectAsStateWithLifecycle(Tunnel.State.DOWN) val tunnelName by viewModel.tunnelName.collectAsStateWithLifecycle("") @@ -131,7 +122,7 @@ fun MainScreen(viewModel: MainViewModel = hiltViewModel(), padding : PaddingValu ) { Icon( imageVector = Icons.Rounded.Add, - contentDescription = "Add Tunnel", + contentDescription = stringResource(id = R.string.add_tunnel), tint = Color.DarkGray, ) } @@ -157,24 +148,30 @@ fun MainScreen(viewModel: MainViewModel = hiltViewModel(), padding : PaddingValu ) { // Sheet content Row( - modifier = Modifier.fillMaxWidth().clickable { - showBottomSheet = false - pickFileLauncher.launch("*/*") - }.padding(10.dp) + modifier = Modifier + .fillMaxWidth() + .clickable { + showBottomSheet = false + pickFileLauncher.launch("*/*") + } + .padding(10.dp) ) { - Icon(Icons.Filled.FileOpen, contentDescription = "File Open", modifier = Modifier.padding(10.dp)) - Text("Add tunnel from files", modifier = Modifier.padding(10.dp)) + Icon(Icons.Filled.FileOpen, contentDescription = stringResource(id = R.string.open_file), modifier = Modifier.padding(10.dp)) + Text(stringResource(id = R.string.add_from_file), modifier = Modifier.padding(10.dp)) } Divider() - Row(modifier = Modifier.fillMaxWidth().clickable { - scope.launch { - showBottomSheet = false - viewModel.onTunnelQRSelected() + Row(modifier = Modifier + .fillMaxWidth() + .clickable { + scope.launch { + showBottomSheet = false + viewModel.onTunnelQRSelected() + } } - }.padding(10.dp) + .padding(10.dp) ) { - Icon(Icons.Filled.QrCode, contentDescription = "QR Scan", modifier = Modifier.padding(10.dp)) - Text("Add tunnel from QR code", modifier = Modifier.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)) } } } @@ -201,12 +198,12 @@ fun MainScreen(viewModel: MainViewModel = hiltViewModel(), padding : PaddingValu if (tunnel.id == selectedTunnel?.id) { Row() { IconButton(onClick = { - showAlertDialog = true + navController.navigate("${Routes.Config.name}/${selectedTunnel?.id}") }) { - Icon(Icons.Rounded.Edit, "Edit") + Icon(Icons.Rounded.Edit, stringResource(id = R.string.edit)) } IconButton(onClick = { viewModel.onDelete(tunnel) }) { - Icon(Icons.Rounded.Delete, "Delete") + Icon(Icons.Rounded.Delete, stringResource(id = R.string.delete)) } } } else { @@ -220,40 +217,6 @@ fun MainScreen(viewModel: MainViewModel = hiltViewModel(), padding : PaddingValu }) } } - if (showAlertDialog && selectedTunnel != null) { - AlertDialog(onDismissRequest = { - showAlertDialog = false - }, confirmButton = { - Button(onClick = { - if (tunnels.any { it.name == selectedTunnel?.name }) { - Toast.makeText( - context, - context.resources.getString(R.string.tunnel_exists), - Toast.LENGTH_LONG - ) - .show() - return@Button - } - viewModel.onEditTunnel(selectedTunnel!!) - showAlertDialog = false - }) { - Text("Save") - } - }, - title = { Text("Tunnel Edit") }, text = { - OutlinedTextField( - value = selectedTunnel!!.name, - onValueChange = { - selectedTunnel = selectedTunnel!!.copy( - name = it - ) - }, - label = { Text("Tunnel Name") }, - modifier = Modifier.padding(start = 15.dp, top = 5.dp), - maxLines = 1, - ) - }) - } } } } 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 9d652f9..5867670 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 @@ -90,22 +90,6 @@ class MainViewModel @Inject constructor(private val application : Application, } } - - fun onEditTunnel(tunnel: TunnelConfig) { - viewModelScope.launch { - tunnelRepo.save(tunnel) - val settings = settingsRepo.getAll() - if(!settings.isNullOrEmpty() && settings[0].defaultTunnel != null) { - val setting = settings[0] - val defaultTunnelConfig = TunnelConfig.from(setting.defaultTunnel!!) - if(defaultTunnelConfig.id == tunnel.id) { - setting.defaultTunnel = tunnel.toString() - settingsRepo.save(setting) - } - } - } - } - fun onTunnelStart(tunnelConfig : TunnelConfig) = viewModelScope.launch { ServiceTracker.actionOnService( Action.START, application, WireGuardTunnelService::class.java, mapOf(application.resources.getString(R.string.tunnel_extras_key) to tunnelConfig.toString())) @@ -118,8 +102,10 @@ class MainViewModel @Inject constructor(private val application : Application, suspend fun onTunnelQRSelected() { codeScanner.scan().collect { Timber.d(it) - if(!it.isNullOrEmpty()) { + if(!it.isNullOrEmpty() && it.contains(application.resources.getString(R.string.config_validation))) { tunnelRepo.save(TunnelConfig(name = defaultConfigName(), wgQuick = it)) + } else { + showSnackBarMessage("Invalid QR code. Try again.") } } } 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 0196a2e..1b80a18 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 @@ -4,28 +4,24 @@ import android.Manifest import android.content.Intent import android.net.Uri import android.provider.Settings +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.ExperimentalLayoutApi import androidx.compose.foundation.layout.FlowRow import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Close -import androidx.compose.material.icons.filled.Done import androidx.compose.material.icons.outlined.Add -import androidx.compose.material.icons.outlined.AddCircleOutline -import androidx.compose.material.icons.outlined.Done import androidx.compose.material.icons.rounded.LocationOff -import androidx.compose.material.icons.rounded.Map import androidx.compose.material3.Button import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.ExperimentalMaterial3Api @@ -51,6 +47,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext +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 @@ -68,7 +65,6 @@ import com.zaneschepke.wireguardautotunnel.R import com.zaneschepke.wireguardautotunnel.service.tunnel.model.TunnelConfig import com.zaneschepke.wireguardautotunnel.ui.Routes import com.zaneschepke.wireguardautotunnel.ui.common.ClickableIconButton -import com.zaneschepke.wireguardautotunnel.ui.common.PermissionRequestFailedScreen import kotlinx.coroutines.launch @OptIn(ExperimentalMaterial3Api::class, ExperimentalPermissionsApi::class, @@ -84,6 +80,9 @@ fun SettingsScreen( val scope = rememberCoroutineScope() val context = LocalContext.current + val focusManager = LocalFocusManager.current + val interactionSource = remember { MutableInteractionSource() } + var expanded by remember { mutableStateOf(false) } val viewState by viewModel.viewState.collectAsStateWithLifecycle() val settings by viewModel.settings.collectAsStateWithLifecycle() @@ -122,7 +121,7 @@ fun SettingsScreen( modifier = Modifier .fillMaxSize() .padding(padding)) { - Icon(Icons.Rounded.LocationOff, contentDescription = "Map", modifier = Modifier + Icon(Icons.Rounded.LocationOff, contentDescription = stringResource(id = R.string.map), modifier = Modifier .padding(30.dp) .size(128.dp)) Text(stringResource(R.string.prominent_background_location_title), textAlign = TextAlign.Center, modifier = Modifier.padding(30.dp), fontSize = 20.sp) @@ -138,7 +137,7 @@ fun SettingsScreen( Button(onClick = { navController.navigate(Routes.Main.name) }) { - Text("No thanks") + Text(stringResource(id = R.string.no_thanks)) } Button(onClick = { scope.launch { @@ -149,7 +148,7 @@ fun SettingsScreen( context.startActivity(intentSettings) } }) { - Text("Turn on") + Text(stringResource(id = R.string.turn_on)) } } } @@ -179,6 +178,9 @@ fun SettingsScreen( verticalArrangement = Arrangement.Top, modifier = Modifier .fillMaxSize() + .clickable(indication = null, interactionSource = interactionSource) { + focusManager.clearFocus() + } .padding(padding) ) { Row( diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 7b4b424..edf1b30 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -37,4 +37,24 @@ Thank you for using WG Tunnel! If you are experiencing issues with the app, please reach out on Discord or create an issue on Github. I will try to address the issue as quickly as possible. Thank you! Enter SSID Submit SSID + [Interface] + Invalid QR code. + Add tunnel from files + File Open + Add tunnel from QR code + QR Scan + Tunnel Edit + Tunnel Name + Edit + Delete + Add Tunnel + Exclude + Include + Tunnel all applications + Configuration changes saved. + Save changes + Icon + No thanks + Turn on + Map \ No newline at end of file diff --git a/asset/config_screen.png b/asset/config_screen.png new file mode 100644 index 0000000..97d497b Binary files /dev/null and b/asset/config_screen.png differ diff --git a/build.gradle.kts b/build.gradle.kts index 3ecb7a8..7b15349 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -4,12 +4,11 @@ buildscript { val objectBoxVersion by extra("3.5.1") val hiltVersion by extra("2.44") val accompanistVersion by extra("0.31.2-alpha") - val cameraVersion by extra("1.3.0-beta01") dependencies { classpath("io.objectbox:objectbox-gradle-plugin:$objectBoxVersion") classpath("com.google.gms:google-services:4.3.15") - classpath("com.google.firebase:firebase-crashlytics-gradle:2.9.5") + classpath("com.google.firebase:firebase-crashlytics-gradle:2.9.6") } }