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:
parent
4fc8ffbcbb
commit
5946d7c10d
|
@ -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 }}
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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')"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
|
@ -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),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
)
|
)
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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,49 +119,18 @@ 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)
|
} finally {
|
||||||
} else {
|
release()
|
||||||
Timber.i("Initiating wakelock with 30 min timeout")
|
|
||||||
acquire(Constants.DEFAULT_WATCHER_WAKE_LOCK_TIMEOUT)
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
release()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun cancelWatcherJob() {
|
private fun cancelWatcherJob() {
|
||||||
|
@ -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")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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
|
||||||
)
|
)
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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
|
||||||
|
)
|
|
@ -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
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,56 +84,78 @@ 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(this@MainActivity)
|
||||||
WireGuardAutoTunnel.requestTileServiceStateUpdate()
|
val settings = settingsRepository.getSettings()
|
||||||
} catch (e: IOException) {
|
if (settings.isAutoTunnelEnabled) {
|
||||||
Timber.e("Failed to load preferences")
|
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) {
|
||||||
val result =
|
val result =
|
||||||
snackbarHostState.showSnackbar(
|
snackbarHostState.showSnackbar(
|
||||||
message = message.asString(this@MainActivity),
|
message = message.asString(this@MainActivity),
|
||||||
duration = SnackbarDuration.Short,
|
duration = SnackbarDuration.Short,
|
||||||
)
|
)
|
||||||
when (result) {
|
when (result) {
|
||||||
SnackbarResult.ActionPerformed,
|
SnackbarResult.ActionPerformed,
|
||||||
SnackbarResult.Dismissed -> {
|
SnackbarResult.Dismissed -> {
|
||||||
snackbarHostState.currentSnackbarData?.dismiss()
|
snackbarHostState.currentSnackbarData?.dismiss()
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -139,95 +167,84 @@ class MainActivity : AppCompatActivity() {
|
||||||
snackbarData.visuals.message,
|
snackbarData.visuals.message,
|
||||||
isRtl = false,
|
isRtl = false,
|
||||||
containerColor =
|
containerColor =
|
||||||
MaterialTheme.colorScheme.surfaceColorAtElevation(
|
MaterialTheme.colorScheme.surfaceColorAtElevation(
|
||||||
2.dp,
|
2.dp,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
//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 }
|
||||||
{
|
},
|
||||||
BottomNavBar(
|
bottomBar = {
|
||||||
navController,
|
BottomNavBar(
|
||||||
listOf(
|
navController,
|
||||||
Screen.Main.navItem,
|
listOf(
|
||||||
Screen.Settings.navItem,
|
Screen.Main.navItem,
|
||||||
Screen.Support.navItem,
|
Screen.Settings.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,
|
composable(
|
||||||
null,
|
Screen.Main.route,
|
||||||
)
|
) {
|
||||||
startActivity(intentSettings)
|
MainScreen(
|
||||||
},
|
focusRequester = focusRequester,
|
||||||
message = getString(R.string.notification_permission_required),
|
appViewModel = appViewModel,
|
||||||
getString(R.string.open_settings),
|
navController = navController,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
composable(
|
||||||
|
Screen.Settings.route,
|
||||||
|
) {
|
||||||
|
SettingsScreen(
|
||||||
|
appViewModel = appViewModel,
|
||||||
|
navController = navController,
|
||||||
|
focusRequester = focusRequester
|
||||||
|
)
|
||||||
|
}
|
||||||
|
composable(
|
||||||
|
Screen.Support.route,
|
||||||
|
) {
|
||||||
|
SupportScreen(
|
||||||
|
focusRequester = focusRequester,
|
||||||
|
appViewModel = appViewModel,
|
||||||
|
navController = navController,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
composable(Screen.Support.Logs.route) {
|
||||||
|
LogsScreen(appViewModel)
|
||||||
|
}
|
||||||
|
composable("${Screen.Config.route}/{id}") {
|
||||||
|
val id = it.arguments?.getString("id")
|
||||||
|
if (!id.isNullOrBlank()) {
|
||||||
|
ConfigScreen(
|
||||||
|
navController = navController,
|
||||||
|
id = id,
|
||||||
|
appViewModel = appViewModel,
|
||||||
|
focusRequester = focusRequester,
|
||||||
)
|
)
|
||||||
return@Scaffold
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Column(modifier = Modifier.padding(padding)) {
|
composable(Screen.Lock.route) {
|
||||||
NavHost(navController, startDestination = Screen.Main.route) {
|
PinLockScreen(navController = navController, appViewModel = appViewModel)
|
||||||
composable(
|
|
||||||
Screen.Main.route,
|
|
||||||
) {
|
|
||||||
MainScreen(
|
|
||||||
focusRequester = focusRequester,
|
|
||||||
appViewModel = appViewModel,
|
|
||||||
navController = navController,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
composable(
|
|
||||||
Screen.Settings.route,
|
|
||||||
) {
|
|
||||||
SettingsScreen(
|
|
||||||
appViewModel = appViewModel,
|
|
||||||
focusRequester = focusRequester,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
composable(
|
|
||||||
Screen.Support.route,
|
|
||||||
) {
|
|
||||||
SupportScreen(
|
|
||||||
focusRequester = focusRequester,
|
|
||||||
appViewModel = appViewModel,
|
|
||||||
navController = navController
|
|
||||||
)
|
|
||||||
}
|
|
||||||
composable(Screen.Support.Logs.route,) {
|
|
||||||
LogsScreen()
|
|
||||||
}
|
|
||||||
composable("${Screen.Config.route}/{id}") {
|
|
||||||
val id = it.arguments?.getString("id")
|
|
||||||
if (!id.isNullOrBlank()) {
|
|
||||||
ConfigScreen(
|
|
||||||
navController = navController,
|
|
||||||
id = id,
|
|
||||||
appViewModel = appViewModel,
|
|
||||||
focusRequester = focusRequester,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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")
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +0,0 @@
|
||||||
package com.zaneschepke.wireguardautotunnel.ui
|
|
||||||
|
|
||||||
data class SnackBarState(
|
|
||||||
val snackbarMessage: String = "",
|
|
||||||
val snackbarMessageConsumed: Boolean = true,
|
|
||||||
)
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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),
|
||||||
)
|
)
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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 ?: ""))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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,21 +246,24 @@ 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
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
floatingActionButtonPosition = FabPosition.End,
|
floatingActionButtonPosition = FabPosition.End,
|
||||||
floatingActionButton = {
|
floatingActionButton = {
|
||||||
AnimatedVisibility(
|
AnimatedVisibility(
|
||||||
|
@ -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(
|
||||||
modifier = Modifier
|
icon,
|
||||||
.padding(end = 10.dp)
|
icon.name,
|
||||||
.size(25.dp),
|
modifier = Modifier
|
||||||
tint =
|
.padding(end = 10.dp)
|
||||||
if (uiState.settings.isAutoTunnelPaused) Color.Gray
|
.size(25.dp),
|
||||||
else mint,
|
tint =
|
||||||
) },
|
if (uiState.settings.isAutoTunnelPaused) Color.Gray
|
||||||
|
else mint,
|
||||||
|
)
|
||||||
|
},
|
||||||
text = autoTunnelingLabel.text,
|
text = autoTunnelingLabel.text,
|
||||||
rowButton = {
|
rowButton = {
|
||||||
if (uiState.settings.isAutoTunnelPaused) {
|
if (uiState.settings.isAutoTunnelPaused) {
|
||||||
|
@ -457,7 +447,7 @@ fun MainScreen(
|
||||||
) {
|
) {
|
||||||
Text(stringResource(id = R.string.pause))
|
Text(stringResource(id = R.string.pause))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onClick = {},
|
onClick = {},
|
||||||
onHold = {},
|
onHold = {},
|
||||||
|
@ -473,7 +463,7 @@ fun MainScreen(
|
||||||
val leadingIconColor =
|
val leadingIconColor =
|
||||||
(if (
|
(if (
|
||||||
uiState.vpnState.name == tunnel.name &&
|
uiState.vpnState.name == tunnel.name &&
|
||||||
uiState.vpnState.status == Tunnel.State.UP
|
uiState.vpnState.status == Tunnel.State.UP
|
||||||
) {
|
) {
|
||||||
uiState.vpnState.statistics
|
uiState.vpnState.statistics
|
||||||
?.mapPeerStats()
|
?.mapPeerStats()
|
||||||
|
@ -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,11 +506,11 @@ 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) &&
|
||||||
(tunnel.name == uiState.vpnState.name)
|
(tunnel.name == uiState.vpnState.name)
|
||||||
) {
|
) {
|
||||||
appViewModel.showSnackbarMessage(Event.Message.TunnelOffAction.message)
|
appViewModel.showSnackbarMessage(Event.Message.TunnelOffAction.message)
|
||||||
return@RowListItem
|
return@RowListItem
|
||||||
|
@ -531,7 +522,7 @@ fun MainScreen(
|
||||||
if (!WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
if (!WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
||||||
if (
|
if (
|
||||||
uiState.vpnState.status == Tunnel.State.UP &&
|
uiState.vpnState.status == Tunnel.State.UP &&
|
||||||
(uiState.vpnState.name == tunnel.name)
|
(uiState.vpnState.name == tunnel.name)
|
||||||
) {
|
) {
|
||||||
expanded.value = !expanded.value
|
expanded.value = !expanded.value
|
||||||
}
|
}
|
||||||
|
@ -545,7 +536,7 @@ fun MainScreen(
|
||||||
rowButton = {
|
rowButton = {
|
||||||
if (
|
if (
|
||||||
tunnel.id == selectedTunnel?.id &&
|
tunnel.id == selectedTunnel?.id &&
|
||||||
!WireGuardAutoTunnel.isRunningOnAndroidTv()
|
!WireGuardAutoTunnel.isRunningOnAndroidTv()
|
||||||
) {
|
) {
|
||||||
Row {
|
Row {
|
||||||
if (!uiState.settings.isTunnelConfigDefault(tunnel)) {
|
if (!uiState.settings.isTunnelConfigDefault(tunnel)) {
|
||||||
|
@ -553,7 +544,7 @@ fun MainScreen(
|
||||||
onClick = {
|
onClick = {
|
||||||
if (
|
if (
|
||||||
uiState.settings.isAutoTunnelEnabled &&
|
uiState.settings.isAutoTunnelEnabled &&
|
||||||
!uiState.settings.isAutoTunnelPaused
|
!uiState.settings.isAutoTunnelPaused
|
||||||
) {
|
) {
|
||||||
appViewModel.showSnackbarMessage(
|
appViewModel.showSnackbarMessage(
|
||||||
Event.Message.AutoTunnelOffAction.message,
|
Event.Message.AutoTunnelOffAction.message,
|
||||||
|
@ -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,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -573,10 +565,10 @@ fun MainScreen(
|
||||||
onClick = {
|
onClick = {
|
||||||
if (
|
if (
|
||||||
uiState.settings.isAutoTunnelEnabled &&
|
uiState.settings.isAutoTunnelEnabled &&
|
||||||
uiState.settings.isTunnelConfigDefault(
|
uiState.settings.isTunnelConfigDefault(
|
||||||
tunnel,
|
tunnel,
|
||||||
) &&
|
) &&
|
||||||
!uiState.settings.isAutoTunnelPaused
|
!uiState.settings.isAutoTunnelPaused
|
||||||
) {
|
) {
|
||||||
appViewModel.showSnackbarMessage(
|
appViewModel.showSnackbarMessage(
|
||||||
Event.Message.AutoTunnelOffAction.message,
|
Event.Message.AutoTunnelOffAction.message,
|
||||||
|
@ -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,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -641,26 +643,27 @@ fun MainScreen(
|
||||||
onClick = {
|
onClick = {
|
||||||
if (
|
if (
|
||||||
uiState.vpnState.status == Tunnel.State.UP &&
|
uiState.vpnState.status == Tunnel.State.UP &&
|
||||||
(uiState.vpnState.name == tunnel.name)
|
(uiState.vpnState.name == tunnel.name)
|
||||||
) {
|
) {
|
||||||
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 = {
|
||||||
if (
|
if (
|
||||||
uiState.vpnState.status == Tunnel.State.UP &&
|
uiState.vpnState.status == Tunnel.State.UP &&
|
||||||
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,25 +672,34 @@ 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 = {
|
||||||
if (
|
if (
|
||||||
uiState.vpnState.status == Tunnel.State.UP &&
|
uiState.vpnState.status == Tunnel.State.UP &&
|
||||||
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()
|
||||||
|
|
|
@ -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) {
|
val settings = settingsRepository.getSettings()
|
||||||
|
val isDefault = settings.isTunnelConfigDefault(tunnel)
|
||||||
|
if (tunnelConfigRepository.count() == 1 || isDefault) {
|
||||||
stopWatcherService()
|
stopWatcherService()
|
||||||
val settings = settingsRepository.getSettings()
|
|
||||||
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))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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))
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
|
@ -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()) {
|
||||||
viewModel.toggleAutoTunnel()
|
if (appViewModel.isRequiredPermissionGranted()) {
|
||||||
|
viewModel.toggleAutoTunnel()
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
requestBatteryOptimizationsDisabled()
|
requestBatteryOptimizationsDisabled()
|
||||||
}
|
}
|
||||||
|
@ -202,7 +199,7 @@ fun SettingsScreen(
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||||
if (
|
if (
|
||||||
WireGuardAutoTunnel.isRunningOnAndroidTv() &&
|
WireGuardAutoTunnel.isRunningOnAndroidTv() &&
|
||||||
Build.VERSION.SDK_INT == Build.VERSION_CODES.Q
|
Build.VERSION.SDK_INT == Build.VERSION_CODES.Q
|
||||||
) {
|
) {
|
||||||
checkFineLocationGranted()
|
checkFineLocationGranted()
|
||||||
} else {
|
} else {
|
||||||
|
@ -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),
|
||||||
|
@ -270,11 +271,15 @@ fun SettingsScreen(
|
||||||
)
|
)
|
||||||
Row(
|
Row(
|
||||||
modifier =
|
modifier =
|
||||||
if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
||||||
Modifier.fillMaxWidth().padding(10.dp)
|
Modifier
|
||||||
} else {
|
.fillMaxWidth()
|
||||||
Modifier.fillMaxWidth().padding(30.dp)
|
.padding(10.dp)
|
||||||
},
|
} else {
|
||||||
|
Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(30.dp)
|
||||||
|
},
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
horizontalArrangement = Arrangement.SpaceEvenly,
|
horizontalArrangement = Arrangement.SpaceEvenly,
|
||||||
) {
|
) {
|
||||||
|
@ -330,12 +335,15 @@ fun SettingsScreen(
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
verticalArrangement = Arrangement.Top,
|
verticalArrangement = Arrangement.Top,
|
||||||
modifier =
|
modifier =
|
||||||
Modifier.fillMaxSize().verticalScroll(scrollState).clickable(
|
Modifier
|
||||||
indication = null,
|
.fillMaxSize()
|
||||||
interactionSource = interactionSource,
|
.verticalScroll(scrollState)
|
||||||
) {
|
.clickable(
|
||||||
focusManager.clearFocus()
|
indication = null,
|
||||||
},
|
interactionSource = interactionSource,
|
||||||
|
) {
|
||||||
|
focusManager.clearFocus()
|
||||||
|
},
|
||||||
) {
|
) {
|
||||||
Surface(
|
Surface(
|
||||||
tonalElevation = 2.dp,
|
tonalElevation = 2.dp,
|
||||||
|
@ -343,14 +351,17 @@ fun SettingsScreen(
|
||||||
shape = RoundedCornerShape(12.dp),
|
shape = RoundedCornerShape(12.dp),
|
||||||
color = MaterialTheme.colorScheme.surface,
|
color = MaterialTheme.colorScheme.surface,
|
||||||
modifier =
|
modifier =
|
||||||
(if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
(if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
||||||
Modifier.height(IntrinsicSize.Min)
|
Modifier
|
||||||
.fillMaxWidth(fillMaxWidth)
|
.height(IntrinsicSize.Min)
|
||||||
.padding(top = 10.dp)
|
.fillMaxWidth(fillMaxWidth)
|
||||||
} else {
|
.padding(top = 10.dp)
|
||||||
Modifier.fillMaxWidth(fillMaxWidth).padding(top = 20.dp)
|
} else {
|
||||||
})
|
Modifier
|
||||||
.padding(bottom = 10.dp),
|
.fillMaxWidth(fillMaxWidth)
|
||||||
|
.padding(top = 20.dp)
|
||||||
|
})
|
||||||
|
.padding(bottom = 10.dp),
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
horizontalAlignment = Alignment.Start,
|
horizontalAlignment = Alignment.Start,
|
||||||
|
@ -364,38 +375,43 @@ fun SettingsScreen(
|
||||||
ConfigurationToggle(
|
ConfigurationToggle(
|
||||||
stringResource(id = R.string.tunnel_on_wifi),
|
stringResource(id = R.string.tunnel_on_wifi),
|
||||||
enabled =
|
enabled =
|
||||||
!(uiState.settings.isAutoTunnelEnabled ||
|
!(uiState.settings.isAutoTunnelEnabled ||
|
||||||
uiState.settings.isAlwaysOnVpnEnabled),
|
uiState.settings.isAlwaysOnVpnEnabled),
|
||||||
checked = uiState.settings.isTunnelOnWifiEnabled,
|
checked = uiState.settings.isTunnelOnWifiEnabled,
|
||||||
padding = screenPadding,
|
padding = screenPadding,
|
||||||
onCheckChanged = { viewModel.onToggleTunnelOnWifi() },
|
onCheckChanged = { viewModel.onToggleTunnelOnWifi() },
|
||||||
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 =
|
||||||
!(uiState.settings.isAutoTunnelEnabled ||
|
!(uiState.settings.isAutoTunnelEnabled ||
|
||||||
uiState.settings.isAlwaysOnVpnEnabled),
|
uiState.settings.isAlwaysOnVpnEnabled),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (uiState.settings.trustedNetworkSSIDs.isEmpty()) {
|
if (uiState.settings.trustedNetworkSSIDs.isEmpty()) {
|
||||||
|
@ -408,24 +424,24 @@ fun SettingsScreen(
|
||||||
}
|
}
|
||||||
OutlinedTextField(
|
OutlinedTextField(
|
||||||
enabled =
|
enabled =
|
||||||
!(uiState.settings.isAutoTunnelEnabled ||
|
!(uiState.settings.isAutoTunnelEnabled ||
|
||||||
uiState.settings.isAlwaysOnVpnEnabled),
|
uiState.settings.isAlwaysOnVpnEnabled),
|
||||||
value = currentText,
|
value = currentText,
|
||||||
onValueChange = { currentText = it },
|
onValueChange = { currentText = it },
|
||||||
label = { Text(stringResource(R.string.add_trusted_ssid)) },
|
label = { Text(stringResource(R.string.add_trusted_ssid)) },
|
||||||
modifier =
|
modifier =
|
||||||
Modifier.padding(
|
Modifier
|
||||||
start = screenPadding,
|
.padding(
|
||||||
top = 5.dp,
|
start = screenPadding,
|
||||||
bottom = 10.dp,
|
top = 5.dp,
|
||||||
)
|
bottom = 10.dp,
|
||||||
.focusRequester(focusRequester2),
|
),
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
keyboardOptions =
|
keyboardOptions =
|
||||||
KeyboardOptions(
|
KeyboardOptions(
|
||||||
capitalization = KeyboardCapitalization.None,
|
capitalization = KeyboardCapitalization.None,
|
||||||
imeAction = ImeAction.Done,
|
imeAction = ImeAction.Done,
|
||||||
),
|
),
|
||||||
keyboardActions = KeyboardActions(onDone = { saveTrustedSSID() }),
|
keyboardActions = KeyboardActions(onDone = { saveTrustedSSID() }),
|
||||||
trailingIcon = {
|
trailingIcon = {
|
||||||
if (currentText != "") {
|
if (currentText != "") {
|
||||||
|
@ -433,19 +449,19 @@ fun SettingsScreen(
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Outlined.Add,
|
imageVector = Icons.Outlined.Add,
|
||||||
contentDescription =
|
contentDescription =
|
||||||
if (currentText == "") {
|
if (currentText == "") {
|
||||||
stringResource(
|
stringResource(
|
||||||
id =
|
id =
|
||||||
R.string
|
R.string
|
||||||
.trusted_ssid_empty_description,
|
.trusted_ssid_empty_description,
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
stringResource(
|
stringResource(
|
||||||
id =
|
id =
|
||||||
R.string
|
R.string
|
||||||
.trusted_ssid_value_description,
|
.trusted_ssid_value_description,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
tint = MaterialTheme.colorScheme.primary,
|
tint = MaterialTheme.colorScheme.primary,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -457,8 +473,8 @@ fun SettingsScreen(
|
||||||
ConfigurationToggle(
|
ConfigurationToggle(
|
||||||
stringResource(R.string.tunnel_mobile_data),
|
stringResource(R.string.tunnel_mobile_data),
|
||||||
enabled =
|
enabled =
|
||||||
!(uiState.settings.isAutoTunnelEnabled ||
|
!(uiState.settings.isAutoTunnelEnabled ||
|
||||||
uiState.settings.isAlwaysOnVpnEnabled),
|
uiState.settings.isAlwaysOnVpnEnabled),
|
||||||
checked = uiState.settings.isTunnelOnMobileDataEnabled,
|
checked = uiState.settings.isTunnelOnMobileDataEnabled,
|
||||||
padding = screenPadding,
|
padding = screenPadding,
|
||||||
onCheckChanged = { viewModel.onToggleTunnelOnMobileData() },
|
onCheckChanged = { viewModel.onToggleTunnelOnMobileData() },
|
||||||
|
@ -466,29 +482,29 @@ fun SettingsScreen(
|
||||||
ConfigurationToggle(
|
ConfigurationToggle(
|
||||||
stringResource(id = R.string.tunnel_on_ethernet),
|
stringResource(id = R.string.tunnel_on_ethernet),
|
||||||
enabled =
|
enabled =
|
||||||
!(uiState.settings.isAutoTunnelEnabled ||
|
!(uiState.settings.isAutoTunnelEnabled ||
|
||||||
uiState.settings.isAlwaysOnVpnEnabled),
|
uiState.settings.isAlwaysOnVpnEnabled),
|
||||||
checked = uiState.settings.isTunnelOnEthernetEnabled,
|
checked = uiState.settings.isTunnelOnEthernetEnabled,
|
||||||
padding = screenPadding,
|
padding = screenPadding,
|
||||||
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,
|
||||||
modifier =
|
modifier =
|
||||||
(if (!uiState.settings.isAutoTunnelEnabled) Modifier
|
(if (!uiState.settings.isAutoTunnelEnabled) Modifier
|
||||||
else
|
else
|
||||||
Modifier.focusRequester(
|
Modifier.focusRequester(
|
||||||
focusRequester,
|
focusRequester,
|
||||||
))
|
))
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.padding(top = 5.dp),
|
.padding(top = 5.dp),
|
||||||
horizontalArrangement = Arrangement.Center,
|
horizontalArrangement = Arrangement.Center,
|
||||||
|
@ -498,19 +514,22 @@ fun SettingsScreen(
|
||||||
onClick = {
|
onClick = {
|
||||||
if (
|
if (
|
||||||
uiState.settings.isTunnelOnWifiEnabled &&
|
uiState.settings.isTunnelOnWifiEnabled &&
|
||||||
!uiState.settings.isAutoTunnelEnabled
|
!uiState.settings.isAutoTunnelEnabled
|
||||||
) {
|
) {
|
||||||
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,
|
||||||
|
@ -551,9 +572,9 @@ fun SettingsScreen(
|
||||||
ConfigurationToggle(
|
ConfigurationToggle(
|
||||||
stringResource(R.string.use_kernel),
|
stringResource(R.string.use_kernel),
|
||||||
enabled =
|
enabled =
|
||||||
!(uiState.settings.isAutoTunnelEnabled ||
|
!(uiState.settings.isAutoTunnelEnabled ||
|
||||||
uiState.settings.isAlwaysOnVpnEnabled ||
|
uiState.settings.isAlwaysOnVpnEnabled ||
|
||||||
(uiState.vpnState.status == Tunnel.State.UP)),
|
(uiState.vpnState.status == Tunnel.State.UP)),
|
||||||
checked = uiState.settings.isKernelEnabled,
|
checked = uiState.settings.isKernelEnabled,
|
||||||
padding = screenPadding,
|
padding = screenPadding,
|
||||||
onCheckChanged = {
|
onCheckChanged = {
|
||||||
|
@ -568,26 +589,27 @@ 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
|
||||||
Modifier.fillMaxWidth(fillMaxWidth)
|
.fillMaxWidth(fillMaxWidth)
|
||||||
.padding(vertical = 10.dp)
|
.padding(vertical = 10.dp)
|
||||||
.padding(bottom = 140.dp),
|
.padding(bottom = 140.dp),
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
horizontalAlignment = Alignment.Start,
|
||||||
|
verticalArrangement = Arrangement.Top,
|
||||||
|
modifier = Modifier.padding(15.dp),
|
||||||
) {
|
) {
|
||||||
Column(
|
SectionTitle(
|
||||||
horizontalAlignment = Alignment.Start,
|
title = stringResource(id = R.string.other),
|
||||||
verticalArrangement = Arrangement.Top,
|
padding = screenPadding,
|
||||||
modifier = Modifier.padding(15.dp),
|
)
|
||||||
) {
|
if(!WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
||||||
SectionTitle(
|
|
||||||
title = stringResource(id = R.string.other),
|
|
||||||
padding = screenPadding,
|
|
||||||
)
|
|
||||||
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))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
|
||||||
)
|
)
|
||||||
|
|
|
@ -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,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,32 +224,34 @@ fun SupportScreen(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
HorizontalDivider(
|
if(!WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
||||||
thickness = 0.5.dp,
|
HorizontalDivider(
|
||||||
color = MaterialTheme.colorScheme.onBackground
|
thickness = 0.5.dp,
|
||||||
)
|
color = MaterialTheme.colorScheme.onBackground
|
||||||
TextButton(
|
)
|
||||||
onClick = { navController.navigate(Screen.Support.Logs.route) },
|
TextButton(
|
||||||
modifier = Modifier.padding(vertical = 5.dp),
|
onClick = { navController.navigate(Screen.Support.Logs.route) },
|
||||||
) {
|
modifier = Modifier.padding(vertical = 5.dp),
|
||||||
Row(
|
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
) {
|
) {
|
||||||
Row {
|
Row(
|
||||||
val icon = Icons.Rounded.FormatListNumbered
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
Icon(icon, icon.name)
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
Text(
|
modifier = Modifier.fillMaxWidth(),
|
||||||
stringResource(id = R.string.read_logs),
|
) {
|
||||||
textAlign = TextAlign.Justify,
|
Row {
|
||||||
modifier = Modifier.padding(start = 10.dp),
|
val icon = Icons.Rounded.FormatListNumbered
|
||||||
|
Icon(icon, icon.name)
|
||||||
|
Text(
|
||||||
|
stringResource(id = R.string.read_logs),
|
||||||
|
textAlign = TextAlign.Justify,
|
||||||
|
modifier = Modifier.padding(start = 10.dp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Icon(
|
||||||
|
Icons.AutoMirrored.Rounded.ArrowForward,
|
||||||
|
stringResource(id = R.string.go)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
Icon(
|
|
||||||
Icons.AutoMirrored.Rounded.ArrowForward,
|
|
||||||
stringResource(id = R.string.go)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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())
|
||||||
|
|
|
@ -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),
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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()
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -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
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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? {
|
||||||
|
|
|
@ -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>
|
|
@ -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"
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
What's new:
|
||||||
|
- Add logs screen
|
||||||
|
- Add local app lock
|
||||||
|
- Add restart vpn on failed ping
|
||||||
|
- Various bug fixes
|
|
@ -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" }
|
||||||
|
|
|
@ -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()
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue