Skip to content

Commit

Permalink
feat: add split tunneling and tunnel configuration screen
Browse files Browse the repository at this point in the history
This adds a tunnel configuration screen where user can configure split application tunneling and change tunnel name.

Additional QR code validation added to prevent adding of invalid QR codes to tunnels.

Minor UI quality-of-life changes.

Closes #3
  • Loading branch information
zaneschepke committed Jul 4, 2023
1 parent 49bf7fa commit 8b81831
Show file tree
Hide file tree
Showing 14 changed files with 467 additions and 99 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ This is an alternative Android Application for [WireGuard](https://www.wireguard

<p float="center">
<img label="Main" style="padding-right:25px" src="./asset/main_screen.png" width="200" />
<img label="Config" style="padding-left:25px" src="./asset/config_screen.png" width="200" />
<img label="Settings" style="padding-left:25px" src="./asset/settings_screen.png" width="200" />
<img label="Support" style="padding-left:25px" src="./asset/support_screen.png" width="200" />
</p>
Expand Down
5 changes: 3 additions & 2 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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")}")
Expand Down
5 changes: 5 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,11 @@
<uses-permission android:name="android.permission.WAKE_LOCK" />
<!--start service on boot permission-->
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
<queries>
<intent>
<action android:name="android.intent.action.MAIN" />
</intent>
</queries>
<application
android:allowBackup="true"
android:name=".WireGuardAutoTunnel"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,64 @@ data class TunnelConfig(
var name : String,
var wgQuick : String
) {


override fun toString(): String {
return Json.encodeToString(serializer(), this)
}

companion object {
private const val INCLUDED_APPLICATIONS = "IncludedApplications = "
private const val EXCLUDED_APPLICATIONS = "ExcludedApplications = "
private const val INTERFACE = "[Interface]"
private const val NEWLINE_CHAR = "\n"
private const val APP_CONFIG_SEPARATOR = ", "

private fun addApplicationsToConfig(appConfig : String, wgQuick : String) : String {
val configList = wgQuick.split(NEWLINE_CHAR).toMutableList()
val interfaceIndex = configList.indexOf(INTERFACE)
configList.add(interfaceIndex + 1, appConfig)
return configList.joinToString(NEWLINE_CHAR)
}

fun clearAllApplicationsFromConfig(wgQuick : String) : String {
val configList = wgQuick.split(NEWLINE_CHAR).toMutableList()
val itr = configList.iterator()
while (itr.hasNext()) {
val next = itr.next()
if(next.contains(INCLUDED_APPLICATIONS) || next.contains(EXCLUDED_APPLICATIONS)) {
itr.remove()
}
}
return configList.joinToString(NEWLINE_CHAR)
}


fun setExcludedApplicationsOnQuick(packages : List<String>, wgQuick: String) : String {
if(packages.isEmpty()) {
return wgQuick
}
val clearedWgQuick = clearAllApplicationsFromConfig(wgQuick)
val excludeConfig = buildExcludedApplicationsString(packages)
return addApplicationsToConfig(excludeConfig, clearedWgQuick)
}

fun setIncludedApplicationsOnQuick(packages : List<String>, 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>) : String {
return EXCLUDED_APPLICATIONS + packages.joinToString(APP_CONFIG_SEPARATOR)
}

private fun buildIncludedApplicationsString(packages : List<String>) : String {
return INCLUDED_APPLICATIONS + packages.joinToString(APP_CONFIG_SEPARATOR)
}
fun from(string : String) : TunnelConfig {
return Json.decodeFromString<TunnelConfig>(string)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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) {
Expand All @@ -143,7 +144,7 @@ class MainActivity : AppCompatActivity() {
}

else -> {
fadeIn(animationSpec = tween(2000))
fadeIn(animationSpec = tween(1000))
}
}
}) { SettingsScreen(padding = padding, snackbarHostState = snackbarHostState, navController = navController) }
Expand All @@ -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"))}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ import com.zaneschepke.wireguardautotunnel.ui.common.navigation.BottomNavItem
enum class Routes {
Main,
Settings,
Support;
Support,
Config;


companion object {
Expand Down
Original file line number Diff line number Diff line change
@@ -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))
}
}
}
}
}
Loading

0 comments on commit 8b81831

Please sign in to comment.