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