feat: add lock, logs, and ping

Fixes bug where control tile tunnel did not match with tunnel being controlled Closes #132
Fixes tunnel config edit screen error message #131

Revert to official lib to fix slow speeds issue Closes #137

Adds local app lock feature Closes #88

Adds restart vpn on ping fail with 1 minute interval and 60 minute cooldown Closes #6

Adds ability to easily make a copy of a tunnel.

Fixes bug on AndroidTV where tunnels were not being deleted properly.

Fixes bug where auto tunneling could be turned on before VPN permission was given.
This commit is contained in:
Zane Schepke 2024-03-18 00:27:11 -04:00
parent 4fc8ffbcbb
commit 5946d7c10d
43 changed files with 928 additions and 576 deletions

View File

@ -89,7 +89,6 @@ jobs:
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with: with:
# fix hardcode changelog file name
body_path: ${{ github.workspace }}/fastlane/metadata/android/en-US/changelogs/${{ env.VERSION_CODE }}.txt body_path: ${{ github.workspace }}/fastlane/metadata/android/en-US/changelogs/${{ env.VERSION_CODE }}.txt
tag_name: ${{ github.ref_name }} tag_name: ${{ github.ref_name }}
name: ${{ github.ref_name }} name: ${{ github.ref_name }}

View File

@ -137,7 +137,7 @@ 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 // helpers for implementing LifecycleOwner in a Service
implementation(libs.androidx.lifecycle.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))
@ -201,6 +201,7 @@ dependencies {
// bio // bio
implementation(libs.androidx.biometric.ktx) implementation(libs.androidx.biometric.ktx)
implementation(libs.pin.lock.compose)
// shortcuts // shortcuts
implementation(libs.androidx.core) implementation(libs.androidx.core)

View File

@ -0,0 +1,168 @@
{
"formatVersion": 1,
"database": {
"version": 6,
"identityHash": "625820076477aca948536f7bccccc7ca",
"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, `is_tunnel_on_wifi_enabled` INTEGER NOT NULL DEFAULT false, `is_kernel_enabled` INTEGER NOT NULL DEFAULT false, `is_restore_on_boot_enabled` INTEGER NOT NULL DEFAULT false, `is_multi_tunnel_enabled` INTEGER NOT NULL DEFAULT false, `is_auto_tunnel_paused` INTEGER NOT NULL DEFAULT false, `is_ping_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"
},
{
"fieldPath": "isTunnelOnWifiEnabled",
"columnName": "is_tunnel_on_wifi_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isKernelEnabled",
"columnName": "is_kernel_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isRestoreOnBootEnabled",
"columnName": "is_restore_on_boot_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isMultiTunnelEnabled",
"columnName": "is_multi_tunnel_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isAutoTunnelPaused",
"columnName": "is_auto_tunnel_paused",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isPingEnabled",
"columnName": "is_ping_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, '625820076477aca948536f7bccccc7ca')"
]
}
}

View File

@ -2,12 +2,14 @@ package com.zaneschepke.wireguardautotunnel
import android.app.Application import android.app.Application
import android.content.ComponentName import android.content.ComponentName
import android.content.Context
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.service.quicksettings.TileService import android.service.quicksettings.TileService
import com.zaneschepke.wireguardautotunnel.service.tile.TunnelControlTile import com.zaneschepke.wireguardautotunnel.service.tile.TunnelControlTile
import com.zaneschepke.wireguardautotunnel.util.ReleaseTree import com.zaneschepke.wireguardautotunnel.util.ReleaseTree
import dagger.hilt.android.HiltAndroidApp import dagger.hilt.android.HiltAndroidApp
import timber.log.Timber import timber.log.Timber
import xyz.teamgravity.pin_lock_compose.PinManager
@HiltAndroidApp @HiltAndroidApp
class WireGuardAutoTunnel : Application() { class WireGuardAutoTunnel : Application() {
@ -15,8 +17,8 @@ class WireGuardAutoTunnel : Application() {
super.onCreate() super.onCreate()
instance = this instance = this
if (BuildConfig.DEBUG) Timber.plant(Timber.DebugTree()) else Timber.plant(ReleaseTree()) if (BuildConfig.DEBUG) Timber.plant(Timber.DebugTree()) else Timber.plant(ReleaseTree())
PinManager.initialize(this)
} }
companion object { companion object {
lateinit var instance: WireGuardAutoTunnel lateinit var instance: WireGuardAutoTunnel
private set private set
@ -25,9 +27,9 @@ class WireGuardAutoTunnel : Application() {
return instance.packageManager.hasSystemFeature(PackageManager.FEATURE_LEANBACK) return instance.packageManager.hasSystemFeature(PackageManager.FEATURE_LEANBACK)
} }
fun requestTileServiceStateUpdate() { fun requestTileServiceStateUpdate(context : Context) {
TileService.requestListeningState( TileService.requestListeningState(
instance, context,
ComponentName(instance, TunnelControlTile::class.java), ComponentName(instance, TunnelControlTile::class.java),
) )
} }

View File

@ -9,7 +9,7 @@ import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
@Database( @Database(
entities = [Settings::class, TunnelConfig::class], entities = [Settings::class, TunnelConfig::class],
version = 5, version = 6,
autoMigrations = autoMigrations =
[ [
AutoMigration(from = 1, to = 2), AutoMigration(from = 1, to = 2),
@ -22,6 +22,10 @@ import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
from = 4, from = 4,
to = 5, to = 5,
), ),
AutoMigration(
from = 5,
to = 6,
),
], ],
exportSchema = true, exportSchema = true,
) )

View File

@ -8,6 +8,7 @@ import androidx.datastore.preferences.preferencesDataStore
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.runBlocking
class DataStoreManager(private val context: Context) { class DataStoreManager(private val context: Context) {
companion object { companion object {
@ -32,7 +33,11 @@ class DataStoreManager(private val context: Context) {
fun <T> getFromStoreFlow(key: Preferences.Key<T>) = context.dataStore.data.map { it[key] } fun <T> getFromStoreFlow(key: Preferences.Key<T>) = context.dataStore.data.map { it[key] }
suspend fun <T> getFromStore(key: Preferences.Key<T>) = suspend fun <T> getFromStore(key: Preferences.Key<T>) =
context.dataStore.data.first { it.contains(key) }[key] context.dataStore.data.map{ it[key] }.first()
fun <T> getFromStoreBlocking(key: Preferences.Key<T>) = runBlocking {
context.dataStore.data.map{ it[key] }.first()
}
val preferencesFlow: Flow<Preferences?> = context.dataStore.data val preferencesFlow: Flow<Preferences?> = context.dataStore.data
} }

View File

@ -51,6 +51,11 @@ data class Settings(
defaultValue = "false", defaultValue = "false",
) )
var isAutoTunnelPaused: Boolean = false, var isAutoTunnelPaused: Boolean = false,
@ColumnInfo(
name = "is_ping_enabled",
defaultValue = "false",
)
var isPingEnabled: Boolean = false,
) { ) {
fun isTunnelConfigDefault(tunnelConfig: TunnelConfig): Boolean { fun isTunnelConfigDefault(tunnelConfig: TunnelConfig): Boolean {
return if (defaultTunnel != null) { return if (defaultTunnel != null) {

View File

@ -1,12 +1,8 @@
package com.zaneschepke.wireguardautotunnel.service.foreground package com.zaneschepke.wireguardautotunnel.service.foreground
import android.app.AlarmManager
import android.app.PendingIntent
import android.content.Context import android.content.Context
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 androidx.core.app.ServiceCompat import androidx.core.app.ServiceCompat
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import com.wireguard.android.backend.Tunnel import com.wireguard.android.backend.Tunnel
@ -28,10 +24,11 @@ import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import timber.log.Timber import timber.log.Timber
import java.net.InetAddress
import javax.inject.Inject import javax.inject.Inject
@AndroidEntryPoint @AndroidEntryPoint
class WireGuardConnectivityWatcherService : ForegroundService() { class WireGuardConnectivityWatcherService : ForegroundService() {
private val foregroundId = 122 private val foregroundId = 122
@ -122,44 +119,13 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
launchWatcherNotification(getString(R.string.watcher_notification_text_paused)) launchWatcherNotification(getString(R.string.watcher_notification_text_paused))
} }
// TODO could this be restarting service in a bad state? private fun initWakeLock() {
// try to start task again if killed
override fun onTaskRemoved(rootIntent: Intent) {
Timber.d("Task Removed called")
val restartServiceIntent = Intent(rootIntent)
val restartServicePendingIntent: PendingIntent =
PendingIntent.getService(
this,
1,
restartServiceIntent,
PendingIntent.FLAG_ONE_SHOT or PendingIntent.FLAG_IMMUTABLE,
)
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 suspend fun initWakeLock() {
val isBatterySaverOn =
withContext(lifecycleScope.coroutineContext) {
settingsRepository.getSettings().isBatterySaverEnabled
}
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 {
try { try {
if (isBatterySaverOn) {
Timber.i("Initiating wakelock with 10 min timeout") Timber.i("Initiating wakelock with 10 min timeout")
acquire(Constants.BATTERY_SAVER_WATCHER_WAKE_LOCK_TIMEOUT) acquire(Constants.BATTERY_SAVER_WATCHER_WAKE_LOCK_TIMEOUT)
} else {
Timber.i("Initiating wakelock with 30 min timeout")
acquire(Constants.DEFAULT_WATCHER_WAKE_LOCK_TIMEOUT)
}
} finally { } finally {
release() release()
} }
@ -201,10 +167,17 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
Timber.i("Starting settings watcher") Timber.i("Starting settings watcher")
watchForSettingsChanges() watchForSettingsChanges()
} }
if(setting.isPingEnabled) {
launch {
Timber.i("Starting ping watcher")
watchForPingFailure()
}
}
launch { launch {
Timber.i("Starting management watcher") Timber.i("Starting management watcher")
manageVpn() manageVpn()
} }
} }
} }
@ -236,6 +209,40 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
} }
} }
private suspend fun watchForPingFailure() {
try {
do {
if(vpnService.vpnState.value.status == Tunnel.State.UP) {
val config = vpnService.vpnState.value.config
config?.let {
val results = it.peers.map { peer ->
val host = if(peer.endpoint.isPresent &&
peer.endpoint.get().resolved.isPresent)
peer.endpoint.get().resolved.get().host
else Constants.BACKUP_PING_HOST
Timber.i("Checking reachability of: $host")
val reachable = InetAddress.getByName(host).isReachable(Constants.PING_TIMEOUT.toInt())
Timber.i("Result: reachable - $reachable")
reachable
}
if(results.contains(false)) {
Timber.i("Restarting VPN for ping failure")
ServiceManager.stopVpnService(this)
delay(Constants.VPN_RESTART_DELAY)
val tunnel = networkEventsFlow.value.settings.defaultTunnel
ServiceManager.startVpnServiceForeground(this, tunnel!!)
delay(Constants.PING_COOLDOWN)
}
}
}
delay(Constants.PING_INTERVAL)
} while (true)
} catch (e: Exception) {
Timber.e(e)
}
}
private suspend fun watchForSettingsChanges() { private suspend fun watchForSettingsChanges() {
settingsRepository.getSettingsFlow().collect { settingsRepository.getSettingsFlow().collect {
if (networkEventsFlow.value.settings.isAutoTunnelPaused != it.isAutoTunnelPaused) { if (networkEventsFlow.value.settings.isAutoTunnelPaused != it.isAutoTunnelPaused) {
@ -331,10 +338,9 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
} }
} }
// TODO clean this up
private suspend fun manageVpn() { private suspend fun manageVpn() {
networkEventsFlow.collectLatest { networkEventsFlow.collectLatest {
Timber.i("New watcher state: $it") val autoTunnel = "Auto-tunnel watcher"
if (!it.settings.isAutoTunnelPaused && it.settings.defaultTunnel != null) { if (!it.settings.isAutoTunnelPaused && it.settings.defaultTunnel != null) {
delay(Constants.TOGGLE_TUNNEL_DELAY) delay(Constants.TOGGLE_TUNNEL_DELAY)
when { when {
@ -342,7 +348,7 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
it.settings.isTunnelOnEthernetEnabled && it.settings.isTunnelOnEthernetEnabled &&
!it.isVpnConnected)) -> { !it.isVpnConnected)) -> {
ServiceManager.startVpnServiceForeground(this, it.settings.defaultTunnel!!) ServiceManager.startVpnServiceForeground(this, it.settings.defaultTunnel!!)
Timber.i("Condition 1 met") Timber.i("$autoTunnel condition 1 met")
} }
(!it.isEthernetConnected && (!it.isEthernetConnected &&
it.settings.isTunnelOnMobileDataEnabled && it.settings.isTunnelOnMobileDataEnabled &&
@ -350,14 +356,14 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
it.isMobileDataConnected && it.isMobileDataConnected &&
!it.isVpnConnected) -> { !it.isVpnConnected) -> {
ServiceManager.startVpnServiceForeground(this, it.settings.defaultTunnel!!) ServiceManager.startVpnServiceForeground(this, it.settings.defaultTunnel!!)
Timber.i("Condition 2 met") Timber.i("$autoTunnel condition 2 met")
} }
(!it.isEthernetConnected && (!it.isEthernetConnected &&
!it.settings.isTunnelOnMobileDataEnabled && !it.settings.isTunnelOnMobileDataEnabled &&
!it.isWifiConnected && !it.isWifiConnected &&
it.isVpnConnected) -> { it.isVpnConnected) -> {
ServiceManager.stopVpnService(this) ServiceManager.stopVpnService(this)
Timber.i("Condition 3 met") Timber.i("$autoTunnel condition 3 met")
} }
(!it.isEthernetConnected && (!it.isEthernetConnected &&
it.isWifiConnected && it.isWifiConnected &&
@ -365,31 +371,31 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
it.settings.isTunnelOnWifiEnabled && it.settings.isTunnelOnWifiEnabled &&
(!it.isVpnConnected)) -> { (!it.isVpnConnected)) -> {
ServiceManager.startVpnServiceForeground(this, it.settings.defaultTunnel!!) ServiceManager.startVpnServiceForeground(this, it.settings.defaultTunnel!!)
Timber.i("Condition 4 met") Timber.i("$autoTunnel condition 4 met")
} }
(!it.isEthernetConnected && (!it.isEthernetConnected &&
(it.isWifiConnected && (it.isWifiConnected &&
it.settings.trustedNetworkSSIDs.contains(it.currentNetworkSSID)) && it.settings.trustedNetworkSSIDs.contains(it.currentNetworkSSID)) &&
(it.isVpnConnected)) -> { (it.isVpnConnected)) -> {
ServiceManager.stopVpnService(this) ServiceManager.stopVpnService(this)
Timber.i("Condition 5 met") Timber.i("$autoTunnel condition 5 met")
} }
(!it.isEthernetConnected && (!it.isEthernetConnected &&
(it.isWifiConnected && (it.isWifiConnected &&
!it.settings.isTunnelOnWifiEnabled && !it.settings.isTunnelOnWifiEnabled &&
(it.isVpnConnected))) -> { (it.isVpnConnected))) -> {
ServiceManager.stopVpnService(this) ServiceManager.stopVpnService(this)
Timber.i("Condition 6 met") Timber.i("$autoTunnel condition 6 met")
} }
(!it.isEthernetConnected && (!it.isEthernetConnected &&
!it.isWifiConnected && !it.isWifiConnected &&
!it.isMobileDataConnected && !it.isMobileDataConnected &&
(it.isVpnConnected)) -> { (it.isVpnConnected)) -> {
ServiceManager.stopVpnService(this) ServiceManager.stopVpnService(this)
Timber.i("Condition 7 met") Timber.i("$autoTunnel condition 7 met")
} }
else -> { else -> {
Timber.i("No condition met") Timber.i("$autoTunnel no condition met")
} }
} }
} }

View File

@ -45,8 +45,7 @@ class TunnelControlTile() : TileService() {
setUnavailable() setUnavailable()
return@collect return@collect
} }
tunnelName = tunnelName = it.name.run {
it.name.ifBlank {
val settings = settingsRepository.getSettings() val settings = settingsRepository.getSettings()
if (settings.defaultTunnel != null) { if (settings.defaultTunnel != null) {
TunnelConfig.from(settings.defaultTunnel!!).name TunnelConfig.from(settings.defaultTunnel!!).name
@ -72,15 +71,18 @@ class TunnelControlTile() : TileService() {
unlockAndRun { unlockAndRun {
scope.launch { scope.launch {
try { try {
val tunnelConfig = val defaultTunnel = settingsRepository.getSettings().defaultTunnel
tunnelConfigRepository.getAll().first { it.name == tunnelName } val config = defaultTunnel ?: run {
val tunnelConfigs = tunnelConfigRepository.getAll()
return@run tunnelConfigs.find { it.name == tunnelName }
}
toggleWatcherServicePause() toggleWatcherServicePause()
if (vpnService.getState() == Tunnel.State.UP) { if (vpnService.getState() == Tunnel.State.UP) {
ServiceManager.stopVpnService(this@TunnelControlTile) ServiceManager.stopVpnService(this@TunnelControlTile)
} else { } else {
ServiceManager.startVpnServiceForeground( ServiceManager.startVpnServiceForeground(
this@TunnelControlTile, this@TunnelControlTile,
tunnelConfig.toString(), config.toString(),
) )
} }
} catch (e: Exception) { } catch (e: Exception) {

View File

@ -2,9 +2,12 @@ package com.zaneschepke.wireguardautotunnel.service.tunnel
import com.wireguard.android.backend.Statistics import com.wireguard.android.backend.Statistics
import com.wireguard.android.backend.Tunnel import com.wireguard.android.backend.Tunnel
import com.wireguard.config.Config
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
data class VpnState( data class VpnState(
val status: Tunnel.State = Tunnel.State.DOWN, val status: Tunnel.State = Tunnel.State.DOWN,
val name: String = "", val name: String = "",
val config: Config? = null,
val statistics: Statistics? = null val statistics: Statistics? = null
) )

View File

@ -63,6 +63,7 @@ constructor(
stopTunnelOnConfigChange(tunnelConfig) stopTunnelOnConfigChange(tunnelConfig)
emitTunnelName(tunnelConfig.name) emitTunnelName(tunnelConfig.name)
config = TunnelConfig.configFromQuick(tunnelConfig.wgQuick) config = TunnelConfig.configFromQuick(tunnelConfig.wgQuick)
emitTunnelConfig(config)
val state = val state =
backend.setState( backend.setState(
this, this,
@ -71,7 +72,7 @@ constructor(
) )
emitTunnelState(state) emitTunnelState(state)
state state
} catch (e: Exception) { } catch (e: BackendException) {
Timber.e("Failed to start tunnel with error: ${e.message}") Timber.e("Failed to start tunnel with error: ${e.message}")
State.DOWN State.DOWN
} }
@ -101,6 +102,14 @@ constructor(
) )
} }
private suspend fun emitTunnelConfig(config : Config?) {
_vpnState.emit(
_vpnState.value.copy(
config = config,
),
)
}
private suspend fun stopTunnelOnConfigChange(tunnelConfig: TunnelConfig) { private suspend fun stopTunnelOnConfigChange(tunnelConfig: TunnelConfig) {
if (getState() == State.UP && _vpnState.value.name != tunnelConfig.name) { if (getState() == State.UP && _vpnState.value.name != tunnelConfig.name) {
stopTunnel() stopTunnel()
@ -129,7 +138,7 @@ constructor(
override fun onStateChange(state: State) { override fun onStateChange(state: State) {
val tunnel = this val tunnel = this
emitTunnelState(state) emitTunnelState(state)
WireGuardAutoTunnel.requestTileServiceStateUpdate() WireGuardAutoTunnel.requestTileServiceStateUpdate(WireGuardAutoTunnel.instance)
if (state == State.UP) { if (state == State.UP) {
statsJob = statsJob =
scope.launch { scope.launch {

View File

@ -0,0 +1,9 @@
package com.zaneschepke.wireguardautotunnel.ui
data class AppUiState(
val snackbarMessage: String = "",
val snackbarMessageConsumed: Boolean = true,
val vpnPermissionAccepted: Boolean = false,
val notificationPermissionAccepted: Boolean = false,
val requestPermissions: Boolean = false
)

View File

@ -4,13 +4,26 @@ import android.app.Application
import android.content.ActivityNotFoundException import android.content.ActivityNotFoundException
import android.content.Intent import android.content.Intent
import android.net.Uri import android.net.Uri
import android.widget.Toast
import androidx.compose.runtime.mutableStateListOf
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.wireguard.android.backend.GoBackend
import com.zaneschepke.logcatter.Logcatter
import com.zaneschepke.logcatter.model.LogMessage
import com.zaneschepke.wireguardautotunnel.R import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
import com.zaneschepke.wireguardautotunnel.data.datastore.DataStoreManager
import com.zaneschepke.wireguardautotunnel.util.Constants import com.zaneschepke.wireguardautotunnel.util.Constants
import com.zaneschepke.wireguardautotunnel.util.FileUtils
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
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.withContext
import timber.log.Timber import timber.log.Timber
import java.time.Instant
import javax.inject.Inject import javax.inject.Inject
@HiltViewModel @HiltViewModel
@ -20,8 +33,31 @@ constructor(
private val application: Application, private val application: Application,
) : ViewModel() { ) : ViewModel() {
private val _snackbarState = MutableStateFlow(SnackBarState()) val vpnIntent: Intent? = GoBackend.VpnService.prepare(WireGuardAutoTunnel.instance)
val snackBarState = _snackbarState.asStateFlow()
private val _appUiState = MutableStateFlow(AppUiState(
vpnPermissionAccepted = vpnIntent == null
))
val appUiState = _appUiState.asStateFlow()
fun isRequiredPermissionGranted() : Boolean {
val allAccepted = (_appUiState.value.vpnPermissionAccepted && _appUiState.value.vpnPermissionAccepted)
if(!allAccepted) requestPermissions()
return allAccepted
}
private fun requestPermissions() {
_appUiState.value = _appUiState.value.copy(
requestPermissions = true
)
}
fun permissionsRequested() {
_appUiState.value = _appUiState.value.copy(
requestPermissions = false
)
}
fun openWebPage(url: String) { fun openWebPage(url: String) {
try { try {
@ -36,6 +72,12 @@ constructor(
} }
} }
fun onVpnPermissionAccepted() {
_appUiState.value = _appUiState.value.copy(
vpnPermissionAccepted = true
)
}
fun launchEmail() { fun launchEmail() {
try { try {
val intent = val intent =
@ -55,16 +97,46 @@ constructor(
} }
} }
fun showSnackbarMessage(message : String) { fun showSnackbarMessage(message : String) {
_snackbarState.value = _snackbarState.value.copy( _appUiState.value = _appUiState.value.copy(
snackbarMessage = message, snackbarMessage = message,
snackbarMessageConsumed = false snackbarMessageConsumed = false
) )
} }
fun snackbarMessageConsumed() { fun snackbarMessageConsumed() {
_snackbarState.value = _snackbarState.value.copy( _appUiState.value = _appUiState.value.copy(
snackbarMessage = "", snackbarMessage = "",
snackbarMessageConsumed = true snackbarMessageConsumed = true
) )
} }
val logs = mutableStateListOf<LogMessage>()
fun readLogCatOutput() = viewModelScope.launch(viewModelScope.coroutineContext + Dispatchers.IO) {
launch {
Logcatter.logs {
logs.add(it)
if (logs.size > Constants.LOG_BUFFER_SIZE) {
logs.removeRange(0, (logs.size - Constants.LOG_BUFFER_SIZE).toInt())
}
}
}
}
fun clearLogs() {
logs.clear()
Logcatter.clear()
}
fun saveLogsToFile() {
val fileName = "${Constants.BASE_LOG_FILE_NAME}-${Instant.now().epochSecond}.txt"
val content = logs.joinToString(separator = "\n")
FileUtils.saveFileToDownloads(application.applicationContext, content, fileName)
Toast.makeText(application, application.getString(R.string.logs_saved), Toast.LENGTH_SHORT).show()
}
fun setNotificationPermissionAccepted(accepted: Boolean) {
_appUiState.value = _appUiState.value.copy(
notificationPermissionAccepted = accepted
)
}
} }

View File

@ -1,17 +1,16 @@
package com.zaneschepke.wireguardautotunnel.ui package com.zaneschepke.wireguardautotunnel.ui
import android.Manifest import android.Manifest
import android.content.Intent
import android.net.Uri
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.provider.Settings
import androidx.activity.SystemBarStyle import androidx.activity.SystemBarStyle
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.compose.setContent import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge import androidx.activity.enableEdgeToEdge
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.compose.foundation.focusable import androidx.compose.foundation.focusable
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
@ -20,10 +19,14 @@ import androidx.compose.material3.SnackbarDuration
import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.SnackbarResult import androidx.compose.material3.SnackbarResult
import androidx.compose.material3.Text
import androidx.compose.material3.surfaceColorAtElevation import androidx.compose.material3.surfaceColorAtElevation
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
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.focusProperties import androidx.compose.ui.focus.focusProperties
@ -35,6 +38,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.navigation.compose.NavHost import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable import androidx.navigation.compose.composable
import androidx.navigation.compose.currentBackStackEntryAsState
import androidx.navigation.compose.rememberNavController import androidx.navigation.compose.rememberNavController
import com.google.accompanist.permissions.ExperimentalPermissionsApi import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.google.accompanist.permissions.isGranted import com.google.accompanist.permissions.isGranted
@ -43,22 +47,22 @@ import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
import com.zaneschepke.wireguardautotunnel.data.datastore.DataStoreManager import com.zaneschepke.wireguardautotunnel.data.datastore.DataStoreManager
import com.zaneschepke.wireguardautotunnel.data.repository.SettingsRepository import com.zaneschepke.wireguardautotunnel.data.repository.SettingsRepository
import com.zaneschepke.wireguardautotunnel.ui.common.PermissionRequestFailedScreen import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
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.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.main.MainScreen import com.zaneschepke.wireguardautotunnel.ui.screens.main.MainScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.pinlock.PinLockScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.SettingsScreen import com.zaneschepke.wireguardautotunnel.ui.screens.settings.SettingsScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.support.SupportScreen import com.zaneschepke.wireguardautotunnel.ui.screens.support.SupportScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.support.logs.LogsScreen import com.zaneschepke.wireguardautotunnel.ui.screens.support.logs.LogsScreen
import com.zaneschepke.wireguardautotunnel.ui.theme.WireguardAutoTunnelTheme import com.zaneschepke.wireguardautotunnel.ui.theme.WireguardAutoTunnelTheme
import com.zaneschepke.wireguardautotunnel.util.Constants
import com.zaneschepke.wireguardautotunnel.util.StringValue import com.zaneschepke.wireguardautotunnel.util.StringValue
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import timber.log.Timber import xyz.teamgravity.pin_lock_compose.PinLock
import java.io.IOException import xyz.teamgravity.pin_lock_compose.PinManager
import javax.inject.Inject import javax.inject.Inject
@AndroidEntryPoint @AndroidEntryPoint
@ -67,9 +71,11 @@ class MainActivity : AppCompatActivity() {
@Inject @Inject
lateinit var dataStoreManager: DataStoreManager lateinit var dataStoreManager: DataStoreManager
@Inject lateinit var settingsRepository: SettingsRepository @Inject
lateinit var settingsRepository: SettingsRepository
@OptIn( @OptIn(
ExperimentalPermissionsApi::class ExperimentalPermissionsApi::class,
) )
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@ -78,36 +84,36 @@ class MainActivity : AppCompatActivity() {
// load preferences into memory and init data // load preferences into memory and init data
lifecycleScope.launch { lifecycleScope.launch {
try {
dataStoreManager.init() dataStoreManager.init()
WireGuardAutoTunnel.requestTileServiceStateUpdate() WireGuardAutoTunnel.requestTileServiceStateUpdate(this@MainActivity)
} catch (e: IOException) { val settings = settingsRepository.getSettings()
Timber.e("Failed to load preferences") if (settings.isAutoTunnelEnabled) {
ServiceManager.startWatcherService(application.applicationContext)
} }
} }
setContent { setContent {
val appViewModel = hiltViewModel<AppViewModel>() val appViewModel = hiltViewModel<AppViewModel>()
val snackBarState by appViewModel.snackBarState.collectAsStateWithLifecycle() val appUiState by appViewModel.appUiState.collectAsStateWithLifecycle()
val navController = rememberNavController() val navController = rememberNavController()
val focusRequester = remember { FocusRequester() } val navBackStackEntry by navController.currentBackStackEntryAsState()
WireguardAutoTunnelTheme { val notificationPermissionState =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU)
val snackbarHostState = remember { SnackbarHostState() }
val notificationPermissionState = if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU)
rememberPermissionState(Manifest.permission.POST_NOTIFICATIONS) else null rememberPermissionState(Manifest.permission.POST_NOTIFICATIONS) else null
fun requestNotificationPermission() { val focusRequester = remember { FocusRequester() }
if (notificationPermissionState != null && !notificationPermissionState.status.isGranted val snackbarHostState = remember { SnackbarHostState() }
) {
notificationPermissionState.launchPermissionRequest()
}
}
LaunchedEffect(Unit) { val vpnActivityResultState =
requestNotificationPermission() rememberLauncherForActivityResult(
ActivityResultContracts.StartActivityForResult(),
onResult = {
val accepted = (it.resultCode == RESULT_OK)
if (accepted) {
appViewModel.onVpnPermissionAccepted()
} }
},
)
fun showSnackBarMessage(message: StringValue) { fun showSnackBarMessage(message: StringValue) {
lifecycleScope.launch(Dispatchers.Main) { lifecycleScope.launch(Dispatchers.Main) {
@ -125,9 +131,31 @@ class MainActivity : AppCompatActivity() {
} }
} }
LaunchedEffect(snackBarState.snackbarMessageConsumed) { LaunchedEffect(appUiState.requestPermissions) {
if(!snackBarState.snackbarMessageConsumed) { if (appUiState.requestPermissions) {
showSnackBarMessage(StringValue.DynamicString(snackBarState.snackbarMessage)) appViewModel.permissionsRequested()
if (notificationPermissionState != null && !notificationPermissionState.status.isGranted
) {
showSnackBarMessage(StringValue.StringResource(R.string.notification_permission_required))
return@LaunchedEffect notificationPermissionState.launchPermissionRequest()
}
if (!appUiState.vpnPermissionAccepted) {
return@LaunchedEffect vpnActivityResultState.launch(appViewModel.vpnIntent)
}
}
}
WireguardAutoTunnelTheme {
LaunchedEffect(Unit) {
appViewModel.setNotificationPermissionAccepted(
notificationPermissionState?.status?.isGranted ?: true,
)
if(!WireGuardAutoTunnel.isRunningOnAndroidTv()) appViewModel.readLogCatOutput()
}
LaunchedEffect(appUiState.snackbarMessageConsumed) {
if (!appUiState.snackbarMessageConsumed) {
showSnackBarMessage(StringValue.DynamicString(appUiState.snackbarMessage))
appViewModel.snackbarMessageConsumed() appViewModel.snackbarMessageConsumed()
} }
} }
@ -145,12 +173,14 @@ class MainActivity : AppCompatActivity() {
) )
} }
}, },
//TODO refactor
modifier = Modifier modifier = Modifier
.focusable() .focusable()
.focusProperties { up = focusRequester }, .focusProperties { when(navBackStackEntry?.destination?.route) {
bottomBar = Screen.Lock.route -> Unit
if (notificationPermissionState == null || notificationPermissionState.status.isGranted) { else -> up = focusRequester }
{ },
bottomBar = {
BottomNavBar( BottomNavBar(
navController, navController,
listOf( listOf(
@ -159,33 +189,17 @@ class MainActivity : AppCompatActivity() {
Screen.Support.navItem, Screen.Support.navItem,
), ),
) )
}
} else {
{}
}, },
) { padding -> ) { padding ->
if (notificationPermissionState != null && !notificationPermissionState.status.isGranted) { NavHost(
Column(modifier = Modifier.padding(padding)) { navController,
PermissionRequestFailedScreen( startDestination =
onRequestAgain = { (if (PinManager.pinExists()) Screen.Lock.route else Screen.Main.route),
val intentSettings = modifier =
Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS) Modifier
intentSettings.data = .padding(padding)
Uri.fromParts( .fillMaxSize(),
Constants.URI_PACKAGE_SCHEME, ) {
this@MainActivity.packageName,
null,
)
startActivity(intentSettings)
},
message = getString(R.string.notification_permission_required),
getString(R.string.open_settings),
)
return@Scaffold
}
}
Column(modifier = Modifier.padding(padding)) {
NavHost(navController, startDestination = Screen.Main.route) {
composable( composable(
Screen.Main.route, Screen.Main.route,
) { ) {
@ -200,7 +214,8 @@ class MainActivity : AppCompatActivity() {
) { ) {
SettingsScreen( SettingsScreen(
appViewModel = appViewModel, appViewModel = appViewModel,
focusRequester = focusRequester, navController = navController,
focusRequester = focusRequester
) )
} }
composable( composable(
@ -209,11 +224,11 @@ class MainActivity : AppCompatActivity() {
SupportScreen( SupportScreen(
focusRequester = focusRequester, focusRequester = focusRequester,
appViewModel = appViewModel, appViewModel = appViewModel,
navController = navController navController = navController,
) )
} }
composable(Screen.Support.Logs.route,) { composable(Screen.Support.Logs.route) {
LogsScreen() LogsScreen(appViewModel)
} }
composable("${Screen.Config.route}/{id}") { composable("${Screen.Config.route}/{id}") {
val id = it.arguments?.getString("id") val id = it.arguments?.getString("id")
@ -226,6 +241,8 @@ class MainActivity : AppCompatActivity() {
) )
} }
} }
composable(Screen.Lock.route) {
PinLockScreen(navController = navController, appViewModel = appViewModel)
} }
} }
} }

View File

@ -36,4 +36,5 @@ sealed class Screen(val route: String) {
} }
data object Config : Screen("config") data object Config : Screen("config")
data object Lock : Screen("lock")
} }

View File

@ -1,6 +0,0 @@
package com.zaneschepke.wireguardautotunnel.ui
data class SnackBarState(
val snackbarMessage: String = "",
val snackbarMessageConsumed: Boolean = true,
)

View File

@ -29,7 +29,7 @@ fun ClickableIconButton(
Spacer(modifier = Modifier.size(ButtonDefaults.IconSpacing)) Spacer(modifier = Modifier.size(ButtonDefaults.IconSpacing))
Icon( Icon(
imageVector = icon, imageVector = icon,
contentDescription = stringResource(R.string.delete), contentDescription = icon.name,
modifier = modifier =
Modifier.size(ButtonDefaults.IconSize).weight(1f, false).clickable { Modifier.size(ButtonDefaults.IconSize).weight(1f, false).clickable {
if (enabled) { if (enabled) {

View File

@ -1,36 +0,0 @@
package com.zaneschepke.wireguardautotunnel.ui.common
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Button
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.launch
@Composable
fun PermissionRequestFailedScreen(
onRequestAgain: () -> Unit,
message: String,
buttonText: String
) {
val scope = rememberCoroutineScope()
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center,
modifier = Modifier.fillMaxSize(),
) {
Text(message, textAlign = TextAlign.Center, modifier = Modifier.padding(15.dp))
Button(
onClick = { scope.launch { onRequestAgain() } },
) {
Text(buttonText)
}
}
}

View File

@ -49,7 +49,6 @@ fun RowListItem(
) { ) {
Row( Row(
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.fillMaxWidth(.60f),
) { ) {
icon() icon()
Text(text) Text(text)
@ -65,6 +64,7 @@ fun RowListItem(
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceEvenly, horizontalArrangement = Arrangement.SpaceEvenly,
) { ) {
//TODO change these to string resources
val handshakeEpoch = statistics.peer(it)!!.latestHandshakeEpochMillis val handshakeEpoch = statistics.peer(it)!!.latestHandshakeEpochMillis
val peerTx = statistics.peer(it)!!.txBytes val peerTx = statistics.peer(it)!!.txBytes
val peerRx = statistics.peer(it)!!.rxBytes val peerRx = statistics.peer(it)!!.rxBytes

View File

@ -6,18 +6,33 @@ import androidx.compose.material3.NavigationBar
import androidx.compose.material3.NavigationBarItem import androidx.compose.material3.NavigationBarItem
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.navigation.NavController import androidx.navigation.NavController
import androidx.navigation.compose.currentBackStackEntryAsState import androidx.navigation.compose.currentBackStackEntryAsState
import com.zaneschepke.wireguardautotunnel.ui.Screen
@Composable @Composable
fun BottomNavBar(navController: NavController, bottomNavItems: List<BottomNavItem>) { fun BottomNavBar(navController: NavController, bottomNavItems: List<BottomNavItem>) {
val backStackEntry = navController.currentBackStackEntryAsState() val backStackEntry = navController.currentBackStackEntryAsState()
var showBottomBar by rememberSaveable { mutableStateOf(true) }
val navBackStackEntry by navController.currentBackStackEntryAsState()
//TODO find a better way to hide nav bar
showBottomBar = when (navBackStackEntry?.destination?.route) {
Screen.Lock.route -> false
else -> true
}
NavigationBar( NavigationBar(
containerColor = MaterialTheme.colorScheme.background, containerColor = if(!showBottomBar) Color.Transparent else MaterialTheme.colorScheme.background,
) { ) {
bottomNavItems.forEach { item -> if(showBottomBar) bottomNavItems.forEach { item ->
val selected = item.route == backStackEntry.value?.destination?.route val selected = item.route == backStackEntry.value?.destination?.route
NavigationBarItem( NavigationBarItem(

View File

@ -49,9 +49,10 @@ fun CustomSnackBar(
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Start, horizontalArrangement = Arrangement.Start,
) { ) {
val icon = Icons.Rounded.Info
Icon( Icon(
Icons.Rounded.Info, icon,
contentDescription = stringResource(R.string.info), contentDescription = icon.name,
tint = Color.White, tint = Color.White,
modifier = Modifier.padding(end = 10.dp), modifier = Modifier.padding(end = 10.dp),
) )

View File

@ -257,9 +257,10 @@ fun ConfigScreen(
modifier = Modifier.size(50.dp, 50.dp), modifier = Modifier.size(50.dp, 50.dp),
) )
} else { } else {
val icon = Icons.Rounded.Android
Icon( Icon(
Icons.Rounded.Android, icon,
stringResource(id = R.string.edit), icon.name,
modifier = Modifier.size(50.dp, 50.dp), modifier = Modifier.size(50.dp, 50.dp),
) )
} }
@ -530,7 +531,8 @@ fun ConfigScreen(
padding = screenPadding, padding = screenPadding,
) )
IconButton(onClick = { viewModel.onDeletePeer(index) }) { IconButton(onClick = { viewModel.onDeletePeer(index) }) {
Icon(Icons.Rounded.Delete, stringResource(R.string.delete)) val icon = Icons.Rounded.Delete
Icon(icon, icon.name)
} }
} }

View File

@ -7,8 +7,10 @@ import android.content.pm.PackageManager
import android.os.Build import android.os.Build
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.wireguard.config.BadConfigException
import com.wireguard.config.Config import com.wireguard.config.Config
import com.wireguard.config.Interface import com.wireguard.config.Interface
import com.wireguard.config.ParseException
import com.wireguard.config.Peer import com.wireguard.config.Peer
import com.wireguard.crypto.Key import com.wireguard.crypto.Key
import com.wireguard.crypto.KeyPair import com.wireguard.crypto.KeyPair
@ -152,7 +154,7 @@ constructor(
viewModelScope.launch { viewModelScope.launch {
if (tunnelConfig != null) { if (tunnelConfig != null) {
saveConfig(tunnelConfig).join() saveConfig(tunnelConfig).join()
WireGuardAutoTunnel.requestTileServiceStateUpdate() WireGuardAutoTunnel.requestTileServiceStateUpdate(application)
updateSettingsDefaultTunnel(tunnelConfig) updateSettingsDefaultTunnel(tunnelConfig)
} }
} }
@ -218,7 +220,8 @@ constructor(
Result.Success(Event.Message.ConfigSaved) Result.Success(Event.Message.ConfigSaved)
} catch (e: Exception) { } catch (e: Exception) {
Timber.e(e) Timber.e(e)
Result.Error(Event.Error.Exception(e)) val message = e.message?.substringAfter(":", missingDelimiterValue = "")
Result.Error(Event.Error.ConfigParseError(message ?: ""))
} }
} }

View File

@ -7,7 +7,6 @@ import android.content.pm.PackageManager
import android.os.Build 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.appcompat.app.AppCompatActivity
import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut import androidx.compose.animation.fadeOut
@ -37,6 +36,7 @@ import androidx.compose.material.icons.filled.QrCode
import androidx.compose.material.icons.rounded.Add import androidx.compose.material.icons.rounded.Add
import androidx.compose.material.icons.rounded.Bolt import androidx.compose.material.icons.rounded.Bolt
import androidx.compose.material.icons.rounded.Circle import androidx.compose.material.icons.rounded.Circle
import androidx.compose.material.icons.rounded.CopyAll
import androidx.compose.material.icons.rounded.Delete import androidx.compose.material.icons.rounded.Delete
import androidx.compose.material.icons.rounded.Edit import androidx.compose.material.icons.rounded.Edit
import androidx.compose.material.icons.rounded.Info import androidx.compose.material.icons.rounded.Info
@ -84,7 +84,6 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.navigation.NavController import androidx.navigation.NavController
import com.journeyapps.barcodescanner.ScanContract import com.journeyapps.barcodescanner.ScanContract
import com.journeyapps.barcodescanner.ScanOptions import com.journeyapps.barcodescanner.ScanOptions
import com.wireguard.android.backend.GoBackend
import com.wireguard.android.backend.Tunnel import com.wireguard.android.backend.Tunnel
import com.zaneschepke.wireguardautotunnel.R import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
@ -102,6 +101,7 @@ import com.zaneschepke.wireguardautotunnel.util.Event
import com.zaneschepke.wireguardautotunnel.util.Result import com.zaneschepke.wireguardautotunnel.util.Result
import com.zaneschepke.wireguardautotunnel.util.handshakeStatus import com.zaneschepke.wireguardautotunnel.util.handshakeStatus
import com.zaneschepke.wireguardautotunnel.util.mapPeerStats import com.zaneschepke.wireguardautotunnel.util.mapPeerStats
import com.zaneschepke.wireguardautotunnel.util.truncateWithEllipsis
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -128,29 +128,13 @@ fun MainScreen(
var selectedTunnel by remember { mutableStateOf<TunnelConfig?>(null) } var selectedTunnel by remember { mutableStateOf<TunnelConfig?>(null) }
val uiState by viewModel.uiState.collectAsStateWithLifecycle() val uiState by viewModel.uiState.collectAsStateWithLifecycle()
var vpnIntent by remember { mutableStateOf(GoBackend.VpnService.prepare(WireGuardAutoTunnel.instance)) } LaunchedEffect(Unit) {
val vpnActivityResultState = if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
rememberLauncherForActivityResult(
ActivityResultContracts.StartActivityForResult(),
onResult = {
val accepted = (it.resultCode == AppCompatActivity.RESULT_OK)
if (accepted) {
vpnIntent = null
}
},
)
LaunchedEffect(uiState.loading) {
if (!uiState.loading && WireGuardAutoTunnel.isRunningOnAndroidTv()) {
delay(Constants.FOCUS_REQUEST_DELAY) delay(Constants.FOCUS_REQUEST_DELAY)
focusRequester.requestFocus() focusRequester.requestFocus()
} }
} }
if (uiState.loading) {
LoadingScreen()
return
}
val tunnelFileImportResultLauncher = val tunnelFileImportResultLauncher =
rememberLauncherForActivityResult( rememberLauncherForActivityResult(
object : ActivityResultContracts.GetContent() { object : ActivityResultContracts.GetContent() {
@ -262,18 +246,21 @@ fun MainScreen(
} }
fun onTunnelToggle(checked: Boolean, tunnel: TunnelConfig) { fun onTunnelToggle(checked: Boolean, tunnel: TunnelConfig) {
if (vpnIntent != null) { if(appViewModel.isRequiredPermissionGranted()) {
return vpnActivityResultState.launch(vpnIntent)
}
if (checked) viewModel.onTunnelStart(tunnel) else viewModel.onTunnelStop() if (checked) viewModel.onTunnelStart(tunnel) else viewModel.onTunnelStop()
} }
}
if(uiState.loading) {
return LoadingScreen()
}
Scaffold( Scaffold(
modifier = modifier =
Modifier.pointerInput(Unit) { Modifier.pointerInput(Unit) {
detectTapGestures( detectTapGestures(
onTap = { onTap = {
if (!WireGuardAutoTunnel.isRunningOnAndroidTv()) selectedTunnel = null selectedTunnel = null
}, },
) )
}, },
@ -318,7 +305,7 @@ fun MainScreen(
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center, verticalArrangement = Arrangement.Center,
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize(),
) { ) {
Text(text = stringResource(R.string.no_tunnels), fontStyle = FontStyle.Italic) Text(text = stringResource(R.string.no_tunnels), fontStyle = FontStyle.Italic)
} }
@ -362,7 +349,7 @@ fun MainScreen(
scanOptions.setDesiredBarcodeFormats(ScanOptions.QR_CODE) scanOptions.setDesiredBarcodeFormats(ScanOptions.QR_CODE)
scanOptions.setOrientationLocked(true) scanOptions.setOrientationLocked(true)
scanOptions.setPrompt( scanOptions.setPrompt(
context.getString(R.string.scanning_qr) context.getString(R.string.scanning_qr),
) )
scanOptions.setBeepEnabled(false) scanOptions.setBeepEnabled(false)
scanOptions.captureActivity = scanOptions.captureActivity =
@ -422,27 +409,30 @@ fun MainScreen(
flingBehavior = ScrollableDefaults.flingBehavior(), flingBehavior = ScrollableDefaults.flingBehavior(),
) { ) {
item { item {
if(uiState.settings.isAutoTunnelEnabled){ if (uiState.settings.isAutoTunnelEnabled) {
val autoTunnelingLabel = buildAnnotatedString { val autoTunnelingLabel = buildAnnotatedString {
append(stringResource(id = R.string.auto_tunneling)) append(stringResource(id = R.string.auto_tunneling))
append(": ") append(": ")
if(uiState.settings.isAutoTunnelPaused) append( if (uiState.settings.isAutoTunnelPaused) append(
stringResource(id = R.string.paused) stringResource(id = R.string.paused),
) else append( ) else append(
stringResource(id = R.string.active), stringResource(id = R.string.active),
) )
} }
RowListItem( RowListItem(
icon = { Icon( icon = {
Icons.Rounded.Bolt, val icon = Icons.Rounded.Bolt
stringResource(id = R.string.auto), Icon(
icon,
icon.name,
modifier = Modifier modifier = Modifier
.padding(end = 10.dp) .padding(end = 10.dp)
.size(25.dp), .size(25.dp),
tint = tint =
if (uiState.settings.isAutoTunnelPaused) Color.Gray if (uiState.settings.isAutoTunnelPaused) Color.Gray
else mint, else mint,
) }, )
},
text = autoTunnelingLabel.text, text = autoTunnelingLabel.text,
rowButton = { rowButton = {
if (uiState.settings.isAutoTunnelPaused) { if (uiState.settings.isAutoTunnelPaused) {
@ -484,6 +474,7 @@ fun MainScreen(
statuses?.any { it == HandshakeStatus.STALE } == true -> corn statuses?.any { it == HandshakeStatus.STALE } == true -> corn
statuses?.all { it == HandshakeStatus.NOT_STARTED } == true -> statuses?.all { it == HandshakeStatus.NOT_STARTED } == true ->
Color.Gray Color.Gray
else -> { else -> {
Color.Gray Color.Gray
} }
@ -515,7 +506,7 @@ fun MainScreen(
) )
} }
}, },
text = tunnel.name, text = tunnel.name.truncateWithEllipsis(15),
onHold = { onHold = {
if ( if (
(uiState.vpnState.status == Tunnel.State.UP) && (uiState.vpnState.status == Tunnel.State.UP) &&
@ -563,9 +554,10 @@ fun MainScreen(
} }
}, },
) { ) {
val icon = Icons.Rounded.Star
Icon( Icon(
Icons.Rounded.Star, icon,
stringResource(id = R.string.set_primary), icon.name,
) )
} }
} }
@ -587,13 +579,22 @@ fun MainScreen(
) )
}, },
) { ) {
Icon(Icons.Rounded.Edit, stringResource(id = R.string.edit)) val icon = Icons.Rounded.Edit
Icon(icon, icon.name)
}
IconButton(
modifier = Modifier.focusable(),
onClick = { viewModel.onCopyTunnel(selectedTunnel) },
) {
val icon = Icons.Rounded.CopyAll
Icon(icon, icon.name)
} }
IconButton( IconButton(
modifier = Modifier.focusable(), modifier = Modifier.focusable(),
onClick = { showDeleteTunnelAlertDialog = true }, onClick = { showDeleteTunnelAlertDialog = true },
) { ) {
Icon(Icons.Rounded.Delete, stringResource(id = R.string.delete)) val icon = Icons.Rounded.Delete
Icon(icon, icon.name)
} }
} }
} else { } else {
@ -630,9 +631,10 @@ fun MainScreen(
} }
}, },
) { ) {
val icon = Icons.Rounded.Star
Icon( Icon(
Icons.Rounded.Star, icon,
stringResource(id = R.string.set_primary), icon.name,
) )
} }
} }
@ -646,12 +648,13 @@ fun MainScreen(
expanded.value = !expanded.value expanded.value = !expanded.value
} else { } else {
appViewModel.showSnackbarMessage( appViewModel.showSnackbarMessage(
Event.Message.TunnelOnAction.message Event.Message.TunnelOnAction.message,
) )
} }
}, },
) { ) {
Icon(Icons.Rounded.Info, stringResource(R.string.info)) val icon = Icons.Rounded.Info
Icon(icon, icon.name)
} }
IconButton( IconButton(
onClick = { onClick = {
@ -660,7 +663,7 @@ fun MainScreen(
tunnel.name == uiState.vpnState.name tunnel.name == uiState.vpnState.name
) { ) {
appViewModel.showSnackbarMessage( appViewModel.showSnackbarMessage(
Event.Message.TunnelOffAction.message Event.Message.TunnelOffAction.message,
) )
} else { } else {
navController.navigate( navController.navigate(
@ -669,7 +672,14 @@ fun MainScreen(
} }
}, },
) { ) {
Icon(Icons.Rounded.Edit, stringResource(id = R.string.edit)) val icon = Icons.Rounded.Edit
Icon(icon, icon.name)
}
IconButton(
onClick = { viewModel.onCopyTunnel(tunnel) },
) {
val icon = Icons.Rounded.CopyAll
Icon(icon, icon.name)
} }
IconButton( IconButton(
onClick = { onClick = {
@ -678,16 +688,18 @@ fun MainScreen(
tunnel.name == uiState.vpnState.name tunnel.name == uiState.vpnState.name
) { ) {
appViewModel.showSnackbarMessage( appViewModel.showSnackbarMessage(
Event.Message.TunnelOffAction.message Event.Message.TunnelOffAction.message,
) )
} else { } else {
selectedTunnel = tunnel
showDeleteTunnelAlertDialog = true showDeleteTunnelAlertDialog = true
} }
}, },
) { ) {
val icon = Icons.Rounded.Delete
Icon( Icon(
Icons.Rounded.Delete, icon,
stringResource(id = R.string.delete), icon.name
) )
} }
TunnelSwitch() TunnelSwitch()

View File

@ -7,6 +7,7 @@ import android.net.Uri
import android.provider.OpenableColumns import android.provider.OpenableColumns
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.google.zxing.common.StringUtils
import com.wireguard.config.Config import com.wireguard.config.Config
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
import com.zaneschepke.wireguardautotunnel.data.model.Settings import com.zaneschepke.wireguardautotunnel.data.model.Settings
@ -49,7 +50,6 @@ constructor(
tunnelConfigRepository.getTunnelConfigsFlow(), tunnelConfigRepository.getTunnelConfigsFlow(),
vpnService.vpnState, vpnService.vpnState,
) { settings, tunnels, vpnState -> ) { settings, tunnels, vpnState ->
validateWatcherServiceState(settings)
MainUiState(settings, tunnels, vpnState, false) MainUiState(settings, tunnels, vpnState, false)
} }
.stateIn( .stateIn(
@ -58,13 +58,6 @@ constructor(
MainUiState(), MainUiState(),
) )
private fun validateWatcherServiceState(settings: Settings) =
viewModelScope.launch(Dispatchers.IO) {
if (settings.isAutoTunnelEnabled) {
ServiceManager.startWatcherService(application.applicationContext)
}
}
private fun stopWatcherService() = private fun stopWatcherService() =
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
ServiceManager.stopWatcherService(application.applicationContext) ServiceManager.stopWatcherService(application.applicationContext)
@ -72,16 +65,17 @@ constructor(
fun onDelete(tunnel: TunnelConfig) { fun onDelete(tunnel: TunnelConfig) {
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
if (tunnelConfigRepository.count() == 1) {
stopWatcherService()
val settings = settingsRepository.getSettings() val settings = settingsRepository.getSettings()
val isDefault = settings.isTunnelConfigDefault(tunnel)
if (tunnelConfigRepository.count() == 1 || isDefault) {
stopWatcherService()
settings.defaultTunnel = null settings.defaultTunnel = null
settings.isAutoTunnelEnabled = false settings.isAutoTunnelEnabled = false
settings.isAlwaysOnVpnEnabled = false settings.isAlwaysOnVpnEnabled = false
saveSettings(settings) saveSettings(settings)
} }
tunnelConfigRepository.delete(tunnel) tunnelConfigRepository.delete(tunnel)
WireGuardAutoTunnel.requestTileServiceStateUpdate() WireGuardAutoTunnel.requestTileServiceStateUpdate(application)
} }
} }
@ -106,7 +100,7 @@ constructor(
fun onTunnelStop() = fun onTunnelStop() =
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
Timber.d("Stopping active tunnel") Timber.i("Stopping active tunnel")
ServiceManager.stopVpnService(application.applicationContext) ServiceManager.stopVpnService(application.applicationContext)
} }
@ -192,8 +186,9 @@ constructor(
} }
private suspend fun addTunnel(tunnelConfig: TunnelConfig) { private suspend fun addTunnel(tunnelConfig: TunnelConfig) {
val firstTunnel = tunnelConfigRepository.count() == 0
saveTunnel(tunnelConfig) saveTunnel(tunnelConfig)
WireGuardAutoTunnel.requestTileServiceStateUpdate() if(firstTunnel) WireGuardAutoTunnel.requestTileServiceStateUpdate(application)
} }
fun pauseAutoTunneling() = fun pauseAutoTunneling() =
@ -264,7 +259,13 @@ constructor(
if (selectedTunnel != null) { if (selectedTunnel != null) {
saveSettings(uiState.value.settings.copy(defaultTunnel = selectedTunnel.toString())) saveSettings(uiState.value.settings.copy(defaultTunnel = selectedTunnel.toString()))
.join() .join()
WireGuardAutoTunnel.requestTileServiceStateUpdate() WireGuardAutoTunnel.requestTileServiceStateUpdate(application)
}
}
fun onCopyTunnel(tunnel: TunnelConfig?) = viewModelScope.launch {
tunnel?.let {
saveTunnel(TunnelConfig(name = it.name.plus(NumberUtils.randomThree()), wgQuick = it.wgQuick))
} }
} }
} }

View File

@ -0,0 +1,49 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.pinlock
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.navigation.NavController
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
import com.zaneschepke.wireguardautotunnel.ui.AppViewModel
import com.zaneschepke.wireguardautotunnel.ui.Screen
import com.zaneschepke.wireguardautotunnel.util.StringValue
import xyz.teamgravity.pin_lock_compose.PinLock
@Composable
fun PinLockScreen(navController: NavController, appViewModel: AppViewModel) {
val context = LocalContext.current
PinLock(
title = { pinExists ->
Text(
text = if (pinExists) stringResource(id = R.string.enter_pin) else stringResource(
id = R.string.create_pin,
),
)
},
color = MaterialTheme.colorScheme.surface,
onPinCorrect = {
// pin is correct, navigate or hide pin lock
if(WireGuardAutoTunnel.isRunningOnAndroidTv()) {
navController.navigate(Screen.Main.route)
} else {
val isPopped = navController.popBackStack()
if(!isPopped) {
navController.navigate(Screen.Main.route)
}
}
},
onPinIncorrect = {
// pin is incorrect, show error
appViewModel.showSnackbarMessage(StringValue.StringResource(R.string.incorrect_pin).asString(context))
},
onPinCreated = {
// pin created for the first time, navigate or hide pin lock
appViewModel.showSnackbarMessage(StringValue.StringResource(R.string.pin_created).asString(context))
},
)
}

View File

@ -21,7 +21,6 @@ import androidx.compose.foundation.layout.ExperimentalLayoutApi
import androidx.compose.foundation.layout.FlowRow import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.IntrinsicSize import androidx.compose.foundation.layout.IntrinsicSize
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
@ -54,7 +53,6 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
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.focusProperties
import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
@ -68,6 +66,7 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.navigation.NavController
import com.google.accompanist.permissions.ExperimentalPermissionsApi import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.google.accompanist.permissions.isGranted import com.google.accompanist.permissions.isGranted
import com.google.accompanist.permissions.rememberPermissionState import com.google.accompanist.permissions.rememberPermissionState
@ -76,10 +75,10 @@ import com.wireguard.android.backend.WgQuickBackend
import com.zaneschepke.wireguardautotunnel.R import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
import com.zaneschepke.wireguardautotunnel.ui.AppViewModel import com.zaneschepke.wireguardautotunnel.ui.AppViewModel
import com.zaneschepke.wireguardautotunnel.ui.Screen
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.prompt.AuthorizationPrompt
import com.zaneschepke.wireguardautotunnel.ui.common.screen.LoadingScreen
import com.zaneschepke.wireguardautotunnel.ui.common.text.SectionTitle import com.zaneschepke.wireguardautotunnel.ui.common.text.SectionTitle
import com.zaneschepke.wireguardautotunnel.util.Event import com.zaneschepke.wireguardautotunnel.util.Event
import com.zaneschepke.wireguardautotunnel.util.FileUtils import com.zaneschepke.wireguardautotunnel.util.FileUtils
@ -87,6 +86,7 @@ import com.zaneschepke.wireguardautotunnel.util.Result
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import timber.log.Timber import timber.log.Timber
import xyz.teamgravity.pin_lock_compose.PinManager
import java.io.File import java.io.File
@OptIn( @OptIn(
@ -97,13 +97,15 @@ import java.io.File
fun SettingsScreen( fun SettingsScreen(
viewModel: SettingsViewModel = hiltViewModel(), viewModel: SettingsViewModel = hiltViewModel(),
appViewModel: AppViewModel, appViewModel: AppViewModel,
focusRequester: FocusRequester navController: NavController,
focusRequester: FocusRequester,
) { ) {
val scope = rememberCoroutineScope { Dispatchers.IO } val scope = rememberCoroutineScope { Dispatchers.IO }
val context = LocalContext.current val context = LocalContext.current
val focusManager = LocalFocusManager.current val focusManager = LocalFocusManager.current
val scrollState = rememberScrollState() val scrollState = rememberScrollState()
val interactionSource = remember { MutableInteractionSource() } val interactionSource = remember { MutableInteractionSource() }
val pinExists = remember { mutableStateOf(PinManager.pinExists()) }
val uiState by viewModel.uiState.collectAsStateWithLifecycle() val uiState by viewModel.uiState.collectAsStateWithLifecycle()
@ -113,19 +115,12 @@ fun SettingsScreen(
var showLocationServicesAlertDialog by remember { mutableStateOf(false) } var showLocationServicesAlertDialog by remember { mutableStateOf(false) }
var didExportFiles by remember { mutableStateOf(false) } var didExportFiles by remember { mutableStateOf(false) }
var showAuthPrompt by remember { mutableStateOf(false) } var showAuthPrompt by remember { mutableStateOf(false) }
val focusRequester2 = remember { FocusRequester() }
val screenPadding = 5.dp val screenPadding = 5.dp
val fillMaxWidth = .85f val fillMaxWidth = .85f
if (uiState.loading) {
LoadingScreen()
return
}
val startForResult = val startForResult =
rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) { rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) { result: ActivityResult ->
result: ActivityResult ->
if (result.resultCode == Activity.RESULT_OK) { if (result.resultCode == Activity.RESULT_OK) {
result.data result.data
// Handle the Intent // Handle the Intent
@ -164,7 +159,9 @@ fun SettingsScreen(
fun handleAutoTunnelToggle() { fun handleAutoTunnelToggle() {
if (uiState.isBatteryOptimizeDisableShown || isBatteryOptimizationsDisabled()) { if (uiState.isBatteryOptimizeDisableShown || isBatteryOptimizationsDisabled()) {
if (appViewModel.isRequiredPermissionGranted()) {
viewModel.toggleAutoTunnel() viewModel.toggleAutoTunnel()
}
} else { } else {
requestBatteryOptimizationsDisabled() requestBatteryOptimizationsDisabled()
} }
@ -249,12 +246,16 @@ fun SettingsScreen(
Column( Column(
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Top, verticalArrangement = Arrangement.Top,
modifier = Modifier.fillMaxSize().verticalScroll(scrollState), modifier = Modifier
.fillMaxSize()
.verticalScroll(scrollState),
) { ) {
Icon( Icon(
Icons.Rounded.LocationOff, Icons.Rounded.LocationOff,
contentDescription = stringResource(id = R.string.map), contentDescription = stringResource(id = R.string.map),
modifier = Modifier.padding(30.dp).size(128.dp), modifier = Modifier
.padding(30.dp)
.size(128.dp),
) )
Text( Text(
stringResource(R.string.prominent_background_location_title), stringResource(R.string.prominent_background_location_title),
@ -271,9 +272,13 @@ fun SettingsScreen(
Row( Row(
modifier = modifier =
if (WireGuardAutoTunnel.isRunningOnAndroidTv()) { if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
Modifier.fillMaxWidth().padding(10.dp) Modifier
.fillMaxWidth()
.padding(10.dp)
} else { } else {
Modifier.fillMaxWidth().padding(30.dp) Modifier
.fillMaxWidth()
.padding(30.dp)
}, },
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceEvenly, horizontalArrangement = Arrangement.SpaceEvenly,
@ -330,7 +335,10 @@ fun SettingsScreen(
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Top, verticalArrangement = Arrangement.Top,
modifier = modifier =
Modifier.fillMaxSize().verticalScroll(scrollState).clickable( Modifier
.fillMaxSize()
.verticalScroll(scrollState)
.clickable(
indication = null, indication = null,
interactionSource = interactionSource, interactionSource = interactionSource,
) { ) {
@ -344,11 +352,14 @@ fun SettingsScreen(
color = MaterialTheme.colorScheme.surface, color = MaterialTheme.colorScheme.surface,
modifier = modifier =
(if (WireGuardAutoTunnel.isRunningOnAndroidTv()) { (if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
Modifier.height(IntrinsicSize.Min) Modifier
.height(IntrinsicSize.Min)
.fillMaxWidth(fillMaxWidth) .fillMaxWidth(fillMaxWidth)
.padding(top = 10.dp) .padding(top = 10.dp)
} else { } else {
Modifier.fillMaxWidth(fillMaxWidth).padding(top = 20.dp) Modifier
.fillMaxWidth(fillMaxWidth)
.padding(top = 20.dp)
}) })
.padding(bottom = 10.dp), .padding(bottom = 10.dp),
) { ) {
@ -372,25 +383,30 @@ fun SettingsScreen(
modifier = modifier =
if (uiState.settings.isAutoTunnelEnabled) Modifier if (uiState.settings.isAutoTunnelEnabled) Modifier
else else
Modifier.focusRequester(focusRequester).focusProperties { Modifier
down = focusRequester2 .focusRequester(focusRequester),
},
) )
AnimatedVisibility(visible = uiState.settings.isTunnelOnWifiEnabled) { AnimatedVisibility(visible = uiState.settings.isTunnelOnWifiEnabled) {
Column { Column {
FlowRow( FlowRow(
modifier = Modifier.padding(screenPadding).fillMaxWidth(), modifier = Modifier
.padding(screenPadding)
.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(5.dp), horizontalArrangement = Arrangement.spacedBy(5.dp),
) { ) {
uiState.settings.trustedNetworkSSIDs.forEach { ssid -> uiState.settings.trustedNetworkSSIDs.forEach { ssid ->
ClickableIconButton( ClickableIconButton(
onClick = { onClick = {
if (WireGuardAutoTunnel.isRunningOnAndroidTv()) { if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
focusRequester.requestFocus()
viewModel.onDeleteTrustedSSID(ssid) viewModel.onDeleteTrustedSSID(ssid)
focusRequester2.requestFocus()
} }
}, },
onIconClick = { viewModel.onDeleteTrustedSSID(ssid) }, onIconClick = {
if (WireGuardAutoTunnel.isRunningOnAndroidTv()) focusRequester.requestFocus()
viewModel.onDeleteTrustedSSID(ssid)
},
text = ssid, text = ssid,
icon = Icons.Filled.Close, icon = Icons.Filled.Close,
enabled = enabled =
@ -414,12 +430,12 @@ fun SettingsScreen(
onValueChange = { currentText = it }, onValueChange = { currentText = it },
label = { Text(stringResource(R.string.add_trusted_ssid)) }, label = { Text(stringResource(R.string.add_trusted_ssid)) },
modifier = modifier =
Modifier.padding( Modifier
.padding(
start = screenPadding, start = screenPadding,
top = 5.dp, top = 5.dp,
bottom = 10.dp, bottom = 10.dp,
) ),
.focusRequester(focusRequester2),
maxLines = 1, maxLines = 1,
keyboardOptions = keyboardOptions =
KeyboardOptions( KeyboardOptions(
@ -473,13 +489,13 @@ fun SettingsScreen(
onCheckChanged = { viewModel.onToggleTunnelOnEthernet() }, onCheckChanged = { viewModel.onToggleTunnelOnEthernet() },
) )
ConfigurationToggle( ConfigurationToggle(
stringResource(R.string.battery_saver), stringResource(R.string.restart_on_ping),
enabled = enabled =
!(uiState.settings.isAutoTunnelEnabled || !(uiState.settings.isAutoTunnelEnabled ||
uiState.settings.isAlwaysOnVpnEnabled), uiState.settings.isAlwaysOnVpnEnabled),
checked = uiState.settings.isBatterySaverEnabled, checked = uiState.settings.isPingEnabled,
padding = screenPadding, padding = screenPadding,
onCheckChanged = { viewModel.onToggleBatterySaver() }, onCheckChanged = { viewModel.onToggleRestartOnPing() },
) )
Row( Row(
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
@ -503,14 +519,17 @@ fun SettingsScreen(
when (false) { when (false) {
isBackgroundLocationGranted -> isBackgroundLocationGranted ->
appViewModel.showSnackbarMessage( appViewModel.showSnackbarMessage(
Event.Error.BackgroundLocationRequired.message Event.Error.BackgroundLocationRequired.message,
) )
fineLocationState.status.isGranted -> fineLocationState.status.isGranted ->
appViewModel.showSnackbarMessage( appViewModel.showSnackbarMessage(
Event.Error.PreciseLocationRequired.message Event.Error.PreciseLocationRequired.message,
) )
viewModel.isLocationEnabled(context) -> viewModel.isLocationEnabled(context) ->
showLocationServicesAlertDialog = true showLocationServicesAlertDialog = true
else -> { else -> {
handleAutoTunnelToggle() handleAutoTunnelToggle()
} }
@ -537,7 +556,9 @@ fun SettingsScreen(
shadowElevation = 2.dp, shadowElevation = 2.dp,
shape = RoundedCornerShape(12.dp), shape = RoundedCornerShape(12.dp),
color = MaterialTheme.colorScheme.surface, color = MaterialTheme.colorScheme.surface,
modifier = Modifier.fillMaxWidth(fillMaxWidth).padding(vertical = 10.dp), modifier = Modifier
.fillMaxWidth(fillMaxWidth)
.padding(vertical = 10.dp),
) { ) {
Column( Column(
horizontalAlignment = Alignment.Start, horizontalAlignment = Alignment.Start,
@ -568,14 +589,14 @@ fun SettingsScreen(
} }
} }
} }
if (!WireGuardAutoTunnel.isRunningOnAndroidTv()) {
Surface( Surface(
tonalElevation = 2.dp, tonalElevation = 2.dp,
shadowElevation = 2.dp, shadowElevation = 2.dp,
shape = RoundedCornerShape(12.dp), shape = RoundedCornerShape(12.dp),
color = MaterialTheme.colorScheme.surface, color = MaterialTheme.colorScheme.surface,
modifier = modifier =
Modifier.fillMaxWidth(fillMaxWidth) Modifier
.fillMaxWidth(fillMaxWidth)
.padding(vertical = 10.dp) .padding(vertical = 10.dp)
.padding(bottom = 140.dp), .padding(bottom = 140.dp),
) { ) {
@ -588,6 +609,7 @@ fun SettingsScreen(
title = stringResource(id = R.string.other), title = stringResource(id = R.string.other),
padding = screenPadding, padding = screenPadding,
) )
if(!WireGuardAutoTunnel.isRunningOnAndroidTv()) {
ConfigurationToggle( ConfigurationToggle(
stringResource(R.string.always_on_vpn_support), stringResource(R.string.always_on_vpn_support),
enabled = !uiState.settings.isAutoTunnelEnabled, enabled = !uiState.settings.isAutoTunnelEnabled,
@ -602,9 +624,27 @@ fun SettingsScreen(
padding = screenPadding, padding = screenPadding,
onCheckChanged = { viewModel.onToggleShortcutsEnabled() }, onCheckChanged = { viewModel.onToggleShortcutsEnabled() },
) )
}
ConfigurationToggle(
stringResource(R.string.enable_app_lock),
enabled = true,
checked = pinExists.value,
padding = screenPadding,
onCheckChanged = {
if (pinExists.value) {
PinManager.clearPin()
pinExists.value = PinManager.pinExists()
} else {
navController.navigate(Screen.Lock.route)
}
},
)
if(!WireGuardAutoTunnel.isRunningOnAndroidTv()) {
Row( Row(
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.fillMaxSize().padding(top = 5.dp), modifier = Modifier
.fillMaxSize()
.padding(top = 5.dp),
horizontalArrangement = Arrangement.Center, horizontalArrangement = Arrangement.Center,
) { ) {
TextButton( TextButton(
@ -617,9 +657,6 @@ fun SettingsScreen(
} }
} }
} }
if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
Spacer(modifier = Modifier.weight(.17f))
}
} }
} }
} }

View File

@ -9,6 +9,5 @@ data class SettingsUiState(
val tunnels: List<TunnelConfig> = emptyList(), val tunnels: List<TunnelConfig> = emptyList(),
val vpnState: VpnState = VpnState(), val vpnState: VpnState = VpnState(),
val isLocationDisclosureShown: Boolean = true, val isLocationDisclosureShown: Boolean = true,
val isBatteryOptimizeDisableShown: Boolean = false, val isBatteryOptimizeDisableShown: Boolean = false
val loading: Boolean = true
) )

View File

@ -49,7 +49,6 @@ constructor(
tunnelState, tunnelState,
preferences?.get(DataStoreManager.LOCATION_DISCLOSURE_SHOWN) ?: false, preferences?.get(DataStoreManager.LOCATION_DISCLOSURE_SHOWN) ?: false,
preferences?.get(DataStoreManager.BATTERY_OPTIMIZE_DISABLE_SHOWN) ?: false, preferences?.get(DataStoreManager.BATTERY_OPTIMIZE_DISABLE_SHOWN) ?: false,
false
) )
} }
.stateIn( .stateIn(
@ -195,4 +194,12 @@ constructor(
} }
return Result.Success(Unit) return Result.Success(Unit)
} }
fun onToggleRestartOnPing() = viewModelScope.launch {
saveSettings(
uiState.value.settings.copy(
isPingEnabled = !uiState.value.settings.isPingEnabled,
),
)
}
} }

View File

@ -64,11 +64,6 @@ fun SupportScreen(
val uiState by viewModel.uiState.collectAsStateWithLifecycle() val uiState by viewModel.uiState.collectAsStateWithLifecycle()
if (uiState.loading) {
LoadingScreen()
return
}
Column( Column(
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Top, verticalArrangement = Arrangement.Top,
@ -229,6 +224,7 @@ fun SupportScreen(
) )
} }
} }
if(!WireGuardAutoTunnel.isRunningOnAndroidTv()) {
HorizontalDivider( HorizontalDivider(
thickness = 0.5.dp, thickness = 0.5.dp,
color = MaterialTheme.colorScheme.onBackground color = MaterialTheme.colorScheme.onBackground
@ -259,6 +255,7 @@ fun SupportScreen(
} }
} }
} }
}
Spacer(modifier = Modifier.weight(1f)) Spacer(modifier = Modifier.weight(1f))
Text( Text(
stringResource(id = R.string.privacy_policy), stringResource(id = R.string.privacy_policy),

View File

@ -2,4 +2,4 @@ package com.zaneschepke.wireguardautotunnel.ui.screens.support
import com.zaneschepke.wireguardautotunnel.data.model.Settings import com.zaneschepke.wireguardautotunnel.data.model.Settings
data class SupportUiState(val settings: Settings = Settings(), val loading: Boolean = true) data class SupportUiState(val settings: Settings = Settings())

View File

@ -17,7 +17,7 @@ class SupportViewModel @Inject constructor(private val settingsRepository: Setti
val uiState = val uiState =
settingsRepository settingsRepository
.getSettingsFlow() .getSettingsFlow()
.map { SupportUiState(it, false) } .map { SupportUiState(it) }
.stateIn( .stateIn(
viewModelScope, viewModelScope,
SharingStarted.WhileSubscribed(Constants.SUBSCRIPTION_TIMEOUT), SharingStarted.WhileSubscribed(Constants.SUBSCRIPTION_TIMEOUT),

View File

@ -32,26 +32,22 @@ import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
import com.zaneschepke.wireguardautotunnel.ui.AppViewModel
import com.zaneschepke.wireguardautotunnel.ui.common.text.LogTypeLabel import com.zaneschepke.wireguardautotunnel.ui.common.text.LogTypeLabel
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter") @SuppressLint("UnusedMaterial3ScaffoldPaddingParameter")
@Composable @Composable
fun LogsScreen(logsViewModel: LogsViewModel = hiltViewModel()) { fun LogsScreen(appViewModel: AppViewModel) {
val logs = remember { val logs = remember {
logsViewModel.logs appViewModel.logs
} }
val lazyColumnListState = rememberLazyListState() val lazyColumnListState = rememberLazyListState()
val clipboardManager: ClipboardManager = LocalClipboardManager.current val clipboardManager: ClipboardManager = LocalClipboardManager.current
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
LaunchedEffect(Unit) {
logsViewModel.readLogCatOutput()
}
LaunchedEffect(logs.size){ LaunchedEffect(logs.size){
scope.launch { scope.launch {
lazyColumnListState.animateScrollToItem(logs.size) lazyColumnListState.animateScrollToItem(logs.size)
@ -62,7 +58,7 @@ fun LogsScreen(logsViewModel: LogsViewModel = hiltViewModel()) {
floatingActionButton = { floatingActionButton = {
FloatingActionButton( FloatingActionButton(
onClick = { onClick = {
logsViewModel.saveLogsToFile() appViewModel.saveLogsToFile()
}, },
shape = RoundedCornerShape(16.dp), shape = RoundedCornerShape(16.dp),
containerColor = MaterialTheme.colorScheme.primary containerColor = MaterialTheme.colorScheme.primary

View File

@ -1,53 +0,0 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.support.logs
import android.app.Application
import android.widget.Toast
import androidx.compose.runtime.mutableStateListOf
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.zaneschepke.logcatter.Logcatter
import com.zaneschepke.logcatter.model.LogMessage
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.util.Constants
import com.zaneschepke.wireguardautotunnel.util.FileUtils
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import java.time.Instant
import javax.inject.Inject
@HiltViewModel
class LogsViewModel
@Inject
constructor(
private val application: Application
) : ViewModel() {
val logs = mutableStateListOf<LogMessage>()
fun readLogCatOutput() = viewModelScope.launch(viewModelScope.coroutineContext + Dispatchers.IO) {
launch {
Logcatter.logs {
logs.add(it)
if (logs.size > Constants.LOG_BUFFER_SIZE) {
logs.removeRange(0, (logs.size - Constants.LOG_BUFFER_SIZE).toInt())
}
}
}
}
fun clearLogs() {
logs.clear()
Logcatter.clear()
}
fun saveLogsToFile() {
val fileName = "${Constants.BASE_LOG_FILE_NAME}-${Instant.now().epochSecond}.txt"
val content = logs.joinToString(separator = "\n")
FileUtils.saveFileToDownloads(application.applicationContext, content, fileName)
Toast.makeText(application, application.getString(R.string.logs_saved), Toast.LENGTH_SHORT).show()
}
}

View File

@ -7,14 +7,12 @@ object Constants {
const val MANUAL_TUNNEL_CONFIG_ID = "0" const val MANUAL_TUNNEL_CONFIG_ID = "0"
const val BATTERY_SAVER_WATCHER_WAKE_LOCK_TIMEOUT = 10 * 60 * 1_000L // 10 minutes const val BATTERY_SAVER_WATCHER_WAKE_LOCK_TIMEOUT = 10 * 60 * 1_000L // 10 minutes
const val DEFAULT_WATCHER_WAKE_LOCK_TIMEOUT = 30 * 60 * 1_000L // 30 minutes
const val VPN_STATISTIC_CHECK_INTERVAL = 1_000L const val VPN_STATISTIC_CHECK_INTERVAL = 1_000L
const val VPN_CONNECTED_NOTIFICATION_DELAY = 3_000L const val VPN_CONNECTED_NOTIFICATION_DELAY = 3_000L
const val TOGGLE_TUNNEL_DELAY = 300L const val TOGGLE_TUNNEL_DELAY = 300L
const val CONF_FILE_EXTENSION = ".conf" const val CONF_FILE_EXTENSION = ".conf"
const val ZIP_FILE_EXTENSION = ".zip" 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 ALLOWED_FILE_TYPES = "*/*" const val ALLOWED_FILE_TYPES = "*/*"
const val TEXT_MIME_TYPE = "text/plain" const val TEXT_MIME_TYPE = "text/plain"
const val GOOGLE_TV_EXPLORER_STUB = "com.google.android.tv.frameworkpackagestubs" const val GOOGLE_TV_EXPLORER_STUB = "com.google.android.tv.frameworkpackagestubs"
@ -24,4 +22,11 @@ object Constants {
const val SUBSCRIPTION_TIMEOUT = 5_000L const val SUBSCRIPTION_TIMEOUT = 5_000L
const val FOCUS_REQUEST_DELAY = 500L const val FOCUS_REQUEST_DELAY = 500L
const val BACKUP_PING_HOST = "1.1.1.1"
const val PING_TIMEOUT = 5_000L
const val VPN_RESTART_DELAY = 1_000L
const val PING_INTERVAL = 60_000L
const val PING_COOLDOWN = PING_INTERVAL * 60 //one hour
} }

View File

@ -18,6 +18,12 @@ sealed class Event {
get() = WireGuardAutoTunnel.instance.getString(R.string.error_ssid_exists) get() = WireGuardAutoTunnel.instance.getString(R.string.error_ssid_exists)
} }
data class ConfigParseError(val appendedMessage : String) : Error() {
override val message: String =
WireGuardAutoTunnel.instance.getString(R.string.config_parse_error) + (
if (appendedMessage != "") ": ${appendedMessage.trim()}" else "")
}
data object RootDenied : Error() { data object RootDenied : Error() {
override val message: String override val message: String
get() = WireGuardAutoTunnel.instance.getString(R.string.error_root_denied) get() = WireGuardAutoTunnel.instance.getString(R.string.error_root_denied)

View File

@ -31,6 +31,12 @@ fun BroadcastReceiver.goAsync(
} }
} }
fun String.truncateWithEllipsis(allowedLength : Int) : String {
return if(this.length > allowedLength + 3) {
this.substring(0, allowedLength) + "***"
} else this
}
fun BigDecimal.toThreeDecimalPlaceString(): String { fun BigDecimal.toThreeDecimalPlaceString(): String {
val df = DecimalFormat("#.###") val df = DecimalFormat("#.###")
return df.format(this) return df.format(this)

View File

@ -19,7 +19,15 @@ object NumberUtils {
} }
fun generateRandomTunnelName(): String { fun generateRandomTunnelName(): String {
return "tunnel${(Math.random() * 100000).toInt()}" return "tunnel${randomFive()}"
}
private fun randomFive() : Int {
return (Math.random() * 100000).toInt()
}
fun randomThree() : Int {
return (Math.random() * 1000).toInt()
} }
fun getSecondsBetweenTimestampAndNow(epoch: Long): Long? { fun getSecondsBetweenTimestampAndNow(epoch: Long): Long? {

View File

@ -20,7 +20,7 @@
<string name="tunnel_start_title">VPN Connected</string> <string name="tunnel_start_title">VPN Connected</string>
<string name="tunnel_start_text">Connected to tunnel -</string> <string name="tunnel_start_text">Connected to tunnel -</string>
<string name="vpn_permission_required">VPN permission is required for the app to work properly. If this permission is not launching, please disable \"Always-on VPN\" in your phone settings for the official WireGuard mobile app and try again.</string> <string name="vpn_permission_required">VPN permission is required for the app to work properly. If this permission is not launching, please disable \"Always-on VPN\" in your phone settings for the official WireGuard mobile app and try again.</string>
<string name="notification_permission_required">Notifications permission is required for the app to work properly.</string> <string name="notification_permission_required">Notifications permission required.</string>
<string name="open_settings">Open Settings</string> <string name="open_settings">Open Settings</string>
<string name="add_trusted_ssid">Add trusted wifi name</string> <string name="add_trusted_ssid">Add trusted wifi name</string>
<string name="tunnels">Tunnels</string> <string name="tunnels">Tunnels</string>
@ -46,8 +46,6 @@
<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>
<string name="edit">Edit</string>
<string name="delete">Delete</string>
<string name="add_tunnel">Add Tunnel</string> <string name="add_tunnel">Add Tunnel</string>
<string name="exclude">Exclude</string> <string name="exclude">Exclude</string>
<string name="include">Include</string> <string name="include">Include</string>
@ -105,11 +103,9 @@
<string name="default_vpn_on">Primary VPN on</string> <string name="default_vpn_on">Primary VPN on</string>
<string name="default_vpn_off">Primary VPN off</string> <string name="default_vpn_off">Primary VPN off</string>
<string name="create_import">Create from scratch</string> <string name="create_import">Create from scratch</string>
<string name="set_primary">Set primary</string>
<string name="turn_off_auto">Action requires auto-tunnel disabled</string> <string name="turn_off_auto">Action requires auto-tunnel disabled</string>
<string name="turn_on_tunnel">Action requires active tunnel</string> <string name="turn_on_tunnel">Action requires active tunnel</string>
<string name="add_peer">Add peer</string> <string name="add_peer">Add peer</string>
<string name="info">Info</string>
<string name="done">Done</string> <string name="done">Done</string>
<string name="interface_">Interface</string> <string name="interface_">Interface</string>
<string name="rotate_keys">Rotate keys</string> <string name="rotate_keys">Rotate keys</string>
@ -119,7 +115,6 @@
<string name="comma_separated_list">comma separated list</string> <string name="comma_separated_list">comma separated list</string>
<string name="listen_port">Listen port</string> <string name="listen_port">Listen port</string>
<string name="random">(random)</string> <string name="random">(random)</string>
<string name="auto">(auto)</string>
<string name="optional">(optional)</string> <string name="optional">(optional)</string>
<string name="optional_no_recommend">(optional, not recommended)</string> <string name="optional_no_recommend">(optional, not recommended)</string>
<string name="preshared_key">Pre-shared key</string> <string name="preshared_key">Pre-shared key</string>
@ -177,4 +172,12 @@
<string name="logs_saved">Logs saved to downloads</string> <string name="logs_saved">Logs saved to downloads</string>
<string name="open_issue">Open an issue</string> <string name="open_issue">Open an issue</string>
<string name="read_logs">Read the logs</string> <string name="read_logs">Read the logs</string>
<string name="auto">(auto)</string>
<string name="config_parse_error">Failed to parse config</string>
<string name="incorrect_pin">Pin is incorrect</string>
<string name="pin_created">Pin successfully created</string>
<string name="enter_pin">Enter your pin</string>
<string name="create_pin">Create pin</string>
<string name="enable_app_lock">Enabled app lock</string>
<string name="restart_on_ping">Restart on ping fail</string>
</resources> </resources>

View File

@ -1,7 +1,7 @@
object Constants { object Constants {
const val VERSION_NAME = "3.3.8-ipv6" const val VERSION_NAME = "3.3.9"
const val JVM_TARGET = "17" const val JVM_TARGET = "17"
const val VERSION_CODE = 33803 const val VERSION_CODE = 33900
const val TARGET_SDK = 34 const val TARGET_SDK = 34
const val MIN_SDK = 26 const val MIN_SDK = 26
const val APP_ID = "com.zaneschepke.wireguardautotunnel" const val APP_ID = "com.zaneschepke.wireguardautotunnel"

View File

@ -0,0 +1,5 @@
What's new:
- Add logs screen
- Add local app lock
- Add restart vpn on failed ping
- Various bug fixes

View File

@ -9,27 +9,28 @@ coreKtx = "1.12.0"
datastorePreferences = "1.0.0" datastorePreferences = "1.0.0"
desugar_jdk_libs = "2.0.4" desugar_jdk_libs = "2.0.4"
espressoCore = "3.5.1" espressoCore = "3.5.1"
hiltAndroid = "2.50" hiltAndroid = "2.51"
hiltNavigationCompose = "1.2.0" hiltNavigationCompose = "1.2.0"
junit = "4.13.2" junit = "4.13.2"
kotlinx-serialization-json = "1.6.3" kotlinx-serialization-json = "1.6.3"
lifecycle-runtime-compose = "2.7.0" lifecycle-runtime-compose = "2.7.0"
material3 = "1.2.0" material3 = "1.2.1"
navigationCompose = "2.7.7" navigationCompose = "2.7.7"
pinLockCompose = "1.0.3"
roomVersion = "2.6.1" roomVersion = "2.6.1"
timber = "5.0.1" timber = "5.0.1"
tunnel = "1.1.0" tunnel = "1.0.20230706"
androidGradlePlugin = "8.3.0" androidGradlePlugin = "8.3.1"
kotlin = "1.9.22" kotlin = "1.9.22"
ksp = "1.9.22-1.0.17" ksp = "1.9.22-1.0.17"
composeBom = "2024.02.01" composeBom = "2024.02.02"
compose = "1.6.3" compose = "1.6.3"
zxingAndroidEmbedded = "4.3.0" zxingAndroidEmbedded = "4.3.0"
zxingCore = "3.5.3" zxingCore = "3.5.3"
#plugins #plugins
gradlePlugins-kotlinxSerialization = "1.8.21" gradlePlugins-kotlinxSerialization = "1.8.21"
material = "1.10.0" material = "1.11.0"
[libraries] [libraries]
@ -80,8 +81,9 @@ kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serializa
lifecycle-runtime-compose = { module = "androidx.lifecycle:lifecycle-runtime-compose", version.ref = "lifecycle-runtime-compose" } lifecycle-runtime-compose = { module = "androidx.lifecycle:lifecycle-runtime-compose", version.ref = "lifecycle-runtime-compose" }
material-icons-extended = { module = "androidx.compose.material:material-icons-extended", version.ref = "compose" } material-icons-extended = { module = "androidx.compose.material:material-icons-extended", version.ref = "compose" }
pin-lock-compose = { module = "com.zaneschepke:pin_lock_compose", version.ref = "pinLockCompose" }
timber = { module = "com.jakewharton.timber:timber", version.ref = "timber" } timber = { module = "com.jakewharton.timber:timber", version.ref = "timber" }
tunnel = { module = "com.zaneschepke:wireguard-android", version.ref = "tunnel" } tunnel = { module = "com.wireguard.android:tunnel", version.ref = "tunnel" }
zxing-core = { module = "com.google.zxing:core", version.ref = "zxingCore" } zxing-core = { module = "com.google.zxing:core", version.ref = "zxingCore" }
zxing-android-embedded = { module = "com.journeyapps:zxing-android-embedded", version.ref = "zxingAndroidEmbedded" } zxing-android-embedded = { module = "com.journeyapps:zxing-android-embedded", version.ref = "zxingAndroidEmbedded" }

View File

@ -7,21 +7,11 @@ pluginManagement {
} }
} }
val GITHUB_USER_VAR = "GH_USER"
val GITHUB_TOKEN_VAR = "GH_TOKEN"
dependencyResolutionManagement { dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories { repositories {
mavenLocal() mavenLocal()
maven { maven("https://gitea.zaneschepke.com/api/packages/zane/maven")
name = "GitHubPackages"
url = uri("https://maven.pkg.github.com/zaneschepke/wireguard-android")
credentials {
username = getLocalProperty(GITHUB_USER_VAR) ?: System.getenv(GITHUB_USER_VAR)
password = getLocalProperty(GITHUB_TOKEN_VAR) ?: System.getenv(GITHUB_TOKEN_VAR)
}
}
google() google()
mavenCentral() mavenCentral()
} }