mirror of
https://github.com/wgtunnel/android.git
synced 2026-07-03 14:07:49 +02:00
Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a98a47f54d | |||
| cb3a7d024b | |||
| 259be21b04 | |||
| 8b81831910 | |||
| 49bf7fa8b9 | |||
| 2f53a8f3b4 |
@@ -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
@@ -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
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+12
-4
@@ -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) }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+2
@@ -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 -> {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-3
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+53
@@ -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 {
|
||||||
|
|||||||
+200
@@ -0,0 +1,200 @@
|
|||||||
|
package com.zaneschepke.wireguardautotunnel.ui.screens.config
|
||||||
|
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.compose.foundation.Image
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.fillMaxHeight
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.lazy.items
|
||||||
|
import androidx.compose.foundation.text.KeyboardActions
|
||||||
|
import androidx.compose.foundation.text.KeyboardOptions
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.rounded.Android
|
||||||
|
import androidx.compose.material3.Button
|
||||||
|
import androidx.compose.material3.Checkbox
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.OutlinedTextField
|
||||||
|
import androidx.compose.material3.Switch
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.ExperimentalComposeUiApi
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.platform.LocalFocusManager
|
||||||
|
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.input.ImeAction
|
||||||
|
import androidx.compose.ui.text.input.KeyboardCapitalization
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
|
import androidx.navigation.NavController
|
||||||
|
import com.google.accompanist.drawablepainter.DrawablePainter
|
||||||
|
import com.zaneschepke.wireguardautotunnel.R
|
||||||
|
import com.zaneschepke.wireguardautotunnel.ui.Routes
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
@OptIn(ExperimentalComposeUiApi::class)
|
||||||
|
@Composable
|
||||||
|
fun ConfigScreen(
|
||||||
|
viewModel: ConfigViewModel = hiltViewModel(),
|
||||||
|
padding: PaddingValues,
|
||||||
|
navController: NavController,
|
||||||
|
id : String?
|
||||||
|
) {
|
||||||
|
|
||||||
|
val context = LocalContext.current
|
||||||
|
val focusManager = LocalFocusManager.current
|
||||||
|
val keyboardController = LocalSoftwareKeyboardController.current
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
|
val tunnel by viewModel.tunnel.collectAsStateWithLifecycle(null)
|
||||||
|
val tunnelName = viewModel.tunnelName.collectAsStateWithLifecycle()
|
||||||
|
val packages by viewModel.packages.collectAsStateWithLifecycle()
|
||||||
|
val checkedPackages by viewModel.checkedPackages.collectAsStateWithLifecycle()
|
||||||
|
val include by viewModel.include.collectAsStateWithLifecycle()
|
||||||
|
val allApplications by viewModel.allApplications.collectAsStateWithLifecycle()
|
||||||
|
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
viewModel.getTunnelById(id)
|
||||||
|
viewModel.emitAllInternetCapablePackages()
|
||||||
|
viewModel.emitCurrentPackageConfigurations(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
if(tunnel != null) {
|
||||||
|
Column(
|
||||||
|
horizontalAlignment = Alignment.Start,
|
||||||
|
verticalArrangement = Arrangement.Top,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(padding)
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 20.dp, vertical = 7.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween
|
||||||
|
) {
|
||||||
|
OutlinedTextField(
|
||||||
|
value = tunnelName.value,
|
||||||
|
onValueChange = {
|
||||||
|
viewModel.onTunnelNameChange(it)
|
||||||
|
},
|
||||||
|
label = { Text(stringResource(id = R.string.tunnel_name)) },
|
||||||
|
maxLines = 1,
|
||||||
|
keyboardOptions = KeyboardOptions(
|
||||||
|
capitalization = KeyboardCapitalization.None,
|
||||||
|
imeAction = ImeAction.Done
|
||||||
|
),
|
||||||
|
keyboardActions = KeyboardActions(
|
||||||
|
onDone = {
|
||||||
|
focusManager.clearFocus()
|
||||||
|
keyboardController?.hide()
|
||||||
|
viewModel.onTunnelNameChange(tunnelName.value)
|
||||||
|
}
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 20.dp, vertical = 7.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween
|
||||||
|
) {
|
||||||
|
Text(stringResource(id = R.string.tunnel_all))
|
||||||
|
Switch(
|
||||||
|
checked = allApplications,
|
||||||
|
onCheckedChange = {
|
||||||
|
viewModel.onAllApplicationsChange(!allApplications)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if(!allApplications) {
|
||||||
|
Row(modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 20.dp, vertical = 7.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween) {
|
||||||
|
Row(verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween){
|
||||||
|
Text(stringResource(id = R.string.include))
|
||||||
|
Checkbox(
|
||||||
|
checked = include,
|
||||||
|
onCheckedChange = {
|
||||||
|
viewModel.onIncludeChange(!include)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Row(verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween){
|
||||||
|
Text(stringResource(id = R.string.exclude))
|
||||||
|
Checkbox(
|
||||||
|
checked = !include,
|
||||||
|
onCheckedChange = {
|
||||||
|
viewModel.onIncludeChange(!include)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
LazyColumn(modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.fillMaxHeight(.75f)
|
||||||
|
.padding(horizontal = 14.dp, vertical = 7.dp),
|
||||||
|
verticalArrangement = Arrangement.Center,
|
||||||
|
horizontalAlignment = Alignment.Start) {
|
||||||
|
items(packages) { pack ->
|
||||||
|
Row(verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween) {
|
||||||
|
Row(
|
||||||
|
horizontalArrangement = Arrangement.Center,
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
modifier = Modifier.padding(5.dp)
|
||||||
|
) {
|
||||||
|
val drawable = pack.applicationInfo?.loadIcon(context.packageManager)
|
||||||
|
if(drawable != null) {
|
||||||
|
Image(painter = DrawablePainter(drawable), stringResource(id = R.string.icon), modifier = Modifier.size(50.dp, 50.dp))
|
||||||
|
} else {
|
||||||
|
Icon(Icons.Rounded.Android, stringResource(id = R.string.edit), modifier = Modifier.size(50.dp, 50.dp))
|
||||||
|
}
|
||||||
|
Text(pack.applicationInfo.loadLabel(context.packageManager).toString(), modifier = Modifier.padding(5.dp))
|
||||||
|
}
|
||||||
|
Checkbox(
|
||||||
|
checked = (checkedPackages.contains(pack.packageName)),
|
||||||
|
onCheckedChange = {
|
||||||
|
if(it) viewModel.onAddCheckedPackage(pack.packageName) else viewModel.onRemoveCheckedPackage(pack.packageName)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Row(
|
||||||
|
horizontalArrangement = Arrangement.Center,
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
) {
|
||||||
|
Button(onClick = {
|
||||||
|
scope.launch {
|
||||||
|
viewModel.onSaveAllChanges()
|
||||||
|
Toast.makeText(context, context.resources.getString(R.string.config_changes_saved), Toast.LENGTH_LONG).show()
|
||||||
|
navController.navigate(Routes.Main.name)
|
||||||
|
}
|
||||||
|
}, Modifier.padding(25.dp)) {
|
||||||
|
Text(stringResource(id = R.string.save_changes))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+133
@@ -0,0 +1,133 @@
|
|||||||
|
package com.zaneschepke.wireguardautotunnel.ui.screens.config
|
||||||
|
|
||||||
|
import android.Manifest
|
||||||
|
import android.app.Application
|
||||||
|
import android.content.pm.PackageInfo
|
||||||
|
import android.content.pm.PackageManager
|
||||||
|
import android.os.Build
|
||||||
|
import androidx.compose.runtime.mutableStateListOf
|
||||||
|
import androidx.compose.runtime.toMutableStateList
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import com.zaneschepke.wireguardautotunnel.repository.Repository
|
||||||
|
import com.zaneschepke.wireguardautotunnel.service.tunnel.model.TunnelConfig
|
||||||
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
import timber.log.Timber
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
@HiltViewModel
|
||||||
|
class ConfigViewModel @Inject constructor(private val application : Application,
|
||||||
|
private val tunnelRepo : Repository<TunnelConfig>) : ViewModel() {
|
||||||
|
|
||||||
|
private val _tunnel = MutableStateFlow<TunnelConfig?>(null)
|
||||||
|
private val _tunnelName = MutableStateFlow("")
|
||||||
|
val tunnelName get() = _tunnelName.asStateFlow()
|
||||||
|
val tunnel get() = _tunnel.asStateFlow()
|
||||||
|
private val _packages = MutableStateFlow(emptyList<PackageInfo>())
|
||||||
|
val packages get() = _packages.asStateFlow()
|
||||||
|
private val packageManager = application.packageManager
|
||||||
|
|
||||||
|
private val _checkedPackages = MutableStateFlow(mutableStateListOf<String>())
|
||||||
|
val checkedPackages get() = _checkedPackages.asStateFlow()
|
||||||
|
private val _include = MutableStateFlow(true)
|
||||||
|
val include get() = _include.asStateFlow()
|
||||||
|
|
||||||
|
private val _allApplications = MutableStateFlow(true)
|
||||||
|
val allApplications get() = _allApplications.asStateFlow()
|
||||||
|
|
||||||
|
suspend fun getTunnelById(id : String?) : TunnelConfig? {
|
||||||
|
return try {
|
||||||
|
if(id != null) {
|
||||||
|
val config = tunnelRepo.getById(id.toLong())
|
||||||
|
if (config != null) {
|
||||||
|
_tunnel.emit(config)
|
||||||
|
_tunnelName.emit(config.name)
|
||||||
|
|
||||||
|
}
|
||||||
|
return config
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
} catch (e : Exception) {
|
||||||
|
Timber.e(e.message)
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onTunnelNameChange(name : String) {
|
||||||
|
_tunnelName.value = name
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onIncludeChange(include : Boolean) {
|
||||||
|
_include.value = include
|
||||||
|
}
|
||||||
|
fun onAddCheckedPackage(packageName : String) {
|
||||||
|
_checkedPackages.value.add(packageName)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onAllApplicationsChange(allApplications : Boolean) {
|
||||||
|
_allApplications.value = allApplications
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onRemoveCheckedPackage(packageName : String) {
|
||||||
|
_checkedPackages.value.remove(packageName)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun emitCurrentPackageConfigurations(id : String?) {
|
||||||
|
val tunnelConfig = getTunnelById(id)
|
||||||
|
if(tunnelConfig != null) {
|
||||||
|
val config = TunnelConfig.configFromQuick(tunnelConfig.wgQuick)
|
||||||
|
val excludedApps = config.`interface`.excludedApplications
|
||||||
|
val includedApps = config.`interface`.includedApplications
|
||||||
|
if(excludedApps.isNullOrEmpty() && includedApps.isNullOrEmpty()) {
|
||||||
|
_allApplications.emit(true)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if(excludedApps.isEmpty()) {
|
||||||
|
_include.emit(true)
|
||||||
|
_checkedPackages.emit(includedApps.toMutableStateList())
|
||||||
|
} else {
|
||||||
|
_include.emit(false)
|
||||||
|
_checkedPackages.emit(excludedApps.toMutableStateList())
|
||||||
|
}
|
||||||
|
_allApplications.emit(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun emitAllInternetCapablePackages() {
|
||||||
|
_packages.emit(getAllInternetCapablePackages())
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getAllInternetCapablePackages() : List<PackageInfo> {
|
||||||
|
return getPackagesHoldingPermissions(arrayOf(Manifest.permission.INTERNET))
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getPackagesHoldingPermissions(permissions: Array<String>): List<PackageInfo> {
|
||||||
|
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||||
|
packageManager.getPackagesHoldingPermissions(permissions, PackageManager.PackageInfoFlags.of(0L))
|
||||||
|
} else {
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
packageManager.getPackagesHoldingPermissions(permissions, 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun onSaveAllChanges() {
|
||||||
|
var wgQuick = _tunnel.value?.wgQuick
|
||||||
|
if(wgQuick != null) {
|
||||||
|
wgQuick = if(_include.value) {
|
||||||
|
TunnelConfig.setIncludedApplicationsOnQuick(_checkedPackages.value, wgQuick)
|
||||||
|
} else {
|
||||||
|
TunnelConfig.setExcludedApplicationsOnQuick(_checkedPackages.value, wgQuick)
|
||||||
|
}
|
||||||
|
if(_allApplications.value) {
|
||||||
|
wgQuick = TunnelConfig.clearAllApplicationsFromConfig(wgQuick)
|
||||||
|
}
|
||||||
|
_tunnel.value?.copy(
|
||||||
|
name = _tunnelName.value,
|
||||||
|
wgQuick = wgQuick
|
||||||
|
)?.let {
|
||||||
|
tunnelRepo.save(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+53
-43
@@ -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,
|
|
||||||
)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+31
-25
@@ -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) {
|
||||||
|
|||||||
+12
-10
@@ -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(
|
||||||
|
|||||||
@@ -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
@@ -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")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user