diff --git a/README.md b/README.md
index 99a831b3..e1a667ce 100644
--- a/README.md
+++ b/README.md
@@ -38,7 +38,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 3a14efdf..d06fb277 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -14,8 +14,8 @@ android {
applicationId = "com.zaneschepke.wireguardautotunnel"
minSdk = 26
targetSdk = 34
- versionCode = 30003
- versionName = "3.0.3"
+ versionCode = 31000
+ versionName = "3.1.0"
multiDexEnabled = true
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index be059cb5..03404ee2 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -58,6 +58,8 @@
+
+ android:value="true" />
, 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/service/shortcut/ShortcutsActivity.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/shortcut/ShortcutsActivity.kt
index bb5cc307..11bf45d6 100644
--- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/shortcut/ShortcutsActivity.kt
+++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/shortcut/ShortcutsActivity.kt
@@ -1,52 +1,76 @@
package com.zaneschepke.wireguardautotunnel.service.shortcut
import android.os.Bundle
-import androidx.appcompat.app.AppCompatActivity
-import com.zaneschepke.wireguardautotunnel.R
+import androidx.activity.ComponentActivity
import com.zaneschepke.wireguardautotunnel.repository.SettingsDoa
+import com.zaneschepke.wireguardautotunnel.repository.TunnelConfigDao
+import com.zaneschepke.wireguardautotunnel.repository.model.Settings
+import com.zaneschepke.wireguardautotunnel.repository.model.TunnelConfig
import com.zaneschepke.wireguardautotunnel.service.foreground.Action
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
import com.zaneschepke.wireguardautotunnel.service.foreground.WireGuardTunnelService
+import com.zaneschepke.wireguardautotunnel.util.WgTunnelException
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
+import timber.log.Timber
import javax.inject.Inject
@AndroidEntryPoint
-class ShortcutsActivity : AppCompatActivity() {
+class ShortcutsActivity : ComponentActivity() {
@Inject
lateinit var settingsRepo : SettingsDoa
+ @Inject
+ lateinit var tunnelConfigRepo : TunnelConfigDao
+
private val scope = CoroutineScope(Dispatchers.Main);
private fun attemptWatcherServiceToggle(tunnelConfig : String) {
scope.launch {
- val settings = settingsRepo.getAll()
- if (settings.isNotEmpty()) {
- val setting = settings.first()
- if(setting.isAutoTunnelEnabled) {
- ServiceManager.toggleWatcherServiceForeground(this@ShortcutsActivity, tunnelConfig)
- }
+ val settings = getSettings()
+ if(settings.isAutoTunnelEnabled) {
+ ServiceManager.toggleWatcherServiceForeground(this@ShortcutsActivity, tunnelConfig)
}
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
- if(intent.getStringExtra(ShortcutsManager.CLASS_NAME_EXTRA_KEY)
- .equals(WireGuardTunnelService::class.java.name)) {
-
- intent.getStringExtra(getString(R.string.tunnel_extras_key))?.let {
- attemptWatcherServiceToggle(it)
- }
- when(intent.action){
- Action.STOP.name -> ServiceManager.stopVpnService(this)
- Action.START.name -> intent.getStringExtra(getString(R.string.tunnel_extras_key))
- ?.let { ServiceManager.startVpnService(this, it) }
+ if(intent.getStringExtra(CLASS_NAME_EXTRA_KEY)
+ .equals(WireGuardTunnelService::class.java.simpleName)) {
+ scope.launch {
+ try {
+ val settings = getSettings()
+ val tunnelConfig = if(settings.defaultTunnel == null) {
+ tunnelConfigRepo.getAll().first()
+ } else {
+ TunnelConfig.from(settings.defaultTunnel!!)
+ }
+ attemptWatcherServiceToggle(tunnelConfig.toString())
+ when(intent.action){
+ Action.STOP.name -> ServiceManager.stopVpnService(this@ShortcutsActivity)
+ Action.START.name -> ServiceManager.startVpnService(this@ShortcutsActivity, tunnelConfig.toString())
+ }
+ } catch (e : Exception) {
+ Timber.e(e.message)
+ }
}
}
finish()
}
+
+ private suspend fun getSettings() : Settings {
+ val settings = settingsRepo.getAll()
+ return if (settings.isNotEmpty()) {
+ settings.first()
+ } else {
+ throw WgTunnelException("Settings empty")
+ }
+ }
+ companion object {
+ const val CLASS_NAME_EXTRA_KEY = "className"
+ }
}
\ No newline at end of file
diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/shortcut/ShortcutsManager.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/shortcut/ShortcutsManager.kt
deleted file mode 100644
index 3b02d192..00000000
--- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/shortcut/ShortcutsManager.kt
+++ /dev/null
@@ -1,75 +0,0 @@
-package com.zaneschepke.wireguardautotunnel.service.shortcut
-
-import android.content.Context
-import android.content.Intent
-import androidx.core.content.pm.ShortcutInfoCompat
-import androidx.core.content.pm.ShortcutManagerCompat
-import androidx.core.graphics.drawable.IconCompat
-import com.zaneschepke.wireguardautotunnel.R
-import com.zaneschepke.wireguardautotunnel.service.foreground.Action
-import com.zaneschepke.wireguardautotunnel.service.foreground.WireGuardTunnelService
-import com.zaneschepke.wireguardautotunnel.repository.model.TunnelConfig
-
-object ShortcutsManager {
-
- private const val SHORT_LABEL_MAX_SIZE = 10;
- private const val LONG_LABEL_MAX_SIZE = 25;
- private const val APPEND_ON = " On";
- private const val APPEND_OFF = " Off"
- const val CLASS_NAME_EXTRA_KEY = "className"
-
- private fun createAndPushShortcut(context : Context, intent : Intent, id : String, shortLabel : String,
- longLabel : String, drawable : Int ) {
- val shortcut = ShortcutInfoCompat.Builder(context, id)
- .setShortLabel(shortLabel)
- .setLongLabel(longLabel)
- .setIcon(IconCompat.createWithResource(context, drawable))
- .setIntent(intent)
- .build()
- ShortcutManagerCompat.pushDynamicShortcut(context, shortcut)
- }
-
- fun createTunnelShortcuts(context : Context, tunnelConfig : TunnelConfig) {
- createAndPushShortcut(context,
- createTunnelOnIntent(context, mapOf(context.getString(R.string.tunnel_extras_key) to tunnelConfig.toString())),
- tunnelConfig.id.toString() + APPEND_ON,
- tunnelConfig.name.take((SHORT_LABEL_MAX_SIZE - APPEND_ON.length)) + APPEND_ON,
- tunnelConfig.name.take((LONG_LABEL_MAX_SIZE - APPEND_ON.length)) + APPEND_ON,
- R.drawable.vpn_on
- )
- createAndPushShortcut(context,
- createTunnelOffIntent(context, mapOf(context.getString(R.string.tunnel_extras_key) to tunnelConfig.toString())),
- tunnelConfig.id.toString() + APPEND_OFF,
- tunnelConfig.name.take((SHORT_LABEL_MAX_SIZE - APPEND_OFF.length)) + APPEND_OFF,
- tunnelConfig.name.take((LONG_LABEL_MAX_SIZE - APPEND_OFF.length)) + APPEND_OFF,
- R.drawable.vpn_off
- )
- }
-
- fun removeTunnelShortcuts(context : Context, tunnelConfig : TunnelConfig?) {
- if(tunnelConfig != null) {
- ShortcutManagerCompat.removeDynamicShortcuts(context, listOf(tunnelConfig.id.toString() + APPEND_ON,
- tunnelConfig.id.toString() + APPEND_OFF ))
- }
- }
-
- private fun createTunnelOnIntent(context: Context, extras : Map) : Intent {
- return Intent(context, ShortcutsActivity::class.java).also {
- it.action = Action.START.name
- it.putExtra(CLASS_NAME_EXTRA_KEY, WireGuardTunnelService::class.java.name)
- extras.forEach {(k, v) ->
- it.putExtra(k, v)
- }
- }
- }
-
- private fun createTunnelOffIntent(context : Context, extras : Map) : Intent {
- return Intent(context, ShortcutsActivity::class.java).also {
- it.action = Action.STOP.name
- it.putExtra(CLASS_NAME_EXTRA_KEY, WireGuardTunnelService::class.java.name)
- extras.forEach {(k, v) ->
- it.putExtra(k, v)
- }
- }
- }
-}
\ No newline at end of file
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 548355ae..ff9d8f82 100644
--- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/MainActivity.kt
+++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/MainActivity.kt
@@ -15,9 +15,14 @@ import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn
import androidx.compose.animation.slideInHorizontally
+import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
+import androidx.compose.material3.SnackbarData
+import androidx.compose.material3.SnackbarDuration
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
+import androidx.compose.material3.SnackbarResult
+import androidx.compose.material3.surfaceColorAtElevation
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
@@ -26,6 +31,7 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.input.key.onKeyEvent
+import androidx.compose.ui.unit.dp
import com.google.accompanist.navigation.animation.AnimatedNavHost
import com.google.accompanist.navigation.animation.composable
import com.google.accompanist.navigation.animation.rememberAnimatedNavController
@@ -35,6 +41,7 @@ import com.google.accompanist.permissions.rememberPermissionState
import com.wireguard.android.backend.GoBackend
import com.zaneschepke.wireguardautotunnel.Constants
import com.zaneschepke.wireguardautotunnel.R
+import com.zaneschepke.wireguardautotunnel.ui.common.CustomSnackBar
import com.zaneschepke.wireguardautotunnel.ui.common.PermissionRequestFailedScreen
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.BottomNavBar
import com.zaneschepke.wireguardautotunnel.ui.screens.config.ConfigScreen
@@ -44,10 +51,11 @@ import com.zaneschepke.wireguardautotunnel.ui.screens.settings.SettingsScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.support.SupportScreen
import com.zaneschepke.wireguardautotunnel.ui.theme.TransparentSystemBars
import com.zaneschepke.wireguardautotunnel.ui.theme.WireguardAutoTunnelTheme
-import com.zaneschepke.wireguardautotunnel.util.WgTunnelException
import dagger.hilt.android.AndroidEntryPoint
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
import timber.log.Timber
-import java.lang.IllegalStateException
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
@@ -90,7 +98,29 @@ class MainActivity : AppCompatActivity() {
} else requestNotificationPermission()
}
- Scaffold(snackbarHost = { SnackbarHost(snackbarHostState)},
+ fun showSnackBarMessage(message : String) {
+ CoroutineScope(Dispatchers.Main).launch {
+ val result = snackbarHostState.showSnackbar(
+ message = message,
+ actionLabel = "Okay",
+ duration = SnackbarDuration.Short,
+ )
+ when (result) {
+ SnackbarResult.ActionPerformed -> { snackbarHostState.currentSnackbarData?.dismiss() }
+ SnackbarResult.Dismissed -> { snackbarHostState.currentSnackbarData?.dismiss() }
+ }
+ }
+ }
+
+ Scaffold(snackbarHost = {
+ SnackbarHost(snackbarHostState) { snackbarData: SnackbarData ->
+ CustomSnackBar(
+ snackbarData.visuals.message,
+ isRtl = false,
+ containerColor = MaterialTheme.colorScheme.surfaceColorAtElevation(2.dp)
+ )
+ }
+ },
modifier = Modifier.onKeyEvent {
if (it.nativeKeyEvent.action == KeyEvent.ACTION_UP) {
when (it.nativeKeyEvent.keyCode) {
@@ -140,6 +170,7 @@ class MainActivity : AppCompatActivity() {
)
return@Scaffold
}
+
AnimatedNavHost(navController, startDestination = Routes.Main.name) {
composable(Routes.Main.name, enterTransition = {
when (initialState.destination.route) {
@@ -154,7 +185,7 @@ class MainActivity : AppCompatActivity() {
}
}
}) {
- MainScreen(padding = padding, snackbarHostState = snackbarHostState, navController = navController)
+ MainScreen(padding = padding, showSnackbarMessage = { message -> showSnackBarMessage(message) }, navController = navController)
}
composable(Routes.Settings.name, enterTransition = {
when (initialState.destination.route) {
@@ -175,7 +206,7 @@ class MainActivity : AppCompatActivity() {
fadeIn(animationSpec = tween(Constants.FADE_IN_ANIMATION_DURATION))
}
}
- }) { SettingsScreen(padding = padding, snackbarHostState = snackbarHostState, navController = navController, focusRequester = focusRequester) }
+ }) { SettingsScreen(padding = padding, showSnackbarMessage = { message -> showSnackBarMessage(message) }, focusRequester = focusRequester) }
composable(Routes.Support.name, enterTransition = {
when (initialState.destination.route) {
Routes.Settings.name, Routes.Main.name ->
@@ -191,17 +222,17 @@ class MainActivity : AppCompatActivity() {
}) { SupportScreen(padding = padding, focusRequester) }
composable("${Routes.Config.name}/{id}", enterTransition = {
fadeIn(animationSpec = tween(Constants.FADE_IN_ANIMATION_DURATION))
- }) {
+ }) { it ->
val id = it.arguments?.getString("id")
if(!id.isNullOrBlank()) {
- ConfigScreen(padding = padding, navController = navController, id = id, focusRequester = focusRequester)}
+ ConfigScreen(navController = navController, id = id, showSnackbarMessage = { message -> showSnackBarMessage(message) }, focusRequester = focusRequester)}
}
composable("${Routes.Detail.name}/{id}", enterTransition = {
fadeIn(animationSpec = tween(Constants.FADE_IN_ANIMATION_DURATION))
}) {
val id = it.arguments?.getString("id")
if(!id.isNullOrBlank()) {
- DetailScreen(padding = padding, id = id)
+ DetailScreen(padding = padding, focusRequester = focusRequester, id = id)
}
}
}
diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/ClickableIconButton.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/ClickableIconButton.kt
index 6644796e..3378aef5 100644
--- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/ClickableIconButton.kt
+++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/ClickableIconButton.kt
@@ -3,24 +3,26 @@ package com.zaneschepke.wireguardautotunnel.ui.common
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.size
-import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
+import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
+import androidx.compose.ui.res.stringResource
+import com.zaneschepke.wireguardautotunnel.R
@Composable
fun ClickableIconButton(onIconClick : () -> Unit, text : String, icon : ImageVector, enabled : Boolean) {
- Button(onClick = {},
+ TextButton(onClick = {},
enabled = enabled
) {
Text(text)
Spacer(modifier = Modifier.size(ButtonDefaults.IconSpacing))
Icon(
imageVector = icon,
- contentDescription = "Delete",
+ contentDescription = stringResource(R.string.delete),
modifier = Modifier.size(ButtonDefaults.IconSize).clickable {
if(enabled) {
onIconClick()
diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/CustomSnackbar.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/CustomSnackbar.kt
new file mode 100644
index 00000000..24cdd839
--- /dev/null
+++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/CustomSnackbar.kt
@@ -0,0 +1,58 @@
+package com.zaneschepke.wireguardautotunnel.ui.common
+
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.rounded.Info
+import androidx.compose.material3.Icon
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Snackbar
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.CompositionLocalProvider
+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.LocalLayoutDirection
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.LayoutDirection
+import androidx.compose.ui.unit.dp
+import com.zaneschepke.wireguardautotunnel.R
+import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
+
+@Composable
+fun CustomSnackBar(
+ message: String,
+ isRtl: Boolean = true,
+ containerColor: Color = MaterialTheme.colorScheme.surface
+) {
+ val context = LocalContext.current
+ Snackbar(containerColor = containerColor,
+ modifier = Modifier.fillMaxWidth(
+ if(WireGuardAutoTunnel.isRunningOnAndroidTv(context)) 1/3f else 2/3f).padding(bottom = 100.dp),
+ shape = RoundedCornerShape(16.dp)
+ ) {
+ CompositionLocalProvider(
+ LocalLayoutDirection provides
+ if (isRtl) LayoutDirection.Rtl else LayoutDirection.Ltr
+ ) {
+ Row(
+ modifier = Modifier.fillMaxSize(),
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.SpaceEvenly
+ ) {
+ Icon(
+ Icons.Rounded.Info,
+ contentDescription = stringResource(R.string.info),
+ tint = Color.White
+ )
+ Text(message, color = Color.White, modifier = Modifier.padding(end = 5.dp))
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/RowListItem.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/RowListItem.kt
index 2a9ce58d..f254a36f 100644
--- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/RowListItem.kt
+++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/RowListItem.kt
@@ -19,7 +19,7 @@ import androidx.compose.ui.unit.dp
@OptIn(ExperimentalFoundationApi::class)
@Composable
-fun RowListItem(leadingIcon : ImageVector? = null, leadingIconColor : Color = Color.Gray, text : String, onHold : () -> Unit, onClick: () -> Unit, rowButton : @Composable() () -> Unit ) {
+fun RowListItem(icon : @Composable() () -> Unit, text : String, onHold : () -> Unit, onClick: () -> Unit, rowButton : @Composable() () -> Unit ) {
Box(
modifier = Modifier
.combinedClickable(
@@ -39,13 +39,7 @@ fun RowListItem(leadingIcon : ImageVector? = null, leadingIconColor : Color = Co
horizontalArrangement = Arrangement.SpaceBetween
) {
Row(verticalAlignment = Alignment.CenterVertically,) {
- if(leadingIcon != null) {
- Icon(
- leadingIcon, "status",
- tint = leadingIconColor,
- modifier = Modifier.padding(end = 10.dp).size(15.dp)
- )
- }
+ icon()
Text(text)
}
diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/config/ConfigurationTextBox.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/config/ConfigurationTextBox.kt
new file mode 100644
index 00000000..ab03e6b9
--- /dev/null
+++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/config/ConfigurationTextBox.kt
@@ -0,0 +1,38 @@
+package com.zaneschepke.wireguardautotunnel.ui.common.config
+
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.text.KeyboardActions
+import androidx.compose.foundation.text.KeyboardOptions
+import androidx.compose.material3.OutlinedTextField
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.text.input.ImeAction
+import androidx.compose.ui.text.input.KeyboardCapitalization
+
+@Composable
+fun
+ ConfigurationTextBox(value : String, hint : String, onValueChange : (String) -> Unit, label : String, onDone : () -> Unit, modifier: Modifier) {
+ OutlinedTextField(
+ modifier = modifier,
+ value = value,
+ singleLine = true,
+ onValueChange = {
+ onValueChange(it)
+ },
+ label = { Text(label) },
+ maxLines = 1,
+ placeholder = {
+ Text(hint)
+ },
+ keyboardOptions = KeyboardOptions(
+ capitalization = KeyboardCapitalization.None,
+ imeAction = ImeAction.Done
+ ),
+ keyboardActions = KeyboardActions(
+ onDone = {
+ onDone()
+ }
+ ),
+ )
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/config/ConfigurationToggle.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/config/ConfigurationToggle.kt
new file mode 100644
index 00000000..eadebd10
--- /dev/null
+++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/config/ConfigurationToggle.kt
@@ -0,0 +1,34 @@
+package com.zaneschepke.wireguardautotunnel.ui.common.config
+
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.Switch
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.Dp
+
+@Composable
+fun ConfigurationToggle(label : String, enabled : Boolean, checked : Boolean, padding : Dp,
+ onCheckChanged : () -> Unit) {
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(padding),
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.SpaceBetween
+ ) {
+ Text(label)
+ Switch(
+ enabled = enabled,
+ checked = checked,
+ onCheckedChange = {
+ onCheckChanged()
+ }
+ )
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/text/SectionTitle.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/text/SectionTitle.kt
new file mode 100644
index 00000000..721cf773
--- /dev/null
+++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/text/SectionTitle.kt
@@ -0,0 +1,22 @@
+package com.zaneschepke.wireguardautotunnel.ui.common.text
+
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+
+@Composable
+fun SectionTitle(title : String, padding : Dp) {
+ Text(
+ title,
+ textAlign = TextAlign.Center,
+ style = TextStyle(fontSize = 18.sp, fontWeight = FontWeight.ExtraBold),
+ modifier = Modifier.padding(padding, bottom = 5.dp, top = 5.dp)
+ )
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/models/InterfaceProxy.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/models/InterfaceProxy.kt
new file mode 100644
index 00000000..47eab5b8
--- /dev/null
+++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/models/InterfaceProxy.kt
@@ -0,0 +1,26 @@
+package com.zaneschepke.wireguardautotunnel.ui.models
+
+import com.wireguard.config.Interface
+import com.wireguard.config.Peer
+
+data class InterfaceProxy(
+ var privateKey : String = "",
+ var publicKey : String = "",
+ var addresses : String = "",
+ var dnsServers : String = "",
+ var listenPort : String = "",
+ var mtu : String = "",
+){
+ companion object {
+ fun from(i : Interface) : InterfaceProxy {
+ return InterfaceProxy(
+ publicKey = i.keyPair.publicKey.toBase64(),
+ privateKey = i.keyPair.privateKey.toBase64(),
+ addresses = i.addresses.joinToString(","),
+ dnsServers = i.dnsServers.joinToString(",").replace("/", ""),
+ listenPort = if(i.listenPort.isPresent) i.listenPort.get().toString() else "",
+ mtu = if(i.mtu.isPresent) i.mtu.get().toString() else ""
+ )
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/models/PeerProxy.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/models/PeerProxy.kt
new file mode 100644
index 00000000..2bb6abb3
--- /dev/null
+++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/models/PeerProxy.kt
@@ -0,0 +1,32 @@
+package com.zaneschepke.wireguardautotunnel.ui.models
+
+import com.wireguard.config.Peer
+
+data class PeerProxy(
+ var publicKey : String = "",
+ var preSharedKey : String = "",
+ var persistentKeepalive : String = "",
+ var endpoint : String = "",
+ var allowedIps: String = IPV4_WILDCARD.joinToString(",")
+){
+ companion object {
+ fun from(peer : Peer) : PeerProxy {
+ return PeerProxy(
+ publicKey = peer.publicKey.toBase64(),
+ preSharedKey = if(peer.preSharedKey.isPresent) peer.preSharedKey.get().toString() else "",
+ persistentKeepalive = if(peer.persistentKeepalive.isPresent) peer.persistentKeepalive.get().toString() else "",
+ endpoint = if(peer.endpoint.isPresent) peer.endpoint.get().toString() else "",
+ allowedIps = peer.allowedIps.joinToString(",")
+ )
+ }
+ val IPV4_PUBLIC_NETWORKS = setOf(
+ "0.0.0.0/5", "8.0.0.0/7", "11.0.0.0/8", "12.0.0.0/6", "16.0.0.0/4", "32.0.0.0/3",
+ "64.0.0.0/2", "128.0.0.0/3", "160.0.0.0/5", "168.0.0.0/6", "172.0.0.0/12",
+ "172.32.0.0/11", "172.64.0.0/10", "172.128.0.0/9", "173.0.0.0/8", "174.0.0.0/7",
+ "176.0.0.0/4", "192.0.0.0/9", "192.128.0.0/11", "192.160.0.0/13", "192.169.0.0/16",
+ "192.170.0.0/15", "192.172.0.0/14", "192.176.0.0/12", "192.192.0.0/10",
+ "193.0.0.0/8", "194.0.0.0/7", "196.0.0.0/6", "200.0.0.0/5", "208.0.0.0/4"
+ )
+ val IPV4_WILDCARD = setOf("0.0.0.0/0")
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/config/ConfigScreen.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/config/ConfigScreen.kt
index ccb75b6d..64b583ac 100644
--- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/config/ConfigScreen.kt
+++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/config/ConfigScreen.kt
@@ -1,142 +1,190 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.config
-import android.widget.Toast
+import android.annotation.SuppressLint
+import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.Image
+import androidx.compose.foundation.focusGroup
import androidx.compose.foundation.layout.Arrangement
-import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.IntrinsicSize
import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+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.layout.width
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
+import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.Android
-import androidx.compose.material3.Button
+import androidx.compose.material.icons.rounded.ContentCopy
+import androidx.compose.material.icons.rounded.Delete
+import androidx.compose.material.icons.rounded.Refresh
+import androidx.compose.material.icons.rounded.Save
+import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Checkbox
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.FabPosition
+import androidx.compose.material3.FloatingActionButton
import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.Surface
import androidx.compose.material3.Switch
import androidx.compose.material3.Text
+import androidx.compose.material3.TextButton
+import androidx.compose.material3.surfaceColorAtElevation
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
+import androidx.compose.ui.focus.onFocusChanged
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.platform.ClipboardManager
+import androidx.compose.ui.platform.LocalClipboardManager
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.AnnotatedString
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardCapitalization
+import androidx.compose.ui.text.input.PasswordVisualTransformation
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.Constants
import com.zaneschepke.wireguardautotunnel.R
+import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
import com.zaneschepke.wireguardautotunnel.ui.Routes
import com.zaneschepke.wireguardautotunnel.ui.common.SearchBar
+import com.zaneschepke.wireguardautotunnel.ui.common.config.ConfigurationTextBox
+import com.zaneschepke.wireguardautotunnel.ui.common.text.SectionTitle
import kotlinx.coroutines.launch
+import timber.log.Timber
-@OptIn(ExperimentalComposeUiApi::class)
+@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter")
+@OptIn(ExperimentalComposeUiApi::class, ExperimentalMaterial3Api::class,
+ ExperimentalFoundationApi::class
+)
@Composable
fun ConfigScreen(
viewModel: ConfigViewModel = hiltViewModel(),
- padding: PaddingValues,
focusRequester: FocusRequester,
navController: NavController,
- id : String
+ showSnackbarMessage: (String) -> Unit,
+ id: String
) {
val context = LocalContext.current
val focusManager = LocalFocusManager.current
-
- val keyboardController = LocalSoftwareKeyboardController.current
val scope = rememberCoroutineScope()
+ val clipboardManager: ClipboardManager = LocalClipboardManager.current
+ val keyboardController = LocalSoftwareKeyboardController.current
+
+
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()
- val sortedPackages = remember(packages) {
- packages.sortedBy { viewModel.getPackageLabel(it) }
- }
+ val isAllApplicationsEnabled by viewModel.isAllApplicationsEnabled.collectAsStateWithLifecycle()
+ val proxyPeers by viewModel.proxyPeers.collectAsStateWithLifecycle()
+ val proxyInterface by viewModel.interfaceProxy.collectAsStateWithLifecycle()
+ var showApplicationsDialog by remember { mutableStateOf(false) }
+
+ val keyboardActions = KeyboardActions(
+ onDone = {
+ focusManager.clearFocus()
+ keyboardController?.hide()
+ }
+ )
+
+ val keyboardOptions = KeyboardOptions(
+ capitalization = KeyboardCapitalization.None,
+ imeAction = ImeAction.Done
+ )
+
+ val fillMaxHeight = .85f
+ val fillMaxWidth = .85f
+ val screenPadding = 5.dp
LaunchedEffect(Unit) {
- viewModel.emitScreenData(id)
+ try {
+ viewModel.onScreenLoad(id)
+ } catch (e : Exception) {
+ showSnackbarMessage(e.message!!)
+ navController.navigate(Routes.Main.name)
+ }
}
- if(tunnel != null) {
- LazyColumn(
- horizontalAlignment = Alignment.Start,
- verticalArrangement = Arrangement.Top,
- modifier = Modifier
- .fillMaxSize()
- .padding(padding)
- ) {
- item {
- Row(
- modifier = Modifier
- .fillMaxWidth()
- .padding(horizontal = 20.dp, vertical = 7.dp),
- verticalAlignment = Alignment.CenterVertically,
- horizontalArrangement = Arrangement.SpaceBetween
+ val applicationButtonText = {
+ "Tunneling apps: " +
+ if (isAllApplicationsEnabled) "all"
+ else "${checkedPackages.size} " + (if (include) "included" else "excluded")
+
+ }
+ if (showApplicationsDialog) {
+ val sortedPackages = remember(packages) {
+ packages.sortedBy { viewModel.getPackageLabel(it) }
+ }
+ AlertDialog(onDismissRequest = {
+ showApplicationsDialog = false
+ }) {
+ Surface(
+ tonalElevation = 2.dp,
+ shadowElevation = 2.dp,
+ shape = RoundedCornerShape(12.dp),
+ color = MaterialTheme.colorScheme.surface,
+ modifier = Modifier
+ .fillMaxWidth()
+ .fillMaxHeight(if (isAllApplicationsEnabled) 1 / 5f else 4 / 5f)
+ ) {
+ Column(
+ modifier = Modifier.fillMaxWidth()
) {
- OutlinedTextField(
- modifier = Modifier.focusRequester(focusRequester),
- 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 = isAllApplicationsEnabled,
+ onCheckedChange = {
+ viewModel.onAllApplicationsChange(it)
}
- ),
- )
- }
- }
- item {
- 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) {
- item {
+ )
+ }
+ if (!isAllApplicationsEnabled) {
Row(
modifier = Modifier
.fillMaxWidth()
- .padding(horizontal = 20.dp, vertical = 7.dp),
+ .padding(
+ horizontal = 20.dp,
+ vertical = 7.dp
+ ),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween
) {
@@ -165,77 +213,446 @@ fun ConfigScreen(
)
}
}
- }
- item {
Row(
modifier = Modifier
.fillMaxWidth()
- .padding(horizontal = 20.dp, vertical = 7.dp),
+ .padding(
+ horizontal = 20.dp,
+ vertical = 7.dp
+ ),
verticalAlignment = Alignment.CenterVertically,
- horizontalArrangement = Arrangement.SpaceBetween) {
+ horizontalArrangement = Arrangement.SpaceBetween
+ ) {
SearchBar(viewModel::emitQueriedPackages);
}
+ Spacer(Modifier.padding(5.dp))
+ LazyColumn(
+ horizontalAlignment = Alignment.Start,
+ verticalArrangement = Arrangement.Top,
+ modifier = Modifier
+ .fillMaxHeight(4 / 5f)
+ ) {
+ items(
+ sortedPackages,
+ key = { it.packageName }) { pack ->
+ Row(
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.SpaceBetween,
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(5.dp)
+ ) {
+ Row(
+ modifier = Modifier.fillMaxWidth(
+ fillMaxWidth
+ )
+ ) {
+ 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(
+ viewModel.getPackageLabel(pack),
+ modifier = Modifier.padding(5.dp)
+ )
+ }
+ Checkbox(
+ modifier = Modifier.fillMaxSize(),
+ checked = (checkedPackages.contains(pack.packageName)),
+ onCheckedChange = {
+ if (it) viewModel.onAddCheckedPackage(
+ pack.packageName
+ ) else viewModel.onRemoveCheckedPackage(
+ pack.packageName
+ )
+ }
+ )
+ }
+ }
+ }
}
- items(sortedPackages, key = { it.packageName }) { pack ->
- Row(
- verticalAlignment = Alignment.CenterVertically,
- horizontalArrangement = Arrangement.SpaceBetween
+ Row(
+ verticalAlignment = Alignment.CenterVertically,
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(top = 5.dp),
+ horizontalArrangement = Arrangement.Center
+ ) {
+ TextButton(
+ onClick = {
+ showApplicationsDialog = false
+ }) {
+ Text(stringResource(R.string.done))
+ }
+ }
+ }
+ }
+ }
+ }
+
+
+ if (tunnel != null) {
+ Scaffold(
+ floatingActionButtonPosition = FabPosition.End,
+ floatingActionButton = {
+ val secondaryColor = MaterialTheme.colorScheme.secondary
+ val hoverColor = MaterialTheme.colorScheme.surfaceColorAtElevation(2.dp)
+ var fobColor by remember { mutableStateOf(secondaryColor) }
+ FloatingActionButton(
+ modifier = Modifier.padding(bottom = 90.dp).onFocusChanged {
+ if(WireGuardAutoTunnel.isRunningOnAndroidTv(context)) {
+ fobColor = if (it.isFocused) hoverColor else secondaryColor }
+ },
+ onClick = {
+ scope.launch {
+ try {
+ viewModel.onSaveAllChanges()
+ navController.navigate(Routes.Main.name)
+ showSnackbarMessage(context.resources.getString(R.string.config_changes_saved))
+ } catch (e : Exception) {
+ Timber.e(e.message)
+ showSnackbarMessage(e.message!!)
+ }
+ }
+ },
+ containerColor = fobColor,
+ shape = RoundedCornerShape(16.dp),
+ ) {
+ Icon(
+ imageVector = Icons.Rounded.Save,
+ contentDescription = stringResource(id = R.string.save_changes),
+ tint = Color.DarkGray,
+ )
+ }
+ }) {
+ Column {
+ Column(
+ horizontalAlignment = Alignment.CenterHorizontally,
+ verticalArrangement = Arrangement.Top,
+ modifier = Modifier
+ .verticalScroll(rememberScrollState())
+ .weight(1f, true)
+ .fillMaxSize()
+ ) {
+ Surface(
+ tonalElevation = 2.dp,
+ shadowElevation = 2.dp,
+ shape = RoundedCornerShape(12.dp),
+ color = MaterialTheme.colorScheme.surface,
+ modifier = (if (WireGuardAutoTunnel.isRunningOnAndroidTv(context))
+ Modifier
+ .fillMaxHeight(fillMaxHeight)
+ .fillMaxWidth(fillMaxWidth)
+ else Modifier.fillMaxWidth(fillMaxWidth)).padding(
+ top = 50.dp,
+ bottom = 10.dp
+ )
+ ) {
+ Column(
+ horizontalAlignment = Alignment.Start,
+ verticalArrangement = Arrangement.Top,
+ modifier = Modifier.padding(15.dp).focusGroup()
) {
+ SectionTitle(stringResource(R.string.interface_), padding = screenPadding)
+ ConfigurationTextBox(
+ value = tunnelName.value,
+ onValueChange = { value ->
+ viewModel.onTunnelNameChange(value)
+ },
+ onDone = {
+ focusManager.clearFocus()
+ keyboardController?.hide()
+ },
+ label = stringResource(R.string.name),
+ hint = stringResource(R.string.tunnel_name).lowercase(),
+ modifier = Modifier.fillMaxWidth().focusRequester(focusRequester)
+ )
+ OutlinedTextField(
+ modifier = Modifier.fillMaxWidth(),
+ value = proxyInterface.privateKey,
+ visualTransformation = PasswordVisualTransformation(),
+ enabled = id == Constants.MANUAL_TUNNEL_CONFIG_ID,
+ onValueChange = { value ->
+ viewModel.onPrivateKeyChange(value)
+ },
+ trailingIcon = {
+ IconButton(
+ modifier = Modifier.focusRequester(FocusRequester.Default),
+ onClick = {
+ viewModel.generateKeyPair()
+ }) {
+ Icon(
+ Icons.Rounded.Refresh,
+ stringResource(R.string.rotate_keys),
+ tint = Color.White
+ )
+ }
+ },
+ label = { Text(stringResource(R.string.private_key)) },
+ singleLine = true,
+ placeholder = { Text(stringResource(R.string.base64_key)) },
+ keyboardOptions = keyboardOptions,
+ keyboardActions = keyboardActions
+ )
+ OutlinedTextField(
+ modifier = Modifier.fillMaxWidth().focusRequester(FocusRequester.Default),
+ value = proxyInterface.publicKey,
+ enabled = false,
+ onValueChange = {},
+ trailingIcon = {
+ IconButton(
+ modifier = Modifier.focusRequester(FocusRequester.Default),
+ onClick = {
+ clipboardManager.setText(AnnotatedString(proxyInterface.publicKey))
+ }) {
+ Icon(
+ Icons.Rounded.ContentCopy,
+ stringResource(R.string.copy_public_key),
+ tint = Color.White
+ )
+ }
+ },
+ label = { Text(stringResource(R.string.public_key)) },
+ singleLine = true,
+ placeholder = { Text(stringResource(R.string.base64_key)) },
+ keyboardOptions = keyboardOptions,
+ keyboardActions = keyboardActions
+ )
+ Row(modifier = Modifier.fillMaxWidth()) {
+ ConfigurationTextBox(
+ value = proxyInterface.addresses,
+ onValueChange = { value ->
+ viewModel.onAddressesChanged(value)
+ },
+ onDone = {
+ focusManager.clearFocus()
+ keyboardController?.hide()
+ },
+ label = stringResource(R.string.addresses),
+ hint = stringResource(R.string.comma_separated_list),
+ modifier = Modifier
+ .fillMaxWidth(3 / 5f)
+ .padding(end = 5.dp)
+ )
+ ConfigurationTextBox(
+ value = proxyInterface.listenPort,
+ onValueChange = { value -> viewModel.onListenPortChanged(value) },
+ onDone = {
+ focusManager.clearFocus()
+ keyboardController?.hide()
+ },
+ label = stringResource(R.string.listen_port),
+ hint = stringResource(R.string.random),
+ modifier = Modifier.width(IntrinsicSize.Min)
+ )
+ }
+ Row(modifier = Modifier.fillMaxWidth()) {
+ ConfigurationTextBox(
+ value = proxyInterface.dnsServers,
+ onValueChange = { value -> viewModel.onDnsServersChanged(value) },
+ onDone = {
+ focusManager.clearFocus()
+ keyboardController?.hide()
+ },
+ label = stringResource(R.string.dns_servers),
+ hint = stringResource(R.string.comma_separated_list),
+ modifier = Modifier
+ .fillMaxWidth(3 / 5f)
+ .padding(end = 5.dp)
+ )
+ ConfigurationTextBox(
+ value = proxyInterface.mtu,
+ onValueChange = { value -> viewModel.onMtuChanged(value) },
+ onDone = {
+ focusManager.clearFocus()
+ keyboardController?.hide()
+ },
+ label = stringResource(R.string.mtu),
+ hint = stringResource(R.string.auto),
+ modifier = Modifier.width(IntrinsicSize.Min)
+ )
+ }
Row(
- horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.CenterVertically,
- modifier = Modifier.padding(5.dp)
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(top = 5.dp),
+ horizontalArrangement = Arrangement.Center
) {
- 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)
- )
+ TextButton(
+ onClick = {
+ showApplicationsDialog = true
+ }) {
+ Text(applicationButtonText())
}
- Text(
- viewModel.getPackageLabel(pack), modifier = Modifier.padding(5.dp)
- )
}
- Checkbox(
- checked = (checkedPackages.contains(pack.packageName)),
- onCheckedChange = {
- if (it) viewModel.onAddCheckedPackage(pack.packageName) else viewModel.onRemoveCheckedPackage(
- pack.packageName
- )
- }
+ }
+ }
+ proxyPeers.forEachIndexed { index, peer ->
+ Surface(
+ tonalElevation = 2.dp,
+ shadowElevation = 2.dp,
+ shape = RoundedCornerShape(12.dp),
+ color = MaterialTheme.colorScheme.surface,
+ modifier = (if (WireGuardAutoTunnel.isRunningOnAndroidTv(context))
+ Modifier
+ .fillMaxHeight(fillMaxHeight)
+ .fillMaxWidth(fillMaxWidth)
+ else Modifier.fillMaxWidth(fillMaxWidth)).padding(
+ top = 10.dp,
+ bottom = 10.dp
)
+ ) {
+ Column(
+ horizontalAlignment = Alignment.Start,
+ verticalArrangement = Arrangement.Top,
+ modifier = Modifier
+ .padding(horizontal = 15.dp)
+ .padding(bottom = 10.dp)
+ ) {
+ Row(
+ horizontalArrangement = Arrangement.SpaceBetween,
+ verticalAlignment = Alignment.CenterVertically,
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(horizontal = 5.dp)
+ ) {
+ SectionTitle(stringResource(R.string.peer), padding = screenPadding)
+ IconButton(
+ onClick = {
+ viewModel.onDeletePeer(index)
+ }
+ ) {
+ Icon(Icons.Rounded.Delete, stringResource(R.string.delete))
+ }
+ }
+
+ ConfigurationTextBox(
+ value = peer.publicKey,
+ onValueChange = { value ->
+ viewModel.onPeerPublicKeyChange(
+ index,
+ value
+ )
+ },
+ onDone = {
+ focusManager.clearFocus()
+ keyboardController?.hide()
+ },
+ label = stringResource(R.string.public_key),
+ hint = stringResource(R.string.base64_key),
+ modifier = Modifier.fillMaxWidth()
+ )
+ ConfigurationTextBox(
+ value = peer.preSharedKey,
+ onValueChange = { value ->
+ viewModel.onPreSharedKeyChange(
+ index,
+ value
+ )
+ },
+ onDone = {
+ focusManager.clearFocus()
+ keyboardController?.hide()
+ },
+ label = stringResource(R.string.preshared_key),
+ hint = stringResource(R.string.optional),
+ modifier = Modifier.fillMaxWidth()
+ )
+ OutlinedTextField(
+ modifier = Modifier.fillMaxWidth(),
+ value = peer.persistentKeepalive,
+ enabled = true,
+ onValueChange = { value ->
+ viewModel.onPersistentKeepaliveChanged(index, value)
+ },
+ trailingIcon = { Text(stringResource(R.string.seconds), modifier = Modifier.padding(end = 10.dp)) },
+ label = { Text(stringResource(R.string.persistent_keepalive)) },
+ singleLine = true,
+ placeholder = { Text(stringResource(R.string.optional_no_recommend)) },
+ keyboardOptions = keyboardOptions,
+ keyboardActions = keyboardActions
+ )
+ ConfigurationTextBox(
+ value = peer.endpoint,
+ onValueChange = { value ->
+ viewModel.onEndpointChange(
+ index,
+ value
+ )
+ },
+ onDone = {
+ focusManager.clearFocus()
+ keyboardController?.hide()
+ },
+ label = stringResource(R.string.endpoint),
+ hint = stringResource(R.string.endpoint).lowercase(),
+ modifier = Modifier.fillMaxWidth()
+ )
+ OutlinedTextField(
+ modifier = Modifier.fillMaxWidth(),
+ value = peer.allowedIps,
+ enabled = true,
+ onValueChange = { value ->
+ viewModel.onAllowedIpsChange(
+ index,
+ value
+ )
+ },
+ label = { Text(stringResource(R.string.allowed_ips)) },
+ singleLine = true,
+ placeholder = { Text(stringResource(R.string.comma_separated_list)) },
+ keyboardOptions = keyboardOptions,
+ keyboardActions = keyboardActions
+ )
+ }
}
}
- }
- item {
- 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)
+ Row(
+ horizontalArrangement = Arrangement.SpaceEvenly,
+ verticalAlignment = Alignment.CenterVertically,
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(bottom = 140.dp)
+ ) {
+
+ Row(
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.Center
+ ) {
+ TextButton(
+ onClick = {
+ viewModel.addEmptyPeer()
+ }) {
+ Text(stringResource(R.string.add_peer))
+ }
}
- }, Modifier.padding(25.dp)) {
- Text(stringResource(id = R.string.save_changes))
}
}
+ if(WireGuardAutoTunnel.isRunningOnAndroidTv(context)) {
+ Spacer(modifier = Modifier.weight(.17f))
+ }
}
}
}
diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/config/ConfigViewModel.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/config/ConfigViewModel.kt
index 770b47e7..5d68c942 100644
--- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/config/ConfigViewModel.kt
+++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/config/ConfigViewModel.kt
@@ -10,28 +10,43 @@ import androidx.compose.runtime.toMutableStateList
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.wireguard.config.Config
+import com.wireguard.config.Interface
+import com.wireguard.config.Peer
+import com.wireguard.crypto.Key
+import com.wireguard.crypto.KeyPair
+import com.zaneschepke.wireguardautotunnel.Constants
import com.zaneschepke.wireguardautotunnel.repository.SettingsDoa
import com.zaneschepke.wireguardautotunnel.repository.TunnelConfigDao
-import com.zaneschepke.wireguardautotunnel.service.shortcut.ShortcutsManager
import com.zaneschepke.wireguardautotunnel.repository.model.TunnelConfig
+import com.zaneschepke.wireguardautotunnel.ui.models.InterfaceProxy
+import com.zaneschepke.wireguardautotunnel.ui.models.PeerProxy
+import com.zaneschepke.wireguardautotunnel.util.NumberUtils
import com.zaneschepke.wireguardautotunnel.util.WgTunnelException
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
-import timber.log.Timber
+import kotlinx.coroutines.withContext
import javax.inject.Inject
@HiltViewModel
class ConfigViewModel @Inject constructor(private val application : Application,
private val tunnelRepo : TunnelConfigDao,
- private val settingsRepo : SettingsDoa) : ViewModel() {
+ private val settingsRepo : SettingsDoa
+) : ViewModel() {
private val _tunnel = MutableStateFlow(null)
private val _tunnelName = MutableStateFlow("")
val tunnelName get() = _tunnelName.asStateFlow()
val tunnel get() = _tunnel.asStateFlow()
+
+ private var _proxyPeers = MutableStateFlow(mutableStateListOf())
+ val proxyPeers get() = _proxyPeers.asStateFlow()
+
+ private var _interface = MutableStateFlow(InterfaceProxy())
+ val interfaceProxy = _interface.asStateFlow()
+
private val _packages = MutableStateFlow(emptyList())
val packages get() = _packages.asStateFlow()
private val packageManager = application.packageManager
@@ -41,38 +56,91 @@ class ConfigViewModel @Inject constructor(private val application : Application,
private val _include = MutableStateFlow(true)
val include get() = _include.asStateFlow()
- private val _allApplications = MutableStateFlow(true)
- val allApplications get() = _allApplications.asStateFlow()
+ private val _isAllApplicationsEnabled = MutableStateFlow(false)
+ val isAllApplicationsEnabled get() = _isAllApplicationsEnabled.asStateFlow()
+ private val _isDefaultTunnel = MutableStateFlow(false)
+ val isDefaultTunnel = _isDefaultTunnel.asStateFlow()
- fun emitScreenData(id : String) {
- viewModelScope.launch(Dispatchers.IO) {
- val tunnelConfig = getTunnelConfigById(id);
- emitTunnelConfig(tunnelConfig);
- emitTunnelConfigName(tunnelConfig?.name)
+ private lateinit var tunnelConfig: TunnelConfig
+
+ fun onScreenLoad(id : String) {
+ if(id != Constants.MANUAL_TUNNEL_CONFIG_ID) {
+ viewModelScope.launch(Dispatchers.IO) {
+ tunnelConfig = withContext(this.coroutineContext) {
+ getTunnelConfigById(id) ?: throw WgTunnelException("Config not found")
+ }
+ emitScreenData()
+ }
+ } else {
+ emitEmptyScreenData()
+ }
+ }
+
+ private fun emitEmptyScreenData() {
+ tunnelConfig = TunnelConfig(name = NumberUtils.generateRandomTunnelName(), wgQuick = "")
+ viewModelScope.launch {
+ emitTunnelConfig()
+ emitPeerProxy(PeerProxy())
+ emitInterfaceProxy(InterfaceProxy())
+ emitTunnelConfigName()
+ emitDefaultTunnelStatus()
emitQueriedPackages("")
- emitCurrentPackageConfigurations(id)
+ emitTunnelAllApplicationsEnabled()
+ }
+ }
+
+
+ private suspend fun emitScreenData() {
+ emitTunnelConfig()
+ emitPeersFromConfig()
+ emitInterfaceFromConfig()
+ emitTunnelConfigName()
+ emitDefaultTunnelStatus()
+ emitQueriedPackages("")
+ emitCurrentPackageConfigurations()
+ }
+
+ private suspend fun emitDefaultTunnelStatus() {
+ val settings = settingsRepo.getAll()
+ if(settings.isNotEmpty()) {
+ _isDefaultTunnel.value = settings.first().isTunnelConfigDefault(tunnelConfig)
}
}
+ private fun emitInterfaceFromConfig() {
+ val config = TunnelConfig.configFromQuick(tunnelConfig.wgQuick)
+ _interface.value = InterfaceProxy.from(config.`interface`)
+ }
+
+ private fun emitPeersFromConfig() {
+ val config = TunnelConfig.configFromQuick(tunnelConfig.wgQuick)
+ config.peers.forEach{
+ _proxyPeers.value.add(PeerProxy.from(it))
+ }
+ }
+
+ private fun emitPeerProxy(peerProxy: PeerProxy) {
+ _proxyPeers.value.add(peerProxy)
+ }
+
+ private fun emitInterfaceProxy(interfaceProxy: InterfaceProxy) {
+ _interface.value = interfaceProxy
+ }
+
private suspend fun getTunnelConfigById(id : String) : TunnelConfig? {
return try {
tunnelRepo.getById(id.toLong())
- } catch (e : Exception) {
- Timber.e(e.message)
+ } catch (_ : Exception) {
null
}
}
- private suspend fun emitTunnelConfig(tunnelConfig: TunnelConfig?) {
- if(tunnelConfig != null) {
- _tunnel.emit(tunnelConfig)
- }
+ private suspend fun emitTunnelConfig() {
+ _tunnel.emit(tunnelConfig)
}
- private suspend fun emitTunnelConfigName(name : String?) {
- if(name != null) {
- _tunnelName.emit(name)
- }
+ private suspend fun emitTunnelConfigName() {
+ _tunnelName.emit(tunnelConfig.name)
}
fun onTunnelNameChange(name : String) {
@@ -86,8 +154,8 @@ class ConfigViewModel @Inject constructor(private val application : Application,
_checkedPackages.value.add(packageName)
}
- fun onAllApplicationsChange(allApplications : Boolean) {
- _allApplications.value = allApplications
+ fun onAllApplicationsChange(isAllApplicationsEnabled : Boolean) {
+ _isAllApplicationsEnabled.value = isAllApplicationsEnabled
}
fun onRemoveCheckedPackage(packageName : String) {
@@ -128,20 +196,17 @@ class ConfigViewModel @Inject constructor(private val application : Application,
}
private suspend fun emitTunnelAllApplicationsEnabled() {
- _allApplications.emit(true)
+ _isAllApplicationsEnabled.emit(true)
}
private suspend fun emitTunnelAllApplicationsDisabled() {
- _allApplications.emit(false)
+ _isAllApplicationsEnabled.emit(false)
}
- private fun emitCurrentPackageConfigurations(id : String) {
+ private fun emitCurrentPackageConfigurations() {
viewModelScope.launch(Dispatchers.IO) {
- val tunnelConfig = getTunnelConfigById(id)
- if (tunnelConfig != null) {
- val config = TunnelConfig.configFromQuick(tunnelConfig.wgQuick)
- emitSplitTunnelConfiguration(config)
- }
+ val config = TunnelConfig.configFromQuick(tunnelConfig.wgQuick)
+ emitSplitTunnelConfiguration(config)
}
}
@@ -171,44 +236,20 @@ class ConfigViewModel @Inject constructor(private val application : Application,
}
}
- private fun removeTunnelShortcuts(tunnelConfig: TunnelConfig?) {
- if(tunnelConfig != null) {
- ShortcutsManager.removeTunnelShortcuts(application, tunnelConfig)
- }
-
- }
-
private fun isAllApplicationsEnabled() : Boolean {
- return _allApplications.value
+ return _isAllApplicationsEnabled.value
}
private fun isIncludeApplicationsEnabled() : Boolean {
return _include.value
}
- private fun updateQuickStringWithSelectedPackages() : String {
- var wgQuick = _tunnel.value?.wgQuick
- if(wgQuick != null) {
- wgQuick = if(isAllApplicationsEnabled()) {
- TunnelConfig.clearAllApplicationsFromConfig(wgQuick)
- } else if(isIncludeApplicationsEnabled()) {
- TunnelConfig.setIncludedApplicationsOnQuick(_checkedPackages.value, wgQuick)
- } else {
- TunnelConfig.setExcludedApplicationsOnQuick(_checkedPackages.value, wgQuick)
- }
- } else {
- throw WgTunnelException("Wg quick string is null")
- }
- return wgQuick;
- }
-
private suspend fun saveConfig(tunnelConfig: TunnelConfig) {
tunnelRepo.save(tunnelConfig)
}
private suspend fun updateTunnelConfig(tunnelConfig: TunnelConfig?) {
if(tunnelConfig != null) {
saveConfig(tunnelConfig)
- addTunnelShortcuts(tunnelConfig)
updateSettingsDefaultTunnel(tunnelConfig)
}
}
@@ -227,21 +268,133 @@ class ConfigViewModel @Inject constructor(private val application : Application,
}
}
- private fun addTunnelShortcuts(tunnelConfig: TunnelConfig) {
- ShortcutsManager.createTunnelShortcuts(application, tunnelConfig)
+ fun buildPeerListFromProxyPeers() : List {
+ return _proxyPeers.value.map {
+ val builder = Peer.Builder()
+ if (it.allowedIps.isNotEmpty()) builder.parseAllowedIPs(it.allowedIps)
+ if (it.publicKey.isNotEmpty()) builder.parsePublicKey(it.publicKey)
+ if (it.preSharedKey.isNotEmpty()) builder.parsePreSharedKey(it.preSharedKey)
+ if (it.endpoint.isNotEmpty()) builder.parseEndpoint(it.endpoint)
+ if (it.persistentKeepalive.isNotEmpty()) builder.parsePersistentKeepalive(it.persistentKeepalive)
+ builder.build()
+ }
+ }
+
+ fun buildInterfaceListFromProxyInterface() : Interface {
+ val builder = Interface.Builder()
+ builder.parsePrivateKey(_interface.value.privateKey)
+ builder.parseAddresses(_interface.value.addresses)
+ builder.parseDnsServers(_interface.value.dnsServers)
+ if(_interface.value.mtu.isNotEmpty()) builder.parseMtu(_interface.value.mtu)
+ if(_interface.value.listenPort.isNotEmpty()) builder.parseListenPort(_interface.value.listenPort)
+ if(isAllApplicationsEnabled()) _checkedPackages.value.clear()
+ if(_include.value) builder.includeApplications(_checkedPackages.value)
+ if(!_include.value) builder.excludeApplications(_checkedPackages.value)
+ return builder.build()
}
+
+
suspend fun onSaveAllChanges() {
try {
- removeTunnelShortcuts(_tunnel.value)
- val wgQuick = updateQuickStringWithSelectedPackages()
+ val peerList = buildPeerListFromProxyPeers()
+ val wgInterface = buildInterfaceListFromProxyInterface()
+ val config = Config.Builder().addPeers(peerList).setInterface(wgInterface).build()
val tunnelConfig = _tunnel.value?.copy(
name = _tunnelName.value,
- wgQuick = wgQuick
+ wgQuick = config.toWgQuickString()
)
updateTunnelConfig(tunnelConfig)
} catch (e : Exception) {
- Timber.e(e.message)
+ throw WgTunnelException("Error: ${e.cause?.message?.lowercase() ?: "unknown error occurred"}")
+ }
+ }
+
+ fun onPeerPublicKeyChange(index: Int, publicKey: String) {
+ _proxyPeers.value[index] = _proxyPeers.value[index].copy(
+ publicKey = publicKey
+ )
+ }
+
+ fun onPreSharedKeyChange(index: Int, value: String) {
+ _proxyPeers.value[index] = _proxyPeers.value[index].copy(
+ preSharedKey = value
+ )
+ }
+
+ fun onEndpointChange(index: Int, value: String) {
+ _proxyPeers.value[index] = _proxyPeers.value[index].copy(
+ endpoint = value
+ )
+ }
+
+ fun onAllowedIpsChange(index: Int, value: String) {
+ _proxyPeers.value[index] = _proxyPeers.value[index].copy(
+ allowedIps = value
+ )
+ }
+
+ fun onPersistentKeepaliveChanged(index : Int, value : String) {
+ _proxyPeers.value[index] = _proxyPeers.value[index].copy(
+ persistentKeepalive = value
+ )
+ }
+
+ fun onDeletePeer(index: Int) {
+ proxyPeers.value.removeAt(index)
+ }
+
+ fun addEmptyPeer() {
+ _proxyPeers.value.add(PeerProxy())
+ }
+
+ fun generateKeyPair() {
+ val keyPair = KeyPair()
+ _interface.value = _interface.value.copy(
+ privateKey = keyPair.privateKey.toBase64(),
+ publicKey = keyPair.publicKey.toBase64()
+ )
+ }
+
+ fun onAddressesChanged(value: String) {
+ _interface.value = _interface.value.copy(
+ addresses = value
+ )
+ }
+
+ fun onListenPortChanged(value: String) {
+ _interface.value = _interface.value.copy(
+ listenPort = value
+ )
+ }
+
+ fun onDnsServersChanged(value: String) {
+ _interface.value = _interface.value.copy(
+ dnsServers = value
+ )
+ }
+
+ fun onMtuChanged(value: String) {
+ _interface.value = _interface.value.copy(
+ mtu = value
+ )
+ }
+
+ private fun onInterfacePublicKeyChange(value : String) {
+ _interface.value = _interface.value.copy(
+ publicKey = value
+ )
+ }
+
+ fun onPrivateKeyChange(value: String) {
+ _interface.value = _interface.value.copy(
+ privateKey = value
+ )
+ if(NumberUtils.isValidKey(value)) {
+ val pair = KeyPair(Key.fromBase64(value))
+ onInterfacePublicKeyChange(pair.publicKey.toBase64())
+ } else {
+ onInterfacePublicKeyChange("")
}
}
}
\ No newline at end of file
diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/detail/DetailScreen.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/detail/DetailScreen.kt
index bf19e361..b951d5fc 100644
--- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/detail/DetailScreen.kt
+++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/detail/DetailScreen.kt
@@ -1,11 +1,15 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.detail
+import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.clickable
+import androidx.compose.foundation.focusGroup
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
@@ -17,8 +21,11 @@ import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
+import androidx.compose.ui.focus.FocusRequester
+import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.platform.ClipboardManager
import androidx.compose.ui.platform.LocalClipboardManager
+import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.font.FontStyle
@@ -28,17 +35,21 @@ import androidx.compose.ui.unit.sp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.zaneschepke.wireguardautotunnel.R
+import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
import com.zaneschepke.wireguardautotunnel.util.NumberUtils
import java.time.Duration
import java.time.Instant
+@OptIn(ExperimentalFoundationApi::class)
@Composable
fun DetailScreen(
viewModel: DetailViewModel = hiltViewModel(),
+ focusRequester: FocusRequester,
padding: PaddingValues,
id : String
) {
+ val context = LocalContext.current
val clipboardManager: ClipboardManager = LocalClipboardManager.current
val tunnelStats by viewModel.tunnelStats.collectAsStateWithLifecycle(null)
val tunnel by viewModel.tunnel.collectAsStateWithLifecycle(null)
@@ -62,18 +73,20 @@ fun DetailScreen(
horizontalAlignment = Alignment.Start,
verticalArrangement = Arrangement.Top,
modifier = Modifier
- .fillMaxSize()
+ .fillMaxWidth()
+ .fillMaxHeight(if(WireGuardAutoTunnel.isRunningOnAndroidTv(context)) 4/5f else 1f)
.verticalScroll(rememberScrollState())
+ .focusRequester(focusRequester)
.padding(padding)
) {
Row(
modifier = Modifier
.fillMaxWidth()
- .padding(horizontal = 20.dp, vertical = 7.dp),
+ .padding(horizontal = 20.dp, vertical = 7.dp).focusGroup(),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween
) {
- Column {
+ Column(modifier = Modifier.weight(1f, true)) {
Text(stringResource(R.string.config_interface), fontWeight = FontWeight.Bold, fontSize = 20.sp)
Text(stringResource(R.string.name), fontStyle = FontStyle.Italic)
Text(text = tunnelName, modifier = Modifier.clickable {
@@ -122,15 +135,22 @@ fun DetailScreen(
val rxKB = NumberUtils.bytesToKB(tunnelStats!!.totalRx())
val txKB = NumberUtils.bytesToKB(tunnelStats!!.totalTx())
Text(stringResource(R.string.transfer), fontStyle = FontStyle.Italic)
- Text("rx: ${NumberUtils.formatDecimalTwoPlaces(rxKB)} KB tx: ${NumberUtils.formatDecimalTwoPlaces(txKB)} KB")
+ val transfer = "rx: ${NumberUtils.formatDecimalTwoPlaces(rxKB)} KB tx: ${NumberUtils.formatDecimalTwoPlaces(txKB)} KB"
+ Text(transfer, modifier = Modifier.clickable {
+ clipboardManager.setText(AnnotatedString(transfer))})
Text(stringResource(R.string.last_handshake), fontStyle = FontStyle.Italic)
val handshakeEpoch = lastHandshake[it.publicKey]
if(handshakeEpoch != null) {
if(handshakeEpoch == 0L) {
- Text(stringResource(id = R.string.never))
+ Text(stringResource(id = R.string.never), modifier = Modifier.clickable {
+ clipboardManager.setText(AnnotatedString(context.getString(R.string.never)))
+ })
} else {
val time = Instant.ofEpochMilli(handshakeEpoch)
- Text("${Duration.between(time, Instant.now()).seconds} seconds ago")
+ val duration = "${Duration.between(time, Instant.now()).seconds} seconds ago"
+ Text(duration, modifier = Modifier.clickable {
+ clipboardManager.setText(AnnotatedString(duration))
+ })
}
}
}
diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/detail/DetailViewModel.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/detail/DetailViewModel.kt
index ec48f745..06498e83 100644
--- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/detail/DetailViewModel.kt
+++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/detail/DetailViewModel.kt
@@ -38,6 +38,7 @@ class DetailViewModel @Inject constructor(private val tunnelRepo : TunnelConfigD
viewModelScope.launch(Dispatchers.IO) {
val tunnelConfig = getTunnelConfigById(id)
if(tunnelConfig != null) {
+ _tunnelName.emit(tunnelConfig.name)
_tunnel.emit(TunnelConfig.configFromQuick(tunnelConfig.wgQuick))
}
}
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 012df70c..cfe9441a 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.content.Intent
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.animation.AnimatedVisibility
@@ -17,10 +16,12 @@ import androidx.compose.foundation.layout.Row
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.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Create
import androidx.compose.material.icons.filled.FileOpen
import androidx.compose.material.icons.filled.QrCode
import androidx.compose.material.icons.rounded.Add
@@ -28,6 +29,8 @@ import androidx.compose.material.icons.rounded.Circle
import androidx.compose.material.icons.rounded.Delete
import androidx.compose.material.icons.rounded.Edit
import androidx.compose.material.icons.rounded.Info
+import androidx.compose.material.icons.rounded.Star
+import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Divider
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.FabPosition
@@ -37,14 +40,12 @@ import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ModalBottomSheet
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.TextButton
import androidx.compose.material3.rememberModalBottomSheetState
+import androidx.compose.material3.surfaceColorAtElevation
import androidx.compose.runtime.Composable
-import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
@@ -55,6 +56,7 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
+import androidx.compose.ui.focus.onFocusChanged
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
@@ -89,8 +91,10 @@ import kotlinx.coroutines.launch
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun MainScreen(
- viewModel: MainViewModel = hiltViewModel(), padding: PaddingValues,
- snackbarHostState: SnackbarHostState, navController: NavController
+ viewModel: MainViewModel = hiltViewModel(),
+ padding: PaddingValues,
+ showSnackbarMessage: (String) -> Unit,
+ navController: NavController
) {
val haptic = LocalHapticFeedback.current
@@ -100,13 +104,13 @@ fun MainScreen(
val sheetState = rememberModalBottomSheetState()
var showBottomSheet by remember { mutableStateOf(false) }
+ var showPrimaryChangeAlertDialog by remember { mutableStateOf(false) }
val tunnels by viewModel.tunnels.collectAsStateWithLifecycle(mutableListOf())
val handshakeStatus by viewModel.handshakeStatus.collectAsStateWithLifecycle(HandshakeStatus.NOT_STARTED)
- val viewState = viewModel.viewState.collectAsStateWithLifecycle()
var selectedTunnel by remember { mutableStateOf(null) }
val state by viewModel.state.collectAsStateWithLifecycle(Tunnel.State.DOWN)
val tunnelName by viewModel.tunnelName.collectAsStateWithLifecycle("")
-
+ val settings by viewModel.settings.collectAsStateWithLifecycle()
// Nested scroll for control FAB
val nestedScrollConnection = remember {
@@ -125,24 +129,14 @@ fun MainScreen(
}
}
- LaunchedEffect(viewState.value) {
- if (viewState.value.showSnackbarMessage) {
- val result = snackbarHostState.showSnackbar(
- message = viewState.value.snackbarMessage,
- actionLabel = viewState.value.snackbarActionText,
- duration = SnackbarDuration.Long,
- )
- when (result) {
- SnackbarResult.ActionPerformed -> viewState.value.onSnackbarActionClick
- SnackbarResult.Dismissed -> viewState.value.onSnackbarActionClick
- }
- }
- }
-
val pickFileLauncher = rememberLauncherForActivityResult(
- ActivityResultContracts.StartActivityForResult()
- ) { result ->
- result.data?.data?.let { viewModel.onTunnelFileSelected(it) }
+ ActivityResultContracts.GetContent()
+ ) { result -> if (result != null)
+ try {
+ viewModel.onTunnelFileSelected(result)
+ } catch (e : Exception) {
+ showSnackbarMessage(e.message ?: "Unknown error occurred")
+ }
}
val scanLauncher = rememberLauncherForActivityResult(
@@ -150,12 +144,46 @@ fun MainScreen(
onResult = {
try {
viewModel.onTunnelQrResult(it.contents)
- } catch (e : Exception) {
- viewModel.showSnackBarMessage(context.getString(R.string.qr_result_failed))
+ } catch (e: Exception) {
+ showSnackbarMessage(context.getString(R.string.qr_result_failed))
}
}
)
+ if(showPrimaryChangeAlertDialog) {
+ AlertDialog(
+ onDismissRequest = {
+ showPrimaryChangeAlertDialog = false
+ },
+ confirmButton = {
+ TextButton(onClick = {
+ scope.launch {
+ viewModel.onDefaultTunnelChange(selectedTunnel)
+ showPrimaryChangeAlertDialog = false
+ selectedTunnel = null
+ }
+ })
+ { Text(text = "Okay") }
+ },
+ dismissButton = {
+ TextButton(onClick = {
+ showPrimaryChangeAlertDialog = false
+ })
+ { Text(text = "Cancel") }
+ },
+ title = { Text(text = "Primary tunnel change") },
+ text = { Text(text = "Would you like to make this your primary tunnel?") }
+ )
+ }
+
+ fun onTunnelToggle(checked : Boolean , tunnel : TunnelConfig) {
+ try {
+ if (checked) viewModel.onTunnelStart(tunnel) else viewModel.onTunnelStop()
+ } catch (e : Exception) {
+ showSnackbarMessage(e.message!!)
+ }
+ }
+
Scaffold(
modifier = Modifier.pointerInput(Unit) {
detectTapGestures(onTap = {
@@ -169,12 +197,19 @@ fun MainScreen(
enter = slideInVertically(initialOffsetY = { it * 2 }),
exit = slideOutVertically(targetOffsetY = { it * 2 }),
) {
+ val secondaryColor = MaterialTheme.colorScheme.secondary
+ val hoverColor = MaterialTheme.colorScheme.surfaceColorAtElevation(2.dp)
+ var fobColor by remember { mutableStateOf(secondaryColor) }
FloatingActionButton(
- modifier = Modifier.padding(bottom = 90.dp),
+ modifier = Modifier.padding(bottom = 90.dp).onFocusChanged {
+ if(WireGuardAutoTunnel.isRunningOnAndroidTv(context)) {
+ fobColor = if (it.isFocused) hoverColor else secondaryColor }
+ }
+ ,
onClick = {
showBottomSheet = true
},
- containerColor = MaterialTheme.colorScheme.secondary,
+ containerColor = fobColor,
shape = RoundedCornerShape(16.dp),
) {
Icon(
@@ -210,20 +245,11 @@ fun MainScreen(
.fillMaxWidth()
.clickable {
showBottomSheet = false
- val fileSelectionIntent = Intent(Intent.ACTION_GET_CONTENT).apply {
- addCategory(Intent.CATEGORY_OPENABLE)
- putExtra(Constants.FILES_SHOW_ADVANCED, true)
- type = Constants.ALLOWED_FILE_TYPES
+ try {
+ pickFileLauncher.launch(Constants.ALLOWED_FILE_TYPES)
+ } catch (_: Exception) {
+ showSnackbarMessage("No file explorer")
}
- if(!viewModel.isIntentAvailable(fileSelectionIntent)) {
- fileSelectionIntent.action = Intent.ACTION_OPEN_DOCUMENT
- fileSelectionIntent.setPackage(null)
- if (!viewModel.isIntentAvailable(fileSelectionIntent)) {
- viewModel.showSnackBarMessage(context.getString(R.string.no_file_app))
- return@clickable
- }
- }
- pickFileLauncher.launch(fileSelectionIntent)
}
.padding(10.dp)
) {
@@ -264,6 +290,26 @@ fun MainScreen(
modifier = Modifier.padding(10.dp)
)
}
+ Divider()
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .clickable {
+ showBottomSheet = false
+ navController.navigate("${Routes.Config.name}/${Constants.MANUAL_TUNNEL_CONFIG_ID}")
+ }
+ .padding(10.dp)
+ ) {
+ Icon(
+ Icons.Filled.Create,
+ contentDescription = stringResource(id = R.string.create_import),
+ modifier = Modifier.padding(10.dp)
+ )
+ Text(
+ stringResource(id = R.string.create_import),
+ modifier = Modifier.padding(10.dp)
+ )
+ }
}
}
Column(
@@ -273,27 +319,36 @@ fun MainScreen(
.fillMaxSize()
.padding(padding)
) {
-
LazyColumn(
modifier = Modifier
.fillMaxSize()
.nestedScroll(nestedScrollConnection),
) {
- items(tunnels, key = { tunnel -> tunnel.id }) {tunnel ->
+ items(tunnels, key = { tunnel -> tunnel.id }) { tunnel ->
+ val leadingIconColor = (if (tunnelName == tunnel.name) when (handshakeStatus) {
+ HandshakeStatus.HEALTHY -> mint
+ HandshakeStatus.UNHEALTHY -> brickRed
+ HandshakeStatus.NOT_STARTED -> Color.Gray
+ HandshakeStatus.NEVER_CONNECTED -> brickRed
+ } else {Color.Gray})
val focusRequester = remember { FocusRequester() }
- RowListItem(leadingIcon = Icons.Rounded.Circle,
- leadingIconColor = if (tunnelName == tunnel.name) when (handshakeStatus) {
- HandshakeStatus.HEALTHY -> mint
- HandshakeStatus.UNHEALTHY -> brickRed
- HandshakeStatus.NOT_STARTED -> Color.Gray
- HandshakeStatus.NEVER_CONNECTED -> brickRed
- } else Color.Gray,
+ RowListItem(icon = {
+ if (settings.isTunnelConfigDefault(tunnel))
+ Icon(
+ Icons.Rounded.Star, "status",
+ tint = leadingIconColor,
+ modifier = Modifier.padding(end = 10.dp).size(20.dp)
+ )
+ else Icon(
+ Icons.Rounded.Circle, "status",
+ tint = leadingIconColor,
+ modifier = Modifier.padding(end = 15.dp).size(15.dp)
+ )
+ },
text = tunnel.name,
onHold = {
if (state == Tunnel.State.UP && tunnel.name == tunnelName) {
- scope.launch {
- viewModel.showSnackBarMessage(context.resources.getString(R.string.turn_off_tunnel))
- }
+ showSnackbarMessage(context.resources.getString(R.string.turn_off_tunnel))
return@RowListItem
}
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
@@ -303,12 +358,22 @@ fun MainScreen(
if (!WireGuardAutoTunnel.isRunningOnAndroidTv(context)) {
navController.navigate("${Routes.Detail.name}/${tunnel.id}")
} else {
+ selectedTunnel = tunnel
focusRequester.requestFocus()
}
},
rowButton = {
- if (tunnel.id == selectedTunnel?.id) {
+ if (tunnel.id == selectedTunnel?.id && !WireGuardAutoTunnel.isRunningOnAndroidTv(context)) {
Row {
+ if(!settings.isTunnelConfigDefault(tunnel)) {
+ IconButton(onClick = {
+ if(settings.isAutoTunnelEnabled) {
+ showSnackbarMessage(context.resources.getString(R.string.turn_off_auto))
+ } else showPrimaryChangeAlertDialog = true
+ }) {
+ Icon(Icons.Rounded.Star, stringResource(id = R.string.set_primary))
+ }
+ }
IconButton(onClick = {
navController.navigate("${Routes.Config.name}/${selectedTunnel?.id}")
}) {
@@ -326,6 +391,15 @@ fun MainScreen(
} else {
if (WireGuardAutoTunnel.isRunningOnAndroidTv(context)) {
Row {
+ if(!settings.isTunnelConfigDefault(tunnel)) {
+ IconButton(onClick = {
+ if(settings.isAutoTunnelEnabled) {
+ showSnackbarMessage(context.resources.getString(R.string.turn_off_auto))
+ } else showPrimaryChangeAlertDialog = true
+ }) {
+ Icon(Icons.Rounded.Star, stringResource(id = R.string.set_primary))
+ }
+ }
IconButton(
modifier = Modifier.focusRequester(focusRequester),
onClick = {
@@ -335,13 +409,12 @@ fun MainScreen(
}
IconButton(onClick = {
if (state == Tunnel.State.UP && tunnel.name == tunnelName)
- scope.launch {
- viewModel.showSnackBarMessage(
- context.resources.getString(
- R.string.turn_off_tunnel
- )
+ showSnackbarMessage(
+ context.resources.getString(
+ R.string.turn_off_tunnel
)
- } else {
+ )
+ else {
navController.navigate("${Routes.Config.name}/${tunnel.id}")
}
}) {
@@ -352,13 +425,12 @@ fun MainScreen(
}
IconButton(onClick = {
if (state == Tunnel.State.UP && tunnel.name == tunnelName)
- scope.launch {
- viewModel.showSnackBarMessage(
- context.resources.getString(
- R.string.turn_off_tunnel
- )
+ showSnackbarMessage(
+ context.resources.getString(
+ R.string.turn_off_tunnel
)
- } else {
+ )
+ else {
viewModel.onDelete(tunnel)
}
}) {
@@ -370,7 +442,7 @@ fun MainScreen(
Switch(
checked = (state == Tunnel.State.UP && tunnel.name == tunnelName),
onCheckedChange = { checked ->
- if (checked) viewModel.onTunnelStart(tunnel) else viewModel.onTunnelStop()
+ onTunnelToggle(checked, tunnel)
}
)
}
@@ -378,7 +450,7 @@ fun MainScreen(
Switch(
checked = (state == Tunnel.State.UP && tunnel.name == tunnelName),
onCheckedChange = { checked ->
- if (checked) viewModel.onTunnelStart(tunnel) else viewModel.onTunnelStop()
+ onTunnelToggle(checked, tunnel)
}
)
}
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 99624c35..30227b9b 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
@@ -2,13 +2,12 @@ package com.zaneschepke.wireguardautotunnel.ui.screens.main
import android.app.Application
import android.content.Context
-import android.content.Intent
-import android.content.pm.PackageManager
import android.database.Cursor
import android.net.Uri
import android.provider.OpenableColumns
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
+import androidx.lifecycle.viewmodel.compose.viewModel
import com.wireguard.config.BadConfigException
import com.wireguard.config.Config
import com.zaneschepke.wireguardautotunnel.Constants
@@ -21,13 +20,10 @@ import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceState
import com.zaneschepke.wireguardautotunnel.service.foreground.WireGuardConnectivityWatcherService
import com.zaneschepke.wireguardautotunnel.service.foreground.WireGuardTunnelService
-import com.zaneschepke.wireguardautotunnel.service.shortcut.ShortcutsManager
import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnService
-import com.zaneschepke.wireguardautotunnel.ui.ViewState
import com.zaneschepke.wireguardautotunnel.util.NumberUtils
import com.zaneschepke.wireguardautotunnel.util.WgTunnelException
import dagger.hilt.android.lifecycle.HiltViewModel
-import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
@@ -45,8 +41,6 @@ class MainViewModel @Inject constructor(private val application : Application,
private val vpnService: VpnService
) : ViewModel() {
- private val _viewState = MutableStateFlow(ViewState())
- val viewState get() = _viewState.asStateFlow()
val tunnels get() = tunnelRepo.getAllFlow()
val state get() = vpnService.state
@@ -87,7 +81,6 @@ class MainViewModel @Inject constructor(private val application : Application,
}
}
tunnelRepo.delete(tunnel)
- ShortcutsManager.removeTunnelShortcuts(application.applicationContext, tunnel)
}
}
@@ -127,7 +120,7 @@ class MainViewModel @Inject constructor(private val application : Application,
val tunnelConfig = TunnelConfig(name = NumberUtils.generateRandomTunnelName(), wgQuick = result)
addTunnel(tunnelConfig)
} catch (e : WgTunnelException) {
- showSnackBarMessage(e.message ?: application.getString(R.string.unknown_error_message))
+ throw WgTunnelException(e.message ?: application.getString(R.string.unknown_error_message))
}
}
}
@@ -156,40 +149,25 @@ class MainViewModel @Inject constructor(private val application : Application,
fun onTunnelFileSelected(uri : Uri) {
try {
- val fileName = getFileName(application.applicationContext, uri)
- validateFileExtension(fileName)
- val stream = getInputStreamFromUri(uri)
- saveTunnelConfigFromStream(stream, fileName)
- } catch (e : Exception) {
- showExceptionMessage(e)
- }
- }
-
- private fun showExceptionMessage(e : Exception) {
- when(e) {
- is BadConfigException -> {
- showSnackBarMessage(application.getString(R.string.bad_config))
- }
- is WgTunnelException -> {
- showSnackBarMessage(e.message ?: application.getString(R.string.unknown_error_message))
+ viewModelScope.launch(Dispatchers.IO) {
+ val fileName = getFileName(application.applicationContext, uri)
+ validateFileExtension(fileName)
+ val stream = getInputStreamFromUri(uri)
+ saveTunnelConfigFromStream(stream, fileName)
}
- else -> showSnackBarMessage(application.getString(R.string.unknown_error_message))
+ } catch (e : Exception) {
+ throw WgTunnelException(e.message ?: "Error importing file")
}
}
private suspend fun addTunnel(tunnelConfig: TunnelConfig) {
saveTunnel(tunnelConfig)
- createTunnelAppShortcuts(tunnelConfig)
}
private suspend fun saveTunnel(tunnelConfig : TunnelConfig) {
tunnelRepo.save(tunnelConfig)
}
- private fun createTunnelAppShortcuts(tunnelConfig: TunnelConfig) {
- ShortcutsManager.createTunnelShortcuts(application.applicationContext, tunnelConfig)
- }
-
private fun getFileNameByCursor(context: Context, uri: Uri) : String {
val cursor = context.contentResolver.query(uri, null, null, null, null)
if(cursor != null) {
@@ -209,23 +187,6 @@ class MainViewModel @Inject constructor(private val application : Application,
return columnIndex
}
- fun isIntentAvailable(i: Intent?): Boolean {
- val packageManager = application.packageManager
- val list = packageManager.queryIntentActivities(
- i!!,
- PackageManager.MATCH_DEFAULT_ONLY
- )
- // Ignore the Android TV framework app in the list
- var size = list.size
- for (ri in list) {
- // Ignore stub apps
- if (Constants.ANDROID_TV_STUBS == ri.activityInfo.packageName) {
- size--
- }
- }
- return size > 0
- }
-
private fun getDisplayNameByCursor(cursor: Cursor) : String {
if(cursor.moveToFirst()) {
val index = getDisplayNameColumnIndex(cursor)
@@ -251,29 +212,6 @@ class MainViewModel @Inject constructor(private val application : Application,
}
}
- fun showSnackBarMessage(message : String) {
- CoroutineScope(Dispatchers.IO).launch {
- _viewState.emit(_viewState.value.copy(
- showSnackbarMessage = true,
- snackbarMessage = message,
- snackbarActionText = application.getString(R.string.okay),
- onSnackbarActionClick = {
- viewModelScope.launch {
- dismissSnackBar()
- }
- }
- ))
- delay(Constants.SNACKBAR_DELAY)
- dismissSnackBar()
- }
- }
-
- private suspend fun dismissSnackBar() {
- _viewState.emit(_viewState.value.copy(
- showSnackbarMessage = false
- ))
- }
-
private fun getNameFromFileName(fileName : String) : String {
return fileName.substring(0 , fileName.lastIndexOf('.') )
}
@@ -285,4 +223,13 @@ class MainViewModel @Inject constructor(private val application : Application,
""
}
}
+
+ suspend fun onDefaultTunnelChange(selectedTunnel: TunnelConfig?) {
+ if(selectedTunnel != null) {
+ _settings.emit(_settings.value.copy(
+ defaultTunnel = selectedTunnel.toString()
+ ))
+ settingsRepo.save(_settings.value)
+ }
+ }
}
\ No newline at end of file
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 d1874799..c1e83d6d 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
@@ -11,14 +11,18 @@ 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.IntrinsicSize
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.verticalScroll
@@ -26,22 +30,14 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Close
import androidx.compose.material.icons.outlined.Add
import androidx.compose.material.icons.rounded.LocationOff
-import androidx.compose.material3.Button
-import androidx.compose.material3.DropdownMenuItem
-import androidx.compose.material3.ExperimentalMaterial3Api
-import androidx.compose.material3.ExposedDropdownMenuBox
-import androidx.compose.material3.ExposedDropdownMenuDefaults
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
+import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
-import androidx.compose.material3.SnackbarDuration
-import androidx.compose.material3.SnackbarHostState
-import androidx.compose.material3.SnackbarResult
-import androidx.compose.material3.Switch
+import androidx.compose.material3.Surface
import androidx.compose.material3.Text
-import androidx.compose.material3.TextField
+import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
-import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
@@ -63,27 +59,26 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
-import androidx.navigation.NavController
import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.google.accompanist.permissions.isGranted
import com.google.accompanist.permissions.rememberPermissionState
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
-import com.zaneschepke.wireguardautotunnel.repository.model.TunnelConfig
-import com.zaneschepke.wireguardautotunnel.ui.Routes
import com.zaneschepke.wireguardautotunnel.ui.common.ClickableIconButton
+import com.zaneschepke.wireguardautotunnel.ui.common.config.ConfigurationToggle
+import com.zaneschepke.wireguardautotunnel.ui.common.text.SectionTitle
import kotlinx.coroutines.launch
-@OptIn(ExperimentalMaterial3Api::class, ExperimentalPermissionsApi::class,
+@OptIn(
+ ExperimentalPermissionsApi::class,
ExperimentalLayoutApi::class
)
@Composable
fun SettingsScreen(
viewModel: SettingsViewModel = hiltViewModel(),
padding: PaddingValues,
- navController: NavController,
+ showSnackbarMessage: (String) -> Unit,
focusRequester: FocusRequester,
- snackbarHostState: SnackbarHostState = remember { SnackbarHostState() }
) {
val scope = rememberCoroutineScope()
@@ -91,39 +86,37 @@ fun SettingsScreen(
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()
val trustedSSIDs by viewModel.trustedSSIDs.collectAsStateWithLifecycle()
val tunnels by viewModel.tunnels.collectAsStateWithLifecycle(mutableListOf())
val fineLocationState = rememberPermissionState(Manifest.permission.ACCESS_FINE_LOCATION)
var currentText by remember { mutableStateOf("") }
val scrollState = rememberScrollState()
- var isLocationServicesEnabled by remember { mutableStateOf(viewModel.checkLocationServicesEnabled())}
+ var didShowLocationDisclaimer by remember { mutableStateOf(false) }
+ var isBackgroundLocationGranted by remember { mutableStateOf(true) }
+
+ val screenPadding = 5.dp
+ val fillMaxHeight = .85f
+ val fillMaxWidth = .85f
- LaunchedEffect(viewState) {
- if (viewState.showSnackbarMessage) {
- val result = snackbarHostState.showSnackbar(
- message = viewState.snackbarMessage,
- actionLabel = viewState.snackbarActionText,
- duration = SnackbarDuration.Long,
- )
- when (result) {
- SnackbarResult.ActionPerformed -> viewState.onSnackbarActionClick
- SnackbarResult.Dismissed -> viewState.onSnackbarActionClick
- }
- }
- }
fun saveTrustedSSID() {
if (currentText.isNotEmpty()) {
scope.launch {
- viewModel.onSaveTrustedSSID(currentText)
- currentText = ""
+ try {
+ viewModel.onSaveTrustedSSID(currentText)
+ currentText = ""
+ } catch (e : Exception) {
+ showSnackbarMessage(e.message ?: "Unknown error")
+ }
}
}
}
+ fun isAllAutoTunnelPermissionsEnabled() : Boolean {
+ return(isBackgroundLocationGranted && fineLocationState.status.isGranted && !viewModel.isLocationServicesNeeded())
+ }
+
fun openSettings() {
scope.launch {
val intentSettings =
@@ -133,68 +126,66 @@ fun SettingsScreen(
context.startActivity(intentSettings)
}
}
+
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
val backgroundLocationState =
rememberPermissionState(Manifest.permission.ACCESS_BACKGROUND_LOCATION)
if(!backgroundLocationState.status.isGranted) {
- Column(horizontalAlignment = Alignment.CenterHorizontally,
- verticalArrangement = Arrangement.Top,
- modifier = Modifier
- .fillMaxSize()
- .verticalScroll(scrollState)
- .padding(padding)) {
- 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)
- Text(stringResource(R.string.prominent_background_location_message), textAlign = TextAlign.Center, modifier = Modifier.padding(30.dp), fontSize = 15.sp)
- Row(
- modifier = if(WireGuardAutoTunnel.isRunningOnAndroidTv(context)) Modifier
- .fillMaxWidth()
- .padding(10.dp) else Modifier
- .fillMaxWidth()
- .padding(30.dp),
- verticalAlignment = Alignment.CenterVertically,
- horizontalArrangement = Arrangement.SpaceEvenly
+ isBackgroundLocationGranted = false
+ if(!didShowLocationDisclaimer) {
+ Column(
+ horizontalAlignment = Alignment.CenterHorizontally,
+ verticalArrangement = Arrangement.Top,
+ modifier = Modifier
+ .fillMaxSize()
+ .verticalScroll(scrollState)
+ .padding(padding)
) {
- Button(onClick = {
- navController.navigate(Routes.Main.name)
- }) {
- Text(stringResource(id = R.string.no_thanks))
- }
- Button(modifier = Modifier.focusRequester(focusRequester), onClick = {
- openSettings()
- }) {
- Text(stringResource(id = R.string.turn_on))
+ 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
+ )
+ Text(
+ stringResource(R.string.prominent_background_location_message),
+ textAlign = TextAlign.Center,
+ modifier = Modifier.padding(30.dp),
+ fontSize = 15.sp
+ )
+ Row(
+ modifier = if (WireGuardAutoTunnel.isRunningOnAndroidTv(context)) Modifier
+ .fillMaxWidth()
+ .padding(10.dp) else Modifier
+ .fillMaxWidth()
+ .padding(30.dp),
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.SpaceEvenly
+ ) {
+ TextButton(onClick = {
+ didShowLocationDisclaimer = true
+ }) {
+ Text(stringResource(id = R.string.no_thanks))
+ }
+ TextButton(modifier = Modifier.focusRequester(focusRequester), onClick = {
+ openSettings()
+ }) {
+ Text(stringResource(id = R.string.turn_on))
+ }
}
}
+ return
}
- return
- }
- }
-
- if(!fineLocationState.status.isGranted) {
- Column(
- horizontalAlignment = Alignment.CenterHorizontally,
- verticalArrangement = Arrangement.Center,
- modifier = Modifier
- .fillMaxSize()
- .padding(padding)
- ) {
- Text(
- stringResource(id = R.string.precise_location_message),
- textAlign = TextAlign.Center,
- modifier = Modifier.padding(15.dp),
- fontStyle = FontStyle.Italic
- )
- Button(modifier = Modifier.focusRequester(focusRequester),onClick = {
- fineLocationState.launchPermissionRequest()
- }) {
- Text(stringResource(id = R.string.request))
- }
-
+ } else {
+ isBackgroundLocationGranted = true
}
- return
}
if (tunnels.isEmpty()) {
@@ -214,219 +205,161 @@ fun SettingsScreen(
}
return
}
- if(!isLocationServicesEnabled && Build.VERSION.SDK_INT > Build.VERSION_CODES.P) {
- Column(
- horizontalAlignment = Alignment.CenterHorizontally,
- verticalArrangement = Arrangement.Center,
- modifier = Modifier
- .fillMaxSize()
- .padding(padding)
- ) {
- Text(
- stringResource(id = R.string.location_services_not_detected),
- textAlign = TextAlign.Center,
- modifier = Modifier.padding(15.dp),
- fontStyle = FontStyle.Italic
- )
- Button(modifier = Modifier.focusRequester(focusRequester), onClick = {
- val locationServicesEnabled = viewModel.checkLocationServicesEnabled()
- isLocationServicesEnabled = locationServicesEnabled
- if(!locationServicesEnabled) {
- scope.launch {
- viewModel.showSnackBarMessage(context.getString(R.string.detecting_location_services_disabled))
- }
- }
- }) {
- Text(stringResource(id = R.string.check_again))
- }
- }
- return
- }
- val screenPadding = if(WireGuardAutoTunnel.isRunningOnAndroidTv(context)) 5.dp else 15.dp
+
Column(
- horizontalAlignment = Alignment.Start,
+ horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Top,
- modifier = if(WireGuardAutoTunnel.isRunningOnAndroidTv(context)) Modifier
- .fillMaxHeight(.85f)
- .fillMaxWidth()
- .verticalScroll(scrollState)
- .clickable(indication = null, interactionSource = interactionSource) {
- focusManager.clearFocus()
- }
- .padding(padding) else Modifier
+ modifier = Modifier
.fillMaxSize()
.verticalScroll(scrollState)
.clickable(indication = null, interactionSource = interactionSource) {
focusManager.clearFocus()
}
- .padding(padding)
) {
- Row(
- modifier = Modifier
- .fillMaxWidth()
- .padding(screenPadding),
- verticalAlignment = Alignment.CenterVertically,
- horizontalArrangement = Arrangement.SpaceBetween
+ Surface(
+ tonalElevation = 2.dp,
+ shadowElevation = 2.dp,
+ shape = RoundedCornerShape(12.dp),
+ color = MaterialTheme.colorScheme.surface,
+ modifier = (if (WireGuardAutoTunnel.isRunningOnAndroidTv(context))
+ Modifier
+ .height(IntrinsicSize.Min)
+ .fillMaxWidth(fillMaxWidth)
+ else Modifier.fillMaxWidth(fillMaxWidth)).padding(top = 60.dp, bottom = 25.dp)
) {
- Text(stringResource(R.string.enable_auto_tunnel))
- Switch(
- modifier = Modifier.focusRequester(focusRequester),
- enabled = !settings.isAlwaysOnVpnEnabled,
- checked = settings.isAutoTunnelEnabled,
- onCheckedChange = {
- scope.launch {
- viewModel.toggleAutoTunnel()
+ Column(
+ horizontalAlignment = Alignment.Start,
+ verticalArrangement = Arrangement.Top,
+ modifier = Modifier.padding(15.dp)
+ ) {
+ SectionTitle(title = stringResource(id = R.string.auto_tunneling), padding = screenPadding)
+ Text(
+ stringResource(R.string.trusted_ssid),
+ textAlign = TextAlign.Center,
+ modifier = Modifier.padding(screenPadding, bottom = 5.dp, top = 5.dp)
+ )
+ FlowRow(
+ modifier = Modifier.padding(screenPadding),
+ horizontalArrangement = Arrangement.spacedBy(8.dp),
+ verticalArrangement = Arrangement.SpaceEvenly
+ ) {
+ trustedSSIDs.forEach { ssid ->
+ ClickableIconButton(
+ onIconClick = {
+ scope.launch {
+ viewModel.onDeleteTrustedSSID(ssid)
+ }
+ },
+ text = ssid,
+ icon = Icons.Filled.Close,
+ enabled = !(settings.isAutoTunnelEnabled || settings.isAlwaysOnVpnEnabled)
+ )
+ }
+ if(trustedSSIDs.isEmpty()) {
+ Text(stringResource(R.string.none), fontStyle = FontStyle.Italic, color = Color.Gray)
}
}
- )
+ OutlinedTextField(
+ enabled = !(settings.isAutoTunnelEnabled || settings.isAlwaysOnVpnEnabled),
+ value = currentText,
+ onValueChange = { currentText = it },
+ label = { Text(stringResource(R.string.add_trusted_ssid)) },
+ modifier = Modifier.padding(start = screenPadding, top = 5.dp).focusRequester(focusRequester),
+ maxLines = 1,
+ keyboardOptions = KeyboardOptions(
+ capitalization = KeyboardCapitalization.None,
+ imeAction = ImeAction.Done
+ ),
+ keyboardActions = KeyboardActions(
+ onDone = {
+ saveTrustedSSID()
+ }
+ ),
+ trailingIcon = {
+ IconButton(onClick = { saveTrustedSSID() }) {
+ Icon(
+ imageVector = Icons.Outlined.Add,
+ contentDescription = if (currentText == "") stringResource(id = R.string.trusted_ssid_empty_description) else stringResource(
+ id = R.string.trusted_ssid_value_description
+ ),
+ tint = if (currentText == "") Color.Transparent else MaterialTheme.colorScheme.primary
+ )
+ }
+ },
+ )
+ ConfigurationToggle(stringResource(R.string.tunnel_mobile_data),
+ enabled = !(settings.isAutoTunnelEnabled || settings.isAlwaysOnVpnEnabled),
+ checked = settings.isTunnelOnMobileDataEnabled,
+ padding = screenPadding,
+ onCheckChanged = {
+ scope.launch {
+ viewModel.onToggleTunnelOnMobileData()
+ }
+ }
+ )
+ ConfigurationToggle(stringResource(id = R.string.tunnel_on_ethernet),
+ enabled = !(settings.isAutoTunnelEnabled || settings.isAlwaysOnVpnEnabled),
+ checked = settings.isTunnelOnEthernetEnabled,
+ padding = screenPadding,
+ onCheckChanged = {
+ scope.launch {
+ viewModel.onToggleTunnelOnEthernet()
+ }
+ }
+ )
+ ConfigurationToggle(stringResource(R.string.enable_auto_tunnel),
+ enabled = !settings.isAlwaysOnVpnEnabled,
+ checked = settings.isAutoTunnelEnabled,
+ padding = screenPadding,
+ onCheckChanged = {
+ if(!isAllAutoTunnelPermissionsEnabled()) {
+ val message = if(viewModel.isLocationServicesNeeded()){
+ "Location services required"
+ } else if(!isBackgroundLocationGranted){
+ "Background location required"
+ } else {
+ "Precise location required"
+ }
+ showSnackbarMessage(message)
+ } else scope.launch {
+ viewModel.toggleAutoTunnel()
+ }
+ }
+ )
+ }
+
}
- Text(
- stringResource(id = R.string.select_tunnel),
- textAlign = TextAlign.Center,
- modifier = Modifier.padding(screenPadding, bottom = 5.dp, top = 5.dp)
- )
- ExposedDropdownMenuBox(
- expanded = expanded,
- onExpandedChange = {
- if(!(settings.isAutoTunnelEnabled || settings.isAlwaysOnVpnEnabled)) {
- expanded = !expanded }},
- modifier = Modifier.padding(start = 15.dp, top = 5.dp, bottom = 10.dp).clickable {
- expanded = !expanded
- },
- ) {
- TextField(
- enabled = !(settings.isAutoTunnelEnabled || settings.isAlwaysOnVpnEnabled),
- value = settings.defaultTunnel?.let {
- TunnelConfig.from(it).name }
- ?: "",
- readOnly = true,
- modifier = Modifier.menuAnchor(),
- label = { Text(stringResource(R.string.tunnels)) },
- onValueChange = { },
- trailingIcon = {
- ExposedDropdownMenuDefaults.TrailingIcon(
- expanded = expanded
- )
- }
- )
- ExposedDropdownMenu(
- expanded = expanded,
- onDismissRequest = {
- expanded = false
- }
+ if(!WireGuardAutoTunnel.isRunningOnAndroidTv(context)) {
+ Surface(
+ tonalElevation = 2.dp,
+ shadowElevation = 2.dp,
+ shape = RoundedCornerShape(12.dp),
+ color = MaterialTheme.colorScheme.surface,
+ modifier = Modifier.fillMaxWidth(fillMaxWidth)
+ .height(IntrinsicSize.Min)
+ .padding(bottom = 180.dp)
) {
- tunnels.forEach() { tunnel ->
- DropdownMenuItem(
- onClick = {
+ Column(
+ horizontalAlignment = Alignment.Start,
+ verticalArrangement = Arrangement.Top,
+ modifier = Modifier.padding(15.dp)
+ ) {
+ SectionTitle(title = stringResource(id = R.string.other), padding = screenPadding)
+ ConfigurationToggle(stringResource(R.string.always_on_vpn_support),
+ enabled = !settings.isAutoTunnelEnabled,
+ checked = settings.isAlwaysOnVpnEnabled,
+ padding = screenPadding,
+ onCheckChanged = {
scope.launch {
- viewModel.onDefaultTunnelSelected(tunnel)
+ viewModel.onToggleAlwaysOnVPN()
}
- expanded = false
- },
- text = { Text(text = tunnel.name) }
+ }
)
}
}
}
- Text(
- stringResource(R.string.trusted_ssid),
- textAlign = TextAlign.Center,
- modifier = Modifier.padding(screenPadding, bottom = 5.dp, top = 5.dp)
- )
- FlowRow(
- modifier = Modifier.padding(screenPadding),
- horizontalArrangement = Arrangement.spacedBy(8.dp),
- verticalArrangement = Arrangement.SpaceEvenly
- ) {
- trustedSSIDs.forEach { ssid ->
- ClickableIconButton(onIconClick = {
- scope.launch {
- viewModel.onDeleteTrustedSSID(ssid)
- }
- }, text = ssid, icon = Icons.Filled.Close, enabled = !(settings.isAutoTunnelEnabled || settings.isAlwaysOnVpnEnabled))
- }
- }
- OutlinedTextField(
- enabled = !(settings.isAutoTunnelEnabled || settings.isAlwaysOnVpnEnabled),
- value = currentText,
- onValueChange = { currentText = it },
- label = { Text(stringResource(R.string.add_trusted_ssid)) },
- modifier = Modifier.padding(start = screenPadding, top = 5.dp),
- maxLines = 1,
- keyboardOptions = KeyboardOptions(
- capitalization = KeyboardCapitalization.None,
- imeAction = ImeAction.Done
- ),
- keyboardActions = KeyboardActions(
- onDone = {
- saveTrustedSSID()
- }
- ),
- trailingIcon = {
- IconButton(onClick = { saveTrustedSSID() }) {
- Icon(
- imageVector = Icons.Outlined.Add,
- contentDescription = if (currentText == "") stringResource(id = R.string.trusted_ssid_empty_description) else stringResource(
- id = R.string.trusted_ssid_value_description
- ),
- tint = if(currentText == "") Color.Transparent else Color.Green
- )
- }
- },
- )
- Row(
- modifier = Modifier
- .fillMaxWidth()
- .padding(screenPadding),
- verticalAlignment = Alignment.CenterVertically,
- horizontalArrangement = Arrangement.SpaceBetween
- ) {
- Text(stringResource(R.string.tunnel_mobile_data))
- Switch(
- enabled = !(settings.isAutoTunnelEnabled || settings.isAlwaysOnVpnEnabled),
- checked = settings.isTunnelOnMobileDataEnabled,
- onCheckedChange = {
- scope.launch {
- viewModel.onToggleTunnelOnMobileData()
- }
- }
- )
- }
- Row(
- modifier = Modifier
- .fillMaxWidth()
- .padding(screenPadding),
- verticalAlignment = Alignment.CenterVertically,
- horizontalArrangement = Arrangement.SpaceBetween
- ) {
- Text("Tunnel on Ethernet")
- Switch(
- enabled = !settings.isAutoTunnelEnabled,
- checked = settings.isTunnelOnEthernetEnabled,
- onCheckedChange = {
- scope.launch {
- viewModel.onToggleTunnelOnEthernet()
- }
- }
- )
- }
- Row(
- modifier = Modifier
- .fillMaxWidth()
- .padding(screenPadding),
- verticalAlignment = Alignment.CenterVertically,
- horizontalArrangement = Arrangement.SpaceBetween
- ) {
- Text(stringResource(R.string.always_on_vpn_support))
- Switch(
- enabled = !settings.isAutoTunnelEnabled,
- checked = settings.isAlwaysOnVpnEnabled,
- onCheckedChange = {
- scope.launch {
- viewModel.onToggleAlwaysOnVPN()
- }
- }
- )
+ if(WireGuardAutoTunnel.isRunningOnAndroidTv(context)) {
+ Spacer(modifier = Modifier.weight(.17f))
}
}
}
diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/SettingsViewModel.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/SettingsViewModel.kt
index 4d9bd35b..67c7a6b5 100644
--- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/SettingsViewModel.kt
+++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/SettingsViewModel.kt
@@ -3,22 +3,20 @@ package com.zaneschepke.wireguardautotunnel.ui.screens.settings
import android.app.Application
import android.content.Context
import android.location.LocationManager
+import android.os.Build
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
-import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.repository.SettingsDoa
import com.zaneschepke.wireguardautotunnel.repository.TunnelConfigDao
import com.zaneschepke.wireguardautotunnel.repository.model.Settings
import com.zaneschepke.wireguardautotunnel.repository.model.TunnelConfig
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
-import com.zaneschepke.wireguardautotunnel.ui.ViewState
+import com.zaneschepke.wireguardautotunnel.util.WgTunnelException
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.flow.MutableSharedFlow
+import kotlinx.coroutines.async
import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.asStateFlow
-import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.launch
import javax.inject.Inject
@@ -34,11 +32,8 @@ class SettingsViewModel @Inject constructor(private val application : Applicatio
private val _settings = MutableStateFlow(Settings())
val settings get() = _settings.asStateFlow()
val tunnels get() = tunnelRepo.getAllFlow()
- private val _viewState = MutableStateFlow(ViewState())
- val viewState get() = _viewState.asStateFlow()
-
init {
- checkLocationServicesEnabled()
+ isLocationServicesEnabled()
viewModelScope.launch(Dispatchers.IO) {
settingsRepo.getAllFlow().filter { it.isNotEmpty() }.collect {
val settings = it.first()
@@ -54,16 +49,10 @@ class SettingsViewModel @Inject constructor(private val application : Applicatio
_settings.value.trustedNetworkSSIDs.add(trimmed)
settingsRepo.save(_settings.value)
} else {
- showSnackBarMessage("SSID already exists.")
+ throw WgTunnelException("SSID already exists.")
}
}
- suspend fun onDefaultTunnelSelected(tunnelConfig: TunnelConfig) {
- settingsRepo.save(_settings.value.copy(
- defaultTunnel = tunnelConfig.toString()
- ))
- }
-
suspend fun onToggleTunnelOnMobileData() {
settingsRepo.save(_settings.value.copy(
isTunnelOnMobileDataEnabled = !_settings.value.isTunnelOnMobileDataEnabled
@@ -75,68 +64,65 @@ class SettingsViewModel @Inject constructor(private val application : Applicatio
settingsRepo.save(_settings.value)
}
+ private fun emitFirstTunnelAsDefault() = viewModelScope.async {
+ _settings.emit(_settings.value.copy(defaultTunnel = getFirstTunnelConfig().toString()))
+ }
+
suspend fun toggleAutoTunnel() {
- if(_settings.value.defaultTunnel.isNullOrEmpty() && !_settings.value.isAutoTunnelEnabled) {
- showSnackBarMessage(application.getString(R.string.select_tunnel_message))
- return
- }
if(_settings.value.isAutoTunnelEnabled) {
ServiceManager.stopWatcherService(application)
} else {
- if(_settings.value.defaultTunnel != null) {
- val defaultTunnel = _settings.value.defaultTunnel
- ServiceManager.startWatcherService(application, defaultTunnel!!)
+ if(_settings.value.defaultTunnel == null) {
+ emitFirstTunnelAsDefault().await()
}
+ val defaultTunnel = _settings.value.defaultTunnel
+ ServiceManager.startWatcherService(application, defaultTunnel!!)
}
settingsRepo.save(_settings.value.copy(
isAutoTunnelEnabled = !_settings.value.isAutoTunnelEnabled
))
}
- suspend fun showSnackBarMessage(message : String) {
- _viewState.emit(_viewState.value.copy(
- showSnackbarMessage = true,
- snackbarMessage = message,
- snackbarActionText = "Okay",
- onSnackbarActionClick = {
- viewModelScope.launch {
- dismissSnackBar()
- }
- }
- ))
- }
-
- private suspend fun dismissSnackBar() {
- _viewState.emit(_viewState.value.copy(
- showSnackbarMessage = false
- ))
+ private suspend fun getFirstTunnelConfig() : TunnelConfig {
+ return tunnelRepo.getAll().first();
}
suspend fun onToggleAlwaysOnVPN() {
- if(_settings.value.defaultTunnel != null) {
- _settings.emit(
- _settings.value.copy(isAlwaysOnVpnEnabled = !_settings.value.isAlwaysOnVpnEnabled)
- )
- settingsRepo.save(_settings.value)
- } else {
- showSnackBarMessage(application.getString(R.string.select_tunnel_message))
+ if(_settings.value.defaultTunnel == null) {
+ emitFirstTunnelAsDefault().await()
}
+ val updatedSettings = _settings.value.copy(isAlwaysOnVpnEnabled = !_settings.value.isAlwaysOnVpnEnabled)
+ emitSettings(updatedSettings)
+ saveSettings(updatedSettings)
+ }
+
+ private suspend fun emitSettings(settings: Settings) {
+ _settings.emit(
+ settings
+ )
+ }
+
+ private suspend fun saveSettings(settings: Settings) {
+ settingsRepo.save(settings)
}
suspend fun onToggleTunnelOnEthernet() {
- if(_settings.value.defaultTunnel != null) {
- _settings.emit(
- _settings.value.copy(isTunnelOnEthernetEnabled = !_settings.value.isTunnelOnEthernetEnabled)
- )
- settingsRepo.save(_settings.value)
- } else {
- showSnackBarMessage(application.getString(R.string.select_tunnel_message))
+ if(_settings.value.defaultTunnel == null) {
+ emitFirstTunnelAsDefault().await()
}
+ _settings.emit(
+ _settings.value.copy(isTunnelOnEthernetEnabled = !_settings.value.isTunnelOnEthernetEnabled)
+ )
+ settingsRepo.save(_settings.value)
}
- fun checkLocationServicesEnabled() : Boolean {
+ private fun isLocationServicesEnabled() : Boolean {
val locationManager =
application.getSystemService(Context.LOCATION_SERVICE) as LocationManager
return locationManager.isProviderEnabled(LocationManager.NETWORK_PROVIDER)
}
+
+ fun isLocationServicesNeeded() : Boolean {
+ return(!isLocationServicesEnabled() && Build.VERSION.SDK_INT > Build.VERSION_CODES.P)
+ }
}
\ No newline at end of file
diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/support/SupportScreen.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/support/SupportScreen.kt
index c219eee2..59d53c49 100644
--- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/support/SupportScreen.kt
+++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/support/SupportScreen.kt
@@ -1,7 +1,6 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.support
import android.content.Intent
-import android.content.pm.PackageManager
import android.net.Uri
import androidx.compose.foundation.clickable
import androidx.compose.foundation.focusable
@@ -19,8 +18,6 @@ import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
-import androidx.compose.runtime.LaunchedEffect
-import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/NumberUtils.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/NumberUtils.kt
index dd2a5b8f..257a2d68 100644
--- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/NumberUtils.kt
+++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/NumberUtils.kt
@@ -8,11 +8,16 @@ import java.time.Instant
object NumberUtils {
private const val BYTES_IN_KB = 1024L
+ private val keyValidationRegex = """^[A-Za-z0-9+/]{42}[AEIMQUYcgkosw480]=${'$'}""".toRegex()
fun bytesToKB(bytes : Long) : BigDecimal {
return bytes.toBigDecimal().divide(BYTES_IN_KB.toBigDecimal())
}
+ fun isValidKey(key : String) : Boolean {
+ return key.matches(keyValidationRegex)
+ }
+
fun generateRandomTunnelName() : String {
return "tunnel${(Math.random() * 100000).toInt()}"
}
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index cc42aeb1..633bc2c4 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -21,10 +21,9 @@
Notifications permission is required for the app to work properly.
Open Settings
Add Trusted SSID
- Trusted SSID
+ Trusted SSIDs
Tunnels
- Select Tunnel
- Enable auto tunneling
+ Enable auto-tunneling
Tunnel on mobile data
\"Allow all the time\" location permission is required for retrieving Wi-Fi SSID in the background. Permission is needed for this feature.
Location permission is required for this feature to work properly.
@@ -32,6 +31,7 @@
"Retry"
View Privacy Policy
Okay
+ Tunnel on ethernet
This feature requires background location permission to enable Wi-Fi SSID monitoring even while the application is closed. For more details, please see the Privacy Policy linked on the Support screen.
Background Location Disclosure
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!
@@ -51,7 +51,7 @@
Include
Tunnel all applications
Configuration changes saved.
- Save changes
+ Save
Icon
No thanks
Turn on
@@ -75,7 +75,7 @@
Failed connection to -
Attempting to connect to server after 30 seconds of no response.
Attempting to reconnect to server after more than one minute of no response.
- Enable Always-On VPN support
+ Allow Always-On VPN
Please select a tunnel first
Unable to detect Location Services which are required for this feature. Please enable Location Services.
Check again
@@ -97,4 +97,31 @@
Failed to open file stream.
An unknown error occurred.
No file app installed.
+ Other
+ Auto-tunneling
+ Select tunnel to use
+ VPN on
+ VPN off
+ Primary VPN on
+ Primary VPN off
+ Create from scratch
+ Set primary
+ Action requires auto-tunnel disabled
+ Add peer
+ Info
+ Done
+ Interface
+ Rotate keys
+ Private key
+ Copy public key
+ base64 key
+ comma separated list
+ Listen port
+ (random)
+ (auto)
+ (optional)
+ (optional, not recommended)
+ Pre-shared key
+ seconds
+ Persistent keepalive
\ No newline at end of file
diff --git a/app/src/main/res/xml/shortcuts.xml b/app/src/main/res/xml/shortcuts.xml
new file mode 100644
index 00000000..0fbe1c3d
--- /dev/null
+++ b/app/src/main/res/xml/shortcuts.xml
@@ -0,0 +1,32 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/asset/android_feature_graphic.png b/asset/android_feature_graphic.png
index 5e009c05..e69de29b 100644
Binary files a/asset/android_feature_graphic.png and b/asset/android_feature_graphic.png differ
diff --git a/asset/android_tv_banner.jpg b/asset/android_tv_banner.jpg
index 8c15b527..e69de29b 100644
Binary files a/asset/android_tv_banner.jpg and b/asset/android_tv_banner.jpg differ
diff --git a/asset/config_screen.png b/asset/config_screen.png
index 97d497b3..86080eac 100644
Binary files a/asset/config_screen.png and b/asset/config_screen.png differ
diff --git a/asset/config_screen_old.png b/asset/config_screen_old.png
new file mode 100644
index 00000000..97d497b3
Binary files /dev/null and b/asset/config_screen_old.png differ
diff --git a/asset/main_screen.png b/asset/main_screen.png
index 2b3c6262..12a68531 100644
Binary files a/asset/main_screen.png and b/asset/main_screen.png differ
diff --git a/asset/main_screen_old.png b/asset/main_screen_old.png
index 09b9a2e1..2b3c6262 100644
Binary files a/asset/main_screen_old.png and b/asset/main_screen_old.png differ
diff --git a/asset/main_screen_tv.png b/asset/main_screen_tv.png
index 6f18bdee..e69de29b 100644
Binary files a/asset/main_screen_tv.png and b/asset/main_screen_tv.png differ
diff --git a/asset/main_screen_tv2.png b/asset/main_screen_tv2.png
index 6c37ef64..e69de29b 100644
Binary files a/asset/main_screen_tv2.png and b/asset/main_screen_tv2.png differ
diff --git a/asset/settings_screen.png b/asset/settings_screen.png
index 38d6600e..ecec78d4 100644
Binary files a/asset/settings_screen.png and b/asset/settings_screen.png differ
diff --git a/asset/settings_screen_old.png b/asset/settings_screen_old.png
index 3bbe5da0..38d6600e 100644
Binary files a/asset/settings_screen_old.png and b/asset/settings_screen_old.png differ
diff --git a/asset/settings_screen_tv.png b/asset/settings_screen_tv.png
index f1c4a945..e69de29b 100644
Binary files a/asset/settings_screen_tv.png and b/asset/settings_screen_tv.png differ
diff --git a/asset/support_screen.png b/asset/support_screen.png
index 6405c5d2..d2569cfe 100644
Binary files a/asset/support_screen.png and b/asset/support_screen.png differ
diff --git a/asset/support_screen_old.png b/asset/support_screen_old.png
index 1d91904d..6405c5d2 100644
Binary files a/asset/support_screen_old.png and b/asset/support_screen_old.png differ
diff --git a/asset/support_screen_tv.png b/asset/support_screen_tv.png
index 8917f107..e69de29b 100644
Binary files a/asset/support_screen_tv.png and b/asset/support_screen_tv.png differ
diff --git a/asset/tv_banner.png b/asset/tv_banner.png
index b0e7c637..e69de29b 100644
Binary files a/asset/tv_banner.png and b/asset/tv_banner.png differ
diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/config_screen.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/config_screen.png
index 97d497b3..86080eac 100644
Binary files a/fastlane/metadata/android/en-US/images/phoneScreenshots/config_screen.png and b/fastlane/metadata/android/en-US/images/phoneScreenshots/config_screen.png differ
diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/main_screen.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/main_screen.png
index 2b3c6262..12a68531 100644
Binary files a/fastlane/metadata/android/en-US/images/phoneScreenshots/main_screen.png and b/fastlane/metadata/android/en-US/images/phoneScreenshots/main_screen.png differ
diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/settings_screen.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/settings_screen.png
index 38d6600e..ecec78d4 100644
Binary files a/fastlane/metadata/android/en-US/images/phoneScreenshots/settings_screen.png and b/fastlane/metadata/android/en-US/images/phoneScreenshots/settings_screen.png differ
diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/support_screen.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/support_screen.png
index 6405c5d2..d2569cfe 100644
Binary files a/fastlane/metadata/android/en-US/images/phoneScreenshots/support_screen.png and b/fastlane/metadata/android/en-US/images/phoneScreenshots/support_screen.png differ
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 3d485f83..ab14a366 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -6,25 +6,25 @@ appcompat = "1.6.1"
coreKtx = "1.12.0"
espressoCore = "3.5.1"
firebase-crashlytics-gradle = "2.9.9"
-google-services = "4.3.15"
+google-services = "4.4.0"
hiltAndroid = "2.48"
hiltNavigationCompose = "1.0.0"
junit = "4.13.2"
kotlinx-serialization-json = "1.5.1"
lifecycle-runtime-compose = "2.6.2"
-material-icons-extended = "1.5.1"
-material3 = "1.1.1"
-navigationCompose = "2.7.2"
-roomVersion = "2.6.0-beta01"
+material-icons-extended = "1.5.2"
+material3 = "1.1.2"
+navigationCompose = "2.7.3"
+roomVersion = "2.6.0-rc01"
timber = "5.0.1"
tunnel = "1.0.20230706"
-androidGradlePlugin = "8.2.0-beta03"
+androidGradlePlugin = "8.3.0-alpha06"
kotlin="1.9.10"
ksp="1.9.10-1.0.13"
-composeBom="2023.09.00"
-firebaseBom="32.2.3"
-compose="1.5.1"
-crashlytics="18.4.1"
+composeBom="2023.09.02"
+firebaseBom="32.3.1"
+compose="1.5.2"
+crashlytics="18.4.3"
analytics="21.3.0"
composeCompiler="1.5.3"
zxingAndroidEmbedded = "4.3.0"
@@ -38,6 +38,7 @@ accompanist-flowlayout = { module = "com.google.accompanist:accompanist-flowlayo
accompanist-navigation-animation = { module = "com.google.accompanist:accompanist-navigation-animation", version.ref = "accompanist" }
accompanist-permissions = { module = "com.google.accompanist:accompanist-permissions", version.ref = "accompanist" }
accompanist-systemuicontroller = { module = "com.google.accompanist:accompanist-systemuicontroller", version.ref = "accompanist" }
+
#room
androidx-room-compiler = { module = "androidx.room:room-compiler", version.ref = "roomVersion" }
androidx-room-ktx = { module = "androidx.room:room-ktx", version.ref = "roomVersion" }
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index 96f2e7a9..42d1885d 100644
--- a/gradle/wrapper/gradle-wrapper.properties
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -1,6 +1,6 @@
#Mon Apr 24 22:46:45 EDT 2023
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-8.2-bin.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.3-rc-2-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists