mirror of
https://github.com/wgtunnel/android.git
synced 2026-07-03 14:07:49 +02:00
Compare commits
76 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6448386f76 | |||
| d09e85ba45 | |||
| a9bc1cc7f0 | |||
| 54d9653f04 | |||
| efc66821a6 | |||
| 3af7adc45b | |||
| 5754f2183c | |||
| f7f7f1bd9d | |||
| 57bb3f5e74 | |||
| 49196e7c7b | |||
| 894b63e668 | |||
| e16d44ff20 | |||
| b8b3f3001b | |||
| d142ecea6e | |||
| b793984ede | |||
| ae2532afe5 | |||
| 2720a3b35e | |||
| 2350364543 | |||
| f4172cb1fc | |||
| 90c482ae4f | |||
| 1eb8ad62e0 | |||
| d44baa84a8 | |||
| cb1b8ee7d6 | |||
| 4153351fc4 | |||
| 48e6f341cb | |||
| d531adede5 | |||
| 2df1bb07ab | |||
| a5e60c3fbe | |||
| e4af481402 | |||
| 77b3fc8360 | |||
| 4fd908f271 | |||
| 632da245ab | |||
| 04f22cb92d | |||
| 31194d8b88 | |||
| 421bf418d1 | |||
| e84d7e354c | |||
| 681b066d99 | |||
| ad53fca928 | |||
| f7e4b7e8ef | |||
| b04e8e7f60 | |||
| cbee5cfd1b | |||
| 440fe6ceda | |||
| 27def018bd | |||
| 16979dbb2b | |||
| 5d8190628d | |||
| 6d597a235d | |||
| 61fd2f01b9 | |||
| 914a641977 | |||
| c97a3afbc4 | |||
| fadc2d1562 | |||
| 106ab76b82 | |||
| 48b3f60b37 | |||
| e7f5cee6dd | |||
| 5e01fd6c85 | |||
| dcbd72c6f6 | |||
| f27aa1452a | |||
| 82c3521c58 | |||
| c6535ffe7e | |||
| be5fe94ce6 | |||
| 1e905c1cfb | |||
| a549c111aa | |||
| 5ab344044e | |||
| 7030f68548 | |||
| ecd51b6fe5 | |||
| 3e4f8e0791 | |||
| e940a0dbc5 | |||
| 8e233475df | |||
| 24789826ad | |||
| 787e0d1a71 | |||
| 914ef53ef0 | |||
| a569974beb | |||
| 87bc89b6f1 | |||
| c343220e96 | |||
| 5447ec73f7 | |||
| a2b8eb5b0b | |||
| ee3fcabcf1 |
@@ -0,0 +1,22 @@
|
||||
# Contributor Code of Conduct
|
||||
|
||||
## Pledge
|
||||
|
||||
We as individuals involved in this project, pledge to participate in this
|
||||
community in a respectful, constructive, and civil manner as we work towards a common goal
|
||||
of delivering free, open source, and value adding software for all.
|
||||
|
||||
## Standard
|
||||
|
||||
The standard for this community is the Golden Rule.
|
||||
|
||||
> “Do unto others as you would have them do unto you.”
|
||||
|
||||
## Scope
|
||||
|
||||
This Code of Conduct applies to all spaces related to WG Tunnel.
|
||||
|
||||
## Incidents or Concerns
|
||||
|
||||
For any incidents or concerns, reach out to Zane at
|
||||
<support@zaneschepke.com>.
|
||||
@@ -0,0 +1,20 @@
|
||||
name: Issue Updates Workflow
|
||||
|
||||
on:
|
||||
issues:
|
||||
types: [ opened, closed, reopened ]
|
||||
|
||||
|
||||
jobs:
|
||||
|
||||
build:
|
||||
name: Build
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Send Telegram Message
|
||||
run: |
|
||||
msg_text='${{ github.actor }} updated an issue:
|
||||
status: ${{ github.event.issue.state }} - #${{ github.event.issue.number }} ${{ github.event.issue.title }}
|
||||
https://github.com/zaneschepke/wgtunnel/issues/${{ github.event.issue.number }}'
|
||||
curl -s -X POST 'https://api.telegram.org/bot${{ secrets.TELEGRAM_TOKEN }}/sendMessage' \
|
||||
-d "chat_id=${{ secrets.TELEGRAM_TO }}&text=${msg_text}&message_thread_id=${{ secrets.TELEGRAM_TOPIC }}"
|
||||
@@ -1,4 +1,3 @@
|
||||
# name of the workflow
|
||||
name: Android CI Tag Deployment (Pre-release)
|
||||
|
||||
on:
|
||||
@@ -68,7 +67,7 @@ jobs:
|
||||
echo "VERSION_CODE=$version_code" >> $GITHUB_ENV
|
||||
# Save the APK after the Build job is complete to publish it as a Github release in the next job
|
||||
- name: Upload APK
|
||||
uses: actions/upload-artifact@v4.3.1
|
||||
uses: actions/upload-artifact@v4.3.3
|
||||
with:
|
||||
name: wgtunnel
|
||||
path: ${{ steps.apk-path.outputs.path }}
|
||||
@@ -97,7 +96,7 @@ jobs:
|
||||
|
||||
- name: Get checksum
|
||||
id: checksum
|
||||
run: echo "checksum=$(apksigner verify -print-certs ${{ steps.apk-path.outputs.path }} | grep -Po "(?<=SHA-256 digest:) .*")" | awk '{$1=$1};1' >> $GITHUB_OUTPUT
|
||||
run: echo "checksum=$(apksigner verify -print-certs ${{ steps.apk-path.outputs.path }} | grep -Po "(?<=SHA-256 digest:) .*" | tr -d "[:blank:]")" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Append checksum
|
||||
id: append_checksum
|
||||
@@ -105,8 +104,10 @@ jobs:
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
body: >
|
||||
<br /> SHA256 Checksum: <br /> ```${{ steps.checksum.outputs.checksum }}```
|
||||
body: |
|
||||
|
||||
SHA256 fingerprint:
|
||||
```${{ steps.checksum.outputs.checksum }}```
|
||||
tag_name: ${{ github.ref_name }}
|
||||
name: ${{ github.ref_name }}
|
||||
draft: false
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
name: Release Updates Workflow
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [ published ]
|
||||
|
||||
|
||||
jobs:
|
||||
|
||||
build:
|
||||
name: Build
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Send Telegram Message
|
||||
run: |
|
||||
msg_text='${{ github.actor }} published a new release:
|
||||
Release: ${{ github.event.release.tag_name }}
|
||||
${{ github.event.release.body }}
|
||||
https://github.com/zaneschepke/wgtunnel/releases/tag/${{ github.event.release.tag_name }}'
|
||||
curl -s -X POST 'https://api.telegram.org/bot${{ secrets.TELEGRAM_TOKEN }}/sendMessage' \
|
||||
-d "chat_id=${{ secrets.TELEGRAM_TO }}&text=${msg_text}&message_thread_id=${{ secrets.TELEGRAM_TOPIC }}"
|
||||
@@ -69,7 +69,7 @@ jobs:
|
||||
echo "VERSION_CODE=$version_code" >> $GITHUB_ENV
|
||||
# Save the APK after the Build job is complete to publish it as a Github release in the next job
|
||||
- name: Upload APK
|
||||
uses: actions/upload-artifact@v4.3.1
|
||||
uses: actions/upload-artifact@v4.3.3
|
||||
with:
|
||||
name: wgtunnel
|
||||
path: ${{ steps.apk-path.outputs.path }}
|
||||
@@ -103,7 +103,7 @@ jobs:
|
||||
|
||||
- name: Get checksum
|
||||
id: checksum
|
||||
run: echo "checksum=$(apksigner verify -print-certs ${{ steps.apk-path.outputs.path }} | grep -Po "(?<=SHA-256 digest:) .*")" | awk '{$1=$1};1' >> $GITHUB_OUTPUT
|
||||
run: echo "checksum=$(apksigner verify -print-certs ${{ steps.apk-path.outputs.path }} | grep -Po "(?<=SHA-256 digest:) .*" | tr -d "[:blank:]")" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Append checksum
|
||||
id: append_checksum
|
||||
@@ -111,8 +111,10 @@ jobs:
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
body: >
|
||||
<br /> SHA256 Checksum: <br /> ```${{ steps.checksum.outputs.checksum }}```
|
||||
body: |
|
||||
|
||||
SHA256 fingerprint:
|
||||
```${{ steps.checksum.outputs.checksum }}```
|
||||
tag_name: ${{ github.ref_name }}
|
||||
name: ${{ github.ref_name }}
|
||||
draft: false
|
||||
|
||||
@@ -4,8 +4,9 @@ WG Tunnel
|
||||
|
||||
<div align="center">
|
||||
|
||||
[](https://opensource.org/licenses/MIT)
|
||||
[](https://discord.gg/rbRRNh6H7V)
|
||||
[](https://discord.gg/rbRRNh6H7V)
|
||||
[](https://twitter.com/i/communities/1780655267685736818)
|
||||
[](https://t.me/wgtunnel)
|
||||
|
||||
</div>
|
||||
|
||||
@@ -21,7 +22,8 @@ WG Tunnel
|
||||
|
||||
<div align="left">
|
||||
|
||||
This is an alternative Android Application for [WireGuard](https://www.wireguard.com/) with added
|
||||
This is an alternative Android Application for [WireGuard](https://www.wireguard.com/)
|
||||
and [AmneziaWG](https://docs.amnezia.org/documentation/amnezia-wg/) with added
|
||||
features. Built using the [wireguard-android](https://github.com/WireGuard/wireguard-android)
|
||||
library and [Jetpack Compose](https://developer.android.com/jetpack/compose), this application was
|
||||
inspired by the official [WireGuard Android](https://github.com/WireGuard/wireguard-android) app.
|
||||
@@ -52,8 +54,9 @@ and on while on different networks. This app was created to offer a free solutio
|
||||
* Auto connect to tunnels based on Wi-Fi SSID, ethernet, or mobile data
|
||||
* Split tunneling by application with search
|
||||
* WireGuard support for kernel and userspace modes
|
||||
* Amnezia support for userspace mode for DPI/censorship protection
|
||||
* Always-On VPN support
|
||||
* Export tunnels to zip
|
||||
* Export Amnezia and WireGuard tunnels to zip
|
||||
* Quick tile support for tunnel toggling, auto-tunneling
|
||||
* Static shortcuts support for tunnel toggling, auto-tunneling
|
||||
* Intent automation support for all tunnels
|
||||
@@ -62,13 +65,30 @@ and on while on different networks. This app was created to offer a free solutio
|
||||
* Battery preservation measures
|
||||
* Restart tunnel on ping failure (beta)
|
||||
|
||||
## Docs (WIP)
|
||||
## Docs
|
||||
|
||||
Basic documentation of the feature and behaviors of this app can be
|
||||
found [here](https://zaneschepke.com/wgtunnel-docs/overview.html).
|
||||
|
||||
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
|
||||
|
||||
This app is using [Weblate](https://weblate.org) to assist with translations.
|
||||
|
||||
Help translate WG Tunnel into your language
|
||||
at [Hosted Weblate](https://hosted.weblate.org/engage/wg-tunnel/).\
|
||||
[](https://hosted.weblate.org/engage/wg-tunnel/)
|
||||
|
||||
## Building
|
||||
|
||||
```
|
||||
|
||||
@@ -12,6 +12,10 @@ android {
|
||||
namespace = Constants.APP_ID
|
||||
compileSdk = Constants.TARGET_SDK
|
||||
|
||||
androidResources {
|
||||
generateLocaleConfig = true
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
applicationId = Constants.APP_ID
|
||||
minSdk = Constants.MIN_SDK
|
||||
@@ -25,8 +29,6 @@ android {
|
||||
getByName("debug").assets.srcDirs(files("$projectDir/schemas")) // Room
|
||||
}
|
||||
|
||||
resourceConfigurations.addAll(listOf("en"))
|
||||
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
vectorDrawables { useSupportLibrary = true }
|
||||
}
|
||||
@@ -137,6 +139,7 @@ dependencies {
|
||||
|
||||
implementation(libs.androidx.core.ktx)
|
||||
implementation(libs.androidx.lifecycle.runtime.ktx)
|
||||
|
||||
// helpers for implementing LifecycleOwner in a Service
|
||||
implementation(libs.androidx.lifecycle.service)
|
||||
implementation(libs.androidx.activity.compose)
|
||||
@@ -160,6 +163,7 @@ dependencies {
|
||||
|
||||
// get tunnel lib from github packages or mavenLocal
|
||||
implementation(libs.tunnel)
|
||||
implementation(libs.amneziawg.android)
|
||||
coreLibraryDesugaring(libs.desugar.jdk.libs)
|
||||
|
||||
// logging
|
||||
@@ -170,6 +174,8 @@ dependencies {
|
||||
implementation(libs.androidx.navigation.compose)
|
||||
implementation(libs.androidx.hilt.navigation.compose)
|
||||
|
||||
implementation(libs.zaneschepke.multifab)
|
||||
|
||||
// hilt
|
||||
implementation(libs.hilt.android)
|
||||
ksp(libs.hilt.android.compiler)
|
||||
@@ -197,7 +203,6 @@ dependencies {
|
||||
|
||||
// barcode scanning
|
||||
implementation(libs.zxing.android.embedded)
|
||||
implementation(libs.zxing.core)
|
||||
|
||||
// bio
|
||||
implementation(libs.androidx.biometric.ktx)
|
||||
|
||||
@@ -2,4 +2,4 @@
|
||||
|
||||
-keepclassmembers class * extends androidx.datastore.preferences.protobuf.GeneratedMessageLite {
|
||||
<fields>;
|
||||
}
|
||||
}
|
||||
Vendored
+1
-3
@@ -21,6 +21,4 @@
|
||||
#-renamesourcefileattribute SourceFile
|
||||
-keepclassmembers class * extends androidx.datastore.preferences.protobuf.GeneratedMessageLite {
|
||||
<fields>;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,190 @@
|
||||
{
|
||||
"formatVersion": 1,
|
||||
"database": {
|
||||
"version": 8,
|
||||
"identityHash": "b4d4a7c489f6b2f0d3aa4fa6f37b4935",
|
||||
"entities": [
|
||||
{
|
||||
"tableName": "Settings",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_tunnel_enabled` INTEGER NOT NULL, `is_tunnel_on_mobile_data_enabled` INTEGER NOT NULL, `trusted_network_ssids` TEXT NOT NULL, `is_always_on_vpn_enabled` INTEGER NOT NULL, `is_tunnel_on_ethernet_enabled` INTEGER NOT NULL, `is_shortcuts_enabled` INTEGER NOT NULL DEFAULT false, `is_tunnel_on_wifi_enabled` INTEGER NOT NULL DEFAULT false, `is_kernel_enabled` INTEGER NOT NULL DEFAULT false, `is_restore_on_boot_enabled` INTEGER NOT NULL DEFAULT false, `is_multi_tunnel_enabled` INTEGER NOT NULL DEFAULT false, `is_auto_tunnel_paused` INTEGER NOT NULL DEFAULT false, `is_ping_enabled` INTEGER NOT NULL DEFAULT false, `is_amnezia_enabled` INTEGER NOT NULL DEFAULT false)",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "isAutoTunnelEnabled",
|
||||
"columnName": "is_tunnel_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "isTunnelOnMobileDataEnabled",
|
||||
"columnName": "is_tunnel_on_mobile_data_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "trustedNetworkSSIDs",
|
||||
"columnName": "trusted_network_ssids",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "isAlwaysOnVpnEnabled",
|
||||
"columnName": "is_always_on_vpn_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "isTunnelOnEthernetEnabled",
|
||||
"columnName": "is_tunnel_on_ethernet_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "isShortcutsEnabled",
|
||||
"columnName": "is_shortcuts_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isTunnelOnWifiEnabled",
|
||||
"columnName": "is_tunnel_on_wifi_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isKernelEnabled",
|
||||
"columnName": "is_kernel_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isRestoreOnBootEnabled",
|
||||
"columnName": "is_restore_on_boot_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isMultiTunnelEnabled",
|
||||
"columnName": "is_multi_tunnel_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isAutoTunnelPaused",
|
||||
"columnName": "is_auto_tunnel_paused",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isPingEnabled",
|
||||
"columnName": "is_ping_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isAmneziaEnabled",
|
||||
"columnName": "is_amnezia_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": true,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"tableName": "TunnelConfig",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `wg_quick` TEXT NOT NULL, `tunnel_networks` TEXT NOT NULL DEFAULT '', `is_mobile_data_tunnel` INTEGER NOT NULL DEFAULT false, `is_primary_tunnel` INTEGER NOT NULL DEFAULT false, `am_quick` TEXT NOT NULL DEFAULT '')",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "name",
|
||||
"columnName": "name",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "wgQuick",
|
||||
"columnName": "wg_quick",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "tunnelNetworks",
|
||||
"columnName": "tunnel_networks",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true,
|
||||
"defaultValue": "''"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isMobileDataTunnel",
|
||||
"columnName": "is_mobile_data_tunnel",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isPrimaryTunnel",
|
||||
"columnName": "is_primary_tunnel",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
},
|
||||
{
|
||||
"fieldPath": "amQuick",
|
||||
"columnName": "am_quick",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true,
|
||||
"defaultValue": "''"
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": true,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_TunnelConfig_name",
|
||||
"unique": true,
|
||||
"columnNames": [
|
||||
"name"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_TunnelConfig_name` ON `${TABLE_NAME}` (`name`)"
|
||||
}
|
||||
],
|
||||
"foreignKeys": []
|
||||
}
|
||||
],
|
||||
"views": [],
|
||||
"setupQueries": [
|
||||
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
||||
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'b4d4a7c489f6b2f0d3aa4fa6f37b4935')"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -2,26 +2,60 @@ package com.zaneschepke.wireguardautotunnel
|
||||
|
||||
import android.app.Application
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.StrictMode
|
||||
import android.os.StrictMode.ThreadPolicy
|
||||
import android.service.quicksettings.TileService
|
||||
import com.zaneschepke.logcatter.LocalLogCollector
|
||||
import com.zaneschepke.wireguardautotunnel.module.ApplicationScope
|
||||
import com.zaneschepke.wireguardautotunnel.module.IoDispatcher
|
||||
import com.zaneschepke.wireguardautotunnel.service.tile.AutoTunnelControlTile
|
||||
import com.zaneschepke.wireguardautotunnel.service.tile.TunnelControlTile
|
||||
import com.zaneschepke.wireguardautotunnel.util.ReleaseTree
|
||||
import dagger.hilt.android.HiltAndroidApp
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import xyz.teamgravity.pin_lock_compose.PinManager
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltAndroidApp
|
||||
class WireGuardAutoTunnel : Application() {
|
||||
|
||||
@Inject
|
||||
lateinit var localLogCollector: LocalLogCollector
|
||||
|
||||
@Inject
|
||||
@ApplicationScope
|
||||
lateinit var applicationScope: CoroutineScope
|
||||
|
||||
@Inject
|
||||
@IoDispatcher
|
||||
lateinit var ioDispatcher: CoroutineDispatcher
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
instance = this
|
||||
if (BuildConfig.DEBUG) Timber.plant(Timber.DebugTree()) else Timber.plant(ReleaseTree())
|
||||
PinManager.initialize(this)
|
||||
if (BuildConfig.DEBUG) {
|
||||
Timber.plant(Timber.DebugTree())
|
||||
StrictMode.setThreadPolicy(
|
||||
ThreadPolicy.Builder()
|
||||
.detectDiskReads()
|
||||
.detectDiskWrites()
|
||||
.detectNetwork()
|
||||
.penaltyLog()
|
||||
.build(),
|
||||
)
|
||||
} else Timber.plant(ReleaseTree())
|
||||
applicationScope.launch(ioDispatcher) {
|
||||
PinManager.initialize(this@WireGuardAutoTunnel)
|
||||
if (!isRunningOnAndroidTv()) localLogCollector.start()
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
lateinit var instance: WireGuardAutoTunnel
|
||||
private set
|
||||
|
||||
@@ -29,16 +63,16 @@ class WireGuardAutoTunnel : Application() {
|
||||
return instance.packageManager.hasSystemFeature(PackageManager.FEATURE_LEANBACK)
|
||||
}
|
||||
|
||||
fun requestTunnelTileServiceStateUpdate(context: Context) {
|
||||
fun requestTunnelTileServiceStateUpdate() {
|
||||
TileService.requestListeningState(
|
||||
context,
|
||||
instance,
|
||||
ComponentName(instance, TunnelControlTile::class.java),
|
||||
)
|
||||
}
|
||||
|
||||
fun requestAutoTunnelTileServiceUpdate(context: Context) {
|
||||
fun requestAutoTunnelTileServiceUpdate() {
|
||||
TileService.requestListeningState(
|
||||
context,
|
||||
instance,
|
||||
ComponentName(instance, AutoTunnelControlTile::class.java),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -6,12 +6,12 @@ import androidx.room.DeleteColumn
|
||||
import androidx.room.RoomDatabase
|
||||
import androidx.room.TypeConverters
|
||||
import androidx.room.migration.AutoMigrationSpec
|
||||
import com.zaneschepke.wireguardautotunnel.data.model.Settings
|
||||
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
|
||||
import com.zaneschepke.wireguardautotunnel.data.domain.Settings
|
||||
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
|
||||
|
||||
@Database(
|
||||
entities = [Settings::class, TunnelConfig::class],
|
||||
version = 7,
|
||||
version = 8,
|
||||
autoMigrations =
|
||||
[
|
||||
AutoMigration(from = 1, to = 2),
|
||||
@@ -33,6 +33,7 @@ import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
|
||||
to = 7,
|
||||
spec = RemoveLegacySettingColumnsMigration::class,
|
||||
),
|
||||
AutoMigration(7, 8),
|
||||
],
|
||||
exportSchema = true,
|
||||
)
|
||||
|
||||
@@ -5,7 +5,7 @@ import androidx.room.Delete
|
||||
import androidx.room.Insert
|
||||
import androidx.room.OnConflictStrategy
|
||||
import androidx.room.Query
|
||||
import com.zaneschepke.wireguardautotunnel.data.model.Settings
|
||||
import com.zaneschepke.wireguardautotunnel.data.domain.Settings
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
@Dao
|
||||
|
||||
@@ -5,7 +5,7 @@ import androidx.room.Delete
|
||||
import androidx.room.Insert
|
||||
import androidx.room.OnConflictStrategy
|
||||
import androidx.room.Query
|
||||
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
|
||||
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
|
||||
import com.zaneschepke.wireguardautotunnel.util.TunnelConfigs
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
@@ -20,6 +20,9 @@ interface TunnelConfigDao {
|
||||
@Query("SELECT * FROM TunnelConfig WHERE id=:id")
|
||||
suspend fun getById(id: Long): TunnelConfig?
|
||||
|
||||
@Query("SELECT * FROM TunnelConfig WHERE name=:name")
|
||||
suspend fun getByName(name: String): TunnelConfig?
|
||||
|
||||
@Query("SELECT * FROM TunnelConfig")
|
||||
suspend fun getAll(): TunnelConfigs
|
||||
|
||||
@@ -33,10 +36,10 @@ interface TunnelConfigDao {
|
||||
suspend fun findByTunnelNetworkName(name: String): TunnelConfigs
|
||||
|
||||
@Query("UPDATE TunnelConfig SET is_primary_tunnel = 0 WHERE is_primary_tunnel =1")
|
||||
fun resetPrimaryTunnel()
|
||||
suspend fun resetPrimaryTunnel()
|
||||
|
||||
@Query("UPDATE TunnelConfig SET is_mobile_data_tunnel = 0 WHERE is_mobile_data_tunnel =1")
|
||||
fun resetMobileDataTunnel()
|
||||
suspend fun resetMobileDataTunnel()
|
||||
|
||||
@Query("SELECT * FROM TUNNELCONFIG WHERE is_primary_tunnel=1")
|
||||
suspend fun findByPrimary(): TunnelConfigs
|
||||
|
||||
+28
-16
@@ -7,14 +7,20 @@ import androidx.datastore.preferences.core.edit
|
||||
import androidx.datastore.preferences.core.intPreferencesKey
|
||||
import androidx.datastore.preferences.core.stringPreferencesKey
|
||||
import androidx.datastore.preferences.preferencesDataStore
|
||||
import com.zaneschepke.wireguardautotunnel.module.IoDispatcher
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.withContext
|
||||
import timber.log.Timber
|
||||
import java.io.IOException
|
||||
|
||||
class DataStoreManager(private val context: Context) {
|
||||
class DataStoreManager(
|
||||
private val context: Context,
|
||||
@IoDispatcher private val ioDispatcher: CoroutineDispatcher
|
||||
) {
|
||||
companion object {
|
||||
val LOCATION_DISCLOSURE_SHOWN = booleanPreferencesKey("LOCATION_DISCLOSURE_SHOWN")
|
||||
val BATTERY_OPTIMIZE_DISABLE_SHOWN = booleanPreferencesKey("BATTERY_OPTIMIZE_DISABLE_SHOWN")
|
||||
@@ -32,20 +38,24 @@ class DataStoreManager(private val context: Context) {
|
||||
)
|
||||
|
||||
suspend fun init() {
|
||||
try {
|
||||
context.dataStore.data.first()
|
||||
} catch (e: IOException) {
|
||||
Timber.e(e)
|
||||
withContext(ioDispatcher) {
|
||||
try {
|
||||
context.dataStore.data.first()
|
||||
} catch (e: IOException) {
|
||||
Timber.e(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun <T> saveToDataStore(key: Preferences.Key<T>, value: T) {
|
||||
try {
|
||||
context.dataStore.edit { it[key] = value }
|
||||
} catch (e: IOException) {
|
||||
Timber.e(e)
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e)
|
||||
withContext(ioDispatcher) {
|
||||
try {
|
||||
context.dataStore.edit { it[key] = value }
|
||||
} catch (e: IOException) {
|
||||
Timber.e(e)
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,11 +63,13 @@ class DataStoreManager(private val context: Context) {
|
||||
fun <T> getFromStoreFlow(key: Preferences.Key<T>) = context.dataStore.data.map { it[key] }
|
||||
|
||||
suspend fun <T> getFromStore(key: Preferences.Key<T>): T? {
|
||||
return try {
|
||||
context.dataStore.data.map { it[key] }.first()
|
||||
} catch (e: IOException) {
|
||||
Timber.e(e)
|
||||
null
|
||||
return withContext(ioDispatcher) {
|
||||
try {
|
||||
context.dataStore.data.map { it[key] }.first()
|
||||
} catch (e: IOException) {
|
||||
Timber.e(e)
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package com.zaneschepke.wireguardautotunnel.data.model
|
||||
package com.zaneschepke.wireguardautotunnel.data.domain
|
||||
|
||||
data class GeneralState(
|
||||
val locationDisclosureShown: Boolean = LOCATION_DISCLOSURE_SHOWN_DEFAULT,
|
||||
+6
-1
@@ -1,4 +1,4 @@
|
||||
package com.zaneschepke.wireguardautotunnel.data.model
|
||||
package com.zaneschepke.wireguardautotunnel.data.domain
|
||||
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Entity
|
||||
@@ -50,4 +50,9 @@ data class Settings(
|
||||
defaultValue = "false",
|
||||
)
|
||||
val isPingEnabled: Boolean = false,
|
||||
@ColumnInfo(
|
||||
name = "is_amnezia_enabled",
|
||||
defaultValue = "false",
|
||||
)
|
||||
val isAmneziaEnabled: Boolean = false,
|
||||
)
|
||||
+19
-4
@@ -1,4 +1,4 @@
|
||||
package com.zaneschepke.wireguardautotunnel.data.model
|
||||
package com.zaneschepke.wireguardautotunnel.data.domain
|
||||
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Entity
|
||||
@@ -27,12 +27,27 @@ data class TunnelConfig(
|
||||
defaultValue = "false",
|
||||
)
|
||||
val isPrimaryTunnel: Boolean = false,
|
||||
@ColumnInfo(
|
||||
name = "am_quick",
|
||||
defaultValue = "",
|
||||
)
|
||||
val amQuick: String = AM_QUICK_DEFAULT,
|
||||
) {
|
||||
companion object {
|
||||
fun configFromQuick(wgQuick: String): Config {
|
||||
fun configFromWgQuick(wgQuick: String): Config {
|
||||
val inputStream: InputStream = wgQuick.byteInputStream()
|
||||
val reader = inputStream.bufferedReader(Charsets.UTF_8)
|
||||
return Config.parse(reader)
|
||||
return inputStream.bufferedReader(Charsets.UTF_8).use {
|
||||
Config.parse(it)
|
||||
}
|
||||
}
|
||||
|
||||
fun configFromAmQuick(amQuick: String): org.amnezia.awg.config.Config {
|
||||
val inputStream: InputStream = amQuick.byteInputStream()
|
||||
return inputStream.bufferedReader(Charsets.UTF_8).use {
|
||||
org.amnezia.awg.config.Config.parse(it)
|
||||
}
|
||||
}
|
||||
|
||||
const val AM_QUICK_DEFAULT = ""
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
package com.zaneschepke.wireguardautotunnel.data.repository
|
||||
|
||||
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
|
||||
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
|
||||
|
||||
interface AppDataRepository {
|
||||
suspend fun getPrimaryOrFirstTunnel(): TunnelConfig?
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
package com.zaneschepke.wireguardautotunnel.data.repository
|
||||
|
||||
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
|
||||
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
|
||||
import javax.inject.Inject
|
||||
|
||||
class AppDataRoomRepository @Inject constructor(
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
package com.zaneschepke.wireguardautotunnel.data.repository
|
||||
|
||||
import com.zaneschepke.wireguardautotunnel.data.model.GeneralState
|
||||
import com.zaneschepke.wireguardautotunnel.data.domain.GeneralState
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
interface AppStateRepository {
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
package com.zaneschepke.wireguardautotunnel.data.repository
|
||||
|
||||
import com.zaneschepke.wireguardautotunnel.data.datastore.DataStoreManager
|
||||
import com.zaneschepke.wireguardautotunnel.data.model.GeneralState
|
||||
import com.zaneschepke.wireguardautotunnel.data.domain.GeneralState
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import timber.log.Timber
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
package com.zaneschepke.wireguardautotunnel.data.repository
|
||||
|
||||
import com.zaneschepke.wireguardautotunnel.data.SettingsDao
|
||||
import com.zaneschepke.wireguardautotunnel.data.model.Settings
|
||||
import com.zaneschepke.wireguardautotunnel.data.domain.Settings
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
class RoomSettingsRepository(private val settingsDoa: SettingsDao) : SettingsRepository {
|
||||
|
||||
+5
-1
@@ -1,7 +1,7 @@
|
||||
package com.zaneschepke.wireguardautotunnel.data.repository
|
||||
|
||||
import com.zaneschepke.wireguardautotunnel.data.TunnelConfigDao
|
||||
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
|
||||
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
|
||||
import com.zaneschepke.wireguardautotunnel.util.TunnelConfigs
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
@@ -54,6 +54,10 @@ class RoomTunnelConfigRepository(private val tunnelConfigDao: TunnelConfigDao) :
|
||||
return tunnelConfigDao.count().toInt()
|
||||
}
|
||||
|
||||
override suspend fun findByTunnelName(name: String): TunnelConfig? {
|
||||
return tunnelConfigDao.getByName(name)
|
||||
}
|
||||
|
||||
override suspend fun findByTunnelNetworksName(name: String): TunnelConfigs {
|
||||
return tunnelConfigDao.findByTunnelNetworkName(name)
|
||||
}
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
package com.zaneschepke.wireguardautotunnel.data.repository
|
||||
|
||||
import com.zaneschepke.wireguardautotunnel.data.model.Settings
|
||||
import com.zaneschepke.wireguardautotunnel.data.domain.Settings
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
interface SettingsRepository {
|
||||
|
||||
+3
-1
@@ -1,6 +1,6 @@
|
||||
package com.zaneschepke.wireguardautotunnel.data.repository
|
||||
|
||||
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
|
||||
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
|
||||
import com.zaneschepke.wireguardautotunnel.util.TunnelConfigs
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
@@ -22,6 +22,8 @@ interface TunnelConfigRepository {
|
||||
|
||||
suspend fun count(): Int
|
||||
|
||||
suspend fun findByTunnelName(name: String): TunnelConfig?
|
||||
|
||||
suspend fun findByTunnelNetworksName(name: String): TunnelConfigs
|
||||
|
||||
suspend fun findByMobileDataTunnel(): TunnelConfigs
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
+4
@@ -2,6 +2,10 @@ package com.zaneschepke.wireguardautotunnel.module
|
||||
|
||||
import javax.inject.Qualifier
|
||||
|
||||
@Qualifier
|
||||
@Retention(AnnotationRetention.BINARY)
|
||||
annotation class Kernel
|
||||
|
||||
@Qualifier
|
||||
@Retention(AnnotationRetention.BINARY)
|
||||
annotation class Userspace
|
||||
@@ -0,0 +1,27 @@
|
||||
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
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
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
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
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()
|
||||
}
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
package com.zaneschepke.wireguardautotunnel.module
|
||||
|
||||
import javax.inject.Qualifier
|
||||
|
||||
@Qualifier
|
||||
@Retention(AnnotationRetention.BINARY)
|
||||
annotation class Kernel
|
||||
@@ -1,7 +1,10 @@
|
||||
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 com.zaneschepke.wireguardautotunnel.data.SettingsDao
|
||||
import com.zaneschepke.wireguardautotunnel.data.TunnelConfigDao
|
||||
import com.zaneschepke.wireguardautotunnel.data.datastore.DataStoreManager
|
||||
@@ -18,11 +21,25 @@ import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
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
|
||||
@Provides
|
||||
fun provideSettingsDoa(appDatabase: AppDatabase): SettingsDao {
|
||||
@@ -49,8 +66,11 @@ class RepositoryModule {
|
||||
|
||||
@Singleton
|
||||
@Provides
|
||||
fun providePreferencesDataStore(@ApplicationContext context: Context): DataStoreManager {
|
||||
return DataStoreManager(context)
|
||||
fun providePreferencesDataStore(
|
||||
@ApplicationContext context: Context,
|
||||
@IoDispatcher ioDispatcher: CoroutineDispatcher
|
||||
): DataStoreManager {
|
||||
return DataStoreManager(context, ioDispatcher)
|
||||
}
|
||||
|
||||
@Provides
|
||||
|
||||
@@ -15,6 +15,8 @@ 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 javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
@@ -42,17 +44,36 @@ class TunnelModule {
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideVpnService(
|
||||
@Userspace userspaceBackend: Backend,
|
||||
@Kernel kernelBackend: Backend,
|
||||
appDataRepository: AppDataRepository
|
||||
): VpnService {
|
||||
return WireGuardTunnel(userspaceBackend, kernelBackend, appDataRepository)
|
||||
fun provideAmneziaBackend(@ApplicationContext context: Context): org.amnezia.awg.backend.Backend {
|
||||
return org.amnezia.awg.backend.GoBackend(context)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideServiceManager(appDataRepository: AppDataRepository): ServiceManager {
|
||||
return ServiceManager(appDataRepository)
|
||||
fun provideVpnService(
|
||||
amneziaBackend: org.amnezia.awg.backend.Backend,
|
||||
@Userspace userspaceBackend: Backend,
|
||||
@Kernel kernelBackend: Backend,
|
||||
appDataRepository: AppDataRepository,
|
||||
@ApplicationScope applicationScope: CoroutineScope,
|
||||
@IoDispatcher ioDispatcher: CoroutineDispatcher
|
||||
): VpnService {
|
||||
return WireGuardTunnel(
|
||||
amneziaBackend,
|
||||
userspaceBackend,
|
||||
kernelBackend,
|
||||
appDataRepository,
|
||||
applicationScope,
|
||||
ioDispatcher,
|
||||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideServiceManager(
|
||||
appDataRepository: AppDataRepository,
|
||||
@IoDispatcher ioDispatcher: CoroutineDispatcher
|
||||
): ServiceManager {
|
||||
return ServiceManager(appDataRepository, ioDispatcher)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -24,7 +24,7 @@ class NotificationActionReceiver : BroadcastReceiver() {
|
||||
override fun onReceive(context: Context, intent: Intent?) = goAsync {
|
||||
try {
|
||||
//TODO fix for manual start changes when enabled
|
||||
serviceManager.stopVpnService(context)
|
||||
serviceManager.stopVpnServiceForeground(context)
|
||||
delay(Constants.TOGGLE_TUNNEL_DELAY)
|
||||
serviceManager.startVpnServiceForeground(context)
|
||||
} catch (e: Exception) {
|
||||
|
||||
@@ -3,5 +3,6 @@ package com.zaneschepke.wireguardautotunnel.service.foreground
|
||||
enum class Action {
|
||||
START,
|
||||
START_FOREGROUND,
|
||||
STOP
|
||||
STOP,
|
||||
STOP_FOREGROUND
|
||||
}
|
||||
|
||||
+3
-13
@@ -25,6 +25,7 @@ open class ForegroundService : LifecycleService() {
|
||||
Action.START.name,
|
||||
Action.START_FOREGROUND.name -> startService(intent.extras)
|
||||
|
||||
Action.STOP.name, Action.STOP_FOREGROUND.name -> stopService()
|
||||
Constants.ALWAYS_ON_VPN_ACTION -> {
|
||||
Timber.i("Always-on VPN starting service")
|
||||
startService(intent.extras)
|
||||
@@ -37,16 +38,9 @@ open class ForegroundService : LifecycleService() {
|
||||
"with a null intent. It has been probably restarted by the system.",
|
||||
)
|
||||
}
|
||||
// by returning this we make sure the service is restarted if the system kills the service
|
||||
return START_STICKY
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
Timber.d("The service has been destroyed")
|
||||
stopService()
|
||||
}
|
||||
|
||||
protected open fun startService(extras: Bundle?) {
|
||||
if (isServiceStarted) return
|
||||
Timber.d("Starting ${this.javaClass.simpleName}")
|
||||
@@ -55,12 +49,8 @@ open class ForegroundService : LifecycleService() {
|
||||
|
||||
protected open fun stopService() {
|
||||
Timber.d("Stopping ${this.javaClass.simpleName}")
|
||||
try {
|
||||
stopForeground(STOP_FOREGROUND_REMOVE)
|
||||
stopSelf()
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e)
|
||||
}
|
||||
stopForeground(STOP_FOREGROUND_REMOVE)
|
||||
stopSelf()
|
||||
isServiceStarted = false
|
||||
}
|
||||
}
|
||||
|
||||
+42
-18
@@ -4,10 +4,16 @@ import android.app.Service
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
|
||||
import com.zaneschepke.wireguardautotunnel.module.IoDispatcher
|
||||
import com.zaneschepke.wireguardautotunnel.util.Constants
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.withContext
|
||||
import timber.log.Timber
|
||||
|
||||
class ServiceManager(private val appDataRepository: AppDataRepository) {
|
||||
class ServiceManager(
|
||||
private val appDataRepository: AppDataRepository,
|
||||
@IoDispatcher private val ioDispatcher: CoroutineDispatcher
|
||||
) {
|
||||
|
||||
private fun <T : Service> actionOnService(
|
||||
action: Action,
|
||||
@@ -23,9 +29,11 @@ class ServiceManager(private val appDataRepository: AppDataRepository) {
|
||||
intent.component?.javaClass
|
||||
try {
|
||||
when (action) {
|
||||
Action.START_FOREGROUND -> context.startForegroundService(intent)
|
||||
Action.START -> context.startService(intent)
|
||||
Action.STOP -> context.stopService(intent)
|
||||
Action.START_FOREGROUND, Action.STOP_FOREGROUND -> context.startForegroundService(
|
||||
intent,
|
||||
)
|
||||
|
||||
Action.START, Action.STOP -> context.startService(intent)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e.message)
|
||||
@@ -46,14 +54,28 @@ class ServiceManager(private val appDataRepository: AppDataRepository) {
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun stopVpnServiceForeground(context: Context, isManualStop: Boolean = false) {
|
||||
withContext(ioDispatcher) {
|
||||
if (isManualStop) onManualStop()
|
||||
Timber.i("Stopping vpn service")
|
||||
actionOnService(
|
||||
Action.STOP_FOREGROUND,
|
||||
context,
|
||||
WireGuardTunnelService::class.java,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun stopVpnService(context: Context, isManualStop: Boolean = false) {
|
||||
if (isManualStop) onManualStop()
|
||||
Timber.i("Stopping vpn service")
|
||||
actionOnService(
|
||||
Action.STOP,
|
||||
context,
|
||||
WireGuardTunnelService::class.java,
|
||||
)
|
||||
withContext(ioDispatcher) {
|
||||
if (isManualStop) onManualStop()
|
||||
Timber.i("Stopping vpn service")
|
||||
actionOnService(
|
||||
Action.STOP,
|
||||
context,
|
||||
WireGuardTunnelService::class.java,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun onManualStop() {
|
||||
@@ -71,13 +93,15 @@ class ServiceManager(private val appDataRepository: AppDataRepository) {
|
||||
tunnelId: Int? = null,
|
||||
isManualStart: Boolean = false
|
||||
) {
|
||||
if (isManualStart) onManualStart(tunnelId)
|
||||
actionOnService(
|
||||
Action.START_FOREGROUND,
|
||||
context,
|
||||
WireGuardTunnelService::class.java,
|
||||
tunnelId?.let { mapOf(Constants.TUNNEL_EXTRA_KEY to it) },
|
||||
)
|
||||
withContext(ioDispatcher) {
|
||||
if (isManualStart) onManualStart(tunnelId)
|
||||
actionOnService(
|
||||
Action.START_FOREGROUND,
|
||||
context,
|
||||
WireGuardTunnelService::class.java,
|
||||
tunnelId?.let { mapOf(Constants.TUNNEL_EXTRA_KEY to it) },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun startWatcherServiceForeground(
|
||||
|
||||
+8
-36
@@ -1,84 +1,56 @@
|
||||
package com.zaneschepke.wireguardautotunnel.service.foreground
|
||||
|
||||
import com.wireguard.android.backend.Tunnel
|
||||
import com.zaneschepke.wireguardautotunnel.data.model.Settings
|
||||
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
|
||||
import com.zaneschepke.wireguardautotunnel.data.domain.Settings
|
||||
|
||||
data class WatcherState(
|
||||
val isWifiConnected: Boolean = false,
|
||||
val config: TunnelConfig? = null,
|
||||
val vpnStatus: Tunnel.State = Tunnel.State.DOWN,
|
||||
val isEthernetConnected: Boolean = false,
|
||||
val isMobileDataConnected: Boolean = false,
|
||||
val currentNetworkSSID: String = "",
|
||||
val settings: Settings = Settings()
|
||||
) {
|
||||
|
||||
private fun isVpnConnected() = vpnStatus == Tunnel.State.UP
|
||||
fun isEthernetConditionMet(): Boolean {
|
||||
return (isEthernetConnected &&
|
||||
settings.isTunnelOnEthernetEnabled &&
|
||||
!isVpnConnected())
|
||||
settings.isTunnelOnEthernetEnabled)
|
||||
}
|
||||
|
||||
fun isMobileDataConditionMet(): Boolean {
|
||||
return (!isEthernetConnected &&
|
||||
settings.isTunnelOnMobileDataEnabled &&
|
||||
!isWifiConnected &&
|
||||
isMobileDataConnected &&
|
||||
!isVpnConnected())
|
||||
}
|
||||
|
||||
fun isTunnelNotMobileDataPreferredConditionMet(): Boolean {
|
||||
return (!isEthernetConnected &&
|
||||
settings.isTunnelOnMobileDataEnabled &&
|
||||
!isWifiConnected &&
|
||||
isMobileDataConnected &&
|
||||
config?.isMobileDataTunnel == false && isVpnConnected())
|
||||
isMobileDataConnected)
|
||||
}
|
||||
|
||||
fun isTunnelOffOnMobileDataConditionMet(): Boolean {
|
||||
return (!isEthernetConnected &&
|
||||
!settings.isTunnelOnMobileDataEnabled &&
|
||||
isMobileDataConnected &&
|
||||
!isWifiConnected &&
|
||||
isVpnConnected())
|
||||
!isWifiConnected)
|
||||
}
|
||||
|
||||
fun isUntrustedWifiConditionMet(): Boolean {
|
||||
return (!isEthernetConnected &&
|
||||
isWifiConnected &&
|
||||
!settings.trustedNetworkSSIDs.contains(currentNetworkSSID) &&
|
||||
settings.isTunnelOnWifiEnabled
|
||||
&& !isVpnConnected())
|
||||
}
|
||||
|
||||
fun isTunnelNotWifiNamePreferredMet(ssid: String): Boolean {
|
||||
return (!isEthernetConnected &&
|
||||
isWifiConnected &&
|
||||
!settings.trustedNetworkSSIDs.contains(currentNetworkSSID) &&
|
||||
settings.isTunnelOnWifiEnabled && config?.tunnelNetworks?.contains(ssid) == false && isVpnConnected())
|
||||
settings.isTunnelOnWifiEnabled)
|
||||
}
|
||||
|
||||
fun isTrustedWifiConditionMet(): Boolean {
|
||||
return (!isEthernetConnected &&
|
||||
(isWifiConnected &&
|
||||
settings.trustedNetworkSSIDs.contains(currentNetworkSSID)) &&
|
||||
(isVpnConnected()))
|
||||
settings.trustedNetworkSSIDs.contains(currentNetworkSSID)))
|
||||
}
|
||||
|
||||
fun isTunnelOffOnWifiConditionMet(): Boolean {
|
||||
return (!isEthernetConnected &&
|
||||
(isWifiConnected &&
|
||||
!settings.isTunnelOnWifiEnabled &&
|
||||
(isVpnConnected())))
|
||||
!settings.isTunnelOnWifiEnabled))
|
||||
}
|
||||
|
||||
fun isTunnelOffOnNoConnectivityMet(): Boolean {
|
||||
return (!isEthernetConnected &&
|
||||
!isWifiConnected &&
|
||||
!isMobileDataConnected &&
|
||||
(isVpnConnected()))
|
||||
!isMobileDataConnected)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+228
-197
@@ -5,25 +5,30 @@ import android.os.Bundle
|
||||
import android.os.PowerManager
|
||||
import androidx.core.app.ServiceCompat
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.wireguard.android.backend.Tunnel
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
|
||||
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
|
||||
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
|
||||
import com.zaneschepke.wireguardautotunnel.module.IoDispatcher
|
||||
import com.zaneschepke.wireguardautotunnel.module.MainImmediateDispatcher
|
||||
import com.zaneschepke.wireguardautotunnel.service.network.EthernetService
|
||||
import com.zaneschepke.wireguardautotunnel.service.network.MobileDataService
|
||||
import com.zaneschepke.wireguardautotunnel.service.network.NetworkService
|
||||
import com.zaneschepke.wireguardautotunnel.service.network.NetworkStatus
|
||||
import com.zaneschepke.wireguardautotunnel.service.network.WifiService
|
||||
import com.zaneschepke.wireguardautotunnel.service.notification.NotificationService
|
||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelState
|
||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnService
|
||||
import com.zaneschepke.wireguardautotunnel.util.Constants
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import timber.log.Timber
|
||||
import java.net.InetAddress
|
||||
import javax.inject.Inject
|
||||
@@ -54,16 +59,24 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
|
||||
@Inject
|
||||
lateinit var serviceManager: ServiceManager
|
||||
|
||||
@Inject
|
||||
@IoDispatcher
|
||||
lateinit var ioDispatcher: CoroutineDispatcher
|
||||
|
||||
@Inject
|
||||
@MainImmediateDispatcher
|
||||
lateinit var mainImmediateDispatcher: CoroutineDispatcher
|
||||
|
||||
private val networkEventsFlow = MutableStateFlow(WatcherState())
|
||||
|
||||
private lateinit var watcherJob: Job
|
||||
private var watcherJob: Job? = null
|
||||
|
||||
private var wakeLock: PowerManager.WakeLock? = null
|
||||
private val tag = this.javaClass.name
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
lifecycleScope.launch(Dispatchers.Main) {
|
||||
lifecycleScope.launch(mainImmediateDispatcher) {
|
||||
try {
|
||||
if (appDataRepository.settings.getSettings().isAutoTunnelPaused) {
|
||||
launchWatcherPausedNotification()
|
||||
@@ -74,11 +87,6 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
stopService()
|
||||
}
|
||||
|
||||
override fun startService(extras: Bundle?) {
|
||||
super.startService(extras)
|
||||
try {
|
||||
@@ -139,14 +147,16 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
|
||||
}
|
||||
|
||||
private fun cancelWatcherJob() {
|
||||
if (this::watcherJob.isInitialized) {
|
||||
watcherJob.cancel()
|
||||
try {
|
||||
watcherJob?.cancel()
|
||||
} catch (e: CancellationException) {
|
||||
Timber.i("Watcher job cancelled")
|
||||
}
|
||||
}
|
||||
|
||||
private fun startWatcherJob() {
|
||||
watcherJob =
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
lifecycleScope.launch {
|
||||
val setting = appDataRepository.settings.getSettings()
|
||||
launch {
|
||||
Timber.i("Starting wifi watcher")
|
||||
@@ -164,10 +174,6 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
|
||||
watchForEthernetConnectivityChanges()
|
||||
}
|
||||
}
|
||||
launch {
|
||||
Timber.i("Starting vpn state watcher")
|
||||
watchForVpnConnectivityChanges()
|
||||
}
|
||||
launch {
|
||||
Timber.i("Starting settings watcher")
|
||||
watchForSettingsChanges()
|
||||
@@ -187,158 +193,170 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
|
||||
}
|
||||
|
||||
private suspend fun watchForMobileDataConnectivityChanges() {
|
||||
mobileDataService.networkStatus.collect {
|
||||
when (it) {
|
||||
is NetworkStatus.Available -> {
|
||||
Timber.i("Gained Mobile data connection")
|
||||
networkEventsFlow.value =
|
||||
networkEventsFlow.value.copy(
|
||||
isMobileDataConnected = true,
|
||||
)
|
||||
}
|
||||
withContext(ioDispatcher) {
|
||||
mobileDataService.networkStatus.collect { status ->
|
||||
when (status) {
|
||||
is NetworkStatus.Available -> {
|
||||
Timber.i("Gained Mobile data connection")
|
||||
networkEventsFlow.update {
|
||||
it.copy(
|
||||
isMobileDataConnected = true,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
is NetworkStatus.CapabilitiesChanged -> {
|
||||
networkEventsFlow.value =
|
||||
networkEventsFlow.value.copy(
|
||||
isMobileDataConnected = true,
|
||||
)
|
||||
Timber.i("Mobile data capabilities changed")
|
||||
}
|
||||
is NetworkStatus.CapabilitiesChanged -> {
|
||||
networkEventsFlow.update {
|
||||
it.copy(
|
||||
isMobileDataConnected = true,
|
||||
)
|
||||
}
|
||||
Timber.i("Mobile data capabilities changed")
|
||||
}
|
||||
|
||||
is NetworkStatus.Unavailable -> {
|
||||
networkEventsFlow.value =
|
||||
networkEventsFlow.value.copy(
|
||||
isMobileDataConnected = false,
|
||||
)
|
||||
Timber.i("Lost mobile data connection")
|
||||
is NetworkStatus.Unavailable -> {
|
||||
networkEventsFlow.update {
|
||||
it.copy(
|
||||
isMobileDataConnected = false,
|
||||
)
|
||||
}
|
||||
Timber.i("Lost mobile data connection")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun watchForPingFailure() {
|
||||
try {
|
||||
do {
|
||||
if (vpnService.vpnState.value.status == Tunnel.State.UP) {
|
||||
val tunnelConfig = vpnService.vpnState.value.tunnelConfig
|
||||
tunnelConfig?.let {
|
||||
val config = TunnelConfig.configFromQuick(it.wgQuick)
|
||||
val results = config.peers.map { peer ->
|
||||
val host = if (peer.endpoint.isPresent &&
|
||||
peer.endpoint.get().resolved.isPresent)
|
||||
peer.endpoint.get().resolved.get().host
|
||||
else Constants.BACKUP_PING_HOST
|
||||
Timber.i("Checking reachability of: $host")
|
||||
val reachable = InetAddress.getByName(host)
|
||||
.isReachable(Constants.PING_TIMEOUT.toInt())
|
||||
Timber.i("Result: reachable - $reachable")
|
||||
reachable
|
||||
}
|
||||
if (results.contains(false)) {
|
||||
Timber.i("Restarting VPN for ping failure")
|
||||
serviceManager.stopVpnService(this)
|
||||
delay(Constants.VPN_RESTART_DELAY)
|
||||
serviceManager.startVpnServiceForeground(this)
|
||||
delay(Constants.PING_COOLDOWN)
|
||||
val context = this
|
||||
withContext(ioDispatcher) {
|
||||
try {
|
||||
do {
|
||||
if (vpnService.vpnState.value.status == TunnelState.UP) {
|
||||
val tunnelConfig = vpnService.vpnState.value.tunnelConfig
|
||||
tunnelConfig?.let {
|
||||
val config = TunnelConfig.configFromWgQuick(it.wgQuick)
|
||||
val results = config.peers.map { peer ->
|
||||
val host = if (peer.endpoint.isPresent &&
|
||||
peer.endpoint.get().resolved.isPresent)
|
||||
peer.endpoint.get().resolved.get().host
|
||||
else Constants.DEFAULT_PING_IP
|
||||
Timber.i("Checking reachability of: $host")
|
||||
val reachable = InetAddress.getByName(host)
|
||||
.isReachable(Constants.PING_TIMEOUT.toInt())
|
||||
Timber.i("Result: reachable - $reachable")
|
||||
reachable
|
||||
}
|
||||
if (results.contains(false)) {
|
||||
Timber.i("Restarting VPN for ping failure")
|
||||
serviceManager.stopVpnServiceForeground(context)
|
||||
delay(Constants.VPN_RESTART_DELAY)
|
||||
serviceManager.startVpnServiceForeground(context, it.id)
|
||||
delay(Constants.PING_COOLDOWN)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
delay(Constants.PING_INTERVAL)
|
||||
} while (true)
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e)
|
||||
delay(Constants.PING_INTERVAL)
|
||||
} while (true)
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun watchForSettingsChanges() {
|
||||
appDataRepository.settings.getSettingsFlow().collect {
|
||||
if (networkEventsFlow.value.settings.isAutoTunnelPaused != it.isAutoTunnelPaused) {
|
||||
when (it.isAutoTunnelPaused) {
|
||||
appDataRepository.settings.getSettingsFlow().collect { settings ->
|
||||
if (networkEventsFlow.value.settings.isAutoTunnelPaused != settings.isAutoTunnelPaused) {
|
||||
when (settings.isAutoTunnelPaused) {
|
||||
true -> launchWatcherPausedNotification()
|
||||
false -> launchWatcherNotification()
|
||||
}
|
||||
}
|
||||
networkEventsFlow.value =
|
||||
networkEventsFlow.value.copy(
|
||||
settings = it,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun watchForVpnConnectivityChanges() {
|
||||
vpnService.vpnState.collect {
|
||||
networkEventsFlow.value =
|
||||
networkEventsFlow.value.copy(
|
||||
vpnStatus = it.status,
|
||||
config = it.tunnelConfig,
|
||||
networkEventsFlow.update {
|
||||
it.copy(
|
||||
settings = settings,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun watchForEthernetConnectivityChanges() {
|
||||
ethernetService.networkStatus.collect {
|
||||
when (it) {
|
||||
is NetworkStatus.Available -> {
|
||||
Timber.i("Gained Ethernet connection")
|
||||
networkEventsFlow.value =
|
||||
networkEventsFlow.value.copy(
|
||||
isEthernetConnected = true,
|
||||
)
|
||||
}
|
||||
withContext(ioDispatcher) {
|
||||
ethernetService.networkStatus.collect { status ->
|
||||
when (status) {
|
||||
is NetworkStatus.Available -> {
|
||||
Timber.i("Gained Ethernet connection")
|
||||
networkEventsFlow.update {
|
||||
it.copy(
|
||||
isEthernetConnected = true,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
is NetworkStatus.CapabilitiesChanged -> {
|
||||
Timber.i("Ethernet capabilities changed")
|
||||
networkEventsFlow.value =
|
||||
networkEventsFlow.value.copy(
|
||||
isEthernetConnected = true,
|
||||
)
|
||||
}
|
||||
is NetworkStatus.CapabilitiesChanged -> {
|
||||
Timber.i("Ethernet capabilities changed")
|
||||
networkEventsFlow.update {
|
||||
it.copy(
|
||||
isEthernetConnected = true,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
is NetworkStatus.Unavailable -> {
|
||||
networkEventsFlow.value =
|
||||
networkEventsFlow.value.copy(
|
||||
isEthernetConnected = false,
|
||||
)
|
||||
Timber.i("Lost Ethernet connection")
|
||||
is NetworkStatus.Unavailable -> {
|
||||
networkEventsFlow.update {
|
||||
it.copy(
|
||||
isEthernetConnected = false,
|
||||
)
|
||||
}
|
||||
Timber.i("Lost Ethernet connection")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun watchForWifiConnectivityChanges() {
|
||||
wifiService.networkStatus.collect {
|
||||
when (it) {
|
||||
is NetworkStatus.Available -> {
|
||||
Timber.i("Gained Wi-Fi connection")
|
||||
networkEventsFlow.value =
|
||||
networkEventsFlow.value.copy(
|
||||
isWifiConnected = true,
|
||||
)
|
||||
}
|
||||
|
||||
is NetworkStatus.CapabilitiesChanged -> {
|
||||
Timber.i("Wifi capabilities changed")
|
||||
networkEventsFlow.value =
|
||||
networkEventsFlow.value.copy(
|
||||
isWifiConnected = true,
|
||||
)
|
||||
val ssid = wifiService.getNetworkName(it.networkCapabilities)
|
||||
ssid?.let {
|
||||
Timber.i("Detected SSID: $ssid")
|
||||
appDataRepository.appState.setCurrentSsid(ssid)
|
||||
networkEventsFlow.value =
|
||||
networkEventsFlow.value.copy(
|
||||
currentNetworkSSID = ssid,
|
||||
withContext(ioDispatcher) {
|
||||
wifiService.networkStatus.collect { status ->
|
||||
when (status) {
|
||||
is NetworkStatus.Available -> {
|
||||
Timber.i("Gained Wi-Fi connection")
|
||||
networkEventsFlow.update {
|
||||
it.copy(
|
||||
isWifiConnected = true,
|
||||
)
|
||||
} ?: Timber.w("Failed to read ssid")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
is NetworkStatus.Unavailable -> {
|
||||
networkEventsFlow.value =
|
||||
networkEventsFlow.value.copy(
|
||||
isWifiConnected = false,
|
||||
)
|
||||
Timber.i("Lost Wi-Fi connection")
|
||||
is NetworkStatus.CapabilitiesChanged -> {
|
||||
Timber.i("Wifi capabilities changed")
|
||||
networkEventsFlow.update {
|
||||
it.copy(
|
||||
isWifiConnected = true,
|
||||
)
|
||||
}
|
||||
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 -> {
|
||||
networkEventsFlow.update {
|
||||
it.copy(
|
||||
isWifiConnected = false,
|
||||
)
|
||||
}
|
||||
Timber.i("Lost Wi-Fi connection")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -352,76 +370,89 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
|
||||
return appDataRepository.tunnels.findByTunnelNetworksName(ssid).firstOrNull()
|
||||
}
|
||||
|
||||
private fun isTunnelDown(): Boolean {
|
||||
return vpnService.vpnState.value.status == TunnelState.DOWN
|
||||
}
|
||||
|
||||
private suspend fun manageVpn() {
|
||||
networkEventsFlow.collectLatest { watcherState ->
|
||||
val autoTunnel = "Auto-tunnel watcher"
|
||||
if (!watcherState.settings.isAutoTunnelPaused) {
|
||||
//delay for rapid network state changes and then collect latest
|
||||
delay(Constants.WATCHER_COLLECTION_DELAY)
|
||||
when {
|
||||
watcherState.isEthernetConditionMet() -> {
|
||||
Timber.i("$autoTunnel - tunnel on on ethernet condition met")
|
||||
serviceManager.startVpnServiceForeground(this)
|
||||
}
|
||||
|
||||
watcherState.isMobileDataConditionMet() -> {
|
||||
Timber.i("$autoTunnel - tunnel on on mobile data condition met")
|
||||
serviceManager.startVpnServiceForeground(this, getMobileDataTunnel()?.id)
|
||||
}
|
||||
|
||||
watcherState.isTunnelNotMobileDataPreferredConditionMet() -> {
|
||||
getMobileDataTunnel()?.let {
|
||||
Timber.i("$autoTunnel - tunnel connected on mobile data is not preferred condition met, switching to preferred")
|
||||
serviceManager.startVpnServiceForeground(
|
||||
this,
|
||||
getMobileDataTunnel()?.id,
|
||||
)
|
||||
val context = this
|
||||
withContext(ioDispatcher) {
|
||||
networkEventsFlow.collectLatest { watcherState ->
|
||||
val autoTunnel = "Auto-tunnel watcher"
|
||||
if (!watcherState.settings.isAutoTunnelPaused) {
|
||||
//delay for rapid network state changes and then collect latest
|
||||
delay(Constants.WATCHER_COLLECTION_DELAY)
|
||||
val tunnelConfig = vpnService.vpnState.value.tunnelConfig
|
||||
when {
|
||||
watcherState.isEthernetConditionMet() -> {
|
||||
Timber.i("$autoTunnel - tunnel on on ethernet condition met")
|
||||
if (isTunnelDown()) serviceManager.startVpnServiceForeground(context)
|
||||
}
|
||||
}
|
||||
|
||||
watcherState.isTunnelOffOnMobileDataConditionMet() -> {
|
||||
Timber.i("$autoTunnel - tunnel off on mobile data met, turning vpn off")
|
||||
serviceManager.stopVpnService(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)
|
||||
watcherState.isMobileDataConditionMet() -> {
|
||||
Timber.i("$autoTunnel - tunnel on on mobile data condition met")
|
||||
val mobileDataTunnel = getMobileDataTunnel()
|
||||
val tunnel =
|
||||
mobileDataTunnel ?: appDataRepository.getPrimaryOrFirstTunnel()
|
||||
if (isTunnelDown()) return@collectLatest serviceManager.startVpnServiceForeground(
|
||||
context,
|
||||
tunnel?.id,
|
||||
)
|
||||
if (tunnelConfig?.isMobileDataTunnel == false && mobileDataTunnel != null) {
|
||||
Timber.i("$autoTunnel - tunnel connected on mobile data is not preferred condition met, switching to preferred")
|
||||
serviceManager.startVpnServiceForeground(
|
||||
context,
|
||||
mobileDataTunnel.id,
|
||||
)
|
||||
}
|
||||
}.invoke()
|
||||
}
|
||||
}
|
||||
|
||||
watcherState.isUntrustedWifiConditionMet() -> {
|
||||
Timber.i("$autoTunnel - tunnel on untrusted wifi condition met")
|
||||
serviceManager.startVpnServiceForeground(
|
||||
this,
|
||||
getSsidTunnel(watcherState.currentNetworkSSID)?.id,
|
||||
)
|
||||
}
|
||||
watcherState.isTunnelOffOnMobileDataConditionMet() -> {
|
||||
Timber.i("$autoTunnel - tunnel off on mobile data met, turning vpn off")
|
||||
if (!isTunnelDown()) serviceManager.stopVpnServiceForeground(context)
|
||||
}
|
||||
|
||||
watcherState.isTrustedWifiConditionMet() -> {
|
||||
Timber.i("$autoTunnel - tunnel off on trusted wifi condition met, turning vpn off")
|
||||
serviceManager.stopVpnService(this)
|
||||
}
|
||||
watcherState.isUntrustedWifiConditionMet() -> {
|
||||
if (tunnelConfig?.tunnelNetworks?.contains(watcherState.currentNetworkSSID) == false ||
|
||||
tunnelConfig == null) {
|
||||
Timber.i("$autoTunnel - tunnel on ssid not associated with current tunnel condition met")
|
||||
getSsidTunnel(watcherState.currentNetworkSSID)?.let {
|
||||
Timber.i("Found tunnel associated with this SSID, bringing tunnel up")
|
||||
if (isTunnelDown()) serviceManager.startVpnServiceForeground(
|
||||
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() -> {
|
||||
Timber.i("$autoTunnel - tunnel off on wifi condition met, turning vpn off")
|
||||
serviceManager.stopVpnService(this)
|
||||
}
|
||||
}
|
||||
}.invoke()
|
||||
}
|
||||
}
|
||||
|
||||
watcherState.isTunnelOffOnNoConnectivityMet() -> {
|
||||
Timber.i("$autoTunnel - tunnel off on no connectivity met, turning vpn off")
|
||||
serviceManager.stopVpnService(this)
|
||||
}
|
||||
watcherState.isTrustedWifiConditionMet() -> {
|
||||
Timber.i("$autoTunnel - tunnel off on trusted wifi condition met, turning vpn off")
|
||||
if (!isTunnelDown()) serviceManager.stopVpnServiceForeground(context)
|
||||
}
|
||||
|
||||
else -> {
|
||||
Timber.i("$autoTunnel - no condition met")
|
||||
watcherState.isTunnelOffOnWifiConditionMet() -> {
|
||||
Timber.i("$autoTunnel - tunnel off on wifi condition met, turning vpn off")
|
||||
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")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+54
-42
@@ -5,21 +5,26 @@ import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import androidx.core.app.ServiceCompat
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.wireguard.android.backend.Tunnel
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
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.service.notification.NotificationService
|
||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.HandshakeStatus
|
||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelState
|
||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnService
|
||||
import com.zaneschepke.wireguardautotunnel.util.Constants
|
||||
import com.zaneschepke.wireguardautotunnel.util.handshakeStatus
|
||||
import com.zaneschepke.wireguardautotunnel.util.mapPeerStats
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
@AndroidEntryPoint
|
||||
@@ -35,13 +40,21 @@ class WireGuardTunnelService : ForegroundService() {
|
||||
@Inject
|
||||
lateinit var notificationService: NotificationService
|
||||
|
||||
private lateinit var job: Job
|
||||
@Inject
|
||||
@MainImmediateDispatcher
|
||||
lateinit var mainImmediateDispatcher: CoroutineDispatcher
|
||||
|
||||
@Inject
|
||||
@IoDispatcher
|
||||
lateinit var ioDispatcher: CoroutineDispatcher
|
||||
|
||||
private var job: Job? = null
|
||||
|
||||
private var didShowConnected = false
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
lifecycleScope.launch(Dispatchers.Main) {
|
||||
lifecycleScope.launch(mainImmediateDispatcher) {
|
||||
//TODO fix this to not launch if AOVPN
|
||||
if (appDataRepository.tunnels.count() != 0) {
|
||||
launchVpnNotification()
|
||||
@@ -49,19 +62,14 @@ class WireGuardTunnelService : ForegroundService() {
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
|
||||
}
|
||||
|
||||
override fun startService(extras: Bundle?) {
|
||||
super.startService(extras)
|
||||
cancelJob()
|
||||
job =
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
lifecycleScope.launch {
|
||||
launch {
|
||||
val tunnelId = extras?.getInt(Constants.TUNNEL_EXTRA_KEY)
|
||||
if (vpnService.getState() == Tunnel.State.UP) {
|
||||
if (vpnService.getState() == TunnelState.UP) {
|
||||
vpnService.stopTunnel()
|
||||
}
|
||||
vpnService.startTunnel(
|
||||
@@ -78,39 +86,41 @@ class WireGuardTunnelService : ForegroundService() {
|
||||
|
||||
//TODO improve tunnel notifications
|
||||
private suspend fun handshakeNotifications() {
|
||||
var tunnelName: String? = null
|
||||
vpnService.vpnState.collect { state ->
|
||||
state.statistics
|
||||
?.mapPeerStats()
|
||||
?.map { it.value?.handshakeStatus() }
|
||||
.let { statuses ->
|
||||
when {
|
||||
statuses?.all { it == HandshakeStatus.HEALTHY } == true -> {
|
||||
if (!didShowConnected) {
|
||||
delay(Constants.VPN_CONNECTED_NOTIFICATION_DELAY)
|
||||
tunnelName = state.tunnelConfig?.name
|
||||
launchVpnNotification(
|
||||
getString(R.string.tunnel_start_title),
|
||||
"${getString(R.string.tunnel_start_text)} - $tunnelName",
|
||||
)
|
||||
didShowConnected = true
|
||||
withContext(ioDispatcher) {
|
||||
var tunnelName: String? = null
|
||||
vpnService.vpnState.collect { state ->
|
||||
state.statistics
|
||||
?.mapPeerStats()
|
||||
?.map { it.value?.handshakeStatus() }
|
||||
.let { statuses ->
|
||||
when {
|
||||
statuses?.all { it == HandshakeStatus.HEALTHY } == true -> {
|
||||
if (!didShowConnected) {
|
||||
delay(Constants.VPN_CONNECTED_NOTIFICATION_DELAY)
|
||||
tunnelName = state.tunnelConfig?.name
|
||||
launchVpnNotification(
|
||||
getString(R.string.tunnel_start_title),
|
||||
"${getString(R.string.tunnel_start_text)} - $tunnelName",
|
||||
)
|
||||
didShowConnected = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
statuses?.any { it == HandshakeStatus.STALE } == true -> {}
|
||||
statuses?.all { it == HandshakeStatus.NOT_STARTED } ==
|
||||
true -> {
|
||||
}
|
||||
statuses?.any { it == HandshakeStatus.STALE } == true -> {}
|
||||
statuses?.all { it == HandshakeStatus.NOT_STARTED } ==
|
||||
true -> {
|
||||
}
|
||||
|
||||
else -> {}
|
||||
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",
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -124,7 +134,7 @@ class WireGuardTunnelService : ForegroundService() {
|
||||
|
||||
override fun stopService() {
|
||||
super.stopService()
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
lifecycleScope.launch {
|
||||
vpnService.stopTunnel()
|
||||
didShowConnected = false
|
||||
}
|
||||
@@ -182,8 +192,10 @@ class WireGuardTunnelService : ForegroundService() {
|
||||
}
|
||||
|
||||
private fun cancelJob() {
|
||||
if (this::job.isInitialized) {
|
||||
job.cancel()
|
||||
try {
|
||||
job?.cancel()
|
||||
} catch (e: CancellationException) {
|
||||
Timber.i("Tunnel job cancelled")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+8
-4
@@ -2,14 +2,14 @@ package com.zaneschepke.wireguardautotunnel.service.shortcut
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
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.ServiceManager
|
||||
import com.zaneschepke.wireguardautotunnel.service.foreground.WireGuardConnectivityWatcherService
|
||||
import com.zaneschepke.wireguardautotunnel.service.foreground.WireGuardTunnelService
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
@@ -22,9 +22,13 @@ class ShortcutsActivity : ComponentActivity() {
|
||||
@Inject
|
||||
lateinit var serviceManager: ServiceManager
|
||||
|
||||
@Inject
|
||||
@ApplicationScope
|
||||
lateinit var applicationScope: CoroutineScope
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
lifecycleScope.launch(Dispatchers.Main) {
|
||||
applicationScope.launch {
|
||||
val settings = appDataRepository.settings.getSettings()
|
||||
if (settings.isShortcutsEnabled) {
|
||||
when (intent.getStringExtra(CLASS_NAME_EXTRA_KEY)) {
|
||||
@@ -40,7 +44,7 @@ class ShortcutsActivity : ComponentActivity() {
|
||||
this@ShortcutsActivity, tunnelConfig?.id, isManualStart = true,
|
||||
)
|
||||
|
||||
Action.STOP.name -> serviceManager.stopVpnService(
|
||||
Action.STOP.name -> serviceManager.stopVpnServiceForeground(
|
||||
this@ShortcutsActivity,
|
||||
isManualStop = true,
|
||||
)
|
||||
|
||||
+20
-30
@@ -4,14 +4,13 @@ import android.os.Build
|
||||
import android.service.quicksettings.Tile
|
||||
import android.service.quicksettings.TileService
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
|
||||
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
|
||||
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
|
||||
import com.zaneschepke.wireguardautotunnel.module.ApplicationScope
|
||||
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
@@ -25,29 +24,30 @@ class AutoTunnelControlTile : TileService() {
|
||||
@Inject
|
||||
lateinit var serviceManager: ServiceManager
|
||||
|
||||
private val scope = CoroutineScope(Dispatchers.IO)
|
||||
@Inject
|
||||
@ApplicationScope
|
||||
lateinit var applicationScope: CoroutineScope
|
||||
|
||||
private var manualStartConfig: TunnelConfig? = null
|
||||
|
||||
override fun onStartListening() {
|
||||
super.onStartListening()
|
||||
scope.launch {
|
||||
appDataRepository.settings.getSettingsFlow().collectLatest {
|
||||
when (it.isAutoTunnelEnabled) {
|
||||
true -> {
|
||||
if (it.isAutoTunnelPaused) {
|
||||
setInactive()
|
||||
setTileDescription(this@AutoTunnelControlTile.getString(R.string.paused))
|
||||
} else {
|
||||
setActive()
|
||||
setTileDescription(this@AutoTunnelControlTile.getString(R.string.active))
|
||||
}
|
||||
applicationScope.launch {
|
||||
val settings = appDataRepository.settings.getSettings()
|
||||
when (settings.isAutoTunnelEnabled) {
|
||||
true -> {
|
||||
if (settings.isAutoTunnelPaused) {
|
||||
setInactive()
|
||||
setTileDescription(this@AutoTunnelControlTile.getString(R.string.paused))
|
||||
} else {
|
||||
setActive()
|
||||
setTileDescription(this@AutoTunnelControlTile.getString(R.string.active))
|
||||
}
|
||||
}
|
||||
|
||||
false -> {
|
||||
setTileDescription(this@AutoTunnelControlTile.getString(R.string.disabled))
|
||||
setUnavailable()
|
||||
}
|
||||
false -> {
|
||||
setTileDescription(this@AutoTunnelControlTile.getString(R.string.disabled))
|
||||
setUnavailable()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -58,20 +58,10 @@ class AutoTunnelControlTile : TileService() {
|
||||
onStartListening()
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
scope.cancel()
|
||||
}
|
||||
|
||||
override fun onTileRemoved() {
|
||||
super.onTileRemoved()
|
||||
scope.cancel()
|
||||
}
|
||||
|
||||
override fun onClick() {
|
||||
super.onClick()
|
||||
unlockAndRun {
|
||||
scope.launch {
|
||||
applicationScope.launch {
|
||||
try {
|
||||
appDataRepository.toggleWatcherServicePause()
|
||||
} catch (e: Exception) {
|
||||
|
||||
+16
-20
@@ -3,14 +3,15 @@ package com.zaneschepke.wireguardautotunnel.service.tile
|
||||
import android.os.Build
|
||||
import android.service.quicksettings.Tile
|
||||
import android.service.quicksettings.TileService
|
||||
import com.wireguard.android.backend.Tunnel
|
||||
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
|
||||
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
|
||||
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
|
||||
import com.zaneschepke.wireguardautotunnel.module.ApplicationScope
|
||||
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
|
||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelState
|
||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnService
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
@@ -28,22 +29,27 @@ class TunnelControlTile : TileService() {
|
||||
@Inject
|
||||
lateinit var serviceManager: ServiceManager
|
||||
|
||||
private val scope = CoroutineScope(Dispatchers.IO)
|
||||
@Inject
|
||||
@ApplicationScope
|
||||
lateinit var applicationScope: CoroutineScope
|
||||
|
||||
private var manualStartConfig: TunnelConfig? = null
|
||||
|
||||
private var job: Job? = null;
|
||||
|
||||
override fun onStartListening() {
|
||||
super.onStartListening()
|
||||
Timber.d("On start listening called")
|
||||
scope.launch {
|
||||
//TODO Fix this
|
||||
if (job == null || job?.isCancelled == true) job = applicationScope.launch {
|
||||
vpnService.vpnState.collect { it ->
|
||||
when (it.status) {
|
||||
Tunnel.State.UP -> {
|
||||
TunnelState.UP -> {
|
||||
setActive()
|
||||
it.tunnelConfig?.name?.let { name -> setTileDescription(name) }
|
||||
}
|
||||
|
||||
Tunnel.State.DOWN -> {
|
||||
TunnelState.DOWN -> {
|
||||
setInactive()
|
||||
val config = appDataRepository.getStartTunnelConfig()?.also { config ->
|
||||
manualStartConfig = config
|
||||
@@ -59,16 +65,6 @@ class TunnelControlTile : TileService() {
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
scope.cancel()
|
||||
}
|
||||
|
||||
override fun onTileRemoved() {
|
||||
super.onTileRemoved()
|
||||
scope.cancel()
|
||||
}
|
||||
|
||||
override fun onTileAdded() {
|
||||
super.onTileAdded()
|
||||
onStartListening()
|
||||
@@ -77,10 +73,10 @@ class TunnelControlTile : TileService() {
|
||||
override fun onClick() {
|
||||
super.onClick()
|
||||
unlockAndRun {
|
||||
scope.launch {
|
||||
applicationScope.launch {
|
||||
try {
|
||||
if (vpnService.getState() == Tunnel.State.UP) {
|
||||
serviceManager.stopVpnService(
|
||||
if (vpnService.getState() == TunnelState.UP) {
|
||||
serviceManager.stopVpnServiceForeground(
|
||||
this@TunnelControlTile,
|
||||
isManualStop = true,
|
||||
)
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
package com.zaneschepke.wireguardautotunnel.service.tunnel
|
||||
|
||||
import com.wireguard.android.backend.Tunnel
|
||||
|
||||
enum class TunnelState {
|
||||
UP,
|
||||
DOWN,
|
||||
TOGGLE;
|
||||
|
||||
fun toWgState(): Tunnel.State {
|
||||
return when (this) {
|
||||
UP -> Tunnel.State.UP
|
||||
DOWN -> Tunnel.State.DOWN
|
||||
TOGGLE -> Tunnel.State.TOGGLE
|
||||
}
|
||||
}
|
||||
|
||||
fun toAmState(): org.amnezia.awg.backend.Tunnel.State {
|
||||
return when (this) {
|
||||
UP -> org.amnezia.awg.backend.Tunnel.State.UP
|
||||
DOWN -> org.amnezia.awg.backend.Tunnel.State.DOWN
|
||||
TOGGLE -> org.amnezia.awg.backend.Tunnel.State.TOGGLE
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun from(state: Tunnel.State): TunnelState {
|
||||
return when (state) {
|
||||
Tunnel.State.DOWN -> DOWN
|
||||
Tunnel.State.TOGGLE -> TOGGLE
|
||||
Tunnel.State.UP -> UP
|
||||
}
|
||||
}
|
||||
|
||||
fun from(state: org.amnezia.awg.backend.Tunnel.State): TunnelState {
|
||||
return when (state) {
|
||||
org.amnezia.awg.backend.Tunnel.State.DOWN -> DOWN
|
||||
org.amnezia.awg.backend.Tunnel.State.TOGGLE -> TOGGLE
|
||||
org.amnezia.awg.backend.Tunnel.State.UP -> UP
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,15 +1,15 @@
|
||||
package com.zaneschepke.wireguardautotunnel.service.tunnel
|
||||
|
||||
import com.wireguard.android.backend.Tunnel
|
||||
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
|
||||
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
|
||||
interface VpnService : Tunnel {
|
||||
suspend fun startTunnel(tunnelConfig: TunnelConfig? = null): Tunnel.State
|
||||
interface VpnService : Tunnel, org.amnezia.awg.backend.Tunnel {
|
||||
suspend fun startTunnel(tunnelConfig: TunnelConfig? = null): TunnelState
|
||||
|
||||
suspend fun stopTunnel()
|
||||
|
||||
val vpnState: StateFlow<VpnState>
|
||||
|
||||
fun getState(): Tunnel.State
|
||||
fun getState(): TunnelState
|
||||
}
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
package com.zaneschepke.wireguardautotunnel.service.tunnel
|
||||
|
||||
import com.wireguard.android.backend.Statistics
|
||||
import com.wireguard.android.backend.Tunnel
|
||||
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
|
||||
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
|
||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.statistics.TunnelStatistics
|
||||
|
||||
data class VpnState(
|
||||
val status: Tunnel.State = Tunnel.State.DOWN,
|
||||
val status: TunnelState = TunnelState.DOWN,
|
||||
val tunnelConfig: TunnelConfig? = null,
|
||||
val statistics: Statistics? = null
|
||||
val statistics: TunnelStatistics? = null
|
||||
)
|
||||
|
||||
+115
-56
@@ -2,81 +2,116 @@ package com.zaneschepke.wireguardautotunnel.service.tunnel
|
||||
|
||||
import com.wireguard.android.backend.Backend
|
||||
import com.wireguard.android.backend.BackendException
|
||||
import com.wireguard.android.backend.Statistics
|
||||
import com.wireguard.android.backend.Tunnel.State
|
||||
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
|
||||
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
|
||||
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
|
||||
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
|
||||
import com.zaneschepke.wireguardautotunnel.module.ApplicationScope
|
||||
import com.zaneschepke.wireguardautotunnel.module.IoDispatcher
|
||||
import com.zaneschepke.wireguardautotunnel.module.Kernel
|
||||
import com.zaneschepke.wireguardautotunnel.module.Userspace
|
||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.statistics.AmneziaStatistics
|
||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.statistics.TunnelStatistics
|
||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.statistics.WireGuardStatistics
|
||||
import com.zaneschepke.wireguardautotunnel.util.Constants
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.amnezia.awg.backend.Tunnel
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
class WireGuardTunnel
|
||||
@Inject
|
||||
constructor(
|
||||
private val userspaceAmneziaBackend: org.amnezia.awg.backend.Backend,
|
||||
@Userspace private val userspaceBackend: Backend,
|
||||
@Kernel private val kernelBackend: Backend,
|
||||
private val appDataRepository: AppDataRepository,
|
||||
@ApplicationScope private val applicationScope: CoroutineScope,
|
||||
@IoDispatcher private val ioDispatcher: CoroutineDispatcher
|
||||
) : VpnService {
|
||||
private val _vpnState = MutableStateFlow(VpnState())
|
||||
override val vpnState: StateFlow<VpnState> = _vpnState.asStateFlow()
|
||||
|
||||
private val scope = CoroutineScope(Dispatchers.IO)
|
||||
|
||||
private lateinit var statsJob: Job
|
||||
private var statsJob: Job? = null
|
||||
|
||||
private var backend: Backend = userspaceBackend
|
||||
|
||||
private var backendIsUserspace = true
|
||||
private var backendIsWgUserspace = true
|
||||
|
||||
private var backendIsAmneziaUserspace = false
|
||||
|
||||
init {
|
||||
scope.launch {
|
||||
applicationScope.launch(ioDispatcher) {
|
||||
appDataRepository.settings.getSettingsFlow().collect {
|
||||
if (it.isKernelEnabled && backendIsUserspace) {
|
||||
Timber.d("Setting kernel backend")
|
||||
if (it.isKernelEnabled && (backendIsWgUserspace || backendIsAmneziaUserspace)) {
|
||||
Timber.i("Setting kernel backend")
|
||||
backend = kernelBackend
|
||||
backendIsUserspace = false
|
||||
} else if (!it.isKernelEnabled && !backendIsUserspace) {
|
||||
Timber.d("Setting userspace backend")
|
||||
backendIsWgUserspace = false
|
||||
backendIsAmneziaUserspace = false
|
||||
} else if (!it.isKernelEnabled && !it.isAmneziaEnabled && !backendIsWgUserspace) {
|
||||
Timber.i("Setting WireGuard userspace backend")
|
||||
backend = userspaceBackend
|
||||
backendIsUserspace = true
|
||||
backendIsWgUserspace = true
|
||||
backendIsAmneziaUserspace = false
|
||||
} else if (it.isAmneziaEnabled && !backendIsAmneziaUserspace) {
|
||||
Timber.i("Setting Amnezia userspace backend")
|
||||
backendIsAmneziaUserspace = true
|
||||
backendIsWgUserspace = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun startTunnel(tunnelConfig: TunnelConfig?): State {
|
||||
return try {
|
||||
//TODO we need better error handling here
|
||||
val config = tunnelConfig ?: appDataRepository.getPrimaryOrFirstTunnel()
|
||||
if (config != null) {
|
||||
emitTunnelConfig(config)
|
||||
val wgConfig = TunnelConfig.configFromQuick(config.wgQuick)
|
||||
val state =
|
||||
backend.setState(
|
||||
this,
|
||||
State.UP,
|
||||
wgConfig,
|
||||
)
|
||||
state
|
||||
} else throw Exception("No tunnels")
|
||||
} catch (e: BackendException) {
|
||||
Timber.e("Failed to start tunnel with error: ${e.message}")
|
||||
State.DOWN
|
||||
private fun setState(tunnelConfig: TunnelConfig?, tunnelState: TunnelState): TunnelState {
|
||||
return if (backendIsAmneziaUserspace) {
|
||||
Timber.i("Using Amnezia backend")
|
||||
val config = tunnelConfig?.let {
|
||||
if (it.amQuick != "") TunnelConfig.configFromAmQuick(it.amQuick) else {
|
||||
Timber.w("Using backwards compatible wg config, amnezia specific config not found.")
|
||||
TunnelConfig.configFromAmQuick(it.wgQuick)
|
||||
}
|
||||
}
|
||||
val state = userspaceAmneziaBackend.setState(this, tunnelState.toAmState(), config)
|
||||
TunnelState.from(state)
|
||||
} else {
|
||||
Timber.i("Using Wg backend")
|
||||
val wgConfig = tunnelConfig?.let { TunnelConfig.configFromWgQuick(it.wgQuick) }
|
||||
val state = backend.setState(
|
||||
this,
|
||||
tunnelState.toWgState(),
|
||||
wgConfig,
|
||||
)
|
||||
TunnelState.from(state)
|
||||
}
|
||||
}
|
||||
|
||||
private fun emitTunnelState(state: State) {
|
||||
override suspend fun startTunnel(tunnelConfig: TunnelConfig?): TunnelState {
|
||||
return withContext(ioDispatcher) {
|
||||
try {
|
||||
//TODO we need better error handling here
|
||||
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 emitTunnelState(state: TunnelState) {
|
||||
_vpnState.tryEmit(
|
||||
_vpnState.value.copy(
|
||||
status = state,
|
||||
@@ -84,7 +119,7 @@ constructor(
|
||||
)
|
||||
}
|
||||
|
||||
private fun emitBackendStatistics(statistics: Statistics) {
|
||||
private fun emitBackendStatistics(statistics: TunnelStatistics) {
|
||||
_vpnState.tryEmit(
|
||||
_vpnState.value.copy(
|
||||
statistics = statistics,
|
||||
@@ -100,43 +135,67 @@ constructor(
|
||||
)
|
||||
}
|
||||
|
||||
private fun resetVpnState() {
|
||||
_vpnState.tryEmit(VpnState())
|
||||
}
|
||||
|
||||
override suspend fun stopTunnel() {
|
||||
try {
|
||||
if (getState() == State.UP) {
|
||||
val state = backend.setState(this, State.DOWN, null)
|
||||
emitTunnelState(state)
|
||||
withContext(ioDispatcher) {
|
||||
try {
|
||||
if (getState() == TunnelState.UP) {
|
||||
val state = setState(null, TunnelState.DOWN)
|
||||
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(): State {
|
||||
return backend.getState(this)
|
||||
override fun getState(): TunnelState {
|
||||
return if (backendIsAmneziaUserspace) TunnelState.from(userspaceAmneziaBackend.getState(this))
|
||||
else TunnelState.from(backend.getState(this))
|
||||
}
|
||||
|
||||
override fun getName(): String {
|
||||
return _vpnState.value.tunnelConfig?.name ?: ""
|
||||
}
|
||||
|
||||
override fun onStateChange(state: State) {
|
||||
val tunnel = this
|
||||
|
||||
override fun onStateChange(newState: Tunnel.State) {
|
||||
handleStateChange(TunnelState.from(newState))
|
||||
}
|
||||
|
||||
private fun handleStateChange(state: TunnelState) {
|
||||
emitTunnelState(state)
|
||||
WireGuardAutoTunnel.requestTunnelTileServiceStateUpdate(WireGuardAutoTunnel.instance)
|
||||
if (state == State.UP) {
|
||||
statsJob =
|
||||
scope.launch {
|
||||
while (true) {
|
||||
val statistics = backend.getStatistics(tunnel)
|
||||
emitBackendStatistics(statistics)
|
||||
delay(Constants.VPN_STATISTIC_CHECK_INTERVAL)
|
||||
}
|
||||
}
|
||||
WireGuardAutoTunnel.requestTunnelTileServiceStateUpdate()
|
||||
if (state == TunnelState.UP) {
|
||||
statsJob = startTunnelStatisticsJob()
|
||||
}
|
||||
if (state == State.DOWN) {
|
||||
if (this::statsJob.isInitialized) {
|
||||
statsJob.cancel()
|
||||
if (state == TunnelState.DOWN) {
|
||||
try {
|
||||
statsJob?.cancel()
|
||||
} catch (e: CancellationException) {
|
||||
Timber.i("Stats job cancelled")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun startTunnelStatisticsJob() = applicationScope.launch(ioDispatcher) {
|
||||
while (true) {
|
||||
if (backendIsAmneziaUserspace) {
|
||||
emitBackendStatistics(AmneziaStatistics(userspaceAmneziaBackend.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))
|
||||
}
|
||||
}
|
||||
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
package com.zaneschepke.wireguardautotunnel.service.tunnel.statistics
|
||||
|
||||
import org.amnezia.awg.backend.Statistics
|
||||
import org.amnezia.awg.crypto.Key
|
||||
|
||||
class AmneziaStatistics(private val statistics: Statistics) : TunnelStatistics() {
|
||||
override fun peerStats(peer: Key): PeerStats? {
|
||||
val key = Key.fromBase64(peer.toBase64())
|
||||
val stats = statistics.peer(key)
|
||||
return stats?.let {
|
||||
PeerStats(
|
||||
rxBytes = stats.rxBytes,
|
||||
txBytes = stats.txBytes,
|
||||
latestHandshakeEpochMillis = stats.latestHandshakeEpochMillis,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun isTunnelStale(): Boolean {
|
||||
return statistics.isStale
|
||||
}
|
||||
|
||||
override fun getPeers(): Array<Key> {
|
||||
return statistics.peers()
|
||||
}
|
||||
|
||||
override fun rx(): Long {
|
||||
return statistics.totalRx()
|
||||
}
|
||||
|
||||
override fun tx(): Long {
|
||||
return statistics.totalTx()
|
||||
}
|
||||
}
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
package com.zaneschepke.wireguardautotunnel.service.tunnel.statistics
|
||||
|
||||
import org.amnezia.awg.crypto.Key
|
||||
|
||||
abstract class TunnelStatistics {
|
||||
@JvmRecord
|
||||
data class PeerStats(val rxBytes: Long, val txBytes: Long, val latestHandshakeEpochMillis: Long)
|
||||
|
||||
abstract fun peerStats(peer: Key): PeerStats?
|
||||
|
||||
abstract fun isTunnelStale(): Boolean
|
||||
|
||||
abstract fun getPeers(): Array<Key>
|
||||
|
||||
abstract fun rx(): Long
|
||||
|
||||
abstract fun tx(): Long
|
||||
}
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
package com.zaneschepke.wireguardautotunnel.service.tunnel.statistics
|
||||
|
||||
import com.wireguard.android.backend.Statistics
|
||||
import org.amnezia.awg.crypto.Key
|
||||
|
||||
class WireGuardStatistics(private val statistics: Statistics) : TunnelStatistics() {
|
||||
override fun peerStats(peer: Key): PeerStats? {
|
||||
val key = com.wireguard.crypto.Key.fromBase64(peer.toBase64())
|
||||
val peerStats = statistics.peer(key)
|
||||
return peerStats?.let {
|
||||
PeerStats(
|
||||
txBytes = peerStats.txBytes,
|
||||
rxBytes = peerStats.rxBytes,
|
||||
latestHandshakeEpochMillis = peerStats.latestHandshakeEpochMillis,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun isTunnelStale(): Boolean {
|
||||
return statistics.isStale
|
||||
}
|
||||
|
||||
override fun getPeers(): Array<Key> {
|
||||
return statistics.peers().map {
|
||||
Key.fromBase64(it.toBase64())
|
||||
}.toTypedArray()
|
||||
}
|
||||
|
||||
override fun rx(): Long {
|
||||
return statistics.totalRx()
|
||||
}
|
||||
|
||||
override fun tx(): Long {
|
||||
return statistics.totalTx()
|
||||
}
|
||||
}
|
||||
@@ -1,35 +1,25 @@
|
||||
package com.zaneschepke.wireguardautotunnel.ui
|
||||
|
||||
import android.app.Application
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.widget.Toast
|
||||
import androidx.compose.runtime.mutableStateListOf
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.wireguard.android.backend.GoBackend
|
||||
import com.zaneschepke.logcatter.Logcatter
|
||||
import com.zaneschepke.logcatter.model.LogMessage
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
|
||||
import com.zaneschepke.wireguardautotunnel.util.Constants
|
||||
import com.zaneschepke.wireguardautotunnel.util.FileUtils
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.flow.update
|
||||
import timber.log.Timber
|
||||
import java.time.Instant
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class AppViewModel
|
||||
@Inject
|
||||
constructor(
|
||||
private val application: Application,
|
||||
) : ViewModel() {
|
||||
constructor() : ViewModel() {
|
||||
|
||||
val vpnIntent: Intent? = GoBackend.VpnService.prepare(WireGuardAutoTunnel.instance)
|
||||
|
||||
@@ -49,100 +39,85 @@ constructor(
|
||||
}
|
||||
|
||||
private fun requestPermissions() {
|
||||
_appUiState.value = _appUiState.value.copy(
|
||||
requestPermissions = true,
|
||||
)
|
||||
_appUiState.update {
|
||||
it.copy(
|
||||
requestPermissions = true,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun permissionsRequested() {
|
||||
_appUiState.value = _appUiState.value.copy(
|
||||
requestPermissions = false,
|
||||
)
|
||||
_appUiState.update {
|
||||
it.copy(
|
||||
requestPermissions = false,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun openWebPage(url: String) {
|
||||
fun openWebPage(url: String, context: Context) {
|
||||
try {
|
||||
val webpage: Uri = Uri.parse(url)
|
||||
val intent = Intent(Intent.ACTION_VIEW, webpage).apply {
|
||||
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
}
|
||||
application.startActivity(intent)
|
||||
context.startActivity(intent)
|
||||
} catch (e: ActivityNotFoundException) {
|
||||
Timber.e(e)
|
||||
showSnackbarMessage(application.getString(R.string.no_browser_detected))
|
||||
showSnackbarMessage(context.getString(R.string.no_browser_detected))
|
||||
}
|
||||
}
|
||||
|
||||
fun onVpnPermissionAccepted() {
|
||||
_appUiState.value = _appUiState.value.copy(
|
||||
vpnPermissionAccepted = true,
|
||||
)
|
||||
_appUiState.update {
|
||||
it.copy(
|
||||
vpnPermissionAccepted = true,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun launchEmail() {
|
||||
fun launchEmail(context: Context) {
|
||||
try {
|
||||
val intent =
|
||||
Intent(Intent.ACTION_SENDTO).apply {
|
||||
type = Constants.EMAIL_MIME_TYPE
|
||||
putExtra(Intent.EXTRA_EMAIL, arrayOf(application.getString(R.string.my_email)))
|
||||
putExtra(Intent.EXTRA_SUBJECT, application.getString(R.string.email_subject))
|
||||
putExtra(Intent.EXTRA_EMAIL, arrayOf(context.getString(R.string.my_email)))
|
||||
putExtra(Intent.EXTRA_SUBJECT, context.getString(R.string.email_subject))
|
||||
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
}
|
||||
application.startActivity(
|
||||
Intent.createChooser(intent, application.getString(R.string.email_chooser)).apply {
|
||||
context.startActivity(
|
||||
Intent.createChooser(intent, context.getString(R.string.email_chooser)).apply {
|
||||
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
},
|
||||
)
|
||||
} catch (e: ActivityNotFoundException) {
|
||||
Timber.e(e)
|
||||
showSnackbarMessage(application.getString(R.string.no_email_detected))
|
||||
showSnackbarMessage(context.getString(R.string.no_email_detected))
|
||||
}
|
||||
}
|
||||
|
||||
fun showSnackbarMessage(message: String) {
|
||||
_appUiState.value = _appUiState.value.copy(
|
||||
snackbarMessage = message,
|
||||
snackbarMessageConsumed = false,
|
||||
)
|
||||
_appUiState.update {
|
||||
it.copy(
|
||||
snackbarMessage = message,
|
||||
snackbarMessageConsumed = false,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun snackbarMessageConsumed() {
|
||||
_appUiState.value = _appUiState.value.copy(
|
||||
snackbarMessage = "",
|
||||
snackbarMessageConsumed = true,
|
||||
)
|
||||
}
|
||||
|
||||
val logs = mutableStateListOf<LogMessage>()
|
||||
|
||||
fun readLogCatOutput() =
|
||||
viewModelScope.launch(viewModelScope.coroutineContext + Dispatchers.IO) {
|
||||
launch {
|
||||
Logcatter.logs {
|
||||
logs.add(it)
|
||||
if (logs.size > Constants.LOG_BUFFER_SIZE) {
|
||||
logs.removeRange(0, (logs.size - Constants.LOG_BUFFER_SIZE).toInt())
|
||||
}
|
||||
}
|
||||
}
|
||||
_appUiState.update {
|
||||
it.copy(
|
||||
snackbarMessage = "",
|
||||
snackbarMessageConsumed = true,
|
||||
)
|
||||
}
|
||||
|
||||
fun clearLogs() {
|
||||
logs.clear()
|
||||
Logcatter.clear()
|
||||
}
|
||||
|
||||
fun saveLogsToFile() {
|
||||
val fileName = "${Constants.BASE_LOG_FILE_NAME}-${Instant.now().epochSecond}.txt"
|
||||
val content = logs.joinToString(separator = "\n")
|
||||
FileUtils.saveFileToDownloads(application.applicationContext, content, fileName)
|
||||
Toast.makeText(application, application.getString(R.string.logs_saved), Toast.LENGTH_SHORT)
|
||||
.show()
|
||||
}
|
||||
|
||||
fun setNotificationPermissionAccepted(accepted: Boolean) {
|
||||
_appUiState.value = _appUiState.value.copy(
|
||||
notificationPermissionAccepted = accepted,
|
||||
)
|
||||
_appUiState.update {
|
||||
it.copy(
|
||||
notificationPermissionAccepted = accepted,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,10 +32,12 @@ import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.navigation.NavType
|
||||
import androidx.navigation.compose.NavHost
|
||||
import androidx.navigation.compose.composable
|
||||
import androidx.navigation.compose.currentBackStackEntryAsState
|
||||
import androidx.navigation.compose.rememberNavController
|
||||
import androidx.navigation.navArgument
|
||||
import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
||||
import com.google.accompanist.permissions.isGranted
|
||||
import com.google.accompanist.permissions.rememberPermissionState
|
||||
@@ -47,6 +49,7 @@ import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.BottomNavBar
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.prompt.CustomSnackBar
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.config.ConfigScreen
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.main.ConfigType
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.main.MainScreen
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.options.OptionsScreen
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.pinlock.PinLockScreen
|
||||
@@ -84,7 +87,7 @@ class MainActivity : AppCompatActivity() {
|
||||
// load preferences into memory and init data
|
||||
lifecycleScope.launch {
|
||||
dataStoreManager.init()
|
||||
WireGuardAutoTunnel.requestTunnelTileServiceStateUpdate(this@MainActivity)
|
||||
WireGuardAutoTunnel.requestTunnelTileServiceStateUpdate()
|
||||
val settings = settingsRepository.getSettings()
|
||||
if (settings.isAutoTunnelEnabled) {
|
||||
serviceManager.startWatcherService(application.applicationContext)
|
||||
@@ -138,7 +141,11 @@ class MainActivity : AppCompatActivity() {
|
||||
return@LaunchedEffect notificationPermissionState.launchPermissionRequest()
|
||||
}
|
||||
if (!appUiState.vpnPermissionAccepted) {
|
||||
return@LaunchedEffect vpnActivityResultState.launch(appViewModel.vpnIntent)
|
||||
return@LaunchedEffect appViewModel.vpnIntent?.let {
|
||||
vpnActivityResultState.launch(
|
||||
it,
|
||||
)
|
||||
}!!
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -148,7 +155,6 @@ class MainActivity : AppCompatActivity() {
|
||||
appViewModel.setNotificationPermissionAccepted(
|
||||
notificationPermissionState?.status?.isGranted ?: true,
|
||||
)
|
||||
if (!WireGuardAutoTunnel.isRunningOnAndroidTv()) appViewModel.readLogCatOutput()
|
||||
}
|
||||
|
||||
LaunchedEffect(appUiState.snackbarMessageConsumed) {
|
||||
@@ -230,16 +236,33 @@ class MainActivity : AppCompatActivity() {
|
||||
)
|
||||
}
|
||||
composable(Screen.Support.Logs.route) {
|
||||
LogsScreen(appViewModel)
|
||||
LogsScreen()
|
||||
}
|
||||
composable("${Screen.Config.route}/{id}") {
|
||||
composable(
|
||||
"${Screen.Config.route}/{id}?configType={configType}",
|
||||
arguments =
|
||||
listOf(
|
||||
navArgument("id") {
|
||||
type = NavType.StringType
|
||||
defaultValue = "0"
|
||||
},
|
||||
navArgument("configType") {
|
||||
type = NavType.StringType
|
||||
defaultValue = ConfigType.WIREGUARD.name
|
||||
},
|
||||
),
|
||||
) {
|
||||
val id = it.arguments?.getString("id")
|
||||
val configType = ConfigType.valueOf(
|
||||
it.arguments?.getString("configType") ?: ConfigType.WIREGUARD.name,
|
||||
)
|
||||
if (!id.isNullOrBlank()) {
|
||||
ConfigScreen(
|
||||
navController = navController,
|
||||
tunnelId = id,
|
||||
appViewModel = appViewModel,
|
||||
focusRequester = focusRequester,
|
||||
configType = configType,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,13 +4,15 @@ import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.rounded.Home
|
||||
import androidx.compose.material.icons.rounded.QuestionMark
|
||||
import androidx.compose.material.icons.rounded.Settings
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.BottomNavItem
|
||||
|
||||
sealed class Screen(val route: String) {
|
||||
data object Main : Screen("main") {
|
||||
val navItem =
|
||||
BottomNavItem(
|
||||
name = "Tunnels",
|
||||
name = WireGuardAutoTunnel.instance.getString(R.string.tunnels),
|
||||
route = route,
|
||||
icon = Icons.Rounded.Home,
|
||||
)
|
||||
@@ -19,7 +21,7 @@ sealed class Screen(val route: String) {
|
||||
data object Settings : Screen("settings") {
|
||||
val navItem =
|
||||
BottomNavItem(
|
||||
name = "Settings",
|
||||
name = WireGuardAutoTunnel.instance.getString(R.string.settings),
|
||||
route = route,
|
||||
icon = Icons.Rounded.Settings,
|
||||
)
|
||||
@@ -28,7 +30,7 @@ sealed class Screen(val route: String) {
|
||||
data object Support : Screen("support") {
|
||||
val navItem =
|
||||
BottomNavItem(
|
||||
name = "Support",
|
||||
name = WireGuardAutoTunnel.instance.getString(R.string.support),
|
||||
route = route,
|
||||
icon = Icons.Rounded.QuestionMark,
|
||||
)
|
||||
|
||||
@@ -15,9 +15,10 @@ import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.wireguard.android.backend.Statistics
|
||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.statistics.TunnelStatistics
|
||||
import com.zaneschepke.wireguardautotunnel.util.NumberUtils
|
||||
import com.zaneschepke.wireguardautotunnel.util.toThreeDecimalPlaceString
|
||||
|
||||
@@ -30,7 +31,7 @@ fun RowListItem(
|
||||
onClick: () -> Unit,
|
||||
rowButton: @Composable () -> Unit,
|
||||
expanded: Boolean,
|
||||
statistics: Statistics?
|
||||
statistics: TunnelStatistics?
|
||||
) {
|
||||
Box(
|
||||
modifier =
|
||||
@@ -52,14 +53,15 @@ fun RowListItem(
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.fillMaxWidth(13 / 20f),
|
||||
) {
|
||||
icon()
|
||||
Text(text)
|
||||
Text(text, maxLines = 1, overflow = TextOverflow.Ellipsis)
|
||||
}
|
||||
rowButton()
|
||||
}
|
||||
if (expanded) {
|
||||
statistics?.peers()?.forEach {
|
||||
statistics?.getPeers()?.forEach {
|
||||
Row(
|
||||
modifier =
|
||||
Modifier
|
||||
@@ -69,9 +71,9 @@ fun RowListItem(
|
||||
horizontalArrangement = Arrangement.SpaceEvenly,
|
||||
) {
|
||||
//TODO change these to string resources
|
||||
val handshakeEpoch = statistics.peer(it)!!.latestHandshakeEpochMillis
|
||||
val peerTx = statistics.peer(it)!!.txBytes
|
||||
val peerRx = statistics.peer(it)!!.rxBytes
|
||||
val handshakeEpoch = statistics.peerStats(it)!!.latestHandshakeEpochMillis
|
||||
val peerTx = statistics.peerStats(it)!!.txBytes
|
||||
val peerRx = statistics.peerStats(it)!!.rxBytes
|
||||
val peerId = it.toBase64().subSequence(0, 3).toString() + "***"
|
||||
val handshakeSec =
|
||||
NumberUtils.getSecondsBetweenTimestampAndNow(handshakeEpoch)
|
||||
|
||||
@@ -44,19 +44,21 @@ fun SearchBar(onQuery: (queryString: String) -> Unit) {
|
||||
onQuery(onQueryChanged)
|
||||
},
|
||||
leadingIcon = {
|
||||
val icon = Icons.Rounded.Search
|
||||
Icon(
|
||||
imageVector = Icons.Rounded.Search,
|
||||
imageVector = icon,
|
||||
tint = MaterialTheme.colorScheme.onBackground,
|
||||
contentDescription = stringResource(id = R.string.search_icon),
|
||||
contentDescription = icon.name,
|
||||
)
|
||||
},
|
||||
trailingIcon = {
|
||||
if (showClearIcon) {
|
||||
IconButton(onClick = { query = "" }) {
|
||||
val icon = Icons.Rounded.Clear
|
||||
Icon(
|
||||
imageVector = Icons.Rounded.Clear,
|
||||
imageVector = icon,
|
||||
tint = MaterialTheme.colorScheme.onBackground,
|
||||
contentDescription = stringResource(id = R.string.clear_icon),
|
||||
contentDescription = icon.name,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+9
-1
@@ -28,7 +28,15 @@ fun ConfigurationToggle(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
) {
|
||||
Text(label, textAlign = TextAlign.Start)
|
||||
Text(
|
||||
label, textAlign = TextAlign.Start,
|
||||
modifier = Modifier
|
||||
.weight(
|
||||
weight = 1.0f,
|
||||
fill = false,
|
||||
),
|
||||
softWrap = true,
|
||||
)
|
||||
Switch(
|
||||
modifier = modifier,
|
||||
enabled = enabled,
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
package com.zaneschepke.wireguardautotunnel.ui.models
|
||||
|
||||
import com.wireguard.config.Interface
|
||||
|
||||
data class InterfaceProxy(
|
||||
var privateKey: String = "",
|
||||
var publicKey: String = "",
|
||||
var addresses: String = "",
|
||||
var dnsServers: String = "",
|
||||
var listenPort: String = "",
|
||||
var mtu: String = ""
|
||||
) {
|
||||
companion object {
|
||||
fun from(i: Interface): InterfaceProxy {
|
||||
return InterfaceProxy(
|
||||
publicKey = i.keyPair.publicKey.toBase64().trim(),
|
||||
privateKey = i.keyPair.privateKey.toBase64().trim(),
|
||||
addresses = i.addresses.joinToString(", ").trim(),
|
||||
dnsServers = i.dnsServers.joinToString(", ").replace("/", "").trim(),
|
||||
listenPort =
|
||||
if (i.listenPort.isPresent) {
|
||||
i.listenPort.get().toString().trim()
|
||||
} else {
|
||||
""
|
||||
},
|
||||
mtu = if (i.mtu.isPresent) i.mtu.get().toString().trim() else "",
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
+127
-14
@@ -79,9 +79,9 @@ import com.zaneschepke.wireguardautotunnel.ui.common.config.ConfigurationTextBox
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.prompt.AuthorizationPrompt
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.screen.LoadingScreen
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.text.SectionTitle
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.main.ConfigType
|
||||
import com.zaneschepke.wireguardautotunnel.util.Constants
|
||||
import com.zaneschepke.wireguardautotunnel.util.Event
|
||||
import com.zaneschepke.wireguardautotunnel.util.Result
|
||||
import com.zaneschepke.wireguardautotunnel.util.getMessage
|
||||
import kotlinx.coroutines.delay
|
||||
|
||||
@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter")
|
||||
@@ -94,7 +94,8 @@ fun ConfigScreen(
|
||||
focusRequester: FocusRequester,
|
||||
navController: NavController,
|
||||
appViewModel: AppViewModel,
|
||||
tunnelId: String
|
||||
tunnelId: String,
|
||||
configType: ConfigType
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val clipboardManager: ClipboardManager = LocalClipboardManager.current
|
||||
@@ -148,11 +149,11 @@ fun ConfigScreen(
|
||||
},
|
||||
onError = {
|
||||
showAuthPrompt = false
|
||||
appViewModel.showSnackbarMessage(Event.Error.AuthenticationFailed.message)
|
||||
appViewModel.showSnackbarMessage(context.getString(R.string.error_authentication_failed))
|
||||
},
|
||||
onFailure = {
|
||||
showAuthPrompt = false
|
||||
appViewModel.showSnackbarMessage(Event.Error.AuthorizationFailed.message)
|
||||
appViewModel.showSnackbarMessage(context.getString(R.string.error_authorization_failed))
|
||||
},
|
||||
)
|
||||
}
|
||||
@@ -319,15 +320,11 @@ fun ConfigScreen(
|
||||
}
|
||||
},
|
||||
onClick = {
|
||||
viewModel.onSaveAllChanges().let {
|
||||
when (it) {
|
||||
is Result.Success -> {
|
||||
appViewModel.showSnackbarMessage(it.data.message)
|
||||
navController.navigate(Screen.Main.route)
|
||||
}
|
||||
|
||||
is Result.Error -> appViewModel.showSnackbarMessage(it.error.message)
|
||||
}
|
||||
viewModel.onSaveAllChanges(configType).onSuccess {
|
||||
appViewModel.showSnackbarMessage(context.getString(R.string.config_changes_saved))
|
||||
navController.navigate(Screen.Main.route)
|
||||
}.onFailure {
|
||||
appViewModel.showSnackbarMessage(it.getMessage(context))
|
||||
}
|
||||
},
|
||||
containerColor = fobColor,
|
||||
@@ -486,6 +483,122 @@ fun ConfigScreen(
|
||||
modifier = Modifier.width(IntrinsicSize.Min),
|
||||
)
|
||||
}
|
||||
if (configType == ConfigType.AMNEZIA) {
|
||||
ConfigurationTextBox(
|
||||
value = uiState.interfaceProxy.junkPacketCount,
|
||||
onValueChange = { value -> viewModel.onJunkPacketCountChanged(value) },
|
||||
keyboardActions = keyboardActions,
|
||||
label = stringResource(R.string.junk_packet_count),
|
||||
hint = stringResource(R.string.junk_packet_count).lowercase(),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.focusRequester(focusRequester),
|
||||
)
|
||||
ConfigurationTextBox(
|
||||
value = uiState.interfaceProxy.junkPacketMinSize,
|
||||
onValueChange = { value ->
|
||||
viewModel.onJunkPacketMinSizeChanged(
|
||||
value,
|
||||
)
|
||||
},
|
||||
keyboardActions = keyboardActions,
|
||||
label = stringResource(R.string.junk_packet_minimum_size),
|
||||
hint = stringResource(R.string.junk_packet_minimum_size).lowercase(),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.focusRequester(focusRequester),
|
||||
)
|
||||
ConfigurationTextBox(
|
||||
value = uiState.interfaceProxy.junkPacketMaxSize,
|
||||
onValueChange = { value ->
|
||||
viewModel.onJunkPacketMaxSizeChanged(
|
||||
value,
|
||||
)
|
||||
},
|
||||
keyboardActions = keyboardActions,
|
||||
label = stringResource(R.string.junk_packet_maximum_size),
|
||||
hint = stringResource(R.string.junk_packet_maximum_size).lowercase(),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.focusRequester(focusRequester),
|
||||
)
|
||||
ConfigurationTextBox(
|
||||
value = uiState.interfaceProxy.initPacketJunkSize,
|
||||
onValueChange = { value ->
|
||||
viewModel.onInitPacketJunkSizeChanged(
|
||||
value,
|
||||
)
|
||||
},
|
||||
keyboardActions = keyboardActions,
|
||||
label = stringResource(R.string.init_packet_junk_size),
|
||||
hint = stringResource(R.string.init_packet_junk_size).lowercase(),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.focusRequester(focusRequester),
|
||||
)
|
||||
ConfigurationTextBox(
|
||||
value = uiState.interfaceProxy.responsePacketJunkSize,
|
||||
onValueChange = { value -> viewModel.onResponsePacketJunkSize(value) },
|
||||
keyboardActions = keyboardActions,
|
||||
label = stringResource(R.string.response_packet_junk_size),
|
||||
hint = stringResource(R.string.response_packet_junk_size).lowercase(),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.focusRequester(focusRequester),
|
||||
)
|
||||
ConfigurationTextBox(
|
||||
value = uiState.interfaceProxy.initPacketMagicHeader,
|
||||
onValueChange = { value -> viewModel.onInitPacketMagicHeader(value) },
|
||||
keyboardActions = keyboardActions,
|
||||
label = stringResource(R.string.init_packet_magic_header),
|
||||
hint = stringResource(R.string.init_packet_magic_header).lowercase(),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.focusRequester(focusRequester),
|
||||
)
|
||||
ConfigurationTextBox(
|
||||
value = uiState.interfaceProxy.responsePacketMagicHeader,
|
||||
onValueChange = { value ->
|
||||
viewModel.onResponsePacketMagicHeader(
|
||||
value,
|
||||
)
|
||||
},
|
||||
keyboardActions = keyboardActions,
|
||||
label = stringResource(R.string.response_packet_magic_header),
|
||||
hint = stringResource(R.string.response_packet_magic_header).lowercase(),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.focusRequester(focusRequester),
|
||||
)
|
||||
ConfigurationTextBox(
|
||||
value = uiState.interfaceProxy.underloadPacketMagicHeader,
|
||||
onValueChange = { value ->
|
||||
viewModel.onUnderloadPacketMagicHeader(
|
||||
value,
|
||||
)
|
||||
},
|
||||
keyboardActions = keyboardActions,
|
||||
label = stringResource(R.string.underload_packet_magic_header),
|
||||
hint = stringResource(R.string.underload_packet_magic_header).lowercase(),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.focusRequester(focusRequester),
|
||||
)
|
||||
ConfigurationTextBox(
|
||||
value = uiState.interfaceProxy.transportPacketMagicHeader,
|
||||
onValueChange = { value ->
|
||||
viewModel.onTransportPacketMagicHeader(
|
||||
value,
|
||||
)
|
||||
},
|
||||
keyboardActions = keyboardActions,
|
||||
label = stringResource(R.string.transport_packet_magic_header),
|
||||
hint = stringResource(R.string.transport_packet_magic_header).lowercase(),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.focusRequester(focusRequester),
|
||||
)
|
||||
}
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier
|
||||
|
||||
+60
-5
@@ -1,8 +1,9 @@
|
||||
package com.zaneschepke.wireguardautotunnel.ui.screens.config
|
||||
|
||||
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
|
||||
import com.zaneschepke.wireguardautotunnel.ui.models.InterfaceProxy
|
||||
import com.zaneschepke.wireguardautotunnel.ui.models.PeerProxy
|
||||
import com.wireguard.config.Config
|
||||
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.config.model.InterfaceProxy
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.config.model.PeerProxy
|
||||
import com.zaneschepke.wireguardautotunnel.util.Packages
|
||||
|
||||
data class ConfigUiState(
|
||||
@@ -14,5 +15,59 @@ data class ConfigUiState(
|
||||
val isAllApplicationsEnabled: Boolean = false,
|
||||
val loading: Boolean = true,
|
||||
val tunnel: TunnelConfig? = null,
|
||||
val tunnelName: String = ""
|
||||
)
|
||||
val tunnelName: String = "",
|
||||
val isAmneziaEnabled: Boolean = false
|
||||
) {
|
||||
companion object {
|
||||
fun from(config: Config): ConfigUiState {
|
||||
val proxyPeers = config.peers.map { PeerProxy.from(it) }
|
||||
val proxyInterface = InterfaceProxy.from(config.`interface`)
|
||||
var include = true
|
||||
var isAllApplicationsEnabled = false
|
||||
val checkedPackages =
|
||||
if (config.`interface`.includedApplications.isNotEmpty()) {
|
||||
config.`interface`.includedApplications
|
||||
} else if (config.`interface`.excludedApplications.isNotEmpty()) {
|
||||
include = false
|
||||
config.`interface`.excludedApplications
|
||||
} else {
|
||||
isAllApplicationsEnabled = true
|
||||
emptySet()
|
||||
}
|
||||
return ConfigUiState(
|
||||
proxyPeers,
|
||||
proxyInterface,
|
||||
emptyList(),
|
||||
checkedPackages.toList(),
|
||||
include,
|
||||
isAllApplicationsEnabled,
|
||||
)
|
||||
}
|
||||
|
||||
fun from(config: org.amnezia.awg.config.Config): ConfigUiState {
|
||||
//TODO update with new values
|
||||
val proxyPeers = config.peers.map { PeerProxy.from(it) }
|
||||
val proxyInterface = InterfaceProxy.from(config.`interface`)
|
||||
var include = true
|
||||
var isAllApplicationsEnabled = false
|
||||
val checkedPackages =
|
||||
if (config.`interface`.includedApplications.isNotEmpty()) {
|
||||
config.`interface`.includedApplications
|
||||
} else if (config.`interface`.excludedApplications.isNotEmpty()) {
|
||||
include = false
|
||||
config.`interface`.excludedApplications
|
||||
} else {
|
||||
isAllApplicationsEnabled = true
|
||||
emptySet()
|
||||
}
|
||||
return ConfigUiState(
|
||||
proxyPeers,
|
||||
proxyInterface,
|
||||
emptyList(),
|
||||
checkedPackages.toList(),
|
||||
include,
|
||||
isAllApplicationsEnabled,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+247
-74
@@ -1,7 +1,6 @@
|
||||
package com.zaneschepke.wireguardautotunnel.ui.screens.config
|
||||
|
||||
import android.Manifest
|
||||
import android.app.Application
|
||||
import android.content.pm.PackageInfo
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Build
|
||||
@@ -12,21 +11,25 @@ import com.wireguard.config.Interface
|
||||
import com.wireguard.config.Peer
|
||||
import com.wireguard.crypto.Key
|
||||
import com.wireguard.crypto.KeyPair
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
|
||||
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
|
||||
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
|
||||
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
|
||||
import com.zaneschepke.wireguardautotunnel.ui.models.InterfaceProxy
|
||||
import com.zaneschepke.wireguardautotunnel.ui.models.PeerProxy
|
||||
import com.zaneschepke.wireguardautotunnel.data.repository.SettingsRepository
|
||||
import com.zaneschepke.wireguardautotunnel.module.IoDispatcher
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.config.model.PeerProxy
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.main.ConfigType
|
||||
import com.zaneschepke.wireguardautotunnel.util.Constants
|
||||
import com.zaneschepke.wireguardautotunnel.util.Event
|
||||
import com.zaneschepke.wireguardautotunnel.util.NumberUtils
|
||||
import com.zaneschepke.wireguardautotunnel.util.Result
|
||||
import com.zaneschepke.wireguardautotunnel.util.StringValue
|
||||
import com.zaneschepke.wireguardautotunnel.util.WgTunnelExceptions
|
||||
import com.zaneschepke.wireguardautotunnel.util.removeAt
|
||||
import com.zaneschepke.wireguardautotunnel.util.update
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
@@ -35,49 +38,36 @@ import javax.inject.Inject
|
||||
class ConfigViewModel
|
||||
@Inject
|
||||
constructor(
|
||||
private val application: Application,
|
||||
private val appDataRepository: AppDataRepository
|
||||
private val settingsRepository: SettingsRepository,
|
||||
private val appDataRepository: AppDataRepository,
|
||||
@IoDispatcher private val ioDispatcher: CoroutineDispatcher
|
||||
) : ViewModel() {
|
||||
|
||||
private val packageManager = application.packageManager
|
||||
private val packageManager = WireGuardAutoTunnel.instance.packageManager
|
||||
|
||||
private val _uiState = MutableStateFlow(ConfigUiState())
|
||||
val uiState = _uiState.asStateFlow()
|
||||
|
||||
fun init(tunnelId: String) =
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
viewModelScope.launch(ioDispatcher) {
|
||||
val packages = getQueriedPackages("")
|
||||
val state =
|
||||
if (tunnelId != Constants.MANUAL_TUNNEL_CONFIG_ID) {
|
||||
val tunnelConfig =
|
||||
appDataRepository.tunnels.getAll()
|
||||
.firstOrNull { it.id.toString() == tunnelId }
|
||||
val isAmneziaEnabled = settingsRepository.getSettings().isAmneziaEnabled
|
||||
if (tunnelConfig != null) {
|
||||
val config = TunnelConfig.configFromQuick(tunnelConfig.wgQuick)
|
||||
val proxyPeers = config.peers.map { PeerProxy.from(it) }
|
||||
val proxyInterface = InterfaceProxy.from(config.`interface`)
|
||||
var include = true
|
||||
var isAllApplicationsEnabled = false
|
||||
val checkedPackages =
|
||||
if (config.`interface`.includedApplications.isNotEmpty()) {
|
||||
config.`interface`.includedApplications
|
||||
} else if (config.`interface`.excludedApplications.isNotEmpty()) {
|
||||
include = false
|
||||
config.`interface`.excludedApplications
|
||||
} else {
|
||||
isAllApplicationsEnabled = true
|
||||
emptySet()
|
||||
}
|
||||
ConfigUiState(
|
||||
proxyPeers,
|
||||
proxyInterface,
|
||||
packages,
|
||||
checkedPackages.toList(),
|
||||
include,
|
||||
isAllApplicationsEnabled,
|
||||
false,
|
||||
tunnelConfig,
|
||||
tunnelConfig.name,
|
||||
(if (isAmneziaEnabled) {
|
||||
val amConfig =
|
||||
if (tunnelConfig.amQuick == "") tunnelConfig.wgQuick else tunnelConfig.amQuick
|
||||
ConfigUiState.from(TunnelConfig.configFromAmQuick(amConfig))
|
||||
} else ConfigUiState.from(TunnelConfig.configFromWgQuick(tunnelConfig.wgQuick))).copy(
|
||||
packages = packages,
|
||||
loading = false,
|
||||
tunnel = tunnelConfig,
|
||||
tunnelName = tunnelConfig.name,
|
||||
isAmneziaEnabled = isAmneziaEnabled,
|
||||
)
|
||||
} else {
|
||||
ConfigUiState(loading = false, packages = packages)
|
||||
@@ -121,7 +111,7 @@ constructor(
|
||||
}
|
||||
|
||||
fun getPackageLabel(packageInfo: PackageInfo): String {
|
||||
return packageInfo.applicationInfo.loadLabel(application.packageManager).toString()
|
||||
return packageInfo.applicationInfo.loadLabel(packageManager).toString()
|
||||
}
|
||||
|
||||
private fun getAllInternetCapablePackages(): List<PackageInfo> {
|
||||
@@ -150,7 +140,7 @@ constructor(
|
||||
viewModelScope.launch {
|
||||
if (tunnelConfig != null) {
|
||||
saveConfig(tunnelConfig).join()
|
||||
WireGuardAutoTunnel.requestTunnelTileServiceStateUpdate(application)
|
||||
WireGuardAutoTunnel.requestTunnelTileServiceStateUpdate()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -168,6 +158,20 @@ constructor(
|
||||
}
|
||||
}
|
||||
|
||||
private fun buildAmPeerListFromProxyPeers(): List<org.amnezia.awg.config.Peer> {
|
||||
return _uiState.value.proxyPeers.map {
|
||||
val builder = org.amnezia.awg.config.Peer.Builder()
|
||||
if (it.allowedIps.isNotEmpty()) builder.parseAllowedIPs(it.allowedIps.trim())
|
||||
if (it.publicKey.isNotEmpty()) builder.parsePublicKey(it.publicKey.trim())
|
||||
if (it.preSharedKey.isNotEmpty()) builder.parsePreSharedKey(it.preSharedKey.trim())
|
||||
if (it.endpoint.isNotEmpty()) builder.parseEndpoint(it.endpoint.trim())
|
||||
if (it.persistentKeepalive.isNotEmpty()) {
|
||||
builder.parsePersistentKeepalive(it.persistentKeepalive.trim())
|
||||
}
|
||||
builder.build()
|
||||
}
|
||||
}
|
||||
|
||||
private fun emptyCheckedPackagesList() {
|
||||
_uiState.value = _uiState.value.copy(checkedPackageNames = emptyList())
|
||||
}
|
||||
@@ -190,147 +194,244 @@ constructor(
|
||||
return builder.build()
|
||||
}
|
||||
|
||||
fun onSaveAllChanges(): Result<Event> {
|
||||
private fun buildAmInterfaceListFromProxyInterface(): org.amnezia.awg.config.Interface {
|
||||
val builder = org.amnezia.awg.config.Interface.Builder()
|
||||
builder.parsePrivateKey(_uiState.value.interfaceProxy.privateKey.trim())
|
||||
builder.parseAddresses(_uiState.value.interfaceProxy.addresses.trim())
|
||||
if (_uiState.value.interfaceProxy.dnsServers.isNotEmpty()) {
|
||||
builder.parseDnsServers(_uiState.value.interfaceProxy.dnsServers.trim())
|
||||
}
|
||||
if (_uiState.value.interfaceProxy.mtu.isNotEmpty())
|
||||
builder.parseMtu(_uiState.value.interfaceProxy.mtu.trim())
|
||||
if (_uiState.value.interfaceProxy.listenPort.isNotEmpty()) {
|
||||
builder.parseListenPort(_uiState.value.interfaceProxy.listenPort.trim())
|
||||
}
|
||||
if (isAllApplicationsEnabled()) emptyCheckedPackagesList()
|
||||
if (_uiState.value.include) builder.includeApplications(_uiState.value.checkedPackageNames)
|
||||
if (!_uiState.value.include) builder.excludeApplications(_uiState.value.checkedPackageNames)
|
||||
if (_uiState.value.interfaceProxy.junkPacketCount.isNotEmpty()) {
|
||||
builder.setJunkPacketCount(_uiState.value.interfaceProxy.junkPacketCount.trim().toInt())
|
||||
}
|
||||
if (_uiState.value.interfaceProxy.junkPacketMinSize.isNotEmpty()) {
|
||||
builder.setJunkPacketMinSize(
|
||||
_uiState.value.interfaceProxy.junkPacketMinSize.trim().toInt(),
|
||||
)
|
||||
}
|
||||
if (_uiState.value.interfaceProxy.junkPacketMaxSize.isNotEmpty()) {
|
||||
builder.setJunkPacketMaxSize(
|
||||
_uiState.value.interfaceProxy.junkPacketMaxSize.trim().toInt(),
|
||||
)
|
||||
}
|
||||
if (_uiState.value.interfaceProxy.initPacketJunkSize.isNotEmpty()) {
|
||||
builder.setInitPacketJunkSize(
|
||||
_uiState.value.interfaceProxy.initPacketJunkSize.trim().toInt(),
|
||||
)
|
||||
}
|
||||
if (_uiState.value.interfaceProxy.responsePacketJunkSize.isNotEmpty()) {
|
||||
builder.setResponsePacketJunkSize(
|
||||
_uiState.value.interfaceProxy.responsePacketJunkSize.trim().toInt(),
|
||||
)
|
||||
}
|
||||
if (_uiState.value.interfaceProxy.initPacketMagicHeader.isNotEmpty()) {
|
||||
builder.setInitPacketMagicHeader(
|
||||
_uiState.value.interfaceProxy.initPacketMagicHeader.trim().toLong(),
|
||||
)
|
||||
}
|
||||
if (_uiState.value.interfaceProxy.responsePacketMagicHeader.isNotEmpty()) {
|
||||
builder.setResponsePacketMagicHeader(
|
||||
_uiState.value.interfaceProxy.responsePacketMagicHeader.trim().toLong(),
|
||||
)
|
||||
}
|
||||
if (_uiState.value.interfaceProxy.transportPacketMagicHeader.isNotEmpty()) {
|
||||
builder.setTransportPacketMagicHeader(
|
||||
_uiState.value.interfaceProxy.transportPacketMagicHeader.trim().toLong(),
|
||||
)
|
||||
}
|
||||
if (_uiState.value.interfaceProxy.underloadPacketMagicHeader.isNotEmpty()) {
|
||||
builder.setUnderloadPacketMagicHeader(
|
||||
_uiState.value.interfaceProxy.underloadPacketMagicHeader.trim().toLong(),
|
||||
)
|
||||
}
|
||||
return builder.build()
|
||||
}
|
||||
|
||||
private fun buildConfig(): Config {
|
||||
val peerList = buildPeerListFromProxyPeers()
|
||||
val wgInterface = buildInterfaceListFromProxyInterface()
|
||||
return Config.Builder().addPeers(peerList).setInterface(wgInterface).build()
|
||||
}
|
||||
|
||||
private fun buildAmConfig(): org.amnezia.awg.config.Config {
|
||||
val peerList = buildAmPeerListFromProxyPeers()
|
||||
val amInterface = buildAmInterfaceListFromProxyInterface()
|
||||
return org.amnezia.awg.config.Config.Builder().addPeers(peerList).setInterface(amInterface)
|
||||
.build()
|
||||
}
|
||||
|
||||
fun onSaveAllChanges(configType: ConfigType): Result<Unit> {
|
||||
return try {
|
||||
val peerList = buildPeerListFromProxyPeers()
|
||||
val wgInterface = buildInterfaceListFromProxyInterface()
|
||||
val config = Config.Builder().addPeers(peerList).setInterface(wgInterface).build()
|
||||
val wgQuick = buildConfig().toWgQuickString()
|
||||
val amQuick = if (configType == ConfigType.AMNEZIA) {
|
||||
buildAmConfig().toAwgQuickString()
|
||||
} else TunnelConfig.AM_QUICK_DEFAULT
|
||||
val tunnelConfig = when (uiState.value.tunnel) {
|
||||
null -> TunnelConfig(
|
||||
name = _uiState.value.tunnelName,
|
||||
wgQuick = config.toWgQuickString(),
|
||||
wgQuick = wgQuick,
|
||||
amQuick = amQuick,
|
||||
)
|
||||
|
||||
else -> uiState.value.tunnel!!.copy(
|
||||
name = _uiState.value.tunnelName,
|
||||
wgQuick = config.toWgQuickString(),
|
||||
wgQuick = wgQuick,
|
||||
amQuick = amQuick,
|
||||
)
|
||||
}
|
||||
updateTunnelConfig(tunnelConfig)
|
||||
Result.Success(Event.Message.ConfigSaved)
|
||||
Result.success(Unit)
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e)
|
||||
val message = e.message?.substringAfter(":", missingDelimiterValue = "")
|
||||
Result.Error(Event.Error.ConfigParseError(message ?: ""))
|
||||
val stringValue = message?.let {
|
||||
StringValue.DynamicString(message)
|
||||
} ?: StringValue.StringResource(R.string.unknown_error)
|
||||
Result.failure(WgTunnelExceptions.ConfigParseError(stringValue))
|
||||
}
|
||||
}
|
||||
|
||||
fun onPeerPublicKeyChange(index: Int, value: String) {
|
||||
_uiState.value =
|
||||
_uiState.value.copy(
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
proxyPeers =
|
||||
_uiState.value.proxyPeers.update(
|
||||
index,
|
||||
_uiState.value.proxyPeers[index].copy(publicKey = value),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun onPreSharedKeyChange(index: Int, value: String) {
|
||||
_uiState.value =
|
||||
_uiState.value.copy(
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
proxyPeers =
|
||||
_uiState.value.proxyPeers.update(
|
||||
index,
|
||||
_uiState.value.proxyPeers[index].copy(preSharedKey = value),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun onEndpointChange(index: Int, value: String) {
|
||||
_uiState.value =
|
||||
_uiState.value.copy(
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
proxyPeers =
|
||||
_uiState.value.proxyPeers.update(
|
||||
index,
|
||||
_uiState.value.proxyPeers[index].copy(endpoint = value),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun onAllowedIpsChange(index: Int, value: String) {
|
||||
_uiState.value =
|
||||
_uiState.value.copy(
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
proxyPeers =
|
||||
_uiState.value.proxyPeers.update(
|
||||
index,
|
||||
_uiState.value.proxyPeers[index].copy(allowedIps = value),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun onPersistentKeepaliveChanged(index: Int, value: String) {
|
||||
_uiState.value =
|
||||
_uiState.value.copy(
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
proxyPeers =
|
||||
_uiState.value.proxyPeers.update(
|
||||
index,
|
||||
_uiState.value.proxyPeers[index].copy(persistentKeepalive = value),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun onDeletePeer(index: Int) {
|
||||
_uiState.value =
|
||||
_uiState.value.copy(
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
proxyPeers = _uiState.value.proxyPeers.removeAt(index),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun addEmptyPeer() {
|
||||
_uiState.value = _uiState.value.copy(proxyPeers = _uiState.value.proxyPeers + PeerProxy())
|
||||
_uiState.update {
|
||||
it.copy(proxyPeers = _uiState.value.proxyPeers + PeerProxy())
|
||||
}
|
||||
}
|
||||
|
||||
fun generateKeyPair() {
|
||||
val keyPair = KeyPair()
|
||||
_uiState.value =
|
||||
_uiState.value.copy(
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
interfaceProxy =
|
||||
_uiState.value.interfaceProxy.copy(
|
||||
privateKey = keyPair.privateKey.toBase64(),
|
||||
publicKey = keyPair.publicKey.toBase64(),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun onAddressesChanged(value: String) {
|
||||
_uiState.value =
|
||||
_uiState.value.copy(
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
interfaceProxy = _uiState.value.interfaceProxy.copy(addresses = value),
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
fun onListenPortChanged(value: String) {
|
||||
_uiState.value =
|
||||
_uiState.value.copy(
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
interfaceProxy = _uiState.value.interfaceProxy.copy(listenPort = value),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun onDnsServersChanged(value: String) {
|
||||
_uiState.value =
|
||||
_uiState.value.copy(
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
interfaceProxy = _uiState.value.interfaceProxy.copy(dnsServers = value),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun onMtuChanged(value: String) {
|
||||
_uiState.value =
|
||||
_uiState.value.copy(interfaceProxy = _uiState.value.interfaceProxy.copy(mtu = value))
|
||||
_uiState.update {
|
||||
it.copy(interfaceProxy = _uiState.value.interfaceProxy.copy(mtu = value))
|
||||
}
|
||||
}
|
||||
|
||||
private fun onInterfacePublicKeyChange(value: String) {
|
||||
_uiState.value =
|
||||
_uiState.value.copy(
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
interfaceProxy = _uiState.value.interfaceProxy.copy(publicKey = value),
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
fun onPrivateKeyChange(value: String) {
|
||||
_uiState.value =
|
||||
_uiState.value.copy(
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
interfaceProxy = _uiState.value.interfaceProxy.copy(privateKey = value),
|
||||
)
|
||||
}
|
||||
if (NumberUtils.isValidKey(value)) {
|
||||
val pair = KeyPair(Key.fromBase64(value))
|
||||
onInterfacePublicKeyChange(pair.publicKey.toBase64())
|
||||
@@ -344,6 +445,78 @@ constructor(
|
||||
getAllInternetCapablePackages().filter {
|
||||
getPackageLabel(it).lowercase().contains(query.lowercase())
|
||||
}
|
||||
_uiState.value = _uiState.value.copy(packages = packages)
|
||||
_uiState.update { it.copy(packages = packages) }
|
||||
}
|
||||
|
||||
fun onJunkPacketCountChanged(value: String) {
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
interfaceProxy = _uiState.value.interfaceProxy.copy(junkPacketCount = value),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun onJunkPacketMinSizeChanged(value: String) {
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
interfaceProxy = _uiState.value.interfaceProxy.copy(junkPacketMinSize = value),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun onJunkPacketMaxSizeChanged(value: String) {
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
interfaceProxy = _uiState.value.interfaceProxy.copy(junkPacketMaxSize = value),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun onInitPacketJunkSizeChanged(value: String) {
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
interfaceProxy = _uiState.value.interfaceProxy.copy(initPacketJunkSize = value),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun onResponsePacketJunkSize(value: String) {
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
interfaceProxy = _uiState.value.interfaceProxy.copy(responsePacketJunkSize = value),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun onInitPacketMagicHeader(value: String) {
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
interfaceProxy = _uiState.value.interfaceProxy.copy(initPacketMagicHeader = value),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun onResponsePacketMagicHeader(value: String) {
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
interfaceProxy = _uiState.value.interfaceProxy.copy(responsePacketMagicHeader = value),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun onTransportPacketMagicHeader(value: String) {
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
interfaceProxy = _uiState.value.interfaceProxy.copy(transportPacketMagicHeader = value),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun onUnderloadPacketMagicHeader(value: String) {
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
interfaceProxy = _uiState.value.interfaceProxy.copy(underloadPacketMagicHeader = value),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+73
@@ -0,0 +1,73 @@
|
||||
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 "",
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
+31
-6
@@ -1,13 +1,13 @@
|
||||
package com.zaneschepke.wireguardautotunnel.ui.models
|
||||
package com.zaneschepke.wireguardautotunnel.ui.screens.config.model
|
||||
|
||||
import com.wireguard.config.Peer
|
||||
|
||||
data class PeerProxy(
|
||||
var publicKey: String = "",
|
||||
var preSharedKey: String = "",
|
||||
var persistentKeepalive: String = "",
|
||||
var endpoint: String = "",
|
||||
var allowedIps: String = IPV4_WILDCARD.joinToString(", ").trim()
|
||||
val publicKey: String = "",
|
||||
val preSharedKey: String = "",
|
||||
val persistentKeepalive: String = "",
|
||||
val endpoint: String = "",
|
||||
val allowedIps: String = IPV4_WILDCARD.joinToString(", ").trim()
|
||||
) {
|
||||
companion object {
|
||||
fun from(peer: Peer): PeerProxy {
|
||||
@@ -35,6 +35,31 @@ data class PeerProxy(
|
||||
)
|
||||
}
|
||||
|
||||
fun from(peer: org.amnezia.awg.config.Peer): PeerProxy {
|
||||
return PeerProxy(
|
||||
publicKey = peer.publicKey.toBase64(),
|
||||
preSharedKey =
|
||||
if (peer.preSharedKey.isPresent) {
|
||||
peer.preSharedKey.get().toBase64().trim()
|
||||
} else {
|
||||
""
|
||||
},
|
||||
persistentKeepalive =
|
||||
if (peer.persistentKeepalive.isPresent) {
|
||||
peer.persistentKeepalive.get().toString().trim()
|
||||
} else {
|
||||
""
|
||||
},
|
||||
endpoint =
|
||||
if (peer.endpoint.isPresent) {
|
||||
peer.endpoint.get().toString().trim()
|
||||
} else {
|
||||
""
|
||||
},
|
||||
allowedIps = peer.allowedIps.joinToString(", ").trim(),
|
||||
)
|
||||
}
|
||||
|
||||
val IPV4_PUBLIC_NETWORKS =
|
||||
setOf(
|
||||
"0.0.0.0/5",
|
||||
@@ -0,0 +1,6 @@
|
||||
package com.zaneschepke.wireguardautotunnel.ui.screens.main
|
||||
|
||||
enum class ConfigType {
|
||||
AMNEZIA,
|
||||
WIREGUARD
|
||||
}
|
||||
+194
-88
@@ -14,6 +14,7 @@ import androidx.compose.animation.slideInVertically
|
||||
import androidx.compose.animation.slideOutVertically
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.focusGroup
|
||||
import androidx.compose.foundation.focusable
|
||||
import androidx.compose.foundation.gestures.ScrollableDefaults
|
||||
import androidx.compose.foundation.gestures.detectTapGestures
|
||||
@@ -29,11 +30,11 @@ import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.foundation.overscroll
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.text.ClickableText
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Create
|
||||
import androidx.compose.material.icons.filled.FileOpen
|
||||
import androidx.compose.material.icons.filled.QrCode
|
||||
import androidx.compose.material.icons.rounded.Add
|
||||
import androidx.compose.material.icons.rounded.Bolt
|
||||
import androidx.compose.material.icons.rounded.Circle
|
||||
import androidx.compose.material.icons.rounded.CopyAll
|
||||
@@ -45,7 +46,6 @@ import androidx.compose.material.icons.rounded.Star
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.FabPosition
|
||||
import androidx.compose.material3.FloatingActionButton
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
@@ -70,26 +70,36 @@ import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.focus.onFocusChanged
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
|
||||
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
|
||||
import androidx.compose.ui.input.nestedscroll.NestedScrollSource
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.input.pointer.pointerInput
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalHapticFeedback
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.SpanStyle
|
||||
import androidx.compose.ui.text.buildAnnotatedString
|
||||
import androidx.compose.ui.text.font.FontStyle
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.withStyle
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.navigation.NavController
|
||||
import com.iamageo.multifablibrary.FabIcon
|
||||
import com.iamageo.multifablibrary.FabOption
|
||||
import com.iamageo.multifablibrary.MultiFabItem
|
||||
import com.iamageo.multifablibrary.MultiFloatingActionButton
|
||||
import com.journeyapps.barcodescanner.ScanContract
|
||||
import com.journeyapps.barcodescanner.ScanOptions
|
||||
import com.wireguard.android.backend.Tunnel
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
|
||||
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
|
||||
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
|
||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.HandshakeStatus
|
||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelState
|
||||
import com.zaneschepke.wireguardautotunnel.ui.AppViewModel
|
||||
import com.zaneschepke.wireguardautotunnel.ui.CaptureActivityPortrait
|
||||
import com.zaneschepke.wireguardautotunnel.ui.Screen
|
||||
@@ -98,12 +108,9 @@ import com.zaneschepke.wireguardautotunnel.ui.common.screen.LoadingScreen
|
||||
import com.zaneschepke.wireguardautotunnel.ui.theme.corn
|
||||
import com.zaneschepke.wireguardautotunnel.ui.theme.mint
|
||||
import com.zaneschepke.wireguardautotunnel.util.Constants
|
||||
import com.zaneschepke.wireguardautotunnel.util.Event
|
||||
import com.zaneschepke.wireguardautotunnel.util.Result
|
||||
import com.zaneschepke.wireguardautotunnel.util.getMessage
|
||||
import com.zaneschepke.wireguardautotunnel.util.handshakeStatus
|
||||
import com.zaneschepke.wireguardautotunnel.util.mapPeerStats
|
||||
import com.zaneschepke.wireguardautotunnel.util.truncateWithEllipsis
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@@ -119,10 +126,30 @@ fun MainScreen(
|
||||
val haptic = LocalHapticFeedback.current
|
||||
val context = LocalContext.current
|
||||
val isVisible = rememberSaveable { mutableStateOf(true) }
|
||||
val scope = rememberCoroutineScope { Dispatchers.IO }
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
val sheetState = rememberModalBottomSheetState()
|
||||
var showBottomSheet by remember { mutableStateOf(false) }
|
||||
var configType by remember { mutableStateOf(ConfigType.WIREGUARD) }
|
||||
|
||||
// Nested scroll for control FAB
|
||||
val nestedScrollConnection = remember {
|
||||
object : NestedScrollConnection {
|
||||
override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
|
||||
// Hide FAB
|
||||
if (available.y < -1) {
|
||||
isVisible.value = false
|
||||
}
|
||||
// Show FAB
|
||||
if (available.y > 1) {
|
||||
isVisible.value = true
|
||||
}
|
||||
|
||||
return Offset.Zero
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
var showDeleteTunnelAlertDialog by remember { mutableStateOf(false) }
|
||||
var selectedTunnel by remember { mutableStateOf<TunnelConfig?>(null) }
|
||||
@@ -164,7 +191,7 @@ fun MainScreen(
|
||||
name.startsWith(Constants.ANDROID_TV_EXPLORER_STUB)
|
||||
}
|
||||
) {
|
||||
appViewModel.showSnackbarMessage(Event.Error.FileExplorerRequired.message)
|
||||
appViewModel.showSnackbarMessage(context.getString(R.string.error_no_file_explorer))
|
||||
}
|
||||
return intent
|
||||
}
|
||||
@@ -172,11 +199,8 @@ fun MainScreen(
|
||||
) { data ->
|
||||
if (data == null) return@rememberLauncherForActivityResult
|
||||
scope.launch {
|
||||
viewModel.onTunnelFileSelected(data).let {
|
||||
when (it) {
|
||||
is Result.Error -> appViewModel.showSnackbarMessage(it.error.message)
|
||||
is Result.Success -> {}
|
||||
}
|
||||
viewModel.onTunnelFileSelected(data, configType, context).onFailure {
|
||||
appViewModel.showSnackbarMessage(it.getMessage(context))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -186,11 +210,8 @@ fun MainScreen(
|
||||
onResult = {
|
||||
if (it.contents != null) {
|
||||
scope.launch {
|
||||
viewModel.onTunnelQrResult(it.contents).let { result ->
|
||||
when (result) {
|
||||
is Result.Success -> {}
|
||||
is Result.Error -> appViewModel.showSnackbarMessage(result.error.message)
|
||||
}
|
||||
viewModel.onTunnelQrResult(it.contents, configType).onFailure { error ->
|
||||
appViewModel.showSnackbarMessage(error.getMessage(context))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -203,7 +224,7 @@ fun MainScreen(
|
||||
confirmButton = {
|
||||
TextButton(
|
||||
onClick = {
|
||||
selectedTunnel?.let { viewModel.onDelete(it) }
|
||||
selectedTunnel?.let { viewModel.onDelete(it, context) }
|
||||
showDeleteTunnelAlertDialog = false
|
||||
selectedTunnel = null
|
||||
},
|
||||
@@ -223,7 +244,9 @@ fun MainScreen(
|
||||
|
||||
fun onTunnelToggle(checked: Boolean, tunnel: TunnelConfig) {
|
||||
if (appViewModel.isRequiredPermissionGranted()) {
|
||||
if (checked) viewModel.onTunnelStart(tunnel) else viewModel.onTunnelStop()
|
||||
if (checked) viewModel.onTunnelStart(tunnel, context) else viewModel.onTunnelStop(
|
||||
context,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -231,14 +254,30 @@ fun MainScreen(
|
||||
return LoadingScreen()
|
||||
}
|
||||
|
||||
fun launchQrScanner() {
|
||||
val scanOptions = ScanOptions()
|
||||
scanOptions.setDesiredBarcodeFormats(ScanOptions.QR_CODE)
|
||||
scanOptions.setOrientationLocked(true)
|
||||
scanOptions.setPrompt(
|
||||
context.getString(R.string.scanning_qr),
|
||||
)
|
||||
scanOptions.setBeepEnabled(false)
|
||||
scanOptions.captureActivity =
|
||||
CaptureActivityPortrait::class.java
|
||||
scanLauncher.launch(scanOptions)
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
modifier =
|
||||
Modifier.pointerInput(Unit) {
|
||||
detectTapGestures(
|
||||
onTap = {
|
||||
selectedTunnel = null
|
||||
},
|
||||
)
|
||||
if (uiState.tunnels.isNotEmpty()) {
|
||||
detectTapGestures(
|
||||
onTap = {
|
||||
selectedTunnel = null
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
},
|
||||
floatingActionButtonPosition = FabPosition.End,
|
||||
floatingActionButton = {
|
||||
@@ -246,49 +285,77 @@ fun MainScreen(
|
||||
visible = isVisible.value,
|
||||
enter = slideInVertically(initialOffsetY = { it * 2 }),
|
||||
exit = slideOutVertically(targetOffsetY = { it * 2 }),
|
||||
modifier = Modifier
|
||||
.focusRequester(focusRequester)
|
||||
.focusGroup(),
|
||||
) {
|
||||
val secondaryColor = MaterialTheme.colorScheme.secondary
|
||||
val hoverColor = MaterialTheme.colorScheme.surfaceColorAtElevation(2.dp)
|
||||
var fobColor by remember { mutableStateOf(secondaryColor) }
|
||||
FloatingActionButton(
|
||||
modifier =
|
||||
(if (
|
||||
WireGuardAutoTunnel.isRunningOnAndroidTv() &&
|
||||
uiState.tunnels.isEmpty()
|
||||
)
|
||||
Modifier.focusRequester(focusRequester)
|
||||
else Modifier)
|
||||
.onFocusChanged {
|
||||
if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
||||
fobColor = if (it.isFocused) hoverColor else secondaryColor
|
||||
}
|
||||
},
|
||||
onClick = { showBottomSheet = true },
|
||||
containerColor = fobColor,
|
||||
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
|
||||
MultiFloatingActionButton(
|
||||
fabIcon = FabIcon(
|
||||
iconRes = R.drawable.add,
|
||||
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.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),
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Rounded.Add,
|
||||
contentDescription = stringResource(id = R.string.add_tunnel),
|
||||
tint = Color.DarkGray,
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
},
|
||||
) {
|
||||
AnimatedVisibility(uiState.tunnels.isEmpty(), exit = fadeOut(), enter = fadeIn()) {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Center,
|
||||
modifier = Modifier
|
||||
.fillMaxSize(),
|
||||
) {
|
||||
Text(text = stringResource(R.string.no_tunnels), fontStyle = FontStyle.Italic)
|
||||
}
|
||||
}
|
||||
if (showBottomSheet) {
|
||||
ModalBottomSheet(
|
||||
onDismissRequest = { showBottomSheet = false },
|
||||
onDismissRequest = {
|
||||
showBottomSheet = false
|
||||
|
||||
},
|
||||
sheetState = sheetState,
|
||||
) {
|
||||
// Sheet content
|
||||
@@ -321,16 +388,7 @@ fun MainScreen(
|
||||
.clickable {
|
||||
scope.launch {
|
||||
showBottomSheet = false
|
||||
val scanOptions = ScanOptions()
|
||||
scanOptions.setDesiredBarcodeFormats(ScanOptions.QR_CODE)
|
||||
scanOptions.setOrientationLocked(true)
|
||||
scanOptions.setPrompt(
|
||||
context.getString(R.string.scanning_qr),
|
||||
)
|
||||
scanOptions.setBeepEnabled(false)
|
||||
scanOptions.captureActivity =
|
||||
CaptureActivityPortrait::class.java
|
||||
scanLauncher.launch(scanOptions)
|
||||
launchQrScanner()
|
||||
}
|
||||
}
|
||||
.padding(10.dp),
|
||||
@@ -354,7 +412,7 @@ fun MainScreen(
|
||||
.clickable {
|
||||
showBottomSheet = false
|
||||
navController.navigate(
|
||||
"${Screen.Config.route}/${Constants.MANUAL_TUNNEL_CONFIG_ID}",
|
||||
"${Screen.Config.route}/${Constants.MANUAL_TUNNEL_CONFIG_ID}?configType=${configType}",
|
||||
)
|
||||
}
|
||||
.padding(10.dp),
|
||||
@@ -377,13 +435,61 @@ fun MainScreen(
|
||||
verticalArrangement = Arrangement.Top,
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.overscroll(ScrollableDefaults.overscrollEffect()),
|
||||
.fillMaxSize()
|
||||
.overscroll(ScrollableDefaults.overscrollEffect())
|
||||
.nestedScroll(nestedScrollConnection),
|
||||
state = rememberLazyListState(0, uiState.tunnels.count()),
|
||||
userScrollEnabled = true,
|
||||
reverseLayout = false,
|
||||
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 {
|
||||
if (uiState.settings.isAutoTunnelEnabled) {
|
||||
val autoTunnelingLabel = buildAnnotatedString {
|
||||
@@ -439,7 +545,7 @@ fun MainScreen(
|
||||
val leadingIconColor =
|
||||
(if (
|
||||
uiState.vpnState.tunnelConfig?.name == tunnel.name &&
|
||||
uiState.vpnState.status == Tunnel.State.UP
|
||||
uiState.vpnState.status == TunnelState.UP
|
||||
) {
|
||||
uiState.vpnState.statistics
|
||||
?.mapPeerStats()
|
||||
@@ -482,13 +588,13 @@ fun MainScreen(
|
||||
.size(if (icon == circleIcon) 15.dp else 20.dp),
|
||||
)
|
||||
},
|
||||
text = tunnel.name.truncateWithEllipsis(Constants.ALLOWED_DISPLAY_NAME_LENGTH),
|
||||
text = tunnel.name,
|
||||
onHold = {
|
||||
if (
|
||||
(uiState.vpnState.status == Tunnel.State.UP) &&
|
||||
(uiState.vpnState.status == TunnelState.UP) &&
|
||||
(tunnel.name == uiState.vpnState.tunnelConfig?.name)
|
||||
) {
|
||||
appViewModel.showSnackbarMessage(Event.Message.TunnelOffAction.message)
|
||||
appViewModel.showSnackbarMessage(context.getString(R.string.turn_off_tunnel))
|
||||
return@RowListItem
|
||||
}
|
||||
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
|
||||
@@ -497,7 +603,7 @@ fun MainScreen(
|
||||
onClick = {
|
||||
if (!WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
||||
if (
|
||||
uiState.vpnState.status == Tunnel.State.UP &&
|
||||
uiState.vpnState.status == TunnelState.UP &&
|
||||
(uiState.vpnState.tunnelConfig?.name == tunnel.name)
|
||||
) {
|
||||
expanded.value = !expanded.value
|
||||
@@ -522,7 +628,7 @@ fun MainScreen(
|
||||
!uiState.settings.isAutoTunnelPaused
|
||||
) {
|
||||
appViewModel.showSnackbarMessage(
|
||||
Event.Message.AutoTunnelOffAction.message,
|
||||
context.getString(R.string.turn_off_tunnel),
|
||||
)
|
||||
} else {
|
||||
navController.navigate(
|
||||
@@ -555,7 +661,7 @@ fun MainScreen(
|
||||
} else {
|
||||
val checked by remember {
|
||||
derivedStateOf {
|
||||
(uiState.vpnState.status == Tunnel.State.UP &&
|
||||
(uiState.vpnState.status == TunnelState.UP &&
|
||||
tunnel.name == uiState.vpnState.tunnelConfig?.name)
|
||||
}
|
||||
}
|
||||
@@ -577,7 +683,7 @@ fun MainScreen(
|
||||
onClick = {
|
||||
if (uiState.settings.isAutoTunnelEnabled) {
|
||||
appViewModel.showSnackbarMessage(
|
||||
Event.Message.AutoTunnelOffAction.message,
|
||||
context.getString(R.string.turn_off_auto),
|
||||
)
|
||||
} else {
|
||||
selectedTunnel = tunnel
|
||||
@@ -597,13 +703,13 @@ fun MainScreen(
|
||||
modifier = Modifier.focusRequester(focusRequester),
|
||||
onClick = {
|
||||
if (
|
||||
uiState.vpnState.status == Tunnel.State.UP &&
|
||||
uiState.vpnState.status == TunnelState.UP &&
|
||||
(uiState.vpnState.tunnelConfig?.name == tunnel.name)
|
||||
) {
|
||||
expanded.value = !expanded.value
|
||||
} else {
|
||||
appViewModel.showSnackbarMessage(
|
||||
Event.Message.TunnelOnAction.message,
|
||||
context.getString(R.string.turn_on_tunnel),
|
||||
)
|
||||
}
|
||||
},
|
||||
@@ -620,11 +726,11 @@ fun MainScreen(
|
||||
IconButton(
|
||||
onClick = {
|
||||
if (
|
||||
uiState.vpnState.status == Tunnel.State.UP &&
|
||||
uiState.vpnState.status == TunnelState.UP &&
|
||||
tunnel.name == uiState.vpnState.tunnelConfig?.name
|
||||
) {
|
||||
appViewModel.showSnackbarMessage(
|
||||
Event.Message.TunnelOffAction.message,
|
||||
context.getString(R.string.turn_off_tunnel),
|
||||
)
|
||||
} else {
|
||||
selectedTunnel = tunnel
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
package com.zaneschepke.wireguardautotunnel.ui.screens.main
|
||||
|
||||
import com.zaneschepke.wireguardautotunnel.data.model.Settings
|
||||
import com.zaneschepke.wireguardautotunnel.data.domain.Settings
|
||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnState
|
||||
import com.zaneschepke.wireguardautotunnel.util.TunnelConfigs
|
||||
|
||||
|
||||
+220
-94
@@ -1,6 +1,5 @@
|
||||
package com.zaneschepke.wireguardautotunnel.ui.screens.main
|
||||
|
||||
import android.app.Application
|
||||
import android.content.Context
|
||||
import android.database.Cursor
|
||||
import android.net.Uri
|
||||
@@ -9,17 +8,18 @@ import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.wireguard.config.Config
|
||||
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
|
||||
import com.zaneschepke.wireguardautotunnel.data.model.Settings
|
||||
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
|
||||
import com.zaneschepke.wireguardautotunnel.data.domain.Settings
|
||||
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
|
||||
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
|
||||
import com.zaneschepke.wireguardautotunnel.module.IoDispatcher
|
||||
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
|
||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnService
|
||||
import com.zaneschepke.wireguardautotunnel.util.Constants
|
||||
import com.zaneschepke.wireguardautotunnel.util.Event
|
||||
import com.zaneschepke.wireguardautotunnel.util.NumberUtils
|
||||
import com.zaneschepke.wireguardautotunnel.util.Result
|
||||
import com.zaneschepke.wireguardautotunnel.util.WgTunnelExceptions
|
||||
import com.zaneschepke.wireguardautotunnel.util.toWgQuickString
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
@@ -34,10 +34,10 @@ import javax.inject.Inject
|
||||
class MainViewModel
|
||||
@Inject
|
||||
constructor(
|
||||
private val application: Application,
|
||||
private val appDataRepository: AppDataRepository,
|
||||
private val serviceManager: ServiceManager,
|
||||
val vpnService: VpnService
|
||||
val vpnService: VpnService,
|
||||
@IoDispatcher private val ioDispatcher: CoroutineDispatcher
|
||||
) : ViewModel() {
|
||||
|
||||
val uiState =
|
||||
@@ -54,21 +54,20 @@ constructor(
|
||||
MainUiState(),
|
||||
)
|
||||
|
||||
private fun stopWatcherService() =
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
serviceManager.stopWatcherService(application.applicationContext)
|
||||
}
|
||||
private fun stopWatcherService(context: Context) {
|
||||
serviceManager.stopWatcherService(context)
|
||||
}
|
||||
|
||||
fun onDelete(tunnel: TunnelConfig) {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
fun onDelete(tunnel: TunnelConfig, context: Context) {
|
||||
viewModelScope.launch {
|
||||
val settings = appDataRepository.settings.getSettings()
|
||||
val isPrimary = tunnel.isPrimaryTunnel
|
||||
if (appDataRepository.tunnels.count() == 1 || isPrimary) {
|
||||
stopWatcherService()
|
||||
stopWatcherService(context)
|
||||
resetTunnelSetting(settings)
|
||||
}
|
||||
appDataRepository.tunnels.delete(tunnel)
|
||||
WireGuardAutoTunnel.requestTunnelTileServiceStateUpdate(application)
|
||||
WireGuardAutoTunnel.requestTunnelTileServiceStateUpdate()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -81,124 +80,251 @@ constructor(
|
||||
)
|
||||
}
|
||||
|
||||
fun onTunnelStart(tunnelConfig: TunnelConfig) =
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
fun onTunnelStart(tunnelConfig: TunnelConfig, context: Context) =
|
||||
viewModelScope.launch {
|
||||
Timber.d("On start called!")
|
||||
serviceManager.startVpnService(
|
||||
application.applicationContext,
|
||||
context,
|
||||
tunnelConfig.id,
|
||||
isManualStart = true,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
fun onTunnelStop() =
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
fun onTunnelStop(context: Context) =
|
||||
viewModelScope.launch {
|
||||
Timber.i("Stopping active tunnel")
|
||||
serviceManager.stopVpnService(application.applicationContext, isManualStop = true)
|
||||
serviceManager.stopVpnService(context, isManualStop = true)
|
||||
}
|
||||
|
||||
private fun validateConfigString(config: String) {
|
||||
TunnelConfig.configFromQuick(config)
|
||||
private fun validateConfigString(config: String, configType: ConfigType) {
|
||||
when (configType) {
|
||||
ConfigType.AMNEZIA -> TunnelConfig.configFromAmQuick(config)
|
||||
ConfigType.WIREGUARD -> TunnelConfig.configFromWgQuick(config)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun onTunnelQrResult(result: String): Result<Unit> {
|
||||
private fun generateQrCodeDefaultName(config: String, configType: ConfigType): String {
|
||||
return try {
|
||||
validateConfigString(result)
|
||||
val tunnelConfig =
|
||||
TunnelConfig(name = NumberUtils.generateRandomTunnelName(), wgQuick = result)
|
||||
addTunnel(tunnelConfig)
|
||||
Result.Success(Unit)
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e)
|
||||
Result.Error(Event.Error.InvalidQrCode)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun saveTunnelConfigFromStream(stream: InputStream, fileName: String) {
|
||||
val bufferReader = stream.bufferedReader(charset = Charsets.UTF_8)
|
||||
val config = Config.parse(bufferReader)
|
||||
val tunnelName = getNameFromFileName(fileName)
|
||||
addTunnel(TunnelConfig(name = tunnelName, wgQuick = config.toWgQuickString()))
|
||||
withContext(Dispatchers.IO) { stream.close() }
|
||||
}
|
||||
|
||||
private fun getInputStreamFromUri(uri: Uri): InputStream? {
|
||||
return application.applicationContext.contentResolver.openInputStream(uri)
|
||||
}
|
||||
|
||||
suspend fun onTunnelFileSelected(uri: Uri): Result<Unit> {
|
||||
try {
|
||||
if (isValidUriContentScheme(uri)) {
|
||||
val fileName = getFileName(application.applicationContext, uri)
|
||||
when (getFileExtensionFromFileName(fileName)) {
|
||||
Constants.CONF_FILE_EXTENSION ->
|
||||
saveTunnelFromConfUri(fileName, uri).let {
|
||||
when (it) {
|
||||
is Result.Error -> return Result.Error(Event.Error.FileReadFailed)
|
||||
is Result.Success -> return it
|
||||
}
|
||||
}
|
||||
|
||||
Constants.ZIP_FILE_EXTENSION -> saveTunnelsFromZipUri(uri)
|
||||
else -> return Result.Error(Event.Error.InvalidFileExtension)
|
||||
when (configType) {
|
||||
ConfigType.AMNEZIA -> {
|
||||
TunnelConfig.configFromAmQuick(config).peers[0].endpoint.get().host
|
||||
}
|
||||
|
||||
ConfigType.WIREGUARD -> {
|
||||
TunnelConfig.configFromWgQuick(config).peers[0].endpoint.get().host
|
||||
}
|
||||
return Result.Success(Unit)
|
||||
} else {
|
||||
return Result.Error(Event.Error.InvalidFileExtension)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e)
|
||||
return Result.Error(Event.Error.FileReadFailed)
|
||||
NumberUtils.generateRandomTunnelName()
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun saveTunnelsFromZipUri(uri: Uri) {
|
||||
ZipInputStream(getInputStreamFromUri(uri)).use { zip ->
|
||||
generateSequence { zip.nextEntry }
|
||||
.filterNot {
|
||||
it.isDirectory ||
|
||||
getFileExtensionFromFileName(it.name) != Constants.CONF_FILE_EXTENSION
|
||||
}
|
||||
.forEach {
|
||||
val name = getNameFromFileName(it.name)
|
||||
val config = Config.parse(zip)
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
addTunnel(TunnelConfig(name = name, wgQuick = config.toWgQuickString()))
|
||||
private fun generateQrCodeTunnelName(config: String, configType: ConfigType): String {
|
||||
var defaultName = generateQrCodeDefaultName(config, configType)
|
||||
val lines = config.lines().toMutableList()
|
||||
val linesIterator = lines.iterator()
|
||||
while (linesIterator.hasNext()) {
|
||||
val next = linesIterator.next()
|
||||
if (next.contains(Constants.QR_CODE_NAME_PROPERTY)) {
|
||||
defaultName = next.substringAfter(Constants.QR_CODE_NAME_PROPERTY).trim()
|
||||
break
|
||||
}
|
||||
}
|
||||
return defaultName
|
||||
}
|
||||
|
||||
suspend fun onTunnelQrResult(result: String, configType: ConfigType): Result<Unit> {
|
||||
return 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 saveTunnelFromConfUri(name: String, uri: Uri): Result<Unit> {
|
||||
val stream = getInputStreamFromUri(uri)
|
||||
return if (stream != null) {
|
||||
saveTunnelConfigFromStream(stream, name)
|
||||
Result.Success(Unit)
|
||||
} else {
|
||||
Result.Error(Event.Error.FileReadFailed)
|
||||
private suspend fun makeTunnelNameUnique(name: String): String {
|
||||
return withContext(ioDispatcher) {
|
||||
val tunnels = appDataRepository.tunnels.getAll()
|
||||
var tunnelName = name
|
||||
var num = 1
|
||||
while (tunnels.any { it.name == tunnelName }) {
|
||||
tunnelName = name + "(${num})"
|
||||
num++
|
||||
}
|
||||
tunnelName
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun addTunnel(tunnelConfig: TunnelConfig) {
|
||||
private fun saveTunnelConfigFromStream(
|
||||
stream: InputStream,
|
||||
fileName: String,
|
||||
type: ConfigType
|
||||
) {
|
||||
var amQuick: String? = null
|
||||
val wgQuick = stream.use {
|
||||
when (type) {
|
||||
ConfigType.AMNEZIA -> {
|
||||
val config = org.amnezia.awg.config.Config.parse(it)
|
||||
amQuick = config.toAwgQuickString()
|
||||
config.toWgQuickString()
|
||||
}
|
||||
|
||||
ConfigType.WIREGUARD -> {
|
||||
Config.parse(it).toWgQuickString()
|
||||
}
|
||||
}
|
||||
}
|
||||
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
|
||||
saveTunnel(tunnelConfig)
|
||||
if (firstTunnel) WireGuardAutoTunnel.requestTunnelTileServiceStateUpdate(application)
|
||||
if (firstTunnel) WireGuardAutoTunnel.requestTunnelTileServiceStateUpdate()
|
||||
}
|
||||
|
||||
fun pauseAutoTunneling() =
|
||||
viewModelScope.launch {
|
||||
appDataRepository.settings.save(uiState.value.settings.copy(isAutoTunnelPaused = true))
|
||||
WireGuardAutoTunnel.requestAutoTunnelTileServiceUpdate(application)
|
||||
WireGuardAutoTunnel.requestAutoTunnelTileServiceUpdate()
|
||||
}
|
||||
|
||||
fun resumeAutoTunneling() =
|
||||
viewModelScope.launch {
|
||||
appDataRepository.settings.save(uiState.value.settings.copy(isAutoTunnelPaused = false))
|
||||
WireGuardAutoTunnel.requestAutoTunnelTileServiceUpdate(application)
|
||||
WireGuardAutoTunnel.requestAutoTunnelTileServiceUpdate()
|
||||
}
|
||||
|
||||
private suspend fun saveTunnel(tunnelConfig: TunnelConfig) {
|
||||
private fun saveTunnel(tunnelConfig: TunnelConfig) = viewModelScope.launch {
|
||||
appDataRepository.tunnels.save(tunnelConfig)
|
||||
}
|
||||
|
||||
@@ -239,17 +365,17 @@ constructor(
|
||||
return fileName.substring(0, fileName.lastIndexOf('.'))
|
||||
}
|
||||
|
||||
private fun getFileExtensionFromFileName(fileName: String): String {
|
||||
private fun getFileExtensionFromFileName(fileName: String): String? {
|
||||
return try {
|
||||
fileName.substring(fileName.lastIndexOf('.'))
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e)
|
||||
""
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private fun saveSettings(settings: Settings) =
|
||||
viewModelScope.launch(Dispatchers.IO) { appDataRepository.settings.save(settings) }
|
||||
viewModelScope.launch { appDataRepository.settings.save(settings) }
|
||||
|
||||
|
||||
fun onCopyTunnel(tunnel: TunnelConfig?) = viewModelScope.launch {
|
||||
|
||||
+252
-180
@@ -1,17 +1,22 @@
|
||||
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.focusGroup
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
||||
import androidx.compose.foundation.layout.FlowRow
|
||||
import androidx.compose.foundation.layout.IntrinsicSize
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
@@ -24,9 +29,10 @@ import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.material3.surfaceColorAtElevation
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
@@ -39,15 +45,21 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalFocusManager
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontStyle
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
import androidx.compose.ui.text.input.KeyboardCapitalization
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.navigation.NavController
|
||||
import com.iamageo.multifablibrary.FabIcon
|
||||
import com.iamageo.multifablibrary.FabOption
|
||||
import com.iamageo.multifablibrary.MultiFabItem
|
||||
import com.iamageo.multifablibrary.MultiFloatingActionButton
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
|
||||
import com.zaneschepke.wireguardautotunnel.ui.AppViewModel
|
||||
@@ -55,11 +67,13 @@ import com.zaneschepke.wireguardautotunnel.ui.Screen
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.ClickableIconButton
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.config.ConfigurationToggle
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.text.SectionTitle
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.main.ConfigType
|
||||
import com.zaneschepke.wireguardautotunnel.util.Constants
|
||||
import com.zaneschepke.wireguardautotunnel.util.Result
|
||||
import com.zaneschepke.wireguardautotunnel.util.getMessage
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter")
|
||||
@OptIn(ExperimentalLayoutApi::class)
|
||||
@Composable
|
||||
fun OptionsScreen(
|
||||
@@ -71,6 +85,8 @@ fun OptionsScreen(
|
||||
) {
|
||||
val scrollState = rememberScrollState()
|
||||
val uiState by optionsViewModel.uiState.collectAsStateWithLifecycle()
|
||||
val context = LocalContext.current
|
||||
|
||||
val interactionSource = remember { MutableInteractionSource() }
|
||||
val scope = rememberCoroutineScope()
|
||||
val focusManager = LocalFocusManager.current
|
||||
@@ -90,196 +106,252 @@ fun OptionsScreen(
|
||||
fun saveTrustedSSID() {
|
||||
if (currentText.isNotEmpty()) {
|
||||
scope.launch {
|
||||
optionsViewModel.onSaveRunSSID(currentText).let {
|
||||
when (it) {
|
||||
is Result.Success -> currentText = ""
|
||||
is Result.Error -> appViewModel.showSnackbarMessage(it.error.message)
|
||||
}
|
||||
optionsViewModel.onSaveRunSSID(currentText).onSuccess {
|
||||
currentText = ""
|
||||
}.onFailure {
|
||||
appViewModel.showSnackbarMessage(it.getMessage(context))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Top,
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.verticalScroll(scrollState)
|
||||
.clickable(
|
||||
indication = null,
|
||||
interactionSource = interactionSource,
|
||||
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(),
|
||||
) {
|
||||
focusManager.clearFocus()
|
||||
},
|
||||
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),
|
||||
)
|
||||
}
|
||||
},
|
||||
) {
|
||||
Surface(
|
||||
tonalElevation = 2.dp,
|
||||
shadowElevation = 2.dp,
|
||||
shape = RoundedCornerShape(12.dp),
|
||||
color = MaterialTheme.colorScheme.surface,
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Top,
|
||||
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,
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.verticalScroll(scrollState)
|
||||
.clickable(
|
||||
indication = null,
|
||||
interactionSource = interactionSource,
|
||||
) {
|
||||
TextButton(
|
||||
onClick = {
|
||||
navController.navigate(
|
||||
"${Screen.Config.route}/${tunnelId}",
|
||||
)
|
||||
},
|
||||
) {
|
||||
Text(stringResource(R.string.edit_tunnel))
|
||||
}
|
||||
focusManager.clearFocus()
|
||||
},
|
||||
) {
|
||||
Surface(
|
||||
tonalElevation = 2.dp,
|
||||
shadowElevation = 2.dp,
|
||||
shape = RoundedCornerShape(12.dp),
|
||||
color = MaterialTheme.colorScheme.surface,
|
||||
modifier =
|
||||
(if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
||||
Modifier
|
||||
.height(IntrinsicSize.Min)
|
||||
.fillMaxWidth(fillMaxWidth)
|
||||
.padding(top = 10.dp)
|
||||
} else {
|
||||
Modifier
|
||||
.fillMaxWidth(fillMaxWidth)
|
||||
.padding(top = 20.dp)
|
||||
})
|
||||
.padding(bottom = 10.dp),
|
||||
) {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.Start,
|
||||
verticalArrangement = Arrangement.Top,
|
||||
modifier = Modifier.padding(15.dp),
|
||||
) {
|
||||
SectionTitle(
|
||||
title = stringResource(id = R.string.general),
|
||||
padding = screenPadding,
|
||||
)
|
||||
ConfigurationToggle(
|
||||
stringResource(R.string.set_primary_tunnel),
|
||||
enabled = true,
|
||||
checked = uiState.isDefaultTunnel,
|
||||
modifier = Modifier
|
||||
.focusRequester(focusRequester),
|
||||
padding = screenPadding,
|
||||
onCheckChanged = { optionsViewModel.onTogglePrimaryTunnel() },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
Surface(
|
||||
tonalElevation = 2.dp,
|
||||
shadowElevation = 2.dp,
|
||||
shape = RoundedCornerShape(12.dp),
|
||||
color = MaterialTheme.colorScheme.surface,
|
||||
modifier =
|
||||
(if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
||||
Modifier
|
||||
.height(IntrinsicSize.Min)
|
||||
.fillMaxWidth(fillMaxWidth)
|
||||
.padding(top = 10.dp)
|
||||
} else {
|
||||
Modifier
|
||||
.fillMaxWidth(fillMaxWidth)
|
||||
.padding(top = 20.dp)
|
||||
})
|
||||
.padding(bottom = 10.dp),
|
||||
) {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.Start,
|
||||
verticalArrangement = Arrangement.Top,
|
||||
modifier = Modifier.padding(15.dp),
|
||||
Surface(
|
||||
tonalElevation = 2.dp,
|
||||
shadowElevation = 2.dp,
|
||||
shape = RoundedCornerShape(12.dp),
|
||||
color = MaterialTheme.colorScheme.surface,
|
||||
modifier =
|
||||
(if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
||||
Modifier
|
||||
.height(IntrinsicSize.Min)
|
||||
.fillMaxWidth(fillMaxWidth)
|
||||
.padding(top = 10.dp)
|
||||
} else {
|
||||
Modifier
|
||||
.fillMaxWidth(fillMaxWidth)
|
||||
.padding(top = 20.dp)
|
||||
})
|
||||
.padding(bottom = 10.dp),
|
||||
) {
|
||||
SectionTitle(
|
||||
title = stringResource(id = R.string.auto_tunneling),
|
||||
padding = screenPadding,
|
||||
)
|
||||
ConfigurationToggle(
|
||||
stringResource(R.string.mobile_data_tunnel),
|
||||
enabled = true,
|
||||
checked = uiState.tunnel?.isMobileDataTunnel == true,
|
||||
padding = screenPadding,
|
||||
onCheckChanged = { optionsViewModel.onToggleIsMobileDataTunnel() },
|
||||
)
|
||||
Column {
|
||||
FlowRow(
|
||||
modifier = Modifier
|
||||
.padding(screenPadding)
|
||||
.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(5.dp),
|
||||
) {
|
||||
uiState.tunnel?.tunnelNetworks?.forEach { ssid ->
|
||||
ClickableIconButton(
|
||||
onClick = {
|
||||
if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
||||
focusRequester.requestFocus()
|
||||
optionsViewModel.onDeleteRunSSID(ssid)
|
||||
}
|
||||
},
|
||||
onIconClick = {
|
||||
if (WireGuardAutoTunnel.isRunningOnAndroidTv()) focusRequester.requestFocus()
|
||||
optionsViewModel.onDeleteRunSSID(ssid)
|
||||
|
||||
},
|
||||
text = ssid,
|
||||
icon = Icons.Filled.Close,
|
||||
enabled = true,
|
||||
)
|
||||
}
|
||||
if (uiState.tunnel == null || uiState.tunnel?.tunnelNetworks?.isEmpty() == true) {
|
||||
Text(
|
||||
stringResource(R.string.no_wifi_names_configured),
|
||||
fontStyle = FontStyle.Italic,
|
||||
color = Color.Gray,
|
||||
)
|
||||
}
|
||||
}
|
||||
OutlinedTextField(
|
||||
enabled = true,
|
||||
value = currentText,
|
||||
onValueChange = { currentText = it },
|
||||
label = { Text(stringResource(id = R.string.use_tunnel_on_wifi_name)) },
|
||||
modifier =
|
||||
Modifier
|
||||
.padding(
|
||||
start = screenPadding,
|
||||
top = 5.dp,
|
||||
bottom = 10.dp,
|
||||
),
|
||||
maxLines = 1,
|
||||
keyboardOptions =
|
||||
KeyboardOptions(
|
||||
capitalization = KeyboardCapitalization.None,
|
||||
imeAction = ImeAction.Done,
|
||||
),
|
||||
keyboardActions = KeyboardActions(onDone = { saveTrustedSSID() }),
|
||||
trailingIcon = {
|
||||
if (currentText != "") {
|
||||
IconButton(onClick = { saveTrustedSSID() }) {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.Add,
|
||||
contentDescription =
|
||||
if (currentText == "") {
|
||||
stringResource(
|
||||
id =
|
||||
R.string
|
||||
.trusted_ssid_empty_description,
|
||||
)
|
||||
} else {
|
||||
stringResource(
|
||||
id =
|
||||
R.string
|
||||
.trusted_ssid_value_description,
|
||||
)
|
||||
},
|
||||
tint = MaterialTheme.colorScheme.primary,
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
Column(
|
||||
horizontalAlignment = Alignment.Start,
|
||||
verticalArrangement = Arrangement.Top,
|
||||
modifier = Modifier.padding(15.dp),
|
||||
) {
|
||||
SectionTitle(
|
||||
title = stringResource(id = R.string.auto_tunneling),
|
||||
padding = screenPadding,
|
||||
)
|
||||
ConfigurationToggle(
|
||||
stringResource(R.string.mobile_data_tunnel),
|
||||
enabled = true,
|
||||
checked = uiState.tunnel?.isMobileDataTunnel == true,
|
||||
padding = screenPadding,
|
||||
onCheckChanged = { optionsViewModel.onToggleIsMobileDataTunnel() },
|
||||
)
|
||||
Column {
|
||||
FlowRow(
|
||||
modifier = Modifier
|
||||
.padding(screenPadding)
|
||||
.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(5.dp),
|
||||
) {
|
||||
uiState.tunnel?.tunnelNetworks?.forEach { ssid ->
|
||||
ClickableIconButton(
|
||||
onClick = {
|
||||
if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
||||
focusRequester.requestFocus()
|
||||
optionsViewModel.onDeleteRunSSID(ssid)
|
||||
}
|
||||
},
|
||||
onIconClick = {
|
||||
if (WireGuardAutoTunnel.isRunningOnAndroidTv()) focusRequester.requestFocus()
|
||||
optionsViewModel.onDeleteRunSSID(ssid)
|
||||
|
||||
},
|
||||
text = ssid,
|
||||
icon = Icons.Filled.Close,
|
||||
enabled = true,
|
||||
)
|
||||
}
|
||||
if (uiState.tunnel == null || uiState.tunnel?.tunnelNetworks?.isEmpty() == true) {
|
||||
Text(
|
||||
stringResource(R.string.no_wifi_names_configured),
|
||||
fontStyle = FontStyle.Italic,
|
||||
color = Color.Gray,
|
||||
)
|
||||
}
|
||||
}
|
||||
OutlinedTextField(
|
||||
enabled = true,
|
||||
value = currentText,
|
||||
onValueChange = { currentText = it },
|
||||
label = { Text(stringResource(id = R.string.use_tunnel_on_wifi_name)) },
|
||||
modifier =
|
||||
Modifier
|
||||
.padding(
|
||||
start = screenPadding,
|
||||
top = 5.dp,
|
||||
bottom = 10.dp,
|
||||
),
|
||||
maxLines = 1,
|
||||
keyboardOptions =
|
||||
KeyboardOptions(
|
||||
capitalization = KeyboardCapitalization.None,
|
||||
imeAction = ImeAction.Done,
|
||||
),
|
||||
keyboardActions = KeyboardActions(onDone = { saveTrustedSSID() }),
|
||||
trailingIcon = {
|
||||
if (currentText != "") {
|
||||
IconButton(onClick = { saveTrustedSSID() }) {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.Add,
|
||||
contentDescription =
|
||||
if (currentText == "") {
|
||||
stringResource(
|
||||
id =
|
||||
R.string
|
||||
.trusted_ssid_empty_description,
|
||||
)
|
||||
} else {
|
||||
stringResource(
|
||||
id =
|
||||
R.string
|
||||
.trusted_ssid_value_description,
|
||||
)
|
||||
},
|
||||
tint = MaterialTheme.colorScheme.primary,
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
package com.zaneschepke.wireguardautotunnel.ui.screens.options
|
||||
|
||||
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
|
||||
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
|
||||
|
||||
data class OptionsUiState(
|
||||
val id: String? = null,
|
||||
|
||||
+15
-14
@@ -4,17 +4,16 @@ import androidx.compose.ui.util.fastFirstOrNull
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
|
||||
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
|
||||
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
|
||||
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
|
||||
import com.zaneschepke.wireguardautotunnel.util.Constants
|
||||
import com.zaneschepke.wireguardautotunnel.util.Event
|
||||
import com.zaneschepke.wireguardautotunnel.util.Result
|
||||
import com.zaneschepke.wireguardautotunnel.util.WgTunnelExceptions
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import javax.inject.Inject
|
||||
@@ -43,12 +42,14 @@ constructor(
|
||||
)
|
||||
|
||||
fun init(tunnelId: String) {
|
||||
_optionState.value = _optionState.value.copy(
|
||||
id = tunnelId,
|
||||
)
|
||||
_optionState.update {
|
||||
it.copy(
|
||||
id = tunnelId,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun onDeleteRunSSID(ssid: String) = viewModelScope.launch(Dispatchers.IO) {
|
||||
fun onDeleteRunSSID(ssid: String) = viewModelScope.launch {
|
||||
uiState.value.tunnel?.let {
|
||||
appDataRepository.tunnels.save(
|
||||
tunnelConfig = it.copy(
|
||||
@@ -58,7 +59,7 @@ constructor(
|
||||
}
|
||||
}
|
||||
|
||||
private fun saveTunnel(tunnelConfig: TunnelConfig?) = viewModelScope.launch(Dispatchers.IO) {
|
||||
private fun saveTunnel(tunnelConfig: TunnelConfig?) = viewModelScope.launch {
|
||||
tunnelConfig?.let {
|
||||
appDataRepository.tunnels.save(it)
|
||||
}
|
||||
@@ -73,13 +74,13 @@ constructor(
|
||||
tunnelsWithName.isEmpty()) {
|
||||
uiState.value.tunnel?.tunnelNetworks?.add(trimmed)
|
||||
saveTunnel(uiState.value.tunnel)
|
||||
Result.Success(Unit)
|
||||
Result.success(Unit)
|
||||
} else {
|
||||
Result.Error(Event.Error.SsidConflict)
|
||||
Result.failure(WgTunnelExceptions.SsidConflict())
|
||||
}
|
||||
}
|
||||
|
||||
fun onToggleIsMobileDataTunnel() = viewModelScope.launch(Dispatchers.IO) {
|
||||
fun onToggleIsMobileDataTunnel() = viewModelScope.launch {
|
||||
uiState.value.tunnel?.let {
|
||||
if (it.isMobileDataTunnel) {
|
||||
appDataRepository.tunnels.updateMobileDataTunnel(null)
|
||||
@@ -87,7 +88,7 @@ constructor(
|
||||
}
|
||||
}
|
||||
|
||||
fun onTogglePrimaryTunnel() = viewModelScope.launch(Dispatchers.IO) {
|
||||
fun onTogglePrimaryTunnel() = viewModelScope.launch {
|
||||
if (uiState.value.tunnel != null) {
|
||||
appDataRepository.tunnels.updatePrimaryTunnel(
|
||||
when (uiState.value.isDefaultTunnel) {
|
||||
@@ -95,7 +96,7 @@ constructor(
|
||||
false -> uiState.value.tunnel
|
||||
},
|
||||
)
|
||||
WireGuardAutoTunnel.requestTunnelTileServiceStateUpdate(WireGuardAutoTunnel.instance)
|
||||
WireGuardAutoTunnel.requestTunnelTileServiceStateUpdate()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+72
-52
@@ -70,20 +70,18 @@ import androidx.navigation.NavController
|
||||
import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
||||
import com.google.accompanist.permissions.isGranted
|
||||
import com.google.accompanist.permissions.rememberPermissionState
|
||||
import com.wireguard.android.backend.Tunnel
|
||||
import com.wireguard.android.backend.WgQuickBackend
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
|
||||
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
|
||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelState
|
||||
import com.zaneschepke.wireguardautotunnel.ui.AppViewModel
|
||||
import com.zaneschepke.wireguardautotunnel.ui.Screen
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.ClickableIconButton
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.config.ConfigurationToggle
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.prompt.AuthorizationPrompt
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.text.SectionTitle
|
||||
import com.zaneschepke.wireguardautotunnel.util.Event
|
||||
import com.zaneschepke.wireguardautotunnel.util.FileUtils
|
||||
import com.zaneschepke.wireguardautotunnel.util.Result
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import com.zaneschepke.wireguardautotunnel.util.getMessage
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import xyz.teamgravity.pin_lock_compose.PinManager
|
||||
@@ -100,9 +98,9 @@ fun SettingsScreen(
|
||||
navController: NavController,
|
||||
focusRequester: FocusRequester,
|
||||
) {
|
||||
val scope = rememberCoroutineScope { Dispatchers.IO }
|
||||
val context = LocalContext.current
|
||||
val focusManager = LocalFocusManager.current
|
||||
val scope = rememberCoroutineScope()
|
||||
val scrollState = rememberScrollState()
|
||||
val interactionSource = remember { MutableInteractionSource() }
|
||||
val pinExists = remember { mutableStateOf(PinManager.pinExists()) }
|
||||
@@ -130,16 +128,32 @@ fun SettingsScreen(
|
||||
|
||||
fun exportAllConfigs() {
|
||||
try {
|
||||
val files = uiState.tunnels.map { File(context.cacheDir, "${it.name}.conf") }
|
||||
files.forEachIndexed { index, file ->
|
||||
file.outputStream().use { it.write(uiState.tunnels[index].wgQuick.toByteArray()) }
|
||||
val wgFiles = uiState.tunnels.map { config ->
|
||||
val file = File(context.cacheDir, "${config.name}-wg.conf")
|
||||
file.outputStream().use {
|
||||
it.write(config.wgQuick.toByteArray())
|
||||
}
|
||||
file
|
||||
}
|
||||
val amFiles = uiState.tunnels.mapNotNull { config ->
|
||||
if (config.amQuick != TunnelConfig.AM_QUICK_DEFAULT) {
|
||||
val file = File(context.cacheDir, "${config.name}-am.conf")
|
||||
file.outputStream().use {
|
||||
it.write(config.amQuick.toByteArray())
|
||||
}
|
||||
file
|
||||
} else null
|
||||
}
|
||||
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) {
|
||||
Timber.e(e)
|
||||
appViewModel.showSnackbarMessage(Event.Error.Exception(e).message)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -160,7 +174,7 @@ fun SettingsScreen(
|
||||
fun handleAutoTunnelToggle() {
|
||||
if (uiState.isBatteryOptimizeDisableShown || isBatteryOptimizationsDisabled()) {
|
||||
if (appViewModel.isRequiredPermissionGranted()) {
|
||||
viewModel.onToggleAutoTunnel()
|
||||
viewModel.onToggleAutoTunnel(context)
|
||||
}
|
||||
} else {
|
||||
requestBatteryOptimizationsDisabled()
|
||||
@@ -169,21 +183,18 @@ fun SettingsScreen(
|
||||
|
||||
fun saveTrustedSSID() {
|
||||
if (currentText.isNotEmpty()) {
|
||||
viewModel.onSaveTrustedSSID(currentText).let {
|
||||
when (it) {
|
||||
is Result.Success -> currentText = ""
|
||||
is Result.Error -> appViewModel.showSnackbarMessage(it.error.message)
|
||||
}
|
||||
viewModel.onSaveTrustedSSID(currentText).onSuccess {
|
||||
currentText = ""
|
||||
}.onFailure {
|
||||
appViewModel.showSnackbarMessage(it.getMessage(context))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun openSettings() {
|
||||
scope.launch {
|
||||
val intentSettings = Intent(ACTION_APPLICATION_DETAILS_SETTINGS)
|
||||
intentSettings.data = Uri.fromParts("package", context.packageName, null)
|
||||
context.startActivity(intentSettings)
|
||||
}
|
||||
val intentSettings = Intent(ACTION_APPLICATION_DETAILS_SETTINGS)
|
||||
intentSettings.data = Uri.fromParts("package", context.packageName, null)
|
||||
context.startActivity(intentSettings)
|
||||
}
|
||||
|
||||
fun checkFineLocationGranted() {
|
||||
@@ -307,11 +318,11 @@ fun SettingsScreen(
|
||||
},
|
||||
onError = { _ ->
|
||||
showAuthPrompt = false
|
||||
appViewModel.showSnackbarMessage(Event.Error.AuthenticationFailed.message)
|
||||
appViewModel.showSnackbarMessage(context.getString(R.string.error_authentication_failed))
|
||||
},
|
||||
onFailure = {
|
||||
showAuthPrompt = false
|
||||
appViewModel.showSnackbarMessage(Event.Error.AuthorizationFailed.message)
|
||||
appViewModel.showSnackbarMessage(context.getString(R.string.error_authorization_failed))
|
||||
},
|
||||
)
|
||||
}
|
||||
@@ -519,12 +530,12 @@ fun SettingsScreen(
|
||||
when (false) {
|
||||
isBackgroundLocationGranted ->
|
||||
appViewModel.showSnackbarMessage(
|
||||
Event.Error.BackgroundLocationRequired.message,
|
||||
context.getString(R.string.background_location_required),
|
||||
)
|
||||
|
||||
fineLocationState.status.isGranted ->
|
||||
appViewModel.showSnackbarMessage(
|
||||
Event.Error.PreciseLocationRequired.message,
|
||||
context.getString(R.string.precise_location_required),
|
||||
)
|
||||
|
||||
viewModel.isLocationEnabled(context) ->
|
||||
@@ -550,39 +561,48 @@ fun SettingsScreen(
|
||||
}
|
||||
}
|
||||
}
|
||||
if (WgQuickBackend.hasKernelSupport()) {
|
||||
Surface(
|
||||
tonalElevation = 2.dp,
|
||||
shadowElevation = 2.dp,
|
||||
shape = RoundedCornerShape(12.dp),
|
||||
color = MaterialTheme.colorScheme.surface,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth(fillMaxWidth)
|
||||
.padding(vertical = 10.dp),
|
||||
Surface(
|
||||
tonalElevation = 2.dp,
|
||||
shadowElevation = 2.dp,
|
||||
shape = RoundedCornerShape(12.dp),
|
||||
color = MaterialTheme.colorScheme.surface,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth(fillMaxWidth)
|
||||
.padding(vertical = 10.dp),
|
||||
) {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.Start,
|
||||
verticalArrangement = Arrangement.Top,
|
||||
modifier = Modifier.padding(15.dp),
|
||||
) {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.Start,
|
||||
verticalArrangement = Arrangement.Top,
|
||||
modifier = Modifier.padding(15.dp),
|
||||
) {
|
||||
SectionTitle(
|
||||
title = stringResource(id = R.string.kernel),
|
||||
padding = screenPadding,
|
||||
)
|
||||
SectionTitle(
|
||||
title = stringResource(id = R.string.backend),
|
||||
padding = screenPadding,
|
||||
)
|
||||
ConfigurationToggle(
|
||||
stringResource(R.string.use_amnezia),
|
||||
enabled =
|
||||
!(uiState.settings.isAutoTunnelEnabled ||
|
||||
uiState.settings.isAlwaysOnVpnEnabled ||
|
||||
(uiState.vpnState.status == TunnelState.UP) || uiState.settings.isKernelEnabled),
|
||||
checked = uiState.settings.isAmneziaEnabled,
|
||||
padding = screenPadding,
|
||||
onCheckChanged = {
|
||||
viewModel.onToggleAmnezia()
|
||||
},
|
||||
)
|
||||
if (WgQuickBackend.hasKernelSupport()) {
|
||||
ConfigurationToggle(
|
||||
stringResource(R.string.use_kernel),
|
||||
enabled =
|
||||
!(uiState.settings.isAutoTunnelEnabled ||
|
||||
uiState.settings.isAlwaysOnVpnEnabled ||
|
||||
(uiState.vpnState.status == Tunnel.State.UP)),
|
||||
(uiState.vpnState.status == TunnelState.UP)),
|
||||
checked = uiState.settings.isKernelEnabled,
|
||||
padding = screenPadding,
|
||||
onCheckChanged = {
|
||||
viewModel.onToggleKernelMode().let {
|
||||
when (it) {
|
||||
is Result.Error -> appViewModel.showSnackbarMessage(it.error.message)
|
||||
is Result.Success -> {}
|
||||
}
|
||||
viewModel.onToggleKernelMode().onFailure {
|
||||
appViewModel.showSnackbarMessage(it.getMessage(context))
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
+2
-2
@@ -1,7 +1,7 @@
|
||||
package com.zaneschepke.wireguardautotunnel.ui.screens.settings
|
||||
|
||||
import com.zaneschepke.wireguardautotunnel.data.model.Settings
|
||||
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
|
||||
import com.zaneschepke.wireguardautotunnel.data.domain.Settings
|
||||
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
|
||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnState
|
||||
|
||||
data class SettingsUiState(
|
||||
|
||||
+39
-14
@@ -1,6 +1,5 @@
|
||||
package com.zaneschepke.wireguardautotunnel.ui.screens.settings
|
||||
|
||||
import android.app.Application
|
||||
import android.content.Context
|
||||
import android.location.LocationManager
|
||||
import androidx.core.location.LocationManagerCompat
|
||||
@@ -8,29 +7,30 @@ import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.wireguard.android.util.RootShell
|
||||
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
|
||||
import com.zaneschepke.wireguardautotunnel.data.model.Settings
|
||||
import com.zaneschepke.wireguardautotunnel.data.domain.Settings
|
||||
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
|
||||
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
|
||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnService
|
||||
import com.zaneschepke.wireguardautotunnel.util.Constants
|
||||
import com.zaneschepke.wireguardautotunnel.util.Event
|
||||
import com.zaneschepke.wireguardautotunnel.util.Result
|
||||
import com.zaneschepke.wireguardautotunnel.util.FileUtils
|
||||
import com.zaneschepke.wireguardautotunnel.util.WgTunnelExceptions
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import java.io.File
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class SettingsViewModel
|
||||
@Inject
|
||||
constructor(
|
||||
private val application: Application,
|
||||
private val appDataRepository: AppDataRepository,
|
||||
private val serviceManager: ServiceManager,
|
||||
private val rootShell: RootShell,
|
||||
private val fileUtils: FileUtils,
|
||||
vpnService: VpnService
|
||||
) : ViewModel() {
|
||||
|
||||
@@ -60,9 +60,9 @@ constructor(
|
||||
return if (!uiState.value.settings.trustedNetworkSSIDs.contains(trimmed)) {
|
||||
uiState.value.settings.trustedNetworkSSIDs.add(trimmed)
|
||||
saveSettings(uiState.value.settings)
|
||||
Result.Success(Unit)
|
||||
Result.success(Unit)
|
||||
} else {
|
||||
Result.Error(Event.Error.SsidConflict)
|
||||
Result.failure(WgTunnelExceptions.SsidConflict())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -93,15 +93,19 @@ constructor(
|
||||
)
|
||||
}
|
||||
|
||||
fun onToggleAutoTunnel() =
|
||||
suspend fun onExportTunnels(files: List<File>): Result<Unit> {
|
||||
return fileUtils.saveFilesToZip(files)
|
||||
}
|
||||
|
||||
fun onToggleAutoTunnel(context: Context) =
|
||||
viewModelScope.launch {
|
||||
val isAutoTunnelEnabled = uiState.value.settings.isAutoTunnelEnabled
|
||||
var isAutoTunnelPaused = uiState.value.settings.isAutoTunnelPaused
|
||||
|
||||
if (isAutoTunnelEnabled) {
|
||||
serviceManager.stopWatcherService(application)
|
||||
serviceManager.stopWatcherService(context)
|
||||
} else {
|
||||
serviceManager.startWatcherService(application)
|
||||
serviceManager.startWatcherService(context)
|
||||
isAutoTunnelPaused = false
|
||||
}
|
||||
saveSettings(
|
||||
@@ -110,7 +114,7 @@ constructor(
|
||||
isAutoTunnelPaused = isAutoTunnelPaused,
|
||||
),
|
||||
)
|
||||
WireGuardAutoTunnel.requestAutoTunnelTileServiceUpdate(application)
|
||||
WireGuardAutoTunnel.requestAutoTunnelTileServiceUpdate()
|
||||
}
|
||||
|
||||
fun onToggleAlwaysOnVPN() =
|
||||
@@ -162,21 +166,42 @@ constructor(
|
||||
)
|
||||
}
|
||||
|
||||
fun onToggleAmnezia() = viewModelScope.launch {
|
||||
if (uiState.value.settings.isKernelEnabled) {
|
||||
saveKernelMode(false)
|
||||
}
|
||||
saveAmneziaMode(!uiState.value.settings.isAmneziaEnabled)
|
||||
}
|
||||
|
||||
private fun saveAmneziaMode(on: Boolean) {
|
||||
saveSettings(
|
||||
uiState.value.settings.copy(
|
||||
isAmneziaEnabled = on,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fun onToggleKernelMode(): Result<Unit> {
|
||||
if (!uiState.value.settings.isKernelEnabled) {
|
||||
try {
|
||||
rootShell.start()
|
||||
Timber.i("Root shell accepted!")
|
||||
saveKernelMode(on = true)
|
||||
saveSettings(
|
||||
uiState.value.settings.copy(
|
||||
isKernelEnabled = true,
|
||||
isAmneziaEnabled = false,
|
||||
),
|
||||
)
|
||||
|
||||
} catch (e: RootShell.RootShellException) {
|
||||
Timber.e(e)
|
||||
saveKernelMode(on = false)
|
||||
return Result.Error(Event.Error.RootDenied)
|
||||
return Result.failure(WgTunnelExceptions.RootDenied())
|
||||
}
|
||||
} else {
|
||||
saveKernelMode(on = false)
|
||||
}
|
||||
return Result.Success(Unit)
|
||||
return Result.success(Unit)
|
||||
}
|
||||
|
||||
fun onToggleRestartOnPing() = viewModelScope.launch {
|
||||
|
||||
+47
-9
@@ -37,6 +37,7 @@ import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.res.vectorResource
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.buildAnnotatedString
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextDecoration
|
||||
@@ -106,7 +107,12 @@ fun SupportScreen(
|
||||
modifier = Modifier.padding(bottom = 20.dp),
|
||||
)
|
||||
TextButton(
|
||||
onClick = { appViewModel.openWebPage(context.resources.getString(R.string.docs_url)) },
|
||||
onClick = {
|
||||
appViewModel.openWebPage(
|
||||
context.resources.getString(R.string.docs_url),
|
||||
context,
|
||||
)
|
||||
},
|
||||
modifier = Modifier
|
||||
.padding(vertical = 5.dp)
|
||||
.focusRequester(focusRequester),
|
||||
@@ -122,7 +128,13 @@ fun SupportScreen(
|
||||
Text(
|
||||
stringResource(id = R.string.docs_description),
|
||||
textAlign = TextAlign.Justify,
|
||||
modifier = Modifier.padding(start = 10.dp),
|
||||
modifier = Modifier
|
||||
.padding(start = 10.dp)
|
||||
.weight(
|
||||
weight = 1.0f,
|
||||
fill = false,
|
||||
),
|
||||
softWrap = true,
|
||||
)
|
||||
}
|
||||
Icon(
|
||||
@@ -136,7 +148,12 @@ fun SupportScreen(
|
||||
color = MaterialTheme.colorScheme.onBackground,
|
||||
)
|
||||
TextButton(
|
||||
onClick = { appViewModel.openWebPage(context.resources.getString(R.string.discord_url)) },
|
||||
onClick = {
|
||||
appViewModel.openWebPage(
|
||||
context.resources.getString(R.string.telegram_url),
|
||||
context,
|
||||
)
|
||||
},
|
||||
modifier = Modifier.padding(vertical = 5.dp),
|
||||
) {
|
||||
Row(
|
||||
@@ -145,7 +162,7 @@ fun SupportScreen(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
) {
|
||||
Row {
|
||||
val icon = ImageVector.vectorResource(R.drawable.discord)
|
||||
val icon = ImageVector.vectorResource(R.drawable.telegram)
|
||||
Icon(
|
||||
icon,
|
||||
icon.name,
|
||||
@@ -168,7 +185,12 @@ fun SupportScreen(
|
||||
color = MaterialTheme.colorScheme.onBackground,
|
||||
)
|
||||
TextButton(
|
||||
onClick = { appViewModel.openWebPage(context.resources.getString(R.string.github_url)) },
|
||||
onClick = {
|
||||
appViewModel.openWebPage(
|
||||
context.resources.getString(R.string.github_url),
|
||||
context,
|
||||
)
|
||||
},
|
||||
modifier = Modifier.padding(vertical = 5.dp),
|
||||
) {
|
||||
Row(
|
||||
@@ -200,7 +222,7 @@ fun SupportScreen(
|
||||
color = MaterialTheme.colorScheme.onBackground,
|
||||
)
|
||||
TextButton(
|
||||
onClick = { appViewModel.launchEmail() },
|
||||
onClick = { appViewModel.launchEmail(context) },
|
||||
modifier = Modifier.padding(vertical = 5.dp),
|
||||
) {
|
||||
Row(
|
||||
@@ -262,7 +284,10 @@ fun SupportScreen(
|
||||
fontSize = 16.sp,
|
||||
modifier =
|
||||
Modifier.clickable {
|
||||
appViewModel.openWebPage(context.resources.getString(R.string.privacy_policy_url))
|
||||
appViewModel.openWebPage(
|
||||
context.resources.getString(R.string.privacy_policy_url),
|
||||
context,
|
||||
)
|
||||
},
|
||||
)
|
||||
Row(
|
||||
@@ -270,8 +295,21 @@ fun SupportScreen(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.padding(25.dp),
|
||||
) {
|
||||
Text("Version: ${BuildConfig.VERSION_NAME}", modifier = Modifier.focusable())
|
||||
Text("Mode: ${if (uiState.settings.isKernelEnabled) "Kernel" else "Userspace"}")
|
||||
val version = buildAnnotatedString {
|
||||
append(stringResource(id = R.string.version))
|
||||
append(": ")
|
||||
append(BuildConfig.VERSION_NAME)
|
||||
}
|
||||
val mode = buildAnnotatedString {
|
||||
append(stringResource(R.string.mode))
|
||||
append(": ")
|
||||
when (uiState.settings.isKernelEnabled) {
|
||||
true -> append(stringResource(id = R.string.kernel))
|
||||
false -> append(stringResource(id = R.string.userspace))
|
||||
}
|
||||
}
|
||||
Text(version.text, modifier = Modifier.focusable())
|
||||
Text(mode.text)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
package com.zaneschepke.wireguardautotunnel.ui.screens.support
|
||||
|
||||
import com.zaneschepke.wireguardautotunnel.data.model.Settings
|
||||
import com.zaneschepke.wireguardautotunnel.data.domain.Settings
|
||||
|
||||
data class SupportUiState(val settings: Settings = Settings())
|
||||
|
||||
+24
-8
@@ -1,6 +1,7 @@
|
||||
package com.zaneschepke.wireguardautotunnel.ui.screens.support.logs
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.widget.Toast
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
@@ -8,7 +9,7 @@ import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
@@ -27,21 +28,24 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.ClipboardManager
|
||||
import androidx.compose.ui.platform.LocalClipboardManager
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.zaneschepke.wireguardautotunnel.ui.AppViewModel
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import com.zaneschepke.logcatter.model.LogMessage
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.text.LogTypeLabel
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter")
|
||||
@Composable
|
||||
fun LogsScreen(appViewModel: AppViewModel) {
|
||||
fun LogsScreen(viewModel: LogsViewModel = hiltViewModel()) {
|
||||
|
||||
val logs = remember {
|
||||
appViewModel.logs
|
||||
}
|
||||
val logs = viewModel.logs
|
||||
|
||||
val context = LocalContext.current
|
||||
|
||||
val lazyColumnListState = rememberLazyListState()
|
||||
val clipboardManager: ClipboardManager = LocalClipboardManager.current
|
||||
@@ -57,7 +61,15 @@ fun LogsScreen(appViewModel: AppViewModel) {
|
||||
floatingActionButton = {
|
||||
FloatingActionButton(
|
||||
onClick = {
|
||||
appViewModel.saveLogsToFile()
|
||||
scope.launch {
|
||||
viewModel.saveLogsToFile().onSuccess {
|
||||
Toast.makeText(
|
||||
context,
|
||||
context.getString(R.string.logs_saved),
|
||||
Toast.LENGTH_SHORT,
|
||||
).show()
|
||||
}
|
||||
}
|
||||
},
|
||||
shape = RoundedCornerShape(16.dp),
|
||||
containerColor = MaterialTheme.colorScheme.primary,
|
||||
@@ -79,7 +91,11 @@ fun LogsScreen(appViewModel: AppViewModel) {
|
||||
.fillMaxSize()
|
||||
.padding(horizontal = 24.dp),
|
||||
) {
|
||||
items(logs) {
|
||||
itemsIndexed(
|
||||
logs,
|
||||
key = { index, _ -> index },
|
||||
contentType = { _: Int, _: LogMessage -> null },
|
||||
) { _, it ->
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(5.dp, Alignment.Start),
|
||||
verticalAlignment = Alignment.Top,
|
||||
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
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 {
|
||||
|
||||
const val BASE_LOG_FILE_NAME = "wgtunnel-logs"
|
||||
const val BASE_LOG_FILE_NAME = "wg_tunnel_logs"
|
||||
const val LOG_BUFFER_SIZE = 3_000L
|
||||
|
||||
const val MANUAL_TUNNEL_CONFIG_ID = "0"
|
||||
@@ -16,16 +16,17 @@ object Constants {
|
||||
const val URI_CONTENT_SCHEME = "content"
|
||||
const val ALLOWED_FILE_TYPES = "*/*"
|
||||
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 ANDROID_TV_EXPLORER_STUB = "com.android.tv.frameworkpackagestubs"
|
||||
const val ALWAYS_ON_VPN_ACTION = "android.net.VpnService"
|
||||
const val EMAIL_MIME_TYPE = "message/rfc822"
|
||||
const val EMAIL_MIME_TYPE = "plain/text"
|
||||
const val SYSTEM_EXEMPT_SERVICE_TYPE_ID = 1024
|
||||
|
||||
const val SUBSCRIPTION_TIMEOUT = 5_000L
|
||||
const val FOCUS_REQUEST_DELAY = 500L
|
||||
|
||||
const val BACKUP_PING_HOST = "1.1.1.1"
|
||||
const val DEFAULT_PING_IP = "1.1.1.1"
|
||||
const val PING_TIMEOUT = 5_000L
|
||||
const val VPN_RESTART_DELAY = 1_000L
|
||||
const val PING_INTERVAL = 60_000L
|
||||
@@ -35,4 +36,9 @@ object Constants {
|
||||
|
||||
const val TUNNEL_EXTRA_KEY = "tunnelId"
|
||||
|
||||
const val UNREADABLE_SSID = "<unknown ssid>"
|
||||
|
||||
val amneziaProperties = listOf("Jc", "Jmin", "Jmax", "S1", "S2", "H1", "H2", "H3", "H4")
|
||||
const val QR_CODE_NAME_PROPERTY = "# Name ="
|
||||
|
||||
}
|
||||
|
||||
@@ -1,117 +0,0 @@
|
||||
package com.zaneschepke.wireguardautotunnel.util
|
||||
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
|
||||
|
||||
sealed class Event {
|
||||
|
||||
abstract val message: String
|
||||
|
||||
sealed class Error : Event() {
|
||||
data object None : Error() {
|
||||
override val message: String
|
||||
get() = WireGuardAutoTunnel.instance.getString(R.string.error_none)
|
||||
}
|
||||
|
||||
data object SsidConflict : Error() {
|
||||
override val message: String
|
||||
get() = WireGuardAutoTunnel.instance.getString(R.string.error_ssid_exists)
|
||||
}
|
||||
|
||||
data class ConfigParseError(val appendedMessage: String) : Error() {
|
||||
override val message: String =
|
||||
WireGuardAutoTunnel.instance.getString(R.string.config_parse_error) + (
|
||||
if (appendedMessage != "") ": ${appendedMessage.trim()}" else "")
|
||||
}
|
||||
|
||||
data object RootDenied : Error() {
|
||||
override val message: String
|
||||
get() = WireGuardAutoTunnel.instance.getString(R.string.error_root_denied)
|
||||
}
|
||||
|
||||
data class General(val customMessage: String) : Error() {
|
||||
override val message: String
|
||||
get() = customMessage
|
||||
}
|
||||
|
||||
data class Exception(val exception: kotlin.Exception) : Error() {
|
||||
override val message: String
|
||||
get() =
|
||||
exception.message
|
||||
?: WireGuardAutoTunnel.instance.getString(R.string.unknown_error)
|
||||
}
|
||||
|
||||
data object InvalidQrCode : Error() {
|
||||
override val message: String
|
||||
get() = WireGuardAutoTunnel.instance.getString(R.string.error_invalid_code)
|
||||
}
|
||||
|
||||
data object InvalidFileExtension : Error() {
|
||||
override val message: String
|
||||
get() = WireGuardAutoTunnel.instance.getString(R.string.error_file_extension)
|
||||
}
|
||||
|
||||
data object FileReadFailed : Error() {
|
||||
override val message: String
|
||||
get() = WireGuardAutoTunnel.instance.getString(R.string.error_file_extension)
|
||||
}
|
||||
|
||||
data object AuthenticationFailed : Error() {
|
||||
override val message: String
|
||||
get() = WireGuardAutoTunnel.instance.getString(R.string.error_authentication_failed)
|
||||
}
|
||||
|
||||
data object AuthorizationFailed : Error() {
|
||||
override val message: String
|
||||
get() = WireGuardAutoTunnel.instance.getString(R.string.error_authorization_failed)
|
||||
}
|
||||
|
||||
data object BackgroundLocationRequired : Error() {
|
||||
override val message: String
|
||||
get() =
|
||||
WireGuardAutoTunnel.instance.getString(R.string.background_location_required)
|
||||
}
|
||||
|
||||
data object LocationServicesRequired : Error() {
|
||||
override val message: String
|
||||
get() = WireGuardAutoTunnel.instance.getString(R.string.location_services_required)
|
||||
}
|
||||
|
||||
data object PreciseLocationRequired : Error() {
|
||||
override val message: String
|
||||
get() = WireGuardAutoTunnel.instance.getString(R.string.precise_location_required)
|
||||
}
|
||||
|
||||
data object FileExplorerRequired : Error() {
|
||||
override val message: String
|
||||
get() = WireGuardAutoTunnel.instance.getString(R.string.error_no_file_explorer)
|
||||
}
|
||||
}
|
||||
|
||||
sealed class Message : Event() {
|
||||
data object ConfigSaved : Message() {
|
||||
override val message: String
|
||||
get() = WireGuardAutoTunnel.instance.getString(R.string.config_changes_saved)
|
||||
}
|
||||
|
||||
data object ConfigsExported : Message() {
|
||||
override val message: String
|
||||
get() = WireGuardAutoTunnel.instance.getString(R.string.exported_configs_message)
|
||||
}
|
||||
|
||||
data object TunnelOffAction : Message() {
|
||||
override val message: String
|
||||
get() = WireGuardAutoTunnel.instance.getString(R.string.turn_off_tunnel)
|
||||
}
|
||||
|
||||
data object TunnelOnAction : Message() {
|
||||
override val message: String
|
||||
get() = WireGuardAutoTunnel.instance.getString(R.string.turn_on_tunnel)
|
||||
}
|
||||
|
||||
data object AutoTunnelOffAction : Message() {
|
||||
override val message: String
|
||||
get() = WireGuardAutoTunnel.instance.getString(R.string.turn_off_auto)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,20 +1,35 @@
|
||||
package com.zaneschepke.wireguardautotunnel.util
|
||||
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.pm.PackageInfo
|
||||
import com.wireguard.android.backend.Statistics
|
||||
import com.wireguard.android.backend.Statistics.PeerStats
|
||||
import com.wireguard.crypto.Key
|
||||
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
|
||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.HandshakeStatus
|
||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.statistics.TunnelStatistics
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.ObsoleteCoroutinesApi
|
||||
import kotlinx.coroutines.channels.ClosedReceiveChannelException
|
||||
import kotlinx.coroutines.channels.ReceiveChannel
|
||||
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.launch
|
||||
import kotlinx.coroutines.selects.whileSelect
|
||||
import org.amnezia.awg.config.Config
|
||||
import timber.log.Timber
|
||||
import java.math.BigDecimal
|
||||
import java.text.DecimalFormat
|
||||
import java.time.Duration
|
||||
import java.util.concurrent.ConcurrentLinkedQueue
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
import kotlin.coroutines.EmptyCoroutineContext
|
||||
import kotlin.coroutines.cancellation.CancellationException
|
||||
|
||||
fun BroadcastReceiver.goAsync(
|
||||
context: CoroutineContext = EmptyCoroutineContext,
|
||||
@@ -31,12 +46,6 @@ fun BroadcastReceiver.goAsync(
|
||||
}
|
||||
}
|
||||
|
||||
fun String.truncateWithEllipsis(allowedLength: Int): String {
|
||||
return if (this.length > allowedLength + 3) {
|
||||
this.substring(0, allowedLength) + "***"
|
||||
} else this
|
||||
}
|
||||
|
||||
fun BigDecimal.toThreeDecimalPlaceString(): String {
|
||||
val df = DecimalFormat("#.###")
|
||||
return df.format(this)
|
||||
@@ -50,15 +59,15 @@ typealias TunnelConfigs = List<TunnelConfig>
|
||||
|
||||
typealias Packages = List<PackageInfo>
|
||||
|
||||
fun Statistics.mapPeerStats(): Map<Key, PeerStats?> {
|
||||
return this.peers().associateWith { key -> (this.peer(key)) }
|
||||
fun TunnelStatistics.mapPeerStats(): Map<org.amnezia.awg.crypto.Key, TunnelStatistics.PeerStats?> {
|
||||
return this.getPeers().associateWith { key -> (this.peerStats(key)) }
|
||||
}
|
||||
|
||||
fun PeerStats.latestHandshakeSeconds(): Long? {
|
||||
fun TunnelStatistics.PeerStats.latestHandshakeSeconds(): Long? {
|
||||
return NumberUtils.getSecondsBetweenTimestampAndNow(this.latestHandshakeEpochMillis)
|
||||
}
|
||||
|
||||
fun PeerStats.handshakeStatus(): HandshakeStatus {
|
||||
fun TunnelStatistics.PeerStats.handshakeStatus(): HandshakeStatus {
|
||||
// TODO add never connected status after duration
|
||||
return this.latestHandshakeSeconds().let {
|
||||
when {
|
||||
@@ -71,3 +80,89 @@ fun 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,19 +6,102 @@ import android.os.Build
|
||||
import android.os.Environment
|
||||
import android.provider.MediaStore
|
||||
import android.provider.MediaStore.MediaColumns
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.withContext
|
||||
import timber.log.Timber
|
||||
import java.io.File
|
||||
import java.io.FileInputStream
|
||||
import java.io.FileOutputStream
|
||||
import java.io.OutputStream
|
||||
import java.time.Instant
|
||||
import java.util.zip.ZipEntry
|
||||
import java.util.zip.ZipOutputStream
|
||||
|
||||
object FileUtils {
|
||||
private const val ZIP_FILE_MIME_TYPE = "application/zip"
|
||||
class FileUtils(
|
||||
private val context: Context,
|
||||
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
|
||||
private fun createDownloadsFileOutputStream(
|
||||
context: Context,
|
||||
fileName: String,
|
||||
mimeType: String = Constants.ALLOWED_FILE_TYPES
|
||||
): OutputStream? {
|
||||
@@ -44,47 +127,4 @@ object FileUtils {
|
||||
}
|
||||
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) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
package com.zaneschepke.wireguardautotunnel.util
|
||||
|
||||
import timber.log.Timber
|
||||
|
||||
sealed class Result<T> {
|
||||
class Success<T>(val data: T) : Result<T>()
|
||||
|
||||
class Error<T>(val error: Event.Error) : Result<T>() {
|
||||
init {
|
||||
when (this.error) {
|
||||
is Event.Error.Exception -> Timber.e(this.error.exception)
|
||||
else -> Timber.e(this.error.message)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 38 KiB |
@@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="960"
|
||||
android:viewportHeight="960">
|
||||
<path
|
||||
android:fillColor="#e8eaed"
|
||||
android:pathData="M440,520L200,520v-80h240v-240h80v240h240v80L520,520v240h-80v-240Z" />
|
||||
</vector>
|
||||
@@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="960"
|
||||
android:viewportHeight="960">
|
||||
<path
|
||||
android: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>
|
||||
@@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="960"
|
||||
android:viewportHeight="960">
|
||||
<path
|
||||
android: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>
|
||||
@@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="50dp"
|
||||
android:height="50dp"
|
||||
android:viewportWidth="50"
|
||||
android:viewportHeight="50">
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M25,2c12.703,0 23,10.297 23,23S37.703,48 25,48S2,37.703 2,25S12.297,2 25,2zM32.934,34.375c0.423,-1.298 2.405,-14.234 2.65,-16.783c0.074,-0.772 -0.17,-1.285 -0.648,-1.514c-0.578,-0.278 -1.434,-0.139 -2.427,0.219c-1.362,0.491 -18.774,7.884 -19.78,8.312c-0.954,0.405 -1.856,0.847 -1.856,1.487c0,0.45 0.267,0.703 1.003,0.966c0.766,0.273 2.695,0.858 3.834,1.172c1.097,0.303 2.346,0.04 3.046,-0.395c0.742,-0.461 9.305,-6.191 9.92,-6.693c0.614,-0.502 1.104,0.141 0.602,0.644c-0.502,0.502 -6.38,6.207 -7.155,6.997c-0.941,0.959 -0.273,1.953 0.358,2.351c0.721,0.454 5.906,3.932 6.687,4.49c0.781,0.558 1.573,0.811 2.298,0.811C32.191,36.439 32.573,35.484 32.934,34.375z" />
|
||||
</vector>
|
||||
@@ -1,5 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@color/ic_channel_background" />
|
||||
<foreground android:drawable="@mipmap/ic_channel_foreground" />
|
||||
</adaptive-icon>
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 8.4 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 26 KiB |
@@ -0,0 +1 @@
|
||||
unqualifiedResLocale=en-US
|
||||
@@ -0,0 +1,147 @@
|
||||
<?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>
|
||||
</resources>
|
||||
@@ -0,0 +1,171 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="app_name">WG Tunnel</string>
|
||||
<string name="error_file_extension">Datei ist keine .conf oder .zip</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="tunnels">Tunnel</string>
|
||||
<string name="enable_auto_tunnel">Auto-Tunneln starten</string>
|
||||
<string name="tunnel_mobile_data">Tunnel für mobile Daten</string>
|
||||
<string name="one_tunnel_required">Mindestens ein Tunnel wird für diese Funktion benötigt</string>
|
||||
<string name="privacy_policy">Datenschutzbestimmungen anzeigen</string>
|
||||
<string name="disable_auto_tunnel">Auto-Tunneln stoppen</string>
|
||||
<string name="okay">Ok</string>
|
||||
<string name="tunnel_on_ethernet">Tunnel für Ethernet</string>
|
||||
<string name="attempt_connection">Verbindungsversuch..</string>
|
||||
<string name="auto_tunneling">Auto-Tunneln</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="watcher_notification_text_paused">Überwachung der Netzwerkänderungen: pausiert</string>
|
||||
<string name="tunnel_start_title">VPN verbunden</string>
|
||||
<string name="tunnel_start_text">Mit Tunnel verbunden</string>
|
||||
<string name="notification_permission_required">Benachrichtigungsberechtigung benötigt.</string>
|
||||
<string name="add_trusted_ssid">Vertrauenswürdigen WLAN-Namen hinzufü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_title">Vereinbarung der Standortberechtigung im Hintergrund</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_value_description">SSID bestätigen</string>
|
||||
<string name="add_tunnels_text">Von Datei oder ZIP hinzufügen</string>
|
||||
<string name="open_file">Datei öffnen</string>
|
||||
<string name="add_from_qr">Über QR-Code hinzufügen</string>
|
||||
<string name="qr_scan">Scanne QR</string>
|
||||
<string name="tunnel_name">Tunnel Name</string>
|
||||
<string name="add_tunnel">Tunnel hinzufügen</string>
|
||||
<string name="exclude">Ausschließen</string>
|
||||
<string name="include">Einschließen</string>
|
||||
<string name="tunnel_all">Alle Apps tunneln</string>
|
||||
<string name="config_changes_saved">Konfigurationsänderungen gespeichert.</string>
|
||||
<string name="save_changes">Speichern</string>
|
||||
<string name="icon">Symbol</string>
|
||||
<string name="no_thanks">Nein danke</string>
|
||||
<string name="turn_on">Einschalten</string>
|
||||
<string name="map">Karte</string>
|
||||
<string name="public_key">Öffentlicher Schlüssel</string>
|
||||
<string name="addresses">Adressen</string>
|
||||
<string name="dns_servers">DNS-Server</string>
|
||||
<string name="mtu">MTU</string>
|
||||
<string name="peer">Peer</string>
|
||||
<string name="allowed_ips">Erlaubte IPs</string>
|
||||
<string name="endpoint">Endpunkt</string>
|
||||
<string name="name">Name</string>
|
||||
<string name="restart">Tunnel neustarten</string>
|
||||
<string name="vpn_connection_failed">Verbindung fehlgeschlagen</string>
|
||||
<string name="always_on_vpn_support">Always-On VPN erlauben</string>
|
||||
<string name="location_services_not_detected">Standortdienste nicht erkannt</string>
|
||||
<string name="hint_search_packages">Pakete suchen</string>
|
||||
<string name="vpn_starting">VPN startet</string>
|
||||
<string name="db_name">wg-tunnel-db</string>
|
||||
<string name="scanning_qr">Scanne nach QR</string>
|
||||
<string name="none">Keine vertrauenswürdigen WLAN Namen</string>
|
||||
<string name="other">Sonstige</string>
|
||||
<string name="vpn_on">VPN an</string>
|
||||
<string name="vpn_off">VPN aus</string>
|
||||
<string name="default_vpn_on">Primärer VPN an</string>
|
||||
<string name="create_import">Von Grund auf neu erstellen</string>
|
||||
<string name="turn_off_auto">Für diese Aktion muss Auto-Tunneln ausgeschaltet oder pausiert sein</string>
|
||||
<string name="add_peer">Peer hinzufügen</string>
|
||||
<string name="done">Erledigt</string>
|
||||
<string name="rotate_keys">Schlüssel rotieren</string>
|
||||
<string name="private_key">Privater Schlüssel</string>
|
||||
<string name="copy_public_key">Öffentlichen Schlüssel kopieren</string>
|
||||
<string name="base64_key">base64-Schlüssel</string>
|
||||
<string name="comma_separated_list">Kommaseparierte Liste</string>
|
||||
<string name="delete_tunnel">Tunnel löschen</string>
|
||||
<string name="persistent_keepalive">Dauerhaftes Keepalive</string>
|
||||
<string name="background_location_required">Hintergrund Standortdienste erforderlich</string>
|
||||
<string name="enable_app_lock">App-Sperre 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>
|
||||
@@ -0,0 +1,155 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="map">Mapa</string>
|
||||
<string name="allowed_ips">Allowed IPs</string>
|
||||
<string name="email_chooser">Enviar un email…</string>
|
||||
<string name="go">ir</string>
|
||||
<string name="discord_description">Únete a la comunidad</string>
|
||||
<string name="kernel">Kernel</string>
|
||||
<string name="restart">Reiniciar túnel</string>
|
||||
<string name="vpn_connection_failed">Error de conexión</string>
|
||||
<string name="error_none">Sin error</string>
|
||||
<string name="other">Otros</string>
|
||||
<string name="add_peer">Añadir peer</string>
|
||||
<string name="done">Hecho</string>
|
||||
<string name="copy_public_key">Copiar public key</string>
|
||||
<string name="base64_key">clave base64</string>
|
||||
<string name="exported_configs_message">Configuración exportada a Descargas</string>
|
||||
<string name="tunnel_on_wifi">Túnel en Wi-Fi no de confianza</string>
|
||||
<string name="location_services_missing_message">La app no detecta activado el servicio de ubicación en tu dispositivo. Dependiendo del dispositivo, esto podría hacer que la característica de Wi-Fi no de confianza falle al leer el nombre Wi-Fi. ¿Quieres continuar de todas formas?</string>
|
||||
<string name="excluded">excluida(s)</string>
|
||||
<string name="all">todas</string>
|
||||
<string name="mobile_data_tunnel">Establecer como túnel en datos móviles</string>
|
||||
<string name="use_tunnel_on_wifi_name">Usar tunnel en nombre Wi-Fi</string>
|
||||
<string name="disabled">desactivado</string>
|
||||
<string name="version">Versión</string>
|
||||
<string name="userspace">Espacio del usuario</string>
|
||||
<string name="mode">Modo</string>
|
||||
<string name="support">Ayuda</string>
|
||||
<string name="private_key">Clave privada</string>
|
||||
<string name="trusted_ssid_value_description">Enviar SSID</string>
|
||||
<string name="trusted_ssid_empty_description">Introducir SSID</string>
|
||||
<string name="add_tunnels_text">Añadir desde archivo o zip</string>
|
||||
<string name="open_file">Abrir archivo</string>
|
||||
<string name="add_from_qr">Añadir mediante código QR</string>
|
||||
<string name="qr_scan">Escanear QR</string>
|
||||
<string name="tunnel_name">Nombre de túnel</string>
|
||||
<string name="add_tunnel">Añadir túnel</string>
|
||||
<string name="exclude">Excluir</string>
|
||||
<string name="include">Incluir</string>
|
||||
<string name="tunnel_all">Todas las apps por el túnel</string>
|
||||
<string name="config_changes_saved">Cambios de configuración guardados.</string>
|
||||
<string name="save_changes">Guardar</string>
|
||||
<string name="icon">Icono</string>
|
||||
<string name="no_thanks">No gracias</string>
|
||||
<string name="turn_on">Activar</string>
|
||||
<string name="mtu">MTU</string>
|
||||
<string name="dns_servers">DNS servers</string>
|
||||
<string name="addresses">Addresses</string>
|
||||
<string name="public_key">Public key</string>
|
||||
<string name="error_file_extension">No es un archivo .conf o .zip</string>
|
||||
<string name="turn_off_tunnel">Desactiva antes el túnel</string>
|
||||
<string name="no_tunnels">¡Ningún túnel añadido aún!</string>
|
||||
<string name="watcher_notification_text_paused">Monitoreando cambios de red: En pausa</string>
|
||||
<string name="watcher_notification_text_active">Monitoreando cambios de red: Activado</string>
|
||||
<string name="tunnel_start_title">VPN conectada</string>
|
||||
<string name="tunnel_start_text">Conectado al túnel</string>
|
||||
<string name="notification_permission_required">Necesita permiso de notificaciones.</string>
|
||||
<string name="add_trusted_ssid">Añadir nombres Wi-Fi de confianza</string>
|
||||
<string name="tunnels">Túneles</string>
|
||||
<string name="enable_auto_tunnel">Iniciar túnel-automático</string>
|
||||
<string name="disable_auto_tunnel">Parar túnel-automático</string>
|
||||
<string name="tunnel_mobile_data">Activar túnel en datos móviles</string>
|
||||
<string name="one_tunnel_required">Esta característica necesita ser usada en almenos por un túnel</string>
|
||||
<string name="privacy_policy">Ver Política de Privacidad</string>
|
||||
<string name="okay">OK</string>
|
||||
<string name="tunnel_on_ethernet">Túnel en ethernet</string>
|
||||
<string name="prominent_background_location_title">Divulgación de la ubicación en segundo plano</string>
|
||||
<string name="thank_you">¡Gracias por usar WG Tunnel!</string>
|
||||
<string name="endpoint">Endpoint</string>
|
||||
<string name="peer">Peer</string>
|
||||
<string name="name">Nombre</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="hint_search_packages">Buscar paquetes</string>
|
||||
<string name="attempt_connection">Intentando conexión...</string>
|
||||
<string name="vpn_starting">Iniciando VPN</string>
|
||||
<string name="db_name">wg-tunnel-db</string>
|
||||
<string name="scanning_qr">Escaneando QR</string>
|
||||
<string name="none">Sin nombres Wi-Fi de confianza</string>
|
||||
<string name="auto_tunneling">Túnel-automático</string>
|
||||
<string name="vpn_on">VPN on</string>
|
||||
<string name="vpn_off">VPN off</string>
|
||||
<string name="default_vpn_on">VPN Principal on</string>
|
||||
<string name="default_vpn_off">VPN Principal off</string>
|
||||
<string name="create_import">Crear desde cero</string>
|
||||
<string name="turn_off_auto">La acción necesita que túnel-automático esté desactivado o en pausa</string>
|
||||
<string name="turn_on_tunnel">La acción necesita un túnel activado</string>
|
||||
<string name="rotate_keys">Rotar claves</string>
|
||||
<string name="comma_separated_list">lista separada por comas</string>
|
||||
<string name="random">(aleatorio)</string>
|
||||
<string name="optional">(opcional)</string>
|
||||
<string name="optional_no_recommend">(opcional, no recomendado)</string>
|
||||
<string name="seconds">segundos</string>
|
||||
<string name="cancel">Cancelar</string>
|
||||
<string name="error_authentication_failed">Fallo de autenticación</string>
|
||||
<string name="error_authorization_failed">Fallo de autorización</string>
|
||||
<string name="enabled_app_shortcuts">Habilitar acesos directos de app</string>
|
||||
<string name="export_configs">Exportar configuración</string>
|
||||
<string name="background_location_required">Se necesita ubicación en segundo plano</string>
|
||||
<string name="location_services_required">Se necesita servicio de ubicación</string>
|
||||
<string name="precise_location_required">Necesita ubicación precisa</string>
|
||||
<string name="unknown_error">Error desconocido</string>
|
||||
<string name="email_subject">Ayuda WG Tunnel</string>
|
||||
<string name="interface_">Interfaz</string>
|
||||
<string name="listen_port">Puerto de escucha</string>
|
||||
<string name="preshared_key">Clave previamente compartida</string>
|
||||
<string name="persistent_keepalive">Keepalive persistente</string>
|
||||
<string name="docs_description">Leer documentación</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="use_kernel">Usar módulo del Kernel</string>
|
||||
<string name="error_ssid_exists">SSID existente</string>
|
||||
<string name="error_root_denied">Shell root denegado</string>
|
||||
<string name="error_no_file_explorer">Explorador de archivos no instalado</string>
|
||||
<string name="error_invalid_code">Código QR no valido</string>
|
||||
<string name="auto_tunnel_title">Servicio túnel-automático</string>
|
||||
<string name="delete_tunnel">Eliminar túnel</string>
|
||||
<string name="delete_tunnel_message">¿Estás seguro de que quieres eliminar este túnel?</string>
|
||||
<string name="yes">Sí</string>
|
||||
<string name="resume">Reanudar</string>
|
||||
<string name="pause">Pausar</string>
|
||||
<string name="active">activado</string>
|
||||
<string name="paused">en pausa</string>
|
||||
<string name="tunneling_apps">Apps por el túnel</string>
|
||||
<string name="included">incluida(s)</string>
|
||||
<string name="always_on_disabled">VPN siempre-activada ha intentado iniciar un túnel, pero está característica está deshabilitada en los ajustes.</string>
|
||||
<string name="no_email_detected">Ninguna app de email detectada</string>
|
||||
<string name="no_browser_detected">Ningún navegador detectado</string>
|
||||
<string name="logs_saved">Registros guardados en Descargas</string>
|
||||
<string name="open_issue">Abrir una incidencia</string>
|
||||
<string name="read_logs">Leer los registros</string>
|
||||
<string name="auto">(automático)</string>
|
||||
<string name="config_parse_error">Fallo al analizar la configuración</string>
|
||||
<string name="incorrect_pin">El pin no es correcto</string>
|
||||
<string name="pin_created">Pin creado con éxito</string>
|
||||
<string name="enter_pin">Introduce tu pin</string>
|
||||
<string name="create_pin">Crear pin</string>
|
||||
<string name="enable_app_lock">Bloqueo de app activado</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="no_wifi_names_configured">No hay nombres Wi-Fi configurados para este túnel</string>
|
||||
<string name="general">General</string>
|
||||
<string name="edit_tunnel">Editar túnel</string>
|
||||
<string name="auto_on">Reanudar túnel-automático</string>
|
||||
<string name="auto_off">Pausar túnel-automático</string>
|
||||
<string name="auto_tun_on">Reanudar el túnel automático</string>
|
||||
<string name="auto_tun_off">Pausa del túnel automático</string>
|
||||
<string name="settings">Ajustes</string>
|
||||
<string name="app_name">WG Tunnel</string>
|
||||
<string name="vpn_channel_id">Canal VPN</string>
|
||||
<string name="vpn_channel_name">Canal de notificación VPN</string>
|
||||
<string name="watcher_channel_id">Canal del obvervador</string>
|
||||
<string name="watcher_channel_name">Canal de notificación del obvervador</string>
|
||||
<string name="prominent_background_location_message">La monitorización SSID Wi-Fi necesita de permiso de ubicación en segundo plano incluso si la app está cerrada. Mira el enlace a la Política de Privacidad en la pantalla de ayuda para más detalles.</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,2 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources></resources>
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="tunnels">Tunele</string>
|
||||
<string name="app_name">WG Tunnel</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,171 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="turn_off_tunnel">Действие требует отключения туннеля</string>
|
||||
<string name="add_tunnel">Добавить туннель</string>
|
||||
<string name="mtu">MTU</string>
|
||||
<string name="tunnel_name">Имя туннеля</string>
|
||||
<string name="public_key">Публичный ключ</string>
|
||||
<string name="name">Имя</string>
|
||||
<string name="peer">Пир</string>
|
||||
<string name="privacy_policy">Посмотреть политику конфиденциальности</string>
|
||||
<string name="icon">Иконка</string>
|
||||
<string name="turn_on">Включить</string>
|
||||
<string name="add_from_qr">Добавить из QR</string>
|
||||
<string name="qr_scan">Сканировать QR</string>
|
||||
<string name="auto_tunneling">Авто-туннелирование</string>
|
||||
<string name="no_tunnels">Туннели ещё не добавлены!</string>
|
||||
<string name="open_file">Открыть файл</string>
|
||||
<string name="exclude">Исключить</string>
|
||||
<string name="include">Включить</string>
|
||||
<string name="tunnel_all">Туннель для всех приложений</string>
|
||||
<string name="config_changes_saved">Изменения конфигурации сохранены.</string>
|
||||
<string name="save_changes">Сохранить</string>
|
||||
<string name="no_thanks">Нет, спасибо</string>
|
||||
<string name="map">Карта</string>
|
||||
<string name="addresses">Адреса</string>
|
||||
<string name="dns_servers">DNS-серверы</string>
|
||||
<string name="allowed_ips">Разрешённые IP</string>
|
||||
<string name="endpoint">Конечная точка</string>
|
||||
<string name="restart">Перезапустить туннель</string>
|
||||
<string name="vpn_connection_failed">Ошибка соединения</string>
|
||||
<string name="always_on_vpn_support">Разрешить постоянный VPN</string>
|
||||
<string name="hint_search_packages">Поиск приложений</string>
|
||||
<string name="other">Другое</string>
|
||||
<string name="vpn_on">VPN вкл.</string>
|
||||
<string name="vpn_off">VPN откл.</string>
|
||||
<string name="interface_">Интерфейс</string>
|
||||
<string name="optional">(необязательно)</string>
|
||||
<string name="optional_no_recommend">(необязательно, не рекомендуется)</string>
|
||||
<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>
|
||||
@@ -0,0 +1,160 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="app_name">WG Tunnel</string>
|
||||
<string name="vpn_channel_id">VPN Kanalı</string>
|
||||
<string name="vpn_channel_name">VPN Bildirim Kanalı</string>
|
||||
<string name="watcher_channel_id">İzleyici Kanalı</string>
|
||||
<string name="watcher_channel_name">İzleyici Bildirim Kanalı</string>
|
||||
<string name="github_url" translatable="false">https://github.com/zaneschepke/wgtunnel/issues</string>
|
||||
<string name="docs_url" translatable="false">https://zaneschepke.com/wgtunnel-docs/overview.html</string>
|
||||
<string name="privacy_policy_url" translatable="false">https://zaneschepke.com/wgtunnel-docs/privacypolicy.html</string>
|
||||
<string name="error_file_extension">Dosya bir .conf veya .zip değil</string>
|
||||
<string name="turn_off_tunnel">Eylem tünelin kapalı olmasını gerektiriyor</string>
|
||||
<string name="no_tunnels">Henüz bir tünel ekli değil!</string>
|
||||
<string name="discord_url" translatable="false">https://discord.gg/rbRRNh6H7V</string>
|
||||
<string name="watcher_notification_text_active">Ağ durumu değişikliklerini izleme: etkin</string>
|
||||
<string name="watcher_notification_text_paused">Ağ durumu değişikliklerini izleme: duraklatıldı</string>
|
||||
<string name="tunnel_start_title">VPN bağlandı</string>
|
||||
<string name="tunnel_start_text">Tünele bağlı</string>
|
||||
<string name="notification_permission_required">Bildirim izni gerekli.</string>
|
||||
<string name="add_trusted_ssid">Güvenilir Wi-Fi adı ekle</string>
|
||||
<string name="tunnels">Tüneller</string>
|
||||
<string name="enable_auto_tunnel">Otomatik tünellemeyi başlat</string>
|
||||
<string name="disable_auto_tunnel">Otomatik tünellemeyi durdur</string>
|
||||
<string name="tunnel_mobile_data">Mobil veri üzerinde tünel</string>
|
||||
<string name="one_tunnel_required">Bu özelliği kullanabilmek için en az bir tünel gereklidir.</string>
|
||||
<string name="privacy_policy">Gizlilik Politikası</string>
|
||||
<string name="okay">Tamam</string>
|
||||
<string name="tunnel_on_ethernet">Ethernet üzerinde tünel</string>
|
||||
<string name="prominent_background_location_message">Bu özellik, uygulama kapalıyken bile Wi-Fi SSID izlemesini etkinleştirmek için arka plan konumu izni gerektirir. Daha fazla bilgi için lütfen Destek ekranında bağlantısı verilen Gizlilik Politikasına bakın.</string>
|
||||
<string name="prominent_background_location_title">Arka Plan Konum Açıklaması</string>
|
||||
<string name="thank_you">WG Tunnel\'i kullandığınız için teşekkür ederiz!</string>
|
||||
<string name="trusted_ssid_empty_description">SSID\'yi gir</string>
|
||||
<string name="trusted_ssid_value_description">SSID\'yi gönder</string>
|
||||
<string name="add_tunnels_text">Dosyadan veya ZIP\'ten ekle</string>
|
||||
<string name="open_file">Dosya Aç</string>
|
||||
<string name="add_from_qr">QR kodundan ekle</string>
|
||||
<string name="qr_scan">QR kodu tarat</string>
|
||||
<string name="tunnel_name">Tünel adı</string>
|
||||
<string name="add_tunnel">Tünel ekle</string>
|
||||
<string name="exclude">Hariç tut</string>
|
||||
<string name="include">Dahil et</string>
|
||||
<string name="tunnel_all">Tüm uygulamaları dahil et</string>
|
||||
<string name="config_changes_saved">Yapılandırma değişiklikleri kaydedildi.</string>
|
||||
<string name="save_changes">Kaydet</string>
|
||||
<string name="icon">Simge</string>
|
||||
<string name="no_thanks">Hayır, teşekkürler</string>
|
||||
<string name="turn_on">Aç</string>
|
||||
<string name="map">Harita</string>
|
||||
<string name="public_key">Genel anahtar</string>
|
||||
<string name="addresses">Adresler</string>
|
||||
<string name="dns_servers">DNS sunucuları</string>
|
||||
<string name="mtu">MTU</string>
|
||||
<string name="peer">Eş (peer)</string>
|
||||
<string name="allowed_ips">İzin verilen IP\'ler</string>
|
||||
<string name="endpoint">Uç nokta (endpoint)</string>
|
||||
<string name="name">Ad</string>
|
||||
<string name="restart">Tüneli Yeniden Başlat</string>
|
||||
<string name="vpn_connection_failed">Bağlantı kurulamadı</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="hint_search_packages">Uygulama arayın</string>
|
||||
<string name="attempt_connection">Bağlantı kurulmaya çalışılıyor..</string>
|
||||
<string name="vpn_starting">VPN başlatılıyor</string>
|
||||
<string name="db_name">wg-tunnel-db</string>
|
||||
<string name="scanning_qr">QR taranıyor</string>
|
||||
<string name="none">Güvenilir Wi-Fi adı yok</string>
|
||||
<string name="other">Diğer</string>
|
||||
<string name="auto_tunneling">Otomatik tünelleme</string>
|
||||
<string name="vpn_on">VPN açık</string>
|
||||
<string name="vpn_off">VPN kapalı</string>
|
||||
<string name="default_vpn_on">Birincil VPN açık</string>
|
||||
<string name="default_vpn_off">Birincil VPN kapalı</string>
|
||||
<string name="create_import">Sıfırdan oluştur</string>
|
||||
<string name="turn_off_auto">Eylem, otomatik tünelin devre dışı bırakılmasını veya duraklatılmasını gerektirir</string>
|
||||
<string name="turn_on_tunnel">Eylem aktif tünel gerektirir</string>
|
||||
<string name="add_peer">Eş (peer) ekle</string>
|
||||
<string name="done">Tamam</string>
|
||||
<string name="interface_">Arayüz</string>
|
||||
<string name="rotate_keys">Tuşları döndür</string>
|
||||
<string name="private_key">Özel anahtar</string>
|
||||
<string name="copy_public_key">Genel anahtarı kopyala</string>
|
||||
<string name="base64_key">Base64 anahtarı</string>
|
||||
<string name="comma_separated_list">Virgül ile ayrılmış liste ekleyin</string>
|
||||
<string name="listen_port">Bağlantı noktası</string>
|
||||
<string name="random">(Rastgele)</string>
|
||||
<string name="optional">(İsteğe bağlı)</string>
|
||||
<string name="optional_no_recommend">(İsteğe bağlı, önerilmez)</string>
|
||||
<string name="preshared_key">Ön paylaşımlı anahtar</string>
|
||||
<string name="seconds">saniye</string>
|
||||
<string name="persistent_keepalive">Kalıcı olarak canlı tutma</string>
|
||||
<string name="cancel">İptal</string>
|
||||
<string name="error_authentication_failed">Kimlik doğrulama başarısız oldu</string>
|
||||
<string name="error_authorization_failed">Yetkilendirilemedi</string>
|
||||
<string name="enabled_app_shortcuts">Uygulama kısayollarını etkinleştir</string>
|
||||
<string name="export_configs">Yapılandırmaları dışa aktar</string>
|
||||
<string name="location_services_required">Gerekli konum hizmetleri</string>
|
||||
<string name="background_location_required">Arka plan konumu gerekli</string>
|
||||
<string name="precise_location_required">Kesin konum gerekli</string>
|
||||
<string name="unknown_error">Bilinmeyen bir hata oluştu</string>
|
||||
<string name="exported_configs_message">Yapılandırmalar indirilenler\'e aktarıldı</string>
|
||||
<string name="tunnel_on_wifi">Güvenilmeyen kablosuz internet bağlantısında tünel</string>
|
||||
<string name="my_email" translatable="false">support@zaneschepke.com</string>
|
||||
<string name="email_subject">WG Tunnel Desteği</string>
|
||||
<string name="email_chooser">Bir e-posta gönderin…</string>
|
||||
<string name="go">git</string>
|
||||
<string name="docs_description">Belgeleri oku</string>
|
||||
<string name="discord_description">Topluluğa katıl</string>
|
||||
<string name="email_description">Bana e-posta gönder</string>
|
||||
<string name="support_help_text">Sorun yaşıyorsanız, iyileştirme fikirleriniz varsa veya sadece etkileşimde bulunmak istiyorsanız, aşağıdaki kaynaklar mevcuttur:</string>
|
||||
<string name="kernel">Çekirdek</string>
|
||||
<string name="use_kernel">Çekirdek modülünü kullan</string>
|
||||
<string name="error_ssid_exists">SSID zaten var</string>
|
||||
<string name="error_root_denied">Kök kabuğu reddedildi</string>
|
||||
<string name="error_no_file_explorer">Yüklü dosya gezgini yok</string>
|
||||
<string name="error_invalid_code">Geçersiz QR kodu</string>
|
||||
<string name="error_none">Hata yok</string>
|
||||
<string name="location_services_missing_message">Uygulama, cihazınızda etkinleştirilen herhangi bir konum hizmeti algılamıyor. Cihaza bağlı olarak bu, güvenilmeyen Wi-Fi özelliğinin Wi-Fi adını okuyamamasına neden olabilir. Yine de devam etmek ister misiniz?</string>
|
||||
<string name="auto_tunnel_title">Otomatik Tünel Hizmeti</string>
|
||||
<string name="delete_tunnel">Tüneli sil</string>
|
||||
<string name="delete_tunnel_message">Bu tüneli silmek istediğinizden emin misiniz?</string>
|
||||
<string name="yes">Evet</string>
|
||||
<string name="resume">Devam et</string>
|
||||
<string name="pause">Duraklat</string>
|
||||
<string name="paused">duraklatıldı</string>
|
||||
<string name="active">aktif</string>
|
||||
<string name="tunneling_apps">Tünellenen uygulamalar</string>
|
||||
<string name="included">dahil edildi</string>
|
||||
<string name="excluded">hariç tutuldu</string>
|
||||
<string name="all">tümü</string>
|
||||
<string name="always_on_disabled">Her zaman açık VPN bir tünel başlatmaya çalıştı, ancak bu özellik ayarlarda devre dışı bırakıldı.</string>
|
||||
<string name="no_email_detected">E-posta uygulaması algılanmadı</string>
|
||||
<string name="no_browser_detected">Tarayıcı algılanmadı</string>
|
||||
<string name="logs_saved">İndirilenlere kaydedilen günlükler</string>
|
||||
<string name="open_issue">Bir sorun bildir</string>
|
||||
<string name="read_logs">Günlükleri oku</string>
|
||||
<string name="auto">(Otomatik)</string>
|
||||
<string name="config_parse_error">Yapılandırma kaydedilemedi</string>
|
||||
<string name="incorrect_pin">PIN kodu yanlış</string>
|
||||
<string name="pin_created">Pin başarıyla oluşturuldu</string>
|
||||
<string name="enter_pin">PIN kodunuzu girin</string>
|
||||
<string name="create_pin">PIN kodu oluştur</string>
|
||||
<string name="enable_app_lock">Uygulama kilidini etkinleştir</string>
|
||||
<string name="restart_on_ping">Ping başarısız olduğunda yeniden başlat (beta)</string>
|
||||
<string name="mobile_data_tunnel">Mobil veri tüneli olarak ayarla</string>
|
||||
<string name="set_primary_tunnel">Birincil tünel olarak ayarla</string>
|
||||
<string name="use_tunnel_on_wifi_name">Wi-Fi adında tünel kullan</string>
|
||||
<string name="no_wifi_names_configured">Bu tünel için yapılandırılmış Wi-Fi adı yok</string>
|
||||
<string name="general">Genel</string>
|
||||
<string name="edit_tunnel">Tüneli düzenle</string>
|
||||
<string name="disabled">Devre dışı</string>
|
||||
<string name="auto_on">Otomatik ayarlamayı sürdür</string>
|
||||
<string name="auto_off">Otomatik ayarlamayı duraklat</string>
|
||||
<string name="auto_tun_on">Otomatik tüneli sürdür</string>
|
||||
<string name="auto_tun_off">Otomatik tüneli duraklat</string>
|
||||
<string name="version">Sürüm</string>
|
||||
<string name="mode">Mod</string>
|
||||
<string name="userspace">Kullanıcı alanı</string>
|
||||
<string name="settings">Ayarlar</string>
|
||||
<string name="support">Destek</string>
|
||||
</resources>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user