mirror of
https://github.com/wgtunnel/android.git
synced 2026-06-02 08:33:40 +02:00
Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| cb3a7d024b | |||
| 259be21b04 | |||
| 8b81831910 |
@@ -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>
|
||||
@@ -42,6 +43,7 @@ The inspiration for this app came from the inconvenience of constantly having to
|
||||
|
||||
* Add tunnels via .conf file
|
||||
* Auto connect to VPN based on Wi-Fi SSID
|
||||
* Split tunneling by application
|
||||
* Configurable Trusted Network list
|
||||
* Optional auto connect on mobile data
|
||||
* Automatic service restart after reboot
|
||||
|
||||
@@ -15,9 +15,9 @@ android {
|
||||
namespace = "com.zaneschepke.wireguardautotunnel"
|
||||
compileSdk = 33
|
||||
|
||||
val versionMajor = 1
|
||||
val versionMinor = 2
|
||||
val versionPatch = 0
|
||||
val versionMajor = 2
|
||||
val versionMinor = 0
|
||||
val versionPatch = 1
|
||||
val versionBuild = 0
|
||||
|
||||
defaultConfig {
|
||||
@@ -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")}")
|
||||
|
||||
@@ -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"
|
||||
|
||||
+53
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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"))}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,8 @@ import com.zaneschepke.wireguardautotunnel.ui.common.navigation.BottomNavItem
|
||||
enum class Routes {
|
||||
Main,
|
||||
Settings,
|
||||
Support;
|
||||
Support,
|
||||
Config;
|
||||
|
||||
|
||||
companion object {
|
||||
|
||||
+200
@@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+133
@@ -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<TunnelConfig>) : ViewModel() {
|
||||
|
||||
private val _tunnel = MutableStateFlow<TunnelConfig?>(null)
|
||||
private val _tunnelName = MutableStateFlow("")
|
||||
val tunnelName get() = _tunnelName.asStateFlow()
|
||||
val tunnel get() = _tunnel.asStateFlow()
|
||||
private val _packages = MutableStateFlow(emptyList<PackageInfo>())
|
||||
val packages get() = _packages.asStateFlow()
|
||||
private val packageManager = application.packageManager
|
||||
|
||||
private val _checkedPackages = MutableStateFlow(mutableStateListOf<String>())
|
||||
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<PackageInfo> {
|
||||
return getPackagesHoldingPermissions(arrayOf(Manifest.permission.INTERNET))
|
||||
}
|
||||
|
||||
private fun getPackagesHoldingPermissions(permissions: Array<String>): List<PackageInfo> {
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+26
-63
@@ -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<TunnelConfig?>(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,
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+3
-17
@@ -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.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+12
-10
@@ -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(
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
<string name="turn_off_tunnel">Turn off tunnel before editing</string>
|
||||
<string name="no_tunnels">No tunnels added yet!</string>
|
||||
<string name="tunnel_exists">Tunnel name already exists</string>
|
||||
<string name="discord_url">https://discord.gg/Ad5fuEts</string>
|
||||
<string name="discord_url">https://discord.gg/rbRRNh6H7V</string>
|
||||
<string name="watcher_notification_title">Watcher Service</string>
|
||||
<string name="watcher_notification_text">Monitoring network state changes</string>
|
||||
<string name="tunnel_start_title">VPN Connected</string>
|
||||
@@ -37,4 +37,24 @@
|
||||
<string name="support_text">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!</string>
|
||||
<string name="trusted_ssid_empty_description">Enter SSID</string>
|
||||
<string name="trusted_ssid_value_description">Submit SSID</string>
|
||||
<string name="config_validation">[Interface]</string>
|
||||
<string name="invalid_qr">Invalid QR code.</string>
|
||||
<string name="add_from_file">Add tunnel from files</string>
|
||||
<string name="open_file">File Open</string>
|
||||
<string name="add_from_qr">Add tunnel from QR code</string>
|
||||
<string name="qr_scan">QR Scan</string>
|
||||
<string name="tunnel_edit">Tunnel Edit</string>
|
||||
<string name="tunnel_name">Tunnel Name</string>
|
||||
<string name="edit">Edit</string>
|
||||
<string name="delete">Delete</string>
|
||||
<string name="add_tunnel">Add Tunnel</string>
|
||||
<string name="exclude">Exclude</string>
|
||||
<string name="include">Include</string>
|
||||
<string name="tunnel_all">Tunnel all applications</string>
|
||||
<string name="config_changes_saved">Configuration changes saved.</string>
|
||||
<string name="save_changes">Save changes</string>
|
||||
<string name="icon">Icon</string>
|
||||
<string name="no_thanks">No thanks</string>
|
||||
<string name="turn_on">Turn on</string>
|
||||
<string name="map">Map</string>
|
||||
</resources>
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 154 KiB |
+1
-2
@@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user