Compare commits

..

22 Commits

Author SHA1 Message Date
Zane Schepke d44baa84a8 feat: improved imports
Added a feature where you can now add a commented "# Name = " property to QR code configs to import them with a name. If there is no name configured, app will use the first peer's host address as a name.

Improved imports so they no longer replace an existing config if that config has the same name. Instead, they will import with a (number) appended for config name duplicates.

Closes #68

Fixed a bug where the initial state of auto tunneling may not be correct and cause unexpected behavior.

Fixed a bug where Amnezia imports were not working when being imported as a zip.

Improved/refactored error handling.
2024-05-11 20:42:24 -04:00
Zane Schepke cb1b8ee7d6 add version metadata 2024-05-11 00:18:58 -04:00
Zane Schepke 4153351fc4 Merge branch 'main' of https://github.com/zaneschepke/wgtunnel 2024-05-10 23:52:43 -04:00
dependabot[bot] 48e6f341cb build(deps): bump actions/upload-artifact from 4.3.1 to 4.3.3 (#179)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-05-10 23:52:03 -04:00
Zane Schepke d531adede5 remove comments 2024-05-10 23:49:38 -04:00
Zane Schepke 2df1bb07ab change auto tunnel to not watch vpn state 2024-05-10 23:46:50 -04:00
Zane Schepke a5e60c3fbe add amnezia import/export 2024-05-10 21:42:59 -04:00
Weblate (bot) e4af481402 Translations update from Hosted Weblate (#206)
Co-authored-by: gallegonovato <fran-carro@hotmail.es>
2024-05-10 12:34:10 -04:00
Weblate (bot) 77b3fc8360 Translations update from Hosted Weblate (#204)
Co-authored-by: gallegonovato <fran-carro@hotmail.es>
2024-05-09 15:26:47 -04:00
Zane Schepke 4fd908f271 fix: notification workflow links 2024-05-08 17:30:00 -04:00
maskedeken 632da245ab fix kernel mode switch not work (#196) 2024-05-08 17:25:28 -04:00
Weblate (bot) 04f22cb92d Translations update from Hosted Weblate (#200)
Co-authored-by: Gabriel Franz <gabrielfranz31@gmail.com>
2024-05-08 13:13:58 -04:00
Weblate (bot) 31194d8b88 Translations update from Hosted Weblate (#194)
Co-authored-by: Mat1RX <ladved@gmail.com>
2024-05-05 21:39:09 -04:00
Zane Schepke 421bf418d1 fix: fastlane metadata 2024-05-05 21:36:44 -04:00
Zane Schepke e84d7e354c feat: add amnezia side-by-side 2024-05-05 00:49:31 -04:00
Zane Schepke 681b066d99 Update issue-workflow.yml 2024-05-03 22:54:35 -04:00
Zane Schepke ad53fca928 Create publish-workflow.yml 2024-05-03 22:53:42 -04:00
Zane Schepke f7e4b7e8ef Update issue-workflow.yml 2024-05-03 22:42:44 -04:00
Zane Schepke b04e8e7f60 Update issue-workflow.yml 2024-05-03 22:38:37 -04:00
Zane Schepke cbee5cfd1b Create issue-workflow.yml 2024-05-03 22:26:05 -04:00
Weblate (bot) 440fe6ceda Translations update from Hosted Weblate (#182)
Co-authored-by: Dominik Thalhammer <dominik@thalhammer.it>
2024-04-29 00:00:28 -04:00
Zane Schepke 27def018bd ci: fix spacing 2024-04-27 21:50:13 -04:00
109 changed files with 2338 additions and 970 deletions
+20
View File
@@ -0,0 +1,20 @@
name: Issue Updates Workflow
on:
issues:
types: [opened, closed, reopened]
jobs:
build:
name: Build
runs-on: ubuntu-latest
steps:
- name: Send Telegram Message
run: |
msg_text='${{ github.actor }} updated an issue:
status: ${{ github.event.issue.state }} - #${{ github.event.issue.number }} ${{ github.event.issue.title }}
https://github.com/zaneschepke/wgtunnel/issues/${{ github.event.issue.number }}'
curl -s -X POST 'https://api.telegram.org/bot${{ secrets.TELEGRAM_TOKEN }}/sendMessage' \
-d "chat_id=${{ secrets.TELEGRAM_TO }}&text=${msg_text}&message_thread_id=${{ secrets.TELEGRAM_TOPIC }}"
+1 -2
View File
@@ -1,4 +1,3 @@
# name of the workflow
name: Android CI Tag Deployment (Pre-release)
on:
@@ -68,7 +67,7 @@ jobs:
echo "VERSION_CODE=$version_code" >> $GITHUB_ENV
# Save the APK after the Build job is complete to publish it as a Github release in the next job
- name: Upload APK
uses: actions/upload-artifact@v4.3.1
uses: actions/upload-artifact@v4.3.3
with:
name: wgtunnel
path: ${{ steps.apk-path.outputs.path }}
+21
View File
@@ -0,0 +1,21 @@
name: Release Updates Workflow
on:
release:
types: [published]
jobs:
build:
name: Build
runs-on: ubuntu-latest
steps:
- name: Send Telegram Message
run: |
msg_text='${{ github.actor }} published a new release:
Release: ${{ github.event.release.tag_name }}
${{ github.event.release.body }}
https://github.com/zaneschepke/wgtunnel/releases/tag/${{ github.event.release.tag_name }}'
curl -s -X POST 'https://api.telegram.org/bot${{ secrets.TELEGRAM_TOKEN }}/sendMessage' \
-d "chat_id=${{ secrets.TELEGRAM_TO }}&text=${msg_text}&message_thread_id=${{ secrets.TELEGRAM_TOPIC }}"
+1 -1
View File
@@ -69,7 +69,7 @@ jobs:
echo "VERSION_CODE=$version_code" >> $GITHUB_ENV
# Save the APK after the Build job is complete to publish it as a Github release in the next job
- name: Upload APK
uses: actions/upload-artifact@v4.3.1
uses: actions/upload-artifact@v4.3.3
with:
name: wgtunnel
path: ${{ steps.apk-path.outputs.path }}
+3 -2
View File
@@ -22,7 +22,7 @@ WG Tunnel
<div align="left">
This is an alternative Android Application for [WireGuard](https://www.wireguard.com/) with added
This is an alternative Android Application for [WireGuard](https://www.wireguard.com/) and [AmneziaWG](https://docs.amnezia.org/documentation/amnezia-wg/) with added
features. Built using the [wireguard-android](https://github.com/WireGuard/wireguard-android)
library and [Jetpack Compose](https://developer.android.com/jetpack/compose), this application was
inspired by the official [WireGuard Android](https://github.com/WireGuard/wireguard-android) app.
@@ -53,8 +53,9 @@ and on while on different networks. This app was created to offer a free solutio
* Auto connect to tunnels based on Wi-Fi SSID, ethernet, or mobile data
* Split tunneling by application with search
* WireGuard support for kernel and userspace modes
* Amnezia support for userspace mode for DPI/censorship protection
* Always-On VPN support
* Export tunnels to zip
* Export Amnezia and WireGuard tunnels to zip
* Quick tile support for tunnel toggling, auto-tunneling
* Static shortcuts support for tunnel toggling, auto-tunneling
* Intent automation support for all tunnels
+5 -1
View File
@@ -139,6 +139,7 @@ dependencies {
implementation(libs.androidx.core.ktx)
implementation(libs.androidx.lifecycle.runtime.ktx)
// helpers for implementing LifecycleOwner in a Service
implementation(libs.androidx.lifecycle.service)
implementation(libs.androidx.activity.compose)
@@ -160,7 +161,8 @@ dependencies {
debugImplementation(libs.androidx.compose.ui.tooling)
debugImplementation(libs.androidx.compose.manifest)
// tunnel
// get tunnel lib from github packages or mavenLocal
implementation(libs.tunnel)
implementation(libs.amneziawg.android)
coreLibraryDesugaring(libs.desugar.jdk.libs)
@@ -172,6 +174,8 @@ dependencies {
implementation(libs.androidx.navigation.compose)
implementation(libs.androidx.hilt.navigation.compose)
implementation(libs.zaneschepke.multifab)
// hilt
implementation(libs.hilt.android)
ksp(libs.hilt.android.compiler)
@@ -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')"
]
}
}
@@ -2,13 +2,14 @@ package com.zaneschepke.wireguardautotunnel
import android.app.Application
import android.content.ComponentName
import android.content.Context
import android.content.pm.PackageManager
import android.service.quicksettings.TileService
import com.zaneschepke.wireguardautotunnel.service.tile.AutoTunnelControlTile
import com.zaneschepke.wireguardautotunnel.service.tile.TunnelControlTile
import com.zaneschepke.wireguardautotunnel.util.ReleaseTree
import dagger.hilt.android.HiltAndroidApp
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.cancel
import timber.log.Timber
import xyz.teamgravity.pin_lock_compose.PinManager
@@ -21,7 +22,16 @@ class WireGuardAutoTunnel : Application() {
PinManager.initialize(this)
}
override fun onLowMemory() {
super.onLowMemory()
applicationScope.cancel("onLowMemory() called by system")
applicationScope = MainScope()
}
companion object {
var applicationScope = MainScope()
lateinit var instance: WireGuardAutoTunnel
private set
@@ -29,16 +39,16 @@ class WireGuardAutoTunnel : Application() {
return instance.packageManager.hasSystemFeature(PackageManager.FEATURE_LEANBACK)
}
fun requestTunnelTileServiceStateUpdate(context: Context) {
fun requestTunnelTileServiceStateUpdate() {
TileService.requestListeningState(
context,
instance,
ComponentName(instance, TunnelControlTile::class.java),
)
}
fun requestAutoTunnelTileServiceUpdate(context: Context) {
fun requestAutoTunnelTileServiceUpdate() {
TileService.requestListeningState(
context,
instance,
ComponentName(instance, AutoTunnelControlTile::class.java),
)
}
@@ -6,12 +6,12 @@ import androidx.room.DeleteColumn
import androidx.room.RoomDatabase
import androidx.room.TypeConverters
import androidx.room.migration.AutoMigrationSpec
import com.zaneschepke.wireguardautotunnel.data.model.Settings
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
import com.zaneschepke.wireguardautotunnel.data.domain.Settings
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
@Database(
entities = [Settings::class, TunnelConfig::class],
version = 7,
version = 8,
autoMigrations =
[
AutoMigration(from = 1, to = 2),
@@ -33,6 +33,7 @@ import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
to = 7,
spec = RemoveLegacySettingColumnsMigration::class,
),
AutoMigration(7, 8)
],
exportSchema = true,
)
@@ -5,7 +5,7 @@ import androidx.room.Delete
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import com.zaneschepke.wireguardautotunnel.data.model.Settings
import com.zaneschepke.wireguardautotunnel.data.domain.Settings
import kotlinx.coroutines.flow.Flow
@Dao
@@ -5,7 +5,7 @@ import androidx.room.Delete
import androidx.room.Insert
import androidx.room.OnConflictStrategy
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 kotlinx.coroutines.flow.Flow
@@ -20,6 +20,9 @@ interface TunnelConfigDao {
@Query("SELECT * FROM TunnelConfig WHERE id=:id")
suspend fun getById(id: Long): TunnelConfig?
@Query("SELECT * FROM TunnelConfig WHERE name=:name")
suspend fun getByName(name: String) : TunnelConfig?
@Query("SELECT * FROM TunnelConfig")
suspend fun getAll(): TunnelConfigs
@@ -1,4 +1,4 @@
package com.zaneschepke.wireguardautotunnel.data.model
package com.zaneschepke.wireguardautotunnel.data.domain
data class GeneralState(
val locationDisclosureShown: Boolean = LOCATION_DISCLOSURE_SHOWN_DEFAULT,
@@ -1,4 +1,4 @@
package com.zaneschepke.wireguardautotunnel.data.model
package com.zaneschepke.wireguardautotunnel.data.domain
import androidx.room.ColumnInfo
import androidx.room.Entity
@@ -50,4 +50,9 @@ data class Settings(
defaultValue = "false",
)
val isPingEnabled: Boolean = false,
@ColumnInfo(
name = "is_amnezia_enabled",
defaultValue = "false",
)
val isAmneziaEnabled: Boolean = false,
)
@@ -1,10 +1,10 @@
package com.zaneschepke.wireguardautotunnel.data.model
package com.zaneschepke.wireguardautotunnel.data.domain
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.Index
import androidx.room.PrimaryKey
import org.amnezia.awg.config.Config
import com.wireguard.config.Config
import java.io.InputStream
@Entity(indices = [Index(value = ["name"], unique = true)])
@@ -27,12 +27,25 @@ data class TunnelConfig(
defaultValue = "false",
)
val isPrimaryTunnel: Boolean = false,
@ColumnInfo(
name = "am_quick",
defaultValue = "",
)
val amQuick: String = AM_QUICK_DEFAULT,
) {
companion object {
fun configFromQuick(wgQuick: String): Config {
fun configFromWgQuick(wgQuick: String): Config {
val inputStream: InputStream = wgQuick.byteInputStream()
val reader = inputStream.bufferedReader(Charsets.UTF_8)
return Config.parse(reader)
return inputStream.bufferedReader(Charsets.UTF_8).use {
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)
}
}
const val AM_QUICK_DEFAULT = ""
}
}
@@ -1,6 +1,6 @@
package com.zaneschepke.wireguardautotunnel.data.repository
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
interface AppDataRepository {
suspend fun getPrimaryOrFirstTunnel(): TunnelConfig?
@@ -1,6 +1,6 @@
package com.zaneschepke.wireguardautotunnel.data.repository
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
import javax.inject.Inject
class AppDataRoomRepository @Inject constructor(
@@ -1,6 +1,6 @@
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
interface AppStateRepository {
@@ -1,7 +1,7 @@
package com.zaneschepke.wireguardautotunnel.data.repository
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.map
import timber.log.Timber
@@ -1,7 +1,7 @@
package com.zaneschepke.wireguardautotunnel.data.repository
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
class RoomSettingsRepository(private val settingsDoa: SettingsDao) : SettingsRepository {
@@ -1,7 +1,7 @@
package com.zaneschepke.wireguardautotunnel.data.repository
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 kotlinx.coroutines.flow.Flow
@@ -54,6 +54,10 @@ class RoomTunnelConfigRepository(private val tunnelConfigDao: TunnelConfigDao) :
return tunnelConfigDao.count().toInt()
}
override suspend fun findByTunnelName(name: String): TunnelConfig? {
return tunnelConfigDao.getByName(name)
}
override suspend fun findByTunnelNetworksName(name: String): TunnelConfigs {
return tunnelConfigDao.findByTunnelNetworkName(name)
}
@@ -1,6 +1,6 @@
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
interface SettingsRepository {
@@ -1,6 +1,6 @@
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 kotlinx.coroutines.flow.Flow
@@ -22,6 +22,8 @@ interface TunnelConfigRepository {
suspend fun count(): Int
suspend fun findByTunnelName(name : String) : TunnelConfig?
suspend fun findByTunnelNetworksName(name: String): TunnelConfigs
suspend fun findByMobileDataTunnel(): TunnelConfigs
@@ -1,6 +1,11 @@
package com.zaneschepke.wireguardautotunnel.module
import android.content.Context
import com.wireguard.android.backend.Backend
import com.wireguard.android.backend.GoBackend
import com.wireguard.android.backend.WgQuickBackend
import com.wireguard.android.util.RootShell
import com.wireguard.android.util.ToolsInstaller
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnService
@@ -10,11 +15,6 @@ import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import org.amnezia.awg.backend.AwgQuickBackend
import org.amnezia.awg.backend.Backend
import org.amnezia.awg.backend.GoBackend
import org.amnezia.awg.util.RootShell
import org.amnezia.awg.util.ToolsInstaller
import javax.inject.Singleton
@Module
@@ -37,17 +37,24 @@ class TunnelModule {
@Singleton
@Kernel
fun provideKernelBackend(@ApplicationContext context: Context, rootShell: RootShell): Backend {
return AwgQuickBackend(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
@Singleton
fun provideVpnService(
amneziaBackend: org.amnezia.awg.backend.Backend,
@Userspace userspaceBackend: Backend,
@Kernel kernelBackend: Backend,
appDataRepository: AppDataRepository
): VpnService {
return WireGuardTunnel(userspaceBackend, kernelBackend, appDataRepository)
return WireGuardTunnel(amneziaBackend,userspaceBackend, kernelBackend, appDataRepository)
}
@Provides
@@ -1,84 +1,63 @@
package com.zaneschepke.wireguardautotunnel.service.foreground
import com.zaneschepke.wireguardautotunnel.data.model.Settings
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
import org.amnezia.awg.backend.Tunnel
import com.zaneschepke.wireguardautotunnel.data.domain.Settings
data class WatcherState(
val isWifiConnected: Boolean = false,
val config: TunnelConfig? = null,
val vpnStatus: Tunnel.State = Tunnel.State.DOWN,
val isEthernetConnected: Boolean = false,
val isMobileDataConnected: Boolean = false,
val currentNetworkSSID: String = "",
val settings: Settings = Settings()
) {
private fun isVpnConnected() = vpnStatus == Tunnel.State.UP
fun isEthernetConditionMet(): Boolean {
return (isEthernetConnected &&
settings.isTunnelOnEthernetEnabled &&
!isVpnConnected())
settings.isTunnelOnEthernetEnabled)
}
fun isMobileDataConditionMet(): Boolean {
return (!isEthernetConnected &&
settings.isTunnelOnMobileDataEnabled &&
!isWifiConnected &&
isMobileDataConnected &&
!isVpnConnected())
isMobileDataConnected)
}
fun isTunnelNotMobileDataPreferredConditionMet(): Boolean {
fun isTunnelOnMobileDataPreferredConditionMet(): Boolean {
return (!isEthernetConnected &&
settings.isTunnelOnMobileDataEnabled &&
!isWifiConnected &&
isMobileDataConnected &&
config?.isMobileDataTunnel == false && isVpnConnected())
isMobileDataConnected)
}
fun isTunnelOffOnMobileDataConditionMet(): Boolean {
return (!isEthernetConnected &&
!settings.isTunnelOnMobileDataEnabled &&
isMobileDataConnected &&
!isWifiConnected &&
isVpnConnected())
!isWifiConnected)
}
fun isUntrustedWifiConditionMet(): Boolean {
return (!isEthernetConnected &&
isWifiConnected &&
!settings.trustedNetworkSSIDs.contains(currentNetworkSSID) &&
settings.isTunnelOnWifiEnabled
&& !isVpnConnected())
}
fun isTunnelNotWifiNamePreferredMet(ssid: String): Boolean {
return (!isEthernetConnected &&
isWifiConnected &&
!settings.trustedNetworkSSIDs.contains(currentNetworkSSID) &&
settings.isTunnelOnWifiEnabled && config?.tunnelNetworks?.contains(ssid) == false && isVpnConnected())
settings.isTunnelOnWifiEnabled)
}
fun isTrustedWifiConditionMet(): Boolean {
return (!isEthernetConnected &&
(isWifiConnected &&
settings.trustedNetworkSSIDs.contains(currentNetworkSSID)) &&
(isVpnConnected()))
settings.trustedNetworkSSIDs.contains(currentNetworkSSID)))
}
fun isTunnelOffOnWifiConditionMet(): Boolean {
return (!isEthernetConnected &&
(isWifiConnected &&
!settings.isTunnelOnWifiEnabled &&
(isVpnConnected())))
!settings.isTunnelOnWifiEnabled))
}
fun isTunnelOffOnNoConnectivityMet(): Boolean {
return (!isEthernetConnected &&
!isWifiConnected &&
!isMobileDataConnected &&
(isVpnConnected()))
!isMobileDataConnected)
}
}
@@ -6,7 +6,7 @@ import android.os.PowerManager
import androidx.core.app.ServiceCompat
import androidx.lifecycle.lifecycleScope
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.service.network.EthernetService
import com.zaneschepke.wireguardautotunnel.service.network.MobileDataService
@@ -14,6 +14,7 @@ import com.zaneschepke.wireguardautotunnel.service.network.NetworkService
import com.zaneschepke.wireguardautotunnel.service.network.NetworkStatus
import com.zaneschepke.wireguardautotunnel.service.network.WifiService
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.util.Constants
import dagger.hilt.android.AndroidEntryPoint
@@ -23,8 +24,8 @@ import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import org.amnezia.awg.backend.Tunnel
import timber.log.Timber
import java.net.InetAddress
import javax.inject.Inject
@@ -162,10 +163,6 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
watchForEthernetConnectivityChanges()
}
}
launch {
Timber.i("Starting vpn state watcher")
watchForVpnConnectivityChanges()
}
launch {
Timber.i("Starting settings watcher")
watchForSettingsChanges()
@@ -185,29 +182,32 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
}
private suspend fun watchForMobileDataConnectivityChanges() {
mobileDataService.networkStatus.collect {
when (it) {
mobileDataService.networkStatus.collect { status ->
when (status) {
is NetworkStatus.Available -> {
Timber.i("Gained Mobile data connection")
networkEventsFlow.value =
networkEventsFlow.value.copy(
networkEventsFlow.update {
it.copy(
isMobileDataConnected = true,
)
}
}
is NetworkStatus.CapabilitiesChanged -> {
networkEventsFlow.value =
networkEventsFlow.value.copy(
networkEventsFlow.update {
it.copy(
isMobileDataConnected = true,
)
}
Timber.i("Mobile data capabilities changed")
}
is NetworkStatus.Unavailable -> {
networkEventsFlow.value =
networkEventsFlow.value.copy(
networkEventsFlow.update {
it.copy(
isMobileDataConnected = false,
)
}
Timber.i("Lost mobile data connection")
}
}
@@ -217,15 +217,15 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
private suspend fun watchForPingFailure() {
try {
do {
if (vpnService.vpnState.value.status == Tunnel.State.UP) {
if (vpnService.vpnState.value.status == TunnelState.UP) {
val tunnelConfig = vpnService.vpnState.value.tunnelConfig
tunnelConfig?.let {
val config = TunnelConfig.configFromQuick(it.wgQuick)
val config = TunnelConfig.configFromWgQuick(it.wgQuick)
val results = config.peers.map { peer ->
val host = if (peer.endpoint.isPresent &&
peer.endpoint.get().resolved.isPresent)
peer.endpoint.get().resolved.get().host
else Constants.BACKUP_PING_HOST
else Constants.DEFAULT_PING_IP
Timber.i("Checking reachability of: $host")
val reachable = InetAddress.getByName(host)
.isReachable(Constants.PING_TIMEOUT.toInt())
@@ -236,7 +236,7 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
Timber.i("Restarting VPN for ping failure")
serviceManager.stopVpnServiceForeground(this)
delay(Constants.VPN_RESTART_DELAY)
serviceManager.startVpnServiceForeground(this)
serviceManager.startVpnServiceForeground(this, it.id)
delay(Constants.PING_COOLDOWN)
}
}
@@ -249,54 +249,48 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
}
private suspend fun watchForSettingsChanges() {
appDataRepository.settings.getSettingsFlow().collect {
if (networkEventsFlow.value.settings.isAutoTunnelPaused != it.isAutoTunnelPaused) {
when (it.isAutoTunnelPaused) {
appDataRepository.settings.getSettingsFlow().collect { settings ->
if (networkEventsFlow.value.settings.isAutoTunnelPaused != settings.isAutoTunnelPaused) {
when (settings.isAutoTunnelPaused) {
true -> launchWatcherPausedNotification()
false -> launchWatcherNotification()
}
}
networkEventsFlow.value =
networkEventsFlow.value.copy(
settings = it,
)
}
}
private suspend fun watchForVpnConnectivityChanges() {
vpnService.vpnState.collect {
networkEventsFlow.value =
networkEventsFlow.value.copy(
vpnStatus = it.status,
config = it.tunnelConfig,
networkEventsFlow.update {
it.copy(
settings = settings,
)
}
}
}
private suspend fun watchForEthernetConnectivityChanges() {
ethernetService.networkStatus.collect {
when (it) {
ethernetService.networkStatus.collect { status ->
when (status) {
is NetworkStatus.Available -> {
Timber.i("Gained Ethernet connection")
networkEventsFlow.value =
networkEventsFlow.value.copy(
networkEventsFlow.update {
it.copy(
isEthernetConnected = true,
)
}
}
is NetworkStatus.CapabilitiesChanged -> {
Timber.i("Ethernet capabilities changed")
networkEventsFlow.value =
networkEventsFlow.value.copy(
networkEventsFlow.update {
it.copy(
isEthernetConnected = true,
)
}
}
is NetworkStatus.Unavailable -> {
networkEventsFlow.value =
networkEventsFlow.value.copy(
networkEventsFlow.update {
it.copy(
isEthernetConnected = false,
)
}
Timber.i("Lost Ethernet connection")
}
}
@@ -304,40 +298,43 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
}
private suspend fun watchForWifiConnectivityChanges() {
wifiService.networkStatus.collect {
when (it) {
wifiService.networkStatus.collect { status ->
when (status) {
is NetworkStatus.Available -> {
Timber.i("Gained Wi-Fi connection")
networkEventsFlow.value =
networkEventsFlow.value.copy(
networkEventsFlow.update {
it.copy(
isWifiConnected = true,
)
}
}
is NetworkStatus.CapabilitiesChanged -> {
Timber.i("Wifi capabilities changed")
networkEventsFlow.value =
networkEventsFlow.value.copy(
networkEventsFlow.update {
it.copy(
isWifiConnected = true,
)
val ssid = wifiService.getNetworkName(it.networkCapabilities)
ssid?.let {
if(it.contains(Constants.UNREADABLE_SSID)) {
}
val ssid = wifiService.getNetworkName(status.networkCapabilities)
ssid?.let { name ->
if(name.contains(Constants.UNREADABLE_SSID)) {
Timber.w("SSID unreadable: missing permissions")
} else Timber.i("Detected valid SSID")
appDataRepository.appState.setCurrentSsid(ssid)
networkEventsFlow.value =
networkEventsFlow.value.copy(
currentNetworkSSID = ssid,
appDataRepository.appState.setCurrentSsid(name)
networkEventsFlow.update {
it.copy(
currentNetworkSSID = name,
)
}
} ?: Timber.w("Failed to read ssid")
}
is NetworkStatus.Unavailable -> {
networkEventsFlow.value =
networkEventsFlow.value.copy(
networkEventsFlow.update {
it.copy(
isWifiConnected = false,
)
}
Timber.i("Lost Wi-Fi connection")
}
}
@@ -352,72 +349,74 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
return appDataRepository.tunnels.findByTunnelNetworksName(ssid).firstOrNull()
}
private fun isTunnelDown() : Boolean {
return vpnService.vpnState.value.status == TunnelState.DOWN
}
private suspend fun manageVpn() {
networkEventsFlow.collectLatest { watcherState ->
val autoTunnel = "Auto-tunnel watcher"
if (!watcherState.settings.isAutoTunnelPaused) {
//delay for rapid network state changes and then collect latest
delay(Constants.WATCHER_COLLECTION_DELAY)
val tunnelConfig = vpnService.vpnState.value.tunnelConfig
when {
watcherState.isEthernetConditionMet() -> {
Timber.i("$autoTunnel - tunnel on on ethernet condition met")
serviceManager.startVpnServiceForeground(this)
if(isTunnelDown()) serviceManager.startVpnServiceForeground(this)
}
watcherState.isMobileDataConditionMet() -> {
Timber.i("$autoTunnel - tunnel on on mobile data condition met")
serviceManager.startVpnServiceForeground(this, getMobileDataTunnel()?.id)
if(isTunnelDown()) serviceManager.startVpnServiceForeground(this, getMobileDataTunnel()?.id)
}
watcherState.isTunnelNotMobileDataPreferredConditionMet() -> {
getMobileDataTunnel()?.let {
Timber.i("$autoTunnel - tunnel connected on mobile data is not preferred condition met, switching to preferred")
serviceManager.startVpnServiceForeground(
this,
getMobileDataTunnel()?.id,
)
watcherState.isTunnelOnMobileDataPreferredConditionMet() -> {
if(tunnelConfig?.isMobileDataTunnel == false) {
getMobileDataTunnel()?.let {
Timber.i("$autoTunnel - tunnel connected on mobile data is not preferred condition met, switching to preferred")
if(isTunnelDown()) serviceManager.startVpnServiceForeground(
this,
getMobileDataTunnel()?.id,
)
}
}
}
watcherState.isTunnelOffOnMobileDataConditionMet() -> {
Timber.i("$autoTunnel - tunnel off on mobile data met, turning vpn off")
serviceManager.stopVpnServiceForeground(this)
}
watcherState.isTunnelNotWifiNamePreferredMet(watcherState.currentNetworkSSID) -> {
Timber.i("$autoTunnel - tunnel on ssid not associated with current tunnel condition met")
getSsidTunnel(watcherState.currentNetworkSSID)?.let {
Timber.i("Found tunnel associated with this SSID, bringing tunnel up")
serviceManager.startVpnServiceForeground(this, it.id)
} ?: suspend {
Timber.i("No tunnel associated with this SSID, using defaults")
if (appDataRepository.getPrimaryOrFirstTunnel()?.name != vpnService.name) {
serviceManager.startVpnServiceForeground(this)
}
}.invoke()
if(!isTunnelDown()) serviceManager.stopVpnServiceForeground(this)
}
watcherState.isUntrustedWifiConditionMet() -> {
Timber.i("$autoTunnel - tunnel on untrusted wifi condition met")
serviceManager.startVpnServiceForeground(
this,
getSsidTunnel(watcherState.currentNetworkSSID)?.id,
)
if(tunnelConfig?.tunnelNetworks?.contains(watcherState.currentNetworkSSID) == false ||
tunnelConfig == null) {
Timber.i("$autoTunnel - tunnel on ssid not associated with current tunnel condition met")
getSsidTunnel(watcherState.currentNetworkSSID)?.let {
Timber.i("Found tunnel associated with this SSID, bringing tunnel up")
if(isTunnelDown()) serviceManager.startVpnServiceForeground(this, it.id)
} ?: suspend {
Timber.i("No tunnel associated with this SSID, using defaults")
if (appDataRepository.getPrimaryOrFirstTunnel()?.name != vpnService.name) {
if(isTunnelDown()) serviceManager.startVpnServiceForeground(this)
}
}.invoke()
}
}
watcherState.isTrustedWifiConditionMet() -> {
Timber.i("$autoTunnel - tunnel off on trusted wifi condition met, turning vpn off")
serviceManager.stopVpnServiceForeground(this)
if(!isTunnelDown()) serviceManager.stopVpnServiceForeground(this)
}
watcherState.isTunnelOffOnWifiConditionMet() -> {
Timber.i("$autoTunnel - tunnel off on wifi condition met, turning vpn off")
serviceManager.stopVpnServiceForeground(this)
if(!isTunnelDown()) serviceManager.stopVpnServiceForeground(this)
}
watcherState.isTunnelOffOnNoConnectivityMet() -> {
Timber.i("$autoTunnel - tunnel off on no connectivity met, turning vpn off")
serviceManager.stopVpnServiceForeground(this)
if(!isTunnelDown()) serviceManager.stopVpnServiceForeground(this)
}
else -> {
@@ -10,6 +10,7 @@ import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
import com.zaneschepke.wireguardautotunnel.receiver.NotificationActionReceiver
import com.zaneschepke.wireguardautotunnel.service.notification.NotificationService
import com.zaneschepke.wireguardautotunnel.service.tunnel.HandshakeStatus
import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelState
import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnService
import com.zaneschepke.wireguardautotunnel.util.Constants
import com.zaneschepke.wireguardautotunnel.util.handshakeStatus
@@ -20,7 +21,6 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import org.amnezia.awg.backend.Tunnel
import timber.log.Timber
import javax.inject.Inject
@@ -58,7 +58,7 @@ class WireGuardTunnelService : ForegroundService() {
lifecycleScope.launch(Dispatchers.IO) {
launch {
val tunnelId = extras?.getInt(Constants.TUNNEL_EXTRA_KEY)
if (vpnService.getState() == Tunnel.State.UP) {
if (vpnService.getState() == TunnelState.UP) {
vpnService.stopTunnel()
}
vpnService.startTunnel(
@@ -77,7 +77,7 @@ class WireGuardTunnelService : ForegroundService() {
private suspend fun handshakeNotifications() {
var tunnelName: String? = null
vpnService.vpnState.collect { state ->
state.statistics
state.statistics
?.mapPeerStats()
?.map { it.value?.handshakeStatus() }
.let { statuses ->
@@ -102,7 +102,7 @@ class WireGuardTunnelService : ForegroundService() {
else -> {}
}
}
if (state.status == Tunnel.State.UP && state.tunnelConfig?.name != tunnelName) {
if (state.status == TunnelState.UP && state.tunnelConfig?.name != tunnelName) {
tunnelName = state.tunnelConfig?.name
launchVpnNotification(
getString(R.string.tunnel_start_title),
@@ -2,7 +2,7 @@ package com.zaneschepke.wireguardautotunnel.service.shortcut
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.lifecycle.lifecycleScope
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
import com.zaneschepke.wireguardautotunnel.service.foreground.Action
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
@@ -24,7 +24,7 @@ class ShortcutsActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
lifecycleScope.launch(Dispatchers.Main) {
WireGuardAutoTunnel.applicationScope.launch(Dispatchers.IO) {
val settings = appDataRepository.settings.getSettings()
if (settings.isShortcutsEnabled) {
when (intent.getStringExtra(CLASS_NAME_EXTRA_KEY)) {
@@ -4,7 +4,7 @@ import android.os.Build
import android.service.quicksettings.Tile
import android.service.quicksettings.TileService
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.service.foreground.ServiceManager
import dagger.hilt.android.AndroidEntryPoint
@@ -3,16 +3,16 @@ package com.zaneschepke.wireguardautotunnel.service.tile
import android.os.Build
import android.service.quicksettings.Tile
import android.service.quicksettings.TileService
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.service.foreground.ServiceManager
import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelState
import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnService
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch
import org.amnezia.awg.backend.Tunnel
import timber.log.Timber
import javax.inject.Inject
@@ -38,12 +38,12 @@ class TunnelControlTile : TileService() {
scope.launch {
vpnService.vpnState.collect { it ->
when (it.status) {
Tunnel.State.UP -> {
TunnelState.UP -> {
setActive()
it.tunnelConfig?.name?.let { name -> setTileDescription(name) }
}
Tunnel.State.DOWN -> {
TunnelState.DOWN -> {
setInactive()
val config = appDataRepository.getStartTunnelConfig()?.also { config ->
manualStartConfig = config
@@ -52,7 +52,6 @@ class TunnelControlTile : TileService() {
setTileDescription(it.name)
} ?: setUnavailable()
}
else -> setInactive()
}
}
@@ -79,7 +78,7 @@ class TunnelControlTile : TileService() {
unlockAndRun {
scope.launch {
try {
if (vpnService.getState() == Tunnel.State.UP) {
if (vpnService.getState() == TunnelState.UP) {
serviceManager.stopVpnServiceForeground(
this@TunnelControlTile,
isManualStop = true,
@@ -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
}
}
}
}
@@ -1,15 +1,15 @@
package com.zaneschepke.wireguardautotunnel.service.tunnel
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
import com.wireguard.android.backend.Tunnel
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
import kotlinx.coroutines.flow.StateFlow
import org.amnezia.awg.backend.Tunnel
interface VpnService : Tunnel {
suspend fun startTunnel(tunnelConfig: TunnelConfig? = null): Tunnel.State
interface VpnService : Tunnel, org.amnezia.awg.backend.Tunnel {
suspend fun startTunnel(tunnelConfig: TunnelConfig? = null): TunnelState
suspend fun stopTunnel()
val vpnState: StateFlow<VpnState>
fun getState(): Tunnel.State
fun getState(): TunnelState
}
@@ -1,11 +1,10 @@
package com.zaneschepke.wireguardautotunnel.service.tunnel
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
import org.amnezia.awg.backend.Statistics
import org.amnezia.awg.backend.Tunnel
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
import com.zaneschepke.wireguardautotunnel.service.tunnel.statistics.TunnelStatistics
data class VpnState(
val status: Tunnel.State = Tunnel.State.DOWN,
val status: TunnelState = TunnelState.DOWN,
val tunnelConfig: TunnelConfig? = null,
val statistics: Statistics? = null
val statistics: TunnelStatistics? = null
)
@@ -1,10 +1,16 @@
package com.zaneschepke.wireguardautotunnel.service.tunnel
import com.wireguard.android.backend.Backend
import com.wireguard.android.backend.BackendException
import com.wireguard.android.backend.Tunnel.State
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.module.Kernel
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 kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineScope
@@ -15,9 +21,6 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
import org.amnezia.awg.backend.Backend
import org.amnezia.awg.backend.BackendException
import org.amnezia.awg.backend.Statistics
import org.amnezia.awg.backend.Tunnel
import timber.log.Timber
import javax.inject.Inject
@@ -25,6 +28,7 @@ import javax.inject.Inject
class WireGuardTunnel
@Inject
constructor(
private val userspaceAmneziaBackend : org.amnezia.awg.backend.Backend,
@Userspace private val userspaceBackend: Backend,
@Kernel private val kernelBackend: Backend,
private val appDataRepository: AppDataRepository,
@@ -38,46 +42,70 @@ constructor(
private var backend: Backend = userspaceBackend
private var backendIsUserspace = true
private var backendIsWgUserspace = true
private var backendIsAmneziaUserspace = false
init {
scope.launch {
appDataRepository.settings.getSettingsFlow().collect {
if (it.isKernelEnabled && backendIsUserspace) {
if (it.isKernelEnabled && (backendIsWgUserspace || backendIsAmneziaUserspace)) {
Timber.d("Setting kernel backend")
backend = kernelBackend
backendIsUserspace = false
} else if (!it.isKernelEnabled && !backendIsUserspace) {
Timber.d("Setting userspace backend")
backendIsWgUserspace = false
backendIsAmneziaUserspace = false
} else if (!it.isKernelEnabled && !it.isAmneziaEnabled && !backendIsWgUserspace) {
Timber.d("Setting WireGuard userspace backend")
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?): Tunnel.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 {
//TODO we need better error handling here
val config = tunnelConfig ?: appDataRepository.getPrimaryOrFirstTunnel()
if (config != null) {
emitTunnelConfig(config)
val wgConfig = TunnelConfig.configFromQuick(config.wgQuick)
val state =
backend.setState(
this,
Tunnel.State.UP,
wgConfig,
)
state
setState(config, TunnelState.UP)
} else throw Exception("No tunnels")
} catch (e: BackendException) {
Timber.e("Failed to start tunnel with error: ${e.message}")
Tunnel.State.DOWN
TunnelState.from(State.DOWN)
}
}
private fun emitTunnelState(state: Tunnel.State) {
private fun emitTunnelState(state : TunnelState) {
_vpnState.tryEmit(
_vpnState.value.copy(
status = state,
@@ -85,7 +113,7 @@ constructor(
)
}
private fun emitBackendStatistics(statistics: Statistics) {
private fun emitBackendStatistics(statistics: TunnelStatistics) {
_vpnState.tryEmit(
_vpnState.value.copy(
statistics = statistics,
@@ -101,40 +129,56 @@ constructor(
)
}
private fun resetVpnState() {
_vpnState.tryEmit(VpnState())
}
override suspend fun stopTunnel() {
try {
if (getState() == Tunnel.State.UP) {
val state = backend.setState(this, Tunnel.State.DOWN, null)
if (getState() == TunnelState.UP) {
val state = setState(null, TunnelState.DOWN)
resetVpnState()
emitTunnelState(state)
}
} 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(): Tunnel.State {
return backend.getState(this)
override fun getState(): TunnelState {
return if(backendIsAmneziaUserspace) TunnelState.from(userspaceAmneziaBackend.getState(this))
else TunnelState.from(backend.getState(this))
}
override fun getName(): String {
return _vpnState.value.tunnelConfig?.name ?: ""
}
override fun onStateChange(state: Tunnel.State) {
override fun onStateChange(newState: Tunnel.State) {
handleStateChange(TunnelState.from(newState))
}
private fun handleStateChange(state: TunnelState) {
val tunnel = this
emitTunnelState(state)
WireGuardAutoTunnel.requestTunnelTileServiceStateUpdate(WireGuardAutoTunnel.instance)
if (state == Tunnel.State.UP) {
WireGuardAutoTunnel.requestTunnelTileServiceStateUpdate()
if (state == TunnelState.UP) {
statsJob =
scope.launch {
while (true) {
val statistics = backend.getStatistics(tunnel)
emitBackendStatistics(statistics)
if(backendIsAmneziaUserspace) {
emitBackendStatistics(AmneziaStatistics(userspaceAmneziaBackend.getStatistics(tunnel)))
} else {
emitBackendStatistics(WireGuardStatistics(backend.getStatistics(tunnel)))
}
delay(Constants.VPN_STATISTIC_CHECK_INTERVAL)
}
}
}
if (state == Tunnel.State.DOWN) {
if (state == TunnelState.DOWN) {
try {
statsJob?.cancel()
} catch (e : CancellationException) {
@@ -142,4 +186,8 @@ constructor(
}
}
}
override fun onStateChange(state: State) {
handleStateChange(TunnelState.from(state))
}
}
@@ -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()
}
}
@@ -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
}
@@ -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()
}
}
@@ -1,13 +1,14 @@
package com.zaneschepke.wireguardautotunnel.ui
import android.app.Application
import android.content.ActivityNotFoundException
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.widget.Toast
import androidx.compose.runtime.mutableStateListOf
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.wireguard.android.backend.GoBackend
import com.zaneschepke.logcatter.Logcatter
import com.zaneschepke.logcatter.model.LogMessage
import com.zaneschepke.wireguardautotunnel.R
@@ -18,8 +19,8 @@ import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import org.amnezia.awg.backend.GoBackend
import timber.log.Timber
import java.time.Instant
import javax.inject.Inject
@@ -27,9 +28,7 @@ import javax.inject.Inject
@HiltViewModel
class AppViewModel
@Inject
constructor(
private val application: Application,
) : ViewModel() {
constructor() : ViewModel() {
val vpnIntent: Intent? = GoBackend.VpnService.prepare(WireGuardAutoTunnel.instance)
@@ -49,68 +48,78 @@ constructor(
}
private fun requestPermissions() {
_appUiState.value = _appUiState.value.copy(
requestPermissions = true,
)
_appUiState.update {
it.copy(
requestPermissions = true
)
}
}
fun permissionsRequested() {
_appUiState.value = _appUiState.value.copy(
requestPermissions = false,
)
_appUiState.update {
it.copy(
requestPermissions = false
)
}
}
fun openWebPage(url: String) {
fun openWebPage(url: String, context : Context) {
try {
val webpage: Uri = Uri.parse(url)
val intent = Intent(Intent.ACTION_VIEW, webpage).apply {
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
}
application.startActivity(intent)
context.startActivity(intent)
} catch (e: ActivityNotFoundException) {
Timber.e(e)
showSnackbarMessage(application.getString(R.string.no_browser_detected))
showSnackbarMessage(context.getString(R.string.no_browser_detected))
}
}
fun onVpnPermissionAccepted() {
_appUiState.value = _appUiState.value.copy(
vpnPermissionAccepted = true,
)
_appUiState.update {
it.copy(
vpnPermissionAccepted = true
)
}
}
fun launchEmail() {
fun launchEmail(context: Context) {
try {
val intent =
Intent(Intent.ACTION_SENDTO).apply {
type = Constants.EMAIL_MIME_TYPE
putExtra(Intent.EXTRA_EMAIL, arrayOf(application.getString(R.string.my_email)))
putExtra(Intent.EXTRA_SUBJECT, application.getString(R.string.email_subject))
putExtra(Intent.EXTRA_EMAIL, arrayOf(context.getString(R.string.my_email)))
putExtra(Intent.EXTRA_SUBJECT, context.getString(R.string.email_subject))
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
}
application.startActivity(
Intent.createChooser(intent, application.getString(R.string.email_chooser)).apply {
context.startActivity(
Intent.createChooser(intent, context.getString(R.string.email_chooser)).apply {
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
},
)
} catch (e: ActivityNotFoundException) {
Timber.e(e)
showSnackbarMessage(application.getString(R.string.no_email_detected))
showSnackbarMessage(context.getString(R.string.no_email_detected))
}
}
fun showSnackbarMessage(message: String) {
_appUiState.value = _appUiState.value.copy(
snackbarMessage = message,
snackbarMessageConsumed = false,
)
_appUiState.update {
it.copy(
snackbarMessage = message,
snackbarMessageConsumed = false,
)
}
}
fun snackbarMessageConsumed() {
_appUiState.value = _appUiState.value.copy(
snackbarMessage = "",
snackbarMessageConsumed = true,
)
_appUiState.update {
it.copy(
snackbarMessage = "",
snackbarMessageConsumed = true,
)
}
}
val logs = mutableStateListOf<LogMessage>()
@@ -132,17 +141,19 @@ constructor(
Logcatter.clear()
}
fun saveLogsToFile() {
fun saveLogsToFile(context: Context) {
val fileName = "${Constants.BASE_LOG_FILE_NAME}-${Instant.now().epochSecond}.txt"
val content = logs.joinToString(separator = "\n")
FileUtils.saveFileToDownloads(application.applicationContext, content, fileName)
Toast.makeText(application, application.getString(R.string.logs_saved), Toast.LENGTH_SHORT)
FileUtils.saveFileToDownloads(context.applicationContext, content, fileName)
Toast.makeText(context, context.getString(R.string.logs_saved), Toast.LENGTH_SHORT)
.show()
}
fun setNotificationPermissionAccepted(accepted: Boolean) {
_appUiState.value = _appUiState.value.copy(
notificationPermissionAccepted = accepted,
)
_appUiState.update {
it.copy(
notificationPermissionAccepted = accepted,
)
}
}
}
@@ -32,10 +32,12 @@ import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.lifecycleScope
import androidx.navigation.NavType
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.currentBackStackEntryAsState
import androidx.navigation.compose.rememberNavController
import androidx.navigation.navArgument
import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.google.accompanist.permissions.isGranted
import com.google.accompanist.permissions.rememberPermissionState
@@ -47,6 +49,7 @@ import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.BottomNavBar
import com.zaneschepke.wireguardautotunnel.ui.common.prompt.CustomSnackBar
import com.zaneschepke.wireguardautotunnel.ui.screens.config.ConfigScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.main.ConfigType
import com.zaneschepke.wireguardautotunnel.ui.screens.main.MainScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.options.OptionsScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.pinlock.PinLockScreen
@@ -84,7 +87,7 @@ class MainActivity : AppCompatActivity() {
// load preferences into memory and init data
lifecycleScope.launch {
dataStoreManager.init()
WireGuardAutoTunnel.requestTunnelTileServiceStateUpdate(this@MainActivity)
WireGuardAutoTunnel.requestTunnelTileServiceStateUpdate()
val settings = settingsRepository.getSettings()
if (settings.isAutoTunnelEnabled) {
serviceManager.startWatcherService(application.applicationContext)
@@ -138,7 +141,11 @@ class MainActivity : AppCompatActivity() {
return@LaunchedEffect notificationPermissionState.launchPermissionRequest()
}
if (!appUiState.vpnPermissionAccepted) {
return@LaunchedEffect vpnActivityResultState.launch(appViewModel.vpnIntent)
return@LaunchedEffect appViewModel.vpnIntent?.let {
vpnActivityResultState.launch(
it
)
}!!
}
}
}
@@ -232,14 +239,28 @@ class MainActivity : AppCompatActivity() {
composable(Screen.Support.Logs.route) {
LogsScreen(appViewModel)
}
composable("${Screen.Config.route}/{id}") {
//TODO fix navigation for amnezia
composable("${Screen.Config.route}/{id}?configType={configType}", arguments =
listOf(
navArgument("id") {
type = NavType.StringType
defaultValue = "0"
},
navArgument("configType") {
type = NavType.StringType
defaultValue = ConfigType.WIREGUARD.name
}
)
) {
val id = it.arguments?.getString("id")
val configType = ConfigType.valueOf( it.arguments?.getString("configType") ?: ConfigType.WIREGUARD.name)
if (!id.isNullOrBlank()) {
ConfigScreen(
navController = navController,
tunnelId = id,
appViewModel = appViewModel,
focusRequester = focusRequester,
configType = configType
)
}
}
@@ -4,11 +4,9 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.Home
import androidx.compose.material.icons.rounded.QuestionMark
import androidx.compose.material.icons.rounded.Settings
import androidx.compose.ui.res.stringResource
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.BottomNavItem
import com.zaneschepke.wireguardautotunnel.util.StringValue
sealed class Screen(val route: String) {
data object Main : Screen("main") {
@@ -17,9 +17,9 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.zaneschepke.wireguardautotunnel.service.tunnel.statistics.TunnelStatistics
import com.zaneschepke.wireguardautotunnel.util.NumberUtils
import com.zaneschepke.wireguardautotunnel.util.toThreeDecimalPlaceString
import org.amnezia.awg.backend.Statistics
@OptIn(ExperimentalFoundationApi::class)
@Composable
@@ -30,7 +30,7 @@ fun RowListItem(
onClick: () -> Unit,
rowButton: @Composable () -> Unit,
expanded: Boolean,
statistics: Statistics?
statistics: TunnelStatistics?
) {
Box(
modifier =
@@ -59,7 +59,7 @@ fun RowListItem(
rowButton()
}
if (expanded) {
statistics?.peers()?.forEach {
statistics?.getPeers()?.forEach {
Row(
modifier =
Modifier
@@ -69,9 +69,9 @@ fun RowListItem(
horizontalArrangement = Arrangement.SpaceEvenly,
) {
//TODO change these to string resources
val handshakeEpoch = statistics.peer(it)!!.latestHandshakeEpochMillis
val peerTx = statistics.peer(it)!!.txBytes
val peerRx = statistics.peer(it)!!.rxBytes
val handshakeEpoch = statistics.peerStats(it)!!.latestHandshakeEpochMillis
val peerTx = statistics.peerStats(it)!!.txBytes
val peerRx = statistics.peerStats(it)!!.rxBytes
val peerId = it.toBase64().subSequence(0, 3).toString() + "***"
val handshakeSec =
NumberUtils.getSecondsBetweenTimestampAndNow(handshakeEpoch)
@@ -1,30 +0,0 @@
package com.zaneschepke.wireguardautotunnel.ui.models
import org.amnezia.awg.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 "",
)
}
}
}
@@ -79,9 +79,9 @@ import com.zaneschepke.wireguardautotunnel.ui.common.config.ConfigurationTextBox
import com.zaneschepke.wireguardautotunnel.ui.common.prompt.AuthorizationPrompt
import com.zaneschepke.wireguardautotunnel.ui.common.screen.LoadingScreen
import com.zaneschepke.wireguardautotunnel.ui.common.text.SectionTitle
import com.zaneschepke.wireguardautotunnel.ui.screens.main.ConfigType
import com.zaneschepke.wireguardautotunnel.util.Constants
import com.zaneschepke.wireguardautotunnel.util.Event
import com.zaneschepke.wireguardautotunnel.util.Result
import com.zaneschepke.wireguardautotunnel.util.getMessage
import kotlinx.coroutines.delay
@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter")
@@ -94,7 +94,8 @@ fun ConfigScreen(
focusRequester: FocusRequester,
navController: NavController,
appViewModel: AppViewModel,
tunnelId: String
tunnelId: String,
configType: ConfigType
) {
val context = LocalContext.current
val clipboardManager: ClipboardManager = LocalClipboardManager.current
@@ -148,11 +149,11 @@ fun ConfigScreen(
},
onError = {
showAuthPrompt = false
appViewModel.showSnackbarMessage(Event.Error.AuthenticationFailed.message)
appViewModel.showSnackbarMessage(context.getString(R.string.error_authentication_failed))
},
onFailure = {
showAuthPrompt = false
appViewModel.showSnackbarMessage(Event.Error.AuthorizationFailed.message)
appViewModel.showSnackbarMessage(context.getString(R.string.error_authorization_failed))
},
)
}
@@ -319,15 +320,11 @@ fun ConfigScreen(
}
},
onClick = {
viewModel.onSaveAllChanges().let {
when (it) {
is Result.Success -> {
appViewModel.showSnackbarMessage(it.data.message)
navController.navigate(Screen.Main.route)
}
is Result.Error -> appViewModel.showSnackbarMessage(it.error.message)
}
viewModel.onSaveAllChanges(configType).onSuccess {
appViewModel.showSnackbarMessage(context.getString(R.string.config_changes_saved))
navController.navigate(Screen.Main.route)
}.onFailure {
appViewModel.showSnackbarMessage(it.getMessage(context))
}
},
containerColor = fobColor,
@@ -486,6 +483,98 @@ fun ConfigScreen(
modifier = Modifier.width(IntrinsicSize.Min),
)
}
if(configType == ConfigType.AMNEZIA) {
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.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),
)
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),
)
}
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
@@ -1,8 +1,9 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.config
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
import com.zaneschepke.wireguardautotunnel.ui.models.InterfaceProxy
import com.zaneschepke.wireguardautotunnel.ui.models.PeerProxy
import com.wireguard.config.Config
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
import com.zaneschepke.wireguardautotunnel.ui.screens.config.model.InterfaceProxy
import com.zaneschepke.wireguardautotunnel.ui.screens.config.model.PeerProxy
import com.zaneschepke.wireguardautotunnel.util.Packages
data class ConfigUiState(
@@ -14,5 +15,58 @@ data class ConfigUiState(
val isAllApplicationsEnabled: Boolean = false,
val loading: Boolean = true,
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,
)
}
}
}
@@ -1,33 +1,35 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.config
import android.Manifest
import android.app.Application
import android.content.pm.PackageInfo
import android.content.pm.PackageManager
import android.os.Build
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.wireguard.config.Config
import com.wireguard.config.Interface
import com.wireguard.config.Peer
import com.wireguard.crypto.Key
import com.wireguard.crypto.KeyPair
import com.zaneschepke.wireguardautotunnel.R
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.ui.models.InterfaceProxy
import com.zaneschepke.wireguardautotunnel.ui.models.PeerProxy
import com.zaneschepke.wireguardautotunnel.data.repository.SettingsRepository
import com.zaneschepke.wireguardautotunnel.ui.screens.config.model.PeerProxy
import com.zaneschepke.wireguardautotunnel.ui.screens.main.ConfigType
import com.zaneschepke.wireguardautotunnel.util.Constants
import com.zaneschepke.wireguardautotunnel.util.Event
import com.zaneschepke.wireguardautotunnel.util.NumberUtils
import com.zaneschepke.wireguardautotunnel.util.Result
import com.zaneschepke.wireguardautotunnel.util.StringValue
import com.zaneschepke.wireguardautotunnel.util.WgTunnelExceptions
import com.zaneschepke.wireguardautotunnel.util.removeAt
import com.zaneschepke.wireguardautotunnel.util.update
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import org.amnezia.awg.config.Config
import org.amnezia.awg.config.Interface
import org.amnezia.awg.config.Peer
import org.amnezia.awg.crypto.Key
import org.amnezia.awg.crypto.KeyPair
import timber.log.Timber
import javax.inject.Inject
@@ -35,11 +37,11 @@ import javax.inject.Inject
class ConfigViewModel
@Inject
constructor(
private val application: Application,
private val settingsRepository: SettingsRepository,
private val appDataRepository: AppDataRepository
) : ViewModel() {
private val packageManager = application.packageManager
private val packageManager = WireGuardAutoTunnel.instance.packageManager
private val _uiState = MutableStateFlow(ConfigUiState())
val uiState = _uiState.asStateFlow()
@@ -52,32 +54,17 @@ constructor(
val tunnelConfig =
appDataRepository.tunnels.getAll()
.firstOrNull { it.id.toString() == tunnelId }
val isAmneziaEnabled = settingsRepository.getSettings().isAmneziaEnabled
if (tunnelConfig != null) {
val config = TunnelConfig.configFromQuick(tunnelConfig.wgQuick)
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()
}
ConfigUiState(
proxyPeers,
proxyInterface,
packages,
checkedPackages.toList(),
include,
isAllApplicationsEnabled,
false,
tunnelConfig,
tunnelConfig.name,
(if(isAmneziaEnabled) {
val amConfig = if(tunnelConfig.amQuick == "") tunnelConfig.wgQuick else tunnelConfig.amQuick
ConfigUiState.from(TunnelConfig.configFromAmQuick(amConfig))
} else ConfigUiState.from(TunnelConfig.configFromWgQuick(tunnelConfig.wgQuick))).copy(
packages = packages,
loading = false,
tunnel = tunnelConfig,
tunnelName = tunnelConfig.name,
isAmneziaEnabled = isAmneziaEnabled
)
} else {
ConfigUiState(loading = false, packages = packages)
@@ -121,7 +108,7 @@ constructor(
}
fun getPackageLabel(packageInfo: PackageInfo): String {
return packageInfo.applicationInfo.loadLabel(application.packageManager).toString()
return packageInfo.applicationInfo.loadLabel(packageManager).toString()
}
private fun getAllInternetCapablePackages(): List<PackageInfo> {
@@ -150,7 +137,7 @@ constructor(
viewModelScope.launch {
if (tunnelConfig != null) {
saveConfig(tunnelConfig).join()
WireGuardAutoTunnel.requestTunnelTileServiceStateUpdate(application)
WireGuardAutoTunnel.requestTunnelTileServiceStateUpdate()
}
}
@@ -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() {
_uiState.value = _uiState.value.copy(checkedPackageNames = emptyList())
}
@@ -190,147 +191,226 @@ constructor(
return builder.build()
}
fun onSaveAllChanges(): Result<Event> {
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(configType: ConfigType): Result<Unit> {
return try {
val peerList = buildPeerListFromProxyPeers()
val wgInterface = buildInterfaceListFromProxyInterface()
val config = Config.Builder().addPeers(peerList).setInterface(wgInterface).build()
val wgQuick = buildConfig().toWgQuickString()
val amQuick = if(configType == ConfigType.AMNEZIA) {
buildAmConfig().toAwgQuickString()
} else TunnelConfig.AM_QUICK_DEFAULT
val tunnelConfig = when (uiState.value.tunnel) {
null -> TunnelConfig(
name = _uiState.value.tunnelName,
wgQuick = config.toAwgQuickString(),
wgQuick = wgQuick,
amQuick = amQuick
)
else -> uiState.value.tunnel!!.copy(
name = _uiState.value.tunnelName,
wgQuick = config.toAwgQuickString(),
wgQuick = wgQuick,
amQuick = amQuick
)
}
updateTunnelConfig(tunnelConfig)
Result.Success(Event.Message.ConfigSaved)
Result.success(Unit)
} catch (e: Exception) {
Timber.e(e)
val message = e.message?.substringAfter(":", missingDelimiterValue = "")
Result.Error(Event.Error.ConfigParseError(message ?: ""))
val stringValue = message?.let {
StringValue.DynamicString(message)
} ?: StringValue.StringResource(R.string.unknown_error)
Result.failure(WgTunnelExceptions.ConfigParseError(stringValue))
}
}
fun onPeerPublicKeyChange(index: Int, value: String) {
_uiState.value =
_uiState.value.copy(
_uiState.update {
it.copy(
proxyPeers =
_uiState.value.proxyPeers.update(
index,
_uiState.value.proxyPeers[index].copy(publicKey = value),
),
)
}
}
fun onPreSharedKeyChange(index: Int, value: String) {
_uiState.value =
_uiState.value.copy(
_uiState.update {
it.copy(
proxyPeers =
_uiState.value.proxyPeers.update(
index,
_uiState.value.proxyPeers[index].copy(preSharedKey = value),
),
)
}
}
fun onEndpointChange(index: Int, value: String) {
_uiState.value =
_uiState.value.copy(
_uiState.update {
it.copy(
proxyPeers =
_uiState.value.proxyPeers.update(
index,
_uiState.value.proxyPeers[index].copy(endpoint = value),
),
)
}
}
fun onAllowedIpsChange(index: Int, value: String) {
_uiState.value =
_uiState.value.copy(
_uiState.update {
it.copy(
proxyPeers =
_uiState.value.proxyPeers.update(
index,
_uiState.value.proxyPeers[index].copy(allowedIps = value),
),
)
}
}
fun onPersistentKeepaliveChanged(index: Int, value: String) {
_uiState.value =
_uiState.value.copy(
_uiState.update {
it.copy(
proxyPeers =
_uiState.value.proxyPeers.update(
index,
_uiState.value.proxyPeers[index].copy(persistentKeepalive = value),
),
)
}
}
fun onDeletePeer(index: Int) {
_uiState.value =
_uiState.value.copy(
_uiState.update {
it.copy(
proxyPeers = _uiState.value.proxyPeers.removeAt(index),
)
}
}
fun addEmptyPeer() {
_uiState.value = _uiState.value.copy(proxyPeers = _uiState.value.proxyPeers + PeerProxy())
_uiState.update {
it.copy(proxyPeers = _uiState.value.proxyPeers + PeerProxy())
}
}
fun generateKeyPair() {
val keyPair = KeyPair()
_uiState.value =
_uiState.value.copy(
_uiState.update {
it.copy(
interfaceProxy =
_uiState.value.interfaceProxy.copy(
privateKey = keyPair.privateKey.toBase64(),
publicKey = keyPair.publicKey.toBase64(),
),
)
}
}
fun onAddressesChanged(value: String) {
_uiState.value =
_uiState.value.copy(
_uiState.update {
it.copy(
interfaceProxy = _uiState.value.interfaceProxy.copy(addresses = value),
)
}
}
fun onListenPortChanged(value: String) {
_uiState.value =
_uiState.value.copy(
_uiState.update {
it.copy(
interfaceProxy = _uiState.value.interfaceProxy.copy(listenPort = value),
)
}
}
fun onDnsServersChanged(value: String) {
_uiState.value =
_uiState.value.copy(
_uiState.update {
it.copy(
interfaceProxy = _uiState.value.interfaceProxy.copy(dnsServers = value),
)
}
}
fun onMtuChanged(value: String) {
_uiState.value =
_uiState.value.copy(interfaceProxy = _uiState.value.interfaceProxy.copy(mtu = value))
_uiState.update {
it.copy(interfaceProxy = _uiState.value.interfaceProxy.copy(mtu = value))
}
}
private fun onInterfacePublicKeyChange(value: String) {
_uiState.value =
_uiState.value.copy(
_uiState.update {
it.copy(
interfaceProxy = _uiState.value.interfaceProxy.copy(publicKey = value),
)
}
}
fun onPrivateKeyChange(value: String) {
_uiState.value =
_uiState.value.copy(
_uiState.update {
it.copy(
interfaceProxy = _uiState.value.interfaceProxy.copy(privateKey = value),
)
}
if (NumberUtils.isValidKey(value)) {
val pair = KeyPair(Key.fromBase64(value))
onInterfacePublicKeyChange(pair.publicKey.toBase64())
@@ -344,6 +424,77 @@ constructor(
getAllInternetCapablePackages().filter {
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)
)
}
}
}
@@ -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 "",
transportPacketMagicHeader = if(i.transportPacketMagicHeader.isPresent) i.transportPacketMagicHeader.get().toString() else "",
underloadPacketMagicHeader = if(i.underloadPacketMagicHeader.isPresent) i.underloadPacketMagicHeader.get().toString() else "",
)
}
}
}
@@ -1,13 +1,13 @@
package com.zaneschepke.wireguardautotunnel.ui.models
package com.zaneschepke.wireguardautotunnel.ui.screens.config.model
import org.amnezia.awg.config.Peer
import com.wireguard.config.Peer
data class PeerProxy(
var publicKey: String = "",
var preSharedKey: String = "",
var persistentKeepalive: String = "",
var endpoint: String = "",
var allowedIps: String = IPV4_WILDCARD.joinToString(", ").trim()
val publicKey: String = "",
val preSharedKey: String = "",
val persistentKeepalive: String = "",
val endpoint: String = "",
val allowedIps: String = IPV4_WILDCARD.joinToString(", ").trim()
) {
companion object {
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 =
setOf(
"0.0.0.0/5",
@@ -0,0 +1,6 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.main
enum class ConfigType {
AMNEZIA,
WIREGUARD
}
@@ -29,11 +29,11 @@ import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.overscroll
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.ClickableText
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Create
import androidx.compose.material.icons.filled.FileOpen
import androidx.compose.material.icons.filled.QrCode
import androidx.compose.material.icons.rounded.Add
import androidx.compose.material.icons.rounded.Bolt
import androidx.compose.material.icons.rounded.Circle
import androidx.compose.material.icons.rounded.CopyAll
@@ -45,7 +45,6 @@ import androidx.compose.material.icons.rounded.Star
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.FabPosition
import androidx.compose.material3.FloatingActionButton
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
@@ -81,18 +80,26 @@ import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.buildAnnotatedString
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.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.navigation.NavController
import com.iamageo.multifablibrary.FabIcon
import com.iamageo.multifablibrary.FabOption
import com.iamageo.multifablibrary.MultiFabItem
import com.iamageo.multifablibrary.MultiFloatingActionButton
import com.journeyapps.barcodescanner.ScanContract
import com.journeyapps.barcodescanner.ScanOptions
import com.zaneschepke.wireguardautotunnel.R
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.TunnelState
import com.zaneschepke.wireguardautotunnel.ui.AppViewModel
import com.zaneschepke.wireguardautotunnel.ui.CaptureActivityPortrait
import com.zaneschepke.wireguardautotunnel.ui.Screen
@@ -101,15 +108,13 @@ import com.zaneschepke.wireguardautotunnel.ui.common.screen.LoadingScreen
import com.zaneschepke.wireguardautotunnel.ui.theme.corn
import com.zaneschepke.wireguardautotunnel.ui.theme.mint
import com.zaneschepke.wireguardautotunnel.util.Constants
import com.zaneschepke.wireguardautotunnel.util.Event
import com.zaneschepke.wireguardautotunnel.util.Result
import com.zaneschepke.wireguardautotunnel.util.getMessage
import com.zaneschepke.wireguardautotunnel.util.handshakeStatus
import com.zaneschepke.wireguardautotunnel.util.mapPeerStats
import com.zaneschepke.wireguardautotunnel.util.truncateWithEllipsis
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import org.amnezia.awg.backend.Tunnel
@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter")
@OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class)
@@ -127,6 +132,7 @@ fun MainScreen(
val sheetState = rememberModalBottomSheetState()
var showBottomSheet by remember { mutableStateOf(false) }
var configType by remember { mutableStateOf(ConfigType.WIREGUARD) }
// Nested scroll for control FAB
val nestedScrollConnection = remember {
@@ -187,7 +193,7 @@ fun MainScreen(
name.startsWith(Constants.ANDROID_TV_EXPLORER_STUB)
}
) {
appViewModel.showSnackbarMessage(Event.Error.FileExplorerRequired.message)
appViewModel.showSnackbarMessage(context.getString(R.string.error_no_file_explorer))
}
return intent
}
@@ -195,11 +201,8 @@ fun MainScreen(
) { data ->
if (data == null) return@rememberLauncherForActivityResult
scope.launch {
viewModel.onTunnelFileSelected(data).let {
when (it) {
is Result.Error -> appViewModel.showSnackbarMessage(it.error.message)
is Result.Success -> {}
}
viewModel.onTunnelFileSelected(data, configType, context).onFailure {
appViewModel.showSnackbarMessage(it.getMessage(context))
}
}
}
@@ -209,11 +212,8 @@ fun MainScreen(
onResult = {
if (it.contents != null) {
scope.launch {
viewModel.onTunnelQrResult(it.contents).let { result ->
when (result) {
is Result.Success -> {}
is Result.Error -> appViewModel.showSnackbarMessage(result.error.message)
}
viewModel.onTunnelQrResult(it.contents, configType).onFailure {
appViewModel.showSnackbarMessage(it.getMessage(context))
}
}
}
@@ -226,7 +226,7 @@ fun MainScreen(
confirmButton = {
TextButton(
onClick = {
selectedTunnel?.let { viewModel.onDelete(it) }
selectedTunnel?.let { viewModel.onDelete(it, context) }
showDeleteTunnelAlertDialog = false
selectedTunnel = null
},
@@ -246,7 +246,7 @@ fun MainScreen(
fun onTunnelToggle(checked: Boolean, tunnel: TunnelConfig) {
if (appViewModel.isRequiredPermissionGranted()) {
if (checked) viewModel.onTunnelStart(tunnel) else viewModel.onTunnelStop()
if (checked) viewModel.onTunnelStart(tunnel, context) else viewModel.onTunnelStop(context)
}
}
@@ -254,14 +254,30 @@ fun MainScreen(
return LoadingScreen()
}
fun launchQrScanner() {
val scanOptions = ScanOptions()
scanOptions.setDesiredBarcodeFormats(ScanOptions.QR_CODE)
scanOptions.setOrientationLocked(true)
scanOptions.setPrompt(
context.getString(R.string.scanning_qr),
)
scanOptions.setBeepEnabled(false)
scanOptions.captureActivity =
CaptureActivityPortrait::class.java
scanLauncher.launch(scanOptions)
}
Scaffold(
modifier =
Modifier.pointerInput(Unit) {
detectTapGestures(
onTap = {
selectedTunnel = null
},
)
if(uiState.tunnels.isNotEmpty()) {
detectTapGestures(
onTap = {
selectedTunnel = null
},
)
}
},
floatingActionButtonPosition = FabPosition.End,
floatingActionButton = {
@@ -273,7 +289,7 @@ fun MainScreen(
val secondaryColor = MaterialTheme.colorScheme.secondary
val hoverColor = MaterialTheme.colorScheme.surfaceColorAtElevation(2.dp)
var fobColor by remember { mutableStateOf(secondaryColor) }
FloatingActionButton(
MultiFloatingActionButton(
modifier =
(if (
WireGuardAutoTunnel.isRunningOnAndroidTv() &&
@@ -286,29 +302,45 @@ fun MainScreen(
fobColor = if (it.isFocused) hoverColor else secondaryColor
}
},
onClick = { showBottomSheet = true },
containerColor = fobColor,
fabIcon = FabIcon(
iconRes = R.drawable.add,
iconResAfterRotate = R.drawable.close,
iconRotate = 180f
),
fabOption = FabOption(
iconTint = MaterialTheme.colorScheme.background,
backgroundTint = MaterialTheme.colorScheme.primary,
),
itemsMultiFab = listOf(
MultiFabItem(
label = {
Text(
stringResource(id = R.string.amnezia),
color = Color.White,
textAlign = TextAlign.Center,
modifier = Modifier.padding(end = 10.dp)
)
},
icon = R.drawable.add,
value = ConfigType.AMNEZIA.name,
),
MultiFabItem(
label = {
Text(stringResource(id = R.string.wireguard), color = Color.White, textAlign = TextAlign.Center, modifier = Modifier.padding(end = 10.dp))
},
icon = R.drawable.add,
value = ConfigType.WIREGUARD.name
),
),
onFabItemClicked = {
showBottomSheet = true
configType = ConfigType.valueOf(it.value)
},
shape = RoundedCornerShape(16.dp),
) {
Icon(
imageVector = Icons.Rounded.Add,
contentDescription = stringResource(id = R.string.add_tunnel),
tint = Color.DarkGray,
)
}
)
}
},
) {
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) {
ModalBottomSheet(
onDismissRequest = { showBottomSheet = false },
@@ -344,16 +376,7 @@ fun MainScreen(
.clickable {
scope.launch {
showBottomSheet = false
val scanOptions = ScanOptions()
scanOptions.setDesiredBarcodeFormats(ScanOptions.QR_CODE)
scanOptions.setOrientationLocked(true)
scanOptions.setPrompt(
context.getString(R.string.scanning_qr),
)
scanOptions.setBeepEnabled(false)
scanOptions.captureActivity =
CaptureActivityPortrait::class.java
scanLauncher.launch(scanOptions)
launchQrScanner()
}
}
.padding(10.dp),
@@ -377,7 +400,7 @@ fun MainScreen(
.clickable {
showBottomSheet = false
navController.navigate(
"${Screen.Config.route}/${Constants.MANUAL_TUNNEL_CONFIG_ID}",
"${Screen.Config.route}/${Constants.MANUAL_TUNNEL_CONFIG_ID}?configType=${configType}",
)
}
.padding(10.dp),
@@ -401,12 +424,46 @@ fun MainScreen(
modifier =
Modifier
.fillMaxSize()
.overscroll(ScrollableDefaults.overscrollEffect()).nestedScroll(nestedScrollConnection),
.overscroll(ScrollableDefaults.overscrollEffect())
.nestedScroll(nestedScrollConnection),
state = rememberLazyListState(0, uiState.tunnels.count()),
userScrollEnabled = true,
reverseLayout = false,
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 {
if (uiState.settings.isAutoTunnelEnabled) {
val autoTunnelingLabel = buildAnnotatedString {
@@ -462,7 +519,7 @@ fun MainScreen(
val leadingIconColor =
(if (
uiState.vpnState.tunnelConfig?.name == tunnel.name &&
uiState.vpnState.status == Tunnel.State.UP
uiState.vpnState.status == TunnelState.UP
) {
uiState.vpnState.statistics
?.mapPeerStats()
@@ -508,10 +565,10 @@ fun MainScreen(
text = tunnel.name.truncateWithEllipsis(Constants.ALLOWED_DISPLAY_NAME_LENGTH),
onHold = {
if (
(uiState.vpnState.status == Tunnel.State.UP) &&
(uiState.vpnState.status == TunnelState.UP) &&
(tunnel.name == uiState.vpnState.tunnelConfig?.name)
) {
appViewModel.showSnackbarMessage(Event.Message.TunnelOffAction.message)
appViewModel.showSnackbarMessage(context.getString(R.string.turn_off_tunnel))
return@RowListItem
}
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
@@ -520,7 +577,7 @@ fun MainScreen(
onClick = {
if (!WireGuardAutoTunnel.isRunningOnAndroidTv()) {
if (
uiState.vpnState.status == Tunnel.State.UP &&
uiState.vpnState.status == TunnelState.UP &&
(uiState.vpnState.tunnelConfig?.name == tunnel.name)
) {
expanded.value = !expanded.value
@@ -545,7 +602,7 @@ fun MainScreen(
!uiState.settings.isAutoTunnelPaused
) {
appViewModel.showSnackbarMessage(
Event.Message.AutoTunnelOffAction.message,
context.getString(R.string.turn_off_tunnel),
)
} else {
navController.navigate(
@@ -578,7 +635,7 @@ fun MainScreen(
} else {
val checked by remember {
derivedStateOf {
(uiState.vpnState.status == Tunnel.State.UP &&
(uiState.vpnState.status == TunnelState.UP &&
tunnel.name == uiState.vpnState.tunnelConfig?.name)
}
}
@@ -600,7 +657,7 @@ fun MainScreen(
onClick = {
if (uiState.settings.isAutoTunnelEnabled) {
appViewModel.showSnackbarMessage(
Event.Message.AutoTunnelOffAction.message,
context.getString(R.string.turn_off_auto),
)
} else {
selectedTunnel = tunnel
@@ -620,13 +677,13 @@ fun MainScreen(
modifier = Modifier.focusRequester(focusRequester),
onClick = {
if (
uiState.vpnState.status == Tunnel.State.UP &&
uiState.vpnState.status == TunnelState.UP &&
(uiState.vpnState.tunnelConfig?.name == tunnel.name)
) {
expanded.value = !expanded.value
} else {
appViewModel.showSnackbarMessage(
Event.Message.TunnelOnAction.message,
context.getString(R.string.turn_on_tunnel),
)
}
},
@@ -643,11 +700,11 @@ fun MainScreen(
IconButton(
onClick = {
if (
uiState.vpnState.status == Tunnel.State.UP &&
uiState.vpnState.status == TunnelState.UP &&
tunnel.name == uiState.vpnState.tunnelConfig?.name
) {
appViewModel.showSnackbarMessage(
Event.Message.TunnelOffAction.message,
context.getString(R.string.turn_off_tunnel),
)
} else {
selectedTunnel = tunnel
@@ -1,6 +1,6 @@
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.util.TunnelConfigs
@@ -1,22 +1,22 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.main
import android.app.Application
import android.content.Context
import android.database.Cursor
import android.net.Uri
import android.provider.OpenableColumns
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.wireguard.config.Config
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
import com.zaneschepke.wireguardautotunnel.data.model.Settings
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
import com.zaneschepke.wireguardautotunnel.data.domain.Settings
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnService
import com.zaneschepke.wireguardautotunnel.util.Constants
import com.zaneschepke.wireguardautotunnel.util.Event
import com.zaneschepke.wireguardautotunnel.util.NumberUtils
import com.zaneschepke.wireguardautotunnel.util.Result
import com.zaneschepke.wireguardautotunnel.util.WgTunnelExceptions
import com.zaneschepke.wireguardautotunnel.util.toWgQuickString
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.SharingStarted
@@ -24,7 +24,6 @@ import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.amnezia.awg.config.Config
import timber.log.Timber
import java.io.InputStream
import java.util.zip.ZipInputStream
@@ -34,7 +33,6 @@ import javax.inject.Inject
class MainViewModel
@Inject
constructor(
private val application: Application,
private val appDataRepository: AppDataRepository,
private val serviceManager: ServiceManager,
val vpnService: VpnService
@@ -54,21 +52,21 @@ constructor(
MainUiState(),
)
private fun stopWatcherService() =
private fun stopWatcherService(context: Context) =
viewModelScope.launch(Dispatchers.IO) {
serviceManager.stopWatcherService(application.applicationContext)
serviceManager.stopWatcherService(context)
}
fun onDelete(tunnel: TunnelConfig) {
fun onDelete(tunnel: TunnelConfig, context: Context) {
viewModelScope.launch(Dispatchers.IO) {
val settings = appDataRepository.settings.getSettings()
val isPrimary = tunnel.isPrimaryTunnel
if (appDataRepository.tunnels.count() == 1 || isPrimary) {
stopWatcherService()
stopWatcherService(context)
resetTunnelSetting(settings)
}
appDataRepository.tunnels.delete(tunnel)
WireGuardAutoTunnel.requestTunnelTileServiceStateUpdate(application)
WireGuardAutoTunnel.requestTunnelTileServiceStateUpdate()
}
}
@@ -81,80 +79,133 @@ constructor(
)
}
fun onTunnelStart(tunnelConfig: TunnelConfig) =
fun onTunnelStart(tunnelConfig: TunnelConfig, context: Context) =
viewModelScope.launch(Dispatchers.IO) {
Timber.d("On start called!")
serviceManager.startVpnService(
application.applicationContext,
context,
tunnelConfig.id,
isManualStart = true,
)
}
fun onTunnelStop() =
fun onTunnelStop(context: Context) =
viewModelScope.launch(Dispatchers.IO) {
Timber.i("Stopping active tunnel")
serviceManager.stopVpnService(application.applicationContext, isManualStop = true)
serviceManager.stopVpnService(context, isManualStop = true)
}
private fun validateConfigString(config: String) {
TunnelConfig.configFromQuick(config)
private fun validateConfigString(config: String, configType: ConfigType) {
when(configType) {
ConfigType.AMNEZIA -> TunnelConfig.configFromAmQuick(config)
ConfigType.WIREGUARD -> TunnelConfig.configFromWgQuick(config)
}
}
suspend fun onTunnelQrResult(result: String): Result<Unit> {
private fun generateQrCodeDefaultName(config : String, configType: ConfigType) : String {
return try {
validateConfigString(result)
val tunnelConfig =
TunnelConfig(name = NumberUtils.generateRandomTunnelName(), wgQuick = result)
when(configType) {
ConfigType.AMNEZIA -> {
TunnelConfig.configFromAmQuick(config).peers[0].endpoint.get().host
}
ConfigType.WIREGUARD -> {
TunnelConfig.configFromWgQuick(config).peers[0].endpoint.get().host
}
}
} catch (e : Exception) {
Timber.e(e)
NumberUtils.generateRandomTunnelName()
}
}
private fun generateQrCodeTunnelName(config : String, configType: ConfigType) : String {
var defaultName = generateQrCodeDefaultName(config, configType)
val lines = config.lines().toMutableList()
val linesIterator = lines.iterator()
while(linesIterator.hasNext()) {
val next = linesIterator.next()
if(next.contains(Constants.QR_CODE_NAME_PROPERTY)) {
defaultName = next.substringAfter(Constants.QR_CODE_NAME_PROPERTY).trim()
break
}
}
return defaultName
}
suspend fun onTunnelQrResult(result: String, configType: ConfigType): Result<Unit> {
return try {
validateConfigString(result, configType)
val tunnelName = makeTunnelNameUnique(generateQrCodeTunnelName(result, configType))
val tunnelConfig = when(configType) {
ConfigType.AMNEZIA ->{
TunnelConfig(name = tunnelName, amQuick = result,
wgQuick = TunnelConfig.configFromAmQuick(result).toWgQuickString())
}
ConfigType.WIREGUARD -> TunnelConfig(name = tunnelName, wgQuick = result)
}
addTunnel(tunnelConfig)
Result.Success(Unit)
Result.success(Unit)
} catch (e: Exception) {
Timber.e(e)
Result.Error(Event.Error.InvalidQrCode)
Result.failure(WgTunnelExceptions.InvalidQrCode())
}
}
private suspend fun saveTunnelConfigFromStream(stream: InputStream, fileName: String) {
val bufferReader = stream.bufferedReader(charset = Charsets.UTF_8)
val config = Config.parse(bufferReader)
val tunnelName = getNameFromFileName(fileName)
addTunnel(TunnelConfig(name = tunnelName, wgQuick = config.toAwgQuickString()))
withContext(Dispatchers.IO) { stream.close() }
private suspend fun makeTunnelNameUnique(name : String) : String {
val tunnels = appDataRepository.tunnels.getAll()
var tunnelName = name
var num = 1
while (tunnels.any { it.name == tunnelName }) {
tunnelName = name + "(${num})"
num++
}
return tunnelName
}
private fun getInputStreamFromUri(uri: Uri): InputStream? {
return application.applicationContext.contentResolver.openInputStream(uri)
}
suspend fun onTunnelFileSelected(uri: Uri): Result<Unit> {
try {
if (isValidUriContentScheme(uri)) {
val fileName = getFileName(application.applicationContext, uri)
when (getFileExtensionFromFileName(fileName)) {
Constants.CONF_FILE_EXTENSION ->
saveTunnelFromConfUri(fileName, uri).let {
when (it) {
is Result.Error -> return Result.Error(Event.Error.FileReadFailed)
is Result.Success -> return it
}
}
Constants.ZIP_FILE_EXTENSION -> saveTunnelsFromZipUri(uri)
else -> return Result.Error(Event.Error.InvalidFileExtension)
private suspend fun saveTunnelConfigFromStream(stream: InputStream, fileName: String, type: ConfigType) {
var amQuick : String? = null
val wgQuick = stream.use {
when(type) {
ConfigType.AMNEZIA -> {
val config = org.amnezia.awg.config.Config.parse(it)
amQuick = config.toAwgQuickString()
config.toWgQuickString()
}
ConfigType.WIREGUARD -> {
Config.parse(it).toWgQuickString()
}
}
}
val tunnelName = makeTunnelNameUnique(getNameFromFileName(fileName))
addTunnel(TunnelConfig(name = tunnelName, wgQuick = wgQuick, amQuick = amQuick ?: TunnelConfig.AM_QUICK_DEFAULT))
}
private fun getInputStreamFromUri(uri: Uri, context: Context): InputStream? {
return context.applicationContext.contentResolver.openInputStream(uri)
}
suspend fun onTunnelFileSelected(uri: Uri, configType: ConfigType, context: Context): Result<Unit> {
return try {
if (isValidUriContentScheme(uri)) {
val fileName = getFileName(context, uri)
return when (getFileExtensionFromFileName(fileName)) {
Constants.CONF_FILE_EXTENSION ->
saveTunnelFromConfUri(fileName, uri, configType, context)
Constants.ZIP_FILE_EXTENSION -> saveTunnelsFromZipUri(uri, configType, context)
else -> Result.failure(WgTunnelExceptions.InvalidFileExtension())
}
return Result.Success(Unit)
} else {
return Result.Error(Event.Error.InvalidFileExtension)
Result.failure(WgTunnelExceptions.InvalidFileExtension())
}
} catch (e: Exception) {
Timber.e(e)
return Result.Error(Event.Error.FileReadFailed)
Result.failure(WgTunnelExceptions.FileReadFailed())
}
}
private suspend fun saveTunnelsFromZipUri(uri: Uri) {
ZipInputStream(getInputStreamFromUri(uri)).use { zip ->
private suspend fun saveTunnelsFromZipUri(uri: Uri, configType: ConfigType, context: Context) : Result<Unit> {
return ZipInputStream(getInputStreamFromUri(uri, context)).use { zip ->
generateSequence { zip.nextEntry }
.filterNot {
it.isDirectory ||
@@ -162,40 +213,57 @@ constructor(
}
.forEach {
val name = getNameFromFileName(it.name)
val config = Config.parse(zip)
viewModelScope.launch(Dispatchers.IO) {
addTunnel(TunnelConfig(name = name, wgQuick = config.toAwgQuickString()))
withContext(viewModelScope.coroutineContext + Dispatchers.IO) {
try {
var amQuick : String? = null
val wgQuick =
when(configType) {
ConfigType.AMNEZIA -> {
val config = org.amnezia.awg.config.Config.parse(zip)
amQuick = config.toAwgQuickString()
config.toWgQuickString()
}
ConfigType.WIREGUARD -> {
Config.parse(zip).toWgQuickString()
}
}
addTunnel(TunnelConfig(name = makeTunnelNameUnique(name), wgQuick = wgQuick, amQuick = amQuick ?: TunnelConfig.AM_QUICK_DEFAULT))
Result.success(Unit)
} catch (e : Exception) {
Result.failure(WgTunnelExceptions.FileReadFailed())
}
}
}
Result.success(Unit)
}
}
private suspend fun saveTunnelFromConfUri(name: String, uri: Uri): Result<Unit> {
val stream = getInputStreamFromUri(uri)
private suspend fun saveTunnelFromConfUri(name: String, uri: Uri, configType: ConfigType, context: Context): Result<Unit> {
val stream = getInputStreamFromUri(uri, context)
return if (stream != null) {
saveTunnelConfigFromStream(stream, name)
Result.Success(Unit)
saveTunnelConfigFromStream(stream, name, configType)
Result.success(Unit)
} else {
Result.Error(Event.Error.FileReadFailed)
Result.failure(WgTunnelExceptions.FileReadFailed())
}
}
private suspend fun addTunnel(tunnelConfig: TunnelConfig) {
val firstTunnel = appDataRepository.tunnels.count() == 0
saveTunnel(tunnelConfig)
if (firstTunnel) WireGuardAutoTunnel.requestTunnelTileServiceStateUpdate(application)
if (firstTunnel) WireGuardAutoTunnel.requestTunnelTileServiceStateUpdate()
}
fun pauseAutoTunneling() =
viewModelScope.launch {
appDataRepository.settings.save(uiState.value.settings.copy(isAutoTunnelPaused = true))
WireGuardAutoTunnel.requestAutoTunnelTileServiceUpdate(application)
WireGuardAutoTunnel.requestAutoTunnelTileServiceUpdate()
}
fun resumeAutoTunneling() =
viewModelScope.launch {
appDataRepository.settings.save(uiState.value.settings.copy(isAutoTunnelPaused = false))
WireGuardAutoTunnel.requestAutoTunnelTileServiceUpdate(application)
WireGuardAutoTunnel.requestAutoTunnelTileServiceUpdate()
}
private suspend fun saveTunnel(tunnelConfig: TunnelConfig) {
@@ -239,12 +307,12 @@ constructor(
return fileName.substring(0, fileName.lastIndexOf('.'))
}
private fun getFileExtensionFromFileName(fileName: String): String {
private fun getFileExtensionFromFileName(fileName: String): String? {
return try {
fileName.substring(fileName.lastIndexOf('.'))
} catch (e: Exception) {
Timber.e(e)
""
null
}
}
@@ -1,5 +1,6 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.options
import android.annotation.SuppressLint
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement
@@ -7,7 +8,6 @@ import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ExperimentalLayoutApi
import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.IntrinsicSize
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
@@ -24,9 +24,10 @@ import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.surfaceColorAtElevation
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
@@ -38,16 +39,23 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.focus.onFocusChanged
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardCapitalization
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.navigation.NavController
import com.iamageo.multifablibrary.FabIcon
import com.iamageo.multifablibrary.FabOption
import com.iamageo.multifablibrary.MultiFabItem
import com.iamageo.multifablibrary.MultiFloatingActionButton
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
import com.zaneschepke.wireguardautotunnel.ui.AppViewModel
@@ -55,11 +63,13 @@ import com.zaneschepke.wireguardautotunnel.ui.Screen
import com.zaneschepke.wireguardautotunnel.ui.common.ClickableIconButton
import com.zaneschepke.wireguardautotunnel.ui.common.config.ConfigurationToggle
import com.zaneschepke.wireguardautotunnel.ui.common.text.SectionTitle
import com.zaneschepke.wireguardautotunnel.ui.screens.main.ConfigType
import com.zaneschepke.wireguardautotunnel.util.Constants
import com.zaneschepke.wireguardautotunnel.util.Result
import com.zaneschepke.wireguardautotunnel.util.getMessage
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter")
@OptIn(ExperimentalLayoutApi::class)
@Composable
fun OptionsScreen(
@@ -71,6 +81,8 @@ fun OptionsScreen(
) {
val scrollState = rememberScrollState()
val uiState by optionsViewModel.uiState.collectAsStateWithLifecycle()
val context = LocalContext.current
val interactionSource = remember { MutableInteractionSource() }
val scope = rememberCoroutineScope()
val focusManager = LocalFocusManager.current
@@ -90,196 +102,236 @@ fun OptionsScreen(
fun saveTrustedSSID() {
if (currentText.isNotEmpty()) {
scope.launch {
optionsViewModel.onSaveRunSSID(currentText).let {
when (it) {
is Result.Success -> currentText = ""
is Result.Error -> appViewModel.showSnackbarMessage(it.error.message)
}
optionsViewModel.onSaveRunSSID(currentText).onSuccess {
currentText = ""
}.onFailure {
appViewModel.showSnackbarMessage(it.getMessage(context))
}
}
}
}
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Top,
modifier =
Modifier
.fillMaxSize()
.verticalScroll(scrollState)
.clickable(
indication = null,
interactionSource = interactionSource,
) {
focusManager.clearFocus()
},
) {
Surface(
tonalElevation = 2.dp,
shadowElevation = 2.dp,
shape = RoundedCornerShape(12.dp),
color = MaterialTheme.colorScheme.surface,
modifier =
(if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
Modifier
.height(IntrinsicSize.Min)
.fillMaxWidth(fillMaxWidth)
.padding(top = 10.dp)
} else {
Modifier
.fillMaxWidth(fillMaxWidth)
.padding(top = 20.dp)
})
.padding(bottom = 10.dp),
) {
Column(
horizontalAlignment = Alignment.Start,
verticalArrangement = Arrangement.Top,
modifier = Modifier.padding(15.dp),
) {
SectionTitle(
title = stringResource(id = R.string.general),
padding = screenPadding,
Scaffold(
floatingActionButton = {
val secondaryColor = MaterialTheme.colorScheme.secondary
val hoverColor = MaterialTheme.colorScheme.surfaceColorAtElevation(2.dp)
var fobColor by remember { mutableStateOf(secondaryColor) }
MultiFloatingActionButton(
modifier =
(if (
WireGuardAutoTunnel.isRunningOnAndroidTv()
)
ConfigurationToggle(
stringResource(R.string.set_primary_tunnel),
enabled = true,
checked = uiState.isDefaultTunnel,
modifier = Modifier
.focusRequester(focusRequester),
padding = screenPadding,
onCheckChanged = { optionsViewModel.onTogglePrimaryTunnel() },
)
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.fillMaxSize()
.padding(top = 5.dp),
horizontalArrangement = Arrangement.Center,
) {
TextButton(
onClick = {
navController.navigate(
"${Screen.Config.route}/${tunnelId}",
Modifier.focusRequester(focusRequester)
else Modifier)
.onFocusChanged {
if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
fobColor = if (it.isFocused) hoverColor else secondaryColor
}
},
fabIcon = FabIcon(
iconRes = R.drawable.edit,
iconResAfterRotate = R.drawable.close,
iconRotate = 180f
),
fabOption = FabOption(
iconTint = MaterialTheme.colorScheme.background,
backgroundTint = MaterialTheme.colorScheme.primary,
),
itemsMultiFab = listOf(
MultiFabItem(
label = {
Text(
stringResource(id = R.string.amnezia),
color = Color.White,
textAlign = TextAlign.Center,
modifier = Modifier.padding(end = 10.dp)
)
},
) {
Text(stringResource(R.string.edit_tunnel))
}
icon = R.drawable.edit,
value = ConfigType.AMNEZIA.name,
),
MultiFabItem(
label = {
Text(stringResource(id = R.string.wireguard), color = Color.White, textAlign = TextAlign.Center, modifier = Modifier.padding(end = 10.dp))
},
icon = R.drawable.edit,
value = ConfigType.WIREGUARD.name
),
),
onFabItemClicked = {
val configType = ConfigType.valueOf(it.value)
navController.navigate(
"${Screen.Config.route}/${tunnelId}?configType=${configType.name}",
)
},
shape = RoundedCornerShape(16.dp),
)
}
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Top,
modifier =
Modifier
.fillMaxSize()
.verticalScroll(scrollState)
.clickable(
indication = null,
interactionSource = interactionSource,
) {
focusManager.clearFocus()
},
) {
Surface(
tonalElevation = 2.dp,
shadowElevation = 2.dp,
shape = RoundedCornerShape(12.dp),
color = MaterialTheme.colorScheme.surface,
modifier =
(if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
Modifier
.height(IntrinsicSize.Min)
.fillMaxWidth(fillMaxWidth)
.padding(top = 10.dp)
} else {
Modifier
.fillMaxWidth(fillMaxWidth)
.padding(top = 20.dp)
})
.padding(bottom = 10.dp),
) {
Column(
horizontalAlignment = Alignment.Start,
verticalArrangement = Arrangement.Top,
modifier = Modifier.padding(15.dp),
) {
SectionTitle(
title = stringResource(id = R.string.general),
padding = screenPadding,
)
ConfigurationToggle(
stringResource(R.string.set_primary_tunnel),
enabled = true,
checked = uiState.isDefaultTunnel,
modifier = Modifier
.focusRequester(focusRequester),
padding = screenPadding,
onCheckChanged = { optionsViewModel.onTogglePrimaryTunnel() },
)
}
}
}
Surface(
tonalElevation = 2.dp,
shadowElevation = 2.dp,
shape = RoundedCornerShape(12.dp),
color = MaterialTheme.colorScheme.surface,
modifier =
(if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
Modifier
.height(IntrinsicSize.Min)
.fillMaxWidth(fillMaxWidth)
.padding(top = 10.dp)
} else {
Modifier
.fillMaxWidth(fillMaxWidth)
.padding(top = 20.dp)
})
.padding(bottom = 10.dp),
) {
Column(
horizontalAlignment = Alignment.Start,
verticalArrangement = Arrangement.Top,
modifier = Modifier.padding(15.dp),
Surface(
tonalElevation = 2.dp,
shadowElevation = 2.dp,
shape = RoundedCornerShape(12.dp),
color = MaterialTheme.colorScheme.surface,
modifier =
(if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
Modifier
.height(IntrinsicSize.Min)
.fillMaxWidth(fillMaxWidth)
.padding(top = 10.dp)
} else {
Modifier
.fillMaxWidth(fillMaxWidth)
.padding(top = 20.dp)
})
.padding(bottom = 10.dp),
) {
SectionTitle(
title = stringResource(id = R.string.auto_tunneling),
padding = screenPadding,
)
ConfigurationToggle(
stringResource(R.string.mobile_data_tunnel),
enabled = true,
checked = uiState.tunnel?.isMobileDataTunnel == true,
padding = screenPadding,
onCheckChanged = { optionsViewModel.onToggleIsMobileDataTunnel() },
)
Column {
FlowRow(
modifier = Modifier
.padding(screenPadding)
.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(5.dp),
) {
uiState.tunnel?.tunnelNetworks?.forEach { ssid ->
ClickableIconButton(
onClick = {
if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
focusRequester.requestFocus()
optionsViewModel.onDeleteRunSSID(ssid)
}
},
onIconClick = {
if (WireGuardAutoTunnel.isRunningOnAndroidTv()) focusRequester.requestFocus()
optionsViewModel.onDeleteRunSSID(ssid)
},
text = ssid,
icon = Icons.Filled.Close,
enabled = true,
)
}
if (uiState.tunnel == null || uiState.tunnel?.tunnelNetworks?.isEmpty() == true) {
Text(
stringResource(R.string.no_wifi_names_configured),
fontStyle = FontStyle.Italic,
color = Color.Gray,
)
}
}
OutlinedTextField(
enabled = true,
value = currentText,
onValueChange = { currentText = it },
label = { Text(stringResource(id = R.string.use_tunnel_on_wifi_name)) },
modifier =
Modifier
.padding(
start = screenPadding,
top = 5.dp,
bottom = 10.dp,
),
maxLines = 1,
keyboardOptions =
KeyboardOptions(
capitalization = KeyboardCapitalization.None,
imeAction = ImeAction.Done,
),
keyboardActions = KeyboardActions(onDone = { saveTrustedSSID() }),
trailingIcon = {
if (currentText != "") {
IconButton(onClick = { saveTrustedSSID() }) {
Icon(
imageVector = Icons.Outlined.Add,
contentDescription =
if (currentText == "") {
stringResource(
id =
R.string
.trusted_ssid_empty_description,
)
} else {
stringResource(
id =
R.string
.trusted_ssid_value_description,
)
},
tint = MaterialTheme.colorScheme.primary,
)
}
}
},
Column(
horizontalAlignment = Alignment.Start,
verticalArrangement = Arrangement.Top,
modifier = Modifier.padding(15.dp),
) {
SectionTitle(
title = stringResource(id = R.string.auto_tunneling),
padding = screenPadding,
)
ConfigurationToggle(
stringResource(R.string.mobile_data_tunnel),
enabled = true,
checked = uiState.tunnel?.isMobileDataTunnel == true,
padding = screenPadding,
onCheckChanged = { optionsViewModel.onToggleIsMobileDataTunnel() },
)
Column {
FlowRow(
modifier = Modifier
.padding(screenPadding)
.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(5.dp),
) {
uiState.tunnel?.tunnelNetworks?.forEach { ssid ->
ClickableIconButton(
onClick = {
if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
focusRequester.requestFocus()
optionsViewModel.onDeleteRunSSID(ssid)
}
},
onIconClick = {
if (WireGuardAutoTunnel.isRunningOnAndroidTv()) focusRequester.requestFocus()
optionsViewModel.onDeleteRunSSID(ssid)
},
text = ssid,
icon = Icons.Filled.Close,
enabled = true,
)
}
if (uiState.tunnel == null || uiState.tunnel?.tunnelNetworks?.isEmpty() == true) {
Text(
stringResource(R.string.no_wifi_names_configured),
fontStyle = FontStyle.Italic,
color = Color.Gray,
)
}
}
OutlinedTextField(
enabled = true,
value = currentText,
onValueChange = { currentText = it },
label = { Text(stringResource(id = R.string.use_tunnel_on_wifi_name)) },
modifier =
Modifier
.padding(
start = screenPadding,
top = 5.dp,
bottom = 10.dp,
),
maxLines = 1,
keyboardOptions =
KeyboardOptions(
capitalization = KeyboardCapitalization.None,
imeAction = ImeAction.Done,
),
keyboardActions = KeyboardActions(onDone = { saveTrustedSSID() }),
trailingIcon = {
if (currentText != "") {
IconButton(onClick = { saveTrustedSSID() }) {
Icon(
imageVector = Icons.Outlined.Add,
contentDescription =
if (currentText == "") {
stringResource(
id =
R.string
.trusted_ssid_empty_description,
)
} else {
stringResource(
id =
R.string
.trusted_ssid_value_description,
)
},
tint = MaterialTheme.colorScheme.primary,
)
}
}
},
)
}
}
}
}
@@ -1,6 +1,6 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.options
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
data class OptionsUiState(
val id: String? = null,
@@ -4,17 +4,17 @@ import androidx.compose.ui.util.fastFirstOrNull
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
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.util.Constants
import com.zaneschepke.wireguardautotunnel.util.Event
import com.zaneschepke.wireguardautotunnel.util.Result
import com.zaneschepke.wireguardautotunnel.util.WgTunnelExceptions
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import javax.inject.Inject
@@ -43,9 +43,11 @@ constructor(
)
fun init(tunnelId: String) {
_optionState.value = _optionState.value.copy(
id = tunnelId,
)
_optionState.update {
it.copy(
id = tunnelId
)
}
}
fun onDeleteRunSSID(ssid: String) = viewModelScope.launch(Dispatchers.IO) {
@@ -73,9 +75,9 @@ constructor(
tunnelsWithName.isEmpty()) {
uiState.value.tunnel?.tunnelNetworks?.add(trimmed)
saveTunnel(uiState.value.tunnel)
Result.Success(Unit)
Result.success(Unit)
} else {
Result.Error(Event.Error.SsidConflict)
Result.failure(WgTunnelExceptions.SsidConflict())
}
}
@@ -95,7 +97,7 @@ constructor(
false -> uiState.value.tunnel
},
)
WireGuardAutoTunnel.requestTunnelTileServiceStateUpdate(WireGuardAutoTunnel.instance)
WireGuardAutoTunnel.requestTunnelTileServiceStateUpdate()
}
}
}
@@ -26,7 +26,6 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.KeyboardActions
@@ -71,21 +70,21 @@ import androidx.navigation.NavController
import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.google.accompanist.permissions.isGranted
import com.google.accompanist.permissions.rememberPermissionState
import com.wireguard.android.backend.WgQuickBackend
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelState
import com.zaneschepke.wireguardautotunnel.ui.AppViewModel
import com.zaneschepke.wireguardautotunnel.ui.Screen
import com.zaneschepke.wireguardautotunnel.ui.common.ClickableIconButton
import com.zaneschepke.wireguardautotunnel.ui.common.config.ConfigurationToggle
import com.zaneschepke.wireguardautotunnel.ui.common.prompt.AuthorizationPrompt
import com.zaneschepke.wireguardautotunnel.ui.common.text.SectionTitle
import com.zaneschepke.wireguardautotunnel.util.Event
import com.zaneschepke.wireguardautotunnel.util.FileUtils
import com.zaneschepke.wireguardautotunnel.util.Result
import com.zaneschepke.wireguardautotunnel.util.getMessage
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import org.amnezia.awg.backend.AwgQuickBackend
import org.amnezia.awg.backend.Tunnel
import timber.log.Timber
import xyz.teamgravity.pin_lock_compose.PinManager
import java.io.File
@@ -131,16 +130,28 @@ fun SettingsScreen(
fun exportAllConfigs() {
try {
val files = uiState.tunnels.map { File(context.cacheDir, "${it.name}.conf") }
files.forEachIndexed { index, file ->
file.outputStream().use { it.write(uiState.tunnels[index].wgQuick.toByteArray()) }
val wgFiles = uiState.tunnels.map { config ->
val file = File(context.cacheDir, "${config.name}-wg.conf")
file.outputStream().use {
it.write(config.wgQuick.toByteArray())
}
file
}
val amFiles = uiState.tunnels.mapNotNull { config -> if(config.amQuick != TunnelConfig.AM_QUICK_DEFAULT) {
val file = File(context.cacheDir, "${config.name}-am.conf")
file.outputStream().use {
it.write(config.amQuick.toByteArray())
}
file
} else null }
FileUtils.saveFilesToZip(context, wgFiles + amFiles).onFailure {
appViewModel.showSnackbarMessage(it.getMessage(context))
}.onSuccess {
didExportFiles = true
appViewModel.showSnackbarMessage(context.getString(R.string.exported_configs_message))
}
FileUtils.saveFilesToZip(context, files)
didExportFiles = true
appViewModel.showSnackbarMessage(Event.Message.ConfigsExported.message)
} catch (e: Exception) {
Timber.e(e)
appViewModel.showSnackbarMessage(Event.Error.Exception(e).message)
}
}
@@ -161,7 +172,7 @@ fun SettingsScreen(
fun handleAutoTunnelToggle() {
if (uiState.isBatteryOptimizeDisableShown || isBatteryOptimizationsDisabled()) {
if (appViewModel.isRequiredPermissionGranted()) {
viewModel.onToggleAutoTunnel()
viewModel.onToggleAutoTunnel(context)
}
} else {
requestBatteryOptimizationsDisabled()
@@ -170,11 +181,10 @@ fun SettingsScreen(
fun saveTrustedSSID() {
if (currentText.isNotEmpty()) {
viewModel.onSaveTrustedSSID(currentText).let {
when (it) {
is Result.Success -> currentText = ""
is Result.Error -> appViewModel.showSnackbarMessage(it.error.message)
}
viewModel.onSaveTrustedSSID(currentText).onSuccess {
currentText = ""
}.onFailure {
appViewModel.showSnackbarMessage(it.getMessage(context))
}
}
}
@@ -308,11 +318,11 @@ fun SettingsScreen(
},
onError = { _ ->
showAuthPrompt = false
appViewModel.showSnackbarMessage(Event.Error.AuthenticationFailed.message)
appViewModel.showSnackbarMessage(context.getString(R.string.error_authentication_failed))
},
onFailure = {
showAuthPrompt = false
appViewModel.showSnackbarMessage(Event.Error.AuthorizationFailed.message)
appViewModel.showSnackbarMessage(context.getString(R.string.error_authorization_failed))
},
)
}
@@ -520,12 +530,12 @@ fun SettingsScreen(
when (false) {
isBackgroundLocationGranted ->
appViewModel.showSnackbarMessage(
Event.Error.BackgroundLocationRequired.message,
context.getString(R.string.background_location_required),
)
fineLocationState.status.isGranted ->
appViewModel.showSnackbarMessage(
Event.Error.PreciseLocationRequired.message,
context.getString(R.string.precise_location_required),
)
viewModel.isLocationEnabled(context) ->
@@ -551,39 +561,48 @@ fun SettingsScreen(
}
}
}
if (AwgQuickBackend.hasKernelSupport()) {
Surface(
tonalElevation = 2.dp,
shadowElevation = 2.dp,
shape = RoundedCornerShape(12.dp),
color = MaterialTheme.colorScheme.surface,
modifier = Modifier
.fillMaxWidth(fillMaxWidth)
.padding(vertical = 10.dp),
Surface(
tonalElevation = 2.dp,
shadowElevation = 2.dp,
shape = RoundedCornerShape(12.dp),
color = MaterialTheme.colorScheme.surface,
modifier = Modifier
.fillMaxWidth(fillMaxWidth)
.padding(vertical = 10.dp),
) {
Column(
horizontalAlignment = Alignment.Start,
verticalArrangement = Arrangement.Top,
modifier = Modifier.padding(15.dp),
) {
Column(
horizontalAlignment = Alignment.Start,
verticalArrangement = Arrangement.Top,
modifier = Modifier.padding(15.dp),
) {
SectionTitle(
title = stringResource(id = R.string.kernel),
padding = screenPadding,
)
SectionTitle(
title = stringResource(id = R.string.backend),
padding = screenPadding,
)
ConfigurationToggle(
stringResource(R.string.use_amnezia),
enabled =
!(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(
stringResource(R.string.use_kernel),
enabled =
!(uiState.settings.isAutoTunnelEnabled ||
uiState.settings.isAlwaysOnVpnEnabled ||
(uiState.vpnState.status == Tunnel.State.UP)),
(uiState.vpnState.status == TunnelState.UP)),
checked = uiState.settings.isKernelEnabled,
padding = screenPadding,
onCheckChanged = {
viewModel.onToggleKernelMode().let {
when (it) {
is Result.Error -> appViewModel.showSnackbarMessage(it.error.message)
is Result.Success -> {}
}
viewModel.onToggleKernelMode().onFailure {
appViewModel.showSnackbarMessage(it.getMessage(context))
}
},
)
@@ -1,7 +1,7 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.settings
import com.zaneschepke.wireguardautotunnel.data.model.Settings
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
import com.zaneschepke.wireguardautotunnel.data.domain.Settings
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnState
data class SettingsUiState(
@@ -1,25 +1,23 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.settings
import android.app.Application
import android.content.Context
import android.location.LocationManager
import androidx.core.location.LocationManagerCompat
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.wireguard.android.util.RootShell
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.service.foreground.ServiceManager
import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnService
import com.zaneschepke.wireguardautotunnel.util.Constants
import com.zaneschepke.wireguardautotunnel.util.Event
import com.zaneschepke.wireguardautotunnel.util.Result
import com.zaneschepke.wireguardautotunnel.util.WgTunnelExceptions
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import org.amnezia.awg.util.RootShell
import timber.log.Timber
import javax.inject.Inject
@@ -27,7 +25,6 @@ import javax.inject.Inject
class SettingsViewModel
@Inject
constructor(
private val application: Application,
private val appDataRepository: AppDataRepository,
private val serviceManager: ServiceManager,
private val rootShell: RootShell,
@@ -60,9 +57,9 @@ constructor(
return if (!uiState.value.settings.trustedNetworkSSIDs.contains(trimmed)) {
uiState.value.settings.trustedNetworkSSIDs.add(trimmed)
saveSettings(uiState.value.settings)
Result.Success(Unit)
Result.success(Unit)
} else {
Result.Error(Event.Error.SsidConflict)
Result.failure(WgTunnelExceptions.SsidConflict())
}
}
@@ -93,15 +90,15 @@ constructor(
)
}
fun onToggleAutoTunnel() =
fun onToggleAutoTunnel(context: Context) =
viewModelScope.launch {
val isAutoTunnelEnabled = uiState.value.settings.isAutoTunnelEnabled
var isAutoTunnelPaused = uiState.value.settings.isAutoTunnelPaused
if (isAutoTunnelEnabled) {
serviceManager.stopWatcherService(application)
serviceManager.stopWatcherService(context)
} else {
serviceManager.startWatcherService(application)
serviceManager.startWatcherService(context)
isAutoTunnelPaused = false
}
saveSettings(
@@ -110,7 +107,7 @@ constructor(
isAutoTunnelPaused = isAutoTunnelPaused,
),
)
WireGuardAutoTunnel.requestAutoTunnelTileServiceUpdate(application)
WireGuardAutoTunnel.requestAutoTunnelTileServiceUpdate()
}
fun onToggleAlwaysOnVPN() =
@@ -162,21 +159,42 @@ 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> {
if (!uiState.value.settings.isKernelEnabled) {
try {
rootShell.start()
Timber.i("Root shell accepted!")
saveKernelMode(on = true)
saveSettings(
uiState.value.settings.copy(
isKernelEnabled = true,
isAmneziaEnabled = false,
),
)
} catch (e: RootShell.RootShellException) {
Timber.e(e)
saveKernelMode(on = false)
return Result.Error(Event.Error.RootDenied)
return Result.failure(WgTunnelExceptions.RootDenied())
}
} else {
saveKernelMode(on = false)
}
return Result.Success(Unit)
return Result.success(Unit)
}
fun onToggleRestartOnPing() = viewModelScope.launch {
@@ -107,7 +107,7 @@ fun SupportScreen(
modifier = Modifier.padding(bottom = 20.dp),
)
TextButton(
onClick = { appViewModel.openWebPage(context.resources.getString(R.string.docs_url)) },
onClick = { appViewModel.openWebPage(context.resources.getString(R.string.docs_url), context) },
modifier = Modifier
.padding(vertical = 5.dp)
.focusRequester(focusRequester),
@@ -143,7 +143,7 @@ fun SupportScreen(
color = MaterialTheme.colorScheme.onBackground,
)
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),
) {
Row(
@@ -152,7 +152,7 @@ fun SupportScreen(
modifier = Modifier.fillMaxWidth(),
) {
Row {
val icon = ImageVector.vectorResource(R.drawable.discord)
val icon = ImageVector.vectorResource(R.drawable.telegram)
Icon(
icon,
icon.name,
@@ -175,7 +175,7 @@ fun SupportScreen(
color = MaterialTheme.colorScheme.onBackground,
)
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),
) {
Row(
@@ -207,7 +207,7 @@ fun SupportScreen(
color = MaterialTheme.colorScheme.onBackground,
)
TextButton(
onClick = { appViewModel.launchEmail() },
onClick = { appViewModel.launchEmail(context) },
modifier = Modifier.padding(vertical = 5.dp),
) {
Row(
@@ -269,7 +269,7 @@ fun SupportScreen(
fontSize = 16.sp,
modifier =
Modifier.clickable {
appViewModel.openWebPage(context.resources.getString(R.string.privacy_policy_url))
appViewModel.openWebPage(context.resources.getString(R.string.privacy_policy_url), context)
},
)
Row(
@@ -1,5 +1,5 @@
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())
@@ -27,6 +27,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.ClipboardManager
import androidx.compose.ui.platform.LocalClipboardManager
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
@@ -43,6 +44,8 @@ fun LogsScreen(appViewModel: AppViewModel) {
appViewModel.logs
}
val context = LocalContext.current
val lazyColumnListState = rememberLazyListState()
val clipboardManager: ClipboardManager = LocalClipboardManager.current
val scope = rememberCoroutineScope()
@@ -57,7 +60,7 @@ fun LogsScreen(appViewModel: AppViewModel) {
floatingActionButton = {
FloatingActionButton(
onClick = {
appViewModel.saveLogsToFile()
appViewModel.saveLogsToFile(context)
},
shape = RoundedCornerShape(16.dp),
containerColor = MaterialTheme.colorScheme.primary,
@@ -2,7 +2,7 @@ package com.zaneschepke.wireguardautotunnel.util
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 MANUAL_TUNNEL_CONFIG_ID = "0"
@@ -25,7 +25,7 @@ object Constants {
const val SUBSCRIPTION_TIMEOUT = 5_000L
const val FOCUS_REQUEST_DELAY = 500L
const val BACKUP_PING_HOST = "1.1.1.1"
const val DEFAULT_PING_IP = "1.1.1.1"
const val PING_TIMEOUT = 5_000L
const val VPN_RESTART_DELAY = 1_000L
const val PING_INTERVAL = 60_000L
@@ -37,4 +37,7 @@ object Constants {
const val UNREADABLE_SSID = "<unknown ssid>"
val amneziaProperties = listOf("Jc", "Jmin", "Jmax", "S1", "S2", "H1", "H2", "H3", "H4")
const val QR_CODE_NAME_PROPERTY = "# Name ="
}
@@ -1,117 +0,0 @@
package com.zaneschepke.wireguardautotunnel.util
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
sealed class Event {
abstract val message: String
sealed class Error : Event() {
data object None : Error() {
override val message: String
get() = WireGuardAutoTunnel.instance.getString(R.string.error_none)
}
data object SsidConflict : Error() {
override val message: String
get() = WireGuardAutoTunnel.instance.getString(R.string.error_ssid_exists)
}
data class ConfigParseError(val appendedMessage: String) : Error() {
override val message: String =
WireGuardAutoTunnel.instance.getString(R.string.config_parse_error) + (
if (appendedMessage != "") ": ${appendedMessage.trim()}" else "")
}
data object RootDenied : Error() {
override val message: String
get() = WireGuardAutoTunnel.instance.getString(R.string.error_root_denied)
}
data class General(val customMessage: String) : Error() {
override val message: String
get() = customMessage
}
data class Exception(val exception: kotlin.Exception) : Error() {
override val message: String
get() =
exception.message
?: WireGuardAutoTunnel.instance.getString(R.string.unknown_error)
}
data object InvalidQrCode : Error() {
override val message: String
get() = WireGuardAutoTunnel.instance.getString(R.string.error_invalid_code)
}
data object InvalidFileExtension : Error() {
override val message: String
get() = WireGuardAutoTunnel.instance.getString(R.string.error_file_extension)
}
data object FileReadFailed : Error() {
override val message: String
get() = WireGuardAutoTunnel.instance.getString(R.string.error_file_extension)
}
data object AuthenticationFailed : Error() {
override val message: String
get() = WireGuardAutoTunnel.instance.getString(R.string.error_authentication_failed)
}
data object AuthorizationFailed : Error() {
override val message: String
get() = WireGuardAutoTunnel.instance.getString(R.string.error_authorization_failed)
}
data object BackgroundLocationRequired : Error() {
override val message: String
get() =
WireGuardAutoTunnel.instance.getString(R.string.background_location_required)
}
data object LocationServicesRequired : Error() {
override val message: String
get() = WireGuardAutoTunnel.instance.getString(R.string.location_services_required)
}
data object PreciseLocationRequired : Error() {
override val message: String
get() = WireGuardAutoTunnel.instance.getString(R.string.precise_location_required)
}
data object FileExplorerRequired : Error() {
override val message: String
get() = WireGuardAutoTunnel.instance.getString(R.string.error_no_file_explorer)
}
}
sealed class Message : Event() {
data object ConfigSaved : Message() {
override val message: String
get() = WireGuardAutoTunnel.instance.getString(R.string.config_changes_saved)
}
data object ConfigsExported : Message() {
override val message: String
get() = WireGuardAutoTunnel.instance.getString(R.string.exported_configs_message)
}
data object TunnelOffAction : Message() {
override val message: String
get() = WireGuardAutoTunnel.instance.getString(R.string.turn_off_tunnel)
}
data object TunnelOnAction : Message() {
override val message: String
get() = WireGuardAutoTunnel.instance.getString(R.string.turn_on_tunnel)
}
data object AutoTunnelOffAction : Message() {
override val message: String
get() = WireGuardAutoTunnel.instance.getString(R.string.turn_off_auto)
}
}
}
@@ -1,15 +1,17 @@
package com.zaneschepke.wireguardautotunnel.util
import android.content.BroadcastReceiver
import android.content.Context
import android.content.pm.PackageInfo
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
import com.zaneschepke.wireguardautotunnel.service.tunnel.HandshakeStatus
import com.zaneschepke.wireguardautotunnel.service.tunnel.statistics.TunnelStatistics
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import org.amnezia.awg.backend.Statistics
import org.amnezia.awg.crypto.Key
import org.amnezia.awg.config.Config
import java.math.BigDecimal
import java.text.DecimalFormat
import kotlin.coroutines.CoroutineContext
@@ -49,15 +51,15 @@ typealias TunnelConfigs = List<TunnelConfig>
typealias Packages = List<PackageInfo>
fun Statistics.mapPeerStats(): Map<Key, Statistics.PeerStats?> {
return this.peers().associateWith { key -> (this.peer(key)) }
fun TunnelStatistics.mapPeerStats(): Map<org.amnezia.awg.crypto.Key, TunnelStatistics.PeerStats?> {
return this.getPeers().associateWith { key -> (this.peerStats(key)) }
}
fun Statistics.PeerStats.latestHandshakeSeconds(): Long? {
fun TunnelStatistics.PeerStats.latestHandshakeSeconds(): Long? {
return NumberUtils.getSecondsBetweenTimestampAndNow(this.latestHandshakeEpochMillis)
}
fun Statistics.PeerStats.handshakeStatus(): HandshakeStatus {
fun TunnelStatistics.PeerStats.handshakeStatus(): HandshakeStatus {
// TODO add never connected status after duration
return this.latestHandshakeSeconds().let {
when {
@@ -70,3 +72,25 @@ fun Statistics.PeerStats.handshakeStatus(): HandshakeStatus {
}
}
}
fun Config.toWgQuickString() : String {
val amQuick = toAwgQuickString()
val lines = amQuick.lines().toMutableList()
val linesIterator = lines.iterator()
while(linesIterator.hasNext()) {
val next = linesIterator.next()
Constants.amneziaProperties.forEach {
if(next.startsWith(it, ignoreCase = true)) {
linesIterator.remove()
}
}
}
return lines.joinToString(System.lineSeparator())
}
fun Throwable.getMessage(context: Context) : String {
return when(this) {
is WgTunnelExceptions -> this.getMessage(context)
else -> this.message ?: StringValue.StringResource(R.string.unknown_error).asString(context)
}
}
@@ -6,6 +6,7 @@ import android.os.Build
import android.os.Environment
import android.provider.MediaStore
import android.provider.MediaStore.MediaColumns
import timber.log.Timber
import java.io.File
import java.io.FileOutputStream
import java.io.OutputStream
@@ -70,21 +71,27 @@ object FileUtils {
}
}
fun saveFilesToZip(context: Context, files: List<File>) {
val zipOutputStream =
createDownloadsFileOutputStream(
context,
"wg-export_${Instant.now().epochSecond}.zip",
ZIP_FILE_MIME_TYPE,
)
ZipOutputStream(zipOutputStream).use { zos ->
files.forEach { file ->
val entry = ZipEntry(file.name)
zos.putNextEntry(entry)
if (file.isFile) {
file.inputStream().use { fis -> fis.copyTo(zos) }
fun saveFilesToZip(context: Context, files: List<File>) : Result<Unit> {
return try {
val zipOutputStream =
createDownloadsFileOutputStream(
context,
"wg-export_${Instant.now().epochSecond}.zip",
ZIP_FILE_MIME_TYPE,
)
ZipOutputStream(zipOutputStream).use { zos ->
files.forEach { file ->
val entry = ZipEntry(file.name)
zos.putNextEntry(entry)
if (file.isFile) {
file.inputStream().use { fis -> fis.copyTo(zos) }
}
}
return Result.success(Unit)
}
} catch (e : Exception) {
Timber.e(e)
Result.failure(WgTunnelExceptions.ConfigExportFailed())
}
}
}
@@ -1,16 +0,0 @@
package com.zaneschepke.wireguardautotunnel.util
import timber.log.Timber
sealed class Result<T> {
class Success<T>(val data: T) : Result<T>()
class Error<T>(val error: Event.Error) : Result<T>() {
init {
when (this.error) {
is Event.Error.Exception -> Timber.e(this.error.exception)
else -> Timber.e(this.error.message)
}
}
}
}
@@ -0,0 +1,124 @@
package com.zaneschepke.wireguardautotunnel.util
import android.content.Context
import com.zaneschepke.wireguardautotunnel.R
sealed class WgTunnelExceptions : Exception() {
abstract fun getMessage(context: Context) : String
data class General(private val userMessage : StringValue) : WgTunnelExceptions() {
override fun getMessage(context: Context) : String {
return userMessage.asString(context)
}
}
data class SsidConflict(private val userMessage : StringValue = StringValue.StringResource(R.string.error_ssid_exists)) : WgTunnelExceptions() {
override fun getMessage(context: Context) : String {
return userMessage.asString(context)
}
}
data class ConfigExportFailed(private val userMessage : StringValue = StringValue.StringResource(R.string.export_configs_failed)) : WgTunnelExceptions() {
override fun getMessage(context: Context) : String {
return userMessage.asString(context)
}
}
data class ConfigParseError(private val appendMessage : StringValue = StringValue.Empty) : WgTunnelExceptions() {
override fun getMessage(context: Context) : String {
return StringValue.StringResource(R.string.config_parse_error).asString(context) + (
if (appendMessage != StringValue.Empty) ": ${appendMessage.asString(context)}" else "")
}
}
data class RootDenied(private val userMessage : StringValue = StringValue.StringResource(R.string.error_root_denied)) : WgTunnelExceptions() {
override fun getMessage(context: Context) : String {
return userMessage.asString(context)
}
}
data class InvalidQrCode(private val userMessage : StringValue = StringValue.StringResource(R.string.error_invalid_code)) : WgTunnelExceptions() {
override fun getMessage(context: Context) : String {
return userMessage.asString(context)
}
}
data class InvalidFileExtension(private val userMessage : StringValue = StringValue.StringResource(R.string.error_file_extension)) : WgTunnelExceptions() {
override fun getMessage(context: Context) : String {
return userMessage.asString(context)
}
}
data class FileReadFailed(private val userMessage : StringValue = StringValue.StringResource(R.string.error_file_format)) : WgTunnelExceptions() {
override fun getMessage(context: Context) : String {
return userMessage.asString(context)
}
}
data class AuthenticationFailed(private val userMessage : StringValue = StringValue.StringResource(R.string.error_authentication_failed)) : WgTunnelExceptions() {
override fun getMessage(context: Context) : String {
return userMessage.asString(context)
}
}
data class AuthorizationFailed(private val userMessage : StringValue = StringValue.StringResource(R.string.error_authorization_failed)) : WgTunnelExceptions() {
override fun getMessage(context: Context) : String {
return userMessage.asString(context)
}
}
data class BackgroundLocationRequired(private val userMessage : StringValue = StringValue.StringResource(R.string.background_location_required)) : WgTunnelExceptions() {
override fun getMessage(context: Context) : String {
return userMessage.asString(context)
}
}
data class LocationServicesRequired(private val userMessage : StringValue = StringValue.StringResource(R.string.location_services_required)) : WgTunnelExceptions() {
override fun getMessage(context: Context) : String {
return userMessage.asString(context)
}
}
data class PreciseLocationRequired(private val userMessage : StringValue = StringValue.StringResource(R.string.precise_location_required)) : WgTunnelExceptions() {
override fun getMessage(context: Context) : String {
return userMessage.asString(context)
}
}
data class FileExplorerRequired (private val userMessage : StringValue = StringValue.StringResource(R.string.error_no_file_explorer)) : WgTunnelExceptions() {
override fun getMessage(context: Context) : String {
return userMessage.asString(context)
}
}
// sealed class Message : Event() {
// data object ConfigSaved : Message() {
// override val message: String
// get() = WireGuardAutoTunnel.instance.getString(R.string.config_changes_saved)
// }
//
// data object ConfigsExported : Message() {
// override val message: String
// get() = WireGuardAutoTunnel.instance.getString(R.string.exported_configs_message)
// }
//
// data object TunnelOffAction : Message() {
// override val message: String
// get() = WireGuardAutoTunnel.instance.getString(R.string.turn_off_tunnel)
// }
//
// data object TunnelOnAction : Message() {
// override val message: String
// get() = WireGuardAutoTunnel.instance.getString(R.string.turn_on_tunnel)
// }
//
// data object AutoTunnelOffAction : Message() {
// override val message: String
// get() = WireGuardAutoTunnel.instance.getString(R.string.turn_off_auto)
// }
// }
}
+9
View File
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:pathData="M440,520L200,520v-80h240v-240h80v240h240v80L520,520v240h-80v-240Z"
android:fillColor="#e8eaed"/>
</vector>
+9
View File
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:pathData="m256,760 l-56,-56 224,-224 -224,-224 56,-56 224,224 224,-224 56,56 -224,224 224,224 -56,56 -224,-224 -224,224Z"
android:fillColor="#e8eaed"/>
</vector>
+9
View File
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:pathData="M200,760h57l391,-391 -57,-57 -391,391v57ZM120,840v-170l528,-527q12,-11 26.5,-17t30.5,-6q16,0 31,6t26,18l55,56q12,11 17.5,26t5.5,30q0,16 -5.5,30.5T817,313L290,840L120,840ZM760,256 L704,200 760,256ZM619,341 L591,312 648,369 619,341Z"
android:fillColor="#e8eaed"/>
</vector>
+9
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>
+88
View File
@@ -73,4 +73,92 @@
<string name="copy_public_key">Öffentlicher Schlüssel kopieren</string>
<string name="base64_key">base64-Schlüssel</string>
<string name="comma_separated_list">Kommaseparierte Liste</string>
<string name="delete_tunnel">Tunnel löschen</string>
<string name="persistent_keepalive">Dauerhaftes Keepalive</string>
<string name="background_location_required">Hintergrund Standortdienste erforderlich</string>
<string name="enable_app_lock">App Sperre aktiviert</string>
<string name="discord_description">Tritt der Community bei</string>
<string name="interface_">Schnittstelle</string>
<string name="listen_port">Eingehender Port</string>
<string name="random">(zufällig)</string>
<string name="optional">(nicht erforderlich)</string>
<string name="optional_no_recommend">(nicht erforderlich, aber empfohlen)</string>
<string name="seconds">Sekunden</string>
<string name="cancel">Abbrechen</string>
<string name="preshared_key">Geteilter Schlüssel</string>
<string name="enabled_app_shortcuts">Aktiviere App Verknüpfungen</string>
<string name="exported_configs_message">Konfigurationen in den Download Ordner exportiert</string>
<string name="tunnel_on_wifi">Tunnel bei nicht vertrauenswürdigem Wifi</string>
<string name="email_subject">WG Tunnel Unterstützung</string>
<string name="docs_description">Lese die Dokumentation</string>
<string name="email_description">Sende mir eine E-Mail</string>
<string name="support_help_text">Bei Fehlern oder Verbesserungsvorschlägen stehen folgende Ressourcen zur Verfügung:</string>
<string name="error_root_denied">Root Shell verboten</string>
<string name="error_no_file_explorer">Kein Datei-Explorer installiert</string>
<string name="location_services_missing_message">Die App konnte keine aktivierten Standortdienste auf deinem Gerät erkennen. Dies kann auf manchen Geräten ein Auslesen des aktuellen WIFI-Names für die \"Nicht vertrauenswürdiges WIFI\" Funktion verhindern. Möchtest du trotzdem fortfahren ?</string>
<string name="auto_tunnel_title">Auto-Tunnel Service</string>
<string name="delete_tunnel_message">Bist du sicher, dass du den Tunnel löschen möchtest?</string>
<string name="yes">Ja</string>
<string name="resume">Fortsetzen</string>
<string name="pause">Pausieren</string>
<string name="paused">Pausiert</string>
<string name="active">Aktiv</string>
<string name="go">Gehe</string>
<string name="excluded">Ausgeschlossen</string>
<string name="all">Alle</string>
<string name="always_on_disabled">Always-on VPN wollte eine Tunnel starten, aber dieses Feature ist in den Einstellungen deaktiviert.</string>
<string name="no_browser_detected">Kein Browser erkannt</string>
<string name="open_issue">Öffne ein Issue</string>
<string name="read_logs">Lese die Logs</string>
<string name="auto">(automatisch)</string>
<string name="config_parse_error">Fehler beim lesen der Konfiguration</string>
<string name="incorrect_pin">PIN is nicht korrekt</string>
<string name="pin_created">PIN erfolgreich angelegt</string>
<string name="enter_pin">Gib deine PIN ein</string>
<string name="auto_off">Auto-Tunnel pausieren</string>
<string name="auto_tun_on">Auto-Tunnel fortsetzen</string>
<string name="auto_tun_off">Auto-Tunnel pausieren</string>
<string name="version">Version</string>
<string name="mode">Modus</string>
<string name="userspace">Userspace</string>
<string name="settings">Einstellungen</string>
<string name="support">Unterstützung</string>
<string name="watcher_channel_id">Beobachter Kanal</string>
<string name="error_authentication_failed">Anmeldung fehlgeschlagen</string>
<string name="export_configs">Exportiere Konfigurationen</string>
<string name="unknown_error">Ein unbekannter Fehler ist aufgetreten</string>
<string name="email_chooser">Sende eine E-Mail…</string>
<string name="error_authorization_failed">Fehler bei der Authorisierung</string>
<string name="location_services_required">Standortdienste erforderlich</string>
<string name="precise_location_required">Genauer Standort erforderlich</string>
<string name="error_invalid_code">Fehlerhafter QR code</string>
<string name="error_none">Kein Fehler</string>
<string name="tunneling_apps">Tunnel Anwendungen</string>
<string name="included">Hinzugefügt</string>
<string name="no_email_detected">Keine E-Mail Anwendung erkannt</string>
<string name="logs_saved">Logs im Download Ordner gespeichert</string>
<string name="create_pin">Erstelle eine PIN</string>
<string name="use_tunnel_on_wifi_name">Tunnel in Wifi-Namen verwenden</string>
<string name="no_wifi_names_configured">Keine Wifi-Namen für diesen Tunnel konfiguriert</string>
<string name="disabled">Deaktiviert</string>
<string name="mobile_data_tunnel">Als Tunnel für Mobile Daten setzen</string>
<string name="general">Allgemein</string>
<string name="restart_on_ping">Neustart bei PING Fehler (Beta)</string>
<string name="edit_tunnel">Tunnel bearbeiten</string>
<string name="set_primary_tunnel">Als Primären Tunnel setzen</string>
<string name="auto_on">Auto-Tunnel fortsetzen</string>
<string name="vpn_channel_id">VPN Kanal</string>
<string name="vpn_channel_name">VPN Benachrichtigungskanal</string>
<string name="watcher_channel_name">Beobachter Benachrichtigungskanal</string>
<string name="turn_off_tunnel">Aktion erfordert deaktivierten Tunnel</string>
<string name="kernel">Kernel</string>
<string name="use_kernel">Verwende das Kernel Modul</string>
<string name="error_ssid_exists">SSID existiert bereits</string>
<string name="use_amnezia">"Benutze Amnezia Benuzterumgebung "</string>
<string name="junk_packet_count">Müll Packet Anzahk</string>
<string name="junk_packet_maximum_size">Müll Packet maximale Grösse</string>
<string name="init_packet_junk_size">Erstes Packet Müllgrösse</string>
<string name="backend">Backend</string>
<string name="junk_packet_minimum_size">Müll Packet minimale Grösse</string>
<string name="response_packet_junk_size">Antwortpaket Müllgrösse</string>
</resources>
+1 -6
View File
@@ -154,9 +154,4 @@
<string name="watcher_channel_id">Canal del obvervador</string>
<string name="watcher_channel_name">Canal de notificación del obvervador</string>
<string name="prominent_background_location_message">La monitorización SSID Wi-Fi necesita de permiso de ubicación en segundo plano incluso si la app está cerrada. Mira el enlace a la Política de Privacidad en la pantalla de ayuda para más detalles.</string>
<string name="privacy_policy_url" translatable="false"></string>
<string name="docs_url" translatable="false"></string>
<string name="github_url" translatable="false"></string>
<string name="discord_url" translatable="false"></string>
<string name="my_email" translatable="false"></string>
</resources>
</resources>
+37 -1
View File
@@ -1,3 +1,39 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
</resources>
<string name="turn_off_tunnel">Действие требует отключения туннеля</string>
<string name="add_tunnel">Добавить туннель</string>
<string name="mtu">MTU</string>
<string name="tunnel_name">Имя туннеля</string>
<string name="public_key">Публичный ключ</string>
<string name="name">Имя</string>
<string name="peer">Пир</string>
<string name="privacy_policy">Посмотреть политику конфиденциальности</string>
<string name="icon">Иконка</string>
<string name="turn_on">Включить</string>
<string name="add_from_qr">Добавить из QR</string>
<string name="qr_scan">Сканер QR</string>
<string name="auto_tunneling">Авто-туннелирование</string>
<string name="no_tunnels">Туннели еще не добавлены!</string>
<string name="open_file">Открыть файл</string>
<string name="exclude">Исключить</string>
<string name="include">Включить</string>
<string name="tunnel_all">Туннель для всех приложений</string>
<string name="config_changes_saved">Изменения конфигурации сохранены.</string>
<string name="save_changes">Сохранить</string>
<string name="no_thanks">Нет, спасибо</string>
<string name="map">Карта</string>
<string name="addresses">Адреса</string>
<string name="dns_servers">DNS сервера</string>
<string name="allowed_ips">Разрешенные IP</string>
<string name="endpoint">Конечная точка</string>
<string name="restart">Перезагрузить туннель</string>
<string name="vpn_connection_failed">Ошибка соединения</string>
<string name="always_on_vpn_support">Разрешить постоянный VPN</string>
<string name="hint_search_packages">Поиск приложений</string>
<string name="other">Другое</string>
<string name="vpn_on">VPN вкл.</string>
<string name="vpn_off">VPN откл.</string>
<string name="interface_">Интерфейс</string>
<string name="optional">(необязательно)</string>
<string name="optional_no_recommend">(необязательно, не рекомендуется)</string>
</resources>
+22 -2
View File
@@ -94,6 +94,7 @@
<string name="error_authorization_failed">Failed to authorize</string>
<string name="enabled_app_shortcuts">Enable app shortcuts</string>
<string name="export_configs">Export configs</string>
<string name="export_configs_failed">Failed to export configs</string>
<string name="location_services_required">Location services required</string>
<string name="background_location_required">Background location required</string>
<string name="precise_location_required">Precise location required</string>
@@ -108,7 +109,6 @@
<string name="discord_description">Join the community</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="kernel">Kernel</string>
<string name="use_kernel">Use kernel module</string>
<string name="error_ssid_exists">SSID already exists</string>
<string name="error_root_denied">Root shell denied</string>
@@ -140,7 +140,7 @@
<string name="pin_created">Pin successfully created</string>
<string name="enter_pin">Enter your pin</string>
<string name="create_pin">Create pin</string>
<string name="enable_app_lock">Enabled app lock</string>
<string name="enable_app_lock">Enable app lock</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="set_primary_tunnel">Set as primary tunnel</string>
@@ -158,4 +158,24 @@
<string name="userspace">Userspace</string>
<string name="settings">Settings</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>
<string name="amnezia" translatable="false">Amnezia</string>
<string name="wireguard" translatable="false">WireGuard</string>
<string name="error_file_format">Invalid tunnel config format</string>
</resources>
+2 -2
View File
@@ -1,7 +1,7 @@
object Constants {
const val VERSION_NAME = "3.4.3-alpha"
const val VERSION_NAME = "3.4.4"
const val JVM_TARGET = "17"
const val VERSION_CODE = 34201
const val VERSION_CODE = 34400
const val TARGET_SDK = 34
const val MIN_SDK = 26
const val APP_ID = "com.zaneschepke.wireguardautotunnel"
@@ -0,0 +1,7 @@
Verbesserungen:
- Refaktorisierung des Zustandsmanagements
- Verbesserung der AndroidTV Navigation
- Verbesserung der Auto-Tunnel Effizienz
- Verbesserung der Navigation
- Auto-Tunnel Pause Funktion
- Viele Fehlerbehebungen
@@ -0,0 +1,8 @@
Verbesserungen:
- Refaktorisierung des Zustandsmanagements
- Verbesserung der AndroidTV Navigation
- Verbesserung der Auto-Tunnel Effizienz
- Verbesserung der Navigation
- Auto-Tunnel Pause Funktion
- Reparatur des Auto-Tunnel starts im Vordergrund
- Viele Fehlerbehebungen
@@ -0,0 +1,7 @@
Verbesserungen:
- Bestätigung beim Tunnel löschen hinzugefügt
- Hintergrundberechtigung für Energiesparmodus hinzugefügt
Fehlerbehebungen:
- Einfrieren der App bei Tunnel deaktivierung
- Fehler im E-Mail Empfängerfeld
- Konfigurationsbearbeitung mit leerem DNS Feld
@@ -0,0 +1,3 @@
Verbesserungen:
- Erstellte Konfiguration wurde nicht gespeichert
- Version angehoben
@@ -0,0 +1,2 @@
Was ist neu:
- Dies ist eine CI Test version
@@ -0,0 +1,5 @@
Was ist neu:
- Automatischer Start des Always-On VPN im Kernelmodus nach einem Systemneustart
- Unterstützung für adaptive Designicons
- Benachrichtigungs- und Kachelicons korrigiert
- AndroidTV icons korrigiert
@@ -0,0 +1,5 @@
Was ist neu:
- Verbesserung des ersten Starts
- Wechsel zu Wireguard Bibliotheks Fork
- Abfrage der VPN Berechtigung beim ersten VPN Start
- Version angehoben
@@ -0,0 +1,2 @@
Was ist neu:
- GUI Fix for Tunnel Anzeige
@@ -0,0 +1,4 @@
Was ist neu:
- GUI Fix für Konfigurationseditor
- AOVPN Benachrichtung für ersten Start auf GrapheneOS hinzugefügt
- Version angehoben
@@ -0,0 +1,5 @@
Was ist neu:
- Log anzeige hinzugefügt
- Lokale App Sperre hinzugefügt
- Funktion für Neustart des Tunnels nach Fehlerhaftem Ping
- Verschiedene Fehlerbehebungen
@@ -0,0 +1,5 @@
Was ist neu:
- Auto-Tunnel Auswahl nach WLAN-Name
- Auto-Tunnel Kontrolle durch Kacheln und Verknüpfungen
- Automatischer Neustart des Manuellen Tunnels nach einem Systemneustart
- Verschiedene Fehlerbehebungen und Performanceverbesserungen
@@ -0,0 +1,5 @@
Was ist neu:
- Verbesserung der Auto-Tunnel Zuverlässigkeit
- Verbesserung der Kachelsynchronisierung
- AndroidTV Asset hinzugefügt
- APK Fingerabdruck hinzugefügt
@@ -0,0 +1,5 @@
Was ist neu:
- Behebung einer Regression beim Tunnel Stop
- Log Verschleierung hinzugefügt
- Ausblenden des Icons beim Scrollen
- Türkische Übersetzung hinzugefügt
@@ -0,0 +1,3 @@
Was ist neu:
- Amnezia seite-zu-seite mit WireGuard hinzugefügt
- App Shortcut Bug behoben
@@ -1,2 +0,0 @@
What's new:
- AmneziaWG support
@@ -0,0 +1,3 @@
What's new:
- Add Amnezia side-by-side with WireGuard
- Fix app shortcuts bug
@@ -0,0 +1,6 @@
What's new:
- Official support for AmneziaWG
- Import/export for Amnezia configs
- Auto-tunnel to only toggle once per network change
- Additional languages support
- Other bug fixes and improvements
@@ -0,0 +1,5 @@
What's new:
- Improve tunnel import naming
- Fix auto tunneling init state bug
- Improved error handling
- Fix Amnezia zip import bug
@@ -4,8 +4,9 @@ Features
- Auto connect to VPN based on Wi-Fi SSID, ethernet, or mobile data
- Split tunneling by application with search
- WireGuard support for kernel and userspace modes
- Amnezia support for userspace mode for DPI/censorship protection
- Always-On VPN support
- Export tunnels to zip
- Export Amnezia and WireGuard tunnels to zip
- Quick tile support for VPN toggling
- Static shortcuts support for primary tunnel for automation integration
- Intent automation support for all tunnels
@@ -0,0 +1,7 @@
Mejoras:
- Gestión del estado de refactorización.
- Mejorar la navegación de AndroidTV
- Mejorar la eficiencia del túnel automático
- Mejorar la navegación
- Función de pausa de túnel automático
- Muchas correcciones de errores
@@ -0,0 +1,7 @@
Mejoras:
- Gestión del estado de refactorización.
- Mejorar la navegación de AndroidTV
- Mejorar la eficiencia del túnel automático
- Mejorar la navegación
- Función de pausa de túnel automático
- Muchas correcciones de errores
@@ -0,0 +1,8 @@
Mejoras:
- Gestión del estado de refactorización.
- Mejorar la navegación de AndroidTV
- Mejorar la eficiencia del túnel automático
- Mejorar la navegación
- Función de pausa de túnel automático
- Se corrigió el inicio del túnel automático en primer plano.
- Muchas correcciones de errores
@@ -0,0 +1,7 @@
Mejoras:
- Se agregó confirmación al eliminar el túnel.
- Se agregó permiso en segundo plano para el modo de ahorro de energía.
Corrección de errores:
- La aplicación se congela cuando el túnel está desactivado.
- Error en el campo del destinatario del correo electrónico
- Edición de configuración con campo DNS vacío
@@ -0,0 +1,3 @@
Mejoras:
- La configuración creada no se guardó.
- Versión elevada
@@ -0,0 +1,2 @@
Qué hay de nuevo:
- Esta es una versión de prueba de CI
@@ -0,0 +1,5 @@
Qué hay de nuevo:
- Inicio automático al reiniciar para el modo kernel de VPN siempre activo
- Soporte para íconos de temas adaptables
- Arreglar íconos de notificación, ícono de mosaico
- Reparar iconos de AndroidTV

Some files were not shown because too many files have changed in this diff Show More