feat: add amnezia side-by-side

This commit is contained in:
Zane Schepke 2024-05-05 00:45:01 -04:00
parent 681b066d99
commit e84d7e354c
59 changed files with 1172 additions and 309 deletions

View File

@ -1,4 +1,3 @@
# name of the workflow
name: Android CI Tag Deployment (Pre-release) name: Android CI Tag Deployment (Pre-release)
on: on:

View File

@ -162,6 +162,7 @@ dependencies {
// get tunnel lib from github packages or mavenLocal // get tunnel lib from github packages or mavenLocal
implementation(libs.tunnel) implementation(libs.tunnel)
implementation(libs.amneziawg.android)
coreLibraryDesugaring(libs.desugar.jdk.libs) coreLibraryDesugaring(libs.desugar.jdk.libs)
// logging // logging

View File

@ -0,0 +1,190 @@
{
"formatVersion": 1,
"database": {
"version": 8,
"identityHash": "b4d4a7c489f6b2f0d3aa4fa6f37b4935",
"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, `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_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, `is_amnezia_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": "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": "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"
},
{
"fieldPath": "isAmneziaEnabled",
"columnName": "is_amnezia_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, `tunnel_networks` TEXT NOT NULL DEFAULT '', `is_mobile_data_tunnel` INTEGER NOT NULL DEFAULT false, `is_primary_tunnel` INTEGER NOT NULL DEFAULT false, `am_quick` TEXT NOT NULL DEFAULT '')",
"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
},
{
"fieldPath": "tunnelNetworks",
"columnName": "tunnel_networks",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "''"
},
{
"fieldPath": "isMobileDataTunnel",
"columnName": "is_mobile_data_tunnel",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isPrimaryTunnel",
"columnName": "is_primary_tunnel",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "amQuick",
"columnName": "am_quick",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "''"
}
],
"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, 'b4d4a7c489f6b2f0d3aa4fa6f37b4935')"
]
}
}

View File

@ -9,6 +9,8 @@ import com.zaneschepke.wireguardautotunnel.service.tile.AutoTunnelControlTile
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 kotlinx.coroutines.MainScope
import kotlinx.coroutines.cancel
import timber.log.Timber import timber.log.Timber
import xyz.teamgravity.pin_lock_compose.PinManager import xyz.teamgravity.pin_lock_compose.PinManager
@ -21,7 +23,16 @@ class WireGuardAutoTunnel : Application() {
PinManager.initialize(this) PinManager.initialize(this)
} }
override fun onLowMemory() {
super.onLowMemory()
applicationScope.cancel("onLowMemory() called by system")
applicationScope = MainScope()
}
companion object { companion object {
var applicationScope = MainScope()
lateinit var instance: WireGuardAutoTunnel lateinit var instance: WireGuardAutoTunnel
private set private set

View File

@ -6,12 +6,12 @@ import androidx.room.DeleteColumn
import androidx.room.RoomDatabase import androidx.room.RoomDatabase
import androidx.room.TypeConverters import androidx.room.TypeConverters
import androidx.room.migration.AutoMigrationSpec import androidx.room.migration.AutoMigrationSpec
import com.zaneschepke.wireguardautotunnel.data.model.Settings import com.zaneschepke.wireguardautotunnel.data.domain.Settings
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
@Database( @Database(
entities = [Settings::class, TunnelConfig::class], entities = [Settings::class, TunnelConfig::class],
version = 7, version = 8,
autoMigrations = autoMigrations =
[ [
AutoMigration(from = 1, to = 2), AutoMigration(from = 1, to = 2),
@ -33,6 +33,7 @@ import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
to = 7, to = 7,
spec = RemoveLegacySettingColumnsMigration::class, spec = RemoveLegacySettingColumnsMigration::class,
), ),
AutoMigration(7, 8)
], ],
exportSchema = true, exportSchema = true,
) )

View File

@ -5,7 +5,7 @@ import androidx.room.Delete
import androidx.room.Insert import androidx.room.Insert
import androidx.room.OnConflictStrategy import androidx.room.OnConflictStrategy
import androidx.room.Query import androidx.room.Query
import com.zaneschepke.wireguardautotunnel.data.model.Settings import com.zaneschepke.wireguardautotunnel.data.domain.Settings
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
@Dao @Dao

View File

@ -5,7 +5,7 @@ import androidx.room.Delete
import androidx.room.Insert import androidx.room.Insert
import androidx.room.OnConflictStrategy import androidx.room.OnConflictStrategy
import androidx.room.Query import androidx.room.Query
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
import com.zaneschepke.wireguardautotunnel.util.TunnelConfigs import com.zaneschepke.wireguardautotunnel.util.TunnelConfigs
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow

View File

@ -1,4 +1,4 @@
package com.zaneschepke.wireguardautotunnel.data.model package com.zaneschepke.wireguardautotunnel.data.domain
data class GeneralState( data class GeneralState(
val locationDisclosureShown: Boolean = LOCATION_DISCLOSURE_SHOWN_DEFAULT, val locationDisclosureShown: Boolean = LOCATION_DISCLOSURE_SHOWN_DEFAULT,

View File

@ -1,4 +1,4 @@
package com.zaneschepke.wireguardautotunnel.data.model package com.zaneschepke.wireguardautotunnel.data.domain
import androidx.room.ColumnInfo import androidx.room.ColumnInfo
import androidx.room.Entity import androidx.room.Entity
@ -50,4 +50,9 @@ data class Settings(
defaultValue = "false", defaultValue = "false",
) )
val isPingEnabled: Boolean = false, val isPingEnabled: Boolean = false,
@ColumnInfo(
name = "is_amnezia_enabled",
defaultValue = "false",
)
val isAmneziaEnabled: Boolean = false,
) )

View File

@ -1,4 +1,4 @@
package com.zaneschepke.wireguardautotunnel.data.model package com.zaneschepke.wireguardautotunnel.data.domain
import androidx.room.ColumnInfo import androidx.room.ColumnInfo
import androidx.room.Entity import androidx.room.Entity
@ -27,12 +27,24 @@ data class TunnelConfig(
defaultValue = "false", defaultValue = "false",
) )
val isPrimaryTunnel: Boolean = false, val isPrimaryTunnel: Boolean = false,
@ColumnInfo(
name = "am_quick",
defaultValue = "",
)
val amQuick: String = "",
) { ) {
companion object { companion object {
fun configFromQuick(wgQuick: String): Config { fun configFromWgQuick(wgQuick: String): Config {
val inputStream: InputStream = wgQuick.byteInputStream() val inputStream: InputStream = wgQuick.byteInputStream()
val reader = inputStream.bufferedReader(Charsets.UTF_8) return inputStream.bufferedReader(Charsets.UTF_8).use {
return Config.parse(reader) Config.parse(it)
}
}
fun configFromAmQuick(amQuick: String) : org.amnezia.awg.config.Config {
val inputStream: InputStream = amQuick.byteInputStream()
return inputStream.bufferedReader(Charsets.UTF_8).use {
org.amnezia.awg.config.Config.parse(it)
}
} }
} }
} }

View File

@ -1,6 +1,6 @@
package com.zaneschepke.wireguardautotunnel.data.repository package com.zaneschepke.wireguardautotunnel.data.repository
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
interface AppDataRepository { interface AppDataRepository {
suspend fun getPrimaryOrFirstTunnel(): TunnelConfig? suspend fun getPrimaryOrFirstTunnel(): TunnelConfig?

View File

@ -1,6 +1,6 @@
package com.zaneschepke.wireguardautotunnel.data.repository package com.zaneschepke.wireguardautotunnel.data.repository
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
import javax.inject.Inject import javax.inject.Inject
class AppDataRoomRepository @Inject constructor( class AppDataRoomRepository @Inject constructor(

View File

@ -1,6 +1,6 @@
package com.zaneschepke.wireguardautotunnel.data.repository package com.zaneschepke.wireguardautotunnel.data.repository
import com.zaneschepke.wireguardautotunnel.data.model.GeneralState import com.zaneschepke.wireguardautotunnel.data.domain.GeneralState
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
interface AppStateRepository { interface AppStateRepository {

View File

@ -1,7 +1,7 @@
package com.zaneschepke.wireguardautotunnel.data.repository package com.zaneschepke.wireguardautotunnel.data.repository
import com.zaneschepke.wireguardautotunnel.data.datastore.DataStoreManager import com.zaneschepke.wireguardautotunnel.data.datastore.DataStoreManager
import com.zaneschepke.wireguardautotunnel.data.model.GeneralState import com.zaneschepke.wireguardautotunnel.data.domain.GeneralState
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import timber.log.Timber import timber.log.Timber

View File

@ -1,7 +1,7 @@
package com.zaneschepke.wireguardautotunnel.data.repository package com.zaneschepke.wireguardautotunnel.data.repository
import com.zaneschepke.wireguardautotunnel.data.SettingsDao import com.zaneschepke.wireguardautotunnel.data.SettingsDao
import com.zaneschepke.wireguardautotunnel.data.model.Settings import com.zaneschepke.wireguardautotunnel.data.domain.Settings
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
class RoomSettingsRepository(private val settingsDoa: SettingsDao) : SettingsRepository { class RoomSettingsRepository(private val settingsDoa: SettingsDao) : SettingsRepository {

View File

@ -1,7 +1,7 @@
package com.zaneschepke.wireguardautotunnel.data.repository package com.zaneschepke.wireguardautotunnel.data.repository
import com.zaneschepke.wireguardautotunnel.data.TunnelConfigDao import com.zaneschepke.wireguardautotunnel.data.TunnelConfigDao
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
import com.zaneschepke.wireguardautotunnel.util.TunnelConfigs import com.zaneschepke.wireguardautotunnel.util.TunnelConfigs
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow

View File

@ -1,6 +1,6 @@
package com.zaneschepke.wireguardautotunnel.data.repository package com.zaneschepke.wireguardautotunnel.data.repository
import com.zaneschepke.wireguardautotunnel.data.model.Settings import com.zaneschepke.wireguardautotunnel.data.domain.Settings
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
interface SettingsRepository { interface SettingsRepository {

View File

@ -1,6 +1,6 @@
package com.zaneschepke.wireguardautotunnel.data.repository package com.zaneschepke.wireguardautotunnel.data.repository
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
import com.zaneschepke.wireguardautotunnel.util.TunnelConfigs import com.zaneschepke.wireguardautotunnel.util.TunnelConfigs
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow

View File

@ -40,14 +40,21 @@ class TunnelModule {
return WgQuickBackend(context, rootShell, ToolsInstaller(context, rootShell)) return WgQuickBackend(context, rootShell, ToolsInstaller(context, rootShell))
} }
@Provides
@Singleton
fun provideAmneziaBackend(@ApplicationContext context: Context) : org.amnezia.awg.backend.Backend {
return org.amnezia.awg.backend.GoBackend(context)
}
@Provides @Provides
@Singleton @Singleton
fun provideVpnService( fun provideVpnService(
amneziaBackend: org.amnezia.awg.backend.Backend,
@Userspace userspaceBackend: Backend, @Userspace userspaceBackend: Backend,
@Kernel kernelBackend: Backend, @Kernel kernelBackend: Backend,
appDataRepository: AppDataRepository appDataRepository: AppDataRepository
): VpnService { ): VpnService {
return WireGuardTunnel(userspaceBackend, kernelBackend, appDataRepository) return WireGuardTunnel(amneziaBackend,userspaceBackend, kernelBackend, appDataRepository)
} }
@Provides @Provides

View File

@ -1,20 +1,20 @@
package com.zaneschepke.wireguardautotunnel.service.foreground package com.zaneschepke.wireguardautotunnel.service.foreground
import com.wireguard.android.backend.Tunnel import com.zaneschepke.wireguardautotunnel.data.domain.Settings
import com.zaneschepke.wireguardautotunnel.data.model.Settings import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelState
data class WatcherState( data class WatcherState(
val isWifiConnected: Boolean = false, val isWifiConnected: Boolean = false,
val config: TunnelConfig? = null, val config: TunnelConfig? = null,
val vpnStatus: Tunnel.State = Tunnel.State.DOWN, val vpnStatus: TunnelState = TunnelState.DOWN,
val isEthernetConnected: Boolean = false, val isEthernetConnected: Boolean = false,
val isMobileDataConnected: Boolean = false, val isMobileDataConnected: Boolean = false,
val currentNetworkSSID: String = "", val currentNetworkSSID: String = "",
val settings: Settings = Settings() val settings: Settings = Settings()
) { ) {
private fun isVpnConnected() = vpnStatus == Tunnel.State.UP private fun isVpnConnected() = vpnStatus == TunnelState.UP
fun isEthernetConditionMet(): Boolean { fun isEthernetConditionMet(): Boolean {
return (isEthernetConnected && return (isEthernetConnected &&
settings.isTunnelOnEthernetEnabled && settings.isTunnelOnEthernetEnabled &&

View File

@ -5,9 +5,8 @@ import android.os.Bundle
import android.os.PowerManager import android.os.PowerManager
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.zaneschepke.wireguardautotunnel.R import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
import com.zaneschepke.wireguardautotunnel.service.network.EthernetService import com.zaneschepke.wireguardautotunnel.service.network.EthernetService
import com.zaneschepke.wireguardautotunnel.service.network.MobileDataService import com.zaneschepke.wireguardautotunnel.service.network.MobileDataService
@ -15,6 +14,7 @@ import com.zaneschepke.wireguardautotunnel.service.network.NetworkService
import com.zaneschepke.wireguardautotunnel.service.network.NetworkStatus import com.zaneschepke.wireguardautotunnel.service.network.NetworkStatus
import com.zaneschepke.wireguardautotunnel.service.network.WifiService import com.zaneschepke.wireguardautotunnel.service.network.WifiService
import com.zaneschepke.wireguardautotunnel.service.notification.NotificationService import com.zaneschepke.wireguardautotunnel.service.notification.NotificationService
import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelState
import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnService import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnService
import com.zaneschepke.wireguardautotunnel.util.Constants import com.zaneschepke.wireguardautotunnel.util.Constants
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
@ -217,10 +217,10 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
private suspend fun watchForPingFailure() { private suspend fun watchForPingFailure() {
try { try {
do { do {
if (vpnService.vpnState.value.status == Tunnel.State.UP) { if (vpnService.vpnState.value.status == TunnelState.UP) {
val tunnelConfig = vpnService.vpnState.value.tunnelConfig val tunnelConfig = vpnService.vpnState.value.tunnelConfig
tunnelConfig?.let { tunnelConfig?.let {
val config = TunnelConfig.configFromQuick(it.wgQuick) val config = TunnelConfig.configFromWgQuick(it.wgQuick)
val results = config.peers.map { peer -> val results = config.peers.map { peer ->
val host = if (peer.endpoint.isPresent && val host = if (peer.endpoint.isPresent &&
peer.endpoint.get().resolved.isPresent) peer.endpoint.get().resolved.isPresent)
@ -321,14 +321,14 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
isWifiConnected = true, isWifiConnected = true,
) )
val ssid = wifiService.getNetworkName(it.networkCapabilities) val ssid = wifiService.getNetworkName(it.networkCapabilities)
ssid?.let { ssid?.let { name ->
if(it.contains(Constants.UNREADABLE_SSID)) { if(name.contains(Constants.UNREADABLE_SSID)) {
Timber.w("SSID unreadable: missing permissions") Timber.w("SSID unreadable: missing permissions")
} else Timber.i("Detected valid SSID") } else Timber.i("Detected valid SSID")
appDataRepository.appState.setCurrentSsid(ssid) appDataRepository.appState.setCurrentSsid(name)
networkEventsFlow.value = networkEventsFlow.value =
networkEventsFlow.value.copy( networkEventsFlow.value.copy(
currentNetworkSSID = ssid, currentNetworkSSID = name,
) )
} ?: Timber.w("Failed to read ssid") } ?: Timber.w("Failed to read ssid")
} }

View File

@ -11,6 +11,8 @@ import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
import com.zaneschepke.wireguardautotunnel.receiver.NotificationActionReceiver import com.zaneschepke.wireguardautotunnel.receiver.NotificationActionReceiver
import com.zaneschepke.wireguardautotunnel.service.notification.NotificationService import com.zaneschepke.wireguardautotunnel.service.notification.NotificationService
import com.zaneschepke.wireguardautotunnel.service.tunnel.HandshakeStatus import com.zaneschepke.wireguardautotunnel.service.tunnel.HandshakeStatus
import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelState
import com.zaneschepke.wireguardautotunnel.service.tunnel.statistics.TunnelStatistics
import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnService import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnService
import com.zaneschepke.wireguardautotunnel.util.Constants import com.zaneschepke.wireguardautotunnel.util.Constants
import com.zaneschepke.wireguardautotunnel.util.handshakeStatus import com.zaneschepke.wireguardautotunnel.util.handshakeStatus
@ -58,7 +60,7 @@ class WireGuardTunnelService : ForegroundService() {
lifecycleScope.launch(Dispatchers.IO) { lifecycleScope.launch(Dispatchers.IO) {
launch { launch {
val tunnelId = extras?.getInt(Constants.TUNNEL_EXTRA_KEY) val tunnelId = extras?.getInt(Constants.TUNNEL_EXTRA_KEY)
if (vpnService.getState() == Tunnel.State.UP) { if (vpnService.getState() == TunnelState.UP) {
vpnService.stopTunnel() vpnService.stopTunnel()
} }
vpnService.startTunnel( vpnService.startTunnel(
@ -77,7 +79,7 @@ class WireGuardTunnelService : ForegroundService() {
private suspend fun handshakeNotifications() { private suspend fun handshakeNotifications() {
var tunnelName: String? = null var tunnelName: String? = null
vpnService.vpnState.collect { state -> vpnService.vpnState.collect { state ->
state.statistics state.statistics
?.mapPeerStats() ?.mapPeerStats()
?.map { it.value?.handshakeStatus() } ?.map { it.value?.handshakeStatus() }
.let { statuses -> .let { statuses ->
@ -102,7 +104,7 @@ class WireGuardTunnelService : ForegroundService() {
else -> {} else -> {}
} }
} }
if (state.status == Tunnel.State.UP && state.tunnelConfig?.name != tunnelName) { if (state.status == TunnelState.UP && state.tunnelConfig?.name != tunnelName) {
tunnelName = state.tunnelConfig?.name tunnelName = state.tunnelConfig?.name
launchVpnNotification( launchVpnNotification(
getString(R.string.tunnel_start_title), getString(R.string.tunnel_start_title),

View File

@ -2,7 +2,7 @@ package com.zaneschepke.wireguardautotunnel.service.shortcut
import android.os.Bundle import android.os.Bundle
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.lifecycle.lifecycleScope import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
import com.zaneschepke.wireguardautotunnel.service.foreground.Action import com.zaneschepke.wireguardautotunnel.service.foreground.Action
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
@ -24,7 +24,7 @@ class ShortcutsActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
lifecycleScope.launch(Dispatchers.Main) { WireGuardAutoTunnel.applicationScope.launch(Dispatchers.IO) {
val settings = appDataRepository.settings.getSettings() val settings = appDataRepository.settings.getSettings()
if (settings.isShortcutsEnabled) { if (settings.isShortcutsEnabled) {
when (intent.getStringExtra(CLASS_NAME_EXTRA_KEY)) { when (intent.getStringExtra(CLASS_NAME_EXTRA_KEY)) {

View File

@ -4,7 +4,7 @@ import android.os.Build
import android.service.quicksettings.Tile import android.service.quicksettings.Tile
import android.service.quicksettings.TileService import android.service.quicksettings.TileService
import com.zaneschepke.wireguardautotunnel.R import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint

View File

@ -3,10 +3,10 @@ package com.zaneschepke.wireguardautotunnel.service.tile
import android.os.Build import android.os.Build
import android.service.quicksettings.Tile import android.service.quicksettings.Tile
import android.service.quicksettings.TileService import android.service.quicksettings.TileService
import com.wireguard.android.backend.Tunnel import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelState
import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnService import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnService
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
@ -38,12 +38,12 @@ class TunnelControlTile : TileService() {
scope.launch { scope.launch {
vpnService.vpnState.collect { it -> vpnService.vpnState.collect { it ->
when (it.status) { when (it.status) {
Tunnel.State.UP -> { TunnelState.UP -> {
setActive() setActive()
it.tunnelConfig?.name?.let { name -> setTileDescription(name) } it.tunnelConfig?.name?.let { name -> setTileDescription(name) }
} }
Tunnel.State.DOWN -> { TunnelState.DOWN -> {
setInactive() setInactive()
val config = appDataRepository.getStartTunnelConfig()?.also { config -> val config = appDataRepository.getStartTunnelConfig()?.also { config ->
manualStartConfig = config manualStartConfig = config
@ -79,7 +79,7 @@ class TunnelControlTile : TileService() {
unlockAndRun { unlockAndRun {
scope.launch { scope.launch {
try { try {
if (vpnService.getState() == Tunnel.State.UP) { if (vpnService.getState() == TunnelState.UP) {
serviceManager.stopVpnServiceForeground( serviceManager.stopVpnServiceForeground(
this@TunnelControlTile, this@TunnelControlTile,
isManualStop = true, isManualStop = true,

View File

@ -0,0 +1,42 @@
package com.zaneschepke.wireguardautotunnel.service.tunnel
import com.wireguard.android.backend.Tunnel
enum class TunnelState {
UP,
DOWN,
TOGGLE;
fun toWgState() : Tunnel.State {
return when(this) {
UP -> Tunnel.State.UP
DOWN -> Tunnel.State.DOWN
TOGGLE -> Tunnel.State.TOGGLE
}
}
fun toAmState() : org.amnezia.awg.backend.Tunnel.State {
return when(this) {
UP -> org.amnezia.awg.backend.Tunnel.State.UP
DOWN -> org.amnezia.awg.backend.Tunnel.State.DOWN
TOGGLE -> org.amnezia.awg.backend.Tunnel.State.TOGGLE
}
}
companion object {
fun from(state: Tunnel.State) : TunnelState {
return when(state) {
Tunnel.State.DOWN -> DOWN
Tunnel.State.TOGGLE -> TOGGLE
Tunnel.State.UP -> UP
}
}
fun from(state: org.amnezia.awg.backend.Tunnel.State) : TunnelState {
return when(state) {
org.amnezia.awg.backend.Tunnel.State.DOWN -> DOWN
org.amnezia.awg.backend.Tunnel.State.TOGGLE -> TOGGLE
org.amnezia.awg.backend.Tunnel.State.UP -> UP
}
}
}
}

View File

@ -1,15 +1,15 @@
package com.zaneschepke.wireguardautotunnel.service.tunnel package com.zaneschepke.wireguardautotunnel.service.tunnel
import com.wireguard.android.backend.Tunnel import com.wireguard.android.backend.Tunnel
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
interface VpnService : Tunnel { interface VpnService : Tunnel, org.amnezia.awg.backend.Tunnel {
suspend fun startTunnel(tunnelConfig: TunnelConfig? = null): Tunnel.State suspend fun startTunnel(tunnelConfig: TunnelConfig? = null): TunnelState
suspend fun stopTunnel() suspend fun stopTunnel()
val vpnState: StateFlow<VpnState> val vpnState: StateFlow<VpnState>
fun getState(): Tunnel.State fun getState(): TunnelState
} }

View File

@ -1,11 +1,10 @@
package com.zaneschepke.wireguardautotunnel.service.tunnel package com.zaneschepke.wireguardautotunnel.service.tunnel
import com.wireguard.android.backend.Statistics import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
import com.wireguard.android.backend.Tunnel import com.zaneschepke.wireguardautotunnel.service.tunnel.statistics.TunnelStatistics
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
data class VpnState( data class VpnState(
val status: Tunnel.State = Tunnel.State.DOWN, val status: TunnelState = TunnelState.DOWN,
val tunnelConfig: TunnelConfig? = null, val tunnelConfig: TunnelConfig? = null,
val statistics: Statistics? = null val statistics: TunnelStatistics? = null
) )

View File

@ -2,13 +2,15 @@ package com.zaneschepke.wireguardautotunnel.service.tunnel
import com.wireguard.android.backend.Backend import com.wireguard.android.backend.Backend
import com.wireguard.android.backend.BackendException import com.wireguard.android.backend.BackendException
import com.wireguard.android.backend.Statistics
import com.wireguard.android.backend.Tunnel.State import com.wireguard.android.backend.Tunnel.State
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
import com.zaneschepke.wireguardautotunnel.module.Kernel import com.zaneschepke.wireguardautotunnel.module.Kernel
import com.zaneschepke.wireguardautotunnel.module.Userspace import com.zaneschepke.wireguardautotunnel.module.Userspace
import com.zaneschepke.wireguardautotunnel.service.tunnel.statistics.AmneziaStatistics
import com.zaneschepke.wireguardautotunnel.service.tunnel.statistics.TunnelStatistics
import com.zaneschepke.wireguardautotunnel.service.tunnel.statistics.WireGuardStatistics
import com.zaneschepke.wireguardautotunnel.util.Constants import com.zaneschepke.wireguardautotunnel.util.Constants
import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
@ -19,12 +21,15 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.amnezia.awg.backend.Tunnel
import org.amnezia.awg.config.Config
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
class WireGuardTunnel class WireGuardTunnel
@Inject @Inject
constructor( constructor(
private val userspaceAmneziaBackend : org.amnezia.awg.backend.Backend,
@Userspace private val userspaceBackend: Backend, @Userspace private val userspaceBackend: Backend,
@Kernel private val kernelBackend: Backend, @Kernel private val kernelBackend: Backend,
private val appDataRepository: AppDataRepository, private val appDataRepository: AppDataRepository,
@ -38,46 +43,70 @@ constructor(
private var backend: Backend = userspaceBackend private var backend: Backend = userspaceBackend
private var backendIsUserspace = true private var backendIsWgUserspace = true
private var backendIsAmneziaUserspace = false
init { init {
scope.launch { scope.launch {
appDataRepository.settings.getSettingsFlow().collect { appDataRepository.settings.getSettingsFlow().collect {
if (it.isKernelEnabled && backendIsUserspace) { if (it.isKernelEnabled && (backendIsWgUserspace || backendIsAmneziaUserspace)) {
Timber.d("Setting kernel backend") Timber.d("Setting kernel backend")
backend = kernelBackend backend = kernelBackend
backendIsUserspace = false backendIsWgUserspace = false
} else if (!it.isKernelEnabled && !backendIsUserspace) { backendIsAmneziaUserspace = false
Timber.d("Setting userspace backend") } else if (!it.isKernelEnabled && !it.isAmneziaEnabled && !backendIsWgUserspace) {
Timber.d("Setting WireGuard userspace backend")
backend = userspaceBackend backend = userspaceBackend
backendIsUserspace = true backendIsWgUserspace = true
backendIsAmneziaUserspace = false
} else if (it.isAmneziaEnabled && !backendIsAmneziaUserspace) {
Timber.d("Setting Amnezia userspace backend")
backendIsAmneziaUserspace = true
backendIsWgUserspace = false
} }
} }
} }
} }
override suspend fun startTunnel(tunnelConfig: TunnelConfig?): State { private fun setState(tunnelConfig: TunnelConfig?, tunnelState: TunnelState) : TunnelState {
return if(backendIsAmneziaUserspace) {
Timber.i("Using Amnezia backend")
val config = tunnelConfig?.let {
if(it.amQuick != "") TunnelConfig.configFromAmQuick(it.amQuick) else {
Timber.w("Using backwards compatible wg config, amnezia specific config not found.")
TunnelConfig.configFromAmQuick(it.wgQuick)
}
}
val state = userspaceAmneziaBackend.setState(this, tunnelState.toAmState(), config)
TunnelState.from(state)
} else {
Timber.i("Using Wg backend")
val wgConfig = tunnelConfig?.let { TunnelConfig.configFromWgQuick(it.wgQuick) }
val state = backend.setState(
this,
tunnelState.toWgState(),
wgConfig,
)
TunnelState.from(state)
}
}
override suspend fun startTunnel(tunnelConfig: TunnelConfig?): TunnelState {
return try { return try {
//TODO we need better error handling here //TODO we need better error handling here
val config = tunnelConfig ?: appDataRepository.getPrimaryOrFirstTunnel() val config = tunnelConfig ?: appDataRepository.getPrimaryOrFirstTunnel()
if (config != null) { if (config != null) {
emitTunnelConfig(config) emitTunnelConfig(config)
val wgConfig = TunnelConfig.configFromQuick(config.wgQuick) setState(config, TunnelState.UP)
val state =
backend.setState(
this,
State.UP,
wgConfig,
)
state
} else throw Exception("No tunnels") } else throw Exception("No tunnels")
} catch (e: BackendException) { } 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 TunnelState.from(State.DOWN)
} }
} }
private fun emitTunnelState(state: State) { private fun emitTunnelState(state : TunnelState) {
_vpnState.tryEmit( _vpnState.tryEmit(
_vpnState.value.copy( _vpnState.value.copy(
status = state, status = state,
@ -85,7 +114,7 @@ constructor(
) )
} }
private fun emitBackendStatistics(statistics: Statistics) { private fun emitBackendStatistics(statistics: TunnelStatistics) {
_vpnState.tryEmit( _vpnState.tryEmit(
_vpnState.value.copy( _vpnState.value.copy(
statistics = statistics, statistics = statistics,
@ -103,38 +132,49 @@ constructor(
override suspend fun stopTunnel() { override suspend fun stopTunnel() {
try { try {
if (getState() == State.UP) { if (getState() == TunnelState.UP) {
val state = backend.setState(this, State.DOWN, null) val state = setState(null, TunnelState.DOWN)
emitTunnelState(state) emitTunnelState(state)
} }
} catch (e: BackendException) { } catch (e: BackendException) {
Timber.e("Failed to stop tunnel with error: ${e.message}") Timber.e("Failed to stop wireguard tunnel with error: ${e.message}")
} catch (e: org.amnezia.awg.backend.BackendException) {
Timber.e("Failed to stop amnezia tunnel with error: ${e.message}")
} }
} }
override fun getState(): State { override fun getState(): TunnelState {
return backend.getState(this) return if(backendIsAmneziaUserspace) TunnelState.from(userspaceAmneziaBackend.getState(this))
else TunnelState.from(backend.getState(this))
} }
override fun getName(): String { override fun getName(): String {
return _vpnState.value.tunnelConfig?.name ?: "" return _vpnState.value.tunnelConfig?.name ?: ""
} }
override fun onStateChange(state: State) {
override fun onStateChange(newState: Tunnel.State) {
handleStateChange(TunnelState.from(newState))
}
private fun handleStateChange(state: TunnelState) {
val tunnel = this val tunnel = this
emitTunnelState(state) emitTunnelState(state)
WireGuardAutoTunnel.requestTunnelTileServiceStateUpdate(WireGuardAutoTunnel.instance) WireGuardAutoTunnel.requestTunnelTileServiceStateUpdate(WireGuardAutoTunnel.instance)
if (state == State.UP) { if (state == TunnelState.UP) {
statsJob = statsJob =
scope.launch { scope.launch {
while (true) { while (true) {
val statistics = backend.getStatistics(tunnel) if(backendIsAmneziaUserspace) {
emitBackendStatistics(statistics) emitBackendStatistics(AmneziaStatistics(userspaceAmneziaBackend.getStatistics(tunnel)))
} else {
emitBackendStatistics(WireGuardStatistics(backend.getStatistics(tunnel)))
}
delay(Constants.VPN_STATISTIC_CHECK_INTERVAL) delay(Constants.VPN_STATISTIC_CHECK_INTERVAL)
} }
} }
} }
if (state == State.DOWN) { if (state == TunnelState.DOWN) {
try { try {
statsJob?.cancel() statsJob?.cancel()
} catch (e : CancellationException) { } catch (e : CancellationException) {
@ -142,4 +182,8 @@ constructor(
} }
} }
} }
override fun onStateChange(state: State) {
handleStateChange(TunnelState.from(state))
}
} }

View File

@ -0,0 +1,34 @@
package com.zaneschepke.wireguardautotunnel.service.tunnel.statistics
import org.amnezia.awg.backend.Statistics
import org.amnezia.awg.crypto.Key
class AmneziaStatistics(private val statistics: Statistics) : TunnelStatistics() {
override fun peerStats(peer: Key): PeerStats? {
val key = Key.fromBase64(peer.toBase64())
val stats = statistics.peer(key)
return stats?.let {
PeerStats(
rxBytes = stats.rxBytes,
txBytes = stats.txBytes,
latestHandshakeEpochMillis = stats.latestHandshakeEpochMillis
)
}
}
override fun isTunnelStale(): Boolean {
return statistics.isStale
}
override fun getPeers(): Array<Key> {
return statistics.peers()
}
override fun rx(): Long {
return statistics.totalRx()
}
override fun tx(): Long {
return statistics.totalTx()
}
}

View File

@ -0,0 +1,18 @@
package com.zaneschepke.wireguardautotunnel.service.tunnel.statistics
import org.amnezia.awg.crypto.Key
abstract class TunnelStatistics {
@JvmRecord
data class PeerStats(val rxBytes: Long, val txBytes: Long, val latestHandshakeEpochMillis: Long)
abstract fun peerStats(peer: Key): PeerStats?
abstract fun isTunnelStale() : Boolean
abstract fun getPeers(): Array<Key>
abstract fun rx() : Long
abstract fun tx() : Long
}

View File

@ -0,0 +1,36 @@
package com.zaneschepke.wireguardautotunnel.service.tunnel.statistics
import com.wireguard.android.backend.Statistics
import org.amnezia.awg.crypto.Key
class WireGuardStatistics(private val statistics: Statistics) : TunnelStatistics() {
override fun peerStats(peer: Key): PeerStats? {
val key = com.wireguard.crypto.Key.fromBase64(peer.toBase64())
val peerStats = statistics.peer(key)
return peerStats?.let {
PeerStats(
txBytes = peerStats.txBytes,
rxBytes = peerStats.rxBytes,
latestHandshakeEpochMillis = peerStats.latestHandshakeEpochMillis
)
}
}
override fun isTunnelStale(): Boolean {
return statistics.isStale
}
override fun getPeers(): Array<Key> {
return statistics.peers().map {
Key.fromBase64(it.toBase64())
}.toTypedArray()
}
override fun rx(): Long {
return statistics.totalRx()
}
override fun tx(): Long {
return statistics.totalTx()
}
}

View File

@ -2,12 +2,14 @@ package com.zaneschepke.wireguardautotunnel.ui
import android.app.Application import android.app.Application
import android.content.ActivityNotFoundException import android.content.ActivityNotFoundException
import android.content.Context
import android.content.Intent import android.content.Intent
import android.net.Uri import android.net.Uri
import android.widget.Toast import android.widget.Toast
import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.mutableStateListOf
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.journeyapps.barcodescanner.BarcodeEncoder
import com.wireguard.android.backend.GoBackend import com.wireguard.android.backend.GoBackend
import com.zaneschepke.logcatter.Logcatter import com.zaneschepke.logcatter.Logcatter
import com.zaneschepke.logcatter.model.LogMessage import com.zaneschepke.logcatter.model.LogMessage
@ -19,6 +21,7 @@ import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers 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.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import timber.log.Timber import timber.log.Timber
import java.time.Instant import java.time.Instant
@ -27,9 +30,7 @@ import javax.inject.Inject
@HiltViewModel @HiltViewModel
class AppViewModel class AppViewModel
@Inject @Inject
constructor( constructor() : ViewModel() {
private val application: Application,
) : ViewModel() {
val vpnIntent: Intent? = GoBackend.VpnService.prepare(WireGuardAutoTunnel.instance) val vpnIntent: Intent? = GoBackend.VpnService.prepare(WireGuardAutoTunnel.instance)
@ -49,68 +50,78 @@ constructor(
} }
private fun requestPermissions() { private fun requestPermissions() {
_appUiState.value = _appUiState.value.copy( _appUiState.update {
requestPermissions = true, it.copy(
) requestPermissions = true
)
}
} }
fun permissionsRequested() { fun permissionsRequested() {
_appUiState.value = _appUiState.value.copy( _appUiState.update {
requestPermissions = false, it.copy(
) requestPermissions = false
)
}
} }
fun openWebPage(url: String) { fun openWebPage(url: String, context : Context) {
try { try {
val webpage: Uri = Uri.parse(url) val webpage: Uri = Uri.parse(url)
val intent = Intent(Intent.ACTION_VIEW, webpage).apply { val intent = Intent(Intent.ACTION_VIEW, webpage).apply {
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
} }
application.startActivity(intent) context.startActivity(intent)
} catch (e: ActivityNotFoundException) { } catch (e: ActivityNotFoundException) {
Timber.e(e) Timber.e(e)
showSnackbarMessage(application.getString(R.string.no_browser_detected)) showSnackbarMessage(context.getString(R.string.no_browser_detected))
} }
} }
fun onVpnPermissionAccepted() { fun onVpnPermissionAccepted() {
_appUiState.value = _appUiState.value.copy( _appUiState.update {
vpnPermissionAccepted = true, it.copy(
) vpnPermissionAccepted = true
)
}
} }
fun launchEmail() { fun launchEmail(context: Context) {
try { try {
val intent = val intent =
Intent(Intent.ACTION_SENDTO).apply { Intent(Intent.ACTION_SENDTO).apply {
type = Constants.EMAIL_MIME_TYPE type = Constants.EMAIL_MIME_TYPE
putExtra(Intent.EXTRA_EMAIL, arrayOf(application.getString(R.string.my_email))) putExtra(Intent.EXTRA_EMAIL, arrayOf(context.getString(R.string.my_email)))
putExtra(Intent.EXTRA_SUBJECT, application.getString(R.string.email_subject)) putExtra(Intent.EXTRA_SUBJECT, context.getString(R.string.email_subject))
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
} }
application.startActivity( context.startActivity(
Intent.createChooser(intent, application.getString(R.string.email_chooser)).apply { Intent.createChooser(intent, context.getString(R.string.email_chooser)).apply {
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
}, },
) )
} catch (e: ActivityNotFoundException) { } catch (e: ActivityNotFoundException) {
Timber.e(e) Timber.e(e)
showSnackbarMessage(application.getString(R.string.no_email_detected)) showSnackbarMessage(context.getString(R.string.no_email_detected))
} }
} }
fun showSnackbarMessage(message: String) { fun showSnackbarMessage(message: String) {
_appUiState.value = _appUiState.value.copy( _appUiState.update {
snackbarMessage = message, it.copy(
snackbarMessageConsumed = false, snackbarMessage = message,
) snackbarMessageConsumed = false,
)
}
} }
fun snackbarMessageConsumed() { fun snackbarMessageConsumed() {
_appUiState.value = _appUiState.value.copy( _appUiState.update {
snackbarMessage = "", it.copy(
snackbarMessageConsumed = true, snackbarMessage = "",
) snackbarMessageConsumed = true,
)
}
} }
val logs = mutableStateListOf<LogMessage>() val logs = mutableStateListOf<LogMessage>()
@ -132,17 +143,19 @@ constructor(
Logcatter.clear() Logcatter.clear()
} }
fun saveLogsToFile() { fun saveLogsToFile(context: Context) {
val fileName = "${Constants.BASE_LOG_FILE_NAME}-${Instant.now().epochSecond}.txt" val fileName = "${Constants.BASE_LOG_FILE_NAME}-${Instant.now().epochSecond}.txt"
val content = logs.joinToString(separator = "\n") val content = logs.joinToString(separator = "\n")
FileUtils.saveFileToDownloads(application.applicationContext, content, fileName) FileUtils.saveFileToDownloads(context.applicationContext, content, fileName)
Toast.makeText(application, application.getString(R.string.logs_saved), Toast.LENGTH_SHORT) Toast.makeText(context, context.getString(R.string.logs_saved), Toast.LENGTH_SHORT)
.show() .show()
} }
fun setNotificationPermissionAccepted(accepted: Boolean) { fun setNotificationPermissionAccepted(accepted: Boolean) {
_appUiState.value = _appUiState.value.copy( _appUiState.update {
notificationPermissionAccepted = accepted, it.copy(
) notificationPermissionAccepted = accepted,
)
}
} }
} }

View File

@ -138,7 +138,11 @@ class MainActivity : AppCompatActivity() {
return@LaunchedEffect notificationPermissionState.launchPermissionRequest() return@LaunchedEffect notificationPermissionState.launchPermissionRequest()
} }
if (!appUiState.vpnPermissionAccepted) { if (!appUiState.vpnPermissionAccepted) {
return@LaunchedEffect vpnActivityResultState.launch(appViewModel.vpnIntent) return@LaunchedEffect appViewModel.vpnIntent?.let {
vpnActivityResultState.launch(
it
)
}!!
} }
} }
} }

View File

@ -17,7 +17,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
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 com.wireguard.android.backend.Statistics import com.zaneschepke.wireguardautotunnel.service.tunnel.statistics.TunnelStatistics
import com.zaneschepke.wireguardautotunnel.util.NumberUtils import com.zaneschepke.wireguardautotunnel.util.NumberUtils
import com.zaneschepke.wireguardautotunnel.util.toThreeDecimalPlaceString import com.zaneschepke.wireguardautotunnel.util.toThreeDecimalPlaceString
@ -30,7 +30,7 @@ fun RowListItem(
onClick: () -> Unit, onClick: () -> Unit,
rowButton: @Composable () -> Unit, rowButton: @Composable () -> Unit,
expanded: Boolean, expanded: Boolean,
statistics: Statistics? statistics: TunnelStatistics?
) { ) {
Box( Box(
modifier = modifier =
@ -59,7 +59,7 @@ fun RowListItem(
rowButton() rowButton()
} }
if (expanded) { if (expanded) {
statistics?.peers()?.forEach { statistics?.getPeers()?.forEach {
Row( Row(
modifier = modifier =
Modifier Modifier
@ -69,9 +69,9 @@ fun RowListItem(
horizontalArrangement = Arrangement.SpaceEvenly, horizontalArrangement = Arrangement.SpaceEvenly,
) { ) {
//TODO change these to string resources //TODO change these to string resources
val handshakeEpoch = statistics.peer(it)!!.latestHandshakeEpochMillis val handshakeEpoch = statistics.peerStats(it)!!.latestHandshakeEpochMillis
val peerTx = statistics.peer(it)!!.txBytes val peerTx = statistics.peerStats(it)!!.txBytes
val peerRx = statistics.peer(it)!!.rxBytes val peerRx = statistics.peerStats(it)!!.rxBytes
val peerId = it.toBase64().subSequence(0, 3).toString() + "***" val peerId = it.toBase64().subSequence(0, 3).toString() + "***"
val handshakeSec = val handshakeSec =
NumberUtils.getSecondsBetweenTimestampAndNow(handshakeEpoch) NumberUtils.getSecondsBetweenTimestampAndNow(handshakeEpoch)

View File

@ -1,30 +0,0 @@
package com.zaneschepke.wireguardautotunnel.ui.models
import com.wireguard.config.Interface
data class InterfaceProxy(
var privateKey: String = "",
var publicKey: String = "",
var addresses: String = "",
var dnsServers: String = "",
var listenPort: String = "",
var mtu: String = ""
) {
companion object {
fun from(i: Interface): InterfaceProxy {
return InterfaceProxy(
publicKey = i.keyPair.publicKey.toBase64().trim(),
privateKey = i.keyPair.privateKey.toBase64().trim(),
addresses = i.addresses.joinToString(", ").trim(),
dnsServers = i.dnsServers.joinToString(", ").replace("/", "").trim(),
listenPort =
if (i.listenPort.isPresent) {
i.listenPort.get().toString().trim()
} else {
""
},
mtu = if (i.mtu.isPresent) i.mtu.get().toString().trim() else "",
)
}
}
}

View File

@ -486,6 +486,98 @@ fun ConfigScreen(
modifier = Modifier.width(IntrinsicSize.Min), modifier = Modifier.width(IntrinsicSize.Min),
) )
} }
if(uiState.isAmneziaEnabled) {
ConfigurationTextBox(
value = uiState.interfaceProxy.junkPacketCount,
onValueChange = { value -> viewModel.onJunkPacketCountChanged(value) },
keyboardActions = keyboardActions,
label = stringResource(R.string.junk_packet_count),
hint = stringResource(R.string.junk_packet_count).lowercase(),
modifier = Modifier
.fillMaxWidth()
.focusRequester(focusRequester),
)
ConfigurationTextBox(
value = uiState.interfaceProxy.junkPacketMinSize,
onValueChange = { value -> viewModel.onJunkPacketMinSizeChanged(value) },
keyboardActions = keyboardActions,
label = stringResource(R.string.junk_packet_minimum_size),
hint = stringResource(R.string.junk_packet_minimum_size).lowercase(),
modifier = Modifier
.fillMaxWidth()
.focusRequester(focusRequester),
)
ConfigurationTextBox(
value = uiState.interfaceProxy.junkPacketMaxSize,
onValueChange = { value -> viewModel.onJunkPacketMaxSizeChanged(value) },
keyboardActions = keyboardActions,
label = stringResource(R.string.junk_packet_maximum_size),
hint = stringResource(R.string.junk_packet_maximum_size).lowercase(),
modifier = Modifier
.fillMaxWidth()
.focusRequester(focusRequester),
)
ConfigurationTextBox(
value = uiState.interfaceProxy.initPacketJunkSize,
onValueChange = { value -> viewModel.onInitPacketJunkSizeChanged(value) },
keyboardActions = keyboardActions,
label = stringResource(R.string.init_packet_junk_size),
hint = stringResource(R.string.init_packet_junk_size).lowercase(),
modifier = Modifier
.fillMaxWidth()
.focusRequester(focusRequester),
)
ConfigurationTextBox(
value = uiState.interfaceProxy.responsePacketJunkSize,
onValueChange = { value -> viewModel.onResponsePacketJunkSize(value) },
keyboardActions = keyboardActions,
label = stringResource(R.string.response_packet_junk_size),
hint = stringResource(R.string.response_packet_junk_size).lowercase(),
modifier = Modifier
.fillMaxWidth()
.focusRequester(focusRequester),
)
ConfigurationTextBox(
value = uiState.interfaceProxy.initPacketMagicHeader,
onValueChange = { value -> viewModel.onInitPacketMagicHeader(value) },
keyboardActions = keyboardActions,
label = stringResource(R.string.init_packet_magic_header),
hint = stringResource(R.string.init_packet_magic_header).lowercase(),
modifier = Modifier
.fillMaxWidth()
.focusRequester(focusRequester),
)
ConfigurationTextBox(
value = uiState.interfaceProxy.responsePacketMagicHeader,
onValueChange = { value -> viewModel.onResponsePacketMagicHeader(value) },
keyboardActions = keyboardActions,
label = stringResource(R.string.response_packet_magic_header),
hint = stringResource(R.string.response_packet_magic_header).lowercase(),
modifier = Modifier
.fillMaxWidth()
.focusRequester(focusRequester),
)
ConfigurationTextBox(
value = uiState.interfaceProxy.transportPacketMagicHeader,
onValueChange = { value -> viewModel.onTransportPacketMagicHeader(value) },
keyboardActions = keyboardActions,
label = stringResource(R.string.transport_packet_magic_header),
hint = stringResource(R.string.transport_packet_magic_header).lowercase(),
modifier = Modifier
.fillMaxWidth()
.focusRequester(focusRequester),
)
ConfigurationTextBox(
value = uiState.interfaceProxy.underloadPacketMagicHeader,
onValueChange = { value -> viewModel.onUnderloadPacketMagicHeader(value) },
keyboardActions = keyboardActions,
label = stringResource(R.string.underload_packet_magic_header),
hint = stringResource(R.string.underload_packet_magic_header).lowercase(),
modifier = Modifier
.fillMaxWidth()
.focusRequester(focusRequester),
)
}
Row( Row(
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
modifier = Modifier modifier = Modifier

View File

@ -1,8 +1,9 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.config package com.zaneschepke.wireguardautotunnel.ui.screens.config
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig import com.wireguard.config.Config
import com.zaneschepke.wireguardautotunnel.ui.models.InterfaceProxy import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
import com.zaneschepke.wireguardautotunnel.ui.models.PeerProxy import com.zaneschepke.wireguardautotunnel.ui.screens.config.model.InterfaceProxy
import com.zaneschepke.wireguardautotunnel.ui.screens.config.model.PeerProxy
import com.zaneschepke.wireguardautotunnel.util.Packages import com.zaneschepke.wireguardautotunnel.util.Packages
data class ConfigUiState( data class ConfigUiState(
@ -14,5 +15,58 @@ data class ConfigUiState(
val isAllApplicationsEnabled: Boolean = false, val isAllApplicationsEnabled: Boolean = false,
val loading: Boolean = true, val loading: Boolean = true,
val tunnel: TunnelConfig? = null, val tunnel: TunnelConfig? = null,
val tunnelName: String = "" val tunnelName: String = "",
) val isAmneziaEnabled: Boolean = false
) {
companion object {
fun from(config : Config) : ConfigUiState {
val proxyPeers = config.peers.map { PeerProxy.from(it) }
val proxyInterface = InterfaceProxy.from(config.`interface`)
var include = true
var isAllApplicationsEnabled = false
val checkedPackages =
if (config.`interface`.includedApplications.isNotEmpty()) {
config.`interface`.includedApplications
} else if (config.`interface`.excludedApplications.isNotEmpty()) {
include = false
config.`interface`.excludedApplications
} else {
isAllApplicationsEnabled = true
emptySet()
}
return ConfigUiState(
proxyPeers,
proxyInterface,
emptyList(),
checkedPackages.toList(),
include,
isAllApplicationsEnabled,
)
}
fun from(config: org.amnezia.awg.config.Config) : ConfigUiState {
//TODO update with new values
val proxyPeers = config.peers.map { PeerProxy.from(it) }
val proxyInterface = InterfaceProxy.from(config.`interface`)
var include = true
var isAllApplicationsEnabled = false
val checkedPackages =
if (config.`interface`.includedApplications.isNotEmpty()) {
config.`interface`.includedApplications
} else if (config.`interface`.excludedApplications.isNotEmpty()) {
include = false
config.`interface`.excludedApplications
} else {
isAllApplicationsEnabled = true
emptySet()
}
return ConfigUiState(
proxyPeers,
proxyInterface,
emptyList(),
checkedPackages.toList(),
include,
isAllApplicationsEnabled,
)
}
}
}

View File

@ -13,10 +13,10 @@ 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
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
import com.zaneschepke.wireguardautotunnel.ui.models.InterfaceProxy import com.zaneschepke.wireguardautotunnel.data.repository.SettingsRepository
import com.zaneschepke.wireguardautotunnel.ui.models.PeerProxy import com.zaneschepke.wireguardautotunnel.ui.screens.config.model.PeerProxy
import com.zaneschepke.wireguardautotunnel.util.Constants import com.zaneschepke.wireguardautotunnel.util.Constants
import com.zaneschepke.wireguardautotunnel.util.Event import com.zaneschepke.wireguardautotunnel.util.Event
import com.zaneschepke.wireguardautotunnel.util.NumberUtils import com.zaneschepke.wireguardautotunnel.util.NumberUtils
@ -27,6 +27,7 @@ import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers 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.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
@ -36,6 +37,7 @@ class ConfigViewModel
@Inject @Inject
constructor( constructor(
private val application: Application, private val application: Application,
private val settingsRepository: SettingsRepository,
private val appDataRepository: AppDataRepository private val appDataRepository: AppDataRepository
) : ViewModel() { ) : ViewModel() {
@ -52,32 +54,17 @@ constructor(
val tunnelConfig = val tunnelConfig =
appDataRepository.tunnels.getAll() appDataRepository.tunnels.getAll()
.firstOrNull { it.id.toString() == tunnelId } .firstOrNull { it.id.toString() == tunnelId }
val isAmneziaEnabled = settingsRepository.getSettings().isAmneziaEnabled
if (tunnelConfig != null) { if (tunnelConfig != null) {
val config = TunnelConfig.configFromQuick(tunnelConfig.wgQuick) (if(isAmneziaEnabled) {
val proxyPeers = config.peers.map { PeerProxy.from(it) } val amConfig = if(tunnelConfig.amQuick == "") tunnelConfig.wgQuick else tunnelConfig.amQuick
val proxyInterface = InterfaceProxy.from(config.`interface`) ConfigUiState.from(TunnelConfig.configFromAmQuick(amConfig))
var include = true } else ConfigUiState.from(TunnelConfig.configFromWgQuick(tunnelConfig.wgQuick))).copy(
var isAllApplicationsEnabled = false packages = packages,
val checkedPackages = loading = false,
if (config.`interface`.includedApplications.isNotEmpty()) { tunnel = tunnelConfig,
config.`interface`.includedApplications tunnelName = tunnelConfig.name,
} else if (config.`interface`.excludedApplications.isNotEmpty()) { isAmneziaEnabled = isAmneziaEnabled
include = false
config.`interface`.excludedApplications
} else {
isAllApplicationsEnabled = true
emptySet()
}
ConfigUiState(
proxyPeers,
proxyInterface,
packages,
checkedPackages.toList(),
include,
isAllApplicationsEnabled,
false,
tunnelConfig,
tunnelConfig.name,
) )
} else { } else {
ConfigUiState(loading = false, packages = packages) ConfigUiState(loading = false, packages = packages)
@ -168,6 +155,20 @@ constructor(
} }
} }
private fun buildAmPeerListFromProxyPeers(): List<org.amnezia.awg.config.Peer> {
return _uiState.value.proxyPeers.map {
val builder = org.amnezia.awg.config.Peer.Builder()
if (it.allowedIps.isNotEmpty()) builder.parseAllowedIPs(it.allowedIps.trim())
if (it.publicKey.isNotEmpty()) builder.parsePublicKey(it.publicKey.trim())
if (it.preSharedKey.isNotEmpty()) builder.parsePreSharedKey(it.preSharedKey.trim())
if (it.endpoint.isNotEmpty()) builder.parseEndpoint(it.endpoint.trim())
if (it.persistentKeepalive.isNotEmpty()) {
builder.parsePersistentKeepalive(it.persistentKeepalive.trim())
}
builder.build()
}
}
private fun emptyCheckedPackagesList() { private fun emptyCheckedPackagesList() {
_uiState.value = _uiState.value.copy(checkedPackageNames = emptyList()) _uiState.value = _uiState.value.copy(checkedPackageNames = emptyList())
} }
@ -190,20 +191,76 @@ constructor(
return builder.build() return builder.build()
} }
private fun buildAmInterfaceListFromProxyInterface(): org.amnezia.awg.config.Interface {
val builder = org.amnezia.awg.config.Interface.Builder()
builder.parsePrivateKey(_uiState.value.interfaceProxy.privateKey.trim())
builder.parseAddresses(_uiState.value.interfaceProxy.addresses.trim())
if (_uiState.value.interfaceProxy.dnsServers.isNotEmpty()) {
builder.parseDnsServers(_uiState.value.interfaceProxy.dnsServers.trim())
}
if (_uiState.value.interfaceProxy.mtu.isNotEmpty())
builder.parseMtu(_uiState.value.interfaceProxy.mtu.trim())
if (_uiState.value.interfaceProxy.listenPort.isNotEmpty()) {
builder.parseListenPort(_uiState.value.interfaceProxy.listenPort.trim())
}
if (isAllApplicationsEnabled()) emptyCheckedPackagesList()
if (_uiState.value.include) builder.includeApplications(_uiState.value.checkedPackageNames)
if (!_uiState.value.include) builder.excludeApplications(_uiState.value.checkedPackageNames)
if(_uiState.value.interfaceProxy.junkPacketCount.isNotEmpty()) {
builder.setJunkPacketCount(_uiState.value.interfaceProxy.junkPacketCount.trim().toInt())
}
if(_uiState.value.interfaceProxy.junkPacketMinSize.isNotEmpty()) {
builder.setJunkPacketMinSize(_uiState.value.interfaceProxy.junkPacketMinSize.trim().toInt())
}
if(_uiState.value.interfaceProxy.junkPacketMaxSize.isNotEmpty()) {
builder.setJunkPacketMaxSize(_uiState.value.interfaceProxy.junkPacketMaxSize.trim().toInt())
}
if(_uiState.value.interfaceProxy.initPacketJunkSize.isNotEmpty()) {
builder.setInitPacketJunkSize(_uiState.value.interfaceProxy.initPacketJunkSize.trim().toInt())
}
if(_uiState.value.interfaceProxy.responsePacketJunkSize.isNotEmpty()) {
builder.setResponsePacketJunkSize(_uiState.value.interfaceProxy.responsePacketJunkSize.trim().toInt())
}
if(_uiState.value.interfaceProxy.initPacketMagicHeader.isNotEmpty()) {
builder.setInitPacketMagicHeader(_uiState.value.interfaceProxy.initPacketMagicHeader.trim().toLong())
}
if(_uiState.value.interfaceProxy.responsePacketMagicHeader.isNotEmpty()) {
builder.setResponsePacketMagicHeader(_uiState.value.interfaceProxy.responsePacketMagicHeader.trim().toLong())
}
if(_uiState.value.interfaceProxy.transportPacketMagicHeader.isNotEmpty()) {
builder.setTransportPacketMagicHeader(_uiState.value.interfaceProxy.transportPacketMagicHeader.trim().toLong())
}
if(_uiState.value.interfaceProxy.underloadPacketMagicHeader.isNotEmpty()) {
builder.setUnderloadPacketMagicHeader(_uiState.value.interfaceProxy.underloadPacketMagicHeader.trim().toLong())
}
return builder.build()
}
private fun buildConfig() : Config {
val peerList = buildPeerListFromProxyPeers()
val wgInterface = buildInterfaceListFromProxyInterface()
return Config.Builder().addPeers(peerList).setInterface(wgInterface).build()
}
private fun buildAmConfig() : org.amnezia.awg.config.Config {
val peerList = buildAmPeerListFromProxyPeers()
val amInterface = buildAmInterfaceListFromProxyInterface()
return org.amnezia.awg.config.Config.Builder().addPeers(peerList).setInterface(amInterface).build()
}
fun onSaveAllChanges(): Result<Event> { fun onSaveAllChanges(): Result<Event> {
return try { return try {
val peerList = buildPeerListFromProxyPeers() val config = buildConfig()
val wgInterface = buildInterfaceListFromProxyInterface()
val config = Config.Builder().addPeers(peerList).setInterface(wgInterface).build()
val tunnelConfig = when (uiState.value.tunnel) { val tunnelConfig = when (uiState.value.tunnel) {
null -> TunnelConfig( null -> TunnelConfig(
name = _uiState.value.tunnelName, name = _uiState.value.tunnelName,
wgQuick = config.toWgQuickString(), wgQuick = config.toWgQuickString(),
) )
else -> uiState.value.tunnel!!.copy( else -> uiState.value.tunnel!!.copy(
name = _uiState.value.tunnelName, name = _uiState.value.tunnelName,
wgQuick = config.toWgQuickString(), wgQuick = config.toWgQuickString(),
amQuick = if(uiState.value.isAmneziaEnabled) buildAmConfig().toAwgQuickString()
else _uiState.value.tunnel?.amQuick ?: ""
) )
} }
updateTunnelConfig(tunnelConfig) updateTunnelConfig(tunnelConfig)
@ -216,121 +273,138 @@ constructor(
} }
fun onPeerPublicKeyChange(index: Int, value: String) { fun onPeerPublicKeyChange(index: Int, value: String) {
_uiState.value = _uiState.update {
_uiState.value.copy( it.copy(
proxyPeers = proxyPeers =
_uiState.value.proxyPeers.update( _uiState.value.proxyPeers.update(
index, index,
_uiState.value.proxyPeers[index].copy(publicKey = value), _uiState.value.proxyPeers[index].copy(publicKey = value),
), ),
) )
}
} }
fun onPreSharedKeyChange(index: Int, value: String) { fun onPreSharedKeyChange(index: Int, value: String) {
_uiState.value = _uiState.update {
_uiState.value.copy( it.copy(
proxyPeers = proxyPeers =
_uiState.value.proxyPeers.update( _uiState.value.proxyPeers.update(
index, index,
_uiState.value.proxyPeers[index].copy(preSharedKey = value), _uiState.value.proxyPeers[index].copy(preSharedKey = value),
), ),
) )
}
} }
fun onEndpointChange(index: Int, value: String) { fun onEndpointChange(index: Int, value: String) {
_uiState.value = _uiState.update {
_uiState.value.copy( it.copy(
proxyPeers = proxyPeers =
_uiState.value.proxyPeers.update( _uiState.value.proxyPeers.update(
index, index,
_uiState.value.proxyPeers[index].copy(endpoint = value), _uiState.value.proxyPeers[index].copy(endpoint = value),
), ),
) )
}
} }
fun onAllowedIpsChange(index: Int, value: String) { fun onAllowedIpsChange(index: Int, value: String) {
_uiState.value = _uiState.update {
_uiState.value.copy( it.copy(
proxyPeers = proxyPeers =
_uiState.value.proxyPeers.update( _uiState.value.proxyPeers.update(
index, index,
_uiState.value.proxyPeers[index].copy(allowedIps = value), _uiState.value.proxyPeers[index].copy(allowedIps = value),
), ),
) )
}
} }
fun onPersistentKeepaliveChanged(index: Int, value: String) { fun onPersistentKeepaliveChanged(index: Int, value: String) {
_uiState.value = _uiState.update {
_uiState.value.copy( it.copy(
proxyPeers = proxyPeers =
_uiState.value.proxyPeers.update( _uiState.value.proxyPeers.update(
index, index,
_uiState.value.proxyPeers[index].copy(persistentKeepalive = value), _uiState.value.proxyPeers[index].copy(persistentKeepalive = value),
), ),
) )
}
} }
fun onDeletePeer(index: Int) { fun onDeletePeer(index: Int) {
_uiState.value = _uiState.update {
_uiState.value.copy( it.copy(
proxyPeers = _uiState.value.proxyPeers.removeAt(index), proxyPeers = _uiState.value.proxyPeers.removeAt(index),
) )
}
} }
fun addEmptyPeer() { fun addEmptyPeer() {
_uiState.value = _uiState.value.copy(proxyPeers = _uiState.value.proxyPeers + PeerProxy()) _uiState.update {
it.copy(proxyPeers = _uiState.value.proxyPeers + PeerProxy())
}
} }
fun generateKeyPair() { fun generateKeyPair() {
val keyPair = KeyPair() val keyPair = KeyPair()
_uiState.value = _uiState.update {
_uiState.value.copy( it.copy(
interfaceProxy = interfaceProxy =
_uiState.value.interfaceProxy.copy( _uiState.value.interfaceProxy.copy(
privateKey = keyPair.privateKey.toBase64(), privateKey = keyPair.privateKey.toBase64(),
publicKey = keyPair.publicKey.toBase64(), publicKey = keyPair.publicKey.toBase64(),
), ),
) )
}
} }
fun onAddressesChanged(value: String) { fun onAddressesChanged(value: String) {
_uiState.value = _uiState.update {
_uiState.value.copy( it.copy(
interfaceProxy = _uiState.value.interfaceProxy.copy(addresses = value), interfaceProxy = _uiState.value.interfaceProxy.copy(addresses = value),
) )
}
} }
fun onListenPortChanged(value: String) { fun onListenPortChanged(value: String) {
_uiState.value = _uiState.update {
_uiState.value.copy( it.copy(
interfaceProxy = _uiState.value.interfaceProxy.copy(listenPort = value), interfaceProxy = _uiState.value.interfaceProxy.copy(listenPort = value),
) )
}
} }
fun onDnsServersChanged(value: String) { fun onDnsServersChanged(value: String) {
_uiState.value = _uiState.update {
_uiState.value.copy( it.copy(
interfaceProxy = _uiState.value.interfaceProxy.copy(dnsServers = value), interfaceProxy = _uiState.value.interfaceProxy.copy(dnsServers = value),
) )
}
} }
fun onMtuChanged(value: String) { fun onMtuChanged(value: String) {
_uiState.value = _uiState.update {
_uiState.value.copy(interfaceProxy = _uiState.value.interfaceProxy.copy(mtu = value)) it.copy(interfaceProxy = _uiState.value.interfaceProxy.copy(mtu = value))
}
} }
private fun onInterfacePublicKeyChange(value: String) { private fun onInterfacePublicKeyChange(value: String) {
_uiState.value = _uiState.update {
_uiState.value.copy( it.copy(
interfaceProxy = _uiState.value.interfaceProxy.copy(publicKey = value), interfaceProxy = _uiState.value.interfaceProxy.copy(publicKey = value),
) )
}
} }
fun onPrivateKeyChange(value: String) { fun onPrivateKeyChange(value: String) {
_uiState.value = _uiState.update {
_uiState.value.copy( it.copy(
interfaceProxy = _uiState.value.interfaceProxy.copy(privateKey = value), interfaceProxy = _uiState.value.interfaceProxy.copy(privateKey = value),
) )
}
if (NumberUtils.isValidKey(value)) { if (NumberUtils.isValidKey(value)) {
val pair = KeyPair(Key.fromBase64(value)) val pair = KeyPair(Key.fromBase64(value))
onInterfacePublicKeyChange(pair.publicKey.toBase64()) onInterfacePublicKeyChange(pair.publicKey.toBase64())
@ -344,6 +418,77 @@ constructor(
getAllInternetCapablePackages().filter { getAllInternetCapablePackages().filter {
getPackageLabel(it).lowercase().contains(query.lowercase()) getPackageLabel(it).lowercase().contains(query.lowercase())
} }
_uiState.value = _uiState.value.copy(packages = packages) _uiState.update { it.copy(packages = packages) }
}
fun onJunkPacketCountChanged(value: String) {
_uiState.update {
it.copy(
interfaceProxy = _uiState.value.interfaceProxy.copy(junkPacketCount = value)
)
}
}
fun onJunkPacketMinSizeChanged(value: String) {
_uiState.update {
it.copy(
interfaceProxy = _uiState.value.interfaceProxy.copy(junkPacketMinSize = value)
)
}
}
fun onJunkPacketMaxSizeChanged(value: String) {
_uiState.update {
it.copy(
interfaceProxy = _uiState.value.interfaceProxy.copy(junkPacketMaxSize = value)
)
}
}
fun onInitPacketJunkSizeChanged(value: String) {
_uiState.update {
it.copy(
interfaceProxy = _uiState.value.interfaceProxy.copy(initPacketJunkSize = value)
)
}
}
fun onResponsePacketJunkSize(value: String) {
_uiState.update {
it.copy(
interfaceProxy = _uiState.value.interfaceProxy.copy(responsePacketJunkSize = value)
)
}
}
fun onInitPacketMagicHeader(value: String) {
_uiState.update {
it.copy(
interfaceProxy = _uiState.value.interfaceProxy.copy(initPacketMagicHeader = value)
)
}
}
fun onResponsePacketMagicHeader(value: String) {
_uiState.update {
it.copy(
interfaceProxy = _uiState.value.interfaceProxy.copy(responsePacketMagicHeader = value)
)
}
}
fun onTransportPacketMagicHeader(value: String) {
_uiState.update {
it.copy(
interfaceProxy = _uiState.value.interfaceProxy.copy(transportPacketMagicHeader = value)
)
}
}
fun onUnderloadPacketMagicHeader(value: String) {
_uiState.update {
it.copy(
interfaceProxy = _uiState.value.interfaceProxy.copy(underloadPacketMagicHeader = value)
)
}
} }
} }

View File

@ -0,0 +1,63 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.config.model
import com.wireguard.config.Interface
data class InterfaceProxy(
val privateKey: String = "",
val publicKey: String = "",
val addresses: String = "",
val dnsServers: String = "",
val listenPort: String = "",
val mtu: String = "",
val junkPacketCount: String = "",
val junkPacketMinSize: String = "",
val junkPacketMaxSize: String = "",
val initPacketJunkSize: String = "",
val responsePacketJunkSize: String = "",
val initPacketMagicHeader: String = "",
val responsePacketMagicHeader: String = "",
val underloadPacketMagicHeader: String = "",
val transportPacketMagicHeader: String = "",
) {
companion object {
fun from(i: Interface): InterfaceProxy {
return InterfaceProxy(
publicKey = i.keyPair.publicKey.toBase64().trim(),
privateKey = i.keyPair.privateKey.toBase64().trim(),
addresses = i.addresses.joinToString(", ").trim(),
dnsServers = i.dnsServers.joinToString(", ").replace("/", "").trim(),
listenPort =
if (i.listenPort.isPresent) {
i.listenPort.get().toString().trim()
} else {
""
},
mtu = if (i.mtu.isPresent) i.mtu.get().toString().trim() else "",
)
}
fun from(i: org.amnezia.awg.config.Interface) : InterfaceProxy {
return InterfaceProxy(
publicKey = i.keyPair.publicKey.toBase64().trim(),
privateKey = i.keyPair.privateKey.toBase64().trim(),
addresses = i.addresses.joinToString(", ").trim(),
dnsServers = i.dnsServers.joinToString(", ").replace("/", "").trim(),
listenPort =
if (i.listenPort.isPresent) {
i.listenPort.get().toString().trim()
} else {
""
},
mtu = if (i.mtu.isPresent) i.mtu.get().toString().trim() else "",
junkPacketCount = if(i.junkPacketCount.isPresent) i.junkPacketCount.get().toString() else "",
junkPacketMinSize = if(i.junkPacketMinSize.isPresent) i.junkPacketMinSize.get().toString() else "",
junkPacketMaxSize = if(i.junkPacketMaxSize.isPresent) i.junkPacketMaxSize.get().toString() else "",
initPacketJunkSize = if(i.initPacketJunkSize.isPresent) i.initPacketJunkSize.get().toString() else "",
responsePacketJunkSize = if(i.responsePacketJunkSize.isPresent) i.responsePacketJunkSize.get().toString() else "",
initPacketMagicHeader = if(i.initPacketMagicHeader.isPresent) i.initPacketMagicHeader.get().toString() else "",
responsePacketMagicHeader = if(i.responsePacketMagicHeader.isPresent) i.responsePacketMagicHeader.get().toString() else "",
underloadPacketMagicHeader = if(i.underloadPacketMagicHeader.isPresent) i.underloadPacketMagicHeader.get().toString() else "",
transportPacketMagicHeader = if(i.transportPacketMagicHeader.isPresent) i.transportPacketMagicHeader.get().toString() else "",
)
}
}
}

View File

@ -1,13 +1,13 @@
package com.zaneschepke.wireguardautotunnel.ui.models package com.zaneschepke.wireguardautotunnel.ui.screens.config.model
import com.wireguard.config.Peer import com.wireguard.config.Peer
data class PeerProxy( data class PeerProxy(
var publicKey: String = "", val publicKey: String = "",
var preSharedKey: String = "", val preSharedKey: String = "",
var persistentKeepalive: String = "", val persistentKeepalive: String = "",
var endpoint: String = "", val endpoint: String = "",
var allowedIps: String = IPV4_WILDCARD.joinToString(", ").trim() val allowedIps: String = IPV4_WILDCARD.joinToString(", ").trim()
) { ) {
companion object { companion object {
fun from(peer: Peer): PeerProxy { fun from(peer: Peer): PeerProxy {
@ -35,6 +35,31 @@ data class PeerProxy(
) )
} }
fun from(peer: org.amnezia.awg.config.Peer) : PeerProxy {
return PeerProxy(
publicKey = peer.publicKey.toBase64(),
preSharedKey =
if (peer.preSharedKey.isPresent) {
peer.preSharedKey.get().toBase64().trim()
} else {
""
},
persistentKeepalive =
if (peer.persistentKeepalive.isPresent) {
peer.persistentKeepalive.get().toString().trim()
} else {
""
},
endpoint =
if (peer.endpoint.isPresent) {
peer.endpoint.get().toString().trim()
} else {
""
},
allowedIps = peer.allowedIps.joinToString(", ").trim(),
)
}
val IPV4_PUBLIC_NETWORKS = val IPV4_PUBLIC_NETWORKS =
setOf( setOf(
"0.0.0.0/5", "0.0.0.0/5",

View File

@ -29,6 +29,7 @@ import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.overscroll import androidx.compose.foundation.overscroll
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.ClickableText
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Create import androidx.compose.material.icons.filled.Create
import androidx.compose.material.icons.filled.FileOpen import androidx.compose.material.icons.filled.FileOpen
@ -81,19 +82,22 @@ import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalHapticFeedback import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.font.FontStyle import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.withStyle
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.navigation.NavController import 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.Tunnel
import com.zaneschepke.wireguardautotunnel.R import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
import com.zaneschepke.wireguardautotunnel.service.tunnel.HandshakeStatus import com.zaneschepke.wireguardautotunnel.service.tunnel.HandshakeStatus
import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelState
import com.zaneschepke.wireguardautotunnel.ui.AppViewModel import com.zaneschepke.wireguardautotunnel.ui.AppViewModel
import com.zaneschepke.wireguardautotunnel.ui.CaptureActivityPortrait import com.zaneschepke.wireguardautotunnel.ui.CaptureActivityPortrait
import com.zaneschepke.wireguardautotunnel.ui.Screen import com.zaneschepke.wireguardautotunnel.ui.Screen
@ -110,6 +114,8 @@ 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
import timber.log.Timber
import java.util.Timer
@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter") @SuppressLint("UnusedMaterial3ScaffoldPaddingParameter")
@OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class) @OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class)
@ -257,11 +263,14 @@ fun MainScreen(
Scaffold( Scaffold(
modifier = modifier =
Modifier.pointerInput(Unit) { Modifier.pointerInput(Unit) {
detectTapGestures( if(uiState.tunnels.isNotEmpty()) {
onTap = { detectTapGestures(
selectedTunnel = null onTap = {
}, selectedTunnel = null
) },
)
}
}, },
floatingActionButtonPosition = FabPosition.End, floatingActionButtonPosition = FabPosition.End,
floatingActionButton = { floatingActionButton = {
@ -299,16 +308,6 @@ fun MainScreen(
} }
}, },
) { ) {
AnimatedVisibility(uiState.tunnels.isEmpty(), exit = fadeOut(), enter = fadeIn()) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center,
modifier = Modifier
.fillMaxSize(),
) {
Text(text = stringResource(R.string.no_tunnels), fontStyle = FontStyle.Italic)
}
}
if (showBottomSheet) { if (showBottomSheet) {
ModalBottomSheet( ModalBottomSheet(
onDismissRequest = { showBottomSheet = false }, onDismissRequest = { showBottomSheet = false },
@ -401,12 +400,46 @@ fun MainScreen(
modifier = modifier =
Modifier Modifier
.fillMaxSize() .fillMaxSize()
.overscroll(ScrollableDefaults.overscrollEffect()).nestedScroll(nestedScrollConnection), .overscroll(ScrollableDefaults.overscrollEffect())
.nestedScroll(nestedScrollConnection),
state = rememberLazyListState(0, uiState.tunnels.count()), state = rememberLazyListState(0, uiState.tunnels.count()),
userScrollEnabled = true, userScrollEnabled = true,
reverseLayout = false, reverseLayout = false,
flingBehavior = ScrollableDefaults.flingBehavior(), flingBehavior = ScrollableDefaults.flingBehavior(),
) { ) {
item {
val gettingStarted = buildAnnotatedString {
append(stringResource(id = R.string.see_the))
append(" ")
pushStringAnnotation(tag = "gettingStarted", annotation = stringResource(id = R.string.getting_started_url))
withStyle(style = SpanStyle(color = MaterialTheme.colorScheme.primary)) {
append(stringResource(id = R.string.getting_started_guide))
}
pop()
append(" ")
append(stringResource(R.string.unsure_how))
append(".")
}
AnimatedVisibility(
uiState.tunnels.isEmpty(), exit = fadeOut(), enter = fadeIn()) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center,
modifier = Modifier.padding(top = 100.dp)
) {
Text(text = stringResource(R.string.no_tunnels), fontStyle = FontStyle.Italic)
ClickableText(
modifier = Modifier.padding(vertical = 10.dp, horizontal = 24.dp),
text = gettingStarted,
style = MaterialTheme.typography.bodyMedium.copy(color = MaterialTheme.colorScheme.onSurfaceVariant, textAlign = TextAlign.Center),
) {
gettingStarted.getStringAnnotations(tag = "gettingStarted", it, it).firstOrNull()?.let { annotation ->
appViewModel.openWebPage(annotation.item, context)
}
}
}
}
}
item { item {
if (uiState.settings.isAutoTunnelEnabled) { if (uiState.settings.isAutoTunnelEnabled) {
val autoTunnelingLabel = buildAnnotatedString { val autoTunnelingLabel = buildAnnotatedString {
@ -462,7 +495,7 @@ fun MainScreen(
val leadingIconColor = val leadingIconColor =
(if ( (if (
uiState.vpnState.tunnelConfig?.name == tunnel.name && uiState.vpnState.tunnelConfig?.name == tunnel.name &&
uiState.vpnState.status == Tunnel.State.UP uiState.vpnState.status == TunnelState.UP
) { ) {
uiState.vpnState.statistics uiState.vpnState.statistics
?.mapPeerStats() ?.mapPeerStats()
@ -508,7 +541,7 @@ fun MainScreen(
text = tunnel.name.truncateWithEllipsis(Constants.ALLOWED_DISPLAY_NAME_LENGTH), text = tunnel.name.truncateWithEllipsis(Constants.ALLOWED_DISPLAY_NAME_LENGTH),
onHold = { onHold = {
if ( if (
(uiState.vpnState.status == Tunnel.State.UP) && (uiState.vpnState.status == TunnelState.UP) &&
(tunnel.name == uiState.vpnState.tunnelConfig?.name) (tunnel.name == uiState.vpnState.tunnelConfig?.name)
) { ) {
appViewModel.showSnackbarMessage(Event.Message.TunnelOffAction.message) appViewModel.showSnackbarMessage(Event.Message.TunnelOffAction.message)
@ -520,7 +553,7 @@ fun MainScreen(
onClick = { onClick = {
if (!WireGuardAutoTunnel.isRunningOnAndroidTv()) { if (!WireGuardAutoTunnel.isRunningOnAndroidTv()) {
if ( if (
uiState.vpnState.status == Tunnel.State.UP && uiState.vpnState.status == TunnelState.UP &&
(uiState.vpnState.tunnelConfig?.name == tunnel.name) (uiState.vpnState.tunnelConfig?.name == tunnel.name)
) { ) {
expanded.value = !expanded.value expanded.value = !expanded.value
@ -578,7 +611,7 @@ fun MainScreen(
} else { } else {
val checked by remember { val checked by remember {
derivedStateOf { derivedStateOf {
(uiState.vpnState.status == Tunnel.State.UP && (uiState.vpnState.status == TunnelState.UP &&
tunnel.name == uiState.vpnState.tunnelConfig?.name) tunnel.name == uiState.vpnState.tunnelConfig?.name)
} }
} }
@ -620,7 +653,7 @@ fun MainScreen(
modifier = Modifier.focusRequester(focusRequester), modifier = Modifier.focusRequester(focusRequester),
onClick = { onClick = {
if ( if (
uiState.vpnState.status == Tunnel.State.UP && uiState.vpnState.status == TunnelState.UP &&
(uiState.vpnState.tunnelConfig?.name == tunnel.name) (uiState.vpnState.tunnelConfig?.name == tunnel.name)
) { ) {
expanded.value = !expanded.value expanded.value = !expanded.value
@ -643,7 +676,7 @@ fun MainScreen(
IconButton( IconButton(
onClick = { onClick = {
if ( if (
uiState.vpnState.status == Tunnel.State.UP && uiState.vpnState.status == TunnelState.UP &&
tunnel.name == uiState.vpnState.tunnelConfig?.name tunnel.name == uiState.vpnState.tunnelConfig?.name
) { ) {
appViewModel.showSnackbarMessage( appViewModel.showSnackbarMessage(

View File

@ -1,6 +1,6 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.main package com.zaneschepke.wireguardautotunnel.ui.screens.main
import com.zaneschepke.wireguardautotunnel.data.model.Settings import com.zaneschepke.wireguardautotunnel.data.domain.Settings
import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnState import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnState
import com.zaneschepke.wireguardautotunnel.util.TunnelConfigs import com.zaneschepke.wireguardautotunnel.util.TunnelConfigs

View File

@ -9,8 +9,8 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
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.domain.Settings
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnService import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnService
@ -99,7 +99,7 @@ constructor(
} }
private fun validateConfigString(config: String) { private fun validateConfigString(config: String) {
TunnelConfig.configFromQuick(config) TunnelConfig.configFromWgQuick(config)
} }
suspend fun onTunnelQrResult(result: String): Result<Unit> { suspend fun onTunnelQrResult(result: String): Result<Unit> {

View File

@ -1,6 +1,6 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.options package com.zaneschepke.wireguardautotunnel.ui.screens.options
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
data class OptionsUiState( data class OptionsUiState(
val id: String? = null, val id: String? = null,

View File

@ -4,7 +4,7 @@ import androidx.compose.ui.util.fastFirstOrNull
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
import com.zaneschepke.wireguardautotunnel.util.Constants import com.zaneschepke.wireguardautotunnel.util.Constants
import com.zaneschepke.wireguardautotunnel.util.Event import com.zaneschepke.wireguardautotunnel.util.Event
@ -15,6 +15,7 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import javax.inject.Inject import javax.inject.Inject
@ -43,9 +44,11 @@ constructor(
) )
fun init(tunnelId: String) { fun init(tunnelId: String) {
_optionState.value = _optionState.value.copy( _optionState.update {
id = tunnelId, it.copy(
) id = tunnelId
)
}
} }
fun onDeleteRunSSID(ssid: String) = viewModelScope.launch(Dispatchers.IO) { fun onDeleteRunSSID(ssid: String) = viewModelScope.launch(Dispatchers.IO) {

View File

@ -26,7 +26,6 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardActions
@ -75,6 +74,7 @@ import com.wireguard.android.backend.Tunnel
import com.wireguard.android.backend.WgQuickBackend 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.service.tunnel.TunnelState
import com.zaneschepke.wireguardautotunnel.ui.AppViewModel import com.zaneschepke.wireguardautotunnel.ui.AppViewModel
import com.zaneschepke.wireguardautotunnel.ui.Screen import com.zaneschepke.wireguardautotunnel.ui.Screen
import com.zaneschepke.wireguardautotunnel.ui.common.ClickableIconButton import com.zaneschepke.wireguardautotunnel.ui.common.ClickableIconButton
@ -551,31 +551,43 @@ fun SettingsScreen(
} }
} }
} }
if (WgQuickBackend.hasKernelSupport()) { 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), ) {
Column(
horizontalAlignment = Alignment.Start,
verticalArrangement = Arrangement.Top,
modifier = Modifier.padding(15.dp),
) { ) {
Column( SectionTitle(
horizontalAlignment = Alignment.Start, title = stringResource(id = R.string.backend),
verticalArrangement = Arrangement.Top, padding = screenPadding,
modifier = Modifier.padding(15.dp), )
) { ConfigurationToggle(
SectionTitle( stringResource(R.string.use_amnezia),
title = stringResource(id = R.string.kernel), enabled =
padding = screenPadding, !(uiState.settings.isAutoTunnelEnabled ||
) uiState.settings.isAlwaysOnVpnEnabled ||
(uiState.vpnState.status == TunnelState.UP) || uiState.settings.isKernelEnabled),
checked = uiState.settings.isAmneziaEnabled,
padding = screenPadding,
onCheckChanged = {
viewModel.onToggleAmnezia()
},
)
if (WgQuickBackend.hasKernelSupport()) {
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 == TunnelState.UP)),
checked = uiState.settings.isKernelEnabled, checked = uiState.settings.isKernelEnabled,
padding = screenPadding, padding = screenPadding,
onCheckChanged = { onCheckChanged = {

View File

@ -1,7 +1,7 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.settings package com.zaneschepke.wireguardautotunnel.ui.screens.settings
import com.zaneschepke.wireguardautotunnel.data.model.Settings import com.zaneschepke.wireguardautotunnel.data.domain.Settings
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnState import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnState
data class SettingsUiState( data class SettingsUiState(

View File

@ -8,7 +8,7 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.wireguard.android.util.RootShell import com.wireguard.android.util.RootShell
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
import com.zaneschepke.wireguardautotunnel.data.model.Settings import com.zaneschepke.wireguardautotunnel.data.domain.Settings
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnService import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnService
@ -162,12 +162,29 @@ constructor(
) )
} }
fun onToggleAmnezia() = viewModelScope.launch {
if(uiState.value.settings.isKernelEnabled) {
saveKernelMode(false)
}
saveAmneziaMode(!uiState.value.settings.isAmneziaEnabled)
}
private fun saveAmneziaMode(on: Boolean) {
saveSettings(
uiState.value.settings.copy(
isAmneziaEnabled = on
)
)
}
fun onToggleKernelMode(): Result<Unit> { fun onToggleKernelMode(): Result<Unit> {
if (!uiState.value.settings.isKernelEnabled) { if (!uiState.value.settings.isKernelEnabled) {
try { try {
rootShell.start() rootShell.start()
Timber.i("Root shell accepted!") Timber.i("Root shell accepted!")
saveKernelMode(on = true) saveKernelMode(on = true)
saveAmneziaMode(false)
} catch (e: RootShell.RootShellException) { } catch (e: RootShell.RootShellException) {
Timber.e(e) Timber.e(e)
saveKernelMode(on = false) saveKernelMode(on = false)

View File

@ -107,7 +107,7 @@ fun SupportScreen(
modifier = Modifier.padding(bottom = 20.dp), modifier = Modifier.padding(bottom = 20.dp),
) )
TextButton( TextButton(
onClick = { appViewModel.openWebPage(context.resources.getString(R.string.docs_url)) }, onClick = { appViewModel.openWebPage(context.resources.getString(R.string.docs_url), context) },
modifier = Modifier modifier = Modifier
.padding(vertical = 5.dp) .padding(vertical = 5.dp)
.focusRequester(focusRequester), .focusRequester(focusRequester),
@ -143,7 +143,7 @@ fun SupportScreen(
color = MaterialTheme.colorScheme.onBackground, color = MaterialTheme.colorScheme.onBackground,
) )
TextButton( TextButton(
onClick = { appViewModel.openWebPage(context.resources.getString(R.string.discord_url)) }, onClick = { appViewModel.openWebPage(context.resources.getString(R.string.telegram_url), context) },
modifier = Modifier.padding(vertical = 5.dp), modifier = Modifier.padding(vertical = 5.dp),
) { ) {
Row( Row(
@ -152,7 +152,7 @@ fun SupportScreen(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
) { ) {
Row { Row {
val icon = ImageVector.vectorResource(R.drawable.discord) val icon = ImageVector.vectorResource(R.drawable.telegram)
Icon( Icon(
icon, icon,
icon.name, icon.name,
@ -175,7 +175,7 @@ fun SupportScreen(
color = MaterialTheme.colorScheme.onBackground, color = MaterialTheme.colorScheme.onBackground,
) )
TextButton( TextButton(
onClick = { appViewModel.openWebPage(context.resources.getString(R.string.github_url)) }, onClick = { appViewModel.openWebPage(context.resources.getString(R.string.github_url), context) },
modifier = Modifier.padding(vertical = 5.dp), modifier = Modifier.padding(vertical = 5.dp),
) { ) {
Row( Row(
@ -207,7 +207,7 @@ fun SupportScreen(
color = MaterialTheme.colorScheme.onBackground, color = MaterialTheme.colorScheme.onBackground,
) )
TextButton( TextButton(
onClick = { appViewModel.launchEmail() }, onClick = { appViewModel.launchEmail(context) },
modifier = Modifier.padding(vertical = 5.dp), modifier = Modifier.padding(vertical = 5.dp),
) { ) {
Row( Row(
@ -269,7 +269,7 @@ fun SupportScreen(
fontSize = 16.sp, fontSize = 16.sp,
modifier = modifier =
Modifier.clickable { Modifier.clickable {
appViewModel.openWebPage(context.resources.getString(R.string.privacy_policy_url)) appViewModel.openWebPage(context.resources.getString(R.string.privacy_policy_url), context)
}, },
) )
Row( Row(

View File

@ -1,5 +1,5 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.support package com.zaneschepke.wireguardautotunnel.ui.screens.support
import com.zaneschepke.wireguardautotunnel.data.model.Settings import com.zaneschepke.wireguardautotunnel.data.domain.Settings
data class SupportUiState(val settings: Settings = Settings()) data class SupportUiState(val settings: Settings = Settings())

View File

@ -27,6 +27,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.ClipboardManager import androidx.compose.ui.platform.ClipboardManager
import androidx.compose.ui.platform.LocalClipboardManager import androidx.compose.ui.platform.LocalClipboardManager
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
@ -43,6 +44,8 @@ fun LogsScreen(appViewModel: AppViewModel) {
appViewModel.logs appViewModel.logs
} }
val context = LocalContext.current
val lazyColumnListState = rememberLazyListState() val lazyColumnListState = rememberLazyListState()
val clipboardManager: ClipboardManager = LocalClipboardManager.current val clipboardManager: ClipboardManager = LocalClipboardManager.current
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
@ -57,7 +60,7 @@ fun LogsScreen(appViewModel: AppViewModel) {
floatingActionButton = { floatingActionButton = {
FloatingActionButton( FloatingActionButton(
onClick = { onClick = {
appViewModel.saveLogsToFile() appViewModel.saveLogsToFile(context)
}, },
shape = RoundedCornerShape(16.dp), shape = RoundedCornerShape(16.dp),
containerColor = MaterialTheme.colorScheme.primary, containerColor = MaterialTheme.colorScheme.primary,

View File

@ -2,7 +2,7 @@ package com.zaneschepke.wireguardautotunnel.util
object Constants { object Constants {
const val BASE_LOG_FILE_NAME = "wgtunnel-logs" const val BASE_LOG_FILE_NAME = "wg_tunnel_logs"
const val LOG_BUFFER_SIZE = 3_000L const val LOG_BUFFER_SIZE = 3_000L
const val MANUAL_TUNNEL_CONFIG_ID = "0" const val MANUAL_TUNNEL_CONFIG_ID = "0"

View File

@ -2,11 +2,9 @@ package com.zaneschepke.wireguardautotunnel.util
import android.content.BroadcastReceiver import android.content.BroadcastReceiver
import android.content.pm.PackageInfo import android.content.pm.PackageInfo
import com.wireguard.android.backend.Statistics import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
import com.wireguard.android.backend.Statistics.PeerStats
import com.wireguard.crypto.Key
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
import com.zaneschepke.wireguardautotunnel.service.tunnel.HandshakeStatus import com.zaneschepke.wireguardautotunnel.service.tunnel.HandshakeStatus
import com.zaneschepke.wireguardautotunnel.service.tunnel.statistics.TunnelStatistics
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.GlobalScope
@ -50,15 +48,15 @@ typealias TunnelConfigs = List<TunnelConfig>
typealias Packages = List<PackageInfo> typealias Packages = List<PackageInfo>
fun Statistics.mapPeerStats(): Map<Key, PeerStats?> { fun TunnelStatistics.mapPeerStats(): Map<org.amnezia.awg.crypto.Key, TunnelStatistics.PeerStats?> {
return this.peers().associateWith { key -> (this.peer(key)) } return this.getPeers().associateWith { key -> (this.peerStats(key)) }
} }
fun PeerStats.latestHandshakeSeconds(): Long? { fun TunnelStatistics.PeerStats.latestHandshakeSeconds(): Long? {
return NumberUtils.getSecondsBetweenTimestampAndNow(this.latestHandshakeEpochMillis) return NumberUtils.getSecondsBetweenTimestampAndNow(this.latestHandshakeEpochMillis)
} }
fun PeerStats.handshakeStatus(): HandshakeStatus { fun TunnelStatistics.PeerStats.handshakeStatus(): HandshakeStatus {
// TODO add never connected status after duration // TODO add never connected status after duration
return this.latestHandshakeSeconds().let { return this.latestHandshakeSeconds().let {
when { when {

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="50dp"
android:height="50dp"
android:viewportWidth="50"
android:viewportHeight="50">
<path
android:fillColor="#FF000000"
android:pathData="M25,2c12.703,0 23,10.297 23,23S37.703,48 25,48S2,37.703 2,25S12.297,2 25,2zM32.934,34.375c0.423,-1.298 2.405,-14.234 2.65,-16.783c0.074,-0.772 -0.17,-1.285 -0.648,-1.514c-0.578,-0.278 -1.434,-0.139 -2.427,0.219c-1.362,0.491 -18.774,7.884 -19.78,8.312c-0.954,0.405 -1.856,0.847 -1.856,1.487c0,0.45 0.267,0.703 1.003,0.966c0.766,0.273 2.695,0.858 3.834,1.172c1.097,0.303 2.346,0.04 3.046,-0.395c0.742,-0.461 9.305,-6.191 9.92,-6.693c0.614,-0.502 1.104,0.141 0.602,0.644c-0.502,0.502 -6.38,6.207 -7.155,6.997c-0.941,0.959 -0.273,1.953 0.358,2.351c0.721,0.454 5.906,3.932 6.687,4.49c0.781,0.558 1.573,0.811 2.298,0.811C32.191,36.439 32.573,35.484 32.934,34.375z"/>
</vector>

View File

@ -108,7 +108,6 @@
<string name="discord_description">Join the community</string> <string name="discord_description">Join the community</string>
<string name="email_description">Send me an email</string> <string name="email_description">Send me an email</string>
<string name="support_help_text">If you are experiencing issues, have improvement ideas, or just want to engage, the following resources are available:</string> <string name="support_help_text">If you are experiencing issues, have improvement ideas, or just want to engage, the following resources are available:</string>
<string name="kernel">Kernel</string>
<string name="use_kernel">Use kernel module</string> <string name="use_kernel">Use kernel module</string>
<string name="error_ssid_exists">SSID already exists</string> <string name="error_ssid_exists">SSID already exists</string>
<string name="error_root_denied">Root shell denied</string> <string name="error_root_denied">Root shell denied</string>
@ -140,7 +139,7 @@
<string name="pin_created">Pin successfully created</string> <string name="pin_created">Pin successfully created</string>
<string name="enter_pin">Enter your pin</string> <string name="enter_pin">Enter your pin</string>
<string name="create_pin">Create pin</string> <string name="create_pin">Create pin</string>
<string name="enable_app_lock">Enabled app lock</string> <string name="enable_app_lock">Enable app lock</string>
<string name="restart_on_ping">Restart on ping fail (beta)</string> <string name="restart_on_ping">Restart on ping fail (beta)</string>
<string name="mobile_data_tunnel">Set as mobile data tunnel</string> <string name="mobile_data_tunnel">Set as mobile data tunnel</string>
<string name="set_primary_tunnel">Set as primary tunnel</string> <string name="set_primary_tunnel">Set as primary tunnel</string>
@ -158,4 +157,21 @@
<string name="userspace">Userspace</string> <string name="userspace">Userspace</string>
<string name="settings">Settings</string> <string name="settings">Settings</string>
<string name="support">Support</string> <string name="support">Support</string>
<string name="backend">Backend</string>
<string name="kernel">Kernel</string>
<string name="use_amnezia">"Use Amnezia userspace "</string>
<string name="junk_packet_count">Junk packet count</string>
<string name="junk_packet_minimum_size">Junk packet minimum size</string>
<string name="junk_packet_maximum_size">Junk packet maximum size</string>
<string name="init_packet_junk_size">Init packet junk size</string>
<string name="response_packet_junk_size">Response packet junk size</string>
<string name="init_packet_magic_header">Init packet magic header</string>
<string name="response_packet_magic_header">Response packet magic header</string>
<string name="transport_packet_magic_header">Transport packet magic header</string>
<string name="underload_packet_magic_header">Underload packet magic header</string>
<string name="telegram_url" translatable="false">https://t.me/wgtunnel</string>
<string name="unsure_how">if you are unsure how to proceed</string>
<string name="see_the">See the</string>
<string name="getting_started_url" translatable="false">https://zaneschepke.com/wgtunnel-docs/getting-started.html</string>
<string name="getting_started_guide">Getting started guide</string>
</resources> </resources>

View File

@ -1,7 +1,7 @@
object Constants { object Constants {
const val VERSION_NAME = "3.4.2" const val VERSION_NAME = "3.4.3-beta"
const val JVM_TARGET = "17" const val JVM_TARGET = "17"
const val VERSION_CODE = 34200 const val VERSION_CODE = 34202
const val TARGET_SDK = 34 const val TARGET_SDK = 34
const val MIN_SDK = 26 const val MIN_SDK = 26
const val APP_ID = "com.zaneschepke.wireguardautotunnel" const val APP_ID = "com.zaneschepke.wireguardautotunnel"

View File

@ -0,0 +1,3 @@
What's new:
- Add Amnezia side-by-side with WireGuard
- Fix app shortcuts bug

View File

@ -1,12 +1,13 @@
[versions] [versions]
accompanist = "0.34.0" accompanist = "0.34.0"
activityCompose = "1.8.2" activityCompose = "1.9.0"
amneziawgAndroid = "1.2.0"
androidx-junit = "1.1.5" androidx-junit = "1.1.5"
appcompat = "1.6.1" appcompat = "1.6.1"
biometricKtx = "1.2.0-alpha05" biometricKtx = "1.2.0-alpha05"
coreGoogleShortcuts = "1.1.0" coreGoogleShortcuts = "1.1.0"
coreKtx = "1.12.0" coreKtx = "1.13.1"
datastorePreferences = "1.0.0" datastorePreferences = "1.1.1"
desugar_jdk_libs = "2.0.4" desugar_jdk_libs = "2.0.4"
espressoCore = "3.5.1" espressoCore = "3.5.1"
hiltAndroid = "2.51" hiltAndroid = "2.51"
@ -23,8 +24,8 @@ tunnel = "1.0.20230706"
androidGradlePlugin = "8.4.0-rc02" androidGradlePlugin = "8.4.0-rc02"
kotlin = "1.9.23" kotlin = "1.9.23"
ksp = "1.9.23-1.0.19" ksp = "1.9.23-1.0.19"
composeBom = "2024.03.00" composeBom = "2024.05.00"
compose = "1.6.4" compose = "1.6.7"
zxingAndroidEmbedded = "4.3.0" zxingAndroidEmbedded = "4.3.0"
zxingCore = "3.5.3" zxingCore = "3.5.3"
@ -41,6 +42,7 @@ accompanist-flowlayout = { module = "com.google.accompanist:accompanist-flowlayo
accompanist-permissions = { module = "com.google.accompanist:accompanist-permissions", version.ref = "accompanist" } accompanist-permissions = { module = "com.google.accompanist:accompanist-permissions", version.ref = "accompanist" }
#room #room
amneziawg-android = { module = "com.zaneschepke:amneziawg-android", version.ref = "amneziawgAndroid" }
androidx-biometric-ktx = { module = "androidx.biometric:biometric-ktx", version.ref = "biometricKtx" } androidx-biometric-ktx = { module = "androidx.biometric:biometric-ktx", version.ref = "biometricKtx" }
androidx-core = { module = "androidx.core:core", version.ref = "coreKtx" } androidx-core = { module = "androidx.core:core", version.ref = "coreKtx" }
androidx-core-google-shortcuts = { module = "androidx.core:core-google-shortcuts", version.ref = "coreGoogleShortcuts" } androidx-core-google-shortcuts = { module = "androidx.core:core-google-shortcuts", version.ref = "coreGoogleShortcuts" }