Compare commits

..

1 Commits

Author SHA1 Message Date
Zane Schepke 6f48147b3e feat: add amneziawg support 2024-04-27 21:44:41 -04:00
198 changed files with 1697 additions and 4865 deletions
-20
View File
@@ -1,20 +0,0 @@
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 }}"
+2 -1
View File
@@ -1,3 +1,4 @@
# name of the workflow
name: Android CI Tag Deployment (Pre-release) name: Android CI Tag Deployment (Pre-release)
on: on:
@@ -67,7 +68,7 @@ jobs:
echo "VERSION_CODE=$version_code" >> $GITHUB_ENV 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 # Save the APK after the Build job is complete to publish it as a Github release in the next job
- name: Upload APK - name: Upload APK
uses: actions/upload-artifact@v4.3.3 uses: actions/upload-artifact@v4.3.1
with: with:
name: wgtunnel name: wgtunnel
path: ${{ steps.apk-path.outputs.path }} path: ${{ steps.apk-path.outputs.path }}
-21
View File
@@ -1,21 +0,0 @@
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 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 # Save the APK after the Build job is complete to publish it as a Github release in the next job
- name: Upload APK - name: Upload APK
uses: actions/upload-artifact@v4.3.3 uses: actions/upload-artifact@v4.3.1
with: with:
name: wgtunnel name: wgtunnel
path: ${{ steps.apk-path.outputs.path }} path: ${{ steps.apk-path.outputs.path }}
+13 -23
View File
@@ -22,8 +22,7 @@ WG Tunnel
<div align="left"> <div align="left">
This is an alternative Android Application for [WireGuard](https://www.wireguard.com/) This is an alternative Android Application for [WireGuard](https://www.wireguard.com/) with added
and [AmneziaWG](https://docs.amnezia.org/documentation/amnezia-wg/) with added
features. Built using the [wireguard-android](https://github.com/WireGuard/wireguard-android) 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 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. inspired by the official [WireGuard Android](https://github.com/WireGuard/wireguard-android) app.
@@ -54,9 +53,8 @@ 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 * Auto connect to tunnels based on Wi-Fi SSID, ethernet, or mobile data
* Split tunneling by application with search * Split tunneling by application with search
* WireGuard support for kernel and userspace modes * WireGuard support for kernel and userspace modes
* Amnezia support for userspace mode for DPI/censorship protection
* Always-On VPN support * Always-On VPN support
* Export Amnezia and WireGuard tunnels to zip * Export tunnels to zip
* Quick tile support for tunnel toggling, auto-tunneling * Quick tile support for tunnel toggling, auto-tunneling
* Static shortcuts support for tunnel toggling, auto-tunneling * Static shortcuts support for tunnel toggling, auto-tunneling
* Intent automation support for all tunnels * Intent automation support for all tunnels
@@ -65,28 +63,27 @@ and on while on different networks. This app was created to offer a free solutio
* Battery preservation measures * Battery preservation measures
* Restart tunnel on ping failure (beta) * Restart tunnel on ping failure (beta)
## Fdroid
Want updates faster?
Check out my personal [fdroid repository](https://github.com/zaneschepke/fdroid) to get updates the
moment they are released.
## Docs ## Docs
Information about features, behaviors, and answers to common questions can be found in the Basic documentation of the feature and behaviors of this app can be
app [documentation](https://zaneschepke.com/wgtunnel-docs/overview.html). found [here](https://zaneschepke.com/wgtunnel-docs/overview.html).
The repository for these docs can be found [here](https://github.com/zaneschepke/wgtunnel-docs). The repository for these docs can be found [here](https://github.com/zaneschepke/wgtunnel-docs).
## Contributing
Any contributions in the form of feedback, issues, code, or translations are welcome and much appreciated!
Please read the [code of conduct](https://github.com/zaneschepke/wgtunnel?tab=coc-ov-file#contributor-code-of-conduct) before contributing.
## Translation ## Translation
This app is using [Weblate](https://weblate.org) to assist with translations. This app is using [Weblate](https://weblate.org) to assist with translations.
Help translate WG Tunnel into your language Help translate WG Tunnel into your language at [Hosted Weblate](https://hosted.weblate.org/engage/wg-tunnel/).\
at [Hosted Weblate](https://hosted.weblate.org/engage/wg-tunnel/).\
[![Translation status](https://hosted.weblate.org/widgets/wg-tunnel/-/multi-auto.svg)](https://hosted.weblate.org/engage/wg-tunnel/) [![Translation status](https://hosted.weblate.org/widgets/wg-tunnel/-/multi-auto.svg)](https://hosted.weblate.org/engage/wg-tunnel/)
## Building ## Building
``` ```
@@ -100,11 +97,4 @@ And then build the app:
$ ./gradlew assembleDebug $ ./gradlew assembleDebug
``` ```
## Contributing </span>
Any contributions in the form of feedback, issues, code, or translations are welcome and much
appreciated!
Please read
the [code of conduct](https://github.com/zaneschepke/wgtunnel?tab=coc-ov-file#contributor-code-of-conduct)
before contributing.
+5 -8
View File
@@ -112,6 +112,9 @@ android {
} }
create("general") { create("general") {
dimension = Constants.TYPE dimension = Constants.TYPE
if (BuildHelper.isReleaseBuild(gradle) && BuildHelper.isGeneralFlavor(gradle)) {
//any plugins general specific
}
} }
} }
compileOptions { compileOptions {
@@ -136,7 +139,6 @@ dependencies {
implementation(libs.androidx.core.ktx) implementation(libs.androidx.core.ktx)
implementation(libs.androidx.lifecycle.runtime.ktx) implementation(libs.androidx.lifecycle.runtime.ktx)
// helpers for implementing LifecycleOwner in a Service // helpers for implementing LifecycleOwner in a Service
implementation(libs.androidx.lifecycle.service) implementation(libs.androidx.lifecycle.service)
implementation(libs.androidx.activity.compose) implementation(libs.androidx.activity.compose)
@@ -158,8 +160,7 @@ dependencies {
debugImplementation(libs.androidx.compose.ui.tooling) debugImplementation(libs.androidx.compose.ui.tooling)
debugImplementation(libs.androidx.compose.manifest) debugImplementation(libs.androidx.compose.manifest)
// get tunnel lib from github packages or mavenLocal // tunnel
implementation(libs.tunnel)
implementation(libs.amneziawg.android) implementation(libs.amneziawg.android)
coreLibraryDesugaring(libs.desugar.jdk.libs) coreLibraryDesugaring(libs.desugar.jdk.libs)
@@ -171,8 +172,6 @@ dependencies {
implementation(libs.androidx.navigation.compose) implementation(libs.androidx.navigation.compose)
implementation(libs.androidx.hilt.navigation.compose) implementation(libs.androidx.hilt.navigation.compose)
implementation(libs.zaneschepke.multifab)
// hilt // hilt
implementation(libs.hilt.android) implementation(libs.hilt.android)
ksp(libs.hilt.android.compiler) ksp(libs.hilt.android.compiler)
@@ -200,6 +199,7 @@ dependencies {
// barcode scanning // barcode scanning
implementation(libs.zxing.android.embedded) implementation(libs.zxing.android.embedded)
implementation(libs.zxing.core)
// bio // bio
implementation(libs.androidx.biometric.ktx) implementation(libs.androidx.biometric.ktx)
@@ -208,7 +208,4 @@ dependencies {
// shortcuts // shortcuts
implementation(libs.androidx.core) implementation(libs.androidx.core)
implementation(libs.androidx.core.google.shortcuts) implementation(libs.androidx.core.google.shortcuts)
// splash
implementation(libs.androidx.core.splashscreen)
} }
+2
View File
@@ -22,3 +22,5 @@
-keepclassmembers class * extends androidx.datastore.preferences.protobuf.GeneratedMessageLite { -keepclassmembers class * extends androidx.datastore.preferences.protobuf.GeneratedMessageLite {
<fields>; <fields>;
} }
@@ -1,190 +0,0 @@
{
"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')"
]
}
}
+11 -16
View File
@@ -51,7 +51,7 @@
</queries> </queries>
<application <application
android:name=".WireGuardAutoTunnel" android:name=".WireGuardAutoTunnel"
android:allowBackup="false" android:allowBackup="true"
android:banner="@drawable/ic_banner" android:banner="@drawable/ic_banner"
android:dataExtractionRules="@xml/data_extraction_rules" android:dataExtractionRules="@xml/data_extraction_rules"
android:enableOnBackInvokedCallback="true" android:enableOnBackInvokedCallback="true"
@@ -60,36 +60,31 @@
android:label="@string/app_name" android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round" android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true" android:supportsRtl="true"
android:theme="@style/Theme.AppSplashScreen" android:theme="@style/Theme.WireguardAutoTunnel"
tools:targetApi="tiramisu"> tools:targetApi="tiramisu">
<activity <activity
android:name=".ui.SplashActivity" android:name=".ui.MainActivity"
android:exported="true" android:exported="true"
android:theme="@style/Theme.AppSplashScreen"> android:theme="@style/Theme.WireguardAutoTunnel">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN" /> <action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" /> <category android:name="android.intent.category.LAUNCHER" />
<category android:name="android.intent.category.LEANBACK_LAUNCHER" /> <category android:name="android.intent.category.LEANBACK_LAUNCHER" />
<action android:name="android.service.quicksettings.action.QS_TILE_PREFERENCES" />
</intent-filter> </intent-filter>
<meta-data <meta-data
android:name="android.app.shortcuts" android:name="android.app.shortcuts"
android:resource="@xml/shortcuts" /> android:resource="@xml/shortcuts" />
</activity> </activity>
<activity <activity
android:name=".ui.MainActivity" android:name=".ui.CaptureActivityPortrait"
android:exported="true" android:screenOrientation="fullSensor"
android:screenOrientation="portrait" android:stateNotNeeded="true"
android:theme="@style/Theme.WireguardAutoTunnel"> android:theme="@style/zxing_CaptureTheme"
<intent-filter> android:windowSoftInputMode="stateAlwaysHidden"
<action android:name="android.service.quicksettings.action.QS_TILE_PREFERENCES" /> tools:ignore="DiscouragedApi" />
</intent-filter>
</activity>
<activity
android:name="com.journeyapps.barcodescanner.CaptureActivity"
android:screenOrientation="portrait"
tools:replace="screenOrientation" />
<activity <activity
android:name=".service.shortcut.ShortcutsActivity" android:name=".service.shortcut.ShortcutsActivity"
android:enabled="true" android:enabled="true"
@@ -2,37 +2,26 @@ package com.zaneschepke.wireguardautotunnel
import android.app.Application import android.app.Application
import android.content.ComponentName import android.content.ComponentName
import android.content.Context
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.os.StrictMode
import android.os.StrictMode.ThreadPolicy
import android.service.quicksettings.TileService import android.service.quicksettings.TileService
import com.zaneschepke.wireguardautotunnel.service.tile.AutoTunnelControlTile import com.zaneschepke.wireguardautotunnel.service.tile.AutoTunnelControlTile
import com.zaneschepke.wireguardautotunnel.service.tile.TunnelControlTile import com.zaneschepke.wireguardautotunnel.service.tile.TunnelControlTile
import com.zaneschepke.wireguardautotunnel.util.ReleaseTree import com.zaneschepke.wireguardautotunnel.util.ReleaseTree
import dagger.hilt.android.HiltAndroidApp import dagger.hilt.android.HiltAndroidApp
import timber.log.Timber import timber.log.Timber
import xyz.teamgravity.pin_lock_compose.PinManager
@HiltAndroidApp @HiltAndroidApp
class WireGuardAutoTunnel : Application() { class WireGuardAutoTunnel : Application() {
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
instance = this instance = this
if (BuildConfig.DEBUG) { if (BuildConfig.DEBUG) Timber.plant(Timber.DebugTree()) else Timber.plant(ReleaseTree())
Timber.plant(Timber.DebugTree()) PinManager.initialize(this)
StrictMode.setThreadPolicy(
ThreadPolicy.Builder()
.detectDiskReads()
.detectDiskWrites()
.detectNetwork()
.penaltyLog()
.build(),
)
} else Timber.plant(ReleaseTree())
} }
companion object { companion object {
lateinit var instance: WireGuardAutoTunnel lateinit var instance: WireGuardAutoTunnel
private set private set
@@ -40,16 +29,16 @@ class WireGuardAutoTunnel : Application() {
return instance.packageManager.hasSystemFeature(PackageManager.FEATURE_LEANBACK) return instance.packageManager.hasSystemFeature(PackageManager.FEATURE_LEANBACK)
} }
fun requestTunnelTileServiceStateUpdate() { fun requestTunnelTileServiceStateUpdate(context: Context) {
TileService.requestListeningState( TileService.requestListeningState(
instance, context,
ComponentName(instance, TunnelControlTile::class.java), ComponentName(instance, TunnelControlTile::class.java),
) )
} }
fun requestAutoTunnelTileServiceUpdate() { fun requestAutoTunnelTileServiceUpdate(context: Context) {
TileService.requestListeningState( TileService.requestListeningState(
instance, context,
ComponentName(instance, AutoTunnelControlTile::class.java), ComponentName(instance, AutoTunnelControlTile::class.java),
) )
} }
@@ -6,12 +6,12 @@ import androidx.room.DeleteColumn
import androidx.room.RoomDatabase import androidx.room.RoomDatabase
import androidx.room.TypeConverters import androidx.room.TypeConverters
import androidx.room.migration.AutoMigrationSpec import androidx.room.migration.AutoMigrationSpec
import com.zaneschepke.wireguardautotunnel.data.domain.Settings import com.zaneschepke.wireguardautotunnel.data.model.Settings
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
@Database( @Database(
entities = [Settings::class, TunnelConfig::class], entities = [Settings::class, TunnelConfig::class],
version = 8, version = 7,
autoMigrations = autoMigrations =
[ [
AutoMigration(from = 1, to = 2), AutoMigration(from = 1, to = 2),
@@ -33,7 +33,6 @@ import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
to = 7, to = 7,
spec = RemoveLegacySettingColumnsMigration::class, spec = RemoveLegacySettingColumnsMigration::class,
), ),
AutoMigration(7, 8),
], ],
exportSchema = true, exportSchema = true,
) )
@@ -5,7 +5,7 @@ import androidx.room.Delete
import androidx.room.Insert import androidx.room.Insert
import androidx.room.OnConflictStrategy import androidx.room.OnConflictStrategy
import androidx.room.Query import androidx.room.Query
import com.zaneschepke.wireguardautotunnel.data.domain.Settings import com.zaneschepke.wireguardautotunnel.data.model.Settings
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
@Dao @Dao
@@ -5,7 +5,7 @@ import androidx.room.Delete
import androidx.room.Insert import androidx.room.Insert
import androidx.room.OnConflictStrategy import androidx.room.OnConflictStrategy
import androidx.room.Query import androidx.room.Query
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
import com.zaneschepke.wireguardautotunnel.util.TunnelConfigs import com.zaneschepke.wireguardautotunnel.util.TunnelConfigs
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
@@ -20,9 +20,6 @@ interface TunnelConfigDao {
@Query("SELECT * FROM TunnelConfig WHERE id=:id") @Query("SELECT * FROM TunnelConfig WHERE id=:id")
suspend fun getById(id: Long): TunnelConfig? suspend fun getById(id: Long): TunnelConfig?
@Query("SELECT * FROM TunnelConfig WHERE name=:name")
suspend fun getByName(name: String): TunnelConfig?
@Query("SELECT * FROM TunnelConfig") @Query("SELECT * FROM TunnelConfig")
suspend fun getAll(): TunnelConfigs suspend fun getAll(): TunnelConfigs
@@ -36,10 +33,10 @@ interface TunnelConfigDao {
suspend fun findByTunnelNetworkName(name: String): TunnelConfigs suspend fun findByTunnelNetworkName(name: String): TunnelConfigs
@Query("UPDATE TunnelConfig SET is_primary_tunnel = 0 WHERE is_primary_tunnel =1") @Query("UPDATE TunnelConfig SET is_primary_tunnel = 0 WHERE is_primary_tunnel =1")
suspend fun resetPrimaryTunnel() fun resetPrimaryTunnel()
@Query("UPDATE TunnelConfig SET is_mobile_data_tunnel = 0 WHERE is_mobile_data_tunnel =1") @Query("UPDATE TunnelConfig SET is_mobile_data_tunnel = 0 WHERE is_mobile_data_tunnel =1")
suspend fun resetMobileDataTunnel() fun resetMobileDataTunnel()
@Query("SELECT * FROM TUNNELCONFIG WHERE is_primary_tunnel=1") @Query("SELECT * FROM TUNNELCONFIG WHERE is_primary_tunnel=1")
suspend fun findByPrimary(): TunnelConfigs suspend fun findByPrimary(): TunnelConfigs
@@ -7,20 +7,14 @@ import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.intPreferencesKey import androidx.datastore.preferences.core.intPreferencesKey
import androidx.datastore.preferences.core.stringPreferencesKey import androidx.datastore.preferences.core.stringPreferencesKey
import androidx.datastore.preferences.preferencesDataStore import androidx.datastore.preferences.preferencesDataStore
import com.zaneschepke.wireguardautotunnel.module.IoDispatcher
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import timber.log.Timber import timber.log.Timber
import java.io.IOException import java.io.IOException
class DataStoreManager( class DataStoreManager(private val context: Context) {
private val context: Context,
@IoDispatcher private val ioDispatcher: CoroutineDispatcher
) {
companion object { companion object {
val LOCATION_DISCLOSURE_SHOWN = booleanPreferencesKey("LOCATION_DISCLOSURE_SHOWN") val LOCATION_DISCLOSURE_SHOWN = booleanPreferencesKey("LOCATION_DISCLOSURE_SHOWN")
val BATTERY_OPTIMIZE_DISABLE_SHOWN = booleanPreferencesKey("BATTERY_OPTIMIZE_DISABLE_SHOWN") val BATTERY_OPTIMIZE_DISABLE_SHOWN = booleanPreferencesKey("BATTERY_OPTIMIZE_DISABLE_SHOWN")
@@ -28,7 +22,6 @@ class DataStoreManager(
booleanPreferencesKey("TUNNEL_RUNNING_FROM_MANUAL_START") booleanPreferencesKey("TUNNEL_RUNNING_FROM_MANUAL_START")
val ACTIVE_TUNNEL = intPreferencesKey("ACTIVE_TUNNEL") val ACTIVE_TUNNEL = intPreferencesKey("ACTIVE_TUNNEL")
val CURRENT_SSID = stringPreferencesKey("CURRENT_SSID") val CURRENT_SSID = stringPreferencesKey("CURRENT_SSID")
val IS_PIN_LOCK_ENABLED = booleanPreferencesKey("PIN_LOCK_ENABLED")
} }
// preferences // preferences
@@ -39,24 +32,20 @@ class DataStoreManager(
) )
suspend fun init() { suspend fun init() {
withContext(ioDispatcher) { try {
try { context.dataStore.data.first()
context.dataStore.data.first() } catch (e: IOException) {
} catch (e: IOException) { Timber.e(e)
Timber.e(e)
}
} }
} }
suspend fun <T> saveToDataStore(key: Preferences.Key<T>, value: T) { suspend fun <T> saveToDataStore(key: Preferences.Key<T>, value: T) {
withContext(ioDispatcher) { try {
try { context.dataStore.edit { it[key] = value }
context.dataStore.edit { it[key] = value } } catch (e: IOException) {
} catch (e: IOException) { Timber.e(e)
Timber.e(e) } catch (e: Exception) {
} catch (e: Exception) { Timber.e(e)
Timber.e(e)
}
} }
} }
@@ -64,13 +53,11 @@ class DataStoreManager(
fun <T> getFromStoreFlow(key: Preferences.Key<T>) = context.dataStore.data.map { it[key] } fun <T> getFromStoreFlow(key: Preferences.Key<T>) = context.dataStore.data.map { it[key] }
suspend fun <T> getFromStore(key: Preferences.Key<T>): T? { suspend fun <T> getFromStore(key: Preferences.Key<T>): T? {
return withContext(ioDispatcher) { return try {
try { context.dataStore.data.map { it[key] }.first()
context.dataStore.data.map { it[key] }.first() } catch (e: IOException) {
} catch (e: IOException) { Timber.e(e)
Timber.e(e) null
null
}
} }
} }
@@ -1,16 +0,0 @@
package com.zaneschepke.wireguardautotunnel.data.domain
data class GeneralState(
val isLocationDisclosureShown: Boolean = LOCATION_DISCLOSURE_SHOWN_DEFAULT,
val isBatteryOptimizationDisableShown: Boolean = BATTERY_OPTIMIZATION_DISABLE_SHOWN_DEFAULT,
val isTunnelRunningFromManualStart: Boolean = TUNNELING_RUNNING_FROM_MANUAL_START_DEFAULT,
val isPinLockEnabled: Boolean = PIN_LOCK_ENABLED_DEFAULT,
val activeTunnelId: Int? = null
) {
companion object {
const val LOCATION_DISCLOSURE_SHOWN_DEFAULT = false
const val BATTERY_OPTIMIZATION_DISABLE_SHOWN_DEFAULT = false
const val TUNNELING_RUNNING_FROM_MANUAL_START_DEFAULT = false
const val PIN_LOCK_ENABLED_DEFAULT = false
}
}
@@ -0,0 +1,14 @@
package com.zaneschepke.wireguardautotunnel.data.model
data class GeneralState(
val locationDisclosureShown: Boolean = LOCATION_DISCLOSURE_SHOWN_DEFAULT,
val batteryOptimizationDisableShown: Boolean = BATTERY_OPTIMIZATION_DISABLE_SHOWN_DEFAULT,
val tunnelRunningFromManualStart: Boolean = TUNNELING_RUNNING_FROM_MANUAL_START_DEFAULT,
val activeTunnelId: Int? = null
) {
companion object {
const val LOCATION_DISCLOSURE_SHOWN_DEFAULT = false
const val BATTERY_OPTIMIZATION_DISABLE_SHOWN_DEFAULT = false
const val TUNNELING_RUNNING_FROM_MANUAL_START_DEFAULT = false
}
}
@@ -1,4 +1,4 @@
package com.zaneschepke.wireguardautotunnel.data.domain package com.zaneschepke.wireguardautotunnel.data.model
import androidx.room.ColumnInfo import androidx.room.ColumnInfo
import androidx.room.Entity import androidx.room.Entity
@@ -50,9 +50,4 @@ data class Settings(
defaultValue = "false", defaultValue = "false",
) )
val isPingEnabled: Boolean = false, val isPingEnabled: Boolean = false,
@ColumnInfo(
name = "is_amnezia_enabled",
defaultValue = "false",
)
val isAmneziaEnabled: Boolean = false,
) )
@@ -1,10 +1,10 @@
package com.zaneschepke.wireguardautotunnel.data.domain package com.zaneschepke.wireguardautotunnel.data.model
import androidx.room.ColumnInfo import androidx.room.ColumnInfo
import androidx.room.Entity import androidx.room.Entity
import androidx.room.Index import androidx.room.Index
import androidx.room.PrimaryKey import androidx.room.PrimaryKey
import com.wireguard.config.Config import org.amnezia.awg.config.Config
import java.io.InputStream import java.io.InputStream
@Entity(indices = [Index(value = ["name"], unique = true)]) @Entity(indices = [Index(value = ["name"], unique = true)])
@@ -27,27 +27,12 @@ data class TunnelConfig(
defaultValue = "false", defaultValue = "false",
) )
val isPrimaryTunnel: Boolean = false, val isPrimaryTunnel: Boolean = false,
@ColumnInfo(
name = "am_quick",
defaultValue = "",
)
val amQuick: String = AM_QUICK_DEFAULT,
) { ) {
companion object { companion object {
fun configFromWgQuick(wgQuick: String): Config { fun configFromQuick(wgQuick: String): Config {
val inputStream: InputStream = wgQuick.byteInputStream() val inputStream: InputStream = wgQuick.byteInputStream()
return inputStream.bufferedReader(Charsets.UTF_8).use { val reader = inputStream.bufferedReader(Charsets.UTF_8)
Config.parse(it) return Config.parse(reader)
}
} }
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 package com.zaneschepke.wireguardautotunnel.data.repository
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
interface AppDataRepository { interface AppDataRepository {
suspend fun getPrimaryOrFirstTunnel(): TunnelConfig? suspend fun getPrimaryOrFirstTunnel(): TunnelConfig?
@@ -1,6 +1,6 @@
package com.zaneschepke.wireguardautotunnel.data.repository package com.zaneschepke.wireguardautotunnel.data.repository
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
import javax.inject.Inject import javax.inject.Inject
class AppDataRoomRepository @Inject constructor( class AppDataRoomRepository @Inject constructor(
@@ -1,15 +1,12 @@
package com.zaneschepke.wireguardautotunnel.data.repository package com.zaneschepke.wireguardautotunnel.data.repository
import com.zaneschepke.wireguardautotunnel.data.domain.GeneralState import com.zaneschepke.wireguardautotunnel.data.model.GeneralState
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
interface AppStateRepository { interface AppStateRepository {
suspend fun isLocationDisclosureShown(): Boolean suspend fun isLocationDisclosureShown(): Boolean
suspend fun setLocationDisclosureShown(shown: Boolean) suspend fun setLocationDisclosureShown(shown: Boolean)
suspend fun isPinLockEnabled(): Boolean
suspend fun setPinLockEnabled(enabled: Boolean)
suspend fun isBatteryOptimizationDisableShown(): Boolean suspend fun isBatteryOptimizationDisableShown(): Boolean
suspend fun setBatteryOptimizationDisableShown(shown: Boolean) suspend fun setBatteryOptimizationDisableShown(shown: Boolean)
@@ -1,7 +1,7 @@
package com.zaneschepke.wireguardautotunnel.data.repository package com.zaneschepke.wireguardautotunnel.data.repository
import com.zaneschepke.wireguardautotunnel.data.datastore.DataStoreManager import com.zaneschepke.wireguardautotunnel.data.datastore.DataStoreManager
import com.zaneschepke.wireguardautotunnel.data.domain.GeneralState import com.zaneschepke.wireguardautotunnel.data.model.GeneralState
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import timber.log.Timber import timber.log.Timber
@@ -17,15 +17,6 @@ class DataStoreAppStateRepository(private val dataStoreManager: DataStoreManager
dataStoreManager.saveToDataStore(DataStoreManager.LOCATION_DISCLOSURE_SHOWN, shown) dataStoreManager.saveToDataStore(DataStoreManager.LOCATION_DISCLOSURE_SHOWN, shown)
} }
override suspend fun isPinLockEnabled(): Boolean {
return dataStoreManager.getFromStore(DataStoreManager.IS_PIN_LOCK_ENABLED)
?: GeneralState.PIN_LOCK_ENABLED_DEFAULT
}
override suspend fun setPinLockEnabled(enabled: Boolean) {
dataStoreManager.saveToDataStore(DataStoreManager.IS_PIN_LOCK_ENABLED, enabled)
}
override suspend fun isBatteryOptimizationDisableShown(): Boolean { override suspend fun isBatteryOptimizationDisableShown(): Boolean {
return dataStoreManager.getFromStore(DataStoreManager.BATTERY_OPTIMIZE_DISABLE_SHOWN) return dataStoreManager.getFromStore(DataStoreManager.BATTERY_OPTIMIZE_DISABLE_SHOWN)
?: GeneralState.BATTERY_OPTIMIZATION_DISABLE_SHOWN_DEFAULT ?: GeneralState.BATTERY_OPTIMIZATION_DISABLE_SHOWN_DEFAULT
@@ -74,13 +65,11 @@ class DataStoreAppStateRepository(private val dataStoreManager: DataStoreManager
prefs?.let { pref -> prefs?.let { pref ->
try { try {
GeneralState( GeneralState(
isLocationDisclosureShown = pref[DataStoreManager.LOCATION_DISCLOSURE_SHOWN] locationDisclosureShown = pref[DataStoreManager.LOCATION_DISCLOSURE_SHOWN]
?: GeneralState.LOCATION_DISCLOSURE_SHOWN_DEFAULT, ?: GeneralState.LOCATION_DISCLOSURE_SHOWN_DEFAULT,
isBatteryOptimizationDisableShown = pref[DataStoreManager.BATTERY_OPTIMIZE_DISABLE_SHOWN] batteryOptimizationDisableShown = pref[DataStoreManager.BATTERY_OPTIMIZE_DISABLE_SHOWN]
?: GeneralState.BATTERY_OPTIMIZATION_DISABLE_SHOWN_DEFAULT, ?: GeneralState.BATTERY_OPTIMIZATION_DISABLE_SHOWN_DEFAULT,
isTunnelRunningFromManualStart = pref[DataStoreManager.TUNNEL_RUNNING_FROM_MANUAL_START] tunnelRunningFromManualStart = pref[DataStoreManager.TUNNEL_RUNNING_FROM_MANUAL_START]
?: GeneralState.TUNNELING_RUNNING_FROM_MANUAL_START_DEFAULT,
isPinLockEnabled = pref[DataStoreManager.IS_PIN_LOCK_ENABLED]
?: GeneralState.TUNNELING_RUNNING_FROM_MANUAL_START_DEFAULT, ?: GeneralState.TUNNELING_RUNNING_FROM_MANUAL_START_DEFAULT,
) )
} catch (e: IllegalArgumentException) { } catch (e: IllegalArgumentException) {
@@ -1,7 +1,7 @@
package com.zaneschepke.wireguardautotunnel.data.repository package com.zaneschepke.wireguardautotunnel.data.repository
import com.zaneschepke.wireguardautotunnel.data.SettingsDao import com.zaneschepke.wireguardautotunnel.data.SettingsDao
import com.zaneschepke.wireguardautotunnel.data.domain.Settings import com.zaneschepke.wireguardautotunnel.data.model.Settings
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
class RoomSettingsRepository(private val settingsDoa: SettingsDao) : SettingsRepository { class RoomSettingsRepository(private val settingsDoa: SettingsDao) : SettingsRepository {
@@ -1,7 +1,7 @@
package com.zaneschepke.wireguardautotunnel.data.repository package com.zaneschepke.wireguardautotunnel.data.repository
import com.zaneschepke.wireguardautotunnel.data.TunnelConfigDao import com.zaneschepke.wireguardautotunnel.data.TunnelConfigDao
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
import com.zaneschepke.wireguardautotunnel.util.TunnelConfigs import com.zaneschepke.wireguardautotunnel.util.TunnelConfigs
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
@@ -54,10 +54,6 @@ class RoomTunnelConfigRepository(private val tunnelConfigDao: TunnelConfigDao) :
return tunnelConfigDao.count().toInt() return tunnelConfigDao.count().toInt()
} }
override suspend fun findByTunnelName(name: String): TunnelConfig? {
return tunnelConfigDao.getByName(name)
}
override suspend fun findByTunnelNetworksName(name: String): TunnelConfigs { override suspend fun findByTunnelNetworksName(name: String): TunnelConfigs {
return tunnelConfigDao.findByTunnelNetworkName(name) return tunnelConfigDao.findByTunnelNetworkName(name)
} }
@@ -1,6 +1,6 @@
package com.zaneschepke.wireguardautotunnel.data.repository package com.zaneschepke.wireguardautotunnel.data.repository
import com.zaneschepke.wireguardautotunnel.data.domain.Settings import com.zaneschepke.wireguardautotunnel.data.model.Settings
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
interface SettingsRepository { interface SettingsRepository {
@@ -1,6 +1,6 @@
package com.zaneschepke.wireguardautotunnel.data.repository package com.zaneschepke.wireguardautotunnel.data.repository
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
import com.zaneschepke.wireguardautotunnel.util.TunnelConfigs import com.zaneschepke.wireguardautotunnel.util.TunnelConfigs
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
@@ -22,8 +22,6 @@ interface TunnelConfigRepository {
suspend fun count(): Int suspend fun count(): Int
suspend fun findByTunnelName(name: String): TunnelConfig?
suspend fun findByTunnelNetworksName(name: String): TunnelConfigs suspend fun findByTunnelNetworksName(name: String): TunnelConfigs
suspend fun findByMobileDataTunnel(): TunnelConfigs suspend fun findByMobileDataTunnel(): TunnelConfigs
@@ -1,30 +0,0 @@
package com.zaneschepke.wireguardautotunnel.module
import android.content.Context
import com.zaneschepke.logcatter.LocalLogCollector
import com.zaneschepke.logcatter.LogcatHelper
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
class AppModule {
@Singleton
@ApplicationScope
@Provides
fun providesApplicationScope(@DefaultDispatcher defaultDispatcher: CoroutineDispatcher): CoroutineScope =
CoroutineScope(SupervisorJob() + defaultDispatcher)
@Singleton
@Provides
fun provideLogCollect(@ApplicationContext context: Context): LocalLogCollector {
return LogcatHelper.init(context = context)
}
}
@@ -1,27 +0,0 @@
package com.zaneschepke.wireguardautotunnel.module
import javax.inject.Qualifier
@Retention(AnnotationRetention.RUNTIME)
@Qualifier
annotation class DefaultDispatcher
@Retention(AnnotationRetention.RUNTIME)
@Qualifier
annotation class IoDispatcher
@Retention(AnnotationRetention.RUNTIME)
@Qualifier
annotation class MainDispatcher
@Retention(AnnotationRetention.BINARY)
@Qualifier
annotation class MainImmediateDispatcher
@Retention(AnnotationRetention.RUNTIME)
@Qualifier
annotation class ApplicationScope
@Retention(AnnotationRetention.RUNTIME)
@Qualifier
annotation class ServiceScope
@@ -1,28 +0,0 @@
package com.zaneschepke.wireguardautotunnel.module
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers
@Module
@InstallIn(SingletonComponent::class)
object CoroutinesDispatchersModule {
@DefaultDispatcher
@Provides
fun providesDefaultDispatcher(): CoroutineDispatcher = Dispatchers.Default
@IoDispatcher
@Provides
fun providesIoDispatcher(): CoroutineDispatcher = Dispatchers.IO
@MainDispatcher
@Provides
fun providesMainDispatcher(): CoroutineDispatcher = Dispatchers.Main
@MainImmediateDispatcher
@Provides
fun providesMainImmediateDispatcher(): CoroutineDispatcher = Dispatchers.Main.immediate
}
@@ -0,0 +1,30 @@
package com.zaneschepke.wireguardautotunnel.module
import android.content.Context
import androidx.room.Room
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.data.AppDatabase
import com.zaneschepke.wireguardautotunnel.data.DatabaseCallback
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
class DatabaseModule {
@Provides
@Singleton
fun provideDatabase(@ApplicationContext context: Context): AppDatabase {
return Room.databaseBuilder(
context,
AppDatabase::class.java,
context.getString(R.string.db_name),
)
.fallbackToDestructiveMigration()
.addCallback(DatabaseCallback())
.build()
}
}
@@ -0,0 +1,7 @@
package com.zaneschepke.wireguardautotunnel.module
import javax.inject.Qualifier
@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class Kernel
@@ -1,10 +1,7 @@
package com.zaneschepke.wireguardautotunnel.module package com.zaneschepke.wireguardautotunnel.module
import android.content.Context import android.content.Context
import androidx.room.Room
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.data.AppDatabase import com.zaneschepke.wireguardautotunnel.data.AppDatabase
import com.zaneschepke.wireguardautotunnel.data.DatabaseCallback
import com.zaneschepke.wireguardautotunnel.data.SettingsDao import com.zaneschepke.wireguardautotunnel.data.SettingsDao
import com.zaneschepke.wireguardautotunnel.data.TunnelConfigDao import com.zaneschepke.wireguardautotunnel.data.TunnelConfigDao
import com.zaneschepke.wireguardautotunnel.data.datastore.DataStoreManager import com.zaneschepke.wireguardautotunnel.data.datastore.DataStoreManager
@@ -21,25 +18,11 @@ import dagger.Provides
import dagger.hilt.InstallIn import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent import dagger.hilt.components.SingletonComponent
import kotlinx.coroutines.CoroutineDispatcher
import javax.inject.Singleton import javax.inject.Singleton
@Module @Module
@InstallIn(SingletonComponent::class) @InstallIn(SingletonComponent::class)
class RepositoryModule { class RepositoryModule {
@Provides
@Singleton
fun provideDatabase(@ApplicationContext context: Context): AppDatabase {
return Room.databaseBuilder(
context,
AppDatabase::class.java,
context.getString(R.string.db_name),
)
.fallbackToDestructiveMigration()
.addCallback(DatabaseCallback())
.build()
}
@Singleton @Singleton
@Provides @Provides
fun provideSettingsDoa(appDatabase: AppDatabase): SettingsDao { fun provideSettingsDoa(appDatabase: AppDatabase): SettingsDao {
@@ -66,11 +49,8 @@ class RepositoryModule {
@Singleton @Singleton
@Provides @Provides
fun providePreferencesDataStore( fun providePreferencesDataStore(@ApplicationContext context: Context): DataStoreManager {
@ApplicationContext context: Context, return DataStoreManager(context)
@IoDispatcher ioDispatcher: CoroutineDispatcher
): DataStoreManager {
return DataStoreManager(context, ioDispatcher)
} }
@Provides @Provides
@@ -1,11 +1,6 @@
package com.zaneschepke.wireguardautotunnel.module package com.zaneschepke.wireguardautotunnel.module
import android.content.Context 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.data.repository.AppDataRepository
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnService import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnService
@@ -15,9 +10,11 @@ import dagger.Provides
import dagger.hilt.InstallIn import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent import dagger.hilt.components.SingletonComponent
import kotlinx.coroutines.CoroutineDispatcher import org.amnezia.awg.backend.AwgQuickBackend
import kotlinx.coroutines.CoroutineScope import org.amnezia.awg.backend.Backend
import javax.inject.Provider import org.amnezia.awg.backend.GoBackend
import org.amnezia.awg.util.RootShell
import org.amnezia.awg.util.ToolsInstaller
import javax.inject.Singleton import javax.inject.Singleton
@Module @Module
@@ -40,41 +37,22 @@ class TunnelModule {
@Singleton @Singleton
@Kernel @Kernel
fun provideKernelBackend(@ApplicationContext context: Context, rootShell: RootShell): Backend { fun provideKernelBackend(@ApplicationContext context: Context, rootShell: RootShell): Backend {
return WgQuickBackend(context, rootShell, ToolsInstaller(context, rootShell)) return AwgQuickBackend(context, rootShell, ToolsInstaller(context, rootShell))
}
@Provides
@Singleton
fun provideAmneziaBackend(@ApplicationContext context: Context): org.amnezia.awg.backend.Backend {
return org.amnezia.awg.backend.GoBackend(context)
} }
@Provides @Provides
@Singleton @Singleton
fun provideVpnService( fun provideVpnService(
amneziaBackend: Provider<org.amnezia.awg.backend.Backend>, @Userspace userspaceBackend: Backend,
@Userspace userspaceBackend: Provider<Backend>, @Kernel kernelBackend: Backend,
@Kernel kernelBackend: Provider<Backend>, appDataRepository: AppDataRepository
appDataRepository: AppDataRepository,
@ApplicationScope applicationScope: CoroutineScope,
@IoDispatcher ioDispatcher: CoroutineDispatcher
): VpnService { ): VpnService {
return WireGuardTunnel( return WireGuardTunnel(userspaceBackend, kernelBackend, appDataRepository)
amneziaBackend,
userspaceBackend,
kernelBackend,
appDataRepository,
applicationScope,
ioDispatcher,
)
} }
@Provides @Provides
@Singleton @Singleton
fun provideServiceManager( fun provideServiceManager(appDataRepository: AppDataRepository): ServiceManager {
appDataRepository: AppDataRepository, return ServiceManager(appDataRepository)
@IoDispatcher ioDispatcher: CoroutineDispatcher
): ServiceManager {
return ServiceManager(appDataRepository, ioDispatcher)
} }
} }
@@ -2,10 +2,6 @@ package com.zaneschepke.wireguardautotunnel.module
import javax.inject.Qualifier import javax.inject.Qualifier
@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class Kernel
@Qualifier @Qualifier
@Retention(AnnotationRetention.BINARY) @Retention(AnnotationRetention.BINARY)
annotation class Userspace annotation class Userspace
@@ -1,25 +0,0 @@
package com.zaneschepke.wireguardautotunnel.module
import android.content.Context
import com.zaneschepke.wireguardautotunnel.util.FileUtils
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.components.ViewModelComponent
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.android.scopes.ViewModelScoped
import kotlinx.coroutines.CoroutineDispatcher
@Module
@InstallIn(ViewModelComponent::class)
class ViewModelModule {
@ViewModelScoped
@Provides
fun provideFileUtils(
@ApplicationContext context: Context,
@IoDispatcher ioDispatcher: CoroutineDispatcher
): FileUtils {
return FileUtils(context, ioDispatcher)
}
}
@@ -4,11 +4,9 @@ import android.content.BroadcastReceiver
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
import com.zaneschepke.wireguardautotunnel.module.ApplicationScope
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
import com.zaneschepke.wireguardautotunnel.util.goAsync
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
@@ -21,36 +19,27 @@ class BootReceiver : BroadcastReceiver() {
@Inject @Inject
lateinit var serviceManager: ServiceManager lateinit var serviceManager: ServiceManager
@Inject override fun onReceive(context: Context?, intent: Intent?) = goAsync {
@ApplicationScope if (Intent.ACTION_BOOT_COMPLETED != intent?.action) return@goAsync
lateinit var applicationScope: CoroutineScope
override fun onReceive(context: Context?, intent: Intent?) {
if (Intent.ACTION_BOOT_COMPLETED != intent?.action) return
context?.run { context?.run {
applicationScope.launch { val settings = appDataRepository.settings.getSettings()
val settings = appDataRepository.settings.getSettings() if (settings.isAutoTunnelEnabled) {
if(settings.isRestoreOnBootEnabled) { Timber.i("Starting watcher service from boot")
if (settings.isAutoTunnelEnabled) { serviceManager.startWatcherServiceForeground(context)
Timber.i("Starting watcher service from boot") }
serviceManager.startWatcherServiceForeground(context) if (appDataRepository.appState.isTunnelRunningFromManualStart()) {
} appDataRepository.appState.getActiveTunnelId()?.let {
if (appDataRepository.appState.isTunnelRunningFromManualStart()) { Timber.i("Starting tunnel that was active before reboot")
appDataRepository.appState.getActiveTunnelId()?.let { serviceManager.startVpnServiceForeground(
Timber.i("Starting tunnel that was active before reboot") context,
serviceManager.startVpnServiceForeground( appDataRepository.tunnels.getById(it)?.id,
context, )
appDataRepository.tunnels.getById(it)?.id,
)
return@launch
}
}
if (settings.isAlwaysOnVpnEnabled) {
Timber.i("Starting vpn service from boot AOVPN")
serviceManager.startVpnServiceForeground(context)
}
} }
} }
if (settings.isAlwaysOnVpnEnabled) {
Timber.i("Starting vpn service from boot AOVPN")
serviceManager.startVpnServiceForeground(context)
}
} }
} }
} }
@@ -4,14 +4,12 @@ import android.content.BroadcastReceiver
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import com.zaneschepke.wireguardautotunnel.data.repository.SettingsRepository import com.zaneschepke.wireguardautotunnel.data.repository.SettingsRepository
import com.zaneschepke.wireguardautotunnel.module.ApplicationScope
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
import com.zaneschepke.wireguardautotunnel.util.Constants import com.zaneschepke.wireguardautotunnel.util.Constants
import com.zaneschepke.wireguardautotunnel.util.goAsync
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.cancel import kotlinx.coroutines.cancel
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
@@ -23,22 +21,16 @@ class NotificationActionReceiver : BroadcastReceiver() {
@Inject @Inject
lateinit var serviceManager: ServiceManager lateinit var serviceManager: ServiceManager
@Inject override fun onReceive(context: Context, intent: Intent?) = goAsync {
@ApplicationScope try {
lateinit var applicationScope: CoroutineScope //TODO fix for manual start changes when enabled
serviceManager.stopVpnServiceForeground(context)
override fun onReceive(context: Context, intent: Intent?) { delay(Constants.TOGGLE_TUNNEL_DELAY)
applicationScope.launch { serviceManager.startVpnServiceForeground(context)
try { } catch (e: Exception) {
//TODO fix for manual start changes when enabled Timber.e(e)
serviceManager.stopVpnServiceForeground(context) } finally {
delay(Constants.TOGGLE_TUNNEL_DELAY) cancel()
serviceManager.startVpnServiceForeground(context)
} catch (e: Exception) {
Timber.e(e)
} finally {
cancel()
}
} }
} }
} }
@@ -24,7 +24,6 @@ open class ForegroundService : LifecycleService() {
when (action) { when (action) {
Action.START.name, Action.START.name,
Action.START_FOREGROUND.name -> startService(intent.extras) Action.START_FOREGROUND.name -> startService(intent.extras)
Action.STOP.name, Action.STOP_FOREGROUND.name -> stopService() Action.STOP.name, Action.STOP_FOREGROUND.name -> stopService()
Constants.ALWAYS_ON_VPN_ACTION -> { Constants.ALWAYS_ON_VPN_ACTION -> {
Timber.i("Always-on VPN starting service") Timber.i("Always-on VPN starting service")
@@ -4,16 +4,10 @@ import android.app.Service
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
import com.zaneschepke.wireguardautotunnel.module.IoDispatcher
import com.zaneschepke.wireguardautotunnel.util.Constants import com.zaneschepke.wireguardautotunnel.util.Constants
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.withContext
import timber.log.Timber import timber.log.Timber
class ServiceManager( class ServiceManager(private val appDataRepository: AppDataRepository) {
private val appDataRepository: AppDataRepository,
@IoDispatcher private val ioDispatcher: CoroutineDispatcher
) {
private fun <T : Service> actionOnService( private fun <T : Service> actionOnService(
action: Action, action: Action,
@@ -29,10 +23,7 @@ class ServiceManager(
intent.component?.javaClass intent.component?.javaClass
try { try {
when (action) { when (action) {
Action.START_FOREGROUND, Action.STOP_FOREGROUND -> context.startForegroundService( Action.START_FOREGROUND, Action.STOP_FOREGROUND -> context.startForegroundService(intent)
intent,
)
Action.START, Action.STOP -> context.startService(intent) Action.START, Action.STOP -> context.startService(intent)
} }
} catch (e: Exception) { } catch (e: Exception) {
@@ -55,27 +46,23 @@ class ServiceManager(
} }
suspend fun stopVpnServiceForeground(context: Context, isManualStop: Boolean = false) { suspend fun stopVpnServiceForeground(context: Context, isManualStop: Boolean = false) {
withContext(ioDispatcher) { if (isManualStop) onManualStop()
if (isManualStop) onManualStop() Timber.i("Stopping vpn service")
Timber.i("Stopping vpn service") actionOnService(
actionOnService( Action.STOP_FOREGROUND,
Action.STOP_FOREGROUND, context,
context, WireGuardTunnelService::class.java,
WireGuardTunnelService::class.java, )
)
}
} }
suspend fun stopVpnService(context: Context, isManualStop: Boolean = false) { suspend fun stopVpnService(context: Context, isManualStop: Boolean = false) {
withContext(ioDispatcher) { if (isManualStop) onManualStop()
if (isManualStop) onManualStop() Timber.i("Stopping vpn service")
Timber.i("Stopping vpn service") actionOnService(
actionOnService( Action.STOP,
Action.STOP, context,
context, WireGuardTunnelService::class.java,
WireGuardTunnelService::class.java, )
)
}
} }
private suspend fun onManualStop() { private suspend fun onManualStop() {
@@ -93,15 +80,13 @@ class ServiceManager(
tunnelId: Int? = null, tunnelId: Int? = null,
isManualStart: Boolean = false isManualStart: Boolean = false
) { ) {
withContext(ioDispatcher) { if (isManualStart) onManualStart(tunnelId)
if (isManualStart) onManualStart(tunnelId) actionOnService(
actionOnService( Action.START_FOREGROUND,
Action.START_FOREGROUND, context,
context, WireGuardTunnelService::class.java,
WireGuardTunnelService::class.java, tunnelId?.let { mapOf(Constants.TUNNEL_EXTRA_KEY to it) },
tunnelId?.let { mapOf(Constants.TUNNEL_EXTRA_KEY to it) }, )
)
}
} }
fun startWatcherServiceForeground( fun startWatcherServiceForeground(
@@ -1,56 +1,84 @@
package com.zaneschepke.wireguardautotunnel.service.foreground package com.zaneschepke.wireguardautotunnel.service.foreground
import com.zaneschepke.wireguardautotunnel.data.domain.Settings import com.zaneschepke.wireguardautotunnel.data.model.Settings
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
import org.amnezia.awg.backend.Tunnel
data class WatcherState( data class WatcherState(
val isWifiConnected: Boolean = false, val isWifiConnected: Boolean = false,
val config: TunnelConfig? = null,
val vpnStatus: Tunnel.State = Tunnel.State.DOWN,
val isEthernetConnected: Boolean = false, val isEthernetConnected: Boolean = false,
val isMobileDataConnected: Boolean = false, val isMobileDataConnected: Boolean = false,
val currentNetworkSSID: String = "", val currentNetworkSSID: String = "",
val settings: Settings = Settings() val settings: Settings = Settings()
) { ) {
private fun isVpnConnected() = vpnStatus == Tunnel.State.UP
fun isEthernetConditionMet(): Boolean { fun isEthernetConditionMet(): Boolean {
return (isEthernetConnected && return (isEthernetConnected &&
settings.isTunnelOnEthernetEnabled) settings.isTunnelOnEthernetEnabled &&
!isVpnConnected())
} }
fun isMobileDataConditionMet(): Boolean { fun isMobileDataConditionMet(): Boolean {
return (!isEthernetConnected && return (!isEthernetConnected &&
settings.isTunnelOnMobileDataEnabled && settings.isTunnelOnMobileDataEnabled &&
!isWifiConnected && !isWifiConnected &&
isMobileDataConnected) isMobileDataConnected &&
!isVpnConnected())
}
fun isTunnelNotMobileDataPreferredConditionMet(): Boolean {
return (!isEthernetConnected &&
settings.isTunnelOnMobileDataEnabled &&
!isWifiConnected &&
isMobileDataConnected &&
config?.isMobileDataTunnel == false && isVpnConnected())
} }
fun isTunnelOffOnMobileDataConditionMet(): Boolean { fun isTunnelOffOnMobileDataConditionMet(): Boolean {
return (!isEthernetConnected && return (!isEthernetConnected &&
!settings.isTunnelOnMobileDataEnabled && !settings.isTunnelOnMobileDataEnabled &&
isMobileDataConnected && isMobileDataConnected &&
!isWifiConnected) !isWifiConnected &&
isVpnConnected())
} }
fun isUntrustedWifiConditionMet(): Boolean { fun isUntrustedWifiConditionMet(): Boolean {
return (!isEthernetConnected && return (!isEthernetConnected &&
isWifiConnected && isWifiConnected &&
!settings.trustedNetworkSSIDs.contains(currentNetworkSSID) && !settings.trustedNetworkSSIDs.contains(currentNetworkSSID) &&
settings.isTunnelOnWifiEnabled) settings.isTunnelOnWifiEnabled
&& !isVpnConnected())
}
fun isTunnelNotWifiNamePreferredMet(ssid: String): Boolean {
return (!isEthernetConnected &&
isWifiConnected &&
!settings.trustedNetworkSSIDs.contains(currentNetworkSSID) &&
settings.isTunnelOnWifiEnabled && config?.tunnelNetworks?.contains(ssid) == false && isVpnConnected())
} }
fun isTrustedWifiConditionMet(): Boolean { fun isTrustedWifiConditionMet(): Boolean {
return (!isEthernetConnected && return (!isEthernetConnected &&
(isWifiConnected && (isWifiConnected &&
settings.trustedNetworkSSIDs.contains(currentNetworkSSID))) settings.trustedNetworkSSIDs.contains(currentNetworkSSID)) &&
(isVpnConnected()))
} }
fun isTunnelOffOnWifiConditionMet(): Boolean { fun isTunnelOffOnWifiConditionMet(): Boolean {
return (!isEthernetConnected && return (!isEthernetConnected &&
(isWifiConnected && (isWifiConnected &&
!settings.isTunnelOnWifiEnabled)) !settings.isTunnelOnWifiEnabled &&
(isVpnConnected())))
} }
fun isTunnelOffOnNoConnectivityMet(): Boolean { fun isTunnelOffOnNoConnectivityMet(): Boolean {
return (!isEthernetConnected && return (!isEthernetConnected &&
!isWifiConnected && !isWifiConnected &&
!isMobileDataConnected) !isMobileDataConnected &&
(isVpnConnected()))
} }
} }
@@ -6,29 +6,25 @@ import android.os.PowerManager
import androidx.core.app.ServiceCompat import androidx.core.app.ServiceCompat
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import com.zaneschepke.wireguardautotunnel.R import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
import com.zaneschepke.wireguardautotunnel.module.IoDispatcher
import com.zaneschepke.wireguardautotunnel.module.MainImmediateDispatcher
import com.zaneschepke.wireguardautotunnel.service.network.EthernetService import com.zaneschepke.wireguardautotunnel.service.network.EthernetService
import com.zaneschepke.wireguardautotunnel.service.network.MobileDataService import com.zaneschepke.wireguardautotunnel.service.network.MobileDataService
import com.zaneschepke.wireguardautotunnel.service.network.NetworkService import com.zaneschepke.wireguardautotunnel.service.network.NetworkService
import com.zaneschepke.wireguardautotunnel.service.network.NetworkStatus import com.zaneschepke.wireguardautotunnel.service.network.NetworkStatus
import com.zaneschepke.wireguardautotunnel.service.network.WifiService import com.zaneschepke.wireguardautotunnel.service.network.WifiService
import com.zaneschepke.wireguardautotunnel.service.notification.NotificationService import com.zaneschepke.wireguardautotunnel.service.notification.NotificationService
import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelState
import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnService import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnService
import com.zaneschepke.wireguardautotunnel.util.Constants import com.zaneschepke.wireguardautotunnel.util.Constants
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import org.amnezia.awg.backend.Tunnel
import timber.log.Timber import timber.log.Timber
import java.net.InetAddress import java.net.InetAddress
import javax.inject.Inject import javax.inject.Inject
@@ -59,14 +55,6 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
@Inject @Inject
lateinit var serviceManager: ServiceManager lateinit var serviceManager: ServiceManager
@Inject
@IoDispatcher
lateinit var ioDispatcher: CoroutineDispatcher
@Inject
@MainImmediateDispatcher
lateinit var mainImmediateDispatcher: CoroutineDispatcher
private val networkEventsFlow = MutableStateFlow(WatcherState()) private val networkEventsFlow = MutableStateFlow(WatcherState())
private var watcherJob: Job? = null private var watcherJob: Job? = null
@@ -76,7 +64,7 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
lifecycleScope.launch(mainImmediateDispatcher) { lifecycleScope.launch(Dispatchers.Main) {
try { try {
if (appDataRepository.settings.getSettings().isAutoTunnelPaused) { if (appDataRepository.settings.getSettings().isAutoTunnelPaused) {
launchWatcherPausedNotification() launchWatcherPausedNotification()
@@ -149,14 +137,14 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
private fun cancelWatcherJob() { private fun cancelWatcherJob() {
try { try {
watcherJob?.cancel() watcherJob?.cancel()
} catch (e: CancellationException) { } catch (e : CancellationException) {
Timber.i("Watcher job cancelled") Timber.i("Watcher job cancelled")
} }
} }
private fun startWatcherJob() { private fun startWatcherJob() {
watcherJob = watcherJob =
lifecycleScope.launch { lifecycleScope.launch(Dispatchers.IO) {
val setting = appDataRepository.settings.getSettings() val setting = appDataRepository.settings.getSettings()
launch { launch {
Timber.i("Starting wifi watcher") Timber.i("Starting wifi watcher")
@@ -174,6 +162,10 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
watchForEthernetConnectivityChanges() watchForEthernetConnectivityChanges()
} }
} }
launch {
Timber.i("Starting vpn state watcher")
watchForVpnConnectivityChanges()
}
launch { launch {
Timber.i("Starting settings watcher") Timber.i("Starting settings watcher")
watchForSettingsChanges() watchForSettingsChanges()
@@ -193,170 +185,160 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
} }
private suspend fun watchForMobileDataConnectivityChanges() { private suspend fun watchForMobileDataConnectivityChanges() {
withContext(ioDispatcher) { mobileDataService.networkStatus.collect {
mobileDataService.networkStatus.collect { status -> when (it) {
when (status) { is NetworkStatus.Available -> {
is NetworkStatus.Available -> { Timber.i("Gained Mobile data connection")
Timber.i("Gained Mobile data connection") networkEventsFlow.value =
networkEventsFlow.update { networkEventsFlow.value.copy(
it.copy( isMobileDataConnected = true,
isMobileDataConnected = true, )
) }
}
}
is NetworkStatus.CapabilitiesChanged -> { is NetworkStatus.CapabilitiesChanged -> {
networkEventsFlow.update { networkEventsFlow.value =
it.copy( networkEventsFlow.value.copy(
isMobileDataConnected = true, isMobileDataConnected = true,
) )
} Timber.i("Mobile data capabilities changed")
Timber.i("Mobile data capabilities changed") }
}
is NetworkStatus.Unavailable -> { is NetworkStatus.Unavailable -> {
networkEventsFlow.update { networkEventsFlow.value =
it.copy( networkEventsFlow.value.copy(
isMobileDataConnected = false, isMobileDataConnected = false,
) )
} Timber.i("Lost mobile data connection")
Timber.i("Lost mobile data connection")
}
} }
} }
} }
} }
private suspend fun watchForPingFailure() { private suspend fun watchForPingFailure() {
val context = this try {
withContext(ioDispatcher) { do {
try { if (vpnService.vpnState.value.status == Tunnel.State.UP) {
do { val tunnelConfig = vpnService.vpnState.value.tunnelConfig
if (vpnService.vpnState.value.status == TunnelState.UP) { tunnelConfig?.let {
val tunnelConfig = vpnService.vpnState.value.tunnelConfig val config = TunnelConfig.configFromQuick(it.wgQuick)
tunnelConfig?.let { val results = config.peers.map { peer ->
val config = TunnelConfig.configFromWgQuick(it.wgQuick) val host = if (peer.endpoint.isPresent &&
val results = config.peers.map { peer -> peer.endpoint.get().resolved.isPresent)
val host = if (peer.endpoint.isPresent && peer.endpoint.get().resolved.get().host
peer.endpoint.get().resolved.isPresent) else Constants.BACKUP_PING_HOST
peer.endpoint.get().resolved.get().host Timber.i("Checking reachability of: $host")
else Constants.DEFAULT_PING_IP val reachable = InetAddress.getByName(host)
Timber.i("Checking reachability of: $host") .isReachable(Constants.PING_TIMEOUT.toInt())
val reachable = InetAddress.getByName(host) Timber.i("Result: reachable - $reachable")
.isReachable(Constants.PING_TIMEOUT.toInt()) reachable
Timber.i("Result: reachable - $reachable") }
reachable if (results.contains(false)) {
} Timber.i("Restarting VPN for ping failure")
if (results.contains(false)) { serviceManager.stopVpnServiceForeground(this)
Timber.i("Restarting VPN for ping failure") delay(Constants.VPN_RESTART_DELAY)
serviceManager.stopVpnServiceForeground(context) serviceManager.startVpnServiceForeground(this)
delay(Constants.VPN_RESTART_DELAY) delay(Constants.PING_COOLDOWN)
serviceManager.startVpnServiceForeground(context, it.id)
delay(Constants.PING_COOLDOWN)
}
} }
} }
delay(Constants.PING_INTERVAL) }
} while (true) delay(Constants.PING_INTERVAL)
} catch (e: Exception) { } while (true)
Timber.e(e) } catch (e: Exception) {
} Timber.e(e)
} }
} }
private suspend fun watchForSettingsChanges() { private suspend fun watchForSettingsChanges() {
appDataRepository.settings.getSettingsFlow().collect { settings -> appDataRepository.settings.getSettingsFlow().collect {
if (networkEventsFlow.value.settings.isAutoTunnelPaused != settings.isAutoTunnelPaused) { if (networkEventsFlow.value.settings.isAutoTunnelPaused != it.isAutoTunnelPaused) {
when (settings.isAutoTunnelPaused) { when (it.isAutoTunnelPaused) {
true -> launchWatcherPausedNotification() true -> launchWatcherPausedNotification()
false -> launchWatcherNotification() false -> launchWatcherNotification()
} }
} }
networkEventsFlow.update { networkEventsFlow.value =
it.copy( networkEventsFlow.value.copy(
settings = settings, settings = it,
)
}
}
private suspend fun watchForVpnConnectivityChanges() {
vpnService.vpnState.collect {
networkEventsFlow.value =
networkEventsFlow.value.copy(
vpnStatus = it.status,
config = it.tunnelConfig,
) )
}
} }
} }
private suspend fun watchForEthernetConnectivityChanges() { private suspend fun watchForEthernetConnectivityChanges() {
withContext(ioDispatcher) { ethernetService.networkStatus.collect {
ethernetService.networkStatus.collect { status -> when (it) {
when (status) { is NetworkStatus.Available -> {
is NetworkStatus.Available -> { Timber.i("Gained Ethernet connection")
Timber.i("Gained Ethernet connection") networkEventsFlow.value =
networkEventsFlow.update { networkEventsFlow.value.copy(
it.copy( isEthernetConnected = true,
isEthernetConnected = true, )
) }
}
}
is NetworkStatus.CapabilitiesChanged -> { is NetworkStatus.CapabilitiesChanged -> {
Timber.i("Ethernet capabilities changed") Timber.i("Ethernet capabilities changed")
networkEventsFlow.update { networkEventsFlow.value =
it.copy( networkEventsFlow.value.copy(
isEthernetConnected = true, isEthernetConnected = true,
) )
} }
}
is NetworkStatus.Unavailable -> { is NetworkStatus.Unavailable -> {
networkEventsFlow.update { networkEventsFlow.value =
it.copy( networkEventsFlow.value.copy(
isEthernetConnected = false, isEthernetConnected = false,
) )
} Timber.i("Lost Ethernet connection")
Timber.i("Lost Ethernet connection")
}
} }
} }
} }
} }
private suspend fun watchForWifiConnectivityChanges() { private suspend fun watchForWifiConnectivityChanges() {
withContext(ioDispatcher) { wifiService.networkStatus.collect {
wifiService.networkStatus.collect { status -> when (it) {
when (status) { is NetworkStatus.Available -> {
is NetworkStatus.Available -> { Timber.i("Gained Wi-Fi connection")
Timber.i("Gained Wi-Fi connection") networkEventsFlow.value =
networkEventsFlow.update { networkEventsFlow.value.copy(
it.copy( isWifiConnected = true,
isWifiConnected = true, )
) }
}
}
is NetworkStatus.CapabilitiesChanged -> { is NetworkStatus.CapabilitiesChanged -> {
Timber.i("Wifi capabilities changed") Timber.i("Wifi capabilities changed")
networkEventsFlow.update { networkEventsFlow.value =
it.copy( networkEventsFlow.value.copy(
isWifiConnected = true, isWifiConnected = true,
)
val ssid = wifiService.getNetworkName(it.networkCapabilities)
ssid?.let {
if(it.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,
) )
} } ?: Timber.w("Failed to read 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(name)
networkEventsFlow.update {
it.copy(
currentNetworkSSID = name,
)
}
} ?: Timber.w("Failed to read ssid")
}
is NetworkStatus.Unavailable -> { is NetworkStatus.Unavailable -> {
networkEventsFlow.update { networkEventsFlow.value =
it.copy( networkEventsFlow.value.copy(
isWifiConnected = false, isWifiConnected = false,
) )
} Timber.i("Lost Wi-Fi connection")
Timber.i("Lost Wi-Fi connection")
}
} }
} }
} }
@@ -370,84 +352,76 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
return appDataRepository.tunnels.findByTunnelNetworksName(ssid).firstOrNull() return appDataRepository.tunnels.findByTunnelNetworksName(ssid).firstOrNull()
} }
private fun isTunnelDown(): Boolean {
return vpnService.vpnState.value.status == TunnelState.DOWN
}
private suspend fun manageVpn() { private suspend fun manageVpn() {
val context = this networkEventsFlow.collectLatest { watcherState ->
withContext(ioDispatcher) { val autoTunnel = "Auto-tunnel watcher"
networkEventsFlow.collectLatest { watcherState -> if (!watcherState.settings.isAutoTunnelPaused) {
val autoTunnel = "Auto-tunnel watcher" //delay for rapid network state changes and then collect latest
if (!watcherState.settings.isAutoTunnelPaused) { delay(Constants.WATCHER_COLLECTION_DELAY)
//delay for rapid network state changes and then collect latest when {
delay(Constants.WATCHER_COLLECTION_DELAY) watcherState.isEthernetConditionMet() -> {
val tunnelConfig = vpnService.vpnState.value.tunnelConfig Timber.i("$autoTunnel - tunnel on on ethernet condition met")
when { serviceManager.startVpnServiceForeground(this)
watcherState.isEthernetConditionMet() -> { }
Timber.i("$autoTunnel - tunnel on on ethernet condition met")
if (isTunnelDown()) serviceManager.startVpnServiceForeground(context)
}
watcherState.isMobileDataConditionMet() -> { watcherState.isMobileDataConditionMet() -> {
Timber.i("$autoTunnel - tunnel on mobile data condition met") Timber.i("$autoTunnel - tunnel on on mobile data condition met")
val mobileDataTunnel = getMobileDataTunnel() serviceManager.startVpnServiceForeground(this, getMobileDataTunnel()?.id)
val tunnel = }
mobileDataTunnel ?: appDataRepository.getPrimaryOrFirstTunnel()
if (isTunnelDown() || tunnelConfig?.isMobileDataTunnel == false) { watcherState.isTunnelNotMobileDataPreferredConditionMet() -> {
serviceManager.startVpnServiceForeground( getMobileDataTunnel()?.let {
context, Timber.i("$autoTunnel - tunnel connected on mobile data is not preferred condition met, switching to preferred")
tunnel?.id, 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()
}
watcherState.isTunnelOffOnMobileDataConditionMet() -> { watcherState.isUntrustedWifiConditionMet() -> {
Timber.i("$autoTunnel - tunnel off on mobile data met, turning vpn off") Timber.i("$autoTunnel - tunnel on untrusted wifi condition met")
if (!isTunnelDown()) serviceManager.stopVpnServiceForeground(context) serviceManager.startVpnServiceForeground(
} this,
getSsidTunnel(watcherState.currentNetworkSSID)?.id,
)
}
watcherState.isUntrustedWifiConditionMet() -> { watcherState.isTrustedWifiConditionMet() -> {
if (tunnelConfig?.tunnelNetworks?.contains(watcherState.currentNetworkSSID) == false || Timber.i("$autoTunnel - tunnel off on trusted wifi condition met, turning vpn off")
tunnelConfig == null) { serviceManager.stopVpnServiceForeground(this)
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: ${it.name}")
if (isTunnelDown() || tunnelConfig?.id != it.id) serviceManager.startVpnServiceForeground(
context,
it.id,
)
} ?: suspend {
Timber.i("No tunnel associated with this SSID, using defaults")
val default = appDataRepository.getPrimaryOrFirstTunnel()
if (default?.name != vpnService.name) {
default?.let {
serviceManager.startVpnServiceForeground(context, it.id)
}
} watcherState.isTunnelOffOnWifiConditionMet() -> {
}.invoke() Timber.i("$autoTunnel - tunnel off on wifi condition met, turning vpn off")
} serviceManager.stopVpnServiceForeground(this)
} }
watcherState.isTrustedWifiConditionMet() -> { watcherState.isTunnelOffOnNoConnectivityMet() -> {
Timber.i("$autoTunnel - tunnel off on trusted wifi condition met, turning vpn off") Timber.i("$autoTunnel - tunnel off on no connectivity met, turning vpn off")
if (!isTunnelDown()) serviceManager.stopVpnServiceForeground(context) serviceManager.stopVpnServiceForeground(this)
} }
watcherState.isTunnelOffOnWifiConditionMet() -> { else -> {
Timber.i("$autoTunnel - tunnel off on wifi condition met, turning vpn off") Timber.i("$autoTunnel - no condition met")
if (!isTunnelDown()) serviceManager.stopVpnServiceForeground(context)
}
watcherState.isTunnelOffOnNoConnectivityMet() -> {
Timber.i("$autoTunnel - tunnel off on no connectivity met, turning vpn off")
if (!isTunnelDown()) serviceManager.stopVpnServiceForeground(context)
}
else -> {
Timber.i("$autoTunnel - no condition met")
}
} }
} }
} }
@@ -7,23 +7,20 @@ import androidx.core.app.ServiceCompat
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import com.zaneschepke.wireguardautotunnel.R import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
import com.zaneschepke.wireguardautotunnel.module.IoDispatcher
import com.zaneschepke.wireguardautotunnel.module.MainImmediateDispatcher
import com.zaneschepke.wireguardautotunnel.receiver.NotificationActionReceiver import com.zaneschepke.wireguardautotunnel.receiver.NotificationActionReceiver
import com.zaneschepke.wireguardautotunnel.service.notification.NotificationService import com.zaneschepke.wireguardautotunnel.service.notification.NotificationService
import com.zaneschepke.wireguardautotunnel.service.tunnel.HandshakeStatus import com.zaneschepke.wireguardautotunnel.service.tunnel.HandshakeStatus
import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelState
import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnService import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnService
import com.zaneschepke.wireguardautotunnel.util.Constants import com.zaneschepke.wireguardautotunnel.util.Constants
import com.zaneschepke.wireguardautotunnel.util.handshakeStatus import com.zaneschepke.wireguardautotunnel.util.handshakeStatus
import com.zaneschepke.wireguardautotunnel.util.mapPeerStats import com.zaneschepke.wireguardautotunnel.util.mapPeerStats
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import org.amnezia.awg.backend.Tunnel
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
@@ -40,21 +37,13 @@ class WireGuardTunnelService : ForegroundService() {
@Inject @Inject
lateinit var notificationService: NotificationService lateinit var notificationService: NotificationService
@Inject
@MainImmediateDispatcher
lateinit var mainImmediateDispatcher: CoroutineDispatcher
@Inject
@IoDispatcher
lateinit var ioDispatcher: CoroutineDispatcher
private var job: Job? = null private var job: Job? = null
private var didShowConnected = false private var didShowConnected = false
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
lifecycleScope.launch(mainImmediateDispatcher) { lifecycleScope.launch(Dispatchers.Main) {
//TODO fix this to not launch if AOVPN //TODO fix this to not launch if AOVPN
if (appDataRepository.tunnels.count() != 0) { if (appDataRepository.tunnels.count() != 0) {
launchVpnNotification() launchVpnNotification()
@@ -66,10 +55,10 @@ class WireGuardTunnelService : ForegroundService() {
super.startService(extras) super.startService(extras)
cancelJob() cancelJob()
job = job =
lifecycleScope.launch { lifecycleScope.launch(Dispatchers.IO) {
launch { launch {
val tunnelId = extras?.getInt(Constants.TUNNEL_EXTRA_KEY) val tunnelId = extras?.getInt(Constants.TUNNEL_EXTRA_KEY)
if (vpnService.getState() == TunnelState.UP) { if (vpnService.getState() == Tunnel.State.UP) {
vpnService.stopTunnel() vpnService.stopTunnel()
} }
vpnService.startTunnel( vpnService.startTunnel(
@@ -86,41 +75,39 @@ class WireGuardTunnelService : ForegroundService() {
//TODO improve tunnel notifications //TODO improve tunnel notifications
private suspend fun handshakeNotifications() { private suspend fun handshakeNotifications() {
withContext(ioDispatcher) { var tunnelName: String? = null
var tunnelName: String? = null vpnService.vpnState.collect { state ->
vpnService.vpnState.collect { state -> state.statistics
state.statistics ?.mapPeerStats()
?.mapPeerStats() ?.map { it.value?.handshakeStatus() }
?.map { it.value?.handshakeStatus() } .let { statuses ->
.let { statuses -> when {
when { statuses?.all { it == HandshakeStatus.HEALTHY } == true -> {
statuses?.all { it == HandshakeStatus.HEALTHY } == true -> { if (!didShowConnected) {
if (!didShowConnected) { delay(Constants.VPN_CONNECTED_NOTIFICATION_DELAY)
delay(Constants.VPN_CONNECTED_NOTIFICATION_DELAY) tunnelName = state.tunnelConfig?.name
tunnelName = state.tunnelConfig?.name launchVpnNotification(
launchVpnNotification( getString(R.string.tunnel_start_title),
getString(R.string.tunnel_start_title), "${getString(R.string.tunnel_start_text)} - $tunnelName",
"${getString(R.string.tunnel_start_text)} - $tunnelName", )
) didShowConnected = true
didShowConnected = true
}
} }
statuses?.any { it == HandshakeStatus.STALE } == true -> {}
statuses?.all { it == HandshakeStatus.NOT_STARTED } ==
true -> {
}
else -> {}
} }
statuses?.any { it == HandshakeStatus.STALE } == true -> {}
statuses?.all { it == HandshakeStatus.NOT_STARTED } ==
true -> {
}
else -> {}
} }
if (state.status == TunnelState.UP && state.tunnelConfig?.name != tunnelName) {
tunnelName = state.tunnelConfig?.name
launchVpnNotification(
getString(R.string.tunnel_start_title),
"${getString(R.string.tunnel_start_text)} - $tunnelName",
)
} }
if (state.status == Tunnel.State.UP && state.tunnelConfig?.name != tunnelName) {
tunnelName = state.tunnelConfig?.name
launchVpnNotification(
getString(R.string.tunnel_start_title),
"${getString(R.string.tunnel_start_text)} - $tunnelName",
)
} }
} }
} }
@@ -134,7 +121,7 @@ class WireGuardTunnelService : ForegroundService() {
override fun stopService() { override fun stopService() {
super.stopService() super.stopService()
lifecycleScope.launch { lifecycleScope.launch(Dispatchers.IO) {
vpnService.stopTunnel() vpnService.stopTunnel()
didShowConnected = false didShowConnected = false
} }
@@ -194,7 +181,7 @@ class WireGuardTunnelService : ForegroundService() {
private fun cancelJob() { private fun cancelJob() {
try { try {
job?.cancel() job?.cancel()
} catch (e: CancellationException) { } catch (e : CancellationException) {
Timber.i("Tunnel job cancelled") Timber.i("Tunnel job cancelled")
} }
} }
@@ -2,14 +2,14 @@ package com.zaneschepke.wireguardautotunnel.service.shortcut
import android.os.Bundle import android.os.Bundle
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.lifecycle.lifecycleScope
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
import com.zaneschepke.wireguardautotunnel.module.ApplicationScope
import com.zaneschepke.wireguardautotunnel.service.foreground.Action import com.zaneschepke.wireguardautotunnel.service.foreground.Action
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
import com.zaneschepke.wireguardautotunnel.service.foreground.WireGuardConnectivityWatcherService import com.zaneschepke.wireguardautotunnel.service.foreground.WireGuardConnectivityWatcherService
import com.zaneschepke.wireguardautotunnel.service.foreground.WireGuardTunnelService import com.zaneschepke.wireguardautotunnel.service.foreground.WireGuardTunnelService
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import javax.inject.Inject import javax.inject.Inject
@@ -22,13 +22,9 @@ class ShortcutsActivity : ComponentActivity() {
@Inject @Inject
lateinit var serviceManager: ServiceManager lateinit var serviceManager: ServiceManager
@Inject
@ApplicationScope
lateinit var applicationScope: CoroutineScope
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
applicationScope.launch { lifecycleScope.launch(Dispatchers.Main) {
val settings = appDataRepository.settings.getSettings() val settings = appDataRepository.settings.getSettings()
if (settings.isShortcutsEnabled) { if (settings.isShortcutsEnabled) {
when (intent.getStringExtra(CLASS_NAME_EXTRA_KEY)) { when (intent.getStringExtra(CLASS_NAME_EXTRA_KEY)) {
@@ -4,13 +4,14 @@ import android.os.Build
import android.service.quicksettings.Tile import android.service.quicksettings.Tile
import android.service.quicksettings.TileService import android.service.quicksettings.TileService
import com.zaneschepke.wireguardautotunnel.R import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
import com.zaneschepke.wireguardautotunnel.module.ApplicationScope
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.cancel import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
@@ -24,30 +25,29 @@ class AutoTunnelControlTile : TileService() {
@Inject @Inject
lateinit var serviceManager: ServiceManager lateinit var serviceManager: ServiceManager
@Inject private val scope = CoroutineScope(Dispatchers.IO)
@ApplicationScope
lateinit var applicationScope: CoroutineScope
private var manualStartConfig: TunnelConfig? = null private var manualStartConfig: TunnelConfig? = null
override fun onStartListening() { override fun onStartListening() {
super.onStartListening() super.onStartListening()
applicationScope.launch { scope.launch {
val settings = appDataRepository.settings.getSettings() appDataRepository.settings.getSettingsFlow().collectLatest {
when (settings.isAutoTunnelEnabled) { when (it.isAutoTunnelEnabled) {
true -> { true -> {
if (settings.isAutoTunnelPaused) { if (it.isAutoTunnelPaused) {
setInactive() setInactive()
setTileDescription(this@AutoTunnelControlTile.getString(R.string.paused)) setTileDescription(this@AutoTunnelControlTile.getString(R.string.paused))
} else { } else {
setActive() setActive()
setTileDescription(this@AutoTunnelControlTile.getString(R.string.active)) setTileDescription(this@AutoTunnelControlTile.getString(R.string.active))
}
} }
}
false -> { false -> {
setTileDescription(this@AutoTunnelControlTile.getString(R.string.disabled)) setTileDescription(this@AutoTunnelControlTile.getString(R.string.disabled))
setUnavailable() setUnavailable()
}
} }
} }
} }
@@ -58,10 +58,20 @@ class AutoTunnelControlTile : TileService() {
onStartListening() onStartListening()
} }
override fun onDestroy() {
super.onDestroy()
scope.cancel()
}
override fun onTileRemoved() {
super.onTileRemoved()
scope.cancel()
}
override fun onClick() { override fun onClick() {
super.onClick() super.onClick()
unlockAndRun { unlockAndRun {
applicationScope.launch { scope.launch {
try { try {
appDataRepository.toggleWatcherServicePause() appDataRepository.toggleWatcherServicePause()
} catch (e: Exception) { } catch (e: Exception) {
@@ -3,16 +3,16 @@ package com.zaneschepke.wireguardautotunnel.service.tile
import android.os.Build import android.os.Build
import android.service.quicksettings.Tile import android.service.quicksettings.Tile
import android.service.quicksettings.TileService import android.service.quicksettings.TileService
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
import com.zaneschepke.wireguardautotunnel.module.ApplicationScope
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelState
import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnService import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnService
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.cancel import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.amnezia.awg.backend.Tunnel
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
@@ -28,37 +28,47 @@ class TunnelControlTile : TileService() {
@Inject @Inject
lateinit var serviceManager: ServiceManager lateinit var serviceManager: ServiceManager
@Inject private val scope = CoroutineScope(Dispatchers.IO)
@ApplicationScope
lateinit var applicationScope: CoroutineScope
private var manualStartConfig: TunnelConfig? = null private var manualStartConfig: TunnelConfig? = null
override fun onStartListening() { override fun onStartListening() {
super.onStartListening() super.onStartListening()
Timber.d("On start listening called") Timber.d("On start listening called")
applicationScope.launch { scope.launch {
when (vpnService.getState()) { vpnService.vpnState.collect { it ->
TunnelState.UP -> { when (it.status) {
setActive() Tunnel.State.UP -> {
setTileDescription(vpnService.name) setActive()
} it.tunnelConfig?.name?.let { name -> setTileDescription(name) }
}
TunnelState.DOWN -> { Tunnel.State.DOWN -> {
setInactive() setInactive()
val config = appDataRepository.getStartTunnelConfig()?.also { config -> val config = appDataRepository.getStartTunnelConfig()?.also { config ->
manualStartConfig = config manualStartConfig = config
} ?: appDataRepository.getPrimaryOrFirstTunnel() } ?: appDataRepository.getPrimaryOrFirstTunnel()
config?.let { config?.let {
setTileDescription(it.name) setTileDescription(it.name)
} ?: setUnavailable() } ?: setUnavailable()
} }
else -> setInactive() else -> setInactive()
}
} }
} }
} }
override fun onDestroy() {
super.onDestroy()
scope.cancel()
}
override fun onTileRemoved() {
super.onTileRemoved()
scope.cancel()
}
override fun onTileAdded() { override fun onTileAdded() {
super.onTileAdded() super.onTileAdded()
onStartListening() onStartListening()
@@ -67,9 +77,9 @@ class TunnelControlTile : TileService() {
override fun onClick() { override fun onClick() {
super.onClick() super.onClick()
unlockAndRun { unlockAndRun {
applicationScope.launch { scope.launch {
try { try {
if (vpnService.getState() == TunnelState.UP) { if (vpnService.getState() == Tunnel.State.UP) {
serviceManager.stopVpnServiceForeground( serviceManager.stopVpnServiceForeground(
this@TunnelControlTile, this@TunnelControlTile,
isManualStop = true, isManualStop = true,
@@ -1,43 +0,0 @@
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 package com.zaneschepke.wireguardautotunnel.service.tunnel
import com.wireguard.android.backend.Tunnel import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import org.amnezia.awg.backend.Tunnel
interface VpnService : Tunnel, org.amnezia.awg.backend.Tunnel { interface VpnService : Tunnel {
suspend fun startTunnel(tunnelConfig: TunnelConfig? = null): TunnelState suspend fun startTunnel(tunnelConfig: TunnelConfig? = null): Tunnel.State
suspend fun stopTunnel() suspend fun stopTunnel()
val vpnState: StateFlow<VpnState> val vpnState: StateFlow<VpnState>
fun getState(): TunnelState fun getState(): Tunnel.State
} }
@@ -1,10 +1,11 @@
package com.zaneschepke.wireguardautotunnel.service.tunnel package com.zaneschepke.wireguardautotunnel.service.tunnel
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
import com.zaneschepke.wireguardautotunnel.service.tunnel.statistics.TunnelStatistics import org.amnezia.awg.backend.Statistics
import org.amnezia.awg.backend.Tunnel
data class VpnState( data class VpnState(
val status: TunnelState = TunnelState.DOWN, val status: Tunnel.State = Tunnel.State.DOWN,
val tunnelConfig: TunnelConfig? = null, val tunnelConfig: TunnelConfig? = null,
val statistics: TunnelStatistics? = null val statistics: Statistics? = null
) )
@@ -1,132 +1,83 @@
package com.zaneschepke.wireguardautotunnel.service.tunnel 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.WireGuardAutoTunnel
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
import com.zaneschepke.wireguardautotunnel.module.ApplicationScope
import com.zaneschepke.wireguardautotunnel.module.IoDispatcher
import com.zaneschepke.wireguardautotunnel.module.Kernel import com.zaneschepke.wireguardautotunnel.module.Kernel
import com.zaneschepke.wireguardautotunnel.module.Userspace import com.zaneschepke.wireguardautotunnel.module.Userspace
import com.zaneschepke.wireguardautotunnel.service.tunnel.statistics.AmneziaStatistics
import com.zaneschepke.wireguardautotunnel.service.tunnel.statistics.TunnelStatistics
import com.zaneschepke.wireguardautotunnel.service.tunnel.statistics.WireGuardStatistics
import com.zaneschepke.wireguardautotunnel.util.Constants import com.zaneschepke.wireguardautotunnel.util.Constants
import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext 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 org.amnezia.awg.backend.Tunnel
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Provider
class WireGuardTunnel class WireGuardTunnel
@Inject @Inject
constructor( constructor(
private val userspaceAmneziaBackend: Provider<org.amnezia.awg.backend.Backend>, @Userspace private val userspaceBackend: Backend,
@Userspace private val userspaceBackend: Provider<Backend>, @Kernel private val kernelBackend: Backend,
@Kernel private val kernelBackend: Provider<Backend>,
private val appDataRepository: AppDataRepository, private val appDataRepository: AppDataRepository,
@ApplicationScope private val applicationScope: CoroutineScope,
@IoDispatcher private val ioDispatcher: CoroutineDispatcher
) : VpnService { ) : VpnService {
private val _vpnState = MutableStateFlow(VpnState()) private val _vpnState = MutableStateFlow(VpnState())
override val vpnState: StateFlow<VpnState> = _vpnState.asStateFlow() override val vpnState: StateFlow<VpnState> = _vpnState.asStateFlow()
private val scope = CoroutineScope(Dispatchers.IO)
private var statsJob: Job? = null private var statsJob: Job? = null
private var backendIsWgUserspace = true private var backend: Backend = userspaceBackend
private var backendIsAmneziaUserspace = false private var backendIsUserspace = true
init { init {
applicationScope.launch(ioDispatcher) { scope.launch {
appDataRepository.settings.getSettingsFlow().collect { appDataRepository.settings.getSettingsFlow().collect {
if (it.isKernelEnabled && (backendIsWgUserspace || backendIsAmneziaUserspace)) { if (it.isKernelEnabled && backendIsUserspace) {
Timber.i("Setting kernel backend") Timber.d("Setting kernel backend")
backendIsWgUserspace = false backend = kernelBackend
backendIsAmneziaUserspace = false backendIsUserspace = false
} else if (!it.isKernelEnabled && !it.isAmneziaEnabled && !backendIsWgUserspace) { } else if (!it.isKernelEnabled && !backendIsUserspace) {
Timber.i("Setting WireGuard userspace backend") Timber.d("Setting userspace backend")
backendIsWgUserspace = true backend = userspaceBackend
backendIsAmneziaUserspace = false backendIsUserspace = true
} else if (it.isAmneziaEnabled && !backendIsAmneziaUserspace) {
Timber.i("Setting Amnezia userspace backend")
backendIsAmneziaUserspace = true
backendIsWgUserspace = false
} }
} }
} }
} }
private fun setState(tunnelConfig: TunnelConfig?, tunnelState: TunnelState): TunnelState { override suspend fun startTunnel(tunnelConfig: TunnelConfig?): Tunnel.State {
return if (backendIsAmneziaUserspace) { return try {
Timber.i("Using Amnezia backend") //TODO we need better error handling here
val config = tunnelConfig?.let { val config = tunnelConfig ?: appDataRepository.getPrimaryOrFirstTunnel()
if (it.amQuick != "") TunnelConfig.configFromAmQuick(it.amQuick) else { if (config != null) {
Timber.w("Using backwards compatible wg config, amnezia specific config not found.") emitTunnelConfig(config)
TunnelConfig.configFromAmQuick(it.wgQuick) val wgConfig = TunnelConfig.configFromQuick(config.wgQuick)
} val state =
} backend.setState(
val state = this,
userspaceAmneziaBackend.get().setState(this, tunnelState.toAmState(), config) Tunnel.State.UP,
TunnelState.from(state) wgConfig,
} else { )
Timber.i("Using Wg backend") state
val wgConfig = tunnelConfig?.let { TunnelConfig.configFromWgQuick(it.wgQuick) } } else throw Exception("No tunnels")
val state = backend().setState( } catch (e: BackendException) {
this, Timber.e("Failed to start tunnel with error: ${e.message}")
tunnelState.toWgState(), Tunnel.State.DOWN
wgConfig,
)
TunnelState.from(state)
} }
} }
override suspend fun startTunnel(tunnelConfig: TunnelConfig?): TunnelState { private fun emitTunnelState(state: Tunnel.State) {
return withContext(ioDispatcher) {
try {
//TODO we need better error handling here
// need to bubble up these errors to the UI
val config = tunnelConfig ?: appDataRepository.getPrimaryOrFirstTunnel()
if (config != null) {
emitTunnelConfig(config)
setState(config, TunnelState.UP)
} else throw Exception("No tunnels")
} catch (e: BackendException) {
Timber.e("Failed to start tunnel with error: ${e.message}")
TunnelState.from(State.DOWN)
}
}
}
private fun backend(): Backend {
return when {
backendIsWgUserspace -> {
userspaceBackend.get()
}
!backendIsWgUserspace && !backendIsAmneziaUserspace -> {
kernelBackend.get()
}
else -> {
userspaceBackend.get()
}
}
}
private fun emitTunnelState(state: TunnelState) {
_vpnState.tryEmit( _vpnState.tryEmit(
_vpnState.value.copy( _vpnState.value.copy(
status = state, status = state,
@@ -134,7 +85,7 @@ constructor(
) )
} }
private fun emitBackendStatistics(statistics: TunnelStatistics) { private fun emitBackendStatistics(statistics: Statistics) {
_vpnState.tryEmit( _vpnState.tryEmit(
_vpnState.value.copy( _vpnState.value.copy(
statistics = statistics, statistics = statistics,
@@ -150,73 +101,45 @@ constructor(
) )
} }
private fun resetVpnState() {
_vpnState.tryEmit(VpnState())
}
override suspend fun stopTunnel() { override suspend fun stopTunnel() {
withContext(ioDispatcher) { try {
try { if (getState() == Tunnel.State.UP) {
if (getState() == TunnelState.UP) { val state = backend.setState(this, Tunnel.State.DOWN, null)
val state = setState(null, TunnelState.DOWN) emitTunnelState(state)
resetVpnState()
emitTunnelState(state)
}
} catch (e: BackendException) {
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}")
} }
} catch (e: BackendException) {
Timber.e("Failed to stop tunnel with error: ${e.message}")
} }
} }
override fun getState(): TunnelState { override fun getState(): Tunnel.State {
return if (backendIsAmneziaUserspace) TunnelState.from( return backend.getState(this)
userspaceAmneziaBackend.get().getState(this),
)
else TunnelState.from(backend().getState(this))
} }
override fun getName(): String { override fun getName(): String {
return _vpnState.value.tunnelConfig?.name ?: "" return _vpnState.value.tunnelConfig?.name ?: ""
} }
override fun onStateChange(state: Tunnel.State) {
override fun onStateChange(newState: Tunnel.State) { val tunnel = this
handleStateChange(TunnelState.from(newState))
}
private fun handleStateChange(state: TunnelState) {
emitTunnelState(state) emitTunnelState(state)
WireGuardAutoTunnel.requestTunnelTileServiceStateUpdate() WireGuardAutoTunnel.requestTunnelTileServiceStateUpdate(WireGuardAutoTunnel.instance)
if (state == TunnelState.UP) { if (state == Tunnel.State.UP) {
statsJob = startTunnelStatisticsJob() statsJob =
scope.launch {
while (true) {
val statistics = backend.getStatistics(tunnel)
emitBackendStatistics(statistics)
delay(Constants.VPN_STATISTIC_CHECK_INTERVAL)
}
}
} }
if (state == TunnelState.DOWN) { if (state == Tunnel.State.DOWN) {
try { try {
statsJob?.cancel() statsJob?.cancel()
} catch (e: CancellationException) { } catch (e : CancellationException) {
Timber.i("Stats job cancelled") Timber.i("Stats job cancelled")
} }
} }
} }
private fun startTunnelStatisticsJob() = applicationScope.launch(ioDispatcher) {
while (true) {
if (backendIsAmneziaUserspace) {
emitBackendStatistics(
AmneziaStatistics(
userspaceAmneziaBackend.get().getStatistics(this@WireGuardTunnel),
),
)
} else {
emitBackendStatistics(WireGuardStatistics(backend().getStatistics(this@WireGuardTunnel)))
}
delay(Constants.VPN_STATISTIC_CHECK_INTERVAL)
}
}
override fun onStateChange(state: State) {
handleStateChange(TunnelState.from(state))
}
} }
@@ -1,34 +0,0 @@
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()
}
}
@@ -1,18 +0,0 @@
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
}
@@ -1,36 +0,0 @@
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,25 +1,35 @@
package com.zaneschepke.wireguardautotunnel.ui package com.zaneschepke.wireguardautotunnel.ui
import android.app.Application
import android.content.ActivityNotFoundException import android.content.ActivityNotFoundException
import android.content.Context
import android.content.Intent import android.content.Intent
import android.net.Uri import android.net.Uri
import android.widget.Toast
import androidx.compose.runtime.mutableStateListOf
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import com.wireguard.android.backend.GoBackend import androidx.lifecycle.viewModelScope
import com.zaneschepke.logcatter.Logcatter
import com.zaneschepke.logcatter.model.LogMessage
import com.zaneschepke.wireguardautotunnel.R import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
import com.zaneschepke.wireguardautotunnel.util.Constants import com.zaneschepke.wireguardautotunnel.util.Constants
import com.zaneschepke.wireguardautotunnel.util.FileUtils
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch
import org.amnezia.awg.backend.GoBackend
import timber.log.Timber import timber.log.Timber
import java.time.Instant
import javax.inject.Inject import javax.inject.Inject
@HiltViewModel @HiltViewModel
class AppViewModel class AppViewModel
@Inject @Inject
constructor() : ViewModel() { constructor(
private val application: Application,
) : ViewModel() {
val vpnIntent: Intent? = GoBackend.VpnService.prepare(WireGuardAutoTunnel.instance) val vpnIntent: Intent? = GoBackend.VpnService.prepare(WireGuardAutoTunnel.instance)
@@ -39,85 +49,100 @@ constructor() : ViewModel() {
} }
private fun requestPermissions() { private fun requestPermissions() {
_appUiState.update { _appUiState.value = _appUiState.value.copy(
it.copy( requestPermissions = true,
requestPermissions = true, )
)
}
} }
fun permissionsRequested() { fun permissionsRequested() {
_appUiState.update { _appUiState.value = _appUiState.value.copy(
it.copy( requestPermissions = false,
requestPermissions = false, )
)
}
} }
fun openWebPage(url: String, context: Context) { fun openWebPage(url: String) {
try { try {
val webpage: Uri = Uri.parse(url) val webpage: Uri = Uri.parse(url)
val intent = Intent(Intent.ACTION_VIEW, webpage).apply { val intent = Intent(Intent.ACTION_VIEW, webpage).apply {
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
} }
context.startActivity(intent) application.startActivity(intent)
} catch (e: ActivityNotFoundException) { } catch (e: ActivityNotFoundException) {
Timber.e(e) Timber.e(e)
showSnackbarMessage(context.getString(R.string.no_browser_detected)) showSnackbarMessage(application.getString(R.string.no_browser_detected))
} }
} }
fun onVpnPermissionAccepted() { fun onVpnPermissionAccepted() {
_appUiState.update { _appUiState.value = _appUiState.value.copy(
it.copy( vpnPermissionAccepted = true,
vpnPermissionAccepted = true, )
)
}
} }
fun launchEmail(context: Context) { fun launchEmail() {
try { try {
val intent = val intent =
Intent(Intent.ACTION_SENDTO).apply { Intent(Intent.ACTION_SENDTO).apply {
type = Constants.EMAIL_MIME_TYPE type = Constants.EMAIL_MIME_TYPE
putExtra(Intent.EXTRA_EMAIL, arrayOf(context.getString(R.string.my_email))) putExtra(Intent.EXTRA_EMAIL, arrayOf(application.getString(R.string.my_email)))
putExtra(Intent.EXTRA_SUBJECT, context.getString(R.string.email_subject)) putExtra(Intent.EXTRA_SUBJECT, application.getString(R.string.email_subject))
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
} }
context.startActivity( application.startActivity(
Intent.createChooser(intent, context.getString(R.string.email_chooser)).apply { Intent.createChooser(intent, application.getString(R.string.email_chooser)).apply {
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
}, },
) )
} catch (e: ActivityNotFoundException) { } catch (e: ActivityNotFoundException) {
Timber.e(e) Timber.e(e)
showSnackbarMessage(context.getString(R.string.no_email_detected)) showSnackbarMessage(application.getString(R.string.no_email_detected))
} }
} }
fun showSnackbarMessage(message: String) { fun showSnackbarMessage(message: String) {
_appUiState.update { _appUiState.value = _appUiState.value.copy(
it.copy( snackbarMessage = message,
snackbarMessage = message, snackbarMessageConsumed = false,
snackbarMessageConsumed = false, )
)
}
} }
fun snackbarMessageConsumed() { fun snackbarMessageConsumed() {
_appUiState.update { _appUiState.value = _appUiState.value.copy(
it.copy( snackbarMessage = "",
snackbarMessage = "", snackbarMessageConsumed = true,
snackbarMessageConsumed = true, )
) }
val logs = mutableStateListOf<LogMessage>()
fun readLogCatOutput() =
viewModelScope.launch(viewModelScope.coroutineContext + Dispatchers.IO) {
launch {
Logcatter.logs(callback = {
logs.add(it)
if (logs.size > Constants.LOG_BUFFER_SIZE) {
logs.removeRange(0, (logs.size - Constants.LOG_BUFFER_SIZE).toInt())
}
})
}
} }
fun clearLogs() {
logs.clear()
Logcatter.clear()
}
fun saveLogsToFile() {
val fileName = "${Constants.BASE_LOG_FILE_NAME}-${Instant.now().epochSecond}.txt"
val content = logs.joinToString(separator = "\n")
FileUtils.saveFileToDownloads(application.applicationContext, content, fileName)
Toast.makeText(application, application.getString(R.string.logs_saved), Toast.LENGTH_SHORT)
.show()
} }
fun setNotificationPermissionAccepted(accepted: Boolean) { fun setNotificationPermissionAccepted(accepted: Boolean) {
_appUiState.update { _appUiState.value = _appUiState.value.copy(
it.copy( notificationPermissionAccepted = accepted,
notificationPermissionAccepted = accepted, )
)
}
} }
} }
@@ -0,0 +1,5 @@
package com.zaneschepke.wireguardautotunnel.ui
import com.journeyapps.barcodescanner.CaptureActivity
class CaptureActivityPortrait : CaptureActivity()
@@ -32,24 +32,21 @@ import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.navigation.NavType
import androidx.navigation.compose.NavHost import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable import androidx.navigation.compose.composable
import androidx.navigation.compose.currentBackStackEntryAsState import androidx.navigation.compose.currentBackStackEntryAsState
import androidx.navigation.compose.rememberNavController import androidx.navigation.compose.rememberNavController
import androidx.navigation.navArgument
import com.google.accompanist.permissions.ExperimentalPermissionsApi import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.google.accompanist.permissions.isGranted import com.google.accompanist.permissions.isGranted
import com.google.accompanist.permissions.rememberPermissionState import com.google.accompanist.permissions.rememberPermissionState
import com.zaneschepke.wireguardautotunnel.R import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
import com.zaneschepke.wireguardautotunnel.data.repository.AppStateRepository import com.zaneschepke.wireguardautotunnel.data.datastore.DataStoreManager
import com.zaneschepke.wireguardautotunnel.data.repository.SettingsRepository import com.zaneschepke.wireguardautotunnel.data.repository.SettingsRepository
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.BottomNavBar import com.zaneschepke.wireguardautotunnel.ui.common.navigation.BottomNavBar
import com.zaneschepke.wireguardautotunnel.ui.common.prompt.CustomSnackBar import com.zaneschepke.wireguardautotunnel.ui.common.prompt.CustomSnackBar
import com.zaneschepke.wireguardautotunnel.ui.screens.config.ConfigScreen import com.zaneschepke.wireguardautotunnel.ui.screens.config.ConfigScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.main.ConfigType
import com.zaneschepke.wireguardautotunnel.ui.screens.main.MainScreen import com.zaneschepke.wireguardautotunnel.ui.screens.main.MainScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.options.OptionsScreen import com.zaneschepke.wireguardautotunnel.ui.screens.options.OptionsScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.pinlock.PinLockScreen import com.zaneschepke.wireguardautotunnel.ui.screens.pinlock.PinLockScreen
@@ -61,13 +58,14 @@ import com.zaneschepke.wireguardautotunnel.util.StringValue
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import xyz.teamgravity.pin_lock_compose.PinManager
import javax.inject.Inject import javax.inject.Inject
@AndroidEntryPoint @AndroidEntryPoint
class MainActivity : AppCompatActivity() { class MainActivity : AppCompatActivity() {
@Inject @Inject
lateinit var appStateRepository: AppStateRepository lateinit var dataStoreManager: DataStoreManager
@Inject @Inject
lateinit var settingsRepository: SettingsRepository lateinit var settingsRepository: SettingsRepository
@@ -81,18 +79,17 @@ class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
val isPinLockEnabled = intent.extras?.getBoolean(SplashActivity.IS_PIN_LOCK_ENABLED_KEY)
enableEdgeToEdge(navigationBarStyle = SystemBarStyle.dark(Color.Transparent.toArgb())) enableEdgeToEdge(navigationBarStyle = SystemBarStyle.dark(Color.Transparent.toArgb()))
// load preferences into memory and init data
lifecycleScope.launch { lifecycleScope.launch {
WireGuardAutoTunnel.requestTunnelTileServiceStateUpdate() dataStoreManager.init()
WireGuardAutoTunnel.requestTunnelTileServiceStateUpdate(this@MainActivity)
val settings = settingsRepository.getSettings() val settings = settingsRepository.getSettings()
if (settings.isAutoTunnelEnabled) { if (settings.isAutoTunnelEnabled) {
serviceManager.startWatcherService(application.applicationContext) serviceManager.startWatcherService(application.applicationContext)
} }
} }
setContent { setContent {
val appViewModel = hiltViewModel<AppViewModel>() val appViewModel = hiltViewModel<AppViewModel>()
val appUiState by appViewModel.appUiState.collectAsStateWithLifecycle() val appUiState by appViewModel.appUiState.collectAsStateWithLifecycle()
@@ -141,11 +138,7 @@ class MainActivity : AppCompatActivity() {
return@LaunchedEffect notificationPermissionState.launchPermissionRequest() return@LaunchedEffect notificationPermissionState.launchPermissionRequest()
} }
if (!appUiState.vpnPermissionAccepted) { if (!appUiState.vpnPermissionAccepted) {
return@LaunchedEffect appViewModel.vpnIntent?.let { return@LaunchedEffect vpnActivityResultState.launch(appViewModel.vpnIntent)
vpnActivityResultState.launch(
it,
)
}!!
} }
} }
} }
@@ -155,6 +148,7 @@ class MainActivity : AppCompatActivity() {
appViewModel.setNotificationPermissionAccepted( appViewModel.setNotificationPermissionAccepted(
notificationPermissionState?.status?.isGranted ?: true, notificationPermissionState?.status?.isGranted ?: true,
) )
if (!WireGuardAutoTunnel.isRunningOnAndroidTv()) appViewModel.readLogCatOutput()
} }
LaunchedEffect(appUiState.snackbarMessageConsumed) { LaunchedEffect(appUiState.snackbarMessageConsumed) {
@@ -201,8 +195,10 @@ class MainActivity : AppCompatActivity() {
) { padding -> ) { padding ->
NavHost( NavHost(
navController, navController,
startDestination = (if (isPinLockEnabled == true) Screen.Lock.route else Screen.Main.route), startDestination =
modifier = Modifier (if (PinManager.pinExists()) Screen.Lock.route else Screen.Main.route),
modifier =
Modifier
.padding(padding) .padding(padding)
.fillMaxSize(), .fillMaxSize(),
) { ) {
@@ -234,33 +230,16 @@ class MainActivity : AppCompatActivity() {
) )
} }
composable(Screen.Support.Logs.route) { composable(Screen.Support.Logs.route) {
LogsScreen() LogsScreen(appViewModel)
} }
composable( composable("${Screen.Config.route}/{id}") {
"${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 id = it.arguments?.getString("id")
val configType = ConfigType.valueOf(
it.arguments?.getString("configType") ?: ConfigType.WIREGUARD.name,
)
if (!id.isNullOrBlank()) { if (!id.isNullOrBlank()) {
ConfigScreen( ConfigScreen(
navController = navController, navController = navController,
tunnelId = id, tunnelId = id,
appViewModel = appViewModel, appViewModel = appViewModel,
focusRequester = focusRequester, focusRequester = focusRequester,
configType = configType,
) )
} }
} }
@@ -4,9 +4,11 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.Home import androidx.compose.material.icons.rounded.Home
import androidx.compose.material.icons.rounded.QuestionMark import androidx.compose.material.icons.rounded.QuestionMark
import androidx.compose.material.icons.rounded.Settings import androidx.compose.material.icons.rounded.Settings
import androidx.compose.ui.res.stringResource
import com.zaneschepke.wireguardautotunnel.R import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.BottomNavItem import com.zaneschepke.wireguardautotunnel.ui.common.navigation.BottomNavItem
import com.zaneschepke.wireguardautotunnel.util.StringValue
sealed class Screen(val route: String) { sealed class Screen(val route: String) {
data object Main : Screen("main") { data object Main : Screen("main") {
@@ -1,68 +0,0 @@
package com.zaneschepke.wireguardautotunnel.ui
import android.annotation.SuppressLint
import android.content.Intent
import android.os.Build
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import com.zaneschepke.logcatter.LocalLogCollector
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel.Companion.isRunningOnAndroidTv
import com.zaneschepke.wireguardautotunnel.data.repository.AppStateRepository
import com.zaneschepke.wireguardautotunnel.module.ApplicationScope
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import xyz.teamgravity.pin_lock_compose.PinManager
import javax.inject.Inject
@SuppressLint("CustomSplashScreen")
@AndroidEntryPoint
class SplashActivity : ComponentActivity() {
@Inject
lateinit var appStateRepository: AppStateRepository
@Inject
lateinit var localLogCollector: LocalLogCollector
@Inject
@ApplicationScope
lateinit var applicationScope: CoroutineScope
override fun onCreate(savedInstanceState: Bundle?) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
val splashScreen = installSplashScreen()
splashScreen.setKeepOnScreenCondition { true }
}
super.onCreate(savedInstanceState)
applicationScope.launch {
if (!isRunningOnAndroidTv()) localLogCollector.start()
}
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.CREATED) {
val pinLockEnabled = appStateRepository.isPinLockEnabled()
if (pinLockEnabled) {
PinManager.initialize(WireGuardAutoTunnel.instance)
}
val intent = Intent(this@SplashActivity, MainActivity::class.java).apply {
putExtra(IS_PIN_LOCK_ENABLED_KEY, pinLockEnabled)
}
startActivity(intent)
finish()
}
}
}
companion object {
const val IS_PIN_LOCK_ENABLED_KEY = "is_pin_lock_enabled"
}
}
@@ -15,12 +15,11 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import com.zaneschepke.wireguardautotunnel.service.tunnel.statistics.TunnelStatistics
import com.zaneschepke.wireguardautotunnel.util.NumberUtils import com.zaneschepke.wireguardautotunnel.util.NumberUtils
import com.zaneschepke.wireguardautotunnel.util.toThreeDecimalPlaceString import com.zaneschepke.wireguardautotunnel.util.toThreeDecimalPlaceString
import org.amnezia.awg.backend.Statistics
@OptIn(ExperimentalFoundationApi::class) @OptIn(ExperimentalFoundationApi::class)
@Composable @Composable
@@ -31,7 +30,7 @@ fun RowListItem(
onClick: () -> Unit, onClick: () -> Unit,
rowButton: @Composable () -> Unit, rowButton: @Composable () -> Unit,
expanded: Boolean, expanded: Boolean,
statistics: TunnelStatistics? statistics: Statistics?
) { ) {
Box( Box(
modifier = modifier =
@@ -53,15 +52,14 @@ fun RowListItem(
) { ) {
Row( Row(
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.fillMaxWidth(13 / 20f),
) { ) {
icon() icon()
Text(text, maxLines = 1, overflow = TextOverflow.Ellipsis) Text(text)
} }
rowButton() rowButton()
} }
if (expanded) { if (expanded) {
statistics?.getPeers()?.forEach { statistics?.peers()?.forEach {
Row( Row(
modifier = modifier =
Modifier Modifier
@@ -71,9 +69,9 @@ fun RowListItem(
horizontalArrangement = Arrangement.SpaceEvenly, horizontalArrangement = Arrangement.SpaceEvenly,
) { ) {
//TODO change these to string resources //TODO change these to string resources
val handshakeEpoch = statistics.peerStats(it)!!.latestHandshakeEpochMillis val handshakeEpoch = statistics.peer(it)!!.latestHandshakeEpochMillis
val peerTx = statistics.peerStats(it)!!.txBytes val peerTx = statistics.peer(it)!!.txBytes
val peerRx = statistics.peerStats(it)!!.rxBytes val peerRx = statistics.peer(it)!!.rxBytes
val peerId = it.toBase64().subSequence(0, 3).toString() + "***" val peerId = it.toBase64().subSequence(0, 3).toString() + "***"
val handshakeSec = val handshakeSec =
NumberUtils.getSecondsBetweenTimestampAndNow(handshakeEpoch) NumberUtils.getSecondsBetweenTimestampAndNow(handshakeEpoch)
@@ -44,21 +44,19 @@ fun SearchBar(onQuery: (queryString: String) -> Unit) {
onQuery(onQueryChanged) onQuery(onQueryChanged)
}, },
leadingIcon = { leadingIcon = {
val icon = Icons.Rounded.Search
Icon( Icon(
imageVector = icon, imageVector = Icons.Rounded.Search,
tint = MaterialTheme.colorScheme.onBackground, tint = MaterialTheme.colorScheme.onBackground,
contentDescription = icon.name, contentDescription = stringResource(id = R.string.search_icon),
) )
}, },
trailingIcon = { trailingIcon = {
if (showClearIcon) { if (showClearIcon) {
IconButton(onClick = { query = "" }) { IconButton(onClick = { query = "" }) {
val icon = Icons.Rounded.Clear
Icon( Icon(
imageVector = icon, imageVector = Icons.Rounded.Clear,
tint = MaterialTheme.colorScheme.onBackground, tint = MaterialTheme.colorScheme.onBackground,
contentDescription = icon.name, contentDescription = stringResource(id = R.string.clear_icon),
) )
} }
} }
@@ -28,15 +28,12 @@ fun ConfigurationToggle(
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween, horizontalArrangement = Arrangement.SpaceBetween,
) { ) {
Text( Text(label, textAlign = TextAlign.Start, modifier = Modifier
label, textAlign = TextAlign.Start, .weight(
modifier = Modifier weight = 1.0f,
.weight( fill = false,
weight = 1.0f, ),
fill = false, softWrap = true)
),
softWrap = true,
)
Switch( Switch(
modifier = modifier, modifier = modifier,
enabled = enabled, enabled = enabled,
@@ -0,0 +1,30 @@
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 "",
)
}
}
}
@@ -1,13 +1,13 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.config.model package com.zaneschepke.wireguardautotunnel.ui.models
import com.wireguard.config.Peer import org.amnezia.awg.config.Peer
data class PeerProxy( data class PeerProxy(
val publicKey: String = "", var publicKey: String = "",
val preSharedKey: String = "", var preSharedKey: String = "",
val persistentKeepalive: String = "", var persistentKeepalive: String = "",
val endpoint: String = "", var endpoint: String = "",
val allowedIps: String = IPV4_WILDCARD.joinToString(", ").trim() var allowedIps: String = IPV4_WILDCARD.joinToString(", ").trim()
) { ) {
companion object { companion object {
fun from(peer: Peer): PeerProxy { fun from(peer: Peer): PeerProxy {
@@ -35,31 +35,6 @@ data class PeerProxy(
) )
} }
fun from(peer: org.amnezia.awg.config.Peer): PeerProxy {
return PeerProxy(
publicKey = peer.publicKey.toBase64(),
preSharedKey =
if (peer.preSharedKey.isPresent) {
peer.preSharedKey.get().toBase64().trim()
} else {
""
},
persistentKeepalive =
if (peer.persistentKeepalive.isPresent) {
peer.persistentKeepalive.get().toString().trim()
} else {
""
},
endpoint =
if (peer.endpoint.isPresent) {
peer.endpoint.get().toString().trim()
} else {
""
},
allowedIps = peer.allowedIps.joinToString(", ").trim(),
)
}
val IPV4_PUBLIC_NETWORKS = val IPV4_PUBLIC_NETWORKS =
setOf( setOf(
"0.0.0.0/5", "0.0.0.0/5",
@@ -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.prompt.AuthorizationPrompt
import com.zaneschepke.wireguardautotunnel.ui.common.screen.LoadingScreen import com.zaneschepke.wireguardautotunnel.ui.common.screen.LoadingScreen
import com.zaneschepke.wireguardautotunnel.ui.common.text.SectionTitle import com.zaneschepke.wireguardautotunnel.ui.common.text.SectionTitle
import com.zaneschepke.wireguardautotunnel.ui.screens.main.ConfigType
import com.zaneschepke.wireguardautotunnel.util.Constants import com.zaneschepke.wireguardautotunnel.util.Constants
import com.zaneschepke.wireguardautotunnel.util.getMessage import com.zaneschepke.wireguardautotunnel.util.Event
import com.zaneschepke.wireguardautotunnel.util.Result
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter") @SuppressLint("UnusedMaterial3ScaffoldPaddingParameter")
@@ -94,8 +94,7 @@ fun ConfigScreen(
focusRequester: FocusRequester, focusRequester: FocusRequester,
navController: NavController, navController: NavController,
appViewModel: AppViewModel, appViewModel: AppViewModel,
tunnelId: String, tunnelId: String
configType: ConfigType
) { ) {
val context = LocalContext.current val context = LocalContext.current
val clipboardManager: ClipboardManager = LocalClipboardManager.current val clipboardManager: ClipboardManager = LocalClipboardManager.current
@@ -149,11 +148,11 @@ fun ConfigScreen(
}, },
onError = { onError = {
showAuthPrompt = false showAuthPrompt = false
appViewModel.showSnackbarMessage(context.getString(R.string.error_authentication_failed)) appViewModel.showSnackbarMessage(Event.Error.AuthenticationFailed.message)
}, },
onFailure = { onFailure = {
showAuthPrompt = false showAuthPrompt = false
appViewModel.showSnackbarMessage(context.getString(R.string.error_authorization_failed)) appViewModel.showSnackbarMessage(Event.Error.AuthorizationFailed.message)
}, },
) )
} }
@@ -320,11 +319,15 @@ fun ConfigScreen(
} }
}, },
onClick = { onClick = {
viewModel.onSaveAllChanges(configType).onSuccess { viewModel.onSaveAllChanges().let {
appViewModel.showSnackbarMessage(context.getString(R.string.config_changes_saved)) when (it) {
navController.navigate(Screen.Main.route) is Result.Success -> {
}.onFailure { appViewModel.showSnackbarMessage(it.data.message)
appViewModel.showSnackbarMessage(it.getMessage(context)) navController.navigate(Screen.Main.route)
}
is Result.Error -> appViewModel.showSnackbarMessage(it.error.message)
}
} }
}, },
containerColor = fobColor, containerColor = fobColor,
@@ -483,122 +486,6 @@ fun ConfigScreen(
modifier = Modifier.width(IntrinsicSize.Min), 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( Row(
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
modifier = Modifier modifier = Modifier
@@ -1,9 +1,8 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.config package com.zaneschepke.wireguardautotunnel.ui.screens.config
import com.wireguard.config.Config import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig import com.zaneschepke.wireguardautotunnel.ui.models.InterfaceProxy
import com.zaneschepke.wireguardautotunnel.ui.screens.config.model.InterfaceProxy import com.zaneschepke.wireguardautotunnel.ui.models.PeerProxy
import com.zaneschepke.wireguardautotunnel.ui.screens.config.model.PeerProxy
import com.zaneschepke.wireguardautotunnel.util.Packages import com.zaneschepke.wireguardautotunnel.util.Packages
data class ConfigUiState( data class ConfigUiState(
@@ -15,59 +14,5 @@ data class ConfigUiState(
val isAllApplicationsEnabled: Boolean = false, val isAllApplicationsEnabled: Boolean = false,
val loading: Boolean = true, val loading: Boolean = true,
val tunnel: TunnelConfig? = null, val tunnel: TunnelConfig? = null,
val tunnelName: String = "", val tunnelName: String = ""
val isAmneziaEnabled: Boolean = false )
) {
companion object {
fun from(config: Config): ConfigUiState {
val proxyPeers = config.peers.map { PeerProxy.from(it) }
val proxyInterface = InterfaceProxy.from(config.`interface`)
var include = true
var isAllApplicationsEnabled = false
val checkedPackages =
if (config.`interface`.includedApplications.isNotEmpty()) {
config.`interface`.includedApplications
} else if (config.`interface`.excludedApplications.isNotEmpty()) {
include = false
config.`interface`.excludedApplications
} else {
isAllApplicationsEnabled = true
emptySet()
}
return ConfigUiState(
proxyPeers,
proxyInterface,
emptyList(),
checkedPackages.toList(),
include,
isAllApplicationsEnabled,
)
}
fun from(config: org.amnezia.awg.config.Config): ConfigUiState {
//TODO update with new values
val proxyPeers = config.peers.map { PeerProxy.from(it) }
val proxyInterface = InterfaceProxy.from(config.`interface`)
var include = true
var isAllApplicationsEnabled = false
val checkedPackages =
if (config.`interface`.includedApplications.isNotEmpty()) {
config.`interface`.includedApplications
} else if (config.`interface`.excludedApplications.isNotEmpty()) {
include = false
config.`interface`.excludedApplications
} else {
isAllApplicationsEnabled = true
emptySet()
}
return ConfigUiState(
proxyPeers,
proxyInterface,
emptyList(),
checkedPackages.toList(),
include,
isAllApplicationsEnabled,
)
}
}
}
@@ -1,36 +1,33 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.config package com.zaneschepke.wireguardautotunnel.ui.screens.config
import android.Manifest import android.Manifest
import android.app.Application
import android.content.pm.PackageInfo import android.content.pm.PackageInfo
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.os.Build import android.os.Build
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.wireguard.config.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.WireGuardAutoTunnel
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
import com.zaneschepke.wireguardautotunnel.data.repository.SettingsRepository import com.zaneschepke.wireguardautotunnel.ui.models.InterfaceProxy
import com.zaneschepke.wireguardautotunnel.module.IoDispatcher import com.zaneschepke.wireguardautotunnel.ui.models.PeerProxy
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.Constants
import com.zaneschepke.wireguardautotunnel.util.Event
import com.zaneschepke.wireguardautotunnel.util.NumberUtils import com.zaneschepke.wireguardautotunnel.util.NumberUtils
import com.zaneschepke.wireguardautotunnel.util.StringValue import com.zaneschepke.wireguardautotunnel.util.Result
import com.zaneschepke.wireguardautotunnel.util.WgTunnelExceptions
import com.zaneschepke.wireguardautotunnel.util.removeAt import com.zaneschepke.wireguardautotunnel.util.removeAt
import com.zaneschepke.wireguardautotunnel.util.update import com.zaneschepke.wireguardautotunnel.util.update
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import 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 timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
@@ -38,36 +35,49 @@ import javax.inject.Inject
class ConfigViewModel class ConfigViewModel
@Inject @Inject
constructor( constructor(
private val settingsRepository: SettingsRepository, private val application: Application,
private val appDataRepository: AppDataRepository, private val appDataRepository: AppDataRepository
@IoDispatcher private val ioDispatcher: CoroutineDispatcher
) : ViewModel() { ) : ViewModel() {
private val packageManager = WireGuardAutoTunnel.instance.packageManager private val packageManager = application.packageManager
private val _uiState = MutableStateFlow(ConfigUiState()) private val _uiState = MutableStateFlow(ConfigUiState())
val uiState = _uiState.asStateFlow() val uiState = _uiState.asStateFlow()
fun init(tunnelId: String) = fun init(tunnelId: String) =
viewModelScope.launch(ioDispatcher) { viewModelScope.launch(Dispatchers.IO) {
val packages = getQueriedPackages("") val packages = getQueriedPackages("")
val state = val state =
if (tunnelId != Constants.MANUAL_TUNNEL_CONFIG_ID) { if (tunnelId != Constants.MANUAL_TUNNEL_CONFIG_ID) {
val tunnelConfig = val tunnelConfig =
appDataRepository.tunnels.getAll() appDataRepository.tunnels.getAll()
.firstOrNull { it.id.toString() == tunnelId } .firstOrNull { it.id.toString() == tunnelId }
val isAmneziaEnabled = settingsRepository.getSettings().isAmneziaEnabled
if (tunnelConfig != null) { if (tunnelConfig != null) {
(if (isAmneziaEnabled) { val config = TunnelConfig.configFromQuick(tunnelConfig.wgQuick)
val amConfig = val proxyPeers = config.peers.map { PeerProxy.from(it) }
if (tunnelConfig.amQuick == "") tunnelConfig.wgQuick else tunnelConfig.amQuick val proxyInterface = InterfaceProxy.from(config.`interface`)
ConfigUiState.from(TunnelConfig.configFromAmQuick(amConfig)) var include = true
} else ConfigUiState.from(TunnelConfig.configFromWgQuick(tunnelConfig.wgQuick))).copy( var isAllApplicationsEnabled = false
packages = packages, val checkedPackages =
loading = false, if (config.`interface`.includedApplications.isNotEmpty()) {
tunnel = tunnelConfig, config.`interface`.includedApplications
tunnelName = tunnelConfig.name, } else if (config.`interface`.excludedApplications.isNotEmpty()) {
isAmneziaEnabled = isAmneziaEnabled, include = false
config.`interface`.excludedApplications
} else {
isAllApplicationsEnabled = true
emptySet()
}
ConfigUiState(
proxyPeers,
proxyInterface,
packages,
checkedPackages.toList(),
include,
isAllApplicationsEnabled,
false,
tunnelConfig,
tunnelConfig.name,
) )
} else { } else {
ConfigUiState(loading = false, packages = packages) ConfigUiState(loading = false, packages = packages)
@@ -111,7 +121,7 @@ constructor(
} }
fun getPackageLabel(packageInfo: PackageInfo): String { fun getPackageLabel(packageInfo: PackageInfo): String {
return packageInfo.applicationInfo.loadLabel(packageManager).toString() return packageInfo.applicationInfo.loadLabel(application.packageManager).toString()
} }
private fun getAllInternetCapablePackages(): List<PackageInfo> { private fun getAllInternetCapablePackages(): List<PackageInfo> {
@@ -140,7 +150,7 @@ constructor(
viewModelScope.launch { viewModelScope.launch {
if (tunnelConfig != null) { if (tunnelConfig != null) {
saveConfig(tunnelConfig).join() saveConfig(tunnelConfig).join()
WireGuardAutoTunnel.requestTunnelTileServiceStateUpdate() WireGuardAutoTunnel.requestTunnelTileServiceStateUpdate(application)
} }
} }
@@ -158,20 +168,6 @@ constructor(
} }
} }
private fun buildAmPeerListFromProxyPeers(): List<org.amnezia.awg.config.Peer> {
return _uiState.value.proxyPeers.map {
val builder = org.amnezia.awg.config.Peer.Builder()
if (it.allowedIps.isNotEmpty()) builder.parseAllowedIPs(it.allowedIps.trim())
if (it.publicKey.isNotEmpty()) builder.parsePublicKey(it.publicKey.trim())
if (it.preSharedKey.isNotEmpty()) builder.parsePreSharedKey(it.preSharedKey.trim())
if (it.endpoint.isNotEmpty()) builder.parseEndpoint(it.endpoint.trim())
if (it.persistentKeepalive.isNotEmpty()) {
builder.parsePersistentKeepalive(it.persistentKeepalive.trim())
}
builder.build()
}
}
private fun emptyCheckedPackagesList() { private fun emptyCheckedPackagesList() {
_uiState.value = _uiState.value.copy(checkedPackageNames = emptyList()) _uiState.value = _uiState.value.copy(checkedPackageNames = emptyList())
} }
@@ -194,244 +190,147 @@ constructor(
return builder.build() return builder.build()
} }
private fun buildAmInterfaceListFromProxyInterface(): org.amnezia.awg.config.Interface { fun onSaveAllChanges(): Result<Event> {
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 { return try {
val wgQuick = buildConfig().toWgQuickString() val peerList = buildPeerListFromProxyPeers()
val amQuick = if (configType == ConfigType.AMNEZIA) { val wgInterface = buildInterfaceListFromProxyInterface()
buildAmConfig().toAwgQuickString() val config = Config.Builder().addPeers(peerList).setInterface(wgInterface).build()
} else TunnelConfig.AM_QUICK_DEFAULT
val tunnelConfig = when (uiState.value.tunnel) { val tunnelConfig = when (uiState.value.tunnel) {
null -> TunnelConfig( null -> TunnelConfig(
name = _uiState.value.tunnelName, name = _uiState.value.tunnelName,
wgQuick = wgQuick, wgQuick = config.toAwgQuickString(),
amQuick = amQuick,
) )
else -> uiState.value.tunnel!!.copy( else -> uiState.value.tunnel!!.copy(
name = _uiState.value.tunnelName, name = _uiState.value.tunnelName,
wgQuick = wgQuick, wgQuick = config.toAwgQuickString(),
amQuick = amQuick,
) )
} }
updateTunnelConfig(tunnelConfig) updateTunnelConfig(tunnelConfig)
Result.success(Unit) Result.Success(Event.Message.ConfigSaved)
} catch (e: Exception) { } catch (e: Exception) {
Timber.e(e) Timber.e(e)
val message = e.message?.substringAfter(":", missingDelimiterValue = "") val message = e.message?.substringAfter(":", missingDelimiterValue = "")
val stringValue = message?.let { Result.Error(Event.Error.ConfigParseError(message ?: ""))
StringValue.DynamicString(message)
} ?: StringValue.StringResource(R.string.unknown_error)
Result.failure(WgTunnelExceptions.ConfigParseError(stringValue))
} }
} }
fun onPeerPublicKeyChange(index: Int, value: String) { fun onPeerPublicKeyChange(index: Int, value: String) {
_uiState.update { _uiState.value =
it.copy( _uiState.value.copy(
proxyPeers = proxyPeers =
_uiState.value.proxyPeers.update( _uiState.value.proxyPeers.update(
index, index,
_uiState.value.proxyPeers[index].copy(publicKey = value), _uiState.value.proxyPeers[index].copy(publicKey = value),
), ),
) )
}
} }
fun onPreSharedKeyChange(index: Int, value: String) { fun onPreSharedKeyChange(index: Int, value: String) {
_uiState.update { _uiState.value =
it.copy( _uiState.value.copy(
proxyPeers = proxyPeers =
_uiState.value.proxyPeers.update( _uiState.value.proxyPeers.update(
index, index,
_uiState.value.proxyPeers[index].copy(preSharedKey = value), _uiState.value.proxyPeers[index].copy(preSharedKey = value),
), ),
) )
}
} }
fun onEndpointChange(index: Int, value: String) { fun onEndpointChange(index: Int, value: String) {
_uiState.update { _uiState.value =
it.copy( _uiState.value.copy(
proxyPeers = proxyPeers =
_uiState.value.proxyPeers.update( _uiState.value.proxyPeers.update(
index, index,
_uiState.value.proxyPeers[index].copy(endpoint = value), _uiState.value.proxyPeers[index].copy(endpoint = value),
), ),
) )
}
} }
fun onAllowedIpsChange(index: Int, value: String) { fun onAllowedIpsChange(index: Int, value: String) {
_uiState.update { _uiState.value =
it.copy( _uiState.value.copy(
proxyPeers = proxyPeers =
_uiState.value.proxyPeers.update( _uiState.value.proxyPeers.update(
index, index,
_uiState.value.proxyPeers[index].copy(allowedIps = value), _uiState.value.proxyPeers[index].copy(allowedIps = value),
), ),
) )
}
} }
fun onPersistentKeepaliveChanged(index: Int, value: String) { fun onPersistentKeepaliveChanged(index: Int, value: String) {
_uiState.update { _uiState.value =
it.copy( _uiState.value.copy(
proxyPeers = proxyPeers =
_uiState.value.proxyPeers.update( _uiState.value.proxyPeers.update(
index, index,
_uiState.value.proxyPeers[index].copy(persistentKeepalive = value), _uiState.value.proxyPeers[index].copy(persistentKeepalive = value),
), ),
) )
}
} }
fun onDeletePeer(index: Int) { fun onDeletePeer(index: Int) {
_uiState.update { _uiState.value =
it.copy( _uiState.value.copy(
proxyPeers = _uiState.value.proxyPeers.removeAt(index), proxyPeers = _uiState.value.proxyPeers.removeAt(index),
) )
}
} }
fun addEmptyPeer() { fun addEmptyPeer() {
_uiState.update { _uiState.value = _uiState.value.copy(proxyPeers = _uiState.value.proxyPeers + PeerProxy())
it.copy(proxyPeers = _uiState.value.proxyPeers + PeerProxy())
}
} }
fun generateKeyPair() { fun generateKeyPair() {
val keyPair = KeyPair() val keyPair = KeyPair()
_uiState.update { _uiState.value =
it.copy( _uiState.value.copy(
interfaceProxy = interfaceProxy =
_uiState.value.interfaceProxy.copy( _uiState.value.interfaceProxy.copy(
privateKey = keyPair.privateKey.toBase64(), privateKey = keyPair.privateKey.toBase64(),
publicKey = keyPair.publicKey.toBase64(), publicKey = keyPair.publicKey.toBase64(),
), ),
) )
}
} }
fun onAddressesChanged(value: String) { fun onAddressesChanged(value: String) {
_uiState.update { _uiState.value =
it.copy( _uiState.value.copy(
interfaceProxy = _uiState.value.interfaceProxy.copy(addresses = value), interfaceProxy = _uiState.value.interfaceProxy.copy(addresses = value),
) )
}
} }
fun onListenPortChanged(value: String) { fun onListenPortChanged(value: String) {
_uiState.update { _uiState.value =
it.copy( _uiState.value.copy(
interfaceProxy = _uiState.value.interfaceProxy.copy(listenPort = value), interfaceProxy = _uiState.value.interfaceProxy.copy(listenPort = value),
) )
}
} }
fun onDnsServersChanged(value: String) { fun onDnsServersChanged(value: String) {
_uiState.update { _uiState.value =
it.copy( _uiState.value.copy(
interfaceProxy = _uiState.value.interfaceProxy.copy(dnsServers = value), interfaceProxy = _uiState.value.interfaceProxy.copy(dnsServers = value),
) )
}
} }
fun onMtuChanged(value: String) { fun onMtuChanged(value: String) {
_uiState.update { _uiState.value =
it.copy(interfaceProxy = _uiState.value.interfaceProxy.copy(mtu = value)) _uiState.value.copy(interfaceProxy = _uiState.value.interfaceProxy.copy(mtu = value))
}
} }
private fun onInterfacePublicKeyChange(value: String) { private fun onInterfacePublicKeyChange(value: String) {
_uiState.update { _uiState.value =
it.copy( _uiState.value.copy(
interfaceProxy = _uiState.value.interfaceProxy.copy(publicKey = value), interfaceProxy = _uiState.value.interfaceProxy.copy(publicKey = value),
) )
}
} }
fun onPrivateKeyChange(value: String) { fun onPrivateKeyChange(value: String) {
_uiState.update { _uiState.value =
it.copy( _uiState.value.copy(
interfaceProxy = _uiState.value.interfaceProxy.copy(privateKey = value), interfaceProxy = _uiState.value.interfaceProxy.copy(privateKey = value),
) )
}
if (NumberUtils.isValidKey(value)) { if (NumberUtils.isValidKey(value)) {
val pair = KeyPair(Key.fromBase64(value)) val pair = KeyPair(Key.fromBase64(value))
onInterfacePublicKeyChange(pair.publicKey.toBase64()) onInterfacePublicKeyChange(pair.publicKey.toBase64())
@@ -445,78 +344,6 @@ constructor(
getAllInternetCapablePackages().filter { getAllInternetCapablePackages().filter {
getPackageLabel(it).lowercase().contains(query.lowercase()) getPackageLabel(it).lowercase().contains(query.lowercase())
} }
_uiState.update { it.copy(packages = packages) } _uiState.value = _uiState.value.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),
)
}
} }
} }
@@ -1,73 +0,0 @@
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,6 +0,0 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.main
enum class ConfigType {
AMNEZIA,
WIREGUARD
}
@@ -14,7 +14,6 @@ import androidx.compose.animation.slideInVertically
import androidx.compose.animation.slideOutVertically import androidx.compose.animation.slideOutVertically
import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.focusGroup
import androidx.compose.foundation.focusable import androidx.compose.foundation.focusable
import androidx.compose.foundation.gestures.ScrollableDefaults import androidx.compose.foundation.gestures.ScrollableDefaults
import androidx.compose.foundation.gestures.detectTapGestures import androidx.compose.foundation.gestures.detectTapGestures
@@ -30,11 +29,11 @@ import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.overscroll import androidx.compose.foundation.overscroll
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.ClickableText
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Create import androidx.compose.material.icons.filled.Create
import androidx.compose.material.icons.filled.FileOpen import androidx.compose.material.icons.filled.FileOpen
import androidx.compose.material.icons.filled.QrCode 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.Bolt
import androidx.compose.material.icons.rounded.Circle import androidx.compose.material.icons.rounded.Circle
import androidx.compose.material.icons.rounded.CopyAll import androidx.compose.material.icons.rounded.CopyAll
@@ -46,6 +45,7 @@ import androidx.compose.material.icons.rounded.Star
import androidx.compose.material3.AlertDialog import androidx.compose.material3.AlertDialog
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.FabPosition import androidx.compose.material3.FabPosition
import androidx.compose.material3.FloatingActionButton
import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
@@ -70,6 +70,7 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.focus.onFocusChanged
import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.hapticfeedback.HapticFeedbackType import androidx.compose.ui.hapticfeedback.HapticFeedbackType
@@ -80,38 +81,35 @@ import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalHapticFeedback import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.font.FontStyle import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.withStyle
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.navigation.NavController import androidx.navigation.NavController
import com.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.ScanContract
import com.journeyapps.barcodescanner.ScanOptions import com.journeyapps.barcodescanner.ScanOptions
import com.zaneschepke.wireguardautotunnel.R import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
import com.zaneschepke.wireguardautotunnel.service.tunnel.HandshakeStatus import com.zaneschepke.wireguardautotunnel.service.tunnel.HandshakeStatus
import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelState
import com.zaneschepke.wireguardautotunnel.ui.AppViewModel import com.zaneschepke.wireguardautotunnel.ui.AppViewModel
import com.zaneschepke.wireguardautotunnel.ui.CaptureActivityPortrait
import com.zaneschepke.wireguardautotunnel.ui.Screen import com.zaneschepke.wireguardautotunnel.ui.Screen
import com.zaneschepke.wireguardautotunnel.ui.common.RowListItem import com.zaneschepke.wireguardautotunnel.ui.common.RowListItem
import com.zaneschepke.wireguardautotunnel.ui.common.screen.LoadingScreen import com.zaneschepke.wireguardautotunnel.ui.common.screen.LoadingScreen
import com.zaneschepke.wireguardautotunnel.ui.theme.corn import com.zaneschepke.wireguardautotunnel.ui.theme.corn
import com.zaneschepke.wireguardautotunnel.ui.theme.mint import com.zaneschepke.wireguardautotunnel.ui.theme.mint
import com.zaneschepke.wireguardautotunnel.util.Constants import com.zaneschepke.wireguardautotunnel.util.Constants
import com.zaneschepke.wireguardautotunnel.util.getMessage import com.zaneschepke.wireguardautotunnel.util.Event
import com.zaneschepke.wireguardautotunnel.util.Result
import com.zaneschepke.wireguardautotunnel.util.handshakeStatus import com.zaneschepke.wireguardautotunnel.util.handshakeStatus
import com.zaneschepke.wireguardautotunnel.util.mapPeerStats import com.zaneschepke.wireguardautotunnel.util.mapPeerStats
import com.zaneschepke.wireguardautotunnel.util.truncateWithEllipsis
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.amnezia.awg.backend.Tunnel
@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter") @SuppressLint("UnusedMaterial3ScaffoldPaddingParameter")
@OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class) @OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class)
@@ -125,11 +123,10 @@ fun MainScreen(
val haptic = LocalHapticFeedback.current val haptic = LocalHapticFeedback.current
val context = LocalContext.current val context = LocalContext.current
val isVisible = rememberSaveable { mutableStateOf(true) } val isVisible = rememberSaveable { mutableStateOf(true) }
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope { Dispatchers.IO }
val sheetState = rememberModalBottomSheetState() val sheetState = rememberModalBottomSheetState()
var showBottomSheet by remember { mutableStateOf(false) } var showBottomSheet by remember { mutableStateOf(false) }
var configType by remember { mutableStateOf(ConfigType.WIREGUARD) }
// Nested scroll for control FAB // Nested scroll for control FAB
val nestedScrollConnection = remember { val nestedScrollConnection = remember {
@@ -190,7 +187,7 @@ fun MainScreen(
name.startsWith(Constants.ANDROID_TV_EXPLORER_STUB) name.startsWith(Constants.ANDROID_TV_EXPLORER_STUB)
} }
) { ) {
appViewModel.showSnackbarMessage(context.getString(R.string.error_no_file_explorer)) appViewModel.showSnackbarMessage(Event.Error.FileExplorerRequired.message)
} }
return intent return intent
} }
@@ -198,8 +195,11 @@ fun MainScreen(
) { data -> ) { data ->
if (data == null) return@rememberLauncherForActivityResult if (data == null) return@rememberLauncherForActivityResult
scope.launch { scope.launch {
viewModel.onTunnelFileSelected(data, configType, context).onFailure { viewModel.onTunnelFileSelected(data).let {
appViewModel.showSnackbarMessage(it.getMessage(context)) when (it) {
is Result.Error -> appViewModel.showSnackbarMessage(it.error.message)
is Result.Success -> {}
}
} }
} }
} }
@@ -209,8 +209,11 @@ fun MainScreen(
onResult = { onResult = {
if (it.contents != null) { if (it.contents != null) {
scope.launch { scope.launch {
viewModel.onTunnelQrResult(it.contents, configType).onFailure { error -> viewModel.onTunnelQrResult(it.contents).let { result ->
appViewModel.showSnackbarMessage(error.getMessage(context)) when (result) {
is Result.Success -> {}
is Result.Error -> appViewModel.showSnackbarMessage(result.error.message)
}
} }
} }
} }
@@ -223,7 +226,7 @@ fun MainScreen(
confirmButton = { confirmButton = {
TextButton( TextButton(
onClick = { onClick = {
selectedTunnel?.let { viewModel.onDelete(it, context) } selectedTunnel?.let { viewModel.onDelete(it) }
showDeleteTunnelAlertDialog = false showDeleteTunnelAlertDialog = false
selectedTunnel = null selectedTunnel = null
}, },
@@ -243,9 +246,7 @@ fun MainScreen(
fun onTunnelToggle(checked: Boolean, tunnel: TunnelConfig) { fun onTunnelToggle(checked: Boolean, tunnel: TunnelConfig) {
if (appViewModel.isRequiredPermissionGranted()) { if (appViewModel.isRequiredPermissionGranted()) {
if (checked) viewModel.onTunnelStart(tunnel, context) else viewModel.onTunnelStop( if (checked) viewModel.onTunnelStart(tunnel) else viewModel.onTunnelStop()
context,
)
} }
} }
@@ -253,28 +254,14 @@ fun MainScreen(
return LoadingScreen() 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)
scanLauncher.launch(scanOptions)
}
Scaffold( Scaffold(
modifier = modifier =
Modifier.pointerInput(Unit) { Modifier.pointerInput(Unit) {
if (uiState.tunnels.isNotEmpty()) { detectTapGestures(
detectTapGestures( onTap = {
onTap = { selectedTunnel = null
selectedTunnel = null },
}, )
)
}
}, },
floatingActionButtonPosition = FabPosition.End, floatingActionButtonPosition = FabPosition.End,
floatingActionButton = { floatingActionButton = {
@@ -282,77 +269,49 @@ fun MainScreen(
visible = isVisible.value, visible = isVisible.value,
enter = slideInVertically(initialOffsetY = { it * 2 }), enter = slideInVertically(initialOffsetY = { it * 2 }),
exit = slideOutVertically(targetOffsetY = { it * 2 }), exit = slideOutVertically(targetOffsetY = { it * 2 }),
modifier = Modifier
.focusRequester(focusRequester)
.focusGroup(),
) { ) {
val secondaryColor = MaterialTheme.colorScheme.secondary val secondaryColor = MaterialTheme.colorScheme.secondary
val tvFobColor = MaterialTheme.colorScheme.surfaceColorAtElevation(2.dp) val hoverColor = MaterialTheme.colorScheme.surfaceColorAtElevation(2.dp)
val fobColor = var fobColor by remember { mutableStateOf(secondaryColor) }
if (WireGuardAutoTunnel.isRunningOnAndroidTv()) tvFobColor else secondaryColor FloatingActionButton(
val fobIconColor = modifier =
if (WireGuardAutoTunnel.isRunningOnAndroidTv()) Color.White else MaterialTheme.colorScheme.background (if (
MultiFloatingActionButton( WireGuardAutoTunnel.isRunningOnAndroidTv() &&
fabIcon = FabIcon( uiState.tunnels.isEmpty()
iconRes = R.drawable.add, )
iconResAfterRotate = R.drawable.close, Modifier.focusRequester(focusRequester)
iconRotate = 180f, else Modifier)
), .onFocusChanged {
fabOption = FabOption( if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
iconTint = fobIconColor, fobColor = if (it.isFocused) hoverColor else secondaryColor
backgroundTint = fobColor, }
), },
itemsMultiFab = listOf( onClick = { showBottomSheet = true },
MultiFabItem( containerColor = fobColor,
label = {
Text(
stringResource(id = R.string.amnezia),
color = Color.White,
textAlign = TextAlign.Center,
modifier = Modifier.padding(end = 10.dp),
)
},
modifier = Modifier
.size(40.dp),
icon = R.drawable.add,
value = ConfigType.AMNEZIA.name,
miniFabOption = FabOption(
backgroundTint = fobColor,
fobIconColor,
),
),
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,
miniFabOption = FabOption(
backgroundTint = fobColor,
fobIconColor,
),
),
),
onFabItemClicked = {
showBottomSheet = true
configType = ConfigType.valueOf(it.value)
},
shape = RoundedCornerShape(16.dp), 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) { if (showBottomSheet) {
ModalBottomSheet( ModalBottomSheet(
onDismissRequest = { onDismissRequest = { showBottomSheet = false },
showBottomSheet = false
},
sheetState = sheetState, sheetState = sheetState,
) { ) {
// Sheet content // Sheet content
@@ -385,7 +344,16 @@ fun MainScreen(
.clickable { .clickable {
scope.launch { scope.launch {
showBottomSheet = false showBottomSheet = false
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)
} }
} }
.padding(10.dp), .padding(10.dp),
@@ -409,7 +377,7 @@ fun MainScreen(
.clickable { .clickable {
showBottomSheet = false showBottomSheet = false
navController.navigate( navController.navigate(
"${Screen.Config.route}/${Constants.MANUAL_TUNNEL_CONFIG_ID}?configType=${configType}", "${Screen.Config.route}/${Constants.MANUAL_TUNNEL_CONFIG_ID}",
) )
} }
.padding(10.dp), .padding(10.dp),
@@ -433,60 +401,12 @@ fun MainScreen(
modifier = modifier =
Modifier Modifier
.fillMaxSize() .fillMaxSize()
.overscroll(ScrollableDefaults.overscrollEffect()) .overscroll(ScrollableDefaults.overscrollEffect()).nestedScroll(nestedScrollConnection),
.nestedScroll(nestedScrollConnection),
state = rememberLazyListState(0, uiState.tunnels.count()), state = rememberLazyListState(0, uiState.tunnels.count()),
userScrollEnabled = true, userScrollEnabled = true,
reverseLayout = false, reverseLayout = false,
flingBehavior = ScrollableDefaults.flingBehavior(), flingBehavior = ScrollableDefaults.flingBehavior(),
) { ) {
item {
AnimatedVisibility(
uiState.tunnels.isEmpty(), exit = fadeOut(), enter = fadeIn(),
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center,
modifier = Modifier
.padding(top = 100.dp)
.fillMaxSize(),
) {
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(".")
}
Text(
text = stringResource(R.string.no_tunnels),
fontStyle = FontStyle.Italic,
)
ClickableText(
modifier = Modifier
.padding(vertical = 10.dp, horizontal = 24.dp),
text = gettingStarted,
style = MaterialTheme.typography.bodyMedium.copy(
color = MaterialTheme.colorScheme.onSurfaceVariant,
textAlign = TextAlign.Center,
),
) {
gettingStarted.getStringAnnotations(tag = "gettingStarted", it, it)
.firstOrNull()?.let { annotation ->
appViewModel.openWebPage(annotation.item, context)
}
}
}
}
}
item { item {
if (uiState.settings.isAutoTunnelEnabled) { if (uiState.settings.isAutoTunnelEnabled) {
val autoTunnelingLabel = buildAnnotatedString { val autoTunnelingLabel = buildAnnotatedString {
@@ -542,7 +462,7 @@ fun MainScreen(
val leadingIconColor = val leadingIconColor =
(if ( (if (
uiState.vpnState.tunnelConfig?.name == tunnel.name && uiState.vpnState.tunnelConfig?.name == tunnel.name &&
uiState.vpnState.status == TunnelState.UP uiState.vpnState.status == Tunnel.State.UP
) { ) {
uiState.vpnState.statistics uiState.vpnState.statistics
?.mapPeerStats() ?.mapPeerStats()
@@ -585,13 +505,13 @@ fun MainScreen(
.size(if (icon == circleIcon) 15.dp else 20.dp), .size(if (icon == circleIcon) 15.dp else 20.dp),
) )
}, },
text = tunnel.name, text = tunnel.name.truncateWithEllipsis(Constants.ALLOWED_DISPLAY_NAME_LENGTH),
onHold = { onHold = {
if ( if (
(uiState.vpnState.status == TunnelState.UP) && (uiState.vpnState.status == Tunnel.State.UP) &&
(tunnel.name == uiState.vpnState.tunnelConfig?.name) (tunnel.name == uiState.vpnState.tunnelConfig?.name)
) { ) {
appViewModel.showSnackbarMessage(context.getString(R.string.turn_off_tunnel)) appViewModel.showSnackbarMessage(Event.Message.TunnelOffAction.message)
return@RowListItem return@RowListItem
} }
haptic.performHapticFeedback(HapticFeedbackType.LongPress) haptic.performHapticFeedback(HapticFeedbackType.LongPress)
@@ -600,7 +520,7 @@ fun MainScreen(
onClick = { onClick = {
if (!WireGuardAutoTunnel.isRunningOnAndroidTv()) { if (!WireGuardAutoTunnel.isRunningOnAndroidTv()) {
if ( if (
uiState.vpnState.status == TunnelState.UP && uiState.vpnState.status == Tunnel.State.UP &&
(uiState.vpnState.tunnelConfig?.name == tunnel.name) (uiState.vpnState.tunnelConfig?.name == tunnel.name)
) { ) {
expanded.value = !expanded.value expanded.value = !expanded.value
@@ -625,7 +545,7 @@ fun MainScreen(
!uiState.settings.isAutoTunnelPaused !uiState.settings.isAutoTunnelPaused
) { ) {
appViewModel.showSnackbarMessage( appViewModel.showSnackbarMessage(
context.getString(R.string.turn_off_tunnel), Event.Message.AutoTunnelOffAction.message,
) )
} else { } else {
navController.navigate( navController.navigate(
@@ -658,7 +578,7 @@ fun MainScreen(
} else { } else {
val checked by remember { val checked by remember {
derivedStateOf { derivedStateOf {
(uiState.vpnState.status == TunnelState.UP && (uiState.vpnState.status == Tunnel.State.UP &&
tunnel.name == uiState.vpnState.tunnelConfig?.name) tunnel.name == uiState.vpnState.tunnelConfig?.name)
} }
} }
@@ -680,7 +600,7 @@ fun MainScreen(
onClick = { onClick = {
if (uiState.settings.isAutoTunnelEnabled) { if (uiState.settings.isAutoTunnelEnabled) {
appViewModel.showSnackbarMessage( appViewModel.showSnackbarMessage(
context.getString(R.string.turn_off_auto), Event.Message.AutoTunnelOffAction.message,
) )
} else { } else {
selectedTunnel = tunnel selectedTunnel = tunnel
@@ -700,13 +620,13 @@ fun MainScreen(
modifier = Modifier.focusRequester(focusRequester), modifier = Modifier.focusRequester(focusRequester),
onClick = { onClick = {
if ( if (
uiState.vpnState.status == TunnelState.UP && uiState.vpnState.status == Tunnel.State.UP &&
(uiState.vpnState.tunnelConfig?.name == tunnel.name) (uiState.vpnState.tunnelConfig?.name == tunnel.name)
) { ) {
expanded.value = !expanded.value expanded.value = !expanded.value
} else { } else {
appViewModel.showSnackbarMessage( appViewModel.showSnackbarMessage(
context.getString(R.string.turn_on_tunnel), Event.Message.TunnelOnAction.message,
) )
} }
}, },
@@ -723,11 +643,11 @@ fun MainScreen(
IconButton( IconButton(
onClick = { onClick = {
if ( if (
uiState.vpnState.status == TunnelState.UP && uiState.vpnState.status == Tunnel.State.UP &&
tunnel.name == uiState.vpnState.tunnelConfig?.name tunnel.name == uiState.vpnState.tunnelConfig?.name
) { ) {
appViewModel.showSnackbarMessage( appViewModel.showSnackbarMessage(
context.getString(R.string.turn_off_tunnel), Event.Message.TunnelOffAction.message,
) )
} else { } else {
selectedTunnel = tunnel selectedTunnel = tunnel
@@ -1,6 +1,6 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.main package com.zaneschepke.wireguardautotunnel.ui.screens.main
import com.zaneschepke.wireguardautotunnel.data.domain.Settings import com.zaneschepke.wireguardautotunnel.data.model.Settings
import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnState import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnState
import com.zaneschepke.wireguardautotunnel.util.TunnelConfigs import com.zaneschepke.wireguardautotunnel.util.TunnelConfigs
@@ -1,30 +1,30 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.main package com.zaneschepke.wireguardautotunnel.ui.screens.main
import android.app.Application
import android.content.Context import android.content.Context
import android.database.Cursor import android.database.Cursor
import android.net.Uri import android.net.Uri
import android.provider.OpenableColumns import android.provider.OpenableColumns
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.wireguard.config.Config
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
import com.zaneschepke.wireguardautotunnel.data.domain.Settings import com.zaneschepke.wireguardautotunnel.data.model.Settings
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
import com.zaneschepke.wireguardautotunnel.module.IoDispatcher
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnService import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnService
import com.zaneschepke.wireguardautotunnel.util.Constants import com.zaneschepke.wireguardautotunnel.util.Constants
import com.zaneschepke.wireguardautotunnel.util.Event
import com.zaneschepke.wireguardautotunnel.util.NumberUtils import com.zaneschepke.wireguardautotunnel.util.NumberUtils
import com.zaneschepke.wireguardautotunnel.util.WgTunnelExceptions import com.zaneschepke.wireguardautotunnel.util.Result
import com.zaneschepke.wireguardautotunnel.util.toWgQuickString
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.amnezia.awg.config.Config
import timber.log.Timber import timber.log.Timber
import java.io.InputStream import java.io.InputStream
import java.util.zip.ZipInputStream import java.util.zip.ZipInputStream
@@ -34,10 +34,10 @@ import javax.inject.Inject
class MainViewModel class MainViewModel
@Inject @Inject
constructor( constructor(
private val application: Application,
private val appDataRepository: AppDataRepository, private val appDataRepository: AppDataRepository,
private val serviceManager: ServiceManager, private val serviceManager: ServiceManager,
val vpnService: VpnService, val vpnService: VpnService
@IoDispatcher private val ioDispatcher: CoroutineDispatcher
) : ViewModel() { ) : ViewModel() {
val uiState = val uiState =
@@ -54,20 +54,21 @@ constructor(
MainUiState(), MainUiState(),
) )
private fun stopWatcherService(context: Context) { private fun stopWatcherService() =
serviceManager.stopWatcherService(context) viewModelScope.launch(Dispatchers.IO) {
} serviceManager.stopWatcherService(application.applicationContext)
}
fun onDelete(tunnel: TunnelConfig, context: Context) { fun onDelete(tunnel: TunnelConfig) {
viewModelScope.launch { viewModelScope.launch(Dispatchers.IO) {
val settings = appDataRepository.settings.getSettings() val settings = appDataRepository.settings.getSettings()
val isPrimary = tunnel.isPrimaryTunnel val isPrimary = tunnel.isPrimaryTunnel
if (appDataRepository.tunnels.count() == 1 || isPrimary) { if (appDataRepository.tunnels.count() == 1 || isPrimary) {
stopWatcherService(context) stopWatcherService()
resetTunnelSetting(settings) resetTunnelSetting(settings)
} }
appDataRepository.tunnels.delete(tunnel) appDataRepository.tunnels.delete(tunnel)
WireGuardAutoTunnel.requestTunnelTileServiceStateUpdate() WireGuardAutoTunnel.requestTunnelTileServiceStateUpdate(application)
} }
} }
@@ -80,251 +81,124 @@ constructor(
) )
} }
fun onTunnelStart(tunnelConfig: TunnelConfig, context: Context) = fun onTunnelStart(tunnelConfig: TunnelConfig) =
viewModelScope.launch { viewModelScope.launch(Dispatchers.IO) {
Timber.d("On start called!") Timber.d("On start called!")
serviceManager.startVpnService( serviceManager.startVpnService(
context, application.applicationContext,
tunnelConfig.id, tunnelConfig.id,
isManualStart = true, isManualStart = true,
) )
} }
fun onTunnelStop(context: Context) = fun onTunnelStop() =
viewModelScope.launch { viewModelScope.launch(Dispatchers.IO) {
Timber.i("Stopping active tunnel") Timber.i("Stopping active tunnel")
serviceManager.stopVpnService(context, isManualStop = true) serviceManager.stopVpnService(application.applicationContext, isManualStop = true)
} }
private fun validateConfigString(config: String, configType: ConfigType) { private fun validateConfigString(config: String) {
when (configType) { TunnelConfig.configFromQuick(config)
ConfigType.AMNEZIA -> TunnelConfig.configFromAmQuick(config) }
ConfigType.WIREGUARD -> TunnelConfig.configFromWgQuick(config)
suspend fun onTunnelQrResult(result: String): Result<Unit> {
return try {
validateConfigString(result)
val tunnelConfig =
TunnelConfig(name = NumberUtils.generateRandomTunnelName(), wgQuick = result)
addTunnel(tunnelConfig)
Result.Success(Unit)
} catch (e: Exception) {
Timber.e(e)
Result.Error(Event.Error.InvalidQrCode)
} }
} }
private fun generateQrCodeDefaultName(config: String, configType: ConfigType): String { private suspend fun saveTunnelConfigFromStream(stream: InputStream, fileName: String) {
return try { val bufferReader = stream.bufferedReader(charset = Charsets.UTF_8)
when (configType) { val config = Config.parse(bufferReader)
ConfigType.AMNEZIA -> { val tunnelName = getNameFromFileName(fileName)
TunnelConfig.configFromAmQuick(config).peers[0].endpoint.get().host addTunnel(TunnelConfig(name = tunnelName, wgQuick = config.toAwgQuickString()))
} withContext(Dispatchers.IO) { stream.close() }
}
ConfigType.WIREGUARD -> { private fun getInputStreamFromUri(uri: Uri): InputStream? {
TunnelConfig.configFromWgQuick(config).peers[0].endpoint.get().host 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)
} }
return Result.Success(Unit)
} else {
return Result.Error(Event.Error.InvalidFileExtension)
} }
} catch (e: Exception) { } catch (e: Exception) {
Timber.e(e) Timber.e(e)
NumberUtils.generateRandomTunnelName() return Result.Error(Event.Error.FileReadFailed)
} }
} }
private fun generateQrCodeTunnelName(config: String, configType: ConfigType): String { private suspend fun saveTunnelsFromZipUri(uri: Uri) {
var defaultName = generateQrCodeDefaultName(config, configType) ZipInputStream(getInputStreamFromUri(uri)).use { zip ->
val lines = config.lines().toMutableList() generateSequence { zip.nextEntry }
val linesIterator = lines.iterator() .filterNot {
while (linesIterator.hasNext()) { it.isDirectory ||
val next = linesIterator.next() getFileExtensionFromFileName(it.name) != Constants.CONF_FILE_EXTENSION
if (next.contains(Constants.QR_CODE_NAME_PROPERTY)) { }
defaultName = next.substringAfter(Constants.QR_CODE_NAME_PROPERTY).trim() .forEach {
break val name = getNameFromFileName(it.name)
} val config = Config.parse(zip)
} viewModelScope.launch(Dispatchers.IO) {
return defaultName addTunnel(TunnelConfig(name = name, wgQuick = config.toAwgQuickString()))
}
suspend fun onTunnelQrResult(result: String, configType: ConfigType): Result<Unit> {
return withContext(ioDispatcher) {
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)
} catch (e: Exception) {
Timber.e(e)
Result.failure(WgTunnelExceptions.InvalidQrCode())
}
} }
} }
private suspend fun makeTunnelNameUnique(name: String): String { private suspend fun saveTunnelFromConfUri(name: String, uri: Uri): Result<Unit> {
return withContext(ioDispatcher) { val stream = getInputStreamFromUri(uri)
val tunnels = appDataRepository.tunnels.getAll() return if (stream != null) {
var tunnelName = name saveTunnelConfigFromStream(stream, name)
var num = 1 Result.Success(Unit)
while (tunnels.any { it.name == tunnelName }) { } else {
tunnelName = name + "(${num})" Result.Error(Event.Error.FileReadFailed)
num++
}
tunnelName
} }
} }
private fun saveTunnelConfigFromStream( private suspend fun addTunnel(tunnelConfig: TunnelConfig) {
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()
}
}
}
viewModelScope.launch {
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 withContext(ioDispatcher) {
try {
if (isValidUriContentScheme(uri)) {
val fileName = getFileName(context, uri)
return@withContext 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())
}
} else {
Result.failure(WgTunnelExceptions.InvalidFileExtension())
}
} catch (e: Exception) {
Timber.e(e)
Result.failure(WgTunnelExceptions.FileReadFailed())
}
}
}
private suspend fun saveTunnelsFromZipUri(
uri: Uri,
configType: ConfigType,
context: Context
): Result<Unit> {
return withContext(ioDispatcher) {
ZipInputStream(getInputStreamFromUri(uri, context)).use { zip ->
generateSequence { zip.nextEntry }
.filterNot {
it.isDirectory ||
getFileExtensionFromFileName(it.name) != Constants.CONF_FILE_EXTENSION
}
.forEach {
val name = getNameFromFileName(it.name)
withContext(viewModelScope.coroutineContext) {
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,
configType: ConfigType,
context: Context
): Result<Unit> {
return withContext(ioDispatcher) {
val stream = getInputStreamFromUri(uri, context)
return@withContext if (stream != null) {
try {
saveTunnelConfigFromStream(stream, name, configType)
} catch (e: Exception) {
return@withContext Result.failure(WgTunnelExceptions.ConfigParseError())
}
Result.success(Unit)
} else {
Result.failure(WgTunnelExceptions.FileReadFailed())
}
}
}
private fun addTunnel(tunnelConfig: TunnelConfig) = viewModelScope.launch {
val firstTunnel = appDataRepository.tunnels.count() == 0 val firstTunnel = appDataRepository.tunnels.count() == 0
saveTunnel(tunnelConfig) saveTunnel(tunnelConfig)
if (firstTunnel) WireGuardAutoTunnel.requestTunnelTileServiceStateUpdate() if (firstTunnel) WireGuardAutoTunnel.requestTunnelTileServiceStateUpdate(application)
} }
fun pauseAutoTunneling() = fun pauseAutoTunneling() =
viewModelScope.launch { viewModelScope.launch {
appDataRepository.settings.save(uiState.value.settings.copy(isAutoTunnelPaused = true)) appDataRepository.settings.save(uiState.value.settings.copy(isAutoTunnelPaused = true))
WireGuardAutoTunnel.requestAutoTunnelTileServiceUpdate() WireGuardAutoTunnel.requestAutoTunnelTileServiceUpdate(application)
} }
fun resumeAutoTunneling() = fun resumeAutoTunneling() =
viewModelScope.launch { viewModelScope.launch {
appDataRepository.settings.save(uiState.value.settings.copy(isAutoTunnelPaused = false)) appDataRepository.settings.save(uiState.value.settings.copy(isAutoTunnelPaused = false))
WireGuardAutoTunnel.requestAutoTunnelTileServiceUpdate() WireGuardAutoTunnel.requestAutoTunnelTileServiceUpdate(application)
} }
private fun saveTunnel(tunnelConfig: TunnelConfig) = viewModelScope.launch { private suspend fun saveTunnel(tunnelConfig: TunnelConfig) {
appDataRepository.tunnels.save(tunnelConfig) appDataRepository.tunnels.save(tunnelConfig)
} }
@@ -365,17 +239,17 @@ constructor(
return fileName.substring(0, fileName.lastIndexOf('.')) return fileName.substring(0, fileName.lastIndexOf('.'))
} }
private fun getFileExtensionFromFileName(fileName: String): String? { private fun getFileExtensionFromFileName(fileName: String): String {
return try { return try {
fileName.substring(fileName.lastIndexOf('.')) fileName.substring(fileName.lastIndexOf('.'))
} catch (e: Exception) { } catch (e: Exception) {
Timber.e(e) Timber.e(e)
null ""
} }
} }
private fun saveSettings(settings: Settings) = private fun saveSettings(settings: Settings) =
viewModelScope.launch { appDataRepository.settings.save(settings) } viewModelScope.launch(Dispatchers.IO) { appDataRepository.settings.save(settings) }
fun onCopyTunnel(tunnel: TunnelConfig?) = viewModelScope.launch { fun onCopyTunnel(tunnel: TunnelConfig?) = viewModelScope.launch {
@@ -1,22 +1,17 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.options package com.zaneschepke.wireguardautotunnel.ui.screens.options
import android.annotation.SuppressLint
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.slideInVertically
import androidx.compose.animation.slideOutVertically
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.focusGroup
import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ExperimentalLayoutApi import androidx.compose.foundation.layout.ExperimentalLayoutApi
import androidx.compose.foundation.layout.FlowRow import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.IntrinsicSize import androidx.compose.foundation.layout.IntrinsicSize
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardActions
@@ -29,10 +24,9 @@ import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Surface import androidx.compose.material3.Surface
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.surfaceColorAtElevation import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
@@ -45,21 +39,15 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontStyle import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardCapitalization import androidx.compose.ui.text.input.KeyboardCapitalization
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.navigation.NavController import androidx.navigation.NavController
import com.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.R
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
import com.zaneschepke.wireguardautotunnel.ui.AppViewModel import com.zaneschepke.wireguardautotunnel.ui.AppViewModel
@@ -67,13 +55,11 @@ import com.zaneschepke.wireguardautotunnel.ui.Screen
import com.zaneschepke.wireguardautotunnel.ui.common.ClickableIconButton import com.zaneschepke.wireguardautotunnel.ui.common.ClickableIconButton
import com.zaneschepke.wireguardautotunnel.ui.common.config.ConfigurationToggle import com.zaneschepke.wireguardautotunnel.ui.common.config.ConfigurationToggle
import com.zaneschepke.wireguardautotunnel.ui.common.text.SectionTitle 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.Constants
import com.zaneschepke.wireguardautotunnel.util.getMessage import com.zaneschepke.wireguardautotunnel.util.Result
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter")
@OptIn(ExperimentalLayoutApi::class) @OptIn(ExperimentalLayoutApi::class)
@Composable @Composable
fun OptionsScreen( fun OptionsScreen(
@@ -85,8 +71,6 @@ fun OptionsScreen(
) { ) {
val scrollState = rememberScrollState() val scrollState = rememberScrollState()
val uiState by optionsViewModel.uiState.collectAsStateWithLifecycle() val uiState by optionsViewModel.uiState.collectAsStateWithLifecycle()
val context = LocalContext.current
val interactionSource = remember { MutableInteractionSource() } val interactionSource = remember { MutableInteractionSource() }
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
val focusManager = LocalFocusManager.current val focusManager = LocalFocusManager.current
@@ -106,254 +90,198 @@ fun OptionsScreen(
fun saveTrustedSSID() { fun saveTrustedSSID() {
if (currentText.isNotEmpty()) { if (currentText.isNotEmpty()) {
scope.launch { scope.launch {
optionsViewModel.onSaveRunSSID(currentText).onSuccess { optionsViewModel.onSaveRunSSID(currentText).let {
currentText = "" when (it) {
}.onFailure { is Result.Success -> currentText = ""
appViewModel.showSnackbarMessage(it.getMessage(context)) is Result.Error -> appViewModel.showSnackbarMessage(it.error.message)
}
}
}
}
Scaffold(
floatingActionButton = {
val secondaryColor = MaterialTheme.colorScheme.secondary
val tvFobColor = MaterialTheme.colorScheme.surfaceColorAtElevation(2.dp)
val fobColor =
if (WireGuardAutoTunnel.isRunningOnAndroidTv()) tvFobColor else secondaryColor
val fobIconColor =
if (WireGuardAutoTunnel.isRunningOnAndroidTv()) Color.White else MaterialTheme.colorScheme.background
AnimatedVisibility(
visible = true,
enter = slideInVertically(initialOffsetY = { it * 2 }),
exit = slideOutVertically(targetOffsetY = { it * 2 }),
modifier = Modifier
.focusRequester(focusRequester)
.focusGroup(),
) {
MultiFloatingActionButton(
fabIcon = FabIcon(
iconRes = R.drawable.edit,
iconResAfterRotate = R.drawable.close,
iconRotate = 180f,
),
fabOption = FabOption(
iconTint = fobIconColor,
backgroundTint = fobColor,
),
itemsMultiFab = listOf(
MultiFabItem(
label = {
Text(
stringResource(id = R.string.amnezia),
color = Color.White,
textAlign = TextAlign.Center,
modifier = Modifier.padding(end = 10.dp),
)
},
modifier = Modifier
.size(40.dp),
icon = R.drawable.edit,
value = ConfigType.AMNEZIA.name,
miniFabOption = FabOption(
backgroundTint = fobColor,
fobIconColor,
),
),
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,
miniFabOption = FabOption(
backgroundTint = fobColor,
fobIconColor,
),
),
),
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),
) {
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.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() },
)
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.fillMaxSize()
.padding(top = 5.dp),
horizontalArrangement = Arrangement.Center,
) {
TextButton(
onClick = {
navController.navigate(
"${Screen.Config.route}/${tunnelId}",
)
},
) {
Text(stringResource(R.string.edit_tunnel))
}
}
}
}
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.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 package com.zaneschepke.wireguardautotunnel.ui.screens.options
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
data class OptionsUiState( data class OptionsUiState(
val id: String? = null, val id: String? = null,
@@ -4,16 +4,17 @@ import androidx.compose.ui.util.fastFirstOrNull
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
import com.zaneschepke.wireguardautotunnel.util.Constants import com.zaneschepke.wireguardautotunnel.util.Constants
import com.zaneschepke.wireguardautotunnel.util.WgTunnelExceptions import com.zaneschepke.wireguardautotunnel.util.Event
import com.zaneschepke.wireguardautotunnel.util.Result
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import javax.inject.Inject import javax.inject.Inject
@@ -42,14 +43,12 @@ constructor(
) )
fun init(tunnelId: String) { fun init(tunnelId: String) {
_optionState.update { _optionState.value = _optionState.value.copy(
it.copy( id = tunnelId,
id = tunnelId, )
)
}
} }
fun onDeleteRunSSID(ssid: String) = viewModelScope.launch { fun onDeleteRunSSID(ssid: String) = viewModelScope.launch(Dispatchers.IO) {
uiState.value.tunnel?.let { uiState.value.tunnel?.let {
appDataRepository.tunnels.save( appDataRepository.tunnels.save(
tunnelConfig = it.copy( tunnelConfig = it.copy(
@@ -59,7 +58,7 @@ constructor(
} }
} }
private fun saveTunnel(tunnelConfig: TunnelConfig?) = viewModelScope.launch { private fun saveTunnel(tunnelConfig: TunnelConfig?) = viewModelScope.launch(Dispatchers.IO) {
tunnelConfig?.let { tunnelConfig?.let {
appDataRepository.tunnels.save(it) appDataRepository.tunnels.save(it)
} }
@@ -74,13 +73,13 @@ constructor(
tunnelsWithName.isEmpty()) { tunnelsWithName.isEmpty()) {
uiState.value.tunnel?.tunnelNetworks?.add(trimmed) uiState.value.tunnel?.tunnelNetworks?.add(trimmed)
saveTunnel(uiState.value.tunnel) saveTunnel(uiState.value.tunnel)
Result.success(Unit) Result.Success(Unit)
} else { } else {
Result.failure(WgTunnelExceptions.SsidConflict()) Result.Error(Event.Error.SsidConflict)
} }
} }
fun onToggleIsMobileDataTunnel() = viewModelScope.launch { fun onToggleIsMobileDataTunnel() = viewModelScope.launch(Dispatchers.IO) {
uiState.value.tunnel?.let { uiState.value.tunnel?.let {
if (it.isMobileDataTunnel) { if (it.isMobileDataTunnel) {
appDataRepository.tunnels.updateMobileDataTunnel(null) appDataRepository.tunnels.updateMobileDataTunnel(null)
@@ -88,7 +87,7 @@ constructor(
} }
} }
fun onTogglePrimaryTunnel() = viewModelScope.launch { fun onTogglePrimaryTunnel() = viewModelScope.launch(Dispatchers.IO) {
if (uiState.value.tunnel != null) { if (uiState.value.tunnel != null) {
appDataRepository.tunnels.updatePrimaryTunnel( appDataRepository.tunnels.updatePrimaryTunnel(
when (uiState.value.isDefaultTunnel) { when (uiState.value.isDefaultTunnel) {
@@ -96,7 +95,7 @@ constructor(
false -> uiState.value.tunnel false -> uiState.value.tunnel
}, },
) )
WireGuardAutoTunnel.requestTunnelTileServiceStateUpdate() WireGuardAutoTunnel.requestTunnelTileServiceStateUpdate(WireGuardAutoTunnel.instance)
} }
} }
} }
@@ -26,6 +26,7 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardActions
@@ -44,7 +45,6 @@ import androidx.compose.material3.Surface
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextButton import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.SideEffect import androidx.compose.runtime.SideEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
@@ -73,17 +73,21 @@ import com.google.accompanist.permissions.isGranted
import com.google.accompanist.permissions.rememberPermissionState import com.google.accompanist.permissions.rememberPermissionState
import com.zaneschepke.wireguardautotunnel.R import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelState
import com.zaneschepke.wireguardautotunnel.ui.AppViewModel import com.zaneschepke.wireguardautotunnel.ui.AppViewModel
import com.zaneschepke.wireguardautotunnel.ui.Screen import com.zaneschepke.wireguardautotunnel.ui.Screen
import com.zaneschepke.wireguardautotunnel.ui.common.ClickableIconButton import com.zaneschepke.wireguardautotunnel.ui.common.ClickableIconButton
import com.zaneschepke.wireguardautotunnel.ui.common.config.ConfigurationToggle import com.zaneschepke.wireguardautotunnel.ui.common.config.ConfigurationToggle
import com.zaneschepke.wireguardautotunnel.ui.common.prompt.AuthorizationPrompt import com.zaneschepke.wireguardautotunnel.ui.common.prompt.AuthorizationPrompt
import com.zaneschepke.wireguardautotunnel.ui.common.text.SectionTitle import com.zaneschepke.wireguardautotunnel.ui.common.text.SectionTitle
import com.zaneschepke.wireguardautotunnel.util.getMessage import com.zaneschepke.wireguardautotunnel.util.Event
import com.zaneschepke.wireguardautotunnel.util.FileUtils
import com.zaneschepke.wireguardautotunnel.util.Result
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.amnezia.awg.backend.AwgQuickBackend
import org.amnezia.awg.backend.Tunnel
import timber.log.Timber import timber.log.Timber
import xyz.teamgravity.pin_lock_compose.PinManager
import java.io.File import java.io.File
@OptIn( @OptIn(
@@ -97,15 +101,14 @@ fun SettingsScreen(
navController: NavController, navController: NavController,
focusRequester: FocusRequester, focusRequester: FocusRequester,
) { ) {
val scope = rememberCoroutineScope { Dispatchers.IO }
val context = LocalContext.current val context = LocalContext.current
val focusManager = LocalFocusManager.current val focusManager = LocalFocusManager.current
val scope = rememberCoroutineScope()
val scrollState = rememberScrollState() val scrollState = rememberScrollState()
val interactionSource = remember { MutableInteractionSource() } val interactionSource = remember { MutableInteractionSource() }
//val pinExists = remember { mutableStateOf(PinManager.pinExists()) } val pinExists = remember { mutableStateOf(PinManager.pinExists()) }
val uiState by viewModel.uiState.collectAsStateWithLifecycle() val uiState by viewModel.uiState.collectAsStateWithLifecycle()
val kernelSupport by viewModel.kernelSupport.collectAsStateWithLifecycle()
val fineLocationState = rememberPermissionState(Manifest.permission.ACCESS_FINE_LOCATION) val fineLocationState = rememberPermissionState(Manifest.permission.ACCESS_FINE_LOCATION)
var currentText by remember { mutableStateOf("") } var currentText by remember { mutableStateOf("") }
@@ -117,10 +120,6 @@ fun SettingsScreen(
val screenPadding = 5.dp val screenPadding = 5.dp
val fillMaxWidth = .85f val fillMaxWidth = .85f
LaunchedEffect(Unit) {
viewModel.checkKernelSupport()
}
val startForResult = val startForResult =
rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) { result: ActivityResult -> rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) { result: ActivityResult ->
if (result.resultCode == Activity.RESULT_OK) { if (result.resultCode == Activity.RESULT_OK) {
@@ -132,32 +131,16 @@ fun SettingsScreen(
fun exportAllConfigs() { fun exportAllConfigs() {
try { try {
val wgFiles = uiState.tunnels.map { config -> val files = uiState.tunnels.map { File(context.cacheDir, "${it.name}.conf") }
val file = File(context.cacheDir, "${config.name}-wg.conf") files.forEachIndexed { index, file ->
file.outputStream().use { file.outputStream().use { it.write(uiState.tunnels[index].wgQuick.toByteArray()) }
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
}
scope.launch {
viewModel.onExportTunnels(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) { } catch (e: Exception) {
Timber.e(e) Timber.e(e)
appViewModel.showSnackbarMessage(Event.Error.Exception(e).message)
} }
} }
@@ -178,7 +161,7 @@ fun SettingsScreen(
fun handleAutoTunnelToggle() { fun handleAutoTunnelToggle() {
if (uiState.isBatteryOptimizeDisableShown || isBatteryOptimizationsDisabled()) { if (uiState.isBatteryOptimizeDisableShown || isBatteryOptimizationsDisabled()) {
if (appViewModel.isRequiredPermissionGranted()) { if (appViewModel.isRequiredPermissionGranted()) {
viewModel.onToggleAutoTunnel(context) viewModel.onToggleAutoTunnel()
} }
} else { } else {
requestBatteryOptimizationsDisabled() requestBatteryOptimizationsDisabled()
@@ -187,18 +170,21 @@ fun SettingsScreen(
fun saveTrustedSSID() { fun saveTrustedSSID() {
if (currentText.isNotEmpty()) { if (currentText.isNotEmpty()) {
viewModel.onSaveTrustedSSID(currentText).onSuccess { viewModel.onSaveTrustedSSID(currentText).let {
currentText = "" when (it) {
}.onFailure { is Result.Success -> currentText = ""
appViewModel.showSnackbarMessage(it.getMessage(context)) is Result.Error -> appViewModel.showSnackbarMessage(it.error.message)
}
} }
} }
} }
fun openSettings() { fun openSettings() {
val intentSettings = Intent(ACTION_APPLICATION_DETAILS_SETTINGS) scope.launch {
intentSettings.data = Uri.fromParts("package", context.packageName, null) val intentSettings = Intent(ACTION_APPLICATION_DETAILS_SETTINGS)
context.startActivity(intentSettings) intentSettings.data = Uri.fromParts("package", context.packageName, null)
context.startActivity(intentSettings)
}
} }
fun checkFineLocationGranted() { fun checkFineLocationGranted() {
@@ -322,11 +308,11 @@ fun SettingsScreen(
}, },
onError = { _ -> onError = { _ ->
showAuthPrompt = false showAuthPrompt = false
appViewModel.showSnackbarMessage(context.getString(R.string.error_authentication_failed)) appViewModel.showSnackbarMessage(Event.Error.AuthenticationFailed.message)
}, },
onFailure = { onFailure = {
showAuthPrompt = false showAuthPrompt = false
appViewModel.showSnackbarMessage(context.getString(R.string.error_authorization_failed)) appViewModel.showSnackbarMessage(Event.Error.AuthorizationFailed.message)
}, },
) )
} }
@@ -534,12 +520,12 @@ fun SettingsScreen(
when (false) { when (false) {
isBackgroundLocationGranted -> isBackgroundLocationGranted ->
appViewModel.showSnackbarMessage( appViewModel.showSnackbarMessage(
context.getString(R.string.background_location_required), Event.Error.BackgroundLocationRequired.message,
) )
fineLocationState.status.isGranted -> fineLocationState.status.isGranted ->
appViewModel.showSnackbarMessage( appViewModel.showSnackbarMessage(
context.getString(R.string.precise_location_required), Event.Error.PreciseLocationRequired.message,
) )
viewModel.isLocationEnabled(context) -> viewModel.isLocationEnabled(context) ->
@@ -565,49 +551,38 @@ fun SettingsScreen(
} }
} }
} }
Surface( if (AwgQuickBackend.hasKernelSupport()) {
tonalElevation = 2.dp, Surface(
shadowElevation = 2.dp, tonalElevation = 2.dp,
shape = RoundedCornerShape(12.dp), shadowElevation = 2.dp,
color = MaterialTheme.colorScheme.surface, shape = RoundedCornerShape(12.dp),
modifier = Modifier color = MaterialTheme.colorScheme.surface,
.fillMaxWidth(fillMaxWidth) modifier = Modifier
.padding(vertical = 10.dp), .fillMaxWidth(fillMaxWidth)
) { .padding(vertical = 10.dp),
Column(
horizontalAlignment = Alignment.Start,
verticalArrangement = Arrangement.Top,
modifier = Modifier.padding(15.dp),
) { ) {
SectionTitle( Column(
title = stringResource(id = R.string.backend), horizontalAlignment = Alignment.Start,
padding = screenPadding, verticalArrangement = Arrangement.Top,
) modifier = Modifier.padding(15.dp),
ConfigurationToggle( ) {
stringResource(R.string.use_amnezia), SectionTitle(
enabled = title = stringResource(id = R.string.kernel),
!(uiState.settings.isAutoTunnelEnabled || padding = screenPadding,
uiState.settings.isAlwaysOnVpnEnabled || )
(uiState.vpnState.status == TunnelState.UP) || uiState.settings.isKernelEnabled),
checked = uiState.settings.isAmneziaEnabled,
padding = screenPadding,
onCheckChanged = {
viewModel.onToggleAmnezia()
},
)
if (kernelSupport) {
ConfigurationToggle( ConfigurationToggle(
stringResource(R.string.use_kernel), stringResource(R.string.use_kernel),
enabled = enabled =
!(uiState.settings.isAutoTunnelEnabled || !(uiState.settings.isAutoTunnelEnabled ||
uiState.settings.isAlwaysOnVpnEnabled || uiState.settings.isAlwaysOnVpnEnabled ||
(uiState.vpnState.status == TunnelState.UP)), (uiState.vpnState.status == Tunnel.State.UP)),
checked = uiState.settings.isKernelEnabled, checked = uiState.settings.isKernelEnabled,
padding = screenPadding, padding = screenPadding,
onCheckChanged = { onCheckChanged = {
scope.launch { viewModel.onToggleKernelMode().let {
viewModel.onToggleKernelMode().onFailure { when (it) {
appViewModel.showSnackbarMessage(it.getMessage(context)) is Result.Error -> appViewModel.showSnackbarMessage(it.error.message)
is Result.Success -> {}
} }
} }
}, },
@@ -651,25 +626,16 @@ fun SettingsScreen(
onCheckChanged = { viewModel.onToggleShortcutsEnabled() }, onCheckChanged = { viewModel.onToggleShortcutsEnabled() },
) )
} }
ConfigurationToggle(
stringResource(R.string.restart_at_boot),
enabled = true,
checked = uiState.settings.isRestoreOnBootEnabled,
padding = screenPadding,
onCheckChanged = {
viewModel.onToggleRestartAtBoot()
},
)
ConfigurationToggle( ConfigurationToggle(
stringResource(R.string.enable_app_lock), stringResource(R.string.enable_app_lock),
enabled = true, enabled = true,
checked = uiState.isPinLockEnabled, checked = pinExists.value,
padding = screenPadding, padding = screenPadding,
onCheckChanged = { onCheckChanged = {
if (uiState.isPinLockEnabled) { if (pinExists.value) {
viewModel.onPinLockDisabled() PinManager.clearPin()
pinExists.value = PinManager.pinExists()
} else { } else {
viewModel.onPinLockEnabled()
navController.navigate(Screen.Lock.route) navController.navigate(Screen.Lock.route)
} }
}, },
@@ -1,7 +1,7 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.settings package com.zaneschepke.wireguardautotunnel.ui.screens.settings
import com.zaneschepke.wireguardautotunnel.data.domain.Settings import com.zaneschepke.wireguardautotunnel.data.model.Settings
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnState import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnState
data class SettingsUiState( data class SettingsUiState(
@@ -9,6 +9,5 @@ data class SettingsUiState(
val tunnels: List<TunnelConfig> = emptyList(), val tunnels: List<TunnelConfig> = emptyList(),
val vpnState: VpnState = VpnState(), val vpnState: VpnState = VpnState(),
val isLocationDisclosureShown: Boolean = true, val isLocationDisclosureShown: Boolean = true,
val isBatteryOptimizeDisableShown: Boolean = false, val isBatteryOptimizeDisableShown: Boolean = false
val isPinLockEnabled: Boolean = false
) )
@@ -1,52 +1,39 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.settings package com.zaneschepke.wireguardautotunnel.ui.screens.settings
import android.app.Application
import android.content.Context import android.content.Context
import android.location.LocationManager import android.location.LocationManager
import androidx.core.location.LocationManagerCompat import androidx.core.location.LocationManagerCompat
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.wireguard.android.backend.WgQuickBackend
import com.wireguard.android.util.RootShell
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
import com.zaneschepke.wireguardautotunnel.data.domain.Settings import com.zaneschepke.wireguardautotunnel.data.model.Settings
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
import com.zaneschepke.wireguardautotunnel.module.IoDispatcher
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnService import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnService
import com.zaneschepke.wireguardautotunnel.util.Constants import com.zaneschepke.wireguardautotunnel.util.Constants
import com.zaneschepke.wireguardautotunnel.util.FileUtils import com.zaneschepke.wireguardautotunnel.util.Event
import com.zaneschepke.wireguardautotunnel.util.WgTunnelExceptions import com.zaneschepke.wireguardautotunnel.util.Result
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import org.amnezia.awg.util.RootShell
import timber.log.Timber import timber.log.Timber
import xyz.teamgravity.pin_lock_compose.PinManager
import java.io.File
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Provider
@HiltViewModel @HiltViewModel
class SettingsViewModel class SettingsViewModel
@Inject @Inject
constructor( constructor(
private val application: Application,
private val appDataRepository: AppDataRepository, private val appDataRepository: AppDataRepository,
private val serviceManager: ServiceManager, private val serviceManager: ServiceManager,
private val rootShell: Provider<RootShell>, private val rootShell: RootShell,
private val fileUtils: FileUtils,
@IoDispatcher private val ioDispatcher: CoroutineDispatcher,
vpnService: VpnService vpnService: VpnService
) : ViewModel() { ) : ViewModel() {
private val _kernelSupport = MutableStateFlow(false)
val kernelSupport = _kernelSupport.asStateFlow()
val uiState = val uiState =
combine( combine(
appDataRepository.settings.getSettingsFlow(), appDataRepository.settings.getSettingsFlow(),
@@ -58,9 +45,8 @@ constructor(
settings, settings,
tunnels, tunnels,
tunnelState, tunnelState,
generalState.isLocationDisclosureShown, generalState.locationDisclosureShown,
generalState.isBatteryOptimizationDisableShown, generalState.batteryOptimizationDisableShown,
generalState.isPinLockEnabled,
) )
} }
.stateIn( .stateIn(
@@ -74,9 +60,9 @@ constructor(
return if (!uiState.value.settings.trustedNetworkSSIDs.contains(trimmed)) { return if (!uiState.value.settings.trustedNetworkSSIDs.contains(trimmed)) {
uiState.value.settings.trustedNetworkSSIDs.add(trimmed) uiState.value.settings.trustedNetworkSSIDs.add(trimmed)
saveSettings(uiState.value.settings) saveSettings(uiState.value.settings)
Result.success(Unit) Result.Success(Unit)
} else { } else {
Result.failure(WgTunnelExceptions.SsidConflict()) Result.Error(Event.Error.SsidConflict)
} }
} }
@@ -107,19 +93,15 @@ constructor(
) )
} }
suspend fun onExportTunnels(files: List<File>): Result<Unit> { fun onToggleAutoTunnel() =
return fileUtils.saveFilesToZip(files)
}
fun onToggleAutoTunnel(context: Context) =
viewModelScope.launch { viewModelScope.launch {
val isAutoTunnelEnabled = uiState.value.settings.isAutoTunnelEnabled val isAutoTunnelEnabled = uiState.value.settings.isAutoTunnelEnabled
var isAutoTunnelPaused = uiState.value.settings.isAutoTunnelPaused var isAutoTunnelPaused = uiState.value.settings.isAutoTunnelPaused
if (isAutoTunnelEnabled) { if (isAutoTunnelEnabled) {
serviceManager.stopWatcherService(context) serviceManager.stopWatcherService(application)
} else { } else {
serviceManager.startWatcherService(context) serviceManager.startWatcherService(application)
isAutoTunnelPaused = false isAutoTunnelPaused = false
} }
saveSettings( saveSettings(
@@ -128,7 +110,7 @@ constructor(
isAutoTunnelPaused = isAutoTunnelPaused, isAutoTunnelPaused = isAutoTunnelPaused,
), ),
) )
WireGuardAutoTunnel.requestAutoTunnelTileServiceUpdate() WireGuardAutoTunnel.requestAutoTunnelTileServiceUpdate(application)
} }
fun onToggleAlwaysOnVPN() = fun onToggleAlwaysOnVPN() =
@@ -180,44 +162,21 @@ constructor(
) )
} }
fun onToggleAmnezia() = viewModelScope.launch { fun onToggleKernelMode(): Result<Unit> {
if (uiState.value.settings.isKernelEnabled) { if (!uiState.value.settings.isKernelEnabled) {
saveKernelMode(false) try {
} rootShell.start()
saveAmneziaMode(!uiState.value.settings.isAmneziaEnabled) Timber.i("Root shell accepted!")
} saveKernelMode(on = true)
} catch (e: RootShell.RootShellException) {
private fun saveAmneziaMode(on: Boolean) { Timber.e(e)
saveSettings(
uiState.value.settings.copy(
isAmneziaEnabled = on,
),
)
}
suspend fun onToggleKernelMode(): Result<Unit> {
return withContext(ioDispatcher) {
if (!uiState.value.settings.isKernelEnabled) {
try {
rootShell.get().start()
Timber.i("Root shell accepted!")
saveSettings(
uiState.value.settings.copy(
isKernelEnabled = true,
isAmneziaEnabled = false,
),
)
} catch (e: RootShell.RootShellException) {
Timber.e(e)
saveKernelMode(on = false)
return@withContext Result.failure(WgTunnelExceptions.RootDenied())
}
} else {
saveKernelMode(on = false) saveKernelMode(on = false)
return Result.Error(Event.Error.RootDenied)
} }
Result.success(Unit) } else {
saveKernelMode(on = false)
} }
return Result.Success(Unit)
} }
fun onToggleRestartOnPing() = viewModelScope.launch { fun onToggleRestartOnPing() = viewModelScope.launch {
@@ -227,31 +186,4 @@ constructor(
), ),
) )
} }
fun checkKernelSupport() = viewModelScope.launch {
val kernelSupport = withContext(ioDispatcher) {
WgQuickBackend.hasKernelSupport()
}
_kernelSupport.update {
kernelSupport
}
}
fun onPinLockDisabled() = viewModelScope.launch {
PinManager.clearPin()
appDataRepository.appState.setPinLockEnabled(false)
}
fun onPinLockEnabled() = viewModelScope.launch {
PinManager.initialize(WireGuardAutoTunnel.instance)
appDataRepository.appState.setPinLockEnabled(true)
}
fun onToggleRestartAtBoot() = viewModelScope.launch {
saveSettings(
uiState.value.settings.copy(
isRestoreOnBootEnabled = !uiState.value.settings.isRestoreOnBootEnabled
)
)
}
} }
@@ -107,12 +107,7 @@ fun SupportScreen(
modifier = Modifier.padding(bottom = 20.dp), modifier = Modifier.padding(bottom = 20.dp),
) )
TextButton( TextButton(
onClick = { onClick = { appViewModel.openWebPage(context.resources.getString(R.string.docs_url)) },
appViewModel.openWebPage(
context.resources.getString(R.string.docs_url),
context,
)
},
modifier = Modifier modifier = Modifier
.padding(vertical = 5.dp) .padding(vertical = 5.dp)
.focusRequester(focusRequester), .focusRequester(focusRequester),
@@ -134,7 +129,7 @@ fun SupportScreen(
weight = 1.0f, weight = 1.0f,
fill = false, fill = false,
), ),
softWrap = true, softWrap = true
) )
} }
Icon( Icon(
@@ -148,12 +143,7 @@ fun SupportScreen(
color = MaterialTheme.colorScheme.onBackground, color = MaterialTheme.colorScheme.onBackground,
) )
TextButton( TextButton(
onClick = { onClick = { appViewModel.openWebPage(context.resources.getString(R.string.discord_url)) },
appViewModel.openWebPage(
context.resources.getString(R.string.telegram_url),
context,
)
},
modifier = Modifier.padding(vertical = 5.dp), modifier = Modifier.padding(vertical = 5.dp),
) { ) {
Row( Row(
@@ -162,7 +152,7 @@ fun SupportScreen(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
) { ) {
Row { Row {
val icon = ImageVector.vectorResource(R.drawable.telegram) val icon = ImageVector.vectorResource(R.drawable.discord)
Icon( Icon(
icon, icon,
icon.name, icon.name,
@@ -185,12 +175,7 @@ fun SupportScreen(
color = MaterialTheme.colorScheme.onBackground, color = MaterialTheme.colorScheme.onBackground,
) )
TextButton( TextButton(
onClick = { onClick = { appViewModel.openWebPage(context.resources.getString(R.string.github_url)) },
appViewModel.openWebPage(
context.resources.getString(R.string.github_url),
context,
)
},
modifier = Modifier.padding(vertical = 5.dp), modifier = Modifier.padding(vertical = 5.dp),
) { ) {
Row( Row(
@@ -222,7 +207,7 @@ fun SupportScreen(
color = MaterialTheme.colorScheme.onBackground, color = MaterialTheme.colorScheme.onBackground,
) )
TextButton( TextButton(
onClick = { appViewModel.launchEmail(context) }, onClick = { appViewModel.launchEmail() },
modifier = Modifier.padding(vertical = 5.dp), modifier = Modifier.padding(vertical = 5.dp),
) { ) {
Row( Row(
@@ -284,10 +269,7 @@ fun SupportScreen(
fontSize = 16.sp, fontSize = 16.sp,
modifier = modifier =
Modifier.clickable { Modifier.clickable {
appViewModel.openWebPage( appViewModel.openWebPage(context.resources.getString(R.string.privacy_policy_url))
context.resources.getString(R.string.privacy_policy_url),
context,
)
}, },
) )
Row( Row(
@@ -303,7 +285,7 @@ fun SupportScreen(
val mode = buildAnnotatedString { val mode = buildAnnotatedString {
append(stringResource(R.string.mode)) append(stringResource(R.string.mode))
append(": ") append(": ")
when (uiState.settings.isKernelEnabled) { when(uiState.settings.isKernelEnabled){
true -> append(stringResource(id = R.string.kernel)) true -> append(stringResource(id = R.string.kernel))
false -> append(stringResource(id = R.string.userspace)) false -> append(stringResource(id = R.string.userspace))
} }
@@ -1,5 +1,5 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.support package com.zaneschepke.wireguardautotunnel.ui.screens.support
import com.zaneschepke.wireguardautotunnel.data.domain.Settings import com.zaneschepke.wireguardautotunnel.data.model.Settings
data class SupportUiState(val settings: Settings = Settings()) data class SupportUiState(val settings: Settings = Settings())
@@ -1,7 +1,6 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.support.logs package com.zaneschepke.wireguardautotunnel.ui.screens.support.logs
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.widget.Toast
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
@@ -9,7 +8,7 @@ import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
@@ -28,24 +27,21 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.ClipboardManager import androidx.compose.ui.platform.ClipboardManager
import androidx.compose.ui.platform.LocalClipboardManager import androidx.compose.ui.platform.LocalClipboardManager
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import androidx.hilt.navigation.compose.hiltViewModel import com.zaneschepke.wireguardautotunnel.ui.AppViewModel
import com.zaneschepke.logcatter.model.LogMessage
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.ui.common.text.LogTypeLabel import com.zaneschepke.wireguardautotunnel.ui.common.text.LogTypeLabel
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter") @SuppressLint("UnusedMaterial3ScaffoldPaddingParameter")
@Composable @Composable
fun LogsScreen(viewModel: LogsViewModel = hiltViewModel()) { fun LogsScreen(appViewModel: AppViewModel) {
val logs = viewModel.logs val logs = remember {
appViewModel.logs
val context = LocalContext.current }
val lazyColumnListState = rememberLazyListState() val lazyColumnListState = rememberLazyListState()
val clipboardManager: ClipboardManager = LocalClipboardManager.current val clipboardManager: ClipboardManager = LocalClipboardManager.current
@@ -61,15 +57,7 @@ fun LogsScreen(viewModel: LogsViewModel = hiltViewModel()) {
floatingActionButton = { floatingActionButton = {
FloatingActionButton( FloatingActionButton(
onClick = { onClick = {
scope.launch { appViewModel.saveLogsToFile()
viewModel.saveLogsToFile().onSuccess {
Toast.makeText(
context,
context.getString(R.string.logs_saved),
Toast.LENGTH_SHORT,
).show()
}
}
}, },
shape = RoundedCornerShape(16.dp), shape = RoundedCornerShape(16.dp),
containerColor = MaterialTheme.colorScheme.primary, containerColor = MaterialTheme.colorScheme.primary,
@@ -91,11 +79,7 @@ fun LogsScreen(viewModel: LogsViewModel = hiltViewModel()) {
.fillMaxSize() .fillMaxSize()
.padding(horizontal = 24.dp), .padding(horizontal = 24.dp),
) { ) {
itemsIndexed( items(logs) {
logs,
key = { index, _ -> index },
contentType = { _: Int, _: LogMessage -> null },
) { _, it ->
Row( Row(
horizontalArrangement = Arrangement.spacedBy(5.dp, Alignment.Start), horizontalArrangement = Arrangement.spacedBy(5.dp, Alignment.Start),
verticalAlignment = Alignment.Top, verticalAlignment = Alignment.Top,
@@ -1,55 +0,0 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.support.logs
import androidx.compose.runtime.mutableStateListOf
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.zaneschepke.logcatter.LocalLogCollector
import com.zaneschepke.logcatter.model.LogMessage
import com.zaneschepke.wireguardautotunnel.module.IoDispatcher
import com.zaneschepke.wireguardautotunnel.module.MainDispatcher
import com.zaneschepke.wireguardautotunnel.util.Constants
import com.zaneschepke.wireguardautotunnel.util.FileUtils
import com.zaneschepke.wireguardautotunnel.util.chunked
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.time.Duration
import java.time.Instant
import javax.inject.Inject
@HiltViewModel
class LogsViewModel
@Inject constructor(
private val localLogCollector: LocalLogCollector,
private val fileUtils: FileUtils,
@IoDispatcher private val ioDispatcher: CoroutineDispatcher,
@MainDispatcher private val mainDispatcher: CoroutineDispatcher
) : ViewModel() {
val logs = mutableStateListOf<LogMessage>()
init {
viewModelScope.launch(ioDispatcher) {
localLogCollector.bufferedLogs.chunked(500, Duration.ofSeconds(1)).collect {
withContext(mainDispatcher) {
logs.addAll(it)
}
if (logs.size > Constants.LOG_BUFFER_SIZE) {
withContext(mainDispatcher) {
logs.removeRange(0, (logs.size - Constants.LOG_BUFFER_SIZE).toInt())
}
}
}
}
}
suspend fun saveLogsToFile(): Result<Unit> {
val file = localLogCollector.getLogFile().getOrElse {
return Result.failure(it)
}
val fileContent = fileUtils.readBytesFromFile(file)
val fileName = "${Constants.BASE_LOG_FILE_NAME}-${Instant.now().epochSecond}.txt"
return fileUtils.saveByteArrayToDownloads(fileContent, fileName)
}
}
@@ -2,7 +2,7 @@ package com.zaneschepke.wireguardautotunnel.util
object Constants { object Constants {
const val BASE_LOG_FILE_NAME = "wg_tunnel_logs" const val BASE_LOG_FILE_NAME = "wgtunnel-logs"
const val LOG_BUFFER_SIZE = 3_000L const val LOG_BUFFER_SIZE = 3_000L
const val MANUAL_TUNNEL_CONFIG_ID = "0" const val MANUAL_TUNNEL_CONFIG_ID = "0"
@@ -16,17 +16,16 @@ object Constants {
const val URI_CONTENT_SCHEME = "content" const val URI_CONTENT_SCHEME = "content"
const val ALLOWED_FILE_TYPES = "*/*" const val ALLOWED_FILE_TYPES = "*/*"
const val TEXT_MIME_TYPE = "text/plain" const val TEXT_MIME_TYPE = "text/plain"
const val ZIP_FILE_MIME_TYPE = "application/zip"
const val GOOGLE_TV_EXPLORER_STUB = "com.google.android.tv.frameworkpackagestubs" const val GOOGLE_TV_EXPLORER_STUB = "com.google.android.tv.frameworkpackagestubs"
const val ANDROID_TV_EXPLORER_STUB = "com.android.tv.frameworkpackagestubs" const val ANDROID_TV_EXPLORER_STUB = "com.android.tv.frameworkpackagestubs"
const val ALWAYS_ON_VPN_ACTION = "android.net.VpnService" const val ALWAYS_ON_VPN_ACTION = "android.net.VpnService"
const val EMAIL_MIME_TYPE = "plain/text" const val EMAIL_MIME_TYPE = "message/rfc822"
const val SYSTEM_EXEMPT_SERVICE_TYPE_ID = 1024 const val SYSTEM_EXEMPT_SERVICE_TYPE_ID = 1024
const val SUBSCRIPTION_TIMEOUT = 5_000L const val SUBSCRIPTION_TIMEOUT = 5_000L
const val FOCUS_REQUEST_DELAY = 500L const val FOCUS_REQUEST_DELAY = 500L
const val DEFAULT_PING_IP = "1.1.1.1" const val BACKUP_PING_HOST = "1.1.1.1"
const val PING_TIMEOUT = 5_000L const val PING_TIMEOUT = 5_000L
const val VPN_RESTART_DELAY = 1_000L const val VPN_RESTART_DELAY = 1_000L
const val PING_INTERVAL = 60_000L const val PING_INTERVAL = 60_000L
@@ -38,7 +37,4 @@ object Constants {
const val UNREADABLE_SSID = "<unknown ssid>" 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 ="
} }
@@ -0,0 +1,117 @@
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,30 +1,40 @@
package com.zaneschepke.wireguardautotunnel.util package com.zaneschepke.wireguardautotunnel.util
import android.content.Context import android.content.BroadcastReceiver
import android.content.pm.PackageInfo import android.content.pm.PackageInfo
import com.zaneschepke.wireguardautotunnel.R import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
import com.zaneschepke.wireguardautotunnel.service.tunnel.HandshakeStatus import com.zaneschepke.wireguardautotunnel.service.tunnel.HandshakeStatus
import com.zaneschepke.wireguardautotunnel.service.tunnel.statistics.TunnelStatistics
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.ObsoleteCoroutinesApi import kotlinx.coroutines.launch
import kotlinx.coroutines.channels.ClosedReceiveChannelException import org.amnezia.awg.backend.Statistics
import kotlinx.coroutines.channels.ReceiveChannel import org.amnezia.awg.crypto.Key
import kotlinx.coroutines.channels.produce
import kotlinx.coroutines.channels.ticker
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.channelFlow
import kotlinx.coroutines.selects.whileSelect
import org.amnezia.awg.config.Config
import timber.log.Timber
import java.math.BigDecimal import java.math.BigDecimal
import java.text.DecimalFormat import java.text.DecimalFormat
import java.time.Duration import kotlin.coroutines.CoroutineContext
import java.util.concurrent.ConcurrentLinkedQueue import kotlin.coroutines.EmptyCoroutineContext
import kotlin.coroutines.cancellation.CancellationException
fun BroadcastReceiver.goAsync(
context: CoroutineContext = EmptyCoroutineContext,
block: suspend CoroutineScope.() -> Unit
) {
val pendingResult = goAsync()
@OptIn(DelicateCoroutinesApi::class) // Must run globally; there's no teardown callback.
GlobalScope.launch(context) {
try {
block()
} finally {
pendingResult.finish()
}
}
}
fun String.truncateWithEllipsis(allowedLength: Int): String {
return if (this.length > allowedLength + 3) {
this.substring(0, allowedLength) + "***"
} else this
}
fun BigDecimal.toThreeDecimalPlaceString(): String { fun BigDecimal.toThreeDecimalPlaceString(): String {
val df = DecimalFormat("#.###") val df = DecimalFormat("#.###")
@@ -39,15 +49,15 @@ typealias TunnelConfigs = List<TunnelConfig>
typealias Packages = List<PackageInfo> typealias Packages = List<PackageInfo>
fun TunnelStatistics.mapPeerStats(): Map<org.amnezia.awg.crypto.Key, TunnelStatistics.PeerStats?> { fun Statistics.mapPeerStats(): Map<Key, Statistics.PeerStats?> {
return this.getPeers().associateWith { key -> (this.peerStats(key)) } return this.peers().associateWith { key -> (this.peer(key)) }
} }
fun TunnelStatistics.PeerStats.latestHandshakeSeconds(): Long? { fun Statistics.PeerStats.latestHandshakeSeconds(): Long? {
return NumberUtils.getSecondsBetweenTimestampAndNow(this.latestHandshakeEpochMillis) return NumberUtils.getSecondsBetweenTimestampAndNow(this.latestHandshakeEpochMillis)
} }
fun TunnelStatistics.PeerStats.handshakeStatus(): HandshakeStatus { fun Statistics.PeerStats.handshakeStatus(): HandshakeStatus {
// TODO add never connected status after duration // TODO add never connected status after duration
return this.latestHandshakeSeconds().let { return this.latestHandshakeSeconds().let {
when { when {
@@ -60,89 +70,3 @@ fun TunnelStatistics.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)
}
}
/**
* Chunks based on a time or size threshold.
*
* Borrowed from this [Stack Overflow question](https://stackoverflow.com/questions/51022533/kotlin-chunk-sequence-based-on-size-and-time).
*/
@OptIn(ObsoleteCoroutinesApi::class, ExperimentalCoroutinesApi::class)
fun <T> ReceiveChannel<T>.chunked(scope: CoroutineScope, size: Int, time: Duration) =
scope.produce<List<T>> {
while (true) { // this loop goes over each chunk
val chunk = ConcurrentLinkedQueue<T>() // current chunk
val ticker = ticker(time.toMillis()) // time-limit for this chunk
try {
whileSelect {
ticker.onReceive {
false // done with chunk when timer ticks, takes priority over received elements
}
this@chunked.onReceive {
chunk += it
chunk.size < size // continue whileSelect if chunk is not full
}
}
} catch (e: ClosedReceiveChannelException) {
Timber.e(e)
return@produce
} finally {
ticker.cancel()
if (chunk.isNotEmpty()) {
send(chunk.toList())
}
}
}
}
@OptIn(DelicateCoroutinesApi::class, ExperimentalCoroutinesApi::class)
fun <T> Flow<T>.chunked(size: Int, time: Duration) = channelFlow {
coroutineScope {
val channel = asChannel(this@chunked).chunked(this, size, time)
try {
while (!channel.isClosedForReceive) {
send(channel.receive())
}
} catch (e: ClosedReceiveChannelException) {
// Channel was closed by the flow completing, nothing to do
Timber.w(e)
} catch (e: CancellationException) {
channel.cancel(e)
throw e
} catch (e: Exception) {
channel.cancel(CancellationException("Closing channel due to flow exception", e))
throw e
}
}
}
@ExperimentalCoroutinesApi
fun <T> CoroutineScope.asChannel(flow: Flow<T>): ReceiveChannel<T> = produce {
flow.collect { value ->
channel.send(value)
}
}
@@ -6,102 +6,19 @@ import android.os.Build
import android.os.Environment import android.os.Environment
import android.provider.MediaStore import android.provider.MediaStore
import android.provider.MediaStore.MediaColumns import android.provider.MediaStore.MediaColumns
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.withContext
import timber.log.Timber
import java.io.File import java.io.File
import java.io.FileInputStream
import java.io.FileOutputStream import java.io.FileOutputStream
import java.io.OutputStream import java.io.OutputStream
import java.time.Instant import java.time.Instant
import java.util.zip.ZipEntry import java.util.zip.ZipEntry
import java.util.zip.ZipOutputStream import java.util.zip.ZipOutputStream
class FileUtils( object FileUtils {
private val context: Context, private const val ZIP_FILE_MIME_TYPE = "application/zip"
private val ioDispatcher: CoroutineDispatcher,
) {
suspend fun readBytesFromFile(file: File): ByteArray {
return withContext(ioDispatcher) {
FileInputStream(file).use {
it.readBytes()
}
}
}
suspend fun readTextFromFileName(fileName: String): String {
return withContext(ioDispatcher) {
context.assets.open(fileName).use { stream ->
stream.bufferedReader(Charsets.UTF_8).use {
it.readText()
}
}
}
}
suspend fun saveByteArrayToDownloads(content: ByteArray, fileName: String): Result<Unit> {
return withContext(ioDispatcher) {
try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
val contentValues =
ContentValues().apply {
put(MediaColumns.DISPLAY_NAME, fileName)
put(MediaColumns.MIME_TYPE, Constants.TEXT_MIME_TYPE)
put(MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_DOWNLOADS)
}
val resolver = context.contentResolver
val uri =
resolver.insert(MediaStore.Downloads.EXTERNAL_CONTENT_URI, contentValues)
if (uri != null) {
resolver.openOutputStream(uri).use { output ->
output?.write(content)
}
}
} else {
val target =
File(
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS),
fileName,
)
FileOutputStream(target).use { output ->
output.write(content)
}
}
Result.success(Unit)
} catch (e: Exception) {
Result.failure(e)
}
}
}
suspend fun saveFilesToZip(files: List<File>): Result<Unit> {
return withContext(ioDispatcher) {
try {
val zipOutputStream =
createDownloadsFileOutputStream(
"wg-export_${Instant.now().epochSecond}.zip",
Constants.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@withContext Result.success(Unit)
}
} catch (e: Exception) {
Timber.e(e)
Result.failure(WgTunnelExceptions.ConfigExportFailed())
}
}
}
//TODO issue with android 9 //TODO issue with android 9
private fun createDownloadsFileOutputStream( private fun createDownloadsFileOutputStream(
context: Context,
fileName: String, fileName: String,
mimeType: String = Constants.ALLOWED_FILE_TYPES mimeType: String = Constants.ALLOWED_FILE_TYPES
): OutputStream? { ): OutputStream? {
@@ -127,4 +44,47 @@ class FileUtils(
} }
return null return null
} }
fun saveFileToDownloads(context: Context, content: String, fileName: String) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
val contentValues = ContentValues().apply {
put(MediaColumns.DISPLAY_NAME, fileName)
put(MediaColumns.MIME_TYPE, Constants.TEXT_MIME_TYPE)
put(MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_DOWNLOADS)
}
val resolver = context.contentResolver
val uri = resolver.insert(MediaStore.Downloads.EXTERNAL_CONTENT_URI, contentValues)
if (uri != null) {
resolver.openOutputStream(uri).use { output ->
output?.write(content.toByteArray())
}
}
} else {
val target = File(
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS),
fileName,
)
FileOutputStream(target).use { output ->
output.write(content.toByteArray())
}
}
}
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) }
}
}
}
}
} }
@@ -0,0 +1,16 @@
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)
}
}
}
}
@@ -1,130 +0,0 @@
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)
}
}
}
-9
View File
@@ -1,9 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:fillColor="#e8eaed"
android:pathData="M440,520L200,520v-80h240v-240h80v240h240v80L520,520v240h-80v-240Z" />
</vector>
-9
View File
@@ -1,9 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:fillColor="#e8eaed"
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" />
</vector>
-9
View File
@@ -1,9 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:fillColor="#e8eaed"
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" />
</vector>
-9
View File
@@ -1,9 +0,0 @@
<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>
-154
View File
@@ -1,154 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="no_tunnels">Žádné tunely ještě nebyly přidány!</string>
<string name="watcher_notification_text_paused">Monitorování změn ve stavu sítě: pozastaveno</string>
<string name="notification_permission_required">Je vyžadováno oprávnění pro oznámení.</string>
<string name="add_trusted_ssid">Přidat název důvěryhodné Wi-Fi</string>
<string name="enable_auto_tunnel">Spustit automatické tunelování</string>
<string name="tunnel_mobile_data">Tunelování na mobilních datech</string>
<string name="one_tunnel_required">Alespoň jeden tunel je vyžadován pro použití této funkce</string>
<string name="privacy_policy">Otevřít zásady soukromí</string>
<string name="okay">OK</string>
<string name="thank_you">Děkujeme za používání WG Tunnel!</string>
<string name="add_tunnels_text">Přidat ze souboru nebo zipu</string>
<string name="open_file">Otevřít soubor</string>
<string name="add_from_qr">Přidat z QR kódu</string>
<string name="qr_scan">QR skenování</string>
<string name="tunnel_name">Název tunelu</string>
<string name="add_tunnel">Přidat tunel</string>
<string name="exclude">Vyloučit</string>
<string name="include">Zahrnout</string>
<string name="save_changes">Uložit</string>
<string name="endpoint">Koncový bod</string>
<string name="vpn_connection_failed">Připojení selhalo</string>
<string name="always_on_vpn_support">Povolit trvalé připojení VPN</string>
<string name="location_services_not_detected">Služby polohy nebyly detekovány</string>
<string name="hint_search_packages">Hledat balíčky</string>
<string name="attempt_connection">Pokus o připojení...</string>
<string name="vpn_starting">VPN se spouští</string>
<string name="vpn_on">VPN zapnuto</string>
<string name="vpn_off">VPN vypnuto</string>
<string name="default_vpn_on">Hlavní VPN zapnuto</string>
<string name="default_vpn_off">Hlavní VPN vypnuto</string>
<string name="create_import">Vytvořit od základu</string>
<string name="turn_off_auto">Akce vyžaduje, aby bylo automatické tunelování vypnuté či pozastavené</string>
<string name="add_peer">Přidat peer</string>
<string name="comma_separated_list">seznam oddělený čárkami</string>
<string name="random">(náhodné)</string>
<string name="optional">(volitelné)</string>
<string name="optional_no_recommend">(volitelné, nedoporučeno)</string>
<string name="preshared_key">Předsdílený klíč</string>
<string name="seconds">vteřin(y)</string>
<string name="cancel">Zrušit</string>
<string name="error_authentication_failed">Autentizace selhala</string>
<string name="error_authorization_failed">Nepovedlo se autorizovat</string>
<string name="enabled_app_shortcuts">Zapnout zkratky</string>
<string name="export_configs">Exportovat konfigurace</string>
<string name="export_configs_failed">Nepovedlo se exportovat konfigurace</string>
<string name="location_services_required">Služby polohy vyžadovány</string>
<string name="background_location_required">Oprávnění pro polohu na pozadí vyžadováno</string>
<string name="precise_location_required">Oprávnění pro přesnou polohu vyžadováno</string>
<string name="unknown_error">Došlo k neznámé chybě</string>
<string name="exported_configs_message">Konfigurace exportovány do stažených souborů</string>
<string name="tunnel_on_wifi">Tunelovat na nedůvěryhodné Wi-Fi</string>
<string name="email_subject">WG Tunnel podpora</string>
<string name="email_chooser">Poslat email…</string>
<string name="use_kernel">Použít kernel modul</string>
<string name="error_ssid_exists">SSID již existuje</string>
<string name="error_root_denied">Oprávnění root zamítnuto</string>
<string name="error_no_file_explorer">Žádný průzkumník souborů není nainstalován</string>
<string name="error_invalid_code">Neplatný QR kód</string>
<string name="error_none">Žádná chyba</string>
<string name="auto_tunnel_title">Služba automatického tunelování</string>
<string name="resume">Obnovit</string>
<string name="active">aktivní</string>
<string name="open_issue">Otevřít případ</string>
<string name="incorrect_pin">PIN je nesprávný</string>
<string name="create_pin">Vytvořte PIN</string>
<string name="set_primary_tunnel">Nastavit jako hlavní tunel</string>
<string name="use_tunnel_on_wifi_name">Použít tunel pro Wi-Fi</string>
<string name="edit_tunnel">Upravit tunel</string>
<string name="disabled">vypnuto</string>
<string name="auto_tun_on">Obnovit automatické tunelování</string>
<string name="auto_tun_off">Pozastavit automatické tunelování</string>
<string name="version">Verze</string>
<string name="mode">Mód</string>
<string name="use_amnezia">"Použít Amnezia userspace "</string>
<string name="junk_packet_count">Junk packet počet</string>
<string name="junk_packet_minimum_size">Junk packet minimální velikost</string>
<string name="junk_packet_maximum_size">Junk packet maximální velikost</string>
<string name="init_packet_junk_size">Init packet junk velikost</string>
<string name="response_packet_junk_size">Response packet junk velikost</string>
<string name="unsure_how">pokud si nejste jisti, jak postupovat</string>
<string name="see_the">Podívejte se na</string>
<string name="getting_started_guide">začátečnickou příručku</string>
<string name="error_file_format">Neplatný formát konfigurace</string>
<string name="error_file_extension">Soubor není ve formátu .conf nebo .zip</string>
<string name="turn_off_tunnel">Akce vyžaduje vypnutí tunelu</string>
<string name="watcher_notification_text_active">Monitorování změn ve stavu sítě: aktivní</string>
<string name="tunnel_on_ethernet">Tunelovat na ethernetu</string>
<string name="tunnel_start_title">VPN připojeno</string>
<string name="prominent_background_location_message">Tato funkce vyžaduje oprávnění pro přístup k poloze na pozadí pro zapnutí monitorování Wi-Fi SSID, i když je aplikace zavřená. Pro více detailů, podívejte se prosím na zásady soukromí umístěné v kategorii Podpora.</string>
<string name="tunnel_start_text">Připojeno k tunelu</string>
<string name="tunnels">Tunely</string>
<string name="disable_auto_tunnel">Zastavit automatické tunelování</string>
<string name="tunnel_all">Tunelovat všechny aplikace</string>
<string name="config_changes_saved">Změny v konfiguraci uloženy.</string>
<string name="icon">Ikona</string>
<string name="no_thanks">Ne, děkuji</string>
<string name="turn_on">Zapnout</string>
<string name="map">Mapa</string>
<string name="public_key">Veřejný klíč</string>
<string name="addresses">Adresy</string>
<string name="dns_servers">DNS servery</string>
<string name="allowed_ips">Povolené IP adresy</string>
<string name="name">Název</string>
<string name="restart">Restartovat tunel</string>
<string name="scanning_qr">Skenování QR</string>
<string name="none">Žádné názvy důvěryhodných Wi-Fi</string>
<string name="other">Ostatní</string>
<string name="auto_tunneling">Automatické tunelování</string>
<string name="turn_on_tunnel">Akce vyžaduje aktivní tunel</string>
<string name="interface_">Rozhraní</string>
<string name="done">Hotovo</string>
<string name="rotate_keys">Rotovat klíče</string>
<string name="private_key">Soukromý klíč</string>
<string name="copy_public_key">Kopírovat veřejný klíč</string>
<string name="base64_key">base64 klíč</string>
<string name="docs_description">Přečíst si dokumentaci</string>
<string name="discord_description">Přidat se ke komunitě</string>
<string name="email_description">Poslat mi email</string>
<string name="support_help_text">Pokud máte potíže, nápady pro zlepšení, nebo se chcete jen zapojit, následující prostředky jsou k dispozici:</string>
<string name="location_services_missing_message">Aplikace nenašla žádné služby polohy zapnuté na Vašem zařízení. Dle Vašeho zařízení, tohle může způsobit, že funkce nedůvěryhodné Wi-Fi nedokáže přečíst jméno připojené Wi-Fi. Chcete i přesto pokračovat?</string>
<string name="delete_tunnel">Smazat tunel</string>
<string name="delete_tunnel_message">Jste si jisti, že chcete smazat tento tunel?</string>
<string name="yes">Ano</string>
<string name="pause">Pozastavit</string>
<string name="paused">pozastaveno</string>
<string name="tunneling_apps">Tunelování aplikací</string>
<string name="all">vše</string>
<string name="included">zahrnuto</string>
<string name="excluded">vyloučeno</string>
<string name="always_on_disabled">Trvalé VPN připojení se pokusilo spustit tunel, ale tato funkce je vypnutá v nastavení.</string>
<string name="no_email_detected">Žádná emailová aplikace nebyla nalezena</string>
<string name="no_browser_detected">Žádný prohlížeč nebyl nalezen</string>
<string name="logs_saved">Logy uloženy do stažených souborů</string>
<string name="read_logs">Přečíst si logy</string>
<string name="config_parse_error">Nepovedlo se vložit konfiguraci</string>
<string name="pin_created">PIN úspěšně vytvořen</string>
<string name="enter_pin">Vložte Váš PIN</string>
<string name="no_wifi_names_configured">Žádné názvy Wi-Fi nebyly nastaveny pro tento tunel</string>
<string name="enable_app_lock">Zapnout zámek aplikace</string>
<string name="restart_on_ping">Restartovat při selhání pingu</string>
<string name="mobile_data_tunnel">Nastavit jako tunel pro mobilní data</string>
<string name="general">Obecné</string>
<string name="settings">Nastavení</string>
<string name="support">Podpora</string>
<string name="app_name">WG Tunnel</string>
<string name="mtu">MTU</string>
<string name="db_name">wg-tunnel-db</string>
<string name="listen_port">Naslouchací port</string>
<string name="auto">(automaticky)</string>
<string name="kernel">Kernel</string>
<string name="backend">Backend</string>
</resources>
+35 -130
View File
@@ -1,41 +1,42 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<string name="app_name">WG Tunnel</string> <string name="app_name">WG Tunnel</string>
<string name="error_file_extension">Datei ist keine .conf oder .zip</string> <string name="error_file_extension">Dies ist keine .conf oder .zip Datei</string>
<string name="no_tunnels">Noch keine Tunnel hinzugefügt!</string> <string name="no_tunnels">Noch keine Tunnel hinzugefügt!</string>
<string name="watcher_notification_text_active">Überwachung der Netzwerkänderungen: aktiv</string> <string name="watcher_notification_text_active">Überwachung der Netzwerkänderungen: aktiv</string>
<string name="tunnels">Tunnel</string> <string name="tunnels">Tunnel</string>
<string name="enable_auto_tunnel">Auto-Tunneln starten</string> <string name="enable_auto_tunnel">Starte automatisches Verbinden</string>
<string name="tunnel_mobile_data">Tunnel für mobile Daten</string> <string name="tunnel_mobile_data">Verbinden im Mobilnetzwerk</string>
<string name="one_tunnel_required">Mindestens ein Tunnel wird für diese Funktion benötigt</string> <string name="one_tunnel_required">Mindestens eine Verbindung wird für diese Funktion benötigt</string>
<string name="privacy_policy">Datenschutzbestimmungen anzeigen</string> <string name="privacy_policy">Siehe Privacy Policy</string>
<string name="disable_auto_tunnel">Auto-Tunneln stoppen</string> <string name="disable_auto_tunnel">Stoppe automatisches Verbinden</string>
<string name="okay">Ok</string> <string name="okay">Okay</string>
<string name="tunnel_on_ethernet">Tunnel für Ethernet</string> <string name="tunnel_on_ethernet">Verbindung über Ethernet</string>
<string name="attempt_connection">Verbindungsversuch..</string> <string name="search_icon">Suchsymbol</string>
<string name="auto_tunneling">Auto-Tunneln</string> <string name="attempt_connection">Versuche zu verbinden..</string>
<string name="auto_tunneling">Automatisches verbinden</string>
<string name="default_vpn_off">Primärer VPN aus</string> <string name="default_vpn_off">Primärer VPN aus</string>
<string name="turn_on_tunnel">Für diese Aktion muss ein aktiver Tunnel bestehen</string> <string name="turn_on_tunnel">Für diese Altion muss eine Verbindung bestehen</string>
<string name="watcher_notification_text_paused">Überwachung der Netzwerkänderungen: pausiert</string> <string name="watcher_notification_text_paused">Überwachung der Netzwerkänderungen: pausiert</string>
<string name="tunnel_start_title">VPN verbunden</string> <string name="tunnel_start_title">VPN verbunden</string>
<string name="tunnel_start_text">Mit Tunnel verbunden</string> <string name="tunnel_start_text">Mit Tunnel verbunden</string>
<string name="notification_permission_required">Benachrichtigungsberechtigung benötigt.</string> <string name="notification_permission_required">Berechtigung für die Benachrichtigung benötigt.</string>
<string name="add_trusted_ssid">Vertrauenswürdigen WLAN-Namen hinzufügen</string> <string name="add_trusted_ssid">Vertrauenswürdiger WiFi-Name hinzugügen</string>
<string name="prominent_background_location_message">Diese Funktion erfordert die Erlaubnis zur Standortbestimmung im Hintergrund, um die Überwachung der WLAN SSID zu ermöglichen, auch wenn die Anwendung geschlossen ist. Weitere Einzelheiten in den Datenschutzbestimmungen, die auf dem Support-Bildschirm verlinkt sind.</string> <string name="prominent_background_location_message">Diese Funktion benötigt Standortberechtigung im Hintergrund um die WiFi SSIDs auch wenn die Applikation geschlossen ist zu überwachen. Für mehr Infos, siehe Privacy Policy auf der Hilfeseite.</string>
<string name="prominent_background_location_title">Vereinbarung der Standortberechtigung im Hintergrund</string> <string name="prominent_background_location_title">Vereinbarung der Standortberechtigung im Hintergrund</string>
<string name="thank_you">Danke fürs Benutzen von WG Tunnel!</string> <string name="thank_you">Danke fürs benutzen von WG Tunnel!</string>
<string name="trusted_ssid_empty_description">SSID eingeben</string> <string name="trusted_ssid_empty_description">SSID eingeben</string>
<string name="trusted_ssid_value_description">SSID bestätigen</string> <string name="trusted_ssid_value_description">SSID bestätigen</string>
<string name="add_tunnels_text">Von Datei oder ZIP hinzufügen</string> <string name="add_tunnels_text">Von Datei oder ZIP hinzufügen</string>
<string name="open_file">Datei öffnen</string> <string name="open_file">Datei geöffnet</string>
<string name="add_from_qr">Über QR-Code hinzufügen</string> <string name="add_from_qr">Von QR-Code hinzufügen</string>
<string name="qr_scan">Scanne QR</string> <string name="qr_scan">Scanne QR</string>
<string name="tunnel_name">Tunnel Name</string> <string name="tunnel_name">Verbindungsname</string>
<string name="add_tunnel">Tunnel hinzufügen</string> <string name="add_tunnel">Verbindung hinzufügen</string>
<string name="exclude">Ausschließen</string> <string name="exclude">Ausgenommen</string>
<string name="include">Einschließen</string> <string name="include">Eingeschlossen</string>
<string name="tunnel_all">Alle Apps tunneln</string> <string name="tunnel_all">Verbinde alle Apps</string>
<string name="config_changes_saved">Konfigurationsänderungen gespeichert.</string> <string name="config_changes_saved">Änderungen der Konfiguration gespeichert.</string>
<string name="save_changes">Speichern</string> <string name="save_changes">Speichern</string>
<string name="icon">Symbol</string> <string name="icon">Symbol</string>
<string name="no_thanks">Nein danke</string> <string name="no_thanks">Nein danke</string>
@@ -49,123 +50,27 @@
<string name="allowed_ips">Erlaubte IPs</string> <string name="allowed_ips">Erlaubte IPs</string>
<string name="endpoint">Endpunkt</string> <string name="endpoint">Endpunkt</string>
<string name="name">Name</string> <string name="name">Name</string>
<string name="restart">Tunnel neustarten</string> <string name="restart">Verbindung neustarten</string>
<string name="vpn_connection_failed">Verbindung fehlgeschlagen</string> <string name="vpn_connection_failed">Verbindung fehlgeschlagen</string>
<string name="always_on_vpn_support">Always-On VPN erlauben</string> <string name="always_on_vpn_support">Erlaube Always-On VPN</string>
<string name="location_services_not_detected">Standortdienste nicht erkannt</string> <string name="location_services_not_detected">Standortdienste Nicht Erkannt</string>
<string name="hint_search_packages">Pakete suchen</string> <string name="hint_search_packages">Suche packete</string>
<string name="clear_icon">Lösche Symbol</string>
<string name="vpn_starting">VPN startet</string> <string name="vpn_starting">VPN startet</string>
<string name="db_name">wg-tunnel-db</string> <string name="db_name">wg-tunnel-db</string>
<string name="scanning_qr">Scanne nach QR</string> <string name="scanning_qr">Scanne nach QR</string>
<string name="none">Keine vertrauenswürdigen WLAN Namen</string> <string name="none">Keine vertrauenswürdigen WiFi Namen</string>
<string name="other">Sonstige</string> <string name="other">Andere</string>
<string name="vpn_on">VPN an</string> <string name="vpn_on">VPN an</string>
<string name="vpn_off">VPN aus</string> <string name="vpn_off">VPN aus</string>
<string name="default_vpn_on">Primärer VPN an</string> <string name="default_vpn_on">Primärer VPN an</string>
<string name="create_import">Von Grund auf neu erstellen</string> <string name="create_import">Starte von Grund auf neu</string>
<string name="turn_off_auto">Für diese Aktion muss Auto-Tunneln ausgeschaltet oder pausiert sein</string> <string name="turn_off_auto">Für diese Aktion muss automatisches Verbinden ausgeschaltet oder pausiert sein</string>
<string name="add_peer">Peer hinzufügen</string> <string name="add_peer">Peer hinzufügen</string>
<string name="done">Erledigt</string> <string name="done">Erledigt</string>
<string name="rotate_keys">Schlüssel rotieren</string> <string name="rotate_keys">Schlüssel ändern</string>
<string name="private_key">Privater Schlüssel</string> <string name="private_key">Privater Schlüssel</string>
<string name="copy_public_key">Öffentlichen Schlüssel kopieren</string> <string name="copy_public_key">Öffentlicher Schlüssel kopieren</string>
<string name="base64_key">base64-Schlüssel</string> <string name="base64_key">base64-Schlüssel</string>
<string name="comma_separated_list">Kommaseparierte Liste</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 aktivieren</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">(optional)</string>
<string name="optional_no_recommend">(Optional, nicht 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">App-Verknüpfungen aktivieren</string>
<string name="exported_configs_message">Konfigurationen in Download Ordner exportiert</string>
<string name="tunnel_on_wifi">Tunnel bei nicht vertrauenswürdigem WLAN</string>
<string name="email_subject">WG Tunnel Unterstützung</string>
<string name="docs_description">Dokumentation lesen</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 verweigert</string>
<string name="error_no_file_explorer">Kein Datei-Explorer installiert</string>
<string name="location_services_missing_message">Die App erkennt keine auf deinem Gerät aktivierten Standortdienste. Je nach Gerät kann dies dazu führen, dass die Funktion \"Nicht vertrauenswürdiges WLAN\" den WLAN-Namen nicht lesen kann. 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">Los</string>
<string name="excluded">ausgeschlossen</string>
<string name="all">Alle</string>
<string name="always_on_disabled">Always-on VPN wollte einen Tunnel starten, aber dieses Feature ist in den Einstellungen deaktiviert.</string>
<string name="no_browser_detected">Keinen Browser erkannt</string>
<string name="open_issue">Issue öffnen</string>
<string name="read_logs">Logs lesen</string>
<string name="auto">(automatisch)</string>
<string name="config_parse_error">Fehler beim Lesen der Konfiguration</string>
<string name="incorrect_pin">PIN nicht korrekt</string>
<string name="pin_created">PIN erfolgreich erstellt</string>
<string name="enter_pin">Deine PIN eingeben</string>
<string name="auto_off">Auto-Tunneln pausieren</string>
<string name="auto_tun_on">Auto-Tunneln fortsetzen</string>
<string name="auto_tun_off">Auto-Tunneln pausieren</string>
<string name="version">Version</string>
<string name="mode">Modus</string>
<string name="userspace">Benutzerfläche</string>
<string name="settings">Einstellungen</string>
<string name="support">Unterstützung</string>
<string name="watcher_channel_id">Wächterkanal</string>
<string name="error_authentication_failed">Anmeldung fehlgeschlagen</string>
<string name="export_configs">Konfigurationen exportieren</string>
<string name="unknown_error">Unbekannter Fehler aufgetreten</string>
<string name="email_chooser">Sende eine E-Mail…</string>
<string name="error_authorization_failed">Autorisierung fehlgeschlagen</string>
<string name="location_services_required">Standortdienste erforderlich</string>
<string name="precise_location_required">Genauer Standort erforderlich</string>
<string name="error_invalid_code">Ungültiger QR Code</string>
<string name="error_none">Kein Fehler</string>
<string name="tunneling_apps">Getunnelte Apps</string>
<string name="included">eingeschlossen</string>
<string name="no_email_detected">Keine E-Mail-App erkannt</string>
<string name="logs_saved">Logs im Download Ordner gespeichert</string>
<string name="create_pin">PIN erstellen</string>
<string name="use_tunnel_on_wifi_name">Tunnel für WLAN-Namen verwenden</string>
<string name="no_wifi_names_configured">Keine WLAN-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-Tunneln fortsetzen</string>
<string name="vpn_channel_id">VPN Kanal</string>
<string name="vpn_channel_name">VPN Benachrichtigungskanal</string>
<string name="watcher_channel_name">Wächterbenachrichtigungskanal</string>
<string name="turn_off_tunnel">Aktion erfordert deaktivierten Tunnel</string>
<string name="kernel">Kernel</string>
<string name="use_kernel">Kernelmodul verwenden</string>
<string name="error_ssid_exists">SSID existiert bereits</string>
<string name="use_amnezia">"Amnezia Benutzerumgebung benutzen "</string>
<string name="junk_packet_count">Junk-Paket Anzahl</string>
<string name="junk_packet_maximum_size">Junk-Paket maximale Grösse</string>
<string name="init_packet_junk_size">Initial Junk-Paketgröße</string>
<string name="backend">Backend</string>
<string name="junk_packet_minimum_size">Junk-Paket minimale Grösse</string>
<string name="response_packet_junk_size">Antwort Junk-Paketgröße</string>
<string name="init_packet_magic_header">Initialpaket magic header</string>
<string name="getting_started_guide">Startanleitung erhalten</string>
<string name="transport_packet_magic_header">Transportpaket magic header</string>
<string name="underload_packet_magic_header">Unterlastpaket magic header</string>
<string name="see_the">Schaue das</string>
<string name="unsure_how">Wenn du nicht sicher bist, wie du weiterverfahren sollst</string>
<string name="export_configs_failed">Konfigurationsexport fehlgeschlagen</string>
<string name="error_file_format">Ungültige Konfiguration Tunnel-Format</string>
<string name="response_packet_magic_header">Antwortpaket magic header</string>
</resources> </resources>
+8 -6
View File
@@ -72,6 +72,7 @@
<string name="always_on_vpn_support">Permitir VPN siempre-activada</string> <string name="always_on_vpn_support">Permitir VPN siempre-activada</string>
<string name="location_services_not_detected">Servicios de Ubicación No Detectados</string> <string name="location_services_not_detected">Servicios de Ubicación No Detectados</string>
<string name="hint_search_packages">Buscar paquetes</string> <string name="hint_search_packages">Buscar paquetes</string>
<string name="clear_icon">Icono claro</string>
<string name="attempt_connection">Intentando conexión...</string> <string name="attempt_connection">Intentando conexión...</string>
<string name="vpn_starting">Iniciando VPN</string> <string name="vpn_starting">Iniciando VPN</string>
<string name="db_name">wg-tunnel-db</string> <string name="db_name">wg-tunnel-db</string>
@@ -105,6 +106,7 @@
<string name="listen_port">Puerto de escucha</string> <string name="listen_port">Puerto de escucha</string>
<string name="preshared_key">Clave previamente compartida</string> <string name="preshared_key">Clave previamente compartida</string>
<string name="persistent_keepalive">Keepalive persistente</string> <string name="persistent_keepalive">Keepalive persistente</string>
<string name="search_icon">Icono de búsqueda</string>
<string name="docs_description">Leer documentación</string> <string name="docs_description">Leer documentación</string>
<string name="email_description">Envíame un email</string> <string name="email_description">Envíame un email</string>
<string name="support_help_text">Si tienes problemas, ideas para mejoras, o simlemente comprometerte, tienes disponibles los siguientes recursos:</string> <string name="support_help_text">Si tienes problemas, ideas para mejoras, o simlemente comprometerte, tienes disponibles los siguientes recursos:</string>
@@ -135,7 +137,7 @@
<string name="pin_created">Pin creado con éxito</string> <string name="pin_created">Pin creado con éxito</string>
<string name="enter_pin">Introduce tu pin</string> <string name="enter_pin">Introduce tu pin</string>
<string name="create_pin">Crear pin</string> <string name="create_pin">Crear pin</string>
<string name="enable_app_lock">Activar el bloqueo de aplicaciones</string> <string name="enable_app_lock">Bloqueo de app activado</string>
<string name="restart_on_ping">Reiniciar al fallar ping (beta)</string> <string name="restart_on_ping">Reiniciar al fallar ping (beta)</string>
<string name="set_primary_tunnel">Establecer como túnel Principal</string> <string name="set_primary_tunnel">Establecer como túnel Principal</string>
<string name="no_wifi_names_configured">No hay nombres Wi-Fi configurados para este túnel</string> <string name="no_wifi_names_configured">No hay nombres Wi-Fi configurados para este túnel</string>
@@ -152,9 +154,9 @@
<string name="watcher_channel_id">Canal del obvervador</string> <string name="watcher_channel_id">Canal del obvervador</string>
<string name="watcher_channel_name">Canal de notificación 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="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="export_configs_failed">Error al exportar la configuración</string> <string name="privacy_policy_url" translatable="false"></string>
<string name="use_amnezia">"Utilizar el entorno de usuario de Amnezia "</string> <string name="docs_url" translatable="false"></string>
<string name="junk_packet_count">Recuento de paquetes basura</string> <string name="github_url" translatable="false"></string>
<string name="backend">Backend</string> <string name="discord_url" translatable="false"></string>
<string name="junk_packet_minimum_size">Tamaño mínimo del paquete basura</string> <string name="my_email" translatable="false"></string>
</resources> </resources>
-2
View File
@@ -1,2 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>
-5
View File
@@ -1,5 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="tunnels">Tunele</string>
<string name="app_name">WG Tunnel</string>
</resources>
-162
View File
@@ -1,162 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="watcher_notification_text_active">Monitoramento de mudanças do estado da rede: Ativo</string>
<string name="watcher_notification_text_paused">Monitoramento de mudanças do estado da rede: pausado</string>
<string name="tunnel_start_title">VPN conectada</string>
<string name="tunnel_on_ethernet">Túnel na ethernet</string>
<string name="save_changes">Salvar</string>
<string name="public_key">Chave pública</string>
<string name="addresses">Endereços</string>
<string name="dns_servers">Servidores DNS</string>
<string name="endpoint">Endpoint</string>
<string name="name">Nome</string>
<string name="restart">Reiniciar Túnel</string>
<string name="create_import">Criar do zero</string>
<string name="turn_off_auto">Esta ação precisa do auto-túnel desativado ou pausado</string>
<string name="rotate_keys">Revezar chaves</string>
<string name="private_key">Chave privada</string>
<string name="base64_key">Chave base64</string>
<string name="optional_no_recommend">(opcional, não recomendado)</string>
<string name="preshared_key">Chave pré-compartilhada</string>
<string name="seconds">segundos</string>
<string name="export_configs">Exportar configurações</string>
<string name="export_configs_failed">Falhou ao exportar configurações</string>
<string name="go">ir</string>
<string name="error_no_file_explorer">Nenhum explorador de arquivos instalado</string>
<string name="error_invalid_code">Código QR inválido</string>
<string name="auto_tunnel_title">Serviço de Auto-túnel</string>
<string name="excluded">excluído</string>
<string name="all">todos</string>
<string name="enter_pin">Digite seu pin</string>
<string name="use_tunnel_on_wifi_name">Usar túnel em wifi com nome</string>
<string name="auto_tun_on">Continuar auto-túnel</string>
<string name="auto_tun_off">Pausar auto-túnel</string>
<string name="version">Versão</string>
<string name="mode">Modo</string>
<string name="use_amnezia">"Usar Amnezia em modo usuário "</string>
<string name="junk_packet_count">Número de pacotes-lixo</string>
<string name="junk_packet_minimum_size">Tamanho mínimo de pacote-lixo</string>
<string name="junk_packet_maximum_size">Tamanho máximo de pacote-lixo</string>
<string name="init_packet_junk_size">Tamanho de pacote-lixo inicial</string>
<string name="response_packet_junk_size">Tamanho de resposta de pacote-lixo</string>
<string name="app_name">WG Tunnel</string>
<string name="no_tunnels">Nenhum túnel foi adicionado!</string>
<string name="error_file_extension">O arquivo não é .conf ou .zip</string>
<string name="prominent_background_location_message">Este recurso precisa de permissões de localização em segundo plano para habilitar o monitoramento do SSID da rede Wi-Fi mesmo quando o aplicativo está fechado. Para mais detalhes, por favor veja a Política de Privacidade na tela de Suporte.</string>
<string name="turn_off_tunnel">Esta ação só é possível com o túnel inativo</string>
<string name="enabled_app_shortcuts">Habilitar atalhos do aplicativo</string>
<string name="notification_permission_required">Necessita permissões de notificação.</string>
<string name="add_trusted_ssid">Adicionar nome de Wi-Fi confiável</string>
<string name="enable_auto_tunnel">Iniciar auto-túnel</string>
<string name="tunnels">Túneis</string>
<string name="disable_auto_tunnel">Parar auto-túnel</string>
<string name="tunnel_start_text">Conectado ao túnel</string>
<string name="trusted_ssid_empty_description">Digite o SSID</string>
<string name="no_thanks">Não, obrigado</string>
<string name="privacy_policy">Ver a Política de Privacidade</string>
<string name="okay">OK</string>
<string name="tunnel_mobile_data">Túnel em dados móveis</string>
<string name="one_tunnel_required">Pelo menos um túnel é necessário para usar este recurso</string>
<string name="prominent_background_location_title">Revelar a localização em segundo plano</string>
<string name="thank_you">Obrigado por usar o WG Tunnel!</string>
<string name="trusted_ssid_value_description">Envie o SSID</string>
<string name="open_file">Abrir Arquivo</string>
<string name="add_from_qr">Adicionar a partir de código QR</string>
<string name="add_tunnels_text">Adicionar a partir de arquivo ou zip</string>
<string name="tunnel_all">Todos os aplicativos pelo túnel</string>
<string name="icon">Ícone</string>
<string name="turn_on">Ligar</string>
<string name="qr_scan">Escanear o código QR</string>
<string name="tunnel_name">Nome do Túnel</string>
<string name="add_tunnel">Adicionar Túnel</string>
<string name="config_changes_saved">Mudanças nas configurações salvas.</string>
<string name="exclude">Excluir</string>
<string name="include">Incluir</string>
<string name="map">Mapa</string>
<string name="vpn_connection_failed">Falha na conexão</string>
<string name="mtu">MTU</string>
<string name="always_on_vpn_support">Permitir VPN sempre ligada</string>
<string name="allowed_ips">IPs Permitidos</string>
<string name="attempt_connection">Tentando conexão..</string>
<string name="peer">Par</string>
<string name="location_services_not_detected">Serviço de localização não foi detectado</string>
<string name="hint_search_packages">Procurar pacotes</string>
<string name="vpn_starting">Iniciando VPN</string>
<string name="other">Outro</string>
<string name="scanning_qr">Escaneando código QR</string>
<string name="none">Nenhum nome de Wi-Fi confiável</string>
<string name="auto_tunneling">Auto-túnel</string>
<string name="default_vpn_on">VPN Principal ligada</string>
<string name="vpn_on">VPN ligada</string>
<string name="vpn_off">VPN desligada</string>
<string name="listen_port">Porta de escuta</string>
<string name="default_vpn_off">VPN Principal desligada</string>
<string name="turn_on_tunnel">Esta ação precisa um túnel ativo</string>
<string name="done">Feito</string>
<string name="add_peer">Adicionar par</string>
<string name="interface_">Interface</string>
<string name="copy_public_key">Copiar chave pública</string>
<string name="comma_separated_list">Lista separada por vírgulas</string>
<string name="optional">(opcional)</string>
<string name="random">(aleatório)</string>
<string name="persistent_keepalive">Manter a conexão persistente (keepalive)</string>
<string name="cancel">Cancelar</string>
<string name="error_authentication_failed">Autenticação falhou</string>
<string name="error_authorization_failed">Autorização falhou</string>
<string name="restart_on_ping">Reiniciar em falha de ping (beta)</string>
<string name="background_location_required">Necessita a localização em segundo plano</string>
<string name="location_services_required">Necessita dos serviços de localização</string>
<string name="email_description">Me envie um email</string>
<string name="error_ssid_exists">SSID já existe</string>
<string name="delete_tunnel_message">Tem certeza que você quer apagar este túnel?</string>
<string name="yes">Sim</string>
<string name="precise_location_required">Necessita da localização precisa</string>
<string name="exported_configs_message">Configurações exportadas para downloads</string>
<string name="unknown_error">Ocorreu um erro desconhecido</string>
<string name="email_subject">Suporte para o WG Tunnel</string>
<string name="tunnel_on_wifi">Túnel em Wi-Fi não confiável</string>
<string name="error_none">Nenhum erro</string>
<string name="delete_tunnel">Apagar túnel</string>
<string name="email_chooser">Enviar um email…</string>
<string name="use_kernel">Usar o módulo do kernel</string>
<string name="discord_description">Juntar-se à comunidade</string>
<string name="docs_description">Ler a documentação</string>
<string name="support_help_text">Se você enfrentar problemas, tiver ideias para melhorias ou apenas quiser participar, os seguintes recursos estão disponíveis:</string>
<string name="error_root_denied">Shell Root negado</string>
<string name="location_services_missing_message">O aplicativo não detectou o serviço de localização habilitado no seu dispositivo. Dependendo do dispositivo, isto pode causar que a função de Wi-Fi não confiável falhe em ler o nome do Wi-Fi. Quer continuar mesmo assim?</string>
<string name="paused">pausado</string>
<string name="included">incluso</string>
<string name="resume">Continuar</string>
<string name="pause">Pausar</string>
<string name="active">ativo</string>
<string name="always_on_disabled">VPN sempre ligada tentou iniciar um túnel, mas este recurso está desligado nas configurações.</string>
<string name="open_issue">Abrir um problema</string>
<string name="tunneling_apps">Aplicativos em túnel</string>
<string name="no_email_detected">Nenhum aplicativo de email detectado</string>
<string name="no_browser_detected">Nenhum navegador detectado</string>
<string name="logs_saved">Registros salvos em downloads</string>
<string name="incorrect_pin">O Pin está errado</string>
<string name="auto">(automático)</string>
<string name="disabled">desligado</string>
<string name="read_logs">Ler os registros</string>
<string name="config_parse_error">Falha na interpretação das configurações</string>
<string name="pin_created">Pin criado com sucesso</string>
<string name="auto_on">Continuar auto-túnel</string>
<string name="create_pin">Criar um pin</string>
<string name="enable_app_lock">Ligar trava de aplicativo</string>
<string name="no_wifi_names_configured">Nenhum Wi-Fi configurado para este túnel</string>
<string name="edit_tunnel">Editar túnel</string>
<string name="mobile_data_tunnel">Selecionar como túnel em dados móveis</string>
<string name="set_primary_tunnel">Selecionar como túnel principal</string>
<string name="general">Geral</string>
<string name="userspace">Modo usuário</string>
<string name="support">Suporte</string>
<string name="kernel">Kernel</string>
<string name="auto_off">Pausar auto-túnel</string>
<string name="backend">Backend</string>
<string name="settings">Configurações</string>
<string name="unsure_how">se não tiver certeza em como continuar</string>
<string name="see_the">Veja o</string>
<string name="getting_started_guide">guia de início rápido</string>
<string name="error_file_format">Formato de arquivo de configuração inválido</string>
</resources>
+1 -169
View File
@@ -1,171 +1,3 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<string name="turn_off_tunnel">Действие требует отключения туннеля</string> </resources>
<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>
<string name="resume">Восстановить</string>
<string name="tunneling_apps">Туннелируемые приложения</string>
<string name="paused">приостановлено</string>
<string name="active">активно</string>
<string name="excluded">исключено</string>
<string name="all">все</string>
<string name="no_email_detected">Приложение для отправки почты не обнаружено</string>
<string name="enter_pin">Введите свой PIN-код</string>
<string name="enable_app_lock">Включить блокировку приложения</string>
<string name="restart_on_ping">Перезапуск при сбое пинга (бета)</string>
<string name="settings">Настройки</string>
<string name="support">Поддержка</string>
<string name="backend">Модуль</string>
<string name="use_amnezia">"Использовать модуль Amnezia режима пользователя "</string>
<string name="init_packet_junk_size">Начальный размер «мусорного» пакета</string>
<string name="response_packet_junk_size">Размер ответного «мусорного» пакета</string>
<string name="init_packet_magic_header">Заголовок пакета инициализации</string>
<string name="see_the">Смотрите</string>
<string name="tunnel_mobile_data">Туннелировать через мобильный интернет</string>
<string name="tunnel_on_ethernet">Туннелировать через Ethernet</string>
<string name="cancel">Отмена</string>
<string name="docs_description">Посмотреть документацию</string>
<string name="discord_description">Присоединиться к сообществу</string>
<string name="email_chooser">Отправить письмо…</string>
<string name="add_trusted_ssid">Добавить доверенное имя сети Wi-Fi</string>
<string name="included">включено</string>
<string name="vpn_starting">Идёт запуск VPN</string>
<string name="auto_tunnel_title">Сервис авто-туннелирования</string>
<string name="create_import">Создать с нуля</string>
<string name="private_key">Закрытый ключ</string>
<string name="pause">Приостановить</string>
<string name="logs_saved">Журнал сохранён в Загрузки</string>
<string name="db_name">wg-tunnel-db</string>
<string name="base64_key">Ключ в base64</string>
<string name="error_no_file_explorer">Не найден файловый менеджер</string>
<string name="delete_tunnel_message">Вы действительно хотите удалить этот туннель?</string>
<string name="version">Версия</string>
<string name="unknown_error">Неизвестная ошибка</string>
<string name="tunnel_on_wifi">Туннель в недоверенных сетях Wi-Fi</string>
<string name="error_file_extension">Файл не имеет расширение .conf или .zip</string>
<string name="random">(случайно)</string>
<string name="tunnel_start_text">Подключено к туннелю</string>
<string name="app_name">WG Tunnel</string>
<string name="vpn_channel_id">Канал VPN</string>
<string name="vpn_channel_name">Канал уведомлений VPN</string>
<string name="watcher_channel_id">Канал наблюдателя</string>
<string name="watcher_channel_name">Канал уведомлений наблюдателя</string>
<string name="watcher_notification_text_active">Отслеживание состояния сети: активно</string>
<string name="notification_permission_required">Требуется разрешение на отображение уведомлений.</string>
<string name="tunnels">Туннели</string>
<string name="enable_auto_tunnel">Запустить авто-туннель</string>
<string name="disable_auto_tunnel">Остановить авто-туннель</string>
<string name="one_tunnel_required">Для использования этой функции нужно настроить хотя бы один туннель</string>
<string name="okay">Хорошо</string>
<string name="prominent_background_location_title">Фоновая передача местоположения</string>
<string name="thank_you">Благодарим Вас за использование WG Tunnel!</string>
<string name="trusted_ssid_value_description">Отправить SSID</string>
<string name="trusted_ssid_empty_description">Введите SSID</string>
<string name="add_tunnels_text">Добавить из файла или архива</string>
<string name="location_services_not_detected">Сервисы местоположения не обнаружены</string>
<string name="attempt_connection">Попытка соединения…</string>
<string name="scanning_qr">Поиск QR-кода</string>
<string name="none">Нет доверенных имён Wi-Fi</string>
<string name="default_vpn_on">Основной VPN вкл.</string>
<string name="default_vpn_off">Основной VPN выкл.</string>
<string name="turn_off_auto">Необходимо приостановить или отключить авто-туннелирование</string>
<string name="turn_on_tunnel">Действие требует наличие активного туннеля</string>
<string name="add_peer">Добавить пира</string>
<string name="rotate_keys">Обновить ключи</string>
<string name="comma_separated_list">разделённый запятыми список</string>
<string name="listen_port">Порт прослушивания</string>
<string name="preshared_key">Общий ключ</string>
<string name="seconds">секунд</string>
<string name="persistent_keepalive">Поддержание работы туннеля (keepalive)</string>
<string name="error_authentication_failed">Сбой аутентификации</string>
<string name="export_configs">Экспорт конфигурации</string>
<string name="location_services_required">Необходимо наличие служб местоположения</string>
<string name="error_invalid_code">Некорректный QR-код</string>
<string name="location_services_missing_message">Приложение не обнаружило сервис местоположения на вашем устройстве. На некоторых устройствах это может привести к невозможности определения имени сети Wi-Fi и сбою функции недоверенной сети. Всё равно продолжить?</string>
<string name="error_ssid_exists">SSID уже существует</string>
<string name="error_root_denied">Root-доступ запрещён</string>
<string name="error_none">Нет ошибки</string>
<string name="delete_tunnel">Удалить туннель</string>
<string name="yes">Да</string>
<string name="use_tunnel_on_wifi_name">Использовать туннель в сети Wi-Fi</string>
<string name="disabled">отключено</string>
<string name="auto_off">Приостановить авто-туннель</string>
<string name="auto_tun_on">Восстановить авто-туннель</string>
<string name="watcher_notification_text_paused">Отслеживание состояния сети: приостановлено</string>
<string name="tunnel_start_title">VPN подключен</string>
<string name="prominent_background_location_message">Эта функция требует фоновый доступ к местоположению для отслеживания имён сетей Wi-Fi, даже когда приложение закрыто. Для получения дополнительной информации, прочтите политику конфиденциальности на экране поддержки.</string>
<string name="done">Готово</string>
<string name="copy_public_key">Копировать открытый ключ</string>
<string name="error_authorization_failed">Не удалось пройти аутентификацию</string>
<string name="enabled_app_shortcuts">Включить ярлыки приложения</string>
<string name="export_configs_failed">Не удалось экспортировать конфигурацию</string>
<string name="precise_location_required">Необходимо точное местоположение</string>
<string name="open_issue">Сообщить о проблеме</string>
<string name="config_parse_error">Не удалось разобрать файл конфигурации</string>
<string name="incorrect_pin">Некорректный PIN-код</string>
<string name="pin_created">PIN создан успешно</string>
<string name="create_pin">Создать PIN-код</string>
<string name="mobile_data_tunnel">Назначить как туннель для мобильного интернета</string>
<string name="edit_tunnel">Редактировать туннель</string>
<string name="auto_on">Восстановить авто-туннель</string>
<string name="auto_tun_off">Приостановить авто-туннель</string>
<string name="junk_packet_count">Количество «мусорных» пакетов</string>
<string name="junk_packet_maximum_size">Максимальный размер «мусорного» пакета</string>
<string name="error_file_format">некорректный формат конфигурации туннеля</string>
<string name="background_location_required">Необходим фоновый доступ к местоположению</string>
<string name="exported_configs_message">Экспорт конфигурации в Загрузки</string>
<string name="email_subject">Поддержка WG Tunnel</string>
<string name="go">вперёд</string>
<string name="email_description">Отправить письмо автору</string>
<string name="support_help_text">Если у Вас возникли проблемы, или появилась идея по улучшению, или Вы просто хотите пообщаться, используйте следующие ресурсы:</string>
<string name="use_kernel">Использовать модуль режима ядра</string>
<string name="always_on_disabled">Функция всегда доступного VPN попыталась запустить туннель, но эта функция отключена в настройках.</string>
<string name="no_browser_detected">Веб-браузер не обнаружен</string>
<string name="read_logs">Посмотреть журнал</string>
<string name="auto">(авто)</string>
<string name="set_primary_tunnel">Назначить как главный туннель</string>
<string name="no_wifi_names_configured">Имена сетей Wi-Fi не назначены этому туннелю</string>
<string name="general">Общее</string>
<string name="mode">Режим</string>
<string name="userspace">Пользователя</string>
<string name="kernel">Модуль ядра</string>
<string name="junk_packet_minimum_size">Минимальный размер «мусорного» пакета</string>
<string name="response_packet_magic_header">Заголовок пакета ответа</string>
<string name="transport_packet_magic_header">Заголовок транспортного пакета</string>
<string name="getting_started_guide">руководство по началу работы</string>
<string name="unsure_how">, если не уверены, что делать дальше</string>
<string name="underload_packet_magic_header">Заголовок пакета под нагрузкой</string>
</resources>
+2
View File
@@ -59,6 +59,8 @@
<string name="always_on_vpn_support">Her Zaman Açık VPN\'e İzin Ver</string> <string name="always_on_vpn_support">Her Zaman Açık VPN\'e İzin Ver</string>
<string name="location_services_not_detected">Konum Hizmetleri Algılanmadı</string> <string name="location_services_not_detected">Konum Hizmetleri Algılanmadı</string>
<string name="hint_search_packages">Uygulama arayın</string> <string name="hint_search_packages">Uygulama arayın</string>
<string name="clear_icon">Simgeyi Temizle</string>
<string name="search_icon">Simge Ara</string>
<string name="attempt_connection">Bağlantı kurulmaya çalışılıyor..</string> <string name="attempt_connection">Bağlantı kurulmaya çalışılıyor..</string>
<string name="vpn_starting">VPN başlatılıyor</string> <string name="vpn_starting">VPN başlatılıyor</string>
<string name="db_name">wg-tunnel-db</string> <string name="db_name">wg-tunnel-db</string>
-169
View File
@@ -1,169 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="comma_separated_list">список розділений комами</string>
<string name="no_tunnels">Тунелі ще не додані!</string>
<string name="app_name">WG Tunnel</string>
<string name="add_trusted_ssid">Додати ім\'я довіреної мережі Wi-Fi</string>
<string name="tunnels">Тунелі</string>
<string name="enable_auto_tunnel">Запустити авто-тунелі</string>
<string name="disable_auto_tunnel">Зупинити авто-тунелі</string>
<string name="okay">ОК</string>
<string name="tunnel_on_ethernet">Тунелювати Ethernet</string>
<string name="prominent_background_location_title">Фонова передача місцезнаходження</string>
<string name="trusted_ssid_value_description">Підтвердити SSID</string>
<string name="qr_scan">Сканувати QR</string>
<string name="tunnel_name">Ім\'я тунелю</string>
<string name="add_tunnel">Додати тунель</string>
<string name="include">Включити</string>
<string name="tunnel_all">Тунель для всіх додатків</string>
<string name="config_changes_saved">Зміни налаштувань збережено.</string>
<string name="icon">Іконка</string>
<string name="no_thanks">Ні, дякую</string>
<string name="default_vpn_off">Основний VPN вимк.</string>
<string name="turn_on_tunnel">Дія потребує активного тунелю</string>
<string name="rotate_keys">Оновити ключі</string>
<string name="private_key">Закритий ключ</string>
<string name="base64_key">Ключ в base64</string>
<string name="random">(випадково)</string>
<string name="optional">(необов\'язково)</string>
<string name="optional_no_recommend">(необов\'язково, не рекомендується)</string>
<string name="cancel">Скасувати</string>
<string name="export_configs_failed">Помилка експорту конфігурації</string>
<string name="location_services_required">Необхідно сервіси місцезнаходження</string>
<string name="precise_location_required">Необхідно доступ до точного місцезнаходження</string>
<string name="exported_configs_message">Експорт конфігурації в Завантаження</string>
<string name="email_chooser">Надіслати E-Mail…</string>
<string name="error_root_denied">Root доступ заборонено</string>
<string name="error_invalid_code">Некоректний QR-код</string>
<string name="error_none">Нема помилок</string>
<string name="logs_saved">Логи збережено в Завантаженнях</string>
<string name="config_parse_error">Помилка аналізу файлу конфігурації</string>
<string name="incorrect_pin">Невірний PIN-код</string>
<string name="use_tunnel_on_wifi_name">Використовувати тунель в мережі Wi-Fi</string>
<string name="disabled">відключено</string>
<string name="version">Версія</string>
<string name="mode">Режим</string>
<string name="transport_packet_magic_header">Заголовок транспортного пакету</string>
<string name="getting_started_guide">інструкція щодо початку роботи</string>
<string name="error_file_format">некоректний формат конфігурації тунелю</string>
<string name="watcher_notification_text_active">Моніторинг стану мережі: активний</string>
<string name="vpn_channel_name">Канал сповіщення VPN</string>
<string name="vpn_channel_id">Канал VPN</string>
<string name="error_file_extension">Файл не є .conf або .zip файлом</string>
<string name="turn_off_tunnel">Дія потребує вимкнення тунелю</string>
<string name="watcher_notification_text_paused">Моніторинг стану мережі: призупинено</string>
<string name="tunnel_start_text">Підключення до тунелю</string>
<string name="tunnel_start_title">VPN підключено</string>
<string name="notification_permission_required">Потрібен дозвіл на відображення сповіщень.</string>
<string name="tunnel_mobile_data">Тунелювати мобільні дані</string>
<string name="one_tunnel_required">Для використання даної функції потрібно налаштувати мінімум один тунель</string>
<string name="privacy_policy">Переглянути політику конфіденційності</string>
<string name="thank_you">Спасибі за використання WG Tunnel!</string>
<string name="prominent_background_location_message">Дана функція потребує фоновий доступ до служби місцезнаходження для моніторингу назви мереж Wi-Fi навіть коли додаток закрито. Для отримання додаткової інформації прочитайте політику приватності на екрані Підтримки.</string>
<string name="trusted_ssid_empty_description">Введіть SSID</string>
<string name="add_tunnels_text">Додати з файлу або архіву</string>
<string name="open_file">Відкрити файл</string>
<string name="exclude">Виключити</string>
<string name="add_from_qr">Додати з QR коду</string>
<string name="save_changes">Зберегти</string>
<string name="turn_on">Увімкнути</string>
<string name="map">Карта</string>
<string name="public_key">Публічний ключ</string>
<string name="addresses">Адреса</string>
<string name="allowed_ips">Дозволені IP</string>
<string name="dns_servers">DNS-сервери</string>
<string name="mtu">MTU</string>
<string name="peer">Пір</string>
<string name="endpoint">Кінцева точка</string>
<string name="hint_search_packages">Пошук програм</string>
<string name="name">Ім\'я</string>
<string name="vpn_connection_failed">Помилка з\'єднання</string>
<string name="restart">Перезапустити тунель</string>
<string name="always_on_vpn_support">Дозволили Always-ON VPN</string>
<string name="location_services_not_detected">Сервіси місце знаходження не знайдено</string>
<string name="db_name">wg-tunnel-db</string>
<string name="scanning_qr">Сканування QR коду</string>
<string name="attempt_connection">Спроба з\'єднання...</string>
<string name="other">Інше</string>
<string name="vpn_starting">Запуск VPN</string>
<string name="auto_tunneling">Авто-тунелювання</string>
<string name="none">Нема довірених мереж Wi-Fi</string>
<string name="vpn_on">VPN увімк.</string>
<string name="vpn_off">VPN вимк.</string>
<string name="default_vpn_on">Основний VPN увімк.</string>
<string name="create_import">Створити з нуля</string>
<string name="turn_off_auto">Необхідно вимкнути або призупинити авто-тунелювання</string>
<string name="add_peer">Додати peer</string>
<string name="done">Готово</string>
<string name="interface_">Інтерфейс</string>
<string name="copy_public_key">Копіювати відкритий ключ</string>
<string name="listen_port">Слухати порт</string>
<string name="preshared_key">Pre-shared key</string>
<string name="seconds">секунд</string>
<string name="persistent_keepalive">Persistent keepalive</string>
<string name="error_authorization_failed">Не вдалося авторизуватися</string>
<string name="enabled_app_shortcuts">Дозволити ярлики</string>
<string name="error_authentication_failed">Помилка автентифікації</string>
<string name="export_configs">Експорт конфігурації</string>
<string name="background_location_required">Необхідний фоновий доступ до місцезнаходження</string>
<string name="unknown_error">Невідома помилка</string>
<string name="tunnel_on_wifi">Тунелювати недовірені мережі Wi-Fi</string>
<string name="email_subject">Підтримка WG-Tunnel</string>
<string name="go">вперед</string>
<string name="docs_description">Переглянути документацію</string>
<string name="email_description">Відправити email автору</string>
<string name="discord_description">Приєднатися до спільноти</string>
<string name="support_help_text">Якщо у вас виникли проблеми, є ідеї щодо покращення, чи бажання долучитися, скористайтесь наступними ресурсами:</string>
<string name="use_kernel">Використовувати модуль режиму ядра</string>
<string name="error_ssid_exists">SSID вже існує</string>
<string name="error_no_file_explorer">Не знайдено файловий менеджер</string>
<string name="auto_tunnel_title">Сервіс авто-тунелювання</string>
<string name="delete_tunnel">Видалити тунель</string>
<string name="location_services_missing_message">Додаток не знайшов служб місце знаходження на вашому пристрої. На деяких пристроях це може привести до неможливості визначення назви мережі Wi-Fi і помилок функції недовірених Wi-Fi мереж. Все рівно хочете продовжити?</string>
<string name="included">включено</string>
<string name="delete_tunnel_message">Ви дійсно хочете видалити цей тунель?</string>
<string name="yes">Так</string>
<string name="active">активно</string>
<string name="resume">Відновити</string>
<string name="pause">Призупинити</string>
<string name="paused">призупинено</string>
<string name="tunneling_apps">Тунельовані додатки</string>
<string name="excluded">виключено</string>
<string name="always_on_disabled">Функція Always-on VPN спробувала запустити тунель, але функція вимкнена в налаштуваннях.</string>
<string name="auto">(авто)</string>
<string name="all">всі</string>
<string name="no_email_detected">Програми для надсилання email не знайдено</string>
<string name="open_issue">Повідомити про проблему</string>
<string name="read_logs">Переглянути логи</string>
<string name="no_browser_detected">Веб браузер не знайдено</string>
<string name="pin_created">PIN-код створено успішно</string>
<string name="enter_pin">Введіть PIN-код</string>
<string name="create_pin">Створити PINhкод</string>
<string name="enable_app_lock">Увімкнути блокування додатку</string>
<string name="edit_tunnel">Редагувати тунель</string>
<string name="auto_on">Відновити авто-тунель</string>
<string name="restart_on_ping">Перезапуск при помилці ping (бета)</string>
<string name="mobile_data_tunnel">Встановити як тунель для мобільних даних</string>
<string name="set_primary_tunnel">Встановити як основний тунель</string>
<string name="no_wifi_names_configured">Імена мереж Wi-Fi не налаштовано для цього тунелю</string>
<string name="general">Загальне</string>
<string name="auto_tun_off">Призупинити авто-тунель</string>
<string name="auto_off">Призупинити авто-тунель</string>
<string name="auto_tun_on">Відновити авто-тунель</string>
<string name="userspace">Користувача</string>
<string name="support">Підтримка</string>
<string name="settings">Налаштування</string>
<string name="use_amnezia">"Використовувати модуль Amnezia режиму користувача "</string>
<string name="backend">Модуль</string>
<string name="kernel">Модуль ядра</string>
<string name="junk_packet_count">Кількість «сміттєвих» пакетів</string>
<string name="junk_packet_minimum_size">Мінімальний розмір «сміттєвого» пакету</string>
<string name="junk_packet_maximum_size">Максимальний розмір «сміттєвого» пакету</string>
<string name="init_packet_junk_size">Початковий розмір «сміттєвого» пакету</string>
<string name="response_packet_junk_size">Розмір відповіді «сміттєвого» пакету</string>
<string name="init_packet_magic_header">Заголовок пакету ініціалізації</string>
<string name="underload_packet_magic_header">Заголовок пакету під навантаженням</string>
<string name="response_packet_magic_header">Заголовок пакету відповіді</string>
<string name="unsure_how">, якщо не впевнені що робити далі</string>
<string name="see_the">Дивіться</string>
</resources>
+4 -23
View File
@@ -58,6 +58,8 @@
<string name="always_on_vpn_support">Allow Always-On VPN </string> <string name="always_on_vpn_support">Allow Always-On VPN </string>
<string name="location_services_not_detected">Location Services Not Detected</string> <string name="location_services_not_detected">Location Services Not Detected</string>
<string name="hint_search_packages">Search packages</string> <string name="hint_search_packages">Search packages</string>
<string name="clear_icon">Clear Icon</string>
<string name="search_icon">Search Icon</string>
<string name="attempt_connection">Attempting connection..</string> <string name="attempt_connection">Attempting connection..</string>
<string name="vpn_starting">VPN starting</string> <string name="vpn_starting">VPN starting</string>
<string name="db_name">wg-tunnel-db</string> <string name="db_name">wg-tunnel-db</string>
@@ -92,7 +94,6 @@
<string name="error_authorization_failed">Failed to authorize</string> <string name="error_authorization_failed">Failed to authorize</string>
<string name="enabled_app_shortcuts">Enable app shortcuts</string> <string name="enabled_app_shortcuts">Enable app shortcuts</string>
<string name="export_configs">Export configs</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="location_services_required">Location services required</string>
<string name="background_location_required">Background location required</string> <string name="background_location_required">Background location required</string>
<string name="precise_location_required">Precise location required</string> <string name="precise_location_required">Precise location required</string>
@@ -107,6 +108,7 @@
<string name="discord_description">Join the community</string> <string name="discord_description">Join the community</string>
<string name="email_description">Send me an email</string> <string name="email_description">Send me an email</string>
<string name="support_help_text">If you are experiencing issues, have improvement ideas, or just want to engage, the following resources are available:</string> <string name="support_help_text">If you are experiencing issues, have improvement ideas, or just want to engage, the following resources are available:</string>
<string name="kernel">Kernel</string>
<string name="use_kernel">Use kernel module</string> <string name="use_kernel">Use kernel module</string>
<string name="error_ssid_exists">SSID already exists</string> <string name="error_ssid_exists">SSID already exists</string>
<string name="error_root_denied">Root shell denied</string> <string name="error_root_denied">Root shell denied</string>
@@ -138,7 +140,7 @@
<string name="pin_created">Pin successfully created</string> <string name="pin_created">Pin successfully created</string>
<string name="enter_pin">Enter your pin</string> <string name="enter_pin">Enter your pin</string>
<string name="create_pin">Create pin</string> <string name="create_pin">Create pin</string>
<string name="enable_app_lock">Enable app lock</string> <string name="enable_app_lock">Enabled app lock</string>
<string name="restart_on_ping">Restart on ping fail (beta)</string> <string name="restart_on_ping">Restart on ping fail (beta)</string>
<string name="mobile_data_tunnel">Set as mobile data tunnel</string> <string name="mobile_data_tunnel">Set as mobile data tunnel</string>
<string name="set_primary_tunnel">Set as primary tunnel</string> <string name="set_primary_tunnel">Set as primary tunnel</string>
@@ -156,25 +158,4 @@
<string name="userspace">Userspace</string> <string name="userspace">Userspace</string>
<string name="settings">Settings</string> <string name="settings">Settings</string>
<string name="support">Support</string> <string name="support">Support</string>
<string name="backend">Backend</string>
<string name="kernel">Kernel</string>
<string name="use_amnezia">"Use Amnezia userspace "</string>
<string name="junk_packet_count">Junk packet count</string>
<string name="junk_packet_minimum_size">Junk packet minimum size</string>
<string name="junk_packet_maximum_size">Junk packet maximum size</string>
<string name="init_packet_junk_size">Init packet junk size</string>
<string name="response_packet_junk_size">Response packet junk size</string>
<string name="init_packet_magic_header">Init packet magic header</string>
<string name="response_packet_magic_header">Response packet magic header</string>
<string name="transport_packet_magic_header">Transport packet magic header</string>
<string name="underload_packet_magic_header">Underload packet magic header</string>
<string name="telegram_url" translatable="false">https://t.me/wgtunnel</string>
<string name="unsure_how">if you are unsure how to proceed</string>
<string name="see_the">See the</string>
<string name="getting_started_url" translatable="false">https://zaneschepke.com/wgtunnel-docs/getting-started.html</string>
<string name="getting_started_guide">getting started guide</string>
<string name="amnezia" translatable="false">Amnezia</string>
<string name="wireguard" translatable="false">WireGuard</string>
<string name="error_file_format">Invalid tunnel config format</string>
<string name="restart_at_boot">Restart on boot</string>
</resources> </resources>

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