Compare commits

..

8 Commits

Author SHA1 Message Date
Zane Schepke 75790ec6d5 bump: app version to 3.1.6 2023-10-21 14:13:47 -04:00
Zane Schepke a1941b7229 feat: shortcut intents and battery saver
Added the ability to turn on and off tunnels via intents to the shortcut activity.

Added a setting to enable or disable shortcut/intent control of tunnels.

Added an experimental battery saver setting to auto-tunneling to fix the battery drain issue caused by wakelock.

Fixes a bug where sometimes the config screen could crash if there are issues parsing the tunnel config data.

Database migration
2023-10-21 14:03:36 -04:00
Zane Schepke 37bae82700 feat: zip file import export
Added support for importing zip files containing multiple config files.
Closes #33

Added support for exporting all config files to downloads folder as a zip with biometric or security pin approval.

Added support for editing or viewing private key with biometric or security pin approval.

Fixed a bug where VPN status indicator functionality was unintentionally disabled.

Other various enhancements and refactors.
2023-10-16 22:43:28 -04:00
Zane Schepke 77cd328a71 fix: config screen edit bug
Fixes a bug where config edit will break configs that have commas because a comma and space are required in tunnel configs

 #42
2023-10-14 01:32:46 -04:00
Zane Schepke 5a1430706b fix: config edit whitespace bug
Closes #42
2023-10-13 23:42:39 -04:00
Zane Schepke 321730536d fix: file import handling
Fixes bug causing crashes when importing a file that is not a .conf file.

Fixes file import on AndroidTV and FireTV
Closes #39

Fixes usability issue on AndroidTV where textboxes immediately open the keyboard on hover.
Allows users two keypress access to turning on tunnels from app load.
Closes #36

Improves service efficiencies by making coroutines lifecycle aware.
2023-10-12 20:20:53 -04:00
Zane Schepke 2912238f27 Merge pull request #41 from licaon-kter/patch-1
Gradle 8.3 was released
2023-10-11 09:31:02 -04:00
Licaon_Kter bc7daacd13 Gradle 8.3 was released 2023-10-11 11:42:58 +00:00
42 changed files with 1047 additions and 431 deletions
+18 -2
View File
@@ -14,11 +14,15 @@ android {
applicationId = "com.zaneschepke.wireguardautotunnel" applicationId = "com.zaneschepke.wireguardautotunnel"
minSdk = 26 minSdk = 26
targetSdk = 34 targetSdk = 34
versionCode = 31000 versionCode = 31600
versionName = "3.1.0" versionName = "3.1.6"
multiDexEnabled = true multiDexEnabled = true
ksp {
arg("room.schemaLocation", "$projectDir/schemas")
}
resourceConfigurations.addAll(listOf("en")) resourceConfigurations.addAll(listOf("en"))
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
@@ -81,6 +85,8 @@ val generalImplementation by configurations
dependencies { dependencies {
implementation(libs.androidx.core.ktx) implementation(libs.androidx.core.ktx)
implementation(libs.androidx.lifecycle.runtime.ktx) implementation(libs.androidx.lifecycle.runtime.ktx)
// optional - helpers for implementing LifecycleOwner in a Service
implementation(libs.androidx.lifecycle.service)
implementation(libs.androidx.activity.compose) implementation(libs.androidx.activity.compose)
implementation(platform(libs.androidx.compose.bom)) implementation(platform(libs.androidx.compose.bom))
implementation(libs.androidx.compose.ui) implementation(libs.androidx.compose.ui)
@@ -126,6 +132,9 @@ dependencies {
//lifecycle //lifecycle
implementation(libs.lifecycle.runtime.compose) implementation(libs.lifecycle.runtime.compose)
implementation(libs.androidx.lifecycle.runtime.ktx)
implementation(libs.androidx.lifecycle.process)
//icons //icons
implementation(libs.material.icons.extended) implementation(libs.material.icons.extended)
@@ -140,4 +149,11 @@ dependencies {
//barcode scanning //barcode scanning
implementation(libs.zxing.android.embedded) implementation(libs.zxing.android.embedded)
implementation(libs.zxing.core) implementation(libs.zxing.core)
//bio
implementation(libs.androidx.biometric.ktx)
//shortcuts
implementation(libs.androidx.core)
implementation(libs.androidx.core.google.shortcuts)
} }
@@ -0,0 +1,112 @@
{
"formatVersion": 1,
"database": {
"version": 1,
"identityHash": "ba86153e6fb0b823197b987239b03e64",
"entities": [
{
"tableName": "Settings",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_tunnel_enabled` INTEGER NOT NULL, `is_tunnel_on_mobile_data_enabled` INTEGER NOT NULL, `trusted_network_ssids` TEXT NOT NULL, `default_tunnel` TEXT, `is_always_on_vpn_enabled` INTEGER NOT NULL, `is_tunnel_on_ethernet_enabled` INTEGER NOT NULL)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isAutoTunnelEnabled",
"columnName": "is_tunnel_enabled",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isTunnelOnMobileDataEnabled",
"columnName": "is_tunnel_on_mobile_data_enabled",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "trustedNetworkSSIDs",
"columnName": "trusted_network_ssids",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "defaultTunnel",
"columnName": "default_tunnel",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "isAlwaysOnVpnEnabled",
"columnName": "is_always_on_vpn_enabled",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isTunnelOnEthernetEnabled",
"columnName": "is_tunnel_on_ethernet_enabled",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "TunnelConfig",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `wg_quick` TEXT NOT NULL)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "wgQuick",
"columnName": "wg_quick",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
},
"indices": [
{
"name": "index_TunnelConfig_name",
"unique": true,
"columnNames": [
"name"
],
"orders": [],
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_TunnelConfig_name` ON `${TABLE_NAME}` (`name`)"
}
],
"foreignKeys": []
}
],
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'ba86153e6fb0b823197b987239b03e64')"
]
}
}
@@ -0,0 +1,126 @@
{
"formatVersion": 1,
"database": {
"version": 2,
"identityHash": "65b1c9efff61712231fa64d1f19f3915",
"entities": [
{
"tableName": "Settings",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_tunnel_enabled` INTEGER NOT NULL, `is_tunnel_on_mobile_data_enabled` INTEGER NOT NULL, `trusted_network_ssids` TEXT NOT NULL, `default_tunnel` TEXT, `is_always_on_vpn_enabled` INTEGER NOT NULL, `is_tunnel_on_ethernet_enabled` INTEGER NOT NULL, `is_shortcuts_enabled` INTEGER NOT NULL DEFAULT false, `is_battery_saver_enabled` INTEGER NOT NULL DEFAULT false)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isAutoTunnelEnabled",
"columnName": "is_tunnel_enabled",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isTunnelOnMobileDataEnabled",
"columnName": "is_tunnel_on_mobile_data_enabled",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "trustedNetworkSSIDs",
"columnName": "trusted_network_ssids",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "defaultTunnel",
"columnName": "default_tunnel",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "isAlwaysOnVpnEnabled",
"columnName": "is_always_on_vpn_enabled",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isTunnelOnEthernetEnabled",
"columnName": "is_tunnel_on_ethernet_enabled",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isShortcutsEnabled",
"columnName": "is_shortcuts_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isBatterySaverEnabled",
"columnName": "is_battery_saver_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "TunnelConfig",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `wg_quick` TEXT NOT NULL)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "wgQuick",
"columnName": "wg_quick",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
},
"indices": [
{
"name": "index_TunnelConfig_name",
"unique": true,
"columnNames": [
"name"
],
"orders": [],
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_TunnelConfig_name` ON `${TABLE_NAME}` (`name`)"
}
],
"foreignKeys": []
}
],
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '65b1c9efff61712231fa64d1f19f3915')"
]
}
}
@@ -1,13 +1,11 @@
package com.zaneschepke.wireguardautotunnel package com.zaneschepke.wireguardautotunnel
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import org.junit.Assert.assertEquals
import org.junit.Test import org.junit.Test
import org.junit.runner.RunWith import org.junit.runner.RunWith
import org.junit.Assert.*
/** /**
* Instrumented test, which will execute on an Android device. * Instrumented test, which will execute on an Android device.
* *
+4
View File
@@ -4,6 +4,10 @@
<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" />
<uses-permission
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="32"
tools:ignore="ScopedStorage" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" <uses-permission android:name="android.permission.ACCESS_WIFI_STATE"
android:maxSdkVersion="30" android:maxSdkVersion="30"
tools:ignore="LeanbackUsesWifi" /> tools:ignore="LeanbackUsesWifi" />
@@ -2,15 +2,18 @@ package com.zaneschepke.wireguardautotunnel
object Constants { object Constants {
const val MANUAL_TUNNEL_CONFIG_ID = "0" const val MANUAL_TUNNEL_CONFIG_ID = "0"
const val WATCHER_SERVICE_WAKE_LOCK_TIMEOUT = 10*60*1000L /*10 minute*/
const val VPN_CONNECTIVITY_CHECK_INTERVAL = 3000L const val VPN_CONNECTIVITY_CHECK_INTERVAL = 3000L
const val VPN_STATISTIC_CHECK_INTERVAL = 10000L const val VPN_STATISTIC_CHECK_INTERVAL = 10000L
const val TOGGLE_TUNNEL_DELAY = 500L const val TOGGLE_TUNNEL_DELAY = 500L
const val FADE_IN_ANIMATION_DURATION = 1000 const val FADE_IN_ANIMATION_DURATION = 1000
const val SLIDE_IN_ANIMATION_DURATION = 500 const val SLIDE_IN_ANIMATION_DURATION = 500
const val SLIDE_IN_TRANSITION_OFFSET = 1000 const val SLIDE_IN_TRANSITION_OFFSET = 1000
const val VALID_FILE_EXTENSION = ".conf" const val CONF_FILE_EXTENSION = ".conf"
const val ZIP_FILE_EXTENSION = ".zip"
const val URI_CONTENT_SCHEME = "content" const val URI_CONTENT_SCHEME = "content"
const val URI_PACKAGE_SCHEME = "package" const val URI_PACKAGE_SCHEME = "package"
const val ALLOWED_FILE_TYPES = "*/*" const val ALLOWED_FILE_TYPES = "*/*"
const val ANDROID_TV_STUBS = "com.google.android.tv.frameworkpackagestubs" const val GOOGLE_TV_EXPLORER_STUB = "com.google.android.tv.frameworkpackagestubs"
const val ANDROID_TV_EXPLORER_STUB = "com.android.tv.frameworkpackagestubs"
} }
@@ -0,0 +1,24 @@
package com.zaneschepke.wireguardautotunnel
import android.content.BroadcastReceiver
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import kotlin.coroutines.CoroutineContext
import kotlin.coroutines.EmptyCoroutineContext
fun BroadcastReceiver.goAsync(
context: CoroutineContext = EmptyCoroutineContext,
block: suspend CoroutineScope.() -> Unit
) {
val pendingResult = goAsync()
@OptIn(DelicateCoroutinesApi::class) // Must run globally; there's no teardown callback.
GlobalScope.launch(context) {
try {
block()
} finally {
pendingResult.finish()
}
}
}
@@ -3,11 +3,11 @@ package com.zaneschepke.wireguardautotunnel
import android.app.Application import android.app.Application
import android.content.Context import android.content.Context
import android.content.pm.PackageManager import android.content.pm.PackageManager
import androidx.lifecycle.ProcessLifecycleOwner
import androidx.lifecycle.lifecycleScope
import com.zaneschepke.wireguardautotunnel.repository.SettingsDoa import com.zaneschepke.wireguardautotunnel.repository.SettingsDoa
import com.zaneschepke.wireguardautotunnel.repository.model.Settings import com.zaneschepke.wireguardautotunnel.repository.model.Settings
import dagger.hilt.android.HiltAndroidApp import dagger.hilt.android.HiltAndroidApp
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
@@ -27,12 +27,16 @@ class WireGuardAutoTunnel : Application() {
} }
private fun initSettings() { private fun initSettings() {
CoroutineScope(Dispatchers.IO).launch { with(ProcessLifecycleOwner.get()) {
lifecycleScope.launch {
if(settingsRepo.getAll().isEmpty()) { if(settingsRepo.getAll().isEmpty()) {
settingsRepo.save(Settings()) settingsRepo.save(Settings())
} }
} }
} }
}
companion object { companion object {
fun isRunningOnAndroidTv(context : Context) : Boolean { fun isRunningOnAndroidTv(context : Context) : Boolean {
@@ -3,13 +3,11 @@ package com.zaneschepke.wireguardautotunnel.receiver
import android.content.BroadcastReceiver import android.content.BroadcastReceiver
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import com.zaneschepke.wireguardautotunnel.goAsync
import com.zaneschepke.wireguardautotunnel.repository.SettingsDoa import com.zaneschepke.wireguardautotunnel.repository.SettingsDoa
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.cancel import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch
import javax.inject.Inject import javax.inject.Inject
@AndroidEntryPoint @AndroidEntryPoint
@@ -18,9 +16,8 @@ class BootReceiver : BroadcastReceiver() {
@Inject @Inject
lateinit var settingsRepo : SettingsDoa lateinit var settingsRepo : SettingsDoa
override fun onReceive(context: Context, intent: Intent) { override fun onReceive(context: Context, intent: Intent) = goAsync {
if (intent.action == Intent.ACTION_BOOT_COMPLETED) { if (intent.action == Intent.ACTION_BOOT_COMPLETED) {
CoroutineScope(Dispatchers.IO).launch {
try { try {
val settings = settingsRepo.getAll() val settings = settingsRepo.getAll()
if (settings.isNotEmpty()) { if (settings.isNotEmpty()) {
@@ -35,4 +32,3 @@ class BootReceiver : BroadcastReceiver() {
} }
} }
} }
}
@@ -4,14 +4,12 @@ import android.content.BroadcastReceiver
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import com.zaneschepke.wireguardautotunnel.Constants import com.zaneschepke.wireguardautotunnel.Constants
import com.zaneschepke.wireguardautotunnel.goAsync
import com.zaneschepke.wireguardautotunnel.repository.SettingsDoa import com.zaneschepke.wireguardautotunnel.repository.SettingsDoa
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.cancel import kotlinx.coroutines.cancel
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import javax.inject.Inject import javax.inject.Inject
@AndroidEntryPoint @AndroidEntryPoint
@@ -19,8 +17,7 @@ class NotificationActionReceiver : BroadcastReceiver() {
@Inject @Inject
lateinit var settingsRepo : SettingsDoa lateinit var settingsRepo : SettingsDoa
override fun onReceive(context: Context, intent: Intent?) { override fun onReceive(context: Context, intent: Intent?) = goAsync {
CoroutineScope(Dispatchers.IO).launch {
try { try {
val settings = settingsRepo.getAll() val settings = settingsRepo.getAll()
if (settings.isNotEmpty()) { if (settings.isNotEmpty()) {
@@ -36,4 +33,3 @@ class NotificationActionReceiver : BroadcastReceiver() {
} }
} }
} }
}
@@ -1,12 +1,15 @@
package com.zaneschepke.wireguardautotunnel.repository package com.zaneschepke.wireguardautotunnel.repository
import androidx.room.AutoMigration
import androidx.room.Database import androidx.room.Database
import androidx.room.RoomDatabase import androidx.room.RoomDatabase
import androidx.room.TypeConverters import androidx.room.TypeConverters
import com.zaneschepke.wireguardautotunnel.repository.model.Settings import com.zaneschepke.wireguardautotunnel.repository.model.Settings
import com.zaneschepke.wireguardautotunnel.repository.model.TunnelConfig import com.zaneschepke.wireguardautotunnel.repository.model.TunnelConfig
@Database(entities = [Settings::class, TunnelConfig::class], version = 1, exportSchema = false) @Database(entities = [Settings::class, TunnelConfig::class], version = 2, autoMigrations = [
AutoMigration(from = 1, to = 2)
], exportSchema = true)
@TypeConverters(DatabaseListConverters::class) @TypeConverters(DatabaseListConverters::class)
abstract class AppDatabase : RoomDatabase() { abstract class AppDatabase : RoomDatabase() {
abstract fun settingDao(): SettingsDoa abstract fun settingDao(): SettingsDoa
@@ -8,7 +8,7 @@ class DatabaseListConverters {
return value.joinToString(",") return value.joinToString(",")
} }
@TypeConverter @TypeConverter
fun <T> stringToList(value: String): MutableList<String> { fun stringToList(value: String): MutableList<String> {
if(value.isEmpty()) return mutableListOf() if(value.isEmpty()) return mutableListOf()
return value.split(",").toMutableList() return value.split(",").toMutableList()
} }
@@ -13,6 +13,8 @@ data class Settings(
@ColumnInfo(name = "default_tunnel") var defaultTunnel : String? = null, @ColumnInfo(name = "default_tunnel") var defaultTunnel : String? = null,
@ColumnInfo(name = "is_always_on_vpn_enabled") var isAlwaysOnVpnEnabled : Boolean = false, @ColumnInfo(name = "is_always_on_vpn_enabled") var isAlwaysOnVpnEnabled : Boolean = false,
@ColumnInfo(name = "is_tunnel_on_ethernet_enabled") var isTunnelOnEthernetEnabled : Boolean = false, @ColumnInfo(name = "is_tunnel_on_ethernet_enabled") var isTunnelOnEthernetEnabled : Boolean = false,
@ColumnInfo(name = "is_shortcuts_enabled", defaultValue = "false") var isShortcutsEnabled : Boolean = false,
@ColumnInfo(name = "is_battery_saver_enabled", defaultValue = "false") var isBatterySaverEnabled : Boolean = false,
) { ) {
fun isTunnelConfigDefault(tunnelConfig: TunnelConfig) : Boolean { fun isTunnelConfigDefault(tunnelConfig: TunnelConfig) : Boolean {
return if (defaultTunnel != null) { return if (defaultTunnel != null) {
@@ -1,22 +1,24 @@
package com.zaneschepke.wireguardautotunnel.service.foreground package com.zaneschepke.wireguardautotunnel.service.foreground
import android.app.Service
import android.content.Intent import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.os.IBinder import android.os.IBinder
import androidx.lifecycle.LifecycleService
import timber.log.Timber import timber.log.Timber
open class ForegroundService : Service() { open class ForegroundService : LifecycleService() {
private var isServiceStarted = false private var isServiceStarted = false
override fun onBind(intent: Intent): IBinder? { override fun onBind(intent: Intent): IBinder? {
super.onBind(intent)
// We don't provide binding, so return null // We don't provide binding, so return null
return null return null
} }
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
super.onStartCommand(intent, flags, startId)
Timber.d("onStartCommand executed with startId: $startId") Timber.d("onStartCommand executed with startId: $startId")
if (intent != null) { if (intent != null) {
val action = intent.action val action = intent.action
@@ -91,14 +91,6 @@ object ServiceManager {
WireGuardConnectivityWatcherService::class.java) WireGuardConnectivityWatcherService::class.java)
} }
fun toggleWatcherService(context: Context, tunnelConfig : String) {
when(getServiceState( context,
WireGuardConnectivityWatcherService::class.java,)) {
ServiceState.STARTED -> stopWatcherService(context)
ServiceState.STOPPED -> startWatcherService(context, tunnelConfig)
}
}
fun toggleWatcherServiceForeground(context: Context, tunnelConfig : String) { fun toggleWatcherServiceForeground(context: Context, tunnelConfig : String) {
when(getServiceState( context, when(getServiceState( context,
WireGuardConnectivityWatcherService::class.java,)) { WireGuardConnectivityWatcherService::class.java,)) {
@@ -7,6 +7,7 @@ import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.os.PowerManager import android.os.PowerManager
import android.os.SystemClock import android.os.SystemClock
import androidx.lifecycle.lifecycleScope
import com.wireguard.android.backend.Tunnel import com.wireguard.android.backend.Tunnel
import com.zaneschepke.wireguardautotunnel.Constants import com.zaneschepke.wireguardautotunnel.Constants
import com.zaneschepke.wireguardautotunnel.R import com.zaneschepke.wireguardautotunnel.R
@@ -20,18 +21,18 @@ import com.zaneschepke.wireguardautotunnel.service.network.WifiService
import com.zaneschepke.wireguardautotunnel.service.notification.NotificationService import com.zaneschepke.wireguardautotunnel.service.notification.NotificationService
import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnService import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnService
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
@AndroidEntryPoint @AndroidEntryPoint
class WireGuardConnectivityWatcherService : ForegroundService() { class WireGuardConnectivityWatcherService : ForegroundService() {
private val foregroundId = 122; private val foregroundId = 122
@Inject @Inject
lateinit var wifiService: NetworkService<WifiService> lateinit var wifiService: NetworkService<WifiService>
@@ -51,22 +52,22 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
@Inject @Inject
lateinit var vpnService: VpnService lateinit var vpnService: VpnService
private var isWifiConnected = false; private var isWifiConnected = false
private var isEthernetConnected = false; private var isEthernetConnected = false
private var isMobileDataConnected = false; private var isMobileDataConnected = false
private var currentNetworkSSID = ""; private var currentNetworkSSID = ""
private lateinit var watcherJob : Job; private lateinit var watcherJob: Job
private lateinit var setting: Settings private lateinit var setting: Settings
private lateinit var tunnelConfig: String private lateinit var tunnelConfig: String
private var wakeLock: PowerManager.WakeLock? = null private var wakeLock: PowerManager.WakeLock? = null
private val tag = this.javaClass.name; private val tag = this.javaClass.name
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
CoroutineScope(Dispatchers.Main).launch { lifecycleScope.launch(Dispatchers.Main) {
launchWatcherNotification() launchWatcherNotification()
} }
} }
@@ -79,7 +80,9 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
this.tunnelConfig = tunnelId this.tunnelConfig = tunnelId
} }
// we need this lock so our service gets not affected by Doze Mode // we need this lock so our service gets not affected by Doze Mode
lifecycleScope.launch {
initWakeLock() initWakeLock()
}
cancelWatcherJob() cancelWatcherJob()
if (this::tunnelConfig.isInitialized) { if (this::tunnelConfig.isInitialized) {
startWatcherJob() startWatcherJob()
@@ -103,7 +106,8 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
val notification = notificationService.createNotification( val notification = notificationService.createNotification(
channelId = getString(R.string.watcher_channel_id), channelId = getString(R.string.watcher_channel_id),
channelName = getString(R.string.watcher_channel_name), channelName = getString(R.string.watcher_channel_name),
description = getString(R.string.watcher_notification_text)) description = getString(R.string.watcher_notification_text)
)
super.startForeground(foregroundId, notification) super.startForeground(foregroundId, notification)
} }
@@ -111,21 +115,37 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
override fun onTaskRemoved(rootIntent: Intent) { override fun onTaskRemoved(rootIntent: Intent) {
Timber.d("Task Removed called") Timber.d("Task Removed called")
val restartServiceIntent = Intent(rootIntent) val restartServiceIntent = Intent(rootIntent)
val restartServicePendingIntent: PendingIntent = PendingIntent.getService(this, 1, restartServiceIntent, val restartServicePendingIntent: PendingIntent = PendingIntent.getService(
PendingIntent.FLAG_ONE_SHOT or PendingIntent.FLAG_IMMUTABLE); this, 1, restartServiceIntent,
applicationContext.getSystemService(Context.ALARM_SERVICE); PendingIntent.FLAG_ONE_SHOT or PendingIntent.FLAG_IMMUTABLE
val alarmService: AlarmManager = applicationContext.getSystemService(Context.ALARM_SERVICE) as AlarmManager; )
alarmService.set(AlarmManager.ELAPSED_REALTIME, SystemClock.elapsedRealtime() + 1000, restartServicePendingIntent); applicationContext.getSystemService(Context.ALARM_SERVICE)
val alarmService: AlarmManager =
applicationContext.getSystemService(Context.ALARM_SERVICE) as AlarmManager
alarmService.set(
AlarmManager.ELAPSED_REALTIME,
SystemClock.elapsedRealtime() + 1000,
restartServicePendingIntent
)
} }
private fun initWakeLock() { private suspend fun initWakeLock() {
val isBatterySaverOn = withContext(lifecycleScope.coroutineContext) {
settingsRepo.getAll().firstOrNull()?.isBatterySaverEnabled ?: false
}
wakeLock = wakeLock =
(getSystemService(Context.POWER_SERVICE) as PowerManager).run { (getSystemService(Context.POWER_SERVICE) as PowerManager).run {
newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "$tag::lock").apply { newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "$tag::lock").apply {
if (isBatterySaverOn) {
Timber.d("Initiating wakelock with timeout")
acquire(Constants.WATCHER_SERVICE_WAKE_LOCK_TIMEOUT)
} else {
Timber.d("Initiating wakelock with zero timeout")
acquire() acquire()
} }
} }
} }
}
private fun cancelWatcherJob() { private fun cancelWatcherJob() {
if (this::watcherJob.isInitialized) { if (this::watcherJob.isInitialized) {
@@ -134,8 +154,8 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
} }
private fun startWatcherJob() { private fun startWatcherJob() {
watcherJob = CoroutineScope(Dispatchers.IO).launch { watcherJob = lifecycleScope.launch(Dispatchers.IO) {
val settings = settingsRepo.getAll(); val settings = settingsRepo.getAll()
if (settings.isNotEmpty()) { if (settings.isNotEmpty()) {
setting = settings[0] setting = settings[0]
} }
@@ -165,10 +185,12 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
Timber.d("Gained Mobile data connection") Timber.d("Gained Mobile data connection")
isMobileDataConnected = true isMobileDataConnected = true
} }
is NetworkStatus.CapabilitiesChanged -> { is NetworkStatus.CapabilitiesChanged -> {
isMobileDataConnected = true isMobileDataConnected = true
Timber.d("Mobile data capabilities changed") Timber.d("Mobile data capabilities changed")
} }
is NetworkStatus.Unavailable -> { is NetworkStatus.Unavailable -> {
isMobileDataConnected = false isMobileDataConnected = false
Timber.d("Lost mobile data connection") Timber.d("Lost mobile data connection")
@@ -184,10 +206,12 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
Timber.d("Gained Ethernet connection") Timber.d("Gained Ethernet connection")
isEthernetConnected = true isEthernetConnected = true
} }
is NetworkStatus.CapabilitiesChanged -> { is NetworkStatus.CapabilitiesChanged -> {
Timber.d("Ethernet capabilities changed") Timber.d("Ethernet capabilities changed")
isEthernetConnected = true isEthernetConnected = true
} }
is NetworkStatus.Unavailable -> { is NetworkStatus.Unavailable -> {
isEthernetConnected = false isEthernetConnected = false
Timber.d("Lost Ethernet connection") Timber.d("Lost Ethernet connection")
@@ -203,11 +227,13 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
Timber.d("Gained Wi-Fi connection") Timber.d("Gained Wi-Fi connection")
isWifiConnected = true isWifiConnected = true
} }
is NetworkStatus.CapabilitiesChanged -> { is NetworkStatus.CapabilitiesChanged -> {
Timber.d("Wifi capabilities changed") Timber.d("Wifi capabilities changed")
isWifiConnected = true isWifiConnected = true
currentNetworkSSID = wifiService.getNetworkName(it.networkCapabilities) ?: ""; currentNetworkSSID = wifiService.getNetworkName(it.networkCapabilities) ?: ""
} }
is NetworkStatus.Unavailable -> { is NetworkStatus.Unavailable -> {
isWifiConnected = false isWifiConnected = false
Timber.d("Lost Wi-Fi connection") Timber.d("Lost Wi-Fi connection")
@@ -224,19 +250,23 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
if (!isEthernetConnected && setting.isTunnelOnMobileDataEnabled && if (!isEthernetConnected && setting.isTunnelOnMobileDataEnabled &&
!isWifiConnected && !isWifiConnected &&
isMobileDataConnected isMobileDataConnected
&& vpnService.getState() == Tunnel.State.DOWN) { && vpnService.getState() == Tunnel.State.DOWN
) {
ServiceManager.startVpnService(this, tunnelConfig) ServiceManager.startVpnService(this, tunnelConfig)
} else if (!isEthernetConnected && !setting.isTunnelOnMobileDataEnabled && } else if (!isEthernetConnected && !setting.isTunnelOnMobileDataEnabled &&
!isWifiConnected && !isWifiConnected &&
vpnService.getState() == Tunnel.State.UP) { vpnService.getState() == Tunnel.State.UP
) {
ServiceManager.stopVpnService(this) ServiceManager.stopVpnService(this)
} else if (!isEthernetConnected && isWifiConnected && } else if (!isEthernetConnected && isWifiConnected &&
!setting.trustedNetworkSSIDs.contains(currentNetworkSSID) && !setting.trustedNetworkSSIDs.contains(currentNetworkSSID) &&
(vpnService.getState() != Tunnel.State.UP)) { (vpnService.getState() != Tunnel.State.UP)
) {
ServiceManager.startVpnService(this, tunnelConfig) ServiceManager.startVpnService(this, tunnelConfig)
} else if (!isEthernetConnected && (isWifiConnected && } else if (!isEthernetConnected && (isWifiConnected &&
setting.trustedNetworkSSIDs.contains(currentNetworkSSID)) && setting.trustedNetworkSSIDs.contains(currentNetworkSSID)) &&
(vpnService.getState() == Tunnel.State.UP)) { (vpnService.getState() == Tunnel.State.UP)
) {
ServiceManager.stopVpnService(this) ServiceManager.stopVpnService(this)
} }
delay(Constants.VPN_CONNECTIVITY_CHECK_INTERVAL) delay(Constants.VPN_CONNECTIVITY_CHECK_INTERVAL)
@@ -3,15 +3,15 @@ package com.zaneschepke.wireguardautotunnel.service.foreground
import android.app.PendingIntent import android.app.PendingIntent
import android.content.Intent import android.content.Intent
import android.os.Bundle import android.os.Bundle
import androidx.lifecycle.lifecycleScope
import com.zaneschepke.wireguardautotunnel.R import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.receiver.NotificationActionReceiver import com.zaneschepke.wireguardautotunnel.receiver.NotificationActionReceiver
import com.zaneschepke.wireguardautotunnel.repository.SettingsDoa import com.zaneschepke.wireguardautotunnel.repository.SettingsDoa
import com.zaneschepke.wireguardautotunnel.repository.model.TunnelConfig
import com.zaneschepke.wireguardautotunnel.service.notification.NotificationService import com.zaneschepke.wireguardautotunnel.service.notification.NotificationService
import com.zaneschepke.wireguardautotunnel.service.tunnel.HandshakeStatus import com.zaneschepke.wireguardautotunnel.service.tunnel.HandshakeStatus
import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnService import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnService
import com.zaneschepke.wireguardautotunnel.repository.model.TunnelConfig
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@@ -21,7 +21,7 @@ import javax.inject.Inject
@AndroidEntryPoint @AndroidEntryPoint
class WireGuardTunnelService : ForegroundService() { class WireGuardTunnelService : ForegroundService() {
private val foregroundId = 123; private val foregroundId = 123
@Inject @Inject
lateinit var vpnService : VpnService lateinit var vpnService : VpnService
@@ -38,7 +38,7 @@ class WireGuardTunnelService : ForegroundService() {
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
CoroutineScope(Dispatchers.Main).launch { lifecycleScope.launch(Dispatchers.Main) {
launchVpnStartingNotification() launchVpnStartingNotification()
} }
} }
@@ -48,7 +48,8 @@ class WireGuardTunnelService : ForegroundService() {
launchVpnStartingNotification() launchVpnStartingNotification()
val tunnelConfigString = extras?.getString(getString(R.string.tunnel_extras_key)) val tunnelConfigString = extras?.getString(getString(R.string.tunnel_extras_key))
cancelJob() cancelJob()
job = CoroutineScope(Dispatchers.IO).launch { job = lifecycleScope.launch(Dispatchers.IO) {
launch {
if(tunnelConfigString != null) { if(tunnelConfigString != null) {
try { try {
val tunnelConfig = TunnelConfig.from(tunnelConfigString) val tunnelConfig = TunnelConfig.from(tunnelConfigString)
@@ -60,7 +61,7 @@ class WireGuardTunnelService : ForegroundService() {
} }
} else { } else {
Timber.d("Tunnel config null, starting default tunnel") Timber.d("Tunnel config null, starting default tunnel")
val settings = settingsRepo.getAll(); val settings = settingsRepo.getAll()
if(settings.isNotEmpty()) { if(settings.isNotEmpty()) {
val setting = settings[0] val setting = settings[0]
if(setting.defaultTunnel != null && setting.isAlwaysOnVpnEnabled) { if(setting.defaultTunnel != null && setting.isAlwaysOnVpnEnabled) {
@@ -71,7 +72,7 @@ class WireGuardTunnelService : ForegroundService() {
} }
} }
} }
CoroutineScope(job).launch { launch {
var didShowConnected = false var didShowConnected = false
var didShowFailedHandshakeNotification = false var didShowFailedHandshakeNotification = false
vpnService.handshakeStatus.collect { vpnService.handshakeStatus.collect {
@@ -102,10 +103,11 @@ class WireGuardTunnelService : ForegroundService() {
} }
} }
} }
}
override fun stopService(extras : Bundle?) { override fun stopService(extras : Bundle?) {
super.stopService(extras) super.stopService(extras)
CoroutineScope(Dispatchers.IO).launch { lifecycleScope.launch(Dispatchers.IO) {
vpnService.stopTunnel() vpnService.stopTunnel()
} }
cancelJob() cancelJob()
@@ -140,7 +142,8 @@ class WireGuardTunnelService : ForegroundService() {
val notification = notificationService.createNotification( val notification = notificationService.createNotification(
channelId = getString(R.string.vpn_channel_id), channelId = getString(R.string.vpn_channel_id),
channelName = getString(R.string.vpn_channel_name), channelName = getString(R.string.vpn_channel_name),
action = PendingIntent.getBroadcast(this,0,Intent(this, NotificationActionReceiver::class.java),PendingIntent.FLAG_IMMUTABLE), action = PendingIntent.getBroadcast(this,0,
Intent(this, NotificationActionReceiver::class.java),PendingIntent.FLAG_IMMUTABLE),
actionText = getString(R.string.restart), actionText = getString(R.string.restart),
title = getString(R.string.vpn_connection_failed), title = getString(R.string.vpn_connection_failed),
onGoing = false, onGoing = false,
@@ -14,7 +14,7 @@ import javax.inject.Inject
class WireGuardNotification @Inject constructor(@ApplicationContext private val context: Context) : NotificationService { class WireGuardNotification @Inject constructor(@ApplicationContext private val context: Context) : NotificationService {
private val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager; private val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
override fun createNotification( override fun createNotification(
channelId: String, channelId: String,
@@ -2,6 +2,7 @@ package com.zaneschepke.wireguardautotunnel.service.shortcut
import android.os.Bundle import android.os.Bundle
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.lifecycle.lifecycleScope
import com.zaneschepke.wireguardautotunnel.repository.SettingsDoa import com.zaneschepke.wireguardautotunnel.repository.SettingsDoa
import com.zaneschepke.wireguardautotunnel.repository.TunnelConfigDao import com.zaneschepke.wireguardautotunnel.repository.TunnelConfigDao
import com.zaneschepke.wireguardautotunnel.repository.model.Settings import com.zaneschepke.wireguardautotunnel.repository.model.Settings
@@ -11,7 +12,6 @@ import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
import com.zaneschepke.wireguardautotunnel.service.foreground.WireGuardTunnelService import com.zaneschepke.wireguardautotunnel.service.foreground.WireGuardTunnelService
import com.zaneschepke.wireguardautotunnel.util.WgTunnelException import com.zaneschepke.wireguardautotunnel.util.WgTunnelException
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import timber.log.Timber import timber.log.Timber
@@ -26,10 +26,8 @@ class ShortcutsActivity : ComponentActivity() {
@Inject @Inject
lateinit var tunnelConfigRepo : TunnelConfigDao lateinit var tunnelConfigRepo : TunnelConfigDao
private val scope = CoroutineScope(Dispatchers.Main);
private fun attemptWatcherServiceToggle(tunnelConfig : String) { private fun attemptWatcherServiceToggle(tunnelConfig : String) {
scope.launch { lifecycleScope.launch(Dispatchers.Main) {
val settings = getSettings() val settings = getSettings()
if(settings.isAutoTunnelEnabled) { if(settings.isAutoTunnelEnabled) {
ServiceManager.toggleWatcherServiceForeground(this@ShortcutsActivity, tunnelConfig) ServiceManager.toggleWatcherServiceForeground(this@ShortcutsActivity, tunnelConfig)
@@ -41,14 +39,21 @@ class ShortcutsActivity : ComponentActivity() {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
if(intent.getStringExtra(CLASS_NAME_EXTRA_KEY) if(intent.getStringExtra(CLASS_NAME_EXTRA_KEY)
.equals(WireGuardTunnelService::class.java.simpleName)) { .equals(WireGuardTunnelService::class.java.simpleName)) {
scope.launch { lifecycleScope.launch(Dispatchers.Main) {
try {
val settings = getSettings() val settings = getSettings()
val tunnelConfig = if(settings.defaultTunnel == null) { if(settings.isShortcutsEnabled) {
try {
val tunnelName = intent.getStringExtra(TUNNEL_NAME_EXTRA_KEY)
val tunnelConfig = if(tunnelName != null) {
tunnelConfigRepo.getAll().firstOrNull { it.name == tunnelName }
} else {
if(settings.defaultTunnel == null) {
tunnelConfigRepo.getAll().first() tunnelConfigRepo.getAll().first()
} else { } else {
TunnelConfig.from(settings.defaultTunnel!!) TunnelConfig.from(settings.defaultTunnel!!)
} }
}
tunnelConfig ?: return@launch
attemptWatcherServiceToggle(tunnelConfig.toString()) attemptWatcherServiceToggle(tunnelConfig.toString())
when(intent.action){ when(intent.action){
Action.STOP.name -> ServiceManager.stopVpnService(this@ShortcutsActivity) Action.STOP.name -> ServiceManager.stopVpnService(this@ShortcutsActivity)
@@ -59,6 +64,7 @@ class ShortcutsActivity : ComponentActivity() {
} }
} }
} }
}
finish() finish()
} }
@@ -71,6 +77,7 @@ class ShortcutsActivity : ComponentActivity() {
} }
} }
companion object { companion object {
const val TUNNEL_NAME_EXTRA_KEY = "tunnelName"
const val CLASS_NAME_EXTRA_KEY = "className" const val CLASS_NAME_EXTRA_KEY = "className"
} }
} }
@@ -31,7 +31,7 @@ class TunnelControlTile : TileService() {
@Inject @Inject
lateinit var vpnService : VpnService lateinit var vpnService : VpnService
private val scope = CoroutineScope(Dispatchers.Main); private val scope = CoroutineScope(Dispatchers.Main)
private lateinit var job : Job private lateinit var job : Job
@@ -46,7 +46,7 @@ class TunnelControlTile : TileService() {
super.onTileAdded() super.onTileAdded()
qsTile.contentDescription = this.resources.getString(R.string.toggle_vpn) qsTile.contentDescription = this.resources.getString(R.string.toggle_vpn)
scope.launch { scope.launch {
updateTileState(); updateTileState()
} }
} }
@@ -55,12 +55,17 @@ class TunnelControlTile : TileService() {
cancelJob() cancelJob()
} }
override fun onDestroy() {
super.onDestroy()
scope.cancel()
}
override fun onClick() { override fun onClick() {
super.onClick() super.onClick()
unlockAndRun { unlockAndRun {
scope.launch { scope.launch {
try { try {
val tunnel = determineTileTunnel(); val tunnel = determineTileTunnel()
if(tunnel != null) { if(tunnel != null) {
attemptWatcherServiceToggle(tunnel.toString()) attemptWatcherServiceToggle(tunnel.toString())
if(vpnService.getState() == Tunnel.State.UP) { if(vpnService.getState() == Tunnel.State.UP) {
@@ -79,23 +84,23 @@ class TunnelControlTile : TileService() {
} }
private suspend fun determineTileTunnel() : TunnelConfig? { private suspend fun determineTileTunnel() : TunnelConfig? {
var tunnelConfig : TunnelConfig? = null; var tunnelConfig : TunnelConfig? = null
val settings = settingsRepo.getAll() val settings = settingsRepo.getAll()
if (settings.isNotEmpty()) { if (settings.isNotEmpty()) {
val setting = settings.first() val setting = settings.first()
tunnelConfig = if (setting.defaultTunnel != null) { tunnelConfig = if (setting.defaultTunnel != null) {
TunnelConfig.from(setting.defaultTunnel!!); TunnelConfig.from(setting.defaultTunnel!!)
} else { } else {
val configs = configRepo.getAll(); val configs = configRepo.getAll()
val config = if(configs.isNotEmpty()) { val config = if(configs.isNotEmpty()) {
configs.first(); configs.first()
} else { } else {
null null
} }
config config
} }
} }
return tunnelConfig; return tunnelConfig
} }
@@ -118,13 +123,13 @@ class TunnelControlTile : TileService() {
qsTile.state = Tile.STATE_ACTIVE qsTile.state = Tile.STATE_ACTIVE
} }
Tunnel.State.DOWN -> { Tunnel.State.DOWN -> {
qsTile.state = Tile.STATE_INACTIVE; qsTile.state = Tile.STATE_INACTIVE
} }
else -> { else -> {
qsTile.state = Tile.STATE_UNAVAILABLE qsTile.state = Tile.STATE_UNAVAILABLE
} }
} }
val config = determineTileTunnel(); val config = determineTileTunnel()
setTileDescription(config?.name ?: this.resources.getString(R.string.no_tunnel_available)) setTileDescription(config?.name ?: this.resources.getString(R.string.no_tunnel_available))
qsTile.updateTile() qsTile.updateTile()
} }
@@ -135,13 +140,13 @@ class TunnelControlTile : TileService() {
qsTile.subtitle = description qsTile.subtitle = description
} }
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
qsTile.stateDescription = description; qsTile.stateDescription = description
} }
} }
private fun cancelJob() { private fun cancelJob() {
if(this::job.isInitialized) { if(this::job.isInitialized) {
job.cancel(); job.cancel()
} }
} }
} }
@@ -46,26 +46,36 @@ class WireGuardTunnel @Inject constructor(private val backend : Backend,
override val handshakeStatus: SharedFlow<HandshakeStatus> override val handshakeStatus: SharedFlow<HandshakeStatus>
get() = _handshakeStatus.asSharedFlow() get() = _handshakeStatus.asSharedFlow()
private val scope = CoroutineScope(Dispatchers.IO)
private lateinit var statsJob : Job private lateinit var statsJob : Job
override suspend fun startTunnel(tunnelConfig: TunnelConfig) : Tunnel.State{ override suspend fun startTunnel(tunnelConfig: TunnelConfig) : Tunnel.State{
return try { return try {
if(getState() == Tunnel.State.UP && _tunnelName.value != tunnelConfig.name) { stopTunnelOnConfigChange(tunnelConfig)
stopTunnel() emitTunnelName(tunnelConfig.name)
}
_tunnelName.emit(tunnelConfig.name)
val config = TunnelConfig.configFromQuick(tunnelConfig.wgQuick) val config = TunnelConfig.configFromQuick(tunnelConfig.wgQuick)
val state = backend.setState( val state = backend.setState(
this, Tunnel.State.UP, config) this, Tunnel.State.UP, config)
_state.emit(state) _state.emit(state)
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}")
Tunnel.State.DOWN Tunnel.State.DOWN
} }
} }
private suspend fun emitTunnelName(name : String) {
_tunnelName.emit(name)
}
private suspend fun stopTunnelOnConfigChange(tunnelConfig: TunnelConfig) {
if(getState() == Tunnel.State.UP && _tunnelName.value != tunnelConfig.name) {
stopTunnel()
}
}
override fun getName(): String { override fun getName(): String {
return _tunnelName.value return _tunnelName.value
} }
@@ -86,10 +96,10 @@ class WireGuardTunnel @Inject constructor(private val backend : Backend,
} }
override fun onStateChange(state : Tunnel.State) { override fun onStateChange(state : Tunnel.State) {
val tunnel = this; val tunnel = this
_state.tryEmit(state) _state.tryEmit(state)
if(state == Tunnel.State.UP) { if(state == Tunnel.State.UP) {
statsJob = CoroutineScope(Dispatchers.IO).launch { statsJob = scope.launch {
val handshakeMap = HashMap<Key, Long>() val handshakeMap = HashMap<Key, Long>()
var neverHadHandshakeCounter = 0 var neverHadHandshakeCounter = 0
while (true) { while (true) {
@@ -128,4 +138,6 @@ class WireGuardTunnel @Inject constructor(private val backend : Backend,
_lastHandshake.tryEmit(emptyMap()) _lastHandshake.tryEmit(emptyMap())
} }
} }
} }
@@ -11,6 +11,7 @@ import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.compose.setContent import androidx.activity.compose.setContent
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.compose.animation.ExitTransition
import androidx.compose.animation.ExperimentalAnimationApi import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.animation.core.tween import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeIn
@@ -32,6 +33,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.input.key.onKeyEvent import androidx.compose.ui.input.key.onKeyEvent
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.lifecycle.lifecycleScope
import com.google.accompanist.navigation.animation.AnimatedNavHost import com.google.accompanist.navigation.animation.AnimatedNavHost
import com.google.accompanist.navigation.animation.composable import com.google.accompanist.navigation.animation.composable
import com.google.accompanist.navigation.animation.rememberAnimatedNavController import com.google.accompanist.navigation.animation.rememberAnimatedNavController
@@ -41,9 +43,9 @@ import com.google.accompanist.permissions.rememberPermissionState
import com.wireguard.android.backend.GoBackend import com.wireguard.android.backend.GoBackend
import com.zaneschepke.wireguardautotunnel.Constants import com.zaneschepke.wireguardautotunnel.Constants
import com.zaneschepke.wireguardautotunnel.R import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.ui.common.CustomSnackBar
import com.zaneschepke.wireguardautotunnel.ui.common.PermissionRequestFailedScreen import com.zaneschepke.wireguardautotunnel.ui.common.PermissionRequestFailedScreen
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.BottomNavBar import com.zaneschepke.wireguardautotunnel.ui.common.navigation.BottomNavBar
import com.zaneschepke.wireguardautotunnel.ui.common.prompt.CustomSnackBar
import com.zaneschepke.wireguardautotunnel.ui.screens.config.ConfigScreen import com.zaneschepke.wireguardautotunnel.ui.screens.config.ConfigScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.detail.DetailScreen import com.zaneschepke.wireguardautotunnel.ui.screens.detail.DetailScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.main.MainScreen import com.zaneschepke.wireguardautotunnel.ui.screens.main.MainScreen
@@ -52,7 +54,6 @@ import com.zaneschepke.wireguardautotunnel.ui.screens.support.SupportScreen
import com.zaneschepke.wireguardautotunnel.ui.theme.TransparentSystemBars import com.zaneschepke.wireguardautotunnel.ui.theme.TransparentSystemBars
import com.zaneschepke.wireguardautotunnel.ui.theme.WireguardAutoTunnelTheme import com.zaneschepke.wireguardautotunnel.ui.theme.WireguardAutoTunnelTheme
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import timber.log.Timber import timber.log.Timber
@@ -99,10 +100,10 @@ class MainActivity : AppCompatActivity() {
} }
fun showSnackBarMessage(message : String) { fun showSnackBarMessage(message : String) {
CoroutineScope(Dispatchers.Main).launch { lifecycleScope.launch(Dispatchers.Main) {
val result = snackbarHostState.showSnackbar( val result = snackbarHostState.showSnackbar(
message = message, message = message,
actionLabel = "Okay", actionLabel = applicationContext.getString(R.string.okay),
duration = SnackbarDuration.Short, duration = SnackbarDuration.Short,
) )
when (result) { when (result) {
@@ -184,7 +185,10 @@ class MainActivity : AppCompatActivity() {
fadeIn(animationSpec = tween(Constants.FADE_IN_ANIMATION_DURATION)) fadeIn(animationSpec = tween(Constants.FADE_IN_ANIMATION_DURATION))
} }
} }
}) { }, exitTransition = {
ExitTransition.None
}
) {
MainScreen(padding = padding, showSnackbarMessage = { message -> showSnackBarMessage(message) }, navController = navController) MainScreen(padding = padding, showSnackbarMessage = { message -> showSnackBarMessage(message) }, navController = navController)
} }
composable(Routes.Settings.name, enterTransition = { composable(Routes.Settings.name, enterTransition = {
@@ -1,9 +0,0 @@
package com.zaneschepke.wireguardautotunnel.ui
data class ViewState(
val showSnackbarMessage : Boolean = false,
val snackbarMessage : String = "",
val snackbarActionText : String = "",
val onSnackbarActionClick : () -> Unit = {},
val isLoading : Boolean = false
)
@@ -7,14 +7,10 @@ import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
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.material3.Icon
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment 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.vector.ImageVector
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
@OptIn(ExperimentalFoundationApi::class) @OptIn(ExperimentalFoundationApi::class)
@@ -1,6 +1,5 @@
package com.zaneschepke.wireguardautotunnel.ui.common.config package com.zaneschepke.wireguardautotunnel.ui.common.config
import androidx.compose.foundation.layout.fillMaxWidth
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.material3.OutlinedTextField import androidx.compose.material3.OutlinedTextField
@@ -12,7 +11,7 @@ import androidx.compose.ui.text.input.KeyboardCapitalization
@Composable @Composable
fun fun
ConfigurationTextBox(value : String, hint : String, onValueChange : (String) -> Unit, label : String, onDone : () -> Unit, modifier: Modifier) { ConfigurationTextBox(value : String, hint : String, onValueChange : (String) -> Unit, keyboardActions : KeyboardActions, label : String, modifier: Modifier) {
OutlinedTextField( OutlinedTextField(
modifier = modifier, modifier = modifier,
value = value, value = value,
@@ -29,10 +28,6 @@ fun
capitalization = KeyboardCapitalization.None, capitalization = KeyboardCapitalization.None,
imeAction = ImeAction.Done imeAction = ImeAction.Done
), ),
keyboardActions = KeyboardActions( keyboardActions = keyboardActions,
onDone = {
onDone()
}
),
) )
} }
@@ -1,7 +1,6 @@
package com.zaneschepke.wireguardautotunnel.ui.common.config package com.zaneschepke.wireguardautotunnel.ui.common.config
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
@@ -14,7 +13,7 @@ import androidx.compose.ui.unit.Dp
@Composable @Composable
fun ConfigurationToggle(label : String, enabled : Boolean, checked : Boolean, padding : Dp, fun ConfigurationToggle(label : String, enabled : Boolean, checked : Boolean, padding : Dp,
onCheckChanged : () -> Unit) { onCheckChanged : () -> Unit, modifier : Modifier = Modifier) {
Row( Row(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
@@ -24,6 +23,7 @@ fun ConfigurationToggle(label : String, enabled : Boolean, checked : Boolean, pa
) { ) {
Text(label) Text(label)
Switch( Switch(
modifier = modifier,
enabled = enabled, enabled = enabled,
checked = checked, checked = checked,
onCheckedChange = { onCheckedChange = {
@@ -0,0 +1,79 @@
package com.zaneschepke.wireguardautotunnel.ui.common.prompt
import androidx.biometric.BiometricManager
import androidx.biometric.BiometricManager.Authenticators.BIOMETRIC_WEAK
import androidx.biometric.BiometricManager.Authenticators.DEVICE_CREDENTIAL
import androidx.biometric.BiometricPrompt
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.platform.LocalContext
import androidx.core.content.ContextCompat
import androidx.fragment.app.FragmentActivity
@Composable
fun AuthorizationPrompt(onSuccess : () -> Unit, onFailure : () -> Unit, onError : (String) -> Unit) {
val context = LocalContext.current
val biometricManager = BiometricManager.from(context)
val bio = biometricManager.canAuthenticate(BIOMETRIC_WEAK or DEVICE_CREDENTIAL)
val isBiometricAvailable = remember {
when(bio){
BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE -> {
onError("Biometrics not available")
false
}
BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED -> {
onError("Biometrics not created")
false
}
BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE -> {
onError("Biometric hardware not found")
false
}
BiometricManager.BIOMETRIC_ERROR_SECURITY_UPDATE_REQUIRED -> {
onError("Biometric security update required")
false
}
BiometricManager.BIOMETRIC_ERROR_UNSUPPORTED -> {
onError("Biometrics not supported")
false
}
BiometricManager.BIOMETRIC_STATUS_UNKNOWN -> {
onError("Biometrics status unknown")
false
}
BiometricManager.BIOMETRIC_SUCCESS -> true
else -> false
}
}
if(isBiometricAvailable) {
val executor = remember { ContextCompat.getMainExecutor(context) }
val promptInfo = BiometricPrompt.PromptInfo.Builder()
.setAllowedAuthenticators(BIOMETRIC_WEAK or DEVICE_CREDENTIAL)
.setTitle("Biometric Authentication")
.setSubtitle("Log in using your biometric credential")
.build()
val biometricPrompt = BiometricPrompt(
context as FragmentActivity,
executor,
object : BiometricPrompt.AuthenticationCallback() {
override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
super.onAuthenticationError(errorCode, errString)
onFailure()
}
override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
super.onAuthenticationSucceeded(result)
onSuccess()
}
override fun onAuthenticationFailed() {
super.onAuthenticationFailed()
onFailure()
}
}
)
biometricPrompt.authenticate(promptInfo)
}
}
@@ -1,4 +1,4 @@
package com.zaneschepke.wireguardautotunnel.ui.common package com.zaneschepke.wireguardautotunnel.ui.common.prompt
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
@@ -1,7 +1,6 @@
package com.zaneschepke.wireguardautotunnel.ui.models package com.zaneschepke.wireguardautotunnel.ui.models
import com.wireguard.config.Interface import com.wireguard.config.Interface
import com.wireguard.config.Peer
data class InterfaceProxy( data class InterfaceProxy(
var privateKey : String = "", var privateKey : String = "",
@@ -14,12 +13,12 @@ data class InterfaceProxy(
companion object { companion object {
fun from(i : Interface) : InterfaceProxy { fun from(i : Interface) : InterfaceProxy {
return InterfaceProxy( return InterfaceProxy(
publicKey = i.keyPair.publicKey.toBase64(), publicKey = i.keyPair.publicKey.toBase64().trim(),
privateKey = i.keyPair.privateKey.toBase64(), privateKey = i.keyPair.privateKey.toBase64().trim(),
addresses = i.addresses.joinToString(","), addresses = i.addresses.joinToString(", ").trim(),
dnsServers = i.dnsServers.joinToString(",").replace("/", ""), dnsServers = i.dnsServers.joinToString(", ").replace("/", "").trim(),
listenPort = if(i.listenPort.isPresent) i.listenPort.get().toString() else "", listenPort = if(i.listenPort.isPresent) i.listenPort.get().toString().trim() else "",
mtu = if(i.mtu.isPresent) i.mtu.get().toString() else "" mtu = if(i.mtu.isPresent) i.mtu.get().toString().trim() else ""
) )
} }
} }
@@ -7,16 +7,16 @@ data class PeerProxy(
var preSharedKey : String = "", var preSharedKey : String = "",
var persistentKeepalive : String = "", var persistentKeepalive : String = "",
var endpoint : String = "", var endpoint : String = "",
var allowedIps: String = IPV4_WILDCARD.joinToString(",") var allowedIps: String = IPV4_WILDCARD.joinToString(", ").trim()
){ ){
companion object { companion object {
fun from(peer : Peer) : PeerProxy { fun from(peer : Peer) : PeerProxy {
return PeerProxy( return PeerProxy(
publicKey = peer.publicKey.toBase64(), publicKey = peer.publicKey.toBase64(),
preSharedKey = if(peer.preSharedKey.isPresent) peer.preSharedKey.get().toString() else "", preSharedKey = if(peer.preSharedKey.isPresent) peer.preSharedKey.get().toBase64().trim() else "",
persistentKeepalive = if(peer.persistentKeepalive.isPresent) peer.persistentKeepalive.get().toString() else "", persistentKeepalive = if(peer.persistentKeepalive.isPresent) peer.persistentKeepalive.get().toString().trim() else "",
endpoint = if(peer.endpoint.isPresent) peer.endpoint.get().toString() else "", endpoint = if(peer.endpoint.isPresent) peer.endpoint.get().toString().trim() else "",
allowedIps = peer.allowedIps.joinToString(",") allowedIps = peer.allowedIps.joinToString(", ").trim()
) )
} }
val IPV4_PUBLIC_NETWORKS = setOf( val IPV4_PUBLIC_NETWORKS = setOf(
@@ -3,6 +3,7 @@ package com.zaneschepke.wireguardautotunnel.ui.screens.config
import android.annotation.SuppressLint import android.annotation.SuppressLint
import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.Image import androidx.compose.foundation.Image
import androidx.compose.foundation.clickable
import androidx.compose.foundation.focusGroup import androidx.compose.foundation.focusGroup
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
@@ -60,13 +61,13 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.ClipboardManager import androidx.compose.ui.platform.ClipboardManager
import androidx.compose.ui.platform.LocalClipboardManager import androidx.compose.ui.platform.LocalClipboardManager
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardCapitalization import androidx.compose.ui.text.input.KeyboardCapitalization
import androidx.compose.ui.text.input.PasswordVisualTransformation import androidx.compose.ui.text.input.PasswordVisualTransformation
import androidx.compose.ui.text.input.VisualTransformation
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
@@ -78,7 +79,9 @@ import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
import com.zaneschepke.wireguardautotunnel.ui.Routes import com.zaneschepke.wireguardautotunnel.ui.Routes
import com.zaneschepke.wireguardautotunnel.ui.common.SearchBar import com.zaneschepke.wireguardautotunnel.ui.common.SearchBar
import com.zaneschepke.wireguardautotunnel.ui.common.config.ConfigurationTextBox import com.zaneschepke.wireguardautotunnel.ui.common.config.ConfigurationTextBox
import com.zaneschepke.wireguardautotunnel.ui.common.prompt.AuthorizationPrompt
import com.zaneschepke.wireguardautotunnel.ui.common.text.SectionTitle import com.zaneschepke.wireguardautotunnel.ui.common.text.SectionTitle
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import timber.log.Timber import timber.log.Timber
@@ -96,8 +99,7 @@ fun ConfigScreen(
) { ) {
val context = LocalContext.current val context = LocalContext.current
val focusManager = LocalFocusManager.current val scope = rememberCoroutineScope { Dispatchers.IO }
val scope = rememberCoroutineScope()
val clipboardManager: ClipboardManager = LocalClipboardManager.current val clipboardManager: ClipboardManager = LocalClipboardManager.current
val keyboardController = LocalSoftwareKeyboardController.current val keyboardController = LocalSoftwareKeyboardController.current
@@ -111,10 +113,16 @@ fun ConfigScreen(
val proxyPeers by viewModel.proxyPeers.collectAsStateWithLifecycle() val proxyPeers by viewModel.proxyPeers.collectAsStateWithLifecycle()
val proxyInterface by viewModel.interfaceProxy.collectAsStateWithLifecycle() val proxyInterface by viewModel.interfaceProxy.collectAsStateWithLifecycle()
var showApplicationsDialog by remember { mutableStateOf(false) } var showApplicationsDialog by remember { mutableStateOf(false) }
var showAuthPrompt by remember { mutableStateOf(false) }
var isAuthenticated by remember { mutableStateOf(false) }
val baseTextBoxModifier = Modifier.onFocusChanged {
if(WireGuardAutoTunnel.isRunningOnAndroidTv(context)) {
keyboardController?.hide()
}
}
val keyboardActions = KeyboardActions( val keyboardActions = KeyboardActions(
onDone = { onDone = {
focusManager.clearFocus()
keyboardController?.hide() keyboardController?.hide()
} }
) )
@@ -129,6 +137,7 @@ fun ConfigScreen(
val screenPadding = 5.dp val screenPadding = 5.dp
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
scope.launch(Dispatchers.IO) {
try { try {
viewModel.onScreenLoad(id) viewModel.onScreenLoad(id)
} catch (e : Exception) { } catch (e : Exception) {
@@ -136,6 +145,7 @@ fun ConfigScreen(
navController.navigate(Routes.Main.name) navController.navigate(Routes.Main.name)
} }
} }
}
val applicationButtonText = { val applicationButtonText = {
"Tunneling apps: " + "Tunneling apps: " +
@@ -143,6 +153,21 @@ fun ConfigScreen(
else "${checkedPackages.size} " + (if (include) "included" else "excluded") else "${checkedPackages.size} " + (if (include) "included" else "excluded")
} }
if(showAuthPrompt) {
AuthorizationPrompt(onSuccess = {
showAuthPrompt = false
isAuthenticated = true },
onError = { error ->
showSnackbarMessage(error)
showAuthPrompt = false
},
onFailure = {
showAuthPrompt = false
showSnackbarMessage(context.getString(R.string.authentication_failed))
})
}
if (showApplicationsDialog) { if (showApplicationsDialog) {
val sortedPackages = remember(packages) { val sortedPackages = remember(packages) {
packages.sortedBy { viewModel.getPackageLabel(it) } packages.sortedBy { viewModel.getPackageLabel(it) }
@@ -223,7 +248,7 @@ fun ConfigScreen(
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween horizontalArrangement = Arrangement.SpaceBetween
) { ) {
SearchBar(viewModel::emitQueriedPackages); SearchBar(viewModel::emitQueriedPackages)
} }
Spacer(Modifier.padding(5.dp)) Spacer(Modifier.padding(5.dp))
LazyColumn( LazyColumn(
@@ -380,19 +405,18 @@ fun ConfigScreen(
onValueChange = { value -> onValueChange = { value ->
viewModel.onTunnelNameChange(value) viewModel.onTunnelNameChange(value)
}, },
onDone = { keyboardActions = keyboardActions,
focusManager.clearFocus()
keyboardController?.hide()
},
label = stringResource(R.string.name), label = stringResource(R.string.name),
hint = stringResource(R.string.tunnel_name).lowercase(), hint = stringResource(R.string.tunnel_name).lowercase(),
modifier = Modifier.fillMaxWidth().focusRequester(focusRequester) modifier = baseTextBoxModifier.fillMaxWidth().focusRequester(focusRequester)
) )
OutlinedTextField( OutlinedTextField(
modifier = Modifier.fillMaxWidth(), modifier = baseTextBoxModifier.fillMaxWidth().clickable {
showAuthPrompt = true
},
value = proxyInterface.privateKey, value = proxyInterface.privateKey,
visualTransformation = PasswordVisualTransformation(), visualTransformation = if((id == Constants.MANUAL_TUNNEL_CONFIG_ID) || isAuthenticated) VisualTransformation.None else PasswordVisualTransformation(),
enabled = id == Constants.MANUAL_TUNNEL_CONFIG_ID, enabled = (id == Constants.MANUAL_TUNNEL_CONFIG_ID) || isAuthenticated,
onValueChange = { value -> onValueChange = { value ->
viewModel.onPrivateKeyChange(value) viewModel.onPrivateKeyChange(value)
}, },
@@ -416,7 +440,7 @@ fun ConfigScreen(
keyboardActions = keyboardActions keyboardActions = keyboardActions
) )
OutlinedTextField( OutlinedTextField(
modifier = Modifier.fillMaxWidth().focusRequester(FocusRequester.Default), modifier = baseTextBoxModifier.fillMaxWidth().focusRequester(FocusRequester.Default),
value = proxyInterface.publicKey, value = proxyInterface.publicKey,
enabled = false, enabled = false,
onValueChange = {}, onValueChange = {},
@@ -445,52 +469,40 @@ fun ConfigScreen(
onValueChange = { value -> onValueChange = { value ->
viewModel.onAddressesChanged(value) viewModel.onAddressesChanged(value)
}, },
onDone = { keyboardActions = keyboardActions,
focusManager.clearFocus()
keyboardController?.hide()
},
label = stringResource(R.string.addresses), label = stringResource(R.string.addresses),
hint = stringResource(R.string.comma_separated_list), hint = stringResource(R.string.comma_separated_list),
modifier = Modifier modifier = baseTextBoxModifier
.fillMaxWidth(3 / 5f) .fillMaxWidth(3 / 5f)
.padding(end = 5.dp) .padding(end = 5.dp)
) )
ConfigurationTextBox( ConfigurationTextBox(
value = proxyInterface.listenPort, value = proxyInterface.listenPort,
onValueChange = { value -> viewModel.onListenPortChanged(value) }, onValueChange = { value -> viewModel.onListenPortChanged(value) },
onDone = { keyboardActions = keyboardActions,
focusManager.clearFocus()
keyboardController?.hide()
},
label = stringResource(R.string.listen_port), label = stringResource(R.string.listen_port),
hint = stringResource(R.string.random), hint = stringResource(R.string.random),
modifier = Modifier.width(IntrinsicSize.Min) modifier = baseTextBoxModifier.width(IntrinsicSize.Min)
) )
} }
Row(modifier = Modifier.fillMaxWidth()) { Row(modifier = Modifier.fillMaxWidth()) {
ConfigurationTextBox( ConfigurationTextBox(
value = proxyInterface.dnsServers, value = proxyInterface.dnsServers,
onValueChange = { value -> viewModel.onDnsServersChanged(value) }, onValueChange = { value -> viewModel.onDnsServersChanged(value) },
onDone = { keyboardActions = keyboardActions,
focusManager.clearFocus()
keyboardController?.hide()
},
label = stringResource(R.string.dns_servers), label = stringResource(R.string.dns_servers),
hint = stringResource(R.string.comma_separated_list), hint = stringResource(R.string.comma_separated_list),
modifier = Modifier modifier = baseTextBoxModifier
.fillMaxWidth(3 / 5f) .fillMaxWidth(3 / 5f)
.padding(end = 5.dp) .padding(end = 5.dp)
) )
ConfigurationTextBox( ConfigurationTextBox(
value = proxyInterface.mtu, value = proxyInterface.mtu,
onValueChange = { value -> viewModel.onMtuChanged(value) }, onValueChange = { value -> viewModel.onMtuChanged(value) },
onDone = { keyboardActions = keyboardActions,
focusManager.clearFocus()
keyboardController?.hide()
},
label = stringResource(R.string.mtu), label = stringResource(R.string.mtu),
hint = stringResource(R.string.auto), hint = stringResource(R.string.auto),
modifier = Modifier.width(IntrinsicSize.Min) modifier = baseTextBoxModifier.width(IntrinsicSize.Min)
) )
} }
Row( Row(
@@ -556,13 +568,10 @@ fun ConfigScreen(
value value
) )
}, },
onDone = { keyboardActions = keyboardActions,
focusManager.clearFocus()
keyboardController?.hide()
},
label = stringResource(R.string.public_key), label = stringResource(R.string.public_key),
hint = stringResource(R.string.base64_key), hint = stringResource(R.string.base64_key),
modifier = Modifier.fillMaxWidth() modifier = baseTextBoxModifier.fillMaxWidth()
) )
ConfigurationTextBox( ConfigurationTextBox(
value = peer.preSharedKey, value = peer.preSharedKey,
@@ -572,16 +581,13 @@ fun ConfigScreen(
value value
) )
}, },
onDone = { keyboardActions = keyboardActions,
focusManager.clearFocus()
keyboardController?.hide()
},
label = stringResource(R.string.preshared_key), label = stringResource(R.string.preshared_key),
hint = stringResource(R.string.optional), hint = stringResource(R.string.optional),
modifier = Modifier.fillMaxWidth() modifier = baseTextBoxModifier.fillMaxWidth()
) )
OutlinedTextField( OutlinedTextField(
modifier = Modifier.fillMaxWidth(), modifier = baseTextBoxModifier.fillMaxWidth(),
value = peer.persistentKeepalive, value = peer.persistentKeepalive,
enabled = true, enabled = true,
onValueChange = { value -> onValueChange = { value ->
@@ -602,16 +608,13 @@ fun ConfigScreen(
value value
) )
}, },
onDone = { keyboardActions = keyboardActions,
focusManager.clearFocus()
keyboardController?.hide()
},
label = stringResource(R.string.endpoint), label = stringResource(R.string.endpoint),
hint = stringResource(R.string.endpoint).lowercase(), hint = stringResource(R.string.endpoint).lowercase(),
modifier = Modifier.fillMaxWidth() modifier = baseTextBoxModifier.fillMaxWidth()
) )
OutlinedTextField( OutlinedTextField(
modifier = Modifier.fillMaxWidth(), modifier = baseTextBoxModifier.fillMaxWidth(),
value = peer.allowedIps, value = peer.allowedIps,
enabled = true, enabled = true,
onValueChange = { value -> onValueChange = { value ->
@@ -27,7 +27,6 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import javax.inject.Inject import javax.inject.Inject
@HiltViewModel @HiltViewModel
@@ -63,14 +62,10 @@ class ConfigViewModel @Inject constructor(private val application : Application,
private lateinit var tunnelConfig: TunnelConfig private lateinit var tunnelConfig: TunnelConfig
fun onScreenLoad(id : String) { suspend fun onScreenLoad(id : String) {
if(id != Constants.MANUAL_TUNNEL_CONFIG_ID) { if(id != Constants.MANUAL_TUNNEL_CONFIG_ID) {
viewModelScope.launch(Dispatchers.IO) { tunnelConfig = getTunnelConfigById(id) ?: throw WgTunnelException("Config not found")
tunnelConfig = withContext(this.coroutineContext) {
getTunnelConfigById(id) ?: throw WgTunnelException("Config not found")
}
emitScreenData() emitScreenData()
}
} else { } else {
emitEmptyScreenData() emitEmptyScreenData()
} }
@@ -271,22 +266,22 @@ class ConfigViewModel @Inject constructor(private val application : Application,
fun buildPeerListFromProxyPeers() : List<Peer> { fun buildPeerListFromProxyPeers() : List<Peer> {
return _proxyPeers.value.map { return _proxyPeers.value.map {
val builder = Peer.Builder() val builder = Peer.Builder()
if (it.allowedIps.isNotEmpty()) builder.parseAllowedIPs(it.allowedIps) if (it.allowedIps.isNotEmpty()) builder.parseAllowedIPs(it.allowedIps.trim())
if (it.publicKey.isNotEmpty()) builder.parsePublicKey(it.publicKey) if (it.publicKey.isNotEmpty()) builder.parsePublicKey(it.publicKey.trim())
if (it.preSharedKey.isNotEmpty()) builder.parsePreSharedKey(it.preSharedKey) if (it.preSharedKey.isNotEmpty()) builder.parsePreSharedKey(it.preSharedKey.trim())
if (it.endpoint.isNotEmpty()) builder.parseEndpoint(it.endpoint) if (it.endpoint.isNotEmpty()) builder.parseEndpoint(it.endpoint.trim())
if (it.persistentKeepalive.isNotEmpty()) builder.parsePersistentKeepalive(it.persistentKeepalive) if (it.persistentKeepalive.isNotEmpty()) builder.parsePersistentKeepalive(it.persistentKeepalive.trim())
builder.build() builder.build()
} }
} }
fun buildInterfaceListFromProxyInterface() : Interface { fun buildInterfaceListFromProxyInterface() : Interface {
val builder = Interface.Builder() val builder = Interface.Builder()
builder.parsePrivateKey(_interface.value.privateKey) builder.parsePrivateKey(_interface.value.privateKey.trim())
builder.parseAddresses(_interface.value.addresses) builder.parseAddresses(_interface.value.addresses.trim())
builder.parseDnsServers(_interface.value.dnsServers) builder.parseDnsServers(_interface.value.dnsServers.trim())
if(_interface.value.mtu.isNotEmpty()) builder.parseMtu(_interface.value.mtu) if(_interface.value.mtu.isNotEmpty()) builder.parseMtu(_interface.value.mtu.trim())
if(_interface.value.listenPort.isNotEmpty()) builder.parseListenPort(_interface.value.listenPort) if(_interface.value.listenPort.isNotEmpty()) builder.parseListenPort(_interface.value.listenPort.trim())
if(isAllApplicationsEnabled()) _checkedPackages.value.clear() if(isAllApplicationsEnabled()) _checkedPackages.value.clear()
if(_include.value) builder.includeApplications(_checkedPackages.value) if(_include.value) builder.includeApplications(_checkedPackages.value)
if(!_include.value) builder.excludeApplications(_checkedPackages.value) if(!_include.value) builder.excludeApplications(_checkedPackages.value)
@@ -8,9 +8,7 @@ import androidx.compose.foundation.layout.Box
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.fillMaxHeight import androidx.compose.foundation.layout.fillMaxHeight
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.rememberScrollState import androidx.compose.foundation.rememberScrollState
@@ -110,7 +108,7 @@ fun DetailScreen(
}) })
Box(modifier = Modifier.padding(10.dp)) Box(modifier = Modifier.padding(10.dp))
tunnel?.peers?.forEach{ tunnel?.peers?.forEach{
val peerKey = it.publicKey.toBase64().toString() val peerKey = it.publicKey.toBase64()
val allowedIps = it.allowedIps.joinToString() val allowedIps = it.allowedIps.joinToString()
val endpoint = if(it.endpoint.isPresent) it.endpoint.get().toString() else stringResource( val endpoint = if(it.endpoint.isPresent) it.endpoint.get().toString() else stringResource(
id = R.string.none id = R.string.none
@@ -1,6 +1,10 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.main package com.zaneschepke.wireguardautotunnel.ui.screens.main
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.os.Build
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.animation.AnimatedVisibility import androidx.compose.animation.AnimatedVisibility
@@ -85,6 +89,8 @@ import com.zaneschepke.wireguardautotunnel.ui.Routes
import com.zaneschepke.wireguardautotunnel.ui.common.RowListItem import com.zaneschepke.wireguardautotunnel.ui.common.RowListItem
import com.zaneschepke.wireguardautotunnel.ui.theme.brickRed import com.zaneschepke.wireguardautotunnel.ui.theme.brickRed
import com.zaneschepke.wireguardautotunnel.ui.theme.mint import com.zaneschepke.wireguardautotunnel.ui.theme.mint
import com.zaneschepke.wireguardautotunnel.util.WgTunnelException
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter") @SuppressLint("UnusedMaterial3ScaffoldPaddingParameter")
@@ -100,7 +106,7 @@ fun MainScreen(
val haptic = LocalHapticFeedback.current val haptic = LocalHapticFeedback.current
val context = LocalContext.current val context = LocalContext.current
val isVisible = rememberSaveable { mutableStateOf(true) } val isVisible = rememberSaveable { mutableStateOf(true) }
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope { Dispatchers.IO }
val sheetState = rememberModalBottomSheetState() val sheetState = rememberModalBottomSheetState()
var showBottomSheet by remember { mutableStateOf(false) } var showBottomSheet by remember { mutableStateOf(false) }
@@ -129,13 +135,33 @@ fun MainScreen(
} }
} }
val pickFileLauncher = rememberLauncherForActivityResult( val tunnelFileImportResultLauncher = rememberLauncherForActivityResult(object : ActivityResultContracts.GetContent() {
ActivityResultContracts.GetContent() override fun createIntent(context: Context, input: String): Intent {
) { result -> if (result != null) val intent = super.createIntent(context, input)
/* AndroidTV now comes with stubs that do nothing but display a Toast less helpful than
* what we can do, so detect this and throw an exception that we can catch later. */
val activitiesToResolveIntent = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
context.packageManager.queryIntentActivities(intent, PackageManager.ResolveInfoFlags.of(PackageManager.MATCH_DEFAULT_ONLY.toLong()))
} else {
context.packageManager.queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY)
}
if (activitiesToResolveIntent.all {
val name = it.activityInfo.packageName
name.startsWith(Constants.GOOGLE_TV_EXPLORER_STUB) || name.startsWith(Constants.ANDROID_TV_EXPLORER_STUB)
}) {
throw WgTunnelException(context.getString(R.string.no_file_explorer))
}
return intent
}
}) { data ->
if (data == null) return@rememberLauncherForActivityResult
scope.launch(Dispatchers.IO) {
try { try {
viewModel.onTunnelFileSelected(result) viewModel.onTunnelFileSelected(data)
} catch (e : Exception) { } catch (e : Exception) {
showSnackbarMessage(e.message ?: "Unknown error occurred") showSnackbarMessage(e.message ?: context.getString(R.string.unknown_error))
}
} }
} }
@@ -163,16 +189,16 @@ fun MainScreen(
selectedTunnel = null selectedTunnel = null
} }
}) })
{ Text(text = "Okay") } { Text(text = stringResource(R.string.okay)) }
}, },
dismissButton = { dismissButton = {
TextButton(onClick = { TextButton(onClick = {
showPrimaryChangeAlertDialog = false showPrimaryChangeAlertDialog = false
}) })
{ Text(text = "Cancel") } { Text(text = stringResource(R.string.cancel)) }
}, },
title = { Text(text = "Primary tunnel change") }, title = { Text(text = stringResource(R.string.primary_tunnel_change)) },
text = { Text(text = "Would you like to make this your primary tunnel?") } text = { Text(text = stringResource(R.string.primary_tunnel_change_question)) }
) )
} }
@@ -246,9 +272,9 @@ fun MainScreen(
.clickable { .clickable {
showBottomSheet = false showBottomSheet = false
try { try {
pickFileLauncher.launch(Constants.ALLOWED_FILE_TYPES) tunnelFileImportResultLauncher.launch(Constants.ALLOWED_FILE_TYPES)
} catch (_: Exception) { } catch (e : Exception) {
showSnackbarMessage("No file explorer") showSnackbarMessage(e.message!!)
} }
} }
.padding(10.dp) .padding(10.dp)
@@ -259,10 +285,11 @@ fun MainScreen(
modifier = Modifier.padding(10.dp) modifier = Modifier.padding(10.dp)
) )
Text( Text(
stringResource(id = R.string.add_from_file), stringResource(id = R.string.add_tunnels_text),
modifier = Modifier.padding(10.dp) modifier = Modifier.padding(10.dp)
) )
} }
if(!WireGuardAutoTunnel.isRunningOnAndroidTv(context)) {
Divider() Divider()
Row(modifier = Modifier Row(modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
@@ -290,6 +317,7 @@ fun MainScreen(
modifier = Modifier.padding(10.dp) modifier = Modifier.padding(10.dp)
) )
} }
}
Divider() Divider()
Row( Row(
modifier = Modifier modifier = Modifier
@@ -335,12 +363,12 @@ fun MainScreen(
RowListItem(icon = { RowListItem(icon = {
if (settings.isTunnelConfigDefault(tunnel)) if (settings.isTunnelConfigDefault(tunnel))
Icon( Icon(
Icons.Rounded.Star, "status", Icons.Rounded.Star, stringResource(R.string.status),
tint = leadingIconColor, tint = leadingIconColor,
modifier = Modifier.padding(end = 10.dp).size(20.dp) modifier = Modifier.padding(end = 10.dp).size(20.dp)
) )
else Icon( else Icon(
Icons.Rounded.Circle, "status", Icons.Rounded.Circle, stringResource(R.string.status),
tint = leadingIconColor, tint = leadingIconColor,
modifier = Modifier.padding(end = 15.dp).size(15.dp) modifier = Modifier.padding(end = 15.dp).size(15.dp)
) )
@@ -405,7 +433,7 @@ fun MainScreen(
onClick = { onClick = {
navController.navigate("${Routes.Detail.name}/${tunnel.id}") navController.navigate("${Routes.Detail.name}/${tunnel.id}")
}) { }) {
Icon(Icons.Rounded.Info, "Info") Icon(Icons.Rounded.Info, stringResource(R.string.info))
} }
IconButton(onClick = { IconButton(onClick = {
if (state == Tunnel.State.UP && tunnel.name == tunnelName) if (state == Tunnel.State.UP && tunnel.name == tunnelName)
@@ -440,6 +468,7 @@ fun MainScreen(
) )
} }
Switch( Switch(
modifier = Modifier.focusRequester(focusRequester),
checked = (state == Tunnel.State.UP && tunnel.name == tunnelName), checked = (state == Tunnel.State.UP && tunnel.name == tunnelName),
onCheckedChange = { checked -> onCheckedChange = { checked ->
onTunnelToggle(checked, tunnel) onTunnelToggle(checked, tunnel)
@@ -7,8 +7,6 @@ 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 androidx.lifecycle.viewmodel.compose.viewModel
import com.wireguard.config.BadConfigException
import com.wireguard.config.Config import com.wireguard.config.Config
import com.zaneschepke.wireguardautotunnel.Constants import com.zaneschepke.wireguardautotunnel.Constants
import com.zaneschepke.wireguardautotunnel.R import com.zaneschepke.wireguardautotunnel.R
@@ -31,11 +29,13 @@ import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.io.InputStream import java.io.InputStream
import java.util.zip.ZipInputStream
import javax.inject.Inject import javax.inject.Inject
@HiltViewModel @HiltViewModel
class MainViewModel @Inject constructor(private val application : Application, class MainViewModel @Inject constructor(
private val application: Application,
private val tunnelRepo: TunnelConfigDao, private val tunnelRepo: TunnelConfigDao,
private val settingsRepo: SettingsDoa, private val settingsRepo: SettingsDoa,
private val vpnService: VpnService private val vpnService: VpnService
@@ -60,9 +60,15 @@ class MainViewModel @Inject constructor(private val application : Application,
} }
private fun validateWatcherServiceState(settings: Settings) { private fun validateWatcherServiceState(settings: Settings) {
val watcherState = ServiceManager.getServiceState(application.applicationContext, WireGuardConnectivityWatcherService::class.java) val watcherState = ServiceManager.getServiceState(
application.applicationContext,
WireGuardConnectivityWatcherService::class.java
)
if (settings.isAutoTunnelEnabled && watcherState == ServiceState.STOPPED && settings.defaultTunnel != null) { if (settings.isAutoTunnelEnabled && watcherState == ServiceState.STOPPED && settings.defaultTunnel != null) {
ServiceManager.startWatcherService(application.applicationContext, settings.defaultTunnel!!) ServiceManager.startWatcherService(
application.applicationContext,
settings.defaultTunnel!!
)
} }
} }
@@ -96,8 +102,11 @@ class MainViewModel @Inject constructor(private val application : Application,
} }
private suspend fun stopActiveTunnel() { private suspend fun stopActiveTunnel() {
if(ServiceManager.getServiceState(application.applicationContext, if (ServiceManager.getServiceState(
WireGuardTunnelService::class.java, ) == ServiceState.STARTED) { application.applicationContext,
WireGuardTunnelService::class.java,
) == ServiceState.STARTED
) {
onTunnelStop() onTunnelStop()
delay(Constants.TOGGLE_TUNNEL_DELAY) delay(Constants.TOGGLE_TUNNEL_DELAY)
} }
@@ -117,21 +126,17 @@ class MainViewModel @Inject constructor(private val application : Application,
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
try { try {
validateConfigString(result) validateConfigString(result)
val tunnelConfig = TunnelConfig(name = NumberUtils.generateRandomTunnelName(), wgQuick = result) val tunnelConfig =
TunnelConfig(name = NumberUtils.generateRandomTunnelName(), wgQuick = result)
addTunnel(tunnelConfig) addTunnel(tunnelConfig)
} catch (e: WgTunnelException) { } catch (e: WgTunnelException) {
throw WgTunnelException(e.message ?: application.getString(R.string.unknown_error_message)) throw WgTunnelException(
e.message ?: application.getString(R.string.unknown_error_message)
)
} }
} }
} }
private fun validateFileExtension(fileName : String) {
val extension = getFileExtensionFromFileName(fileName)
if(extension != Constants.VALID_FILE_EXTENSION) {
throw WgTunnelException(application.getString(R.string.file_extension_message))
}
}
private fun saveTunnelConfigFromStream(stream: InputStream, fileName: String) { private fun saveTunnelConfigFromStream(stream: InputStream, fileName: String) {
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
val bufferReader = stream.bufferedReader(charset = Charsets.UTF_8) val bufferReader = stream.bufferedReader(charset = Charsets.UTF_8)
@@ -147,19 +152,41 @@ class MainViewModel @Inject constructor(private val application : Application,
?: throw WgTunnelException(application.getString(R.string.stream_failed)) ?: throw WgTunnelException(application.getString(R.string.stream_failed))
} }
fun onTunnelFileSelected(uri : Uri) { suspend fun onTunnelFileSelected(uri: Uri) {
try { try {
viewModelScope.launch(Dispatchers.IO) {
val fileName = getFileName(application.applicationContext, uri) val fileName = getFileName(application.applicationContext, uri)
validateFileExtension(fileName) val fileExtension = getFileExtensionFromFileName(fileName)
val stream = getInputStreamFromUri(uri) when(fileExtension){
saveTunnelConfigFromStream(stream, fileName) Constants.CONF_FILE_EXTENSION -> saveTunnelFromConfUri(fileName, uri)
Constants.ZIP_FILE_EXTENSION -> saveTunnelsFromZipUri(uri)
else -> throw WgTunnelException(application.getString(R.string.file_extension_message))
} }
} catch (e: Exception) { } catch (e: Exception) {
throw WgTunnelException(e.message ?: "Error importing file") throw WgTunnelException(e.message ?: "Error importing file")
} }
} }
private suspend fun saveTunnelsFromZipUri(uri: Uri) {
ZipInputStream(getInputStreamFromUri(uri)).use { zip ->
generateSequence { zip.nextEntry }
.filterNot { it.isDirectory ||
getFileExtensionFromFileName(it.name) != Constants.CONF_FILE_EXTENSION }
.forEach {
val name = getNameFromFileName(it.name)
val config = Config.parse(zip)
viewModelScope.launch(Dispatchers.IO) {
addTunnel(TunnelConfig(name = name, wgQuick = config.toWgQuickString()))
}
}
}
}
private fun saveTunnelFromConfUri(name : String, uri: Uri) {
val stream = getInputStreamFromUri(uri)
saveTunnelConfigFromStream(stream, name)
}
private suspend fun addTunnel(tunnelConfig: TunnelConfig) { private suspend fun addTunnel(tunnelConfig: TunnelConfig) {
saveTunnel(tunnelConfig) saveTunnel(tunnelConfig)
} }
@@ -226,9 +253,11 @@ class MainViewModel @Inject constructor(private val application : Application,
suspend fun onDefaultTunnelChange(selectedTunnel: TunnelConfig?) { suspend fun onDefaultTunnelChange(selectedTunnel: TunnelConfig?) {
if (selectedTunnel != null) { if (selectedTunnel != null) {
_settings.emit(_settings.value.copy( _settings.emit(
_settings.value.copy(
defaultTunnel = selectedTunnel.toString() defaultTunnel = selectedTunnel.toString()
)) )
)
settingsRepo.save(_settings.value) settingsRepo.save(_settings.value)
} }
} }
@@ -15,7 +15,6 @@ import androidx.compose.foundation.layout.IntrinsicSize
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.Spacer
import androidx.compose.foundation.layout.fillMaxHeight
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.height import androidx.compose.foundation.layout.height
@@ -44,12 +43,15 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.focus.onFocusChanged
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
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
@@ -66,12 +68,16 @@ import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
import com.zaneschepke.wireguardautotunnel.ui.common.ClickableIconButton import com.zaneschepke.wireguardautotunnel.ui.common.ClickableIconButton
import com.zaneschepke.wireguardautotunnel.ui.common.config.ConfigurationToggle import com.zaneschepke.wireguardautotunnel.ui.common.config.ConfigurationToggle
import com.zaneschepke.wireguardautotunnel.ui.common.prompt.AuthorizationPrompt
import com.zaneschepke.wireguardautotunnel.ui.common.text.SectionTitle import com.zaneschepke.wireguardautotunnel.ui.common.text.SectionTitle
import com.zaneschepke.wireguardautotunnel.util.StorageUtil
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.io.File
@OptIn( @OptIn(
ExperimentalPermissionsApi::class, ExperimentalPermissionsApi::class,
ExperimentalLayoutApi::class ExperimentalLayoutApi::class, ExperimentalComposeUiApi::class
) )
@Composable @Composable
fun SettingsScreen( fun SettingsScreen(
@@ -81,9 +87,10 @@ fun SettingsScreen(
focusRequester: FocusRequester, focusRequester: FocusRequester,
) { ) {
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope { Dispatchers.IO }
val context = LocalContext.current val context = LocalContext.current
val focusManager = LocalFocusManager.current val focusManager = LocalFocusManager.current
val keyboardController = LocalSoftwareKeyboardController.current
val interactionSource = remember { MutableInteractionSource() } val interactionSource = remember { MutableInteractionSource() }
val settings by viewModel.settings.collectAsStateWithLifecycle() val settings by viewModel.settings.collectAsStateWithLifecycle()
@@ -94,11 +101,28 @@ fun SettingsScreen(
val scrollState = rememberScrollState() val scrollState = rememberScrollState()
var didShowLocationDisclaimer by remember { mutableStateOf(false) } var didShowLocationDisclaimer by remember { mutableStateOf(false) }
var isBackgroundLocationGranted by remember { mutableStateOf(true) } var isBackgroundLocationGranted by remember { mutableStateOf(true) }
var showAuthPrompt by remember { mutableStateOf(false) }
var didExportFiles by remember { mutableStateOf(false) }
val screenPadding = 5.dp val screenPadding = 5.dp
val fillMaxHeight = .85f
val fillMaxWidth = .85f val fillMaxWidth = .85f
fun exportAllConfigs() {
try {
val files = tunnels.map { File(context.cacheDir, "${it.name}.conf") }
files.forEachIndexed { index, file ->
file.outputStream().use {
it.write(tunnels[index].wgQuick.toByteArray())
}
}
StorageUtil.saveFilesToZip(context, files)
didExportFiles = true
showSnackbarMessage(context.getString(R.string.exported_configs_message))
} catch (e : Exception) {
showSnackbarMessage(e.message!!)
}
}
fun saveTrustedSSID() { fun saveTrustedSSID() {
if (currentText.isNotEmpty()) { if (currentText.isNotEmpty()) {
@@ -107,7 +131,7 @@ fun SettingsScreen(
viewModel.onSaveTrustedSSID(currentText) viewModel.onSaveTrustedSSID(currentText)
currentText = "" currentText = ""
} catch (e : Exception) { } catch (e : Exception) {
showSnackbarMessage(e.message ?: "Unknown error") showSnackbarMessage(e.message ?: context.getString(R.string.unknown_error))
} }
} }
} }
@@ -188,6 +212,20 @@ fun SettingsScreen(
} }
} }
if(showAuthPrompt) {
AuthorizationPrompt(onSuccess = {
showAuthPrompt = false
exportAllConfigs() },
onError = { error ->
showSnackbarMessage(error)
showAuthPrompt = false
},
onFailure = {
showAuthPrompt = false
showSnackbarMessage(context.getString(R.string.authentication_failed))
})
}
if (tunnels.isEmpty()) { if (tunnels.isEmpty()) {
Column( Column(
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
@@ -264,7 +302,11 @@ fun SettingsScreen(
value = currentText, value = currentText,
onValueChange = { currentText = it }, onValueChange = { currentText = it },
label = { Text(stringResource(R.string.add_trusted_ssid)) }, label = { Text(stringResource(R.string.add_trusted_ssid)) },
modifier = Modifier.padding(start = screenPadding, top = 5.dp).focusRequester(focusRequester), modifier = Modifier.padding(start = screenPadding, top = 5.dp).focusRequester(focusRequester).onFocusChanged {
if(WireGuardAutoTunnel.isRunningOnAndroidTv(context)) {
keyboardController?.hide()
}
},
maxLines = 1, maxLines = 1,
keyboardOptions = KeyboardOptions( keyboardOptions = KeyboardOptions(
capitalization = KeyboardCapitalization.None, capitalization = KeyboardCapitalization.None,
@@ -307,6 +349,17 @@ fun SettingsScreen(
} }
} }
) )
ConfigurationToggle(
stringResource(R.string.battery_saver),
enabled = !(settings.isAutoTunnelEnabled || settings.isAlwaysOnVpnEnabled),
checked = settings.isBatterySaverEnabled,
padding = screenPadding,
onCheckChanged = {
scope.launch {
viewModel.onToggleBatterySaver()
}
}
)
ConfigurationToggle(stringResource(R.string.enable_auto_tunnel), ConfigurationToggle(stringResource(R.string.enable_auto_tunnel),
enabled = !settings.isAlwaysOnVpnEnabled, enabled = !settings.isAlwaysOnVpnEnabled,
checked = settings.isAutoTunnelEnabled, checked = settings.isAutoTunnelEnabled,
@@ -314,11 +367,11 @@ fun SettingsScreen(
onCheckChanged = { onCheckChanged = {
if(!isAllAutoTunnelPermissionsEnabled()) { if(!isAllAutoTunnelPermissionsEnabled()) {
val message = if(viewModel.isLocationServicesNeeded()){ val message = if(viewModel.isLocationServicesNeeded()){
"Location services required" context.getString(R.string.location_services_required)
} else if(!isBackgroundLocationGranted){ } else if(!isBackgroundLocationGranted){
"Background location required" context.getString(R.string.background_location_required)
} else { } else {
"Precise location required" context.getString(R.string.precise_location_required)
} }
showSnackbarMessage(message) showSnackbarMessage(message)
} else scope.launch { } else scope.launch {
@@ -355,6 +408,31 @@ fun SettingsScreen(
} }
} }
) )
ConfigurationToggle(stringResource(R.string.enabled_app_shortcuts),
enabled = true,
checked = settings.isShortcutsEnabled,
padding = screenPadding,
onCheckChanged = {
scope.launch {
viewModel.onToggleShortcutsEnabled()
}
}
)
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.fillMaxSize()
.padding(top = 5.dp),
horizontalArrangement = Arrangement.Center
) {
TextButton(
enabled = !didExportFiles,
onClick = {
showAuthPrompt = true
}) {
Text(stringResource(R.string.export_configs))
}
}
} }
} }
} }
@@ -84,7 +84,7 @@ class SettingsViewModel @Inject constructor(private val application : Applicatio
} }
private suspend fun getFirstTunnelConfig() : TunnelConfig { private suspend fun getFirstTunnelConfig() : TunnelConfig {
return tunnelRepo.getAll().first(); return tunnelRepo.getAll().first()
} }
suspend fun onToggleAlwaysOnVPN() { suspend fun onToggleAlwaysOnVPN() {
@@ -125,4 +125,16 @@ class SettingsViewModel @Inject constructor(private val application : Applicatio
fun isLocationServicesNeeded() : Boolean { fun isLocationServicesNeeded() : Boolean {
return(!isLocationServicesEnabled() && Build.VERSION.SDK_INT > Build.VERSION_CODES.P) return(!isLocationServicesEnabled() && Build.VERSION.SDK_INT > Build.VERSION_CODES.P)
} }
suspend fun onToggleShortcutsEnabled() {
settingsRepo.save(_settings.value.copy(
isShortcutsEnabled = !_settings.value.isShortcutsEnabled
))
}
suspend fun onToggleBatterySaver() {
settingsRepo.save(_settings.value.copy(
isBatterySaverEnabled = !_settings.value.isBatterySaverEnabled
))
}
} }
@@ -0,0 +1,53 @@
package com.zaneschepke.wireguardautotunnel.util
import android.content.ContentValues
import android.content.Context
import android.os.Build
import android.os.Environment
import android.provider.MediaStore
import android.provider.MediaStore.MediaColumns
import com.zaneschepke.wireguardautotunnel.Constants
import java.io.File
import java.io.OutputStream
import java.time.Instant
import java.util.zip.ZipEntry
import java.util.zip.ZipOutputStream
object StorageUtil {
private const val ZIP_FILE_MIME_TYPE = "application/zip"
private fun createDownloadsFileOutputStream(context: Context, fileName: String, mimeType : String = Constants.ALLOWED_FILE_TYPES) : OutputStream? {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
val resolver = context.contentResolver
val contentValues = ContentValues().apply {
put(MediaColumns.DISPLAY_NAME, fileName)
put(MediaColumns.MIME_TYPE, mimeType)
put(MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_DOWNLOADS)
}
val uri = resolver.insert(MediaStore.Downloads.EXTERNAL_CONTENT_URI, contentValues)
if (uri != null) {
return resolver.openOutputStream(uri)
}
} else {
val target = File(
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS),
fileName
)
return target.outputStream()
}
return null
}
fun saveFilesToZip(context: Context, files : List<File>) {
val zipOutputStream = createDownloadsFileOutputStream(context, "wg-export_${Instant.now().epochSecond}.zip", ZIP_FILE_MIME_TYPE)
ZipOutputStream(zipOutputStream).use { zos ->
files.forEach { file ->
val entry = ZipEntry( file.name)
zos.putNextEntry(entry)
if (file.isFile) {
file.inputStream().use { fis -> fis.copyTo(zos) }
}
}
}
}
}
+17 -3
View File
@@ -8,7 +8,7 @@
<string name="foreground_file">FOREGROUND_FILE</string> <string name="foreground_file">FOREGROUND_FILE</string>
<string name="github_url">https://github.com/zaneschepke/wgtunnel</string> <string name="github_url">https://github.com/zaneschepke/wgtunnel</string>
<string name="privacy_policy_url">https://zaneschepke.github.io/wgtunnel/</string> <string name="privacy_policy_url">https://zaneschepke.github.io/wgtunnel/</string>
<string name="file_extension_message">File is not a .conf file</string> <string name="file_extension_message">File is not a .conf or .zip</string>
<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>
@@ -38,9 +38,9 @@
<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="config_validation">[Interface]</string>
<string name="add_from_file">Add tunnel from files</string> <string name="add_tunnels_text">Add from file or zip</string>
<string name="open_file">File Open</string> <string name="open_file">File Open</string>
<string name="add_from_qr">Add tunnel from QR code</string> <string name="add_from_qr">Add from QR code</string>
<string name="qr_scan">QR Scan</string> <string name="qr_scan">QR Scan</string>
<string name="tunnel_edit">Tunnel Edit</string> <string name="tunnel_edit">Tunnel Edit</string>
<string name="tunnel_name">Tunnel Name</string> <string name="tunnel_name">Tunnel Name</string>
@@ -124,4 +124,18 @@
<string name="preshared_key">Pre-shared key</string> <string name="preshared_key">Pre-shared key</string>
<string name="seconds">seconds</string> <string name="seconds">seconds</string>
<string name="persistent_keepalive">Persistent keepalive</string> <string name="persistent_keepalive">Persistent keepalive</string>
<string name="cancel">Cancel</string>
<string name="primary_tunnel_change">Primary tunnel change</string>
<string name="primary_tunnel_change_question">Would you like to make this your primary tunnel?</string>
<string name="authentication_failed">Authentication failed</string>
<string name="enabled_app_shortcuts">Enable app shortcuts</string>
<string name="export_configs">Export configs</string>
<string name="battery_saver">Battery saver (experimental)</string>
<string name="location_services_required">Location services required</string>
<string name="background_location_required">Background location required</string>
<string name="precise_location_required">Precise location required</string>
<string name="unknown_error">Unknown error occurred</string>
<string name="exported_configs_message">Exported configs to downloads</string>
<string name="no_file_explorer">No file explorer installed</string>
<string name="status">status</string>
</resources> </resources>
@@ -1,9 +1,8 @@
package com.zaneschepke.wireguardautotunnel package com.zaneschepke.wireguardautotunnel
import org.junit.Assert.assertEquals
import org.junit.Test import org.junit.Test
import org.junit.Assert.*
/** /**
* Example local unit test, which will execute on the development machine (host). * Example local unit test, which will execute on the development machine (host).
* *
+12 -5
View File
@@ -1,8 +1,10 @@
[versions] [versions]
accompanist = "0.31.2-alpha" accompanist = "0.31.2-alpha"
activityCompose = "1.7.2" activityCompose = "1.8.0"
androidx-junit = "1.1.5" androidx-junit = "1.1.5"
appcompat = "1.6.1" appcompat = "1.6.1"
biometricKtx = "1.2.0-alpha05"
coreGoogleShortcuts = "1.1.0"
coreKtx = "1.12.0" coreKtx = "1.12.0"
espressoCore = "3.5.1" espressoCore = "3.5.1"
firebase-crashlytics-gradle = "2.9.9" firebase-crashlytics-gradle = "2.9.9"
@@ -12,18 +14,18 @@ hiltNavigationCompose = "1.0.0"
junit = "4.13.2" junit = "4.13.2"
kotlinx-serialization-json = "1.5.1" kotlinx-serialization-json = "1.5.1"
lifecycle-runtime-compose = "2.6.2" lifecycle-runtime-compose = "2.6.2"
material-icons-extended = "1.5.2" material-icons-extended = "1.5.3"
material3 = "1.1.2" material3 = "1.1.2"
navigationCompose = "2.7.3" navigationCompose = "2.7.4"
roomVersion = "2.6.0-rc01" roomVersion = "2.6.0-rc01"
timber = "5.0.1" timber = "5.0.1"
tunnel = "1.0.20230706" tunnel = "1.0.20230706"
androidGradlePlugin = "8.3.0-alpha06" androidGradlePlugin = "8.3.0-alpha06"
kotlin="1.9.10" kotlin="1.9.10"
ksp="1.9.10-1.0.13" ksp="1.9.10-1.0.13"
composeBom="2023.09.02" composeBom="2023.10.00"
firebaseBom="32.3.1" firebaseBom="32.3.1"
compose="1.5.2" compose="1.5.3"
crashlytics="18.4.3" crashlytics="18.4.3"
analytics="21.3.0" analytics="21.3.0"
composeCompiler="1.5.3" composeCompiler="1.5.3"
@@ -40,6 +42,11 @@ accompanist-permissions = { module = "com.google.accompanist:accompanist-permiss
accompanist-systemuicontroller = { module = "com.google.accompanist:accompanist-systemuicontroller", version.ref = "accompanist" } accompanist-systemuicontroller = { module = "com.google.accompanist:accompanist-systemuicontroller", version.ref = "accompanist" }
#room #room
androidx-biometric-ktx = { module = "androidx.biometric:biometric-ktx", version.ref = "biometricKtx" }
androidx-core = { module = "androidx.core:core", version.ref = "coreKtx" }
androidx-core-google-shortcuts = { module = "androidx.core:core-google-shortcuts", version.ref = "coreGoogleShortcuts" }
androidx-lifecycle-process = { module = "androidx.lifecycle:lifecycle-process", version.ref = "lifecycle-runtime-compose" }
androidx-lifecycle-service = { module = "androidx.lifecycle:lifecycle-service", version.ref = "lifecycle-runtime-compose" }
androidx-room-compiler = { module = "androidx.room:room-compiler", version.ref = "roomVersion" } androidx-room-compiler = { module = "androidx.room:room-compiler", version.ref = "roomVersion" }
androidx-room-ktx = { module = "androidx.room:room-ktx", version.ref = "roomVersion" } androidx-room-ktx = { module = "androidx.room:room-ktx", version.ref = "roomVersion" }
androidx-room-runtime = { module = "androidx.room:room-runtime", version.ref = "roomVersion" } androidx-room-runtime = { module = "androidx.room:room-runtime", version.ref = "roomVersion" }
+2 -2
View File
@@ -1,6 +1,6 @@
#Mon Apr 24 22:46:45 EDT 2023 #Wed Oct 11 22:39:21 EDT 2023
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.3-rc-2-bin.zip distributionUrl=https\://services.gradle.org/distributions/gradle-8.3-bin.zip
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists