Compare commits

..

6 Commits

Author SHA1 Message Date
Zane Schepke a98a47f54d fix: prevent crash from bad file config upload and bug preventing vpn from starting via watcher service after reboot
Add catch for file configs that are malformed and display message to the user

Pass full tunnel config to watcher service after detecting reboot to allow watcher service to start tunnel properly.
2023-07-05 23:21:55 -04:00
Zane Schepke cb3a7d024b fix: discord link expired 2023-07-04 22:28:39 -04:00
Zane Schepke 259be21b04 docs: update README 2023-07-04 17:03:30 -04:00
Zane Schepke 8b81831910 feat: add split tunneling and tunnel configuration screen
This adds a tunnel configuration screen where user can configure split application tunneling and change tunnel name.

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

Minor UI quality-of-life changes.

Closes #3
2023-07-04 14:21:45 -04:00
Zane Schepke 49bf7fa8b9 feat: add scan qr code option for importing tunnel configs
Closes #1
2023-07-02 22:45:36 -04:00
Zane Schepke 2f53a8f3b4 Update VPN connection required message to add always-on VPN disclaimer 2023-07-02 11:59:24 -04:00
22 changed files with 629 additions and 99 deletions
+2
View File
@@ -28,6 +28,7 @@ This is an alternative Android Application for [WireGuard](https://www.wireguard
<p float="center"> <p float="center">
<img label="Main" style="padding-right:25px" src="./asset/main_screen.png" width="200" /> <img label="Main" style="padding-right:25px" src="./asset/main_screen.png" width="200" />
<img label="Config" style="padding-left:25px" src="./asset/config_screen.png" width="200" />
<img label="Settings" style="padding-left:25px" src="./asset/settings_screen.png" width="200" /> <img label="Settings" style="padding-left:25px" src="./asset/settings_screen.png" width="200" />
<img label="Support" style="padding-left:25px" src="./asset/support_screen.png" width="200" /> <img label="Support" style="padding-left:25px" src="./asset/support_screen.png" width="200" />
</p> </p>
@@ -42,6 +43,7 @@ The inspiration for this app came from the inconvenience of constantly having to
* Add tunnels via .conf file * Add tunnels via .conf file
* Auto connect to VPN based on Wi-Fi SSID * Auto connect to VPN based on Wi-Fi SSID
* Split tunneling by application
* Configurable Trusted Network list * Configurable Trusted Network list
* Optional auto connect on mobile data * Optional auto connect on mobile data
* Automatic service restart after reboot * Automatic service restart after reboot
+11 -5
View File
@@ -15,9 +15,9 @@ android {
namespace = "com.zaneschepke.wireguardautotunnel" namespace = "com.zaneschepke.wireguardautotunnel"
compileSdk = 33 compileSdk = 33
val versionMajor = 1 val versionMajor = 2
val versionMinor = 1 val versionMinor = 0
val versionPatch = 5 val versionPatch = 2
val versionBuild = 0 val versionBuild = 0
defaultConfig { defaultConfig {
@@ -71,7 +71,7 @@ dependencies {
implementation("androidx.compose.ui:ui") implementation("androidx.compose.ui:ui")
implementation("androidx.compose.ui:ui-graphics") implementation("androidx.compose.ui:ui-graphics")
implementation("androidx.compose.ui:ui-tooling-preview") implementation("androidx.compose.ui:ui-tooling-preview")
implementation("androidx.compose.material3:material3") implementation("androidx.compose.material3:material3:1.1.1")
implementation("androidx.appcompat:appcompat:1.6.1") implementation("androidx.appcompat:appcompat:1.6.1")
testImplementation("junit:junit:4.13.2") testImplementation("junit:junit:4.13.2")
@@ -89,7 +89,7 @@ dependencies {
implementation("com.jakewharton.timber:timber:5.0.1") implementation("com.jakewharton.timber:timber:5.0.1")
// compose navigation // compose navigation
implementation("androidx.navigation:navigation-compose:2.5.3") implementation("androidx.navigation:navigation-compose:2.6.0")
implementation("androidx.hilt:hilt-navigation-compose:1.0.0") implementation("androidx.hilt:hilt-navigation-compose:1.0.0")
// hilt // hilt
@@ -101,6 +101,7 @@ dependencies {
implementation("com.google.accompanist:accompanist-permissions:${rExtra.get("accompanistVersion")}") implementation("com.google.accompanist:accompanist-permissions:${rExtra.get("accompanistVersion")}")
implementation("com.google.accompanist:accompanist-flowlayout:${rExtra.get("accompanistVersion")}") implementation("com.google.accompanist:accompanist-flowlayout:${rExtra.get("accompanistVersion")}")
implementation("com.google.accompanist:accompanist-navigation-animation:${rExtra.get("accompanistVersion")}") implementation("com.google.accompanist:accompanist-navigation-animation:${rExtra.get("accompanistVersion")}")
implementation("com.google.accompanist:accompanist-drawablepainter:${rExtra.get("accompanistVersion")}")
//db //db
implementation("io.objectbox:objectbox-kotlin:${rExtra.get("objectBoxVersion")}") implementation("io.objectbox:objectbox-kotlin:${rExtra.get("objectBoxVersion")}")
@@ -120,6 +121,11 @@ dependencies {
implementation("com.google.firebase:firebase-crashlytics-ktx") implementation("com.google.firebase:firebase-crashlytics-ktx")
implementation("com.google.firebase:firebase-analytics-ktx") implementation("com.google.firebase:firebase-analytics-ktx")
//barcode scanning
implementation("com.google.android.gms:play-services-code-scanner:16.0.0")
} }
kapt { kapt {
correctErrorTypes = true correctErrorTypes = true
+9
View File
@@ -1,6 +1,7 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"> xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"
android:maxSdkVersion="32" /> android:maxSdkVersion="32" />
@@ -16,6 +17,11 @@
<uses-permission android:name="android.permission.WAKE_LOCK" /> <uses-permission android:name="android.permission.WAKE_LOCK" />
<!--start service on boot permission--> <!--start service on boot permission-->
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/> <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
<queries>
<intent>
<action android:name="android.intent.action.MAIN" />
</intent>
</queries>
<application <application
android:allowBackup="true" android:allowBackup="true"
android:name=".WireGuardAutoTunnel" android:name=".WireGuardAutoTunnel"
@@ -59,5 +65,8 @@
<action android:name="android.intent.action.BOOT_COMPLETED"/> <action android:name="android.intent.action.BOOT_COMPLETED"/>
</intent-filter> </intent-filter>
</receiver> </receiver>
<meta-data
android:name="com.google.mlkit.vision.DEPENDENCIES"
android:value="barcode_ui"/>
</application> </application>
</manifest> </manifest>
@@ -36,7 +36,7 @@ class BootReceiver : BroadcastReceiver() {
Action.START, context, Action.START, context,
WireGuardConnectivityWatcherService::class.java, WireGuardConnectivityWatcherService::class.java,
mapOf(context.resources.getString(R.string.tunnel_extras_key) to mapOf(context.resources.getString(R.string.tunnel_extras_key) to
defaultTunnel.id.toString()) defaultTunnel.toString())
) )
} }
} }
@@ -0,0 +1,41 @@
package com.zaneschepke.wireguardautotunnel.module
import android.content.Context
import com.google.mlkit.vision.barcode.common.Barcode
import com.google.mlkit.vision.codescanner.GmsBarcodeScanner
import com.google.mlkit.vision.codescanner.GmsBarcodeScannerOptions
import com.google.mlkit.vision.codescanner.GmsBarcodeScanning
import com.zaneschepke.wireguardautotunnel.service.barcode.CodeScanner
import com.zaneschepke.wireguardautotunnel.service.barcode.QRScanner
import dagger.Binds
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.components.ViewModelComponent
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.android.scopes.ViewModelScoped
@Module
@InstallIn(ViewModelComponent::class)
class ScannerModule {
@ViewModelScoped
@Provides
fun provideBarCodeOptions() : GmsBarcodeScannerOptions {
return GmsBarcodeScannerOptions.Builder()
.setBarcodeFormats(Barcode.FORMAT_QR_CODE)
.build()
}
@ViewModelScoped
@Provides
fun provideBarCodeScanner(@ApplicationContext context: Context, options: GmsBarcodeScannerOptions) : GmsBarcodeScanner {
return GmsBarcodeScanning.getClient(context, options)
}
@ViewModelScoped
@Provides
fun provideQRScanner(gmsBarcodeScanner: GmsBarcodeScanner) : CodeScanner {
return QRScanner(gmsBarcodeScanner)
}
}
@@ -1,5 +1,7 @@
package com.zaneschepke.wireguardautotunnel.module package com.zaneschepke.wireguardautotunnel.module
import com.zaneschepke.wireguardautotunnel.service.barcode.CodeScanner
import com.zaneschepke.wireguardautotunnel.service.barcode.QRScanner
import com.zaneschepke.wireguardautotunnel.service.network.MobileDataService import com.zaneschepke.wireguardautotunnel.service.network.MobileDataService
import com.zaneschepke.wireguardautotunnel.service.network.NetworkService import com.zaneschepke.wireguardautotunnel.service.network.NetworkService
import com.zaneschepke.wireguardautotunnel.service.network.WifiService import com.zaneschepke.wireguardautotunnel.service.network.WifiService
@@ -10,6 +12,7 @@ import dagger.Module
import dagger.hilt.InstallIn import dagger.hilt.InstallIn
import dagger.hilt.android.components.ServiceComponent import dagger.hilt.android.components.ServiceComponent
import dagger.hilt.android.scopes.ServiceScoped import dagger.hilt.android.scopes.ServiceScoped
import dagger.hilt.android.scopes.ViewModelScoped
@Module @Module
@InstallIn(ServiceComponent::class) @InstallIn(ServiceComponent::class)
@@ -0,0 +1,7 @@
package com.zaneschepke.wireguardautotunnel.service.barcode
import kotlinx.coroutines.flow.Flow
interface CodeScanner {
fun scan() : Flow<String?>
}
@@ -0,0 +1,22 @@
package com.zaneschepke.wireguardautotunnel.service.barcode
import com.google.mlkit.vision.codescanner.GmsBarcodeScanner
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.callbackFlow
import timber.log.Timber
import javax.inject.Inject
class QRScanner @Inject constructor(private val gmsBarcodeScanner: GmsBarcodeScanner) : CodeScanner {
override fun scan(): Flow<String?> {
return callbackFlow {
gmsBarcodeScanner.startScan().addOnSuccessListener {
trySend(it.rawValue)
}.addOnFailureListener {
Timber.e(it.message)
}
awaitClose {
}
}
}
}
@@ -6,8 +6,8 @@ import android.app.Service
import android.content.Context import android.content.Context
import android.content.Context.ACTIVITY_SERVICE import android.content.Context.ACTIVITY_SERVICE
import android.content.Intent import android.content.Intent
import android.content.SharedPreferences import com.google.firebase.crashlytics.ktx.crashlytics
import com.zaneschepke.wireguardautotunnel.R import com.google.firebase.ktx.Firebase
object ServiceTracker { object ServiceTracker {
@Suppress("DEPRECATION") @Suppress("DEPRECATION")
@@ -31,7 +31,11 @@ object ServiceTracker {
} }
} }
intent.component?.javaClass intent.component?.javaClass
application.startService(intent) try {
application.startService(intent)
} catch (e : Exception) {
e.message?.let { Firebase.crashlytics.log(it) }
}
} }
fun <T : Service> actionOnService(action: Action, context: Context, cls : Class<T>, extras : Map<String,String>? = null) { fun <T : Service> actionOnService(action: Action, context: Context, cls : Class<T>, extras : Map<String,String>? = null) {
@@ -43,6 +47,10 @@ object ServiceTracker {
} }
} }
intent.component?.javaClass intent.component?.javaClass
context.startService(intent) try {
context.startService(intent)
} catch (e : Exception) {
e.message?.let { Firebase.crashlytics.log(it) }
}
} }
} }
@@ -161,6 +161,7 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
if(!isWifiConnected && vpnService.getState() == Tunnel.State.UP) stopVPN() if(!isWifiConnected && vpnService.getState() == Tunnel.State.UP) stopVPN()
Timber.d("Lost mobile data connection") Timber.d("Lost mobile data connection")
} }
else -> {}
} }
} }
} }
@@ -200,6 +201,7 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
startVPN() startVPN()
} }
} }
else -> {}
} }
} }
} }
@@ -24,7 +24,7 @@ class WireGuardTunnel @Inject constructor(private val backend : Backend) : VpnSe
override val state get() = _state.asSharedFlow() override val state get() = _state.asSharedFlow()
override suspend fun startTunnel(tunnelConfig: TunnelConfig) : Tunnel.State{ override suspend fun startTunnel(tunnelConfig: TunnelConfig) : Tunnel.State{
try { return try {
if(getState() == Tunnel.State.UP && _tunnelName.value != tunnelConfig.name) { if(getState() == Tunnel.State.UP && _tunnelName.value != tunnelConfig.name) {
stopTunnel() stopTunnel()
} }
@@ -33,10 +33,10 @@ class WireGuardTunnel @Inject constructor(private val backend : Backend) : VpnSe
val state = backend.setState( val state = backend.setState(
this, Tunnel.State.UP, config) this, Tunnel.State.UP, config)
_state.emit(state) _state.emit(state)
return state; state;
} catch (e : Exception) { } catch (e : Exception) {
Timber.e("Failed to start tunnel with error: ${e.message}") Timber.e("Failed to start tunnel with error: ${e.message}")
return Tunnel.State.DOWN Tunnel.State.DOWN
} }
} }
@@ -19,11 +19,64 @@ data class TunnelConfig(
var name : String, var name : String,
var wgQuick : String var wgQuick : String
) { ) {
override fun toString(): String { override fun toString(): String {
return Json.encodeToString(serializer(), this) return Json.encodeToString(serializer(), this)
} }
companion object { companion object {
private const val INCLUDED_APPLICATIONS = "IncludedApplications = "
private const val EXCLUDED_APPLICATIONS = "ExcludedApplications = "
private const val INTERFACE = "[Interface]"
private const val NEWLINE_CHAR = "\n"
private const val APP_CONFIG_SEPARATOR = ", "
private fun addApplicationsToConfig(appConfig : String, wgQuick : String) : String {
val configList = wgQuick.split(NEWLINE_CHAR).toMutableList()
val interfaceIndex = configList.indexOf(INTERFACE)
configList.add(interfaceIndex + 1, appConfig)
return configList.joinToString(NEWLINE_CHAR)
}
fun clearAllApplicationsFromConfig(wgQuick : String) : String {
val configList = wgQuick.split(NEWLINE_CHAR).toMutableList()
val itr = configList.iterator()
while (itr.hasNext()) {
val next = itr.next()
if(next.contains(INCLUDED_APPLICATIONS) || next.contains(EXCLUDED_APPLICATIONS)) {
itr.remove()
}
}
return configList.joinToString(NEWLINE_CHAR)
}
fun setExcludedApplicationsOnQuick(packages : List<String>, wgQuick: String) : String {
if(packages.isEmpty()) {
return wgQuick
}
val clearedWgQuick = clearAllApplicationsFromConfig(wgQuick)
val excludeConfig = buildExcludedApplicationsString(packages)
return addApplicationsToConfig(excludeConfig, clearedWgQuick)
}
fun setIncludedApplicationsOnQuick(packages : List<String>, wgQuick: String) : String {
if(packages.isEmpty()) {
return wgQuick
}
val clearedWgQuick = clearAllApplicationsFromConfig(wgQuick)
val includeConfig = buildIncludedApplicationsString(packages)
return addApplicationsToConfig(includeConfig, clearedWgQuick)
}
private fun buildExcludedApplicationsString(packages : List<String>) : String {
return EXCLUDED_APPLICATIONS + packages.joinToString(APP_CONFIG_SEPARATOR)
}
private fun buildIncludedApplicationsString(packages : List<String>) : String {
return INCLUDED_APPLICATIONS + packages.joinToString(APP_CONFIG_SEPARATOR)
}
fun from(string : String) : TunnelConfig { fun from(string : String) : TunnelConfig {
return Json.decodeFromString<TunnelConfig>(string) return Json.decodeFromString<TunnelConfig>(string)
} }
@@ -33,6 +33,7 @@ import com.wireguard.android.backend.GoBackend
import com.zaneschepke.wireguardautotunnel.R import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.ui.common.PermissionRequestFailedScreen import com.zaneschepke.wireguardautotunnel.ui.common.PermissionRequestFailedScreen
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.BottomNavBar import com.zaneschepke.wireguardautotunnel.ui.common.navigation.BottomNavBar
import com.zaneschepke.wireguardautotunnel.ui.screens.config.ConfigScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.main.MainScreen import com.zaneschepke.wireguardautotunnel.ui.screens.main.MainScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.SettingsScreen import com.zaneschepke.wireguardautotunnel.ui.screens.settings.SettingsScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.support.SupportScreen import com.zaneschepke.wireguardautotunnel.ui.screens.support.SupportScreen
@@ -121,11 +122,11 @@ class MainActivity : AppCompatActivity() {
) )
else -> { else -> {
fadeIn(animationSpec = tween(2000)) fadeIn(animationSpec = tween(1000))
} }
} }
}) { }) {
MainScreen(padding = padding, snackbarHostState = snackbarHostState) MainScreen(padding = padding, snackbarHostState = snackbarHostState, navController = navController)
} }
composable(Routes.Settings.name, enterTransition = { composable(Routes.Settings.name, enterTransition = {
when (initialState.destination.route) { when (initialState.destination.route) {
@@ -143,7 +144,7 @@ class MainActivity : AppCompatActivity() {
} }
else -> { else -> {
fadeIn(animationSpec = tween(2000)) fadeIn(animationSpec = tween(1000))
} }
} }
}) { SettingsScreen(padding = padding, snackbarHostState = snackbarHostState, navController = navController) } }) { SettingsScreen(padding = padding, snackbarHostState = snackbarHostState, navController = navController) }
@@ -156,10 +157,13 @@ class MainActivity : AppCompatActivity() {
) )
else -> { else -> {
fadeIn(animationSpec = tween(2000)) fadeIn(animationSpec = tween(1000))
} }
} }
}) { SupportScreen(padding = padding) } }) { SupportScreen(padding = padding) }
composable("${Routes.Config.name}/{id}", enterTransition = {
fadeIn(animationSpec = tween(1000))
}) { ConfigScreen(padding = padding, navController = navController, id = it.arguments?.getString("id"))}
} }
} }
} }
@@ -9,7 +9,8 @@ import com.zaneschepke.wireguardautotunnel.ui.common.navigation.BottomNavItem
enum class Routes { enum class Routes {
Main, Main,
Settings, Settings,
Support; Support,
Config;
companion object { companion object {
@@ -0,0 +1,200 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.config
import android.widget.Toast
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.Android
import androidx.compose.material3.Button
import androidx.compose.material3.Checkbox
import androidx.compose.material3.Icon
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Switch
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardCapitalization
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.navigation.NavController
import com.google.accompanist.drawablepainter.DrawablePainter
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.ui.Routes
import kotlinx.coroutines.launch
@OptIn(ExperimentalComposeUiApi::class)
@Composable
fun ConfigScreen(
viewModel: ConfigViewModel = hiltViewModel(),
padding: PaddingValues,
navController: NavController,
id : String?
) {
val context = LocalContext.current
val focusManager = LocalFocusManager.current
val keyboardController = LocalSoftwareKeyboardController.current
val scope = rememberCoroutineScope()
val tunnel by viewModel.tunnel.collectAsStateWithLifecycle(null)
val tunnelName = viewModel.tunnelName.collectAsStateWithLifecycle()
val packages by viewModel.packages.collectAsStateWithLifecycle()
val checkedPackages by viewModel.checkedPackages.collectAsStateWithLifecycle()
val include by viewModel.include.collectAsStateWithLifecycle()
val allApplications by viewModel.allApplications.collectAsStateWithLifecycle()
LaunchedEffect(Unit) {
viewModel.getTunnelById(id)
viewModel.emitAllInternetCapablePackages()
viewModel.emitCurrentPackageConfigurations(id)
}
if(tunnel != null) {
Column(
horizontalAlignment = Alignment.Start,
verticalArrangement = Arrangement.Top,
modifier = Modifier
.fillMaxSize()
.padding(padding)
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 20.dp, vertical = 7.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween
) {
OutlinedTextField(
value = tunnelName.value,
onValueChange = {
viewModel.onTunnelNameChange(it)
},
label = { Text(stringResource(id = R.string.tunnel_name)) },
maxLines = 1,
keyboardOptions = KeyboardOptions(
capitalization = KeyboardCapitalization.None,
imeAction = ImeAction.Done
),
keyboardActions = KeyboardActions(
onDone = {
focusManager.clearFocus()
keyboardController?.hide()
viewModel.onTunnelNameChange(tunnelName.value)
}
),
)
}
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 20.dp, vertical = 7.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween
) {
Text(stringResource(id = R.string.tunnel_all))
Switch(
checked = allApplications,
onCheckedChange = {
viewModel.onAllApplicationsChange(!allApplications)
}
)
}
if(!allApplications) {
Row(modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 20.dp, vertical = 7.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween) {
Row(verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween){
Text(stringResource(id = R.string.include))
Checkbox(
checked = include,
onCheckedChange = {
viewModel.onIncludeChange(!include)
}
)
}
Row(verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween){
Text(stringResource(id = R.string.exclude))
Checkbox(
checked = !include,
onCheckedChange = {
viewModel.onIncludeChange(!include)
}
)
}
}
LazyColumn(modifier = Modifier
.fillMaxWidth()
.fillMaxHeight(.75f)
.padding(horizontal = 14.dp, vertical = 7.dp),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.Start) {
items(packages) { pack ->
Row(verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween) {
Row(
horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.padding(5.dp)
) {
val drawable = pack.applicationInfo?.loadIcon(context.packageManager)
if(drawable != null) {
Image(painter = DrawablePainter(drawable), stringResource(id = R.string.icon), modifier = Modifier.size(50.dp, 50.dp))
} else {
Icon(Icons.Rounded.Android, stringResource(id = R.string.edit), modifier = Modifier.size(50.dp, 50.dp))
}
Text(pack.applicationInfo.loadLabel(context.packageManager).toString(), modifier = Modifier.padding(5.dp))
}
Checkbox(
checked = (checkedPackages.contains(pack.packageName)),
onCheckedChange = {
if(it) viewModel.onAddCheckedPackage(pack.packageName) else viewModel.onRemoveCheckedPackage(pack.packageName)
}
)
}
}
}
}
Row(
horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.fillMaxWidth()
) {
Button(onClick = {
scope.launch {
viewModel.onSaveAllChanges()
Toast.makeText(context, context.resources.getString(R.string.config_changes_saved), Toast.LENGTH_LONG).show()
navController.navigate(Routes.Main.name)
}
}, Modifier.padding(25.dp)) {
Text(stringResource(id = R.string.save_changes))
}
}
}
}
}
@@ -0,0 +1,133 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.config
import android.Manifest
import android.app.Application
import android.content.pm.PackageInfo
import android.content.pm.PackageManager
import android.os.Build
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.toMutableStateList
import androidx.lifecycle.ViewModel
import com.zaneschepke.wireguardautotunnel.repository.Repository
import com.zaneschepke.wireguardautotunnel.service.tunnel.model.TunnelConfig
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import timber.log.Timber
import javax.inject.Inject
@HiltViewModel
class ConfigViewModel @Inject constructor(private val application : Application,
private val tunnelRepo : Repository<TunnelConfig>) : ViewModel() {
private val _tunnel = MutableStateFlow<TunnelConfig?>(null)
private val _tunnelName = MutableStateFlow("")
val tunnelName get() = _tunnelName.asStateFlow()
val tunnel get() = _tunnel.asStateFlow()
private val _packages = MutableStateFlow(emptyList<PackageInfo>())
val packages get() = _packages.asStateFlow()
private val packageManager = application.packageManager
private val _checkedPackages = MutableStateFlow(mutableStateListOf<String>())
val checkedPackages get() = _checkedPackages.asStateFlow()
private val _include = MutableStateFlow(true)
val include get() = _include.asStateFlow()
private val _allApplications = MutableStateFlow(true)
val allApplications get() = _allApplications.asStateFlow()
suspend fun getTunnelById(id : String?) : TunnelConfig? {
return try {
if(id != null) {
val config = tunnelRepo.getById(id.toLong())
if (config != null) {
_tunnel.emit(config)
_tunnelName.emit(config.name)
}
return config
}
return null
} catch (e : Exception) {
Timber.e(e.message)
null
}
}
fun onTunnelNameChange(name : String) {
_tunnelName.value = name
}
fun onIncludeChange(include : Boolean) {
_include.value = include
}
fun onAddCheckedPackage(packageName : String) {
_checkedPackages.value.add(packageName)
}
fun onAllApplicationsChange(allApplications : Boolean) {
_allApplications.value = allApplications
}
fun onRemoveCheckedPackage(packageName : String) {
_checkedPackages.value.remove(packageName)
}
suspend fun emitCurrentPackageConfigurations(id : String?) {
val tunnelConfig = getTunnelById(id)
if(tunnelConfig != null) {
val config = TunnelConfig.configFromQuick(tunnelConfig.wgQuick)
val excludedApps = config.`interface`.excludedApplications
val includedApps = config.`interface`.includedApplications
if(excludedApps.isNullOrEmpty() && includedApps.isNullOrEmpty()) {
_allApplications.emit(true)
return
}
if(excludedApps.isEmpty()) {
_include.emit(true)
_checkedPackages.emit(includedApps.toMutableStateList())
} else {
_include.emit(false)
_checkedPackages.emit(excludedApps.toMutableStateList())
}
_allApplications.emit(false)
}
}
suspend fun emitAllInternetCapablePackages() {
_packages.emit(getAllInternetCapablePackages())
}
private fun getAllInternetCapablePackages() : List<PackageInfo> {
return getPackagesHoldingPermissions(arrayOf(Manifest.permission.INTERNET))
}
private fun getPackagesHoldingPermissions(permissions: Array<String>): List<PackageInfo> {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
packageManager.getPackagesHoldingPermissions(permissions, PackageManager.PackageInfoFlags.of(0L))
} else {
@Suppress("DEPRECATION")
packageManager.getPackagesHoldingPermissions(permissions, 0)
}
}
suspend fun onSaveAllChanges() {
var wgQuick = _tunnel.value?.wgQuick
if(wgQuick != null) {
wgQuick = if(_include.value) {
TunnelConfig.setIncludedApplicationsOnQuick(_checkedPackages.value, wgQuick)
} else {
TunnelConfig.setExcludedApplicationsOnQuick(_checkedPackages.value, wgQuick)
}
if(_allApplications.value) {
wgQuick = TunnelConfig.clearAllApplicationsFromConfig(wgQuick)
}
_tunnel.value?.copy(
name = _tunnelName.value,
wgQuick = wgQuick
)?.let {
tunnelRepo.save(it)
}
}
}
}
@@ -1,39 +1,41 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.main package com.zaneschepke.wireguardautotunnel.ui.screens.main
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.widget.Toast
import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.clickable
import androidx.compose.foundation.gestures.detectTapGestures import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.FileOpen
import androidx.compose.material.icons.filled.QrCode
import androidx.compose.material.icons.rounded.Add import androidx.compose.material.icons.rounded.Add
import androidx.compose.material.icons.rounded.Delete import androidx.compose.material.icons.rounded.Delete
import androidx.compose.material.icons.rounded.Edit import androidx.compose.material.icons.rounded.Edit
import androidx.compose.material3.AlertDialog import androidx.compose.material3.Divider
import androidx.compose.material3.Button
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.FabPosition import androidx.compose.material3.FabPosition
import androidx.compose.material3.FloatingActionButton import androidx.compose.material3.FloatingActionButton
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.material3.SnackbarDuration import androidx.compose.material3.SnackbarDuration
import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.SnackbarResult import androidx.compose.material3.SnackbarResult
import androidx.compose.material3.Switch import androidx.compose.material3.Switch
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
@@ -53,9 +55,11 @@ import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.navigation.NavController
import com.wireguard.android.backend.Tunnel import com.wireguard.android.backend.Tunnel
import com.zaneschepke.wireguardautotunnel.R import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.service.tunnel.model.TunnelConfig import com.zaneschepke.wireguardautotunnel.service.tunnel.model.TunnelConfig
import com.zaneschepke.wireguardautotunnel.ui.Routes
import com.zaneschepke.wireguardautotunnel.ui.common.RowListItem import com.zaneschepke.wireguardautotunnel.ui.common.RowListItem
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@@ -63,17 +67,17 @@ import kotlinx.coroutines.launch
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun MainScreen(viewModel: MainViewModel = hiltViewModel(), padding : PaddingValues, fun MainScreen(viewModel: MainViewModel = hiltViewModel(), padding : PaddingValues,
snackbarHostState : SnackbarHostState) { snackbarHostState : SnackbarHostState, navController: NavController) {
val haptic = LocalHapticFeedback.current val haptic = LocalHapticFeedback.current
val context = LocalContext.current val context = LocalContext.current
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
val sheetState = rememberModalBottomSheetState()
var showBottomSheet by remember { mutableStateOf(false) }
val tunnels by viewModel.tunnels.collectAsStateWithLifecycle(mutableListOf()) val tunnels by viewModel.tunnels.collectAsStateWithLifecycle(mutableListOf())
val viewState = viewModel.viewState.collectAsStateWithLifecycle() val viewState = viewModel.viewState.collectAsStateWithLifecycle()
var showAlertDialog by remember { mutableStateOf(false) }
var selectedTunnel by remember { mutableStateOf<TunnelConfig?>(null) } var selectedTunnel by remember { mutableStateOf<TunnelConfig?>(null) }
val settings by viewModel.settings.collectAsStateWithLifecycle()
val state by viewModel.state.collectAsStateWithLifecycle(Tunnel.State.DOWN) val state by viewModel.state.collectAsStateWithLifecycle(Tunnel.State.DOWN)
val tunnelName by viewModel.tunnelName.collectAsStateWithLifecycle("") val tunnelName by viewModel.tunnelName.collectAsStateWithLifecycle("")
@@ -111,14 +115,14 @@ fun MainScreen(viewModel: MainViewModel = hiltViewModel(), padding : PaddingValu
FloatingActionButton( FloatingActionButton(
modifier = Modifier.padding(bottom = 90.dp), modifier = Modifier.padding(bottom = 90.dp),
onClick = { onClick = {
pickFileLauncher.launch("*/*") showBottomSheet = true
}, },
containerColor = MaterialTheme.colorScheme.secondary, containerColor = MaterialTheme.colorScheme.secondary,
shape = RoundedCornerShape(16.dp), shape = RoundedCornerShape(16.dp),
) { ) {
Icon( Icon(
imageVector = Icons.Rounded.Add, imageVector = Icons.Rounded.Add,
contentDescription = "Add Tunnel", contentDescription = stringResource(id = R.string.add_tunnel),
tint = Color.DarkGray, tint = Color.DarkGray,
) )
} }
@@ -135,6 +139,42 @@ fun MainScreen(viewModel: MainViewModel = hiltViewModel(), padding : PaddingValu
Text(text = stringResource(R.string.no_tunnels), fontStyle = FontStyle.Italic) Text(text = stringResource(R.string.no_tunnels), fontStyle = FontStyle.Italic)
} }
} }
if (showBottomSheet) {
ModalBottomSheet(
onDismissRequest = {
showBottomSheet = false
},
sheetState = sheetState
) {
// Sheet content
Row(
modifier = Modifier
.fillMaxWidth()
.clickable {
showBottomSheet = false
pickFileLauncher.launch("*/*")
}
.padding(10.dp)
) {
Icon(Icons.Filled.FileOpen, contentDescription = stringResource(id = R.string.open_file), modifier = Modifier.padding(10.dp))
Text(stringResource(id = R.string.add_from_file), modifier = Modifier.padding(10.dp))
}
Divider()
Row(modifier = Modifier
.fillMaxWidth()
.clickable {
scope.launch {
showBottomSheet = false
viewModel.onTunnelQRSelected()
}
}
.padding(10.dp)
) {
Icon(Icons.Filled.QrCode, contentDescription = stringResource(id = R.string.qr_scan), modifier = Modifier.padding(10.dp))
Text(stringResource(id = R.string.add_from_qr), modifier = Modifier.padding(10.dp))
}
}
}
Column( Column(
horizontalAlignment = Alignment.Start, horizontalAlignment = Alignment.Start,
verticalArrangement = Arrangement.Top, verticalArrangement = Arrangement.Top,
@@ -158,12 +198,12 @@ fun MainScreen(viewModel: MainViewModel = hiltViewModel(), padding : PaddingValu
if (tunnel.id == selectedTunnel?.id) { if (tunnel.id == selectedTunnel?.id) {
Row() { Row() {
IconButton(onClick = { IconButton(onClick = {
showAlertDialog = true navController.navigate("${Routes.Config.name}/${selectedTunnel?.id}")
}) { }) {
Icon(Icons.Rounded.Edit, "Edit") Icon(Icons.Rounded.Edit, stringResource(id = R.string.edit))
} }
IconButton(onClick = { viewModel.onDelete(tunnel) }) { IconButton(onClick = { viewModel.onDelete(tunnel) }) {
Icon(Icons.Rounded.Delete, "Delete") Icon(Icons.Rounded.Delete, stringResource(id = R.string.delete))
} }
} }
} else { } else {
@@ -177,36 +217,6 @@ fun MainScreen(viewModel: MainViewModel = hiltViewModel(), padding : PaddingValu
}) })
} }
} }
if (showAlertDialog && selectedTunnel != null) {
AlertDialog(onDismissRequest = {
showAlertDialog = false
}, confirmButton = {
Button(onClick = {
if (tunnels.any { it.name == selectedTunnel?.name }) {
Toast.makeText(context, context.resources.getString(R.string.tunnel_exists), Toast.LENGTH_LONG)
.show()
return@Button
}
viewModel.onEditTunnel(selectedTunnel!!)
showAlertDialog = false
}) {
Text("Save")
}
},
title = { Text("Tunnel Edit") }, text = {
OutlinedTextField(
value = selectedTunnel!!.name,
onValueChange = {
selectedTunnel = selectedTunnel!!.copy(
name = it
)
},
label = { Text("Tunnel Name") },
modifier = Modifier.padding(start = 15.dp, top = 5.dp),
maxLines = 1,
)
})
}
} }
} }
} }
@@ -7,9 +7,11 @@ import android.net.Uri
import android.provider.OpenableColumns import android.provider.OpenableColumns
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.wireguard.config.BadConfigException
import com.wireguard.config.Config import com.wireguard.config.Config
import com.zaneschepke.wireguardautotunnel.R import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.repository.Repository import com.zaneschepke.wireguardautotunnel.repository.Repository
import com.zaneschepke.wireguardautotunnel.service.barcode.CodeScanner
import com.zaneschepke.wireguardautotunnel.service.foreground.Action import com.zaneschepke.wireguardautotunnel.service.foreground.Action
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceState import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceState
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceTracker import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceTracker
@@ -32,7 +34,8 @@ import javax.inject.Inject
class MainViewModel @Inject constructor(private val application : Application, class MainViewModel @Inject constructor(private val application : Application,
private val tunnelRepo : Repository<TunnelConfig>, private val tunnelRepo : Repository<TunnelConfig>,
private val settingsRepo : Repository<Settings>, private val settingsRepo : Repository<Settings>,
private val vpnService: VpnService private val vpnService: VpnService,
private val codeScanner: CodeScanner
) : ViewModel() { ) : ViewModel() {
private val _viewState = MutableStateFlow(ViewState()) private val _viewState = MutableStateFlow(ViewState())
@@ -43,7 +46,9 @@ class MainViewModel @Inject constructor(private val application : Application,
private val _settings = MutableStateFlow(Settings()) private val _settings = MutableStateFlow(Settings())
val settings get() = _settings.asStateFlow() val settings get() = _settings.asStateFlow()
private val defaultConfigName = "tunnel${(Math.random() * 1000).toInt()}" private val defaultConfigName = {
"tunnel${(Math.random() * 100000).toInt()}"
}
init { init {
@@ -86,22 +91,6 @@ class MainViewModel @Inject constructor(private val application : Application,
} }
} }
fun onEditTunnel(tunnel: TunnelConfig) {
viewModelScope.launch {
tunnelRepo.save(tunnel)
val settings = settingsRepo.getAll()
if(!settings.isNullOrEmpty() && settings[0].defaultTunnel != null) {
val setting = settings[0]
val defaultTunnelConfig = TunnelConfig.from(setting.defaultTunnel!!)
if(defaultTunnelConfig.id == tunnel.id) {
setting.defaultTunnel = tunnel.toString()
settingsRepo.save(setting)
}
}
}
}
fun onTunnelStart(tunnelConfig : TunnelConfig) = viewModelScope.launch { fun onTunnelStart(tunnelConfig : TunnelConfig) = viewModelScope.launch {
ServiceTracker.actionOnService( Action.START, application, WireGuardTunnelService::class.java, ServiceTracker.actionOnService( Action.START, application, WireGuardTunnelService::class.java,
mapOf(application.resources.getString(R.string.tunnel_extras_key) to tunnelConfig.toString())) mapOf(application.resources.getString(R.string.tunnel_extras_key) to tunnelConfig.toString()))
@@ -111,6 +100,17 @@ class MainViewModel @Inject constructor(private val application : Application,
ServiceTracker.actionOnService( Action.STOP, application, WireGuardTunnelService::class.java) ServiceTracker.actionOnService( Action.STOP, application, WireGuardTunnelService::class.java)
} }
suspend fun onTunnelQRSelected() {
codeScanner.scan().collect {
Timber.d(it)
if(!it.isNullOrEmpty() && it.contains(application.resources.getString(R.string.config_validation))) {
tunnelRepo.save(TunnelConfig(name = defaultConfigName(), wgQuick = it))
} else {
showSnackBarMessage("Invalid QR code. Try again.")
}
}
}
fun onTunnelFileSelected(uri : Uri) { fun onTunnelFileSelected(uri : Uri) {
val fileName = getFileName(application.applicationContext, uri) val fileName = getFileName(application.applicationContext, uri)
val extension = getFileExtensionFromFileName(fileName) val extension = getFileExtensionFromFileName(fileName)
@@ -123,26 +123,32 @@ class MainViewModel @Inject constructor(private val application : Application,
val stream = application.applicationContext.contentResolver.openInputStream(uri) val stream = application.applicationContext.contentResolver.openInputStream(uri)
stream ?: return stream ?: return
val bufferReader = stream.bufferedReader(charset = Charsets.UTF_8) val bufferReader = stream.bufferedReader(charset = Charsets.UTF_8)
val config = Config.parse(bufferReader) try {
val tunnelName = getNameFromFileName(fileName) val config = Config.parse(bufferReader)
viewModelScope.launch { val tunnelName = getNameFromFileName(fileName)
tunnelRepo.save(TunnelConfig(name = tunnelName, wgQuick = config.toWgQuickString())) viewModelScope.launch {
tunnelRepo.save(TunnelConfig(name = tunnelName, wgQuick = config.toWgQuickString()))
}
stream.close()
} catch(_: BadConfigException) {
viewModelScope.launch {
showSnackBarMessage(application.applicationContext.getString(R.string.bad_config))
}
} }
stream.close()
} }
@SuppressLint("Range") @SuppressLint("Range")
private fun getFileName(context: Context, uri: Uri): String { private fun getFileName(context: Context, uri: Uri): String {
if (uri.scheme == "content") { if (uri.scheme == "content") {
val cursor = context.contentResolver.query(uri, null, null, null, null) val cursor = context.contentResolver.query(uri, null, null, null, null)
cursor ?: return defaultConfigName cursor ?: return defaultConfigName()
cursor.use { cursor.use {
if(cursor.moveToFirst()) { if(cursor.moveToFirst()) {
return cursor.getString(cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME)) return cursor.getString(cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME))
} }
} }
} }
return defaultConfigName return defaultConfigName()
} }
suspend fun showSnackBarMessage(message : String) { suspend fun showSnackBarMessage(message : String) {
@@ -4,28 +4,24 @@ import android.Manifest
import android.content.Intent import android.content.Intent
import android.net.Uri import android.net.Uri
import android.provider.Settings import android.provider.Settings
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ExperimentalLayoutApi import androidx.compose.foundation.layout.ExperimentalLayoutApi
import androidx.compose.foundation.layout.FlowRow import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Close import androidx.compose.material.icons.filled.Close
import androidx.compose.material.icons.filled.Done
import androidx.compose.material.icons.outlined.Add import androidx.compose.material.icons.outlined.Add
import androidx.compose.material.icons.outlined.AddCircleOutline
import androidx.compose.material.icons.outlined.Done
import androidx.compose.material.icons.rounded.LocationOff import androidx.compose.material.icons.rounded.LocationOff
import androidx.compose.material.icons.rounded.Map
import androidx.compose.material3.Button import androidx.compose.material3.Button
import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
@@ -51,6 +47,7 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontStyle import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.ImeAction
@@ -68,7 +65,6 @@ import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.service.tunnel.model.TunnelConfig import com.zaneschepke.wireguardautotunnel.service.tunnel.model.TunnelConfig
import com.zaneschepke.wireguardautotunnel.ui.Routes import com.zaneschepke.wireguardautotunnel.ui.Routes
import com.zaneschepke.wireguardautotunnel.ui.common.ClickableIconButton import com.zaneschepke.wireguardautotunnel.ui.common.ClickableIconButton
import com.zaneschepke.wireguardautotunnel.ui.common.PermissionRequestFailedScreen
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@OptIn(ExperimentalMaterial3Api::class, ExperimentalPermissionsApi::class, @OptIn(ExperimentalMaterial3Api::class, ExperimentalPermissionsApi::class,
@@ -84,6 +80,9 @@ fun SettingsScreen(
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
val context = LocalContext.current val context = LocalContext.current
val focusManager = LocalFocusManager.current
val interactionSource = remember { MutableInteractionSource() }
var expanded by remember { mutableStateOf(false) } var expanded by remember { mutableStateOf(false) }
val viewState by viewModel.viewState.collectAsStateWithLifecycle() val viewState by viewModel.viewState.collectAsStateWithLifecycle()
val settings by viewModel.settings.collectAsStateWithLifecycle() val settings by viewModel.settings.collectAsStateWithLifecycle()
@@ -122,7 +121,7 @@ fun SettingsScreen(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.padding(padding)) { .padding(padding)) {
Icon(Icons.Rounded.LocationOff, contentDescription = "Map", modifier = Modifier Icon(Icons.Rounded.LocationOff, contentDescription = stringResource(id = R.string.map), modifier = Modifier
.padding(30.dp) .padding(30.dp)
.size(128.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_title), textAlign = TextAlign.Center, modifier = Modifier.padding(30.dp), fontSize = 20.sp)
@@ -138,7 +137,7 @@ fun SettingsScreen(
Button(onClick = { Button(onClick = {
navController.navigate(Routes.Main.name) navController.navigate(Routes.Main.name)
}) { }) {
Text("No thanks") Text(stringResource(id = R.string.no_thanks))
} }
Button(onClick = { Button(onClick = {
scope.launch { scope.launch {
@@ -149,7 +148,7 @@ fun SettingsScreen(
context.startActivity(intentSettings) context.startActivity(intentSettings)
} }
}) { }) {
Text("Turn on") Text(stringResource(id = R.string.turn_on))
} }
} }
} }
@@ -179,6 +178,9 @@ fun SettingsScreen(
verticalArrangement = Arrangement.Top, verticalArrangement = Arrangement.Top,
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.clickable(indication = null, interactionSource = interactionSource) {
focusManager.clearFocus()
}
.padding(padding) .padding(padding)
) { ) {
Row( Row(
+23 -2
View File
@@ -12,12 +12,12 @@
<string name="turn_off_tunnel">Turn off tunnel before editing</string> <string name="turn_off_tunnel">Turn off tunnel before editing</string>
<string name="no_tunnels">No tunnels added yet!</string> <string name="no_tunnels">No tunnels added yet!</string>
<string name="tunnel_exists">Tunnel name already exists</string> <string name="tunnel_exists">Tunnel name already exists</string>
<string name="discord_url">https://discord.gg/Ad5fuEts</string> <string name="discord_url">https://discord.gg/rbRRNh6H7V</string>
<string name="watcher_notification_title">Watcher Service</string> <string name="watcher_notification_title">Watcher Service</string>
<string name="watcher_notification_text">Monitoring network state changes</string> <string name="watcher_notification_text">Monitoring network state changes</string>
<string name="tunnel_start_title">VPN Connected</string> <string name="tunnel_start_title">VPN Connected</string>
<string name="tunnel_start_text">Connected to tunnel -</string> <string name="tunnel_start_text">Connected to tunnel -</string>
<string name="vpn_permission_required">VPN permission is required for the app to work properly.</string> <string name="vpn_permission_required">VPN permission is required for the app to work properly. If this permission is not launching, please disable \"Always-on VPN\" in your phone settings for the official WireGuard mobile app and try again.</string>
<string name="notification_permission_required">Notifications permission is required for the app to work properly.</string> <string name="notification_permission_required">Notifications permission is required for the app to work properly.</string>
<string name="open_settings">Open Settings</string> <string name="open_settings">Open Settings</string>
<string name="add_trusted_ssid">Add Trusted SSID</string> <string name="add_trusted_ssid">Add Trusted SSID</string>
@@ -37,4 +37,25 @@
<string name="support_text">Thank you for using WG Tunnel! If you are experiencing issues with the app, please reach out on Discord or create an issue on Github. I will try to address the issue as quickly as possible. Thank you!</string> <string name="support_text">Thank you for using WG Tunnel! If you are experiencing issues with the app, please reach out on Discord or create an issue on Github. I will try to address the issue as quickly as possible. Thank you!</string>
<string name="trusted_ssid_empty_description">Enter SSID</string> <string name="trusted_ssid_empty_description">Enter SSID</string>
<string name="trusted_ssid_value_description">Submit SSID</string> <string name="trusted_ssid_value_description">Submit SSID</string>
<string name="config_validation">[Interface]</string>
<string name="invalid_qr">Invalid QR code.</string>
<string name="add_from_file">Add tunnel from files</string>
<string name="open_file">File Open</string>
<string name="add_from_qr">Add tunnel from QR code</string>
<string name="qr_scan">QR Scan</string>
<string name="tunnel_edit">Tunnel Edit</string>
<string name="tunnel_name">Tunnel Name</string>
<string name="edit">Edit</string>
<string name="delete">Delete</string>
<string name="add_tunnel">Add Tunnel</string>
<string name="exclude">Exclude</string>
<string name="include">Include</string>
<string name="tunnel_all">Tunnel all applications</string>
<string name="config_changes_saved">Configuration changes saved.</string>
<string name="save_changes">Save changes</string>
<string name="icon">Icon</string>
<string name="no_thanks">No thanks</string>
<string name="turn_on">Turn on</string>
<string name="map">Map</string>
<string name="bad_config">Bad config. Please try again.</string>
</resources> </resources>
Binary file not shown.

After

Width:  |  Height:  |  Size: 154 KiB

+1 -1
View File
@@ -8,7 +8,7 @@ buildscript {
dependencies { dependencies {
classpath("io.objectbox:objectbox-gradle-plugin:$objectBoxVersion") classpath("io.objectbox:objectbox-gradle-plugin:$objectBoxVersion")
classpath("com.google.gms:google-services:4.3.15") classpath("com.google.gms:google-services:4.3.15")
classpath("com.google.firebase:firebase-crashlytics-gradle:2.9.5") classpath("com.google.firebase:firebase-crashlytics-gradle:2.9.6")
} }
} }