mirror of
https://github.com/wgtunnel/android.git
synced 2026-07-03 14:07:49 +02:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6f48147b3e |
@@ -1,20 +0,0 @@
|
|||||||
name: Issue Updates Workflow
|
|
||||||
|
|
||||||
on:
|
|
||||||
issues:
|
|
||||||
types: [ opened, closed, reopened ]
|
|
||||||
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
|
|
||||||
build:
|
|
||||||
name: Build
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Send Telegram Message
|
|
||||||
run: |
|
|
||||||
msg_text='${{ github.actor }} updated an issue:
|
|
||||||
status: ${{ github.event.issue.state }} - #${{ github.event.issue.number }} ${{ github.event.issue.title }}
|
|
||||||
https://github.com/zaneschepke/wgtunnel/issues/${{ github.event.issue.number }}'
|
|
||||||
curl -s -X POST 'https://api.telegram.org/bot${{ secrets.TELEGRAM_TOKEN }}/sendMessage' \
|
|
||||||
-d "chat_id=${{ secrets.TELEGRAM_TO }}&text=${msg_text}&message_thread_id=${{ secrets.TELEGRAM_TOPIC }}"
|
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
# name of the workflow
|
||||||
name: Android CI Tag Deployment (Pre-release)
|
name: Android CI Tag Deployment (Pre-release)
|
||||||
|
|
||||||
on:
|
on:
|
||||||
@@ -67,7 +68,7 @@ jobs:
|
|||||||
echo "VERSION_CODE=$version_code" >> $GITHUB_ENV
|
echo "VERSION_CODE=$version_code" >> $GITHUB_ENV
|
||||||
# Save the APK after the Build job is complete to publish it as a Github release in the next job
|
# Save the APK after the Build job is complete to publish it as a Github release in the next job
|
||||||
- name: Upload APK
|
- name: Upload APK
|
||||||
uses: actions/upload-artifact@v4.3.3
|
uses: actions/upload-artifact@v4.3.1
|
||||||
with:
|
with:
|
||||||
name: wgtunnel
|
name: wgtunnel
|
||||||
path: ${{ steps.apk-path.outputs.path }}
|
path: ${{ steps.apk-path.outputs.path }}
|
||||||
|
|||||||
@@ -1,21 +0,0 @@
|
|||||||
name: Release Updates Workflow
|
|
||||||
|
|
||||||
on:
|
|
||||||
release:
|
|
||||||
types: [ published ]
|
|
||||||
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
|
|
||||||
build:
|
|
||||||
name: Build
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Send Telegram Message
|
|
||||||
run: |
|
|
||||||
msg_text='${{ github.actor }} published a new release:
|
|
||||||
Release: ${{ github.event.release.tag_name }}
|
|
||||||
${{ github.event.release.body }}
|
|
||||||
https://github.com/zaneschepke/wgtunnel/releases/tag/${{ github.event.release.tag_name }}'
|
|
||||||
curl -s -X POST 'https://api.telegram.org/bot${{ secrets.TELEGRAM_TOKEN }}/sendMessage' \
|
|
||||||
-d "chat_id=${{ secrets.TELEGRAM_TO }}&text=${msg_text}&message_thread_id=${{ secrets.TELEGRAM_TOPIC }}"
|
|
||||||
@@ -69,7 +69,7 @@ jobs:
|
|||||||
echo "VERSION_CODE=$version_code" >> $GITHUB_ENV
|
echo "VERSION_CODE=$version_code" >> $GITHUB_ENV
|
||||||
# Save the APK after the Build job is complete to publish it as a Github release in the next job
|
# Save the APK after the Build job is complete to publish it as a Github release in the next job
|
||||||
- name: Upload APK
|
- name: Upload APK
|
||||||
uses: actions/upload-artifact@v4.3.3
|
uses: actions/upload-artifact@v4.3.1
|
||||||
with:
|
with:
|
||||||
name: wgtunnel
|
name: wgtunnel
|
||||||
path: ${{ steps.apk-path.outputs.path }}
|
path: ${{ steps.apk-path.outputs.path }}
|
||||||
|
|||||||
@@ -22,8 +22,7 @@ WG Tunnel
|
|||||||
|
|
||||||
<div align="left">
|
<div align="left">
|
||||||
|
|
||||||
This is an alternative Android Application for [WireGuard](https://www.wireguard.com/)
|
This is an alternative Android Application for [WireGuard](https://www.wireguard.com/) with added
|
||||||
and [AmneziaWG](https://docs.amnezia.org/documentation/amnezia-wg/) with added
|
|
||||||
features. Built using the [wireguard-android](https://github.com/WireGuard/wireguard-android)
|
features. Built using the [wireguard-android](https://github.com/WireGuard/wireguard-android)
|
||||||
library and [Jetpack Compose](https://developer.android.com/jetpack/compose), this application was
|
library and [Jetpack Compose](https://developer.android.com/jetpack/compose), this application was
|
||||||
inspired by the official [WireGuard Android](https://github.com/WireGuard/wireguard-android) app.
|
inspired by the official [WireGuard Android](https://github.com/WireGuard/wireguard-android) app.
|
||||||
@@ -54,9 +53,8 @@ and on while on different networks. This app was created to offer a free solutio
|
|||||||
* Auto connect to tunnels based on Wi-Fi SSID, ethernet, or mobile data
|
* Auto connect to tunnels based on Wi-Fi SSID, ethernet, or mobile data
|
||||||
* Split tunneling by application with search
|
* Split tunneling by application with search
|
||||||
* WireGuard support for kernel and userspace modes
|
* WireGuard support for kernel and userspace modes
|
||||||
* Amnezia support for userspace mode for DPI/censorship protection
|
|
||||||
* Always-On VPN support
|
* Always-On VPN support
|
||||||
* Export Amnezia and WireGuard tunnels to zip
|
* Export tunnels to zip
|
||||||
* Quick tile support for tunnel toggling, auto-tunneling
|
* Quick tile support for tunnel toggling, auto-tunneling
|
||||||
* Static shortcuts support for tunnel toggling, auto-tunneling
|
* Static shortcuts support for tunnel toggling, auto-tunneling
|
||||||
* Intent automation support for all tunnels
|
* Intent automation support for all tunnels
|
||||||
@@ -65,28 +63,27 @@ and on while on different networks. This app was created to offer a free solutio
|
|||||||
* Battery preservation measures
|
* Battery preservation measures
|
||||||
* Restart tunnel on ping failure (beta)
|
* Restart tunnel on ping failure (beta)
|
||||||
|
|
||||||
## Fdroid
|
|
||||||
|
|
||||||
Want updates faster?
|
|
||||||
|
|
||||||
Check out my personal [fdroid repository](https://github.com/zaneschepke/fdroid) to get updates the
|
|
||||||
moment they are released.
|
|
||||||
|
|
||||||
## Docs
|
## Docs
|
||||||
|
|
||||||
Information about features, behaviors, and answers to common questions can be found in the
|
Basic documentation of the feature and behaviors of this app can be
|
||||||
app [documentation](https://zaneschepke.com/wgtunnel-docs/overview.html).
|
found [here](https://zaneschepke.com/wgtunnel-docs/overview.html).
|
||||||
|
|
||||||
The repository for these docs can be found [here](https://github.com/zaneschepke/wgtunnel-docs).
|
The repository for these docs can be found [here](https://github.com/zaneschepke/wgtunnel-docs).
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
|
||||||
|
Any contributions in the form of feedback, issues, code, or translations are welcome and much appreciated!
|
||||||
|
|
||||||
|
Please read the [code of conduct](https://github.com/zaneschepke/wgtunnel?tab=coc-ov-file#contributor-code-of-conduct) before contributing.
|
||||||
|
|
||||||
## Translation
|
## Translation
|
||||||
|
|
||||||
This app is using [Weblate](https://weblate.org) to assist with translations.
|
This app is using [Weblate](https://weblate.org) to assist with translations.
|
||||||
|
|
||||||
Help translate WG Tunnel into your language
|
Help translate WG Tunnel into your language at [Hosted Weblate](https://hosted.weblate.org/engage/wg-tunnel/).\
|
||||||
at [Hosted Weblate](https://hosted.weblate.org/engage/wg-tunnel/).\
|
|
||||||
[](https://hosted.weblate.org/engage/wg-tunnel/)
|
[](https://hosted.weblate.org/engage/wg-tunnel/)
|
||||||
|
|
||||||
|
|
||||||
## Building
|
## Building
|
||||||
|
|
||||||
```
|
```
|
||||||
@@ -100,11 +97,4 @@ And then build the app:
|
|||||||
$ ./gradlew assembleDebug
|
$ ./gradlew assembleDebug
|
||||||
```
|
```
|
||||||
|
|
||||||
## Contributing
|
</span>
|
||||||
|
|
||||||
Any contributions in the form of feedback, issues, code, or translations are welcome and much
|
|
||||||
appreciated!
|
|
||||||
|
|
||||||
Please read
|
|
||||||
the [code of conduct](https://github.com/zaneschepke/wgtunnel?tab=coc-ov-file#contributor-code-of-conduct)
|
|
||||||
before contributing.
|
|
||||||
|
|||||||
@@ -112,6 +112,9 @@ android {
|
|||||||
}
|
}
|
||||||
create("general") {
|
create("general") {
|
||||||
dimension = Constants.TYPE
|
dimension = Constants.TYPE
|
||||||
|
if (BuildHelper.isReleaseBuild(gradle) && BuildHelper.isGeneralFlavor(gradle)) {
|
||||||
|
//any plugins general specific
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
compileOptions {
|
compileOptions {
|
||||||
@@ -136,7 +139,6 @@ dependencies {
|
|||||||
|
|
||||||
implementation(libs.androidx.core.ktx)
|
implementation(libs.androidx.core.ktx)
|
||||||
implementation(libs.androidx.lifecycle.runtime.ktx)
|
implementation(libs.androidx.lifecycle.runtime.ktx)
|
||||||
|
|
||||||
// helpers for implementing LifecycleOwner in a Service
|
// helpers for implementing LifecycleOwner in a Service
|
||||||
implementation(libs.androidx.lifecycle.service)
|
implementation(libs.androidx.lifecycle.service)
|
||||||
implementation(libs.androidx.activity.compose)
|
implementation(libs.androidx.activity.compose)
|
||||||
@@ -158,8 +160,7 @@ dependencies {
|
|||||||
debugImplementation(libs.androidx.compose.ui.tooling)
|
debugImplementation(libs.androidx.compose.ui.tooling)
|
||||||
debugImplementation(libs.androidx.compose.manifest)
|
debugImplementation(libs.androidx.compose.manifest)
|
||||||
|
|
||||||
// get tunnel lib from github packages or mavenLocal
|
// tunnel
|
||||||
implementation(libs.tunnel)
|
|
||||||
implementation(libs.amneziawg.android)
|
implementation(libs.amneziawg.android)
|
||||||
coreLibraryDesugaring(libs.desugar.jdk.libs)
|
coreLibraryDesugaring(libs.desugar.jdk.libs)
|
||||||
|
|
||||||
@@ -171,8 +172,6 @@ dependencies {
|
|||||||
implementation(libs.androidx.navigation.compose)
|
implementation(libs.androidx.navigation.compose)
|
||||||
implementation(libs.androidx.hilt.navigation.compose)
|
implementation(libs.androidx.hilt.navigation.compose)
|
||||||
|
|
||||||
implementation(libs.zaneschepke.multifab)
|
|
||||||
|
|
||||||
// hilt
|
// hilt
|
||||||
implementation(libs.hilt.android)
|
implementation(libs.hilt.android)
|
||||||
ksp(libs.hilt.android.compiler)
|
ksp(libs.hilt.android.compiler)
|
||||||
@@ -200,6 +199,7 @@ dependencies {
|
|||||||
|
|
||||||
// barcode scanning
|
// barcode scanning
|
||||||
implementation(libs.zxing.android.embedded)
|
implementation(libs.zxing.android.embedded)
|
||||||
|
implementation(libs.zxing.core)
|
||||||
|
|
||||||
// bio
|
// bio
|
||||||
implementation(libs.androidx.biometric.ktx)
|
implementation(libs.androidx.biometric.ktx)
|
||||||
@@ -208,7 +208,4 @@ dependencies {
|
|||||||
// shortcuts
|
// shortcuts
|
||||||
implementation(libs.androidx.core)
|
implementation(libs.androidx.core)
|
||||||
implementation(libs.androidx.core.google.shortcuts)
|
implementation(libs.androidx.core.google.shortcuts)
|
||||||
|
|
||||||
// splash
|
|
||||||
implementation(libs.androidx.core.splashscreen)
|
|
||||||
}
|
}
|
||||||
|
|||||||
Vendored
+2
@@ -22,3 +22,5 @@
|
|||||||
-keepclassmembers class * extends androidx.datastore.preferences.protobuf.GeneratedMessageLite {
|
-keepclassmembers class * extends androidx.datastore.preferences.protobuf.GeneratedMessageLite {
|
||||||
<fields>;
|
<fields>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,190 +0,0 @@
|
|||||||
{
|
|
||||||
"formatVersion": 1,
|
|
||||||
"database": {
|
|
||||||
"version": 8,
|
|
||||||
"identityHash": "b4d4a7c489f6b2f0d3aa4fa6f37b4935",
|
|
||||||
"entities": [
|
|
||||||
{
|
|
||||||
"tableName": "Settings",
|
|
||||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_tunnel_enabled` INTEGER NOT NULL, `is_tunnel_on_mobile_data_enabled` INTEGER NOT NULL, `trusted_network_ssids` TEXT NOT NULL, `is_always_on_vpn_enabled` INTEGER NOT NULL, `is_tunnel_on_ethernet_enabled` INTEGER NOT NULL, `is_shortcuts_enabled` INTEGER NOT NULL DEFAULT false, `is_tunnel_on_wifi_enabled` INTEGER NOT NULL DEFAULT false, `is_kernel_enabled` INTEGER NOT NULL DEFAULT false, `is_restore_on_boot_enabled` INTEGER NOT NULL DEFAULT false, `is_multi_tunnel_enabled` INTEGER NOT NULL DEFAULT false, `is_auto_tunnel_paused` INTEGER NOT NULL DEFAULT false, `is_ping_enabled` INTEGER NOT NULL DEFAULT false, `is_amnezia_enabled` INTEGER NOT NULL DEFAULT false)",
|
|
||||||
"fields": [
|
|
||||||
{
|
|
||||||
"fieldPath": "id",
|
|
||||||
"columnName": "id",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "isAutoTunnelEnabled",
|
|
||||||
"columnName": "is_tunnel_enabled",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "isTunnelOnMobileDataEnabled",
|
|
||||||
"columnName": "is_tunnel_on_mobile_data_enabled",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "trustedNetworkSSIDs",
|
|
||||||
"columnName": "trusted_network_ssids",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "isAlwaysOnVpnEnabled",
|
|
||||||
"columnName": "is_always_on_vpn_enabled",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "isTunnelOnEthernetEnabled",
|
|
||||||
"columnName": "is_tunnel_on_ethernet_enabled",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "isShortcutsEnabled",
|
|
||||||
"columnName": "is_shortcuts_enabled",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "false"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "isTunnelOnWifiEnabled",
|
|
||||||
"columnName": "is_tunnel_on_wifi_enabled",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "false"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "isKernelEnabled",
|
|
||||||
"columnName": "is_kernel_enabled",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "false"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "isRestoreOnBootEnabled",
|
|
||||||
"columnName": "is_restore_on_boot_enabled",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "false"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "isMultiTunnelEnabled",
|
|
||||||
"columnName": "is_multi_tunnel_enabled",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "false"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "isAutoTunnelPaused",
|
|
||||||
"columnName": "is_auto_tunnel_paused",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "false"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "isPingEnabled",
|
|
||||||
"columnName": "is_ping_enabled",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "false"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "isAmneziaEnabled",
|
|
||||||
"columnName": "is_amnezia_enabled",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "false"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"primaryKey": {
|
|
||||||
"autoGenerate": true,
|
|
||||||
"columnNames": [
|
|
||||||
"id"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"indices": [],
|
|
||||||
"foreignKeys": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"tableName": "TunnelConfig",
|
|
||||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `wg_quick` TEXT NOT NULL, `tunnel_networks` TEXT NOT NULL DEFAULT '', `is_mobile_data_tunnel` INTEGER NOT NULL DEFAULT false, `is_primary_tunnel` INTEGER NOT NULL DEFAULT false, `am_quick` TEXT NOT NULL DEFAULT '')",
|
|
||||||
"fields": [
|
|
||||||
{
|
|
||||||
"fieldPath": "id",
|
|
||||||
"columnName": "id",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "name",
|
|
||||||
"columnName": "name",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "wgQuick",
|
|
||||||
"columnName": "wg_quick",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "tunnelNetworks",
|
|
||||||
"columnName": "tunnel_networks",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "''"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "isMobileDataTunnel",
|
|
||||||
"columnName": "is_mobile_data_tunnel",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "false"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "isPrimaryTunnel",
|
|
||||||
"columnName": "is_primary_tunnel",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "false"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "amQuick",
|
|
||||||
"columnName": "am_quick",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "''"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"primaryKey": {
|
|
||||||
"autoGenerate": true,
|
|
||||||
"columnNames": [
|
|
||||||
"id"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"indices": [
|
|
||||||
{
|
|
||||||
"name": "index_TunnelConfig_name",
|
|
||||||
"unique": true,
|
|
||||||
"columnNames": [
|
|
||||||
"name"
|
|
||||||
],
|
|
||||||
"orders": [],
|
|
||||||
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_TunnelConfig_name` ON `${TABLE_NAME}` (`name`)"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"foreignKeys": []
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"views": [],
|
|
||||||
"setupQueries": [
|
|
||||||
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
|
||||||
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'b4d4a7c489f6b2f0d3aa4fa6f37b4935')"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -51,7 +51,7 @@
|
|||||||
</queries>
|
</queries>
|
||||||
<application
|
<application
|
||||||
android:name=".WireGuardAutoTunnel"
|
android:name=".WireGuardAutoTunnel"
|
||||||
android:allowBackup="false"
|
android:allowBackup="true"
|
||||||
android:banner="@drawable/ic_banner"
|
android:banner="@drawable/ic_banner"
|
||||||
android:dataExtractionRules="@xml/data_extraction_rules"
|
android:dataExtractionRules="@xml/data_extraction_rules"
|
||||||
android:enableOnBackInvokedCallback="true"
|
android:enableOnBackInvokedCallback="true"
|
||||||
@@ -60,36 +60,31 @@
|
|||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
android:roundIcon="@mipmap/ic_launcher_round"
|
android:roundIcon="@mipmap/ic_launcher_round"
|
||||||
android:supportsRtl="true"
|
android:supportsRtl="true"
|
||||||
android:theme="@style/Theme.AppSplashScreen"
|
android:theme="@style/Theme.WireguardAutoTunnel"
|
||||||
tools:targetApi="tiramisu">
|
tools:targetApi="tiramisu">
|
||||||
<activity
|
<activity
|
||||||
android:name=".ui.SplashActivity"
|
android:name=".ui.MainActivity"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
android:theme="@style/Theme.AppSplashScreen">
|
android:theme="@style/Theme.WireguardAutoTunnel">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN" />
|
<action android:name="android.intent.action.MAIN" />
|
||||||
|
|
||||||
<category android:name="android.intent.category.LAUNCHER" />
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
|
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
|
||||||
|
|
||||||
|
<action android:name="android.service.quicksettings.action.QS_TILE_PREFERENCES" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
<meta-data
|
<meta-data
|
||||||
android:name="android.app.shortcuts"
|
android:name="android.app.shortcuts"
|
||||||
android:resource="@xml/shortcuts" />
|
android:resource="@xml/shortcuts" />
|
||||||
</activity>
|
</activity>
|
||||||
<activity
|
<activity
|
||||||
android:name=".ui.MainActivity"
|
android:name=".ui.CaptureActivityPortrait"
|
||||||
android:exported="true"
|
android:screenOrientation="fullSensor"
|
||||||
android:screenOrientation="portrait"
|
android:stateNotNeeded="true"
|
||||||
android:theme="@style/Theme.WireguardAutoTunnel">
|
android:theme="@style/zxing_CaptureTheme"
|
||||||
<intent-filter>
|
android:windowSoftInputMode="stateAlwaysHidden"
|
||||||
<action android:name="android.service.quicksettings.action.QS_TILE_PREFERENCES" />
|
tools:ignore="DiscouragedApi" />
|
||||||
</intent-filter>
|
|
||||||
</activity>
|
|
||||||
<activity
|
|
||||||
android:name="com.journeyapps.barcodescanner.CaptureActivity"
|
|
||||||
android:screenOrientation="portrait"
|
|
||||||
tools:replace="screenOrientation" />
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".service.shortcut.ShortcutsActivity"
|
android:name=".service.shortcut.ShortcutsActivity"
|
||||||
android:enabled="true"
|
android:enabled="true"
|
||||||
|
|||||||
@@ -2,37 +2,26 @@ package com.zaneschepke.wireguardautotunnel
|
|||||||
|
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
import android.content.ComponentName
|
import android.content.ComponentName
|
||||||
|
import android.content.Context
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
import android.os.StrictMode
|
|
||||||
import android.os.StrictMode.ThreadPolicy
|
|
||||||
import android.service.quicksettings.TileService
|
import android.service.quicksettings.TileService
|
||||||
import com.zaneschepke.wireguardautotunnel.service.tile.AutoTunnelControlTile
|
import com.zaneschepke.wireguardautotunnel.service.tile.AutoTunnelControlTile
|
||||||
import com.zaneschepke.wireguardautotunnel.service.tile.TunnelControlTile
|
import com.zaneschepke.wireguardautotunnel.service.tile.TunnelControlTile
|
||||||
import com.zaneschepke.wireguardautotunnel.util.ReleaseTree
|
import com.zaneschepke.wireguardautotunnel.util.ReleaseTree
|
||||||
import dagger.hilt.android.HiltAndroidApp
|
import dagger.hilt.android.HiltAndroidApp
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
import xyz.teamgravity.pin_lock_compose.PinManager
|
||||||
|
|
||||||
@HiltAndroidApp
|
@HiltAndroidApp
|
||||||
class WireGuardAutoTunnel : Application() {
|
class WireGuardAutoTunnel : Application() {
|
||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
instance = this
|
instance = this
|
||||||
if (BuildConfig.DEBUG) {
|
if (BuildConfig.DEBUG) Timber.plant(Timber.DebugTree()) else Timber.plant(ReleaseTree())
|
||||||
Timber.plant(Timber.DebugTree())
|
PinManager.initialize(this)
|
||||||
StrictMode.setThreadPolicy(
|
|
||||||
ThreadPolicy.Builder()
|
|
||||||
.detectDiskReads()
|
|
||||||
.detectDiskWrites()
|
|
||||||
.detectNetwork()
|
|
||||||
.penaltyLog()
|
|
||||||
.build(),
|
|
||||||
)
|
|
||||||
} else Timber.plant(ReleaseTree())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
lateinit var instance: WireGuardAutoTunnel
|
lateinit var instance: WireGuardAutoTunnel
|
||||||
private set
|
private set
|
||||||
|
|
||||||
@@ -40,16 +29,16 @@ class WireGuardAutoTunnel : Application() {
|
|||||||
return instance.packageManager.hasSystemFeature(PackageManager.FEATURE_LEANBACK)
|
return instance.packageManager.hasSystemFeature(PackageManager.FEATURE_LEANBACK)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun requestTunnelTileServiceStateUpdate() {
|
fun requestTunnelTileServiceStateUpdate(context: Context) {
|
||||||
TileService.requestListeningState(
|
TileService.requestListeningState(
|
||||||
instance,
|
context,
|
||||||
ComponentName(instance, TunnelControlTile::class.java),
|
ComponentName(instance, TunnelControlTile::class.java),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun requestAutoTunnelTileServiceUpdate() {
|
fun requestAutoTunnelTileServiceUpdate(context: Context) {
|
||||||
TileService.requestListeningState(
|
TileService.requestListeningState(
|
||||||
instance,
|
context,
|
||||||
ComponentName(instance, AutoTunnelControlTile::class.java),
|
ComponentName(instance, AutoTunnelControlTile::class.java),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,12 +6,12 @@ import androidx.room.DeleteColumn
|
|||||||
import androidx.room.RoomDatabase
|
import androidx.room.RoomDatabase
|
||||||
import androidx.room.TypeConverters
|
import androidx.room.TypeConverters
|
||||||
import androidx.room.migration.AutoMigrationSpec
|
import androidx.room.migration.AutoMigrationSpec
|
||||||
import com.zaneschepke.wireguardautotunnel.data.domain.Settings
|
import com.zaneschepke.wireguardautotunnel.data.model.Settings
|
||||||
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
|
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
|
||||||
|
|
||||||
@Database(
|
@Database(
|
||||||
entities = [Settings::class, TunnelConfig::class],
|
entities = [Settings::class, TunnelConfig::class],
|
||||||
version = 8,
|
version = 7,
|
||||||
autoMigrations =
|
autoMigrations =
|
||||||
[
|
[
|
||||||
AutoMigration(from = 1, to = 2),
|
AutoMigration(from = 1, to = 2),
|
||||||
@@ -33,7 +33,6 @@ import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
|
|||||||
to = 7,
|
to = 7,
|
||||||
spec = RemoveLegacySettingColumnsMigration::class,
|
spec = RemoveLegacySettingColumnsMigration::class,
|
||||||
),
|
),
|
||||||
AutoMigration(7, 8),
|
|
||||||
],
|
],
|
||||||
exportSchema = true,
|
exportSchema = true,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import androidx.room.Delete
|
|||||||
import androidx.room.Insert
|
import androidx.room.Insert
|
||||||
import androidx.room.OnConflictStrategy
|
import androidx.room.OnConflictStrategy
|
||||||
import androidx.room.Query
|
import androidx.room.Query
|
||||||
import com.zaneschepke.wireguardautotunnel.data.domain.Settings
|
import com.zaneschepke.wireguardautotunnel.data.model.Settings
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
||||||
@Dao
|
@Dao
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import androidx.room.Delete
|
|||||||
import androidx.room.Insert
|
import androidx.room.Insert
|
||||||
import androidx.room.OnConflictStrategy
|
import androidx.room.OnConflictStrategy
|
||||||
import androidx.room.Query
|
import androidx.room.Query
|
||||||
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
|
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
|
||||||
import com.zaneschepke.wireguardautotunnel.util.TunnelConfigs
|
import com.zaneschepke.wireguardautotunnel.util.TunnelConfigs
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
||||||
@@ -20,9 +20,6 @@ interface TunnelConfigDao {
|
|||||||
@Query("SELECT * FROM TunnelConfig WHERE id=:id")
|
@Query("SELECT * FROM TunnelConfig WHERE id=:id")
|
||||||
suspend fun getById(id: Long): TunnelConfig?
|
suspend fun getById(id: Long): TunnelConfig?
|
||||||
|
|
||||||
@Query("SELECT * FROM TunnelConfig WHERE name=:name")
|
|
||||||
suspend fun getByName(name: String): TunnelConfig?
|
|
||||||
|
|
||||||
@Query("SELECT * FROM TunnelConfig")
|
@Query("SELECT * FROM TunnelConfig")
|
||||||
suspend fun getAll(): TunnelConfigs
|
suspend fun getAll(): TunnelConfigs
|
||||||
|
|
||||||
@@ -36,10 +33,10 @@ interface TunnelConfigDao {
|
|||||||
suspend fun findByTunnelNetworkName(name: String): TunnelConfigs
|
suspend fun findByTunnelNetworkName(name: String): TunnelConfigs
|
||||||
|
|
||||||
@Query("UPDATE TunnelConfig SET is_primary_tunnel = 0 WHERE is_primary_tunnel =1")
|
@Query("UPDATE TunnelConfig SET is_primary_tunnel = 0 WHERE is_primary_tunnel =1")
|
||||||
suspend fun resetPrimaryTunnel()
|
fun resetPrimaryTunnel()
|
||||||
|
|
||||||
@Query("UPDATE TunnelConfig SET is_mobile_data_tunnel = 0 WHERE is_mobile_data_tunnel =1")
|
@Query("UPDATE TunnelConfig SET is_mobile_data_tunnel = 0 WHERE is_mobile_data_tunnel =1")
|
||||||
suspend fun resetMobileDataTunnel()
|
fun resetMobileDataTunnel()
|
||||||
|
|
||||||
@Query("SELECT * FROM TUNNELCONFIG WHERE is_primary_tunnel=1")
|
@Query("SELECT * FROM TUNNELCONFIG WHERE is_primary_tunnel=1")
|
||||||
suspend fun findByPrimary(): TunnelConfigs
|
suspend fun findByPrimary(): TunnelConfigs
|
||||||
|
|||||||
+16
-29
@@ -7,20 +7,14 @@ import androidx.datastore.preferences.core.edit
|
|||||||
import androidx.datastore.preferences.core.intPreferencesKey
|
import androidx.datastore.preferences.core.intPreferencesKey
|
||||||
import androidx.datastore.preferences.core.stringPreferencesKey
|
import androidx.datastore.preferences.core.stringPreferencesKey
|
||||||
import androidx.datastore.preferences.preferencesDataStore
|
import androidx.datastore.preferences.preferencesDataStore
|
||||||
import com.zaneschepke.wireguardautotunnel.module.IoDispatcher
|
|
||||||
import kotlinx.coroutines.CoroutineDispatcher
|
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.first
|
import kotlinx.coroutines.flow.first
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
|
|
||||||
class DataStoreManager(
|
class DataStoreManager(private val context: Context) {
|
||||||
private val context: Context,
|
|
||||||
@IoDispatcher private val ioDispatcher: CoroutineDispatcher
|
|
||||||
) {
|
|
||||||
companion object {
|
companion object {
|
||||||
val LOCATION_DISCLOSURE_SHOWN = booleanPreferencesKey("LOCATION_DISCLOSURE_SHOWN")
|
val LOCATION_DISCLOSURE_SHOWN = booleanPreferencesKey("LOCATION_DISCLOSURE_SHOWN")
|
||||||
val BATTERY_OPTIMIZE_DISABLE_SHOWN = booleanPreferencesKey("BATTERY_OPTIMIZE_DISABLE_SHOWN")
|
val BATTERY_OPTIMIZE_DISABLE_SHOWN = booleanPreferencesKey("BATTERY_OPTIMIZE_DISABLE_SHOWN")
|
||||||
@@ -28,7 +22,6 @@ class DataStoreManager(
|
|||||||
booleanPreferencesKey("TUNNEL_RUNNING_FROM_MANUAL_START")
|
booleanPreferencesKey("TUNNEL_RUNNING_FROM_MANUAL_START")
|
||||||
val ACTIVE_TUNNEL = intPreferencesKey("ACTIVE_TUNNEL")
|
val ACTIVE_TUNNEL = intPreferencesKey("ACTIVE_TUNNEL")
|
||||||
val CURRENT_SSID = stringPreferencesKey("CURRENT_SSID")
|
val CURRENT_SSID = stringPreferencesKey("CURRENT_SSID")
|
||||||
val IS_PIN_LOCK_ENABLED = booleanPreferencesKey("PIN_LOCK_ENABLED")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// preferences
|
// preferences
|
||||||
@@ -39,24 +32,20 @@ class DataStoreManager(
|
|||||||
)
|
)
|
||||||
|
|
||||||
suspend fun init() {
|
suspend fun init() {
|
||||||
withContext(ioDispatcher) {
|
try {
|
||||||
try {
|
context.dataStore.data.first()
|
||||||
context.dataStore.data.first()
|
} catch (e: IOException) {
|
||||||
} catch (e: IOException) {
|
Timber.e(e)
|
||||||
Timber.e(e)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun <T> saveToDataStore(key: Preferences.Key<T>, value: T) {
|
suspend fun <T> saveToDataStore(key: Preferences.Key<T>, value: T) {
|
||||||
withContext(ioDispatcher) {
|
try {
|
||||||
try {
|
context.dataStore.edit { it[key] = value }
|
||||||
context.dataStore.edit { it[key] = value }
|
} catch (e: IOException) {
|
||||||
} catch (e: IOException) {
|
Timber.e(e)
|
||||||
Timber.e(e)
|
} catch (e: Exception) {
|
||||||
} catch (e: Exception) {
|
Timber.e(e)
|
||||||
Timber.e(e)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -64,13 +53,11 @@ class DataStoreManager(
|
|||||||
fun <T> getFromStoreFlow(key: Preferences.Key<T>) = context.dataStore.data.map { it[key] }
|
fun <T> getFromStoreFlow(key: Preferences.Key<T>) = context.dataStore.data.map { it[key] }
|
||||||
|
|
||||||
suspend fun <T> getFromStore(key: Preferences.Key<T>): T? {
|
suspend fun <T> getFromStore(key: Preferences.Key<T>): T? {
|
||||||
return withContext(ioDispatcher) {
|
return try {
|
||||||
try {
|
context.dataStore.data.map { it[key] }.first()
|
||||||
context.dataStore.data.map { it[key] }.first()
|
} catch (e: IOException) {
|
||||||
} catch (e: IOException) {
|
Timber.e(e)
|
||||||
Timber.e(e)
|
null
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,16 +0,0 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.data.domain
|
|
||||||
|
|
||||||
data class GeneralState(
|
|
||||||
val isLocationDisclosureShown: Boolean = LOCATION_DISCLOSURE_SHOWN_DEFAULT,
|
|
||||||
val isBatteryOptimizationDisableShown: Boolean = BATTERY_OPTIMIZATION_DISABLE_SHOWN_DEFAULT,
|
|
||||||
val isTunnelRunningFromManualStart: Boolean = TUNNELING_RUNNING_FROM_MANUAL_START_DEFAULT,
|
|
||||||
val isPinLockEnabled: Boolean = PIN_LOCK_ENABLED_DEFAULT,
|
|
||||||
val activeTunnelId: Int? = null
|
|
||||||
) {
|
|
||||||
companion object {
|
|
||||||
const val LOCATION_DISCLOSURE_SHOWN_DEFAULT = false
|
|
||||||
const val BATTERY_OPTIMIZATION_DISABLE_SHOWN_DEFAULT = false
|
|
||||||
const val TUNNELING_RUNNING_FROM_MANUAL_START_DEFAULT = false
|
|
||||||
const val PIN_LOCK_ENABLED_DEFAULT = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
package com.zaneschepke.wireguardautotunnel.data.model
|
||||||
|
|
||||||
|
data class GeneralState(
|
||||||
|
val locationDisclosureShown: Boolean = LOCATION_DISCLOSURE_SHOWN_DEFAULT,
|
||||||
|
val batteryOptimizationDisableShown: Boolean = BATTERY_OPTIMIZATION_DISABLE_SHOWN_DEFAULT,
|
||||||
|
val tunnelRunningFromManualStart: Boolean = TUNNELING_RUNNING_FROM_MANUAL_START_DEFAULT,
|
||||||
|
val activeTunnelId: Int? = null
|
||||||
|
) {
|
||||||
|
companion object {
|
||||||
|
const val LOCATION_DISCLOSURE_SHOWN_DEFAULT = false
|
||||||
|
const val BATTERY_OPTIMIZATION_DISABLE_SHOWN_DEFAULT = false
|
||||||
|
const val TUNNELING_RUNNING_FROM_MANUAL_START_DEFAULT = false
|
||||||
|
}
|
||||||
|
}
|
||||||
+1
-6
@@ -1,4 +1,4 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.data.domain
|
package com.zaneschepke.wireguardautotunnel.data.model
|
||||||
|
|
||||||
import androidx.room.ColumnInfo
|
import androidx.room.ColumnInfo
|
||||||
import androidx.room.Entity
|
import androidx.room.Entity
|
||||||
@@ -50,9 +50,4 @@ data class Settings(
|
|||||||
defaultValue = "false",
|
defaultValue = "false",
|
||||||
)
|
)
|
||||||
val isPingEnabled: Boolean = false,
|
val isPingEnabled: Boolean = false,
|
||||||
@ColumnInfo(
|
|
||||||
name = "is_amnezia_enabled",
|
|
||||||
defaultValue = "false",
|
|
||||||
)
|
|
||||||
val isAmneziaEnabled: Boolean = false,
|
|
||||||
)
|
)
|
||||||
+5
-20
@@ -1,10 +1,10 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.data.domain
|
package com.zaneschepke.wireguardautotunnel.data.model
|
||||||
|
|
||||||
import androidx.room.ColumnInfo
|
import androidx.room.ColumnInfo
|
||||||
import androidx.room.Entity
|
import androidx.room.Entity
|
||||||
import androidx.room.Index
|
import androidx.room.Index
|
||||||
import androidx.room.PrimaryKey
|
import androidx.room.PrimaryKey
|
||||||
import com.wireguard.config.Config
|
import org.amnezia.awg.config.Config
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
|
|
||||||
@Entity(indices = [Index(value = ["name"], unique = true)])
|
@Entity(indices = [Index(value = ["name"], unique = true)])
|
||||||
@@ -27,27 +27,12 @@ data class TunnelConfig(
|
|||||||
defaultValue = "false",
|
defaultValue = "false",
|
||||||
)
|
)
|
||||||
val isPrimaryTunnel: Boolean = false,
|
val isPrimaryTunnel: Boolean = false,
|
||||||
@ColumnInfo(
|
|
||||||
name = "am_quick",
|
|
||||||
defaultValue = "",
|
|
||||||
)
|
|
||||||
val amQuick: String = AM_QUICK_DEFAULT,
|
|
||||||
) {
|
) {
|
||||||
companion object {
|
companion object {
|
||||||
fun configFromWgQuick(wgQuick: String): Config {
|
fun configFromQuick(wgQuick: String): Config {
|
||||||
val inputStream: InputStream = wgQuick.byteInputStream()
|
val inputStream: InputStream = wgQuick.byteInputStream()
|
||||||
return inputStream.bufferedReader(Charsets.UTF_8).use {
|
val reader = inputStream.bufferedReader(Charsets.UTF_8)
|
||||||
Config.parse(it)
|
return Config.parse(reader)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun configFromAmQuick(amQuick: String): org.amnezia.awg.config.Config {
|
|
||||||
val inputStream: InputStream = amQuick.byteInputStream()
|
|
||||||
return inputStream.bufferedReader(Charsets.UTF_8).use {
|
|
||||||
org.amnezia.awg.config.Config.parse(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const val AM_QUICK_DEFAULT = ""
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.data.repository
|
package com.zaneschepke.wireguardautotunnel.data.repository
|
||||||
|
|
||||||
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
|
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
|
||||||
|
|
||||||
interface AppDataRepository {
|
interface AppDataRepository {
|
||||||
suspend fun getPrimaryOrFirstTunnel(): TunnelConfig?
|
suspend fun getPrimaryOrFirstTunnel(): TunnelConfig?
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.data.repository
|
package com.zaneschepke.wireguardautotunnel.data.repository
|
||||||
|
|
||||||
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
|
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
class AppDataRoomRepository @Inject constructor(
|
class AppDataRoomRepository @Inject constructor(
|
||||||
|
|||||||
+1
-4
@@ -1,15 +1,12 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.data.repository
|
package com.zaneschepke.wireguardautotunnel.data.repository
|
||||||
|
|
||||||
import com.zaneschepke.wireguardautotunnel.data.domain.GeneralState
|
import com.zaneschepke.wireguardautotunnel.data.model.GeneralState
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
||||||
interface AppStateRepository {
|
interface AppStateRepository {
|
||||||
suspend fun isLocationDisclosureShown(): Boolean
|
suspend fun isLocationDisclosureShown(): Boolean
|
||||||
suspend fun setLocationDisclosureShown(shown: Boolean)
|
suspend fun setLocationDisclosureShown(shown: Boolean)
|
||||||
|
|
||||||
suspend fun isPinLockEnabled(): Boolean
|
|
||||||
suspend fun setPinLockEnabled(enabled: Boolean)
|
|
||||||
|
|
||||||
suspend fun isBatteryOptimizationDisableShown(): Boolean
|
suspend fun isBatteryOptimizationDisableShown(): Boolean
|
||||||
suspend fun setBatteryOptimizationDisableShown(shown: Boolean)
|
suspend fun setBatteryOptimizationDisableShown(shown: Boolean)
|
||||||
|
|
||||||
|
|||||||
+4
-15
@@ -1,7 +1,7 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.data.repository
|
package com.zaneschepke.wireguardautotunnel.data.repository
|
||||||
|
|
||||||
import com.zaneschepke.wireguardautotunnel.data.datastore.DataStoreManager
|
import com.zaneschepke.wireguardautotunnel.data.datastore.DataStoreManager
|
||||||
import com.zaneschepke.wireguardautotunnel.data.domain.GeneralState
|
import com.zaneschepke.wireguardautotunnel.data.model.GeneralState
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
@@ -17,15 +17,6 @@ class DataStoreAppStateRepository(private val dataStoreManager: DataStoreManager
|
|||||||
dataStoreManager.saveToDataStore(DataStoreManager.LOCATION_DISCLOSURE_SHOWN, shown)
|
dataStoreManager.saveToDataStore(DataStoreManager.LOCATION_DISCLOSURE_SHOWN, shown)
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun isPinLockEnabled(): Boolean {
|
|
||||||
return dataStoreManager.getFromStore(DataStoreManager.IS_PIN_LOCK_ENABLED)
|
|
||||||
?: GeneralState.PIN_LOCK_ENABLED_DEFAULT
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun setPinLockEnabled(enabled: Boolean) {
|
|
||||||
dataStoreManager.saveToDataStore(DataStoreManager.IS_PIN_LOCK_ENABLED, enabled)
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun isBatteryOptimizationDisableShown(): Boolean {
|
override suspend fun isBatteryOptimizationDisableShown(): Boolean {
|
||||||
return dataStoreManager.getFromStore(DataStoreManager.BATTERY_OPTIMIZE_DISABLE_SHOWN)
|
return dataStoreManager.getFromStore(DataStoreManager.BATTERY_OPTIMIZE_DISABLE_SHOWN)
|
||||||
?: GeneralState.BATTERY_OPTIMIZATION_DISABLE_SHOWN_DEFAULT
|
?: GeneralState.BATTERY_OPTIMIZATION_DISABLE_SHOWN_DEFAULT
|
||||||
@@ -74,13 +65,11 @@ class DataStoreAppStateRepository(private val dataStoreManager: DataStoreManager
|
|||||||
prefs?.let { pref ->
|
prefs?.let { pref ->
|
||||||
try {
|
try {
|
||||||
GeneralState(
|
GeneralState(
|
||||||
isLocationDisclosureShown = pref[DataStoreManager.LOCATION_DISCLOSURE_SHOWN]
|
locationDisclosureShown = pref[DataStoreManager.LOCATION_DISCLOSURE_SHOWN]
|
||||||
?: GeneralState.LOCATION_DISCLOSURE_SHOWN_DEFAULT,
|
?: GeneralState.LOCATION_DISCLOSURE_SHOWN_DEFAULT,
|
||||||
isBatteryOptimizationDisableShown = pref[DataStoreManager.BATTERY_OPTIMIZE_DISABLE_SHOWN]
|
batteryOptimizationDisableShown = pref[DataStoreManager.BATTERY_OPTIMIZE_DISABLE_SHOWN]
|
||||||
?: GeneralState.BATTERY_OPTIMIZATION_DISABLE_SHOWN_DEFAULT,
|
?: GeneralState.BATTERY_OPTIMIZATION_DISABLE_SHOWN_DEFAULT,
|
||||||
isTunnelRunningFromManualStart = pref[DataStoreManager.TUNNEL_RUNNING_FROM_MANUAL_START]
|
tunnelRunningFromManualStart = pref[DataStoreManager.TUNNEL_RUNNING_FROM_MANUAL_START]
|
||||||
?: GeneralState.TUNNELING_RUNNING_FROM_MANUAL_START_DEFAULT,
|
|
||||||
isPinLockEnabled = pref[DataStoreManager.IS_PIN_LOCK_ENABLED]
|
|
||||||
?: GeneralState.TUNNELING_RUNNING_FROM_MANUAL_START_DEFAULT,
|
?: GeneralState.TUNNELING_RUNNING_FROM_MANUAL_START_DEFAULT,
|
||||||
)
|
)
|
||||||
} catch (e: IllegalArgumentException) {
|
} catch (e: IllegalArgumentException) {
|
||||||
|
|||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.data.repository
|
package com.zaneschepke.wireguardautotunnel.data.repository
|
||||||
|
|
||||||
import com.zaneschepke.wireguardautotunnel.data.SettingsDao
|
import com.zaneschepke.wireguardautotunnel.data.SettingsDao
|
||||||
import com.zaneschepke.wireguardautotunnel.data.domain.Settings
|
import com.zaneschepke.wireguardautotunnel.data.model.Settings
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
||||||
class RoomSettingsRepository(private val settingsDoa: SettingsDao) : SettingsRepository {
|
class RoomSettingsRepository(private val settingsDoa: SettingsDao) : SettingsRepository {
|
||||||
|
|||||||
+1
-5
@@ -1,7 +1,7 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.data.repository
|
package com.zaneschepke.wireguardautotunnel.data.repository
|
||||||
|
|
||||||
import com.zaneschepke.wireguardautotunnel.data.TunnelConfigDao
|
import com.zaneschepke.wireguardautotunnel.data.TunnelConfigDao
|
||||||
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
|
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
|
||||||
import com.zaneschepke.wireguardautotunnel.util.TunnelConfigs
|
import com.zaneschepke.wireguardautotunnel.util.TunnelConfigs
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
||||||
@@ -54,10 +54,6 @@ class RoomTunnelConfigRepository(private val tunnelConfigDao: TunnelConfigDao) :
|
|||||||
return tunnelConfigDao.count().toInt()
|
return tunnelConfigDao.count().toInt()
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun findByTunnelName(name: String): TunnelConfig? {
|
|
||||||
return tunnelConfigDao.getByName(name)
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun findByTunnelNetworksName(name: String): TunnelConfigs {
|
override suspend fun findByTunnelNetworksName(name: String): TunnelConfigs {
|
||||||
return tunnelConfigDao.findByTunnelNetworkName(name)
|
return tunnelConfigDao.findByTunnelNetworkName(name)
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.data.repository
|
package com.zaneschepke.wireguardautotunnel.data.repository
|
||||||
|
|
||||||
import com.zaneschepke.wireguardautotunnel.data.domain.Settings
|
import com.zaneschepke.wireguardautotunnel.data.model.Settings
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
||||||
interface SettingsRepository {
|
interface SettingsRepository {
|
||||||
|
|||||||
+1
-3
@@ -1,6 +1,6 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.data.repository
|
package com.zaneschepke.wireguardautotunnel.data.repository
|
||||||
|
|
||||||
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
|
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
|
||||||
import com.zaneschepke.wireguardautotunnel.util.TunnelConfigs
|
import com.zaneschepke.wireguardautotunnel.util.TunnelConfigs
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
||||||
@@ -22,8 +22,6 @@ interface TunnelConfigRepository {
|
|||||||
|
|
||||||
suspend fun count(): Int
|
suspend fun count(): Int
|
||||||
|
|
||||||
suspend fun findByTunnelName(name: String): TunnelConfig?
|
|
||||||
|
|
||||||
suspend fun findByTunnelNetworksName(name: String): TunnelConfigs
|
suspend fun findByTunnelNetworksName(name: String): TunnelConfigs
|
||||||
|
|
||||||
suspend fun findByMobileDataTunnel(): TunnelConfigs
|
suspend fun findByMobileDataTunnel(): TunnelConfigs
|
||||||
|
|||||||
@@ -1,30 +0,0 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.module
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import com.zaneschepke.logcatter.LocalLogCollector
|
|
||||||
import com.zaneschepke.logcatter.LogcatHelper
|
|
||||||
import dagger.Module
|
|
||||||
import dagger.Provides
|
|
||||||
import dagger.hilt.InstallIn
|
|
||||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
|
||||||
import dagger.hilt.components.SingletonComponent
|
|
||||||
import kotlinx.coroutines.CoroutineDispatcher
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.SupervisorJob
|
|
||||||
import javax.inject.Singleton
|
|
||||||
|
|
||||||
@Module
|
|
||||||
@InstallIn(SingletonComponent::class)
|
|
||||||
class AppModule {
|
|
||||||
@Singleton
|
|
||||||
@ApplicationScope
|
|
||||||
@Provides
|
|
||||||
fun providesApplicationScope(@DefaultDispatcher defaultDispatcher: CoroutineDispatcher): CoroutineScope =
|
|
||||||
CoroutineScope(SupervisorJob() + defaultDispatcher)
|
|
||||||
|
|
||||||
@Singleton
|
|
||||||
@Provides
|
|
||||||
fun provideLogCollect(@ApplicationContext context: Context): LocalLogCollector {
|
|
||||||
return LogcatHelper.init(context = context)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.module
|
|
||||||
|
|
||||||
import javax.inject.Qualifier
|
|
||||||
|
|
||||||
@Retention(AnnotationRetention.RUNTIME)
|
|
||||||
@Qualifier
|
|
||||||
annotation class DefaultDispatcher
|
|
||||||
|
|
||||||
@Retention(AnnotationRetention.RUNTIME)
|
|
||||||
@Qualifier
|
|
||||||
annotation class IoDispatcher
|
|
||||||
|
|
||||||
@Retention(AnnotationRetention.RUNTIME)
|
|
||||||
@Qualifier
|
|
||||||
annotation class MainDispatcher
|
|
||||||
|
|
||||||
@Retention(AnnotationRetention.BINARY)
|
|
||||||
@Qualifier
|
|
||||||
annotation class MainImmediateDispatcher
|
|
||||||
|
|
||||||
@Retention(AnnotationRetention.RUNTIME)
|
|
||||||
@Qualifier
|
|
||||||
annotation class ApplicationScope
|
|
||||||
|
|
||||||
@Retention(AnnotationRetention.RUNTIME)
|
|
||||||
@Qualifier
|
|
||||||
annotation class ServiceScope
|
|
||||||
-28
@@ -1,28 +0,0 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.module
|
|
||||||
|
|
||||||
import dagger.Module
|
|
||||||
import dagger.Provides
|
|
||||||
import dagger.hilt.InstallIn
|
|
||||||
import dagger.hilt.components.SingletonComponent
|
|
||||||
import kotlinx.coroutines.CoroutineDispatcher
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
|
|
||||||
@Module
|
|
||||||
@InstallIn(SingletonComponent::class)
|
|
||||||
object CoroutinesDispatchersModule {
|
|
||||||
@DefaultDispatcher
|
|
||||||
@Provides
|
|
||||||
fun providesDefaultDispatcher(): CoroutineDispatcher = Dispatchers.Default
|
|
||||||
|
|
||||||
@IoDispatcher
|
|
||||||
@Provides
|
|
||||||
fun providesIoDispatcher(): CoroutineDispatcher = Dispatchers.IO
|
|
||||||
|
|
||||||
@MainDispatcher
|
|
||||||
@Provides
|
|
||||||
fun providesMainDispatcher(): CoroutineDispatcher = Dispatchers.Main
|
|
||||||
|
|
||||||
@MainImmediateDispatcher
|
|
||||||
@Provides
|
|
||||||
fun providesMainImmediateDispatcher(): CoroutineDispatcher = Dispatchers.Main.immediate
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
package com.zaneschepke.wireguardautotunnel.module
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.room.Room
|
||||||
|
import com.zaneschepke.wireguardautotunnel.R
|
||||||
|
import com.zaneschepke.wireguardautotunnel.data.AppDatabase
|
||||||
|
import com.zaneschepke.wireguardautotunnel.data.DatabaseCallback
|
||||||
|
import dagger.Module
|
||||||
|
import dagger.Provides
|
||||||
|
import dagger.hilt.InstallIn
|
||||||
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
|
import dagger.hilt.components.SingletonComponent
|
||||||
|
import javax.inject.Singleton
|
||||||
|
|
||||||
|
@Module
|
||||||
|
@InstallIn(SingletonComponent::class)
|
||||||
|
class DatabaseModule {
|
||||||
|
@Provides
|
||||||
|
@Singleton
|
||||||
|
fun provideDatabase(@ApplicationContext context: Context): AppDatabase {
|
||||||
|
return Room.databaseBuilder(
|
||||||
|
context,
|
||||||
|
AppDatabase::class.java,
|
||||||
|
context.getString(R.string.db_name),
|
||||||
|
)
|
||||||
|
.fallbackToDestructiveMigration()
|
||||||
|
.addCallback(DatabaseCallback())
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package com.zaneschepke.wireguardautotunnel.module
|
||||||
|
|
||||||
|
import javax.inject.Qualifier
|
||||||
|
|
||||||
|
@Qualifier
|
||||||
|
@Retention(AnnotationRetention.BINARY)
|
||||||
|
annotation class Kernel
|
||||||
@@ -1,10 +1,7 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.module
|
package com.zaneschepke.wireguardautotunnel.module
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import androidx.room.Room
|
|
||||||
import com.zaneschepke.wireguardautotunnel.R
|
|
||||||
import com.zaneschepke.wireguardautotunnel.data.AppDatabase
|
import com.zaneschepke.wireguardautotunnel.data.AppDatabase
|
||||||
import com.zaneschepke.wireguardautotunnel.data.DatabaseCallback
|
|
||||||
import com.zaneschepke.wireguardautotunnel.data.SettingsDao
|
import com.zaneschepke.wireguardautotunnel.data.SettingsDao
|
||||||
import com.zaneschepke.wireguardautotunnel.data.TunnelConfigDao
|
import com.zaneschepke.wireguardautotunnel.data.TunnelConfigDao
|
||||||
import com.zaneschepke.wireguardautotunnel.data.datastore.DataStoreManager
|
import com.zaneschepke.wireguardautotunnel.data.datastore.DataStoreManager
|
||||||
@@ -21,25 +18,11 @@ import dagger.Provides
|
|||||||
import dagger.hilt.InstallIn
|
import dagger.hilt.InstallIn
|
||||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
import dagger.hilt.components.SingletonComponent
|
import dagger.hilt.components.SingletonComponent
|
||||||
import kotlinx.coroutines.CoroutineDispatcher
|
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
|
|
||||||
@Module
|
@Module
|
||||||
@InstallIn(SingletonComponent::class)
|
@InstallIn(SingletonComponent::class)
|
||||||
class RepositoryModule {
|
class RepositoryModule {
|
||||||
@Provides
|
|
||||||
@Singleton
|
|
||||||
fun provideDatabase(@ApplicationContext context: Context): AppDatabase {
|
|
||||||
return Room.databaseBuilder(
|
|
||||||
context,
|
|
||||||
AppDatabase::class.java,
|
|
||||||
context.getString(R.string.db_name),
|
|
||||||
)
|
|
||||||
.fallbackToDestructiveMigration()
|
|
||||||
.addCallback(DatabaseCallback())
|
|
||||||
.build()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Singleton
|
@Singleton
|
||||||
@Provides
|
@Provides
|
||||||
fun provideSettingsDoa(appDatabase: AppDatabase): SettingsDao {
|
fun provideSettingsDoa(appDatabase: AppDatabase): SettingsDao {
|
||||||
@@ -66,11 +49,8 @@ class RepositoryModule {
|
|||||||
|
|
||||||
@Singleton
|
@Singleton
|
||||||
@Provides
|
@Provides
|
||||||
fun providePreferencesDataStore(
|
fun providePreferencesDataStore(@ApplicationContext context: Context): DataStoreManager {
|
||||||
@ApplicationContext context: Context,
|
return DataStoreManager(context)
|
||||||
@IoDispatcher ioDispatcher: CoroutineDispatcher
|
|
||||||
): DataStoreManager {
|
|
||||||
return DataStoreManager(context, ioDispatcher)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
|
|||||||
@@ -1,11 +1,6 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.module
|
package com.zaneschepke.wireguardautotunnel.module
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import com.wireguard.android.backend.Backend
|
|
||||||
import com.wireguard.android.backend.GoBackend
|
|
||||||
import com.wireguard.android.backend.WgQuickBackend
|
|
||||||
import com.wireguard.android.util.RootShell
|
|
||||||
import com.wireguard.android.util.ToolsInstaller
|
|
||||||
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
|
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
|
||||||
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
|
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
|
||||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnService
|
import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnService
|
||||||
@@ -15,9 +10,11 @@ import dagger.Provides
|
|||||||
import dagger.hilt.InstallIn
|
import dagger.hilt.InstallIn
|
||||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
import dagger.hilt.components.SingletonComponent
|
import dagger.hilt.components.SingletonComponent
|
||||||
import kotlinx.coroutines.CoroutineDispatcher
|
import org.amnezia.awg.backend.AwgQuickBackend
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import org.amnezia.awg.backend.Backend
|
||||||
import javax.inject.Provider
|
import org.amnezia.awg.backend.GoBackend
|
||||||
|
import org.amnezia.awg.util.RootShell
|
||||||
|
import org.amnezia.awg.util.ToolsInstaller
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
|
|
||||||
@Module
|
@Module
|
||||||
@@ -40,41 +37,22 @@ class TunnelModule {
|
|||||||
@Singleton
|
@Singleton
|
||||||
@Kernel
|
@Kernel
|
||||||
fun provideKernelBackend(@ApplicationContext context: Context, rootShell: RootShell): Backend {
|
fun provideKernelBackend(@ApplicationContext context: Context, rootShell: RootShell): Backend {
|
||||||
return WgQuickBackend(context, rootShell, ToolsInstaller(context, rootShell))
|
return AwgQuickBackend(context, rootShell, ToolsInstaller(context, rootShell))
|
||||||
}
|
|
||||||
|
|
||||||
@Provides
|
|
||||||
@Singleton
|
|
||||||
fun provideAmneziaBackend(@ApplicationContext context: Context): org.amnezia.awg.backend.Backend {
|
|
||||||
return org.amnezia.awg.backend.GoBackend(context)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
@Singleton
|
||||||
fun provideVpnService(
|
fun provideVpnService(
|
||||||
amneziaBackend: Provider<org.amnezia.awg.backend.Backend>,
|
@Userspace userspaceBackend: Backend,
|
||||||
@Userspace userspaceBackend: Provider<Backend>,
|
@Kernel kernelBackend: Backend,
|
||||||
@Kernel kernelBackend: Provider<Backend>,
|
appDataRepository: AppDataRepository
|
||||||
appDataRepository: AppDataRepository,
|
|
||||||
@ApplicationScope applicationScope: CoroutineScope,
|
|
||||||
@IoDispatcher ioDispatcher: CoroutineDispatcher
|
|
||||||
): VpnService {
|
): VpnService {
|
||||||
return WireGuardTunnel(
|
return WireGuardTunnel(userspaceBackend, kernelBackend, appDataRepository)
|
||||||
amneziaBackend,
|
|
||||||
userspaceBackend,
|
|
||||||
kernelBackend,
|
|
||||||
appDataRepository,
|
|
||||||
applicationScope,
|
|
||||||
ioDispatcher,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
@Singleton
|
||||||
fun provideServiceManager(
|
fun provideServiceManager(appDataRepository: AppDataRepository): ServiceManager {
|
||||||
appDataRepository: AppDataRepository,
|
return ServiceManager(appDataRepository)
|
||||||
@IoDispatcher ioDispatcher: CoroutineDispatcher
|
|
||||||
): ServiceManager {
|
|
||||||
return ServiceManager(appDataRepository, ioDispatcher)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
-4
@@ -2,10 +2,6 @@ package com.zaneschepke.wireguardautotunnel.module
|
|||||||
|
|
||||||
import javax.inject.Qualifier
|
import javax.inject.Qualifier
|
||||||
|
|
||||||
@Qualifier
|
|
||||||
@Retention(AnnotationRetention.BINARY)
|
|
||||||
annotation class Kernel
|
|
||||||
|
|
||||||
@Qualifier
|
@Qualifier
|
||||||
@Retention(AnnotationRetention.BINARY)
|
@Retention(AnnotationRetention.BINARY)
|
||||||
annotation class Userspace
|
annotation class Userspace
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.module
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import com.zaneschepke.wireguardautotunnel.util.FileUtils
|
|
||||||
import dagger.Module
|
|
||||||
import dagger.Provides
|
|
||||||
import dagger.hilt.InstallIn
|
|
||||||
import dagger.hilt.android.components.ViewModelComponent
|
|
||||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
|
||||||
import dagger.hilt.android.scopes.ViewModelScoped
|
|
||||||
import kotlinx.coroutines.CoroutineDispatcher
|
|
||||||
|
|
||||||
@Module
|
|
||||||
@InstallIn(ViewModelComponent::class)
|
|
||||||
class ViewModelModule {
|
|
||||||
|
|
||||||
@ViewModelScoped
|
|
||||||
@Provides
|
|
||||||
fun provideFileUtils(
|
|
||||||
@ApplicationContext context: Context,
|
|
||||||
@IoDispatcher ioDispatcher: CoroutineDispatcher
|
|
||||||
): FileUtils {
|
|
||||||
return FileUtils(context, ioDispatcher)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -4,11 +4,9 @@ import android.content.BroadcastReceiver
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
|
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
|
||||||
import com.zaneschepke.wireguardautotunnel.module.ApplicationScope
|
|
||||||
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
|
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
|
||||||
|
import com.zaneschepke.wireguardautotunnel.util.goAsync
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@@ -21,36 +19,27 @@ class BootReceiver : BroadcastReceiver() {
|
|||||||
@Inject
|
@Inject
|
||||||
lateinit var serviceManager: ServiceManager
|
lateinit var serviceManager: ServiceManager
|
||||||
|
|
||||||
@Inject
|
override fun onReceive(context: Context?, intent: Intent?) = goAsync {
|
||||||
@ApplicationScope
|
if (Intent.ACTION_BOOT_COMPLETED != intent?.action) return@goAsync
|
||||||
lateinit var applicationScope: CoroutineScope
|
|
||||||
|
|
||||||
override fun onReceive(context: Context?, intent: Intent?) {
|
|
||||||
if (Intent.ACTION_BOOT_COMPLETED != intent?.action) return
|
|
||||||
context?.run {
|
context?.run {
|
||||||
applicationScope.launch {
|
val settings = appDataRepository.settings.getSettings()
|
||||||
val settings = appDataRepository.settings.getSettings()
|
if (settings.isAutoTunnelEnabled) {
|
||||||
if(settings.isRestoreOnBootEnabled) {
|
Timber.i("Starting watcher service from boot")
|
||||||
if (settings.isAutoTunnelEnabled) {
|
serviceManager.startWatcherServiceForeground(context)
|
||||||
Timber.i("Starting watcher service from boot")
|
}
|
||||||
serviceManager.startWatcherServiceForeground(context)
|
if (appDataRepository.appState.isTunnelRunningFromManualStart()) {
|
||||||
}
|
appDataRepository.appState.getActiveTunnelId()?.let {
|
||||||
if (appDataRepository.appState.isTunnelRunningFromManualStart()) {
|
Timber.i("Starting tunnel that was active before reboot")
|
||||||
appDataRepository.appState.getActiveTunnelId()?.let {
|
serviceManager.startVpnServiceForeground(
|
||||||
Timber.i("Starting tunnel that was active before reboot")
|
context,
|
||||||
serviceManager.startVpnServiceForeground(
|
appDataRepository.tunnels.getById(it)?.id,
|
||||||
context,
|
)
|
||||||
appDataRepository.tunnels.getById(it)?.id,
|
|
||||||
)
|
|
||||||
return@launch
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (settings.isAlwaysOnVpnEnabled) {
|
|
||||||
Timber.i("Starting vpn service from boot AOVPN")
|
|
||||||
serviceManager.startVpnServiceForeground(context)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (settings.isAlwaysOnVpnEnabled) {
|
||||||
|
Timber.i("Starting vpn service from boot AOVPN")
|
||||||
|
serviceManager.startVpnServiceForeground(context)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+11
-19
@@ -4,14 +4,12 @@ import android.content.BroadcastReceiver
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import com.zaneschepke.wireguardautotunnel.data.repository.SettingsRepository
|
import com.zaneschepke.wireguardautotunnel.data.repository.SettingsRepository
|
||||||
import com.zaneschepke.wireguardautotunnel.module.ApplicationScope
|
|
||||||
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
|
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
|
||||||
import com.zaneschepke.wireguardautotunnel.util.Constants
|
import com.zaneschepke.wireguardautotunnel.util.Constants
|
||||||
|
import com.zaneschepke.wireguardautotunnel.util.goAsync
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.cancel
|
import kotlinx.coroutines.cancel
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@@ -23,22 +21,16 @@ class NotificationActionReceiver : BroadcastReceiver() {
|
|||||||
@Inject
|
@Inject
|
||||||
lateinit var serviceManager: ServiceManager
|
lateinit var serviceManager: ServiceManager
|
||||||
|
|
||||||
@Inject
|
override fun onReceive(context: Context, intent: Intent?) = goAsync {
|
||||||
@ApplicationScope
|
try {
|
||||||
lateinit var applicationScope: CoroutineScope
|
//TODO fix for manual start changes when enabled
|
||||||
|
serviceManager.stopVpnServiceForeground(context)
|
||||||
override fun onReceive(context: Context, intent: Intent?) {
|
delay(Constants.TOGGLE_TUNNEL_DELAY)
|
||||||
applicationScope.launch {
|
serviceManager.startVpnServiceForeground(context)
|
||||||
try {
|
} catch (e: Exception) {
|
||||||
//TODO fix for manual start changes when enabled
|
Timber.e(e)
|
||||||
serviceManager.stopVpnServiceForeground(context)
|
} finally {
|
||||||
delay(Constants.TOGGLE_TUNNEL_DELAY)
|
cancel()
|
||||||
serviceManager.startVpnServiceForeground(context)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Timber.e(e)
|
|
||||||
} finally {
|
|
||||||
cancel()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
-1
@@ -24,7 +24,6 @@ open class ForegroundService : LifecycleService() {
|
|||||||
when (action) {
|
when (action) {
|
||||||
Action.START.name,
|
Action.START.name,
|
||||||
Action.START_FOREGROUND.name -> startService(intent.extras)
|
Action.START_FOREGROUND.name -> startService(intent.extras)
|
||||||
|
|
||||||
Action.STOP.name, Action.STOP_FOREGROUND.name -> stopService()
|
Action.STOP.name, Action.STOP_FOREGROUND.name -> stopService()
|
||||||
Constants.ALWAYS_ON_VPN_ACTION -> {
|
Constants.ALWAYS_ON_VPN_ACTION -> {
|
||||||
Timber.i("Always-on VPN starting service")
|
Timber.i("Always-on VPN starting service")
|
||||||
|
|||||||
+23
-38
@@ -4,16 +4,10 @@ import android.app.Service
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
|
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
|
||||||
import com.zaneschepke.wireguardautotunnel.module.IoDispatcher
|
|
||||||
import com.zaneschepke.wireguardautotunnel.util.Constants
|
import com.zaneschepke.wireguardautotunnel.util.Constants
|
||||||
import kotlinx.coroutines.CoroutineDispatcher
|
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
|
||||||
class ServiceManager(
|
class ServiceManager(private val appDataRepository: AppDataRepository) {
|
||||||
private val appDataRepository: AppDataRepository,
|
|
||||||
@IoDispatcher private val ioDispatcher: CoroutineDispatcher
|
|
||||||
) {
|
|
||||||
|
|
||||||
private fun <T : Service> actionOnService(
|
private fun <T : Service> actionOnService(
|
||||||
action: Action,
|
action: Action,
|
||||||
@@ -29,10 +23,7 @@ class ServiceManager(
|
|||||||
intent.component?.javaClass
|
intent.component?.javaClass
|
||||||
try {
|
try {
|
||||||
when (action) {
|
when (action) {
|
||||||
Action.START_FOREGROUND, Action.STOP_FOREGROUND -> context.startForegroundService(
|
Action.START_FOREGROUND, Action.STOP_FOREGROUND -> context.startForegroundService(intent)
|
||||||
intent,
|
|
||||||
)
|
|
||||||
|
|
||||||
Action.START, Action.STOP -> context.startService(intent)
|
Action.START, Action.STOP -> context.startService(intent)
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
@@ -55,27 +46,23 @@ class ServiceManager(
|
|||||||
}
|
}
|
||||||
|
|
||||||
suspend fun stopVpnServiceForeground(context: Context, isManualStop: Boolean = false) {
|
suspend fun stopVpnServiceForeground(context: Context, isManualStop: Boolean = false) {
|
||||||
withContext(ioDispatcher) {
|
if (isManualStop) onManualStop()
|
||||||
if (isManualStop) onManualStop()
|
Timber.i("Stopping vpn service")
|
||||||
Timber.i("Stopping vpn service")
|
actionOnService(
|
||||||
actionOnService(
|
Action.STOP_FOREGROUND,
|
||||||
Action.STOP_FOREGROUND,
|
context,
|
||||||
context,
|
WireGuardTunnelService::class.java,
|
||||||
WireGuardTunnelService::class.java,
|
)
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun stopVpnService(context: Context, isManualStop: Boolean = false) {
|
suspend fun stopVpnService(context: Context, isManualStop: Boolean = false) {
|
||||||
withContext(ioDispatcher) {
|
if (isManualStop) onManualStop()
|
||||||
if (isManualStop) onManualStop()
|
Timber.i("Stopping vpn service")
|
||||||
Timber.i("Stopping vpn service")
|
actionOnService(
|
||||||
actionOnService(
|
Action.STOP,
|
||||||
Action.STOP,
|
context,
|
||||||
context,
|
WireGuardTunnelService::class.java,
|
||||||
WireGuardTunnelService::class.java,
|
)
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun onManualStop() {
|
private suspend fun onManualStop() {
|
||||||
@@ -93,15 +80,13 @@ class ServiceManager(
|
|||||||
tunnelId: Int? = null,
|
tunnelId: Int? = null,
|
||||||
isManualStart: Boolean = false
|
isManualStart: Boolean = false
|
||||||
) {
|
) {
|
||||||
withContext(ioDispatcher) {
|
if (isManualStart) onManualStart(tunnelId)
|
||||||
if (isManualStart) onManualStart(tunnelId)
|
actionOnService(
|
||||||
actionOnService(
|
Action.START_FOREGROUND,
|
||||||
Action.START_FOREGROUND,
|
context,
|
||||||
context,
|
WireGuardTunnelService::class.java,
|
||||||
WireGuardTunnelService::class.java,
|
tunnelId?.let { mapOf(Constants.TUNNEL_EXTRA_KEY to it) },
|
||||||
tunnelId?.let { mapOf(Constants.TUNNEL_EXTRA_KEY to it) },
|
)
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun startWatcherServiceForeground(
|
fun startWatcherServiceForeground(
|
||||||
|
|||||||
+36
-8
@@ -1,56 +1,84 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.service.foreground
|
package com.zaneschepke.wireguardautotunnel.service.foreground
|
||||||
|
|
||||||
import com.zaneschepke.wireguardautotunnel.data.domain.Settings
|
import com.zaneschepke.wireguardautotunnel.data.model.Settings
|
||||||
|
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
|
||||||
|
import org.amnezia.awg.backend.Tunnel
|
||||||
|
|
||||||
data class WatcherState(
|
data class WatcherState(
|
||||||
val isWifiConnected: Boolean = false,
|
val isWifiConnected: Boolean = false,
|
||||||
|
val config: TunnelConfig? = null,
|
||||||
|
val vpnStatus: Tunnel.State = Tunnel.State.DOWN,
|
||||||
val isEthernetConnected: Boolean = false,
|
val isEthernetConnected: Boolean = false,
|
||||||
val isMobileDataConnected: Boolean = false,
|
val isMobileDataConnected: Boolean = false,
|
||||||
val currentNetworkSSID: String = "",
|
val currentNetworkSSID: String = "",
|
||||||
val settings: Settings = Settings()
|
val settings: Settings = Settings()
|
||||||
) {
|
) {
|
||||||
|
|
||||||
|
private fun isVpnConnected() = vpnStatus == Tunnel.State.UP
|
||||||
fun isEthernetConditionMet(): Boolean {
|
fun isEthernetConditionMet(): Boolean {
|
||||||
return (isEthernetConnected &&
|
return (isEthernetConnected &&
|
||||||
settings.isTunnelOnEthernetEnabled)
|
settings.isTunnelOnEthernetEnabled &&
|
||||||
|
!isVpnConnected())
|
||||||
}
|
}
|
||||||
|
|
||||||
fun isMobileDataConditionMet(): Boolean {
|
fun isMobileDataConditionMet(): Boolean {
|
||||||
return (!isEthernetConnected &&
|
return (!isEthernetConnected &&
|
||||||
settings.isTunnelOnMobileDataEnabled &&
|
settings.isTunnelOnMobileDataEnabled &&
|
||||||
!isWifiConnected &&
|
!isWifiConnected &&
|
||||||
isMobileDataConnected)
|
isMobileDataConnected &&
|
||||||
|
!isVpnConnected())
|
||||||
|
}
|
||||||
|
|
||||||
|
fun isTunnelNotMobileDataPreferredConditionMet(): Boolean {
|
||||||
|
return (!isEthernetConnected &&
|
||||||
|
settings.isTunnelOnMobileDataEnabled &&
|
||||||
|
!isWifiConnected &&
|
||||||
|
isMobileDataConnected &&
|
||||||
|
config?.isMobileDataTunnel == false && isVpnConnected())
|
||||||
}
|
}
|
||||||
|
|
||||||
fun isTunnelOffOnMobileDataConditionMet(): Boolean {
|
fun isTunnelOffOnMobileDataConditionMet(): Boolean {
|
||||||
return (!isEthernetConnected &&
|
return (!isEthernetConnected &&
|
||||||
!settings.isTunnelOnMobileDataEnabled &&
|
!settings.isTunnelOnMobileDataEnabled &&
|
||||||
isMobileDataConnected &&
|
isMobileDataConnected &&
|
||||||
!isWifiConnected)
|
!isWifiConnected &&
|
||||||
|
isVpnConnected())
|
||||||
}
|
}
|
||||||
|
|
||||||
fun isUntrustedWifiConditionMet(): Boolean {
|
fun isUntrustedWifiConditionMet(): Boolean {
|
||||||
return (!isEthernetConnected &&
|
return (!isEthernetConnected &&
|
||||||
isWifiConnected &&
|
isWifiConnected &&
|
||||||
!settings.trustedNetworkSSIDs.contains(currentNetworkSSID) &&
|
!settings.trustedNetworkSSIDs.contains(currentNetworkSSID) &&
|
||||||
settings.isTunnelOnWifiEnabled)
|
settings.isTunnelOnWifiEnabled
|
||||||
|
&& !isVpnConnected())
|
||||||
|
}
|
||||||
|
|
||||||
|
fun isTunnelNotWifiNamePreferredMet(ssid: String): Boolean {
|
||||||
|
return (!isEthernetConnected &&
|
||||||
|
isWifiConnected &&
|
||||||
|
!settings.trustedNetworkSSIDs.contains(currentNetworkSSID) &&
|
||||||
|
settings.isTunnelOnWifiEnabled && config?.tunnelNetworks?.contains(ssid) == false && isVpnConnected())
|
||||||
}
|
}
|
||||||
|
|
||||||
fun isTrustedWifiConditionMet(): Boolean {
|
fun isTrustedWifiConditionMet(): Boolean {
|
||||||
return (!isEthernetConnected &&
|
return (!isEthernetConnected &&
|
||||||
(isWifiConnected &&
|
(isWifiConnected &&
|
||||||
settings.trustedNetworkSSIDs.contains(currentNetworkSSID)))
|
settings.trustedNetworkSSIDs.contains(currentNetworkSSID)) &&
|
||||||
|
(isVpnConnected()))
|
||||||
}
|
}
|
||||||
|
|
||||||
fun isTunnelOffOnWifiConditionMet(): Boolean {
|
fun isTunnelOffOnWifiConditionMet(): Boolean {
|
||||||
return (!isEthernetConnected &&
|
return (!isEthernetConnected &&
|
||||||
(isWifiConnected &&
|
(isWifiConnected &&
|
||||||
!settings.isTunnelOnWifiEnabled))
|
!settings.isTunnelOnWifiEnabled &&
|
||||||
|
(isVpnConnected())))
|
||||||
}
|
}
|
||||||
|
|
||||||
fun isTunnelOffOnNoConnectivityMet(): Boolean {
|
fun isTunnelOffOnNoConnectivityMet(): Boolean {
|
||||||
return (!isEthernetConnected &&
|
return (!isEthernetConnected &&
|
||||||
!isWifiConnected &&
|
!isWifiConnected &&
|
||||||
!isMobileDataConnected)
|
!isMobileDataConnected &&
|
||||||
|
(isVpnConnected()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+192
-218
@@ -6,29 +6,25 @@ import android.os.PowerManager
|
|||||||
import androidx.core.app.ServiceCompat
|
import androidx.core.app.ServiceCompat
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import com.zaneschepke.wireguardautotunnel.R
|
import com.zaneschepke.wireguardautotunnel.R
|
||||||
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
|
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
|
||||||
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
|
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
|
||||||
import com.zaneschepke.wireguardautotunnel.module.IoDispatcher
|
|
||||||
import com.zaneschepke.wireguardautotunnel.module.MainImmediateDispatcher
|
|
||||||
import com.zaneschepke.wireguardautotunnel.service.network.EthernetService
|
import com.zaneschepke.wireguardautotunnel.service.network.EthernetService
|
||||||
import com.zaneschepke.wireguardautotunnel.service.network.MobileDataService
|
import com.zaneschepke.wireguardautotunnel.service.network.MobileDataService
|
||||||
import com.zaneschepke.wireguardautotunnel.service.network.NetworkService
|
import com.zaneschepke.wireguardautotunnel.service.network.NetworkService
|
||||||
import com.zaneschepke.wireguardautotunnel.service.network.NetworkStatus
|
import com.zaneschepke.wireguardautotunnel.service.network.NetworkStatus
|
||||||
import com.zaneschepke.wireguardautotunnel.service.network.WifiService
|
import com.zaneschepke.wireguardautotunnel.service.network.WifiService
|
||||||
import com.zaneschepke.wireguardautotunnel.service.notification.NotificationService
|
import com.zaneschepke.wireguardautotunnel.service.notification.NotificationService
|
||||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelState
|
|
||||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnService
|
import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnService
|
||||||
import com.zaneschepke.wireguardautotunnel.util.Constants
|
import com.zaneschepke.wireguardautotunnel.util.Constants
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import kotlinx.coroutines.CancellationException
|
import kotlinx.coroutines.CancellationException
|
||||||
import kotlinx.coroutines.CoroutineDispatcher
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.collectLatest
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
import kotlinx.coroutines.flow.update
|
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import org.amnezia.awg.backend.Tunnel
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import java.net.InetAddress
|
import java.net.InetAddress
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
@@ -59,14 +55,6 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
|
|||||||
@Inject
|
@Inject
|
||||||
lateinit var serviceManager: ServiceManager
|
lateinit var serviceManager: ServiceManager
|
||||||
|
|
||||||
@Inject
|
|
||||||
@IoDispatcher
|
|
||||||
lateinit var ioDispatcher: CoroutineDispatcher
|
|
||||||
|
|
||||||
@Inject
|
|
||||||
@MainImmediateDispatcher
|
|
||||||
lateinit var mainImmediateDispatcher: CoroutineDispatcher
|
|
||||||
|
|
||||||
private val networkEventsFlow = MutableStateFlow(WatcherState())
|
private val networkEventsFlow = MutableStateFlow(WatcherState())
|
||||||
|
|
||||||
private var watcherJob: Job? = null
|
private var watcherJob: Job? = null
|
||||||
@@ -76,7 +64,7 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
|
|||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
lifecycleScope.launch(mainImmediateDispatcher) {
|
lifecycleScope.launch(Dispatchers.Main) {
|
||||||
try {
|
try {
|
||||||
if (appDataRepository.settings.getSettings().isAutoTunnelPaused) {
|
if (appDataRepository.settings.getSettings().isAutoTunnelPaused) {
|
||||||
launchWatcherPausedNotification()
|
launchWatcherPausedNotification()
|
||||||
@@ -149,14 +137,14 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
|
|||||||
private fun cancelWatcherJob() {
|
private fun cancelWatcherJob() {
|
||||||
try {
|
try {
|
||||||
watcherJob?.cancel()
|
watcherJob?.cancel()
|
||||||
} catch (e: CancellationException) {
|
} catch (e : CancellationException) {
|
||||||
Timber.i("Watcher job cancelled")
|
Timber.i("Watcher job cancelled")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun startWatcherJob() {
|
private fun startWatcherJob() {
|
||||||
watcherJob =
|
watcherJob =
|
||||||
lifecycleScope.launch {
|
lifecycleScope.launch(Dispatchers.IO) {
|
||||||
val setting = appDataRepository.settings.getSettings()
|
val setting = appDataRepository.settings.getSettings()
|
||||||
launch {
|
launch {
|
||||||
Timber.i("Starting wifi watcher")
|
Timber.i("Starting wifi watcher")
|
||||||
@@ -174,6 +162,10 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
|
|||||||
watchForEthernetConnectivityChanges()
|
watchForEthernetConnectivityChanges()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
launch {
|
||||||
|
Timber.i("Starting vpn state watcher")
|
||||||
|
watchForVpnConnectivityChanges()
|
||||||
|
}
|
||||||
launch {
|
launch {
|
||||||
Timber.i("Starting settings watcher")
|
Timber.i("Starting settings watcher")
|
||||||
watchForSettingsChanges()
|
watchForSettingsChanges()
|
||||||
@@ -193,170 +185,160 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun watchForMobileDataConnectivityChanges() {
|
private suspend fun watchForMobileDataConnectivityChanges() {
|
||||||
withContext(ioDispatcher) {
|
mobileDataService.networkStatus.collect {
|
||||||
mobileDataService.networkStatus.collect { status ->
|
when (it) {
|
||||||
when (status) {
|
is NetworkStatus.Available -> {
|
||||||
is NetworkStatus.Available -> {
|
Timber.i("Gained Mobile data connection")
|
||||||
Timber.i("Gained Mobile data connection")
|
networkEventsFlow.value =
|
||||||
networkEventsFlow.update {
|
networkEventsFlow.value.copy(
|
||||||
it.copy(
|
isMobileDataConnected = true,
|
||||||
isMobileDataConnected = true,
|
)
|
||||||
)
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
is NetworkStatus.CapabilitiesChanged -> {
|
is NetworkStatus.CapabilitiesChanged -> {
|
||||||
networkEventsFlow.update {
|
networkEventsFlow.value =
|
||||||
it.copy(
|
networkEventsFlow.value.copy(
|
||||||
isMobileDataConnected = true,
|
isMobileDataConnected = true,
|
||||||
)
|
)
|
||||||
}
|
Timber.i("Mobile data capabilities changed")
|
||||||
Timber.i("Mobile data capabilities changed")
|
}
|
||||||
}
|
|
||||||
|
|
||||||
is NetworkStatus.Unavailable -> {
|
is NetworkStatus.Unavailable -> {
|
||||||
networkEventsFlow.update {
|
networkEventsFlow.value =
|
||||||
it.copy(
|
networkEventsFlow.value.copy(
|
||||||
isMobileDataConnected = false,
|
isMobileDataConnected = false,
|
||||||
)
|
)
|
||||||
}
|
Timber.i("Lost mobile data connection")
|
||||||
Timber.i("Lost mobile data connection")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun watchForPingFailure() {
|
private suspend fun watchForPingFailure() {
|
||||||
val context = this
|
try {
|
||||||
withContext(ioDispatcher) {
|
do {
|
||||||
try {
|
if (vpnService.vpnState.value.status == Tunnel.State.UP) {
|
||||||
do {
|
val tunnelConfig = vpnService.vpnState.value.tunnelConfig
|
||||||
if (vpnService.vpnState.value.status == TunnelState.UP) {
|
tunnelConfig?.let {
|
||||||
val tunnelConfig = vpnService.vpnState.value.tunnelConfig
|
val config = TunnelConfig.configFromQuick(it.wgQuick)
|
||||||
tunnelConfig?.let {
|
val results = config.peers.map { peer ->
|
||||||
val config = TunnelConfig.configFromWgQuick(it.wgQuick)
|
val host = if (peer.endpoint.isPresent &&
|
||||||
val results = config.peers.map { peer ->
|
peer.endpoint.get().resolved.isPresent)
|
||||||
val host = if (peer.endpoint.isPresent &&
|
peer.endpoint.get().resolved.get().host
|
||||||
peer.endpoint.get().resolved.isPresent)
|
else Constants.BACKUP_PING_HOST
|
||||||
peer.endpoint.get().resolved.get().host
|
Timber.i("Checking reachability of: $host")
|
||||||
else Constants.DEFAULT_PING_IP
|
val reachable = InetAddress.getByName(host)
|
||||||
Timber.i("Checking reachability of: $host")
|
.isReachable(Constants.PING_TIMEOUT.toInt())
|
||||||
val reachable = InetAddress.getByName(host)
|
Timber.i("Result: reachable - $reachable")
|
||||||
.isReachable(Constants.PING_TIMEOUT.toInt())
|
reachable
|
||||||
Timber.i("Result: reachable - $reachable")
|
}
|
||||||
reachable
|
if (results.contains(false)) {
|
||||||
}
|
Timber.i("Restarting VPN for ping failure")
|
||||||
if (results.contains(false)) {
|
serviceManager.stopVpnServiceForeground(this)
|
||||||
Timber.i("Restarting VPN for ping failure")
|
delay(Constants.VPN_RESTART_DELAY)
|
||||||
serviceManager.stopVpnServiceForeground(context)
|
serviceManager.startVpnServiceForeground(this)
|
||||||
delay(Constants.VPN_RESTART_DELAY)
|
delay(Constants.PING_COOLDOWN)
|
||||||
serviceManager.startVpnServiceForeground(context, it.id)
|
|
||||||
delay(Constants.PING_COOLDOWN)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
delay(Constants.PING_INTERVAL)
|
}
|
||||||
} while (true)
|
delay(Constants.PING_INTERVAL)
|
||||||
} catch (e: Exception) {
|
} while (true)
|
||||||
Timber.e(e)
|
} catch (e: Exception) {
|
||||||
}
|
Timber.e(e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun watchForSettingsChanges() {
|
private suspend fun watchForSettingsChanges() {
|
||||||
appDataRepository.settings.getSettingsFlow().collect { settings ->
|
appDataRepository.settings.getSettingsFlow().collect {
|
||||||
if (networkEventsFlow.value.settings.isAutoTunnelPaused != settings.isAutoTunnelPaused) {
|
if (networkEventsFlow.value.settings.isAutoTunnelPaused != it.isAutoTunnelPaused) {
|
||||||
when (settings.isAutoTunnelPaused) {
|
when (it.isAutoTunnelPaused) {
|
||||||
true -> launchWatcherPausedNotification()
|
true -> launchWatcherPausedNotification()
|
||||||
false -> launchWatcherNotification()
|
false -> launchWatcherNotification()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
networkEventsFlow.update {
|
networkEventsFlow.value =
|
||||||
it.copy(
|
networkEventsFlow.value.copy(
|
||||||
settings = settings,
|
settings = it,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun watchForVpnConnectivityChanges() {
|
||||||
|
vpnService.vpnState.collect {
|
||||||
|
networkEventsFlow.value =
|
||||||
|
networkEventsFlow.value.copy(
|
||||||
|
vpnStatus = it.status,
|
||||||
|
config = it.tunnelConfig,
|
||||||
)
|
)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun watchForEthernetConnectivityChanges() {
|
private suspend fun watchForEthernetConnectivityChanges() {
|
||||||
withContext(ioDispatcher) {
|
ethernetService.networkStatus.collect {
|
||||||
ethernetService.networkStatus.collect { status ->
|
when (it) {
|
||||||
when (status) {
|
is NetworkStatus.Available -> {
|
||||||
is NetworkStatus.Available -> {
|
Timber.i("Gained Ethernet connection")
|
||||||
Timber.i("Gained Ethernet connection")
|
networkEventsFlow.value =
|
||||||
networkEventsFlow.update {
|
networkEventsFlow.value.copy(
|
||||||
it.copy(
|
isEthernetConnected = true,
|
||||||
isEthernetConnected = true,
|
)
|
||||||
)
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
is NetworkStatus.CapabilitiesChanged -> {
|
is NetworkStatus.CapabilitiesChanged -> {
|
||||||
Timber.i("Ethernet capabilities changed")
|
Timber.i("Ethernet capabilities changed")
|
||||||
networkEventsFlow.update {
|
networkEventsFlow.value =
|
||||||
it.copy(
|
networkEventsFlow.value.copy(
|
||||||
isEthernetConnected = true,
|
isEthernetConnected = true,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
is NetworkStatus.Unavailable -> {
|
is NetworkStatus.Unavailable -> {
|
||||||
networkEventsFlow.update {
|
networkEventsFlow.value =
|
||||||
it.copy(
|
networkEventsFlow.value.copy(
|
||||||
isEthernetConnected = false,
|
isEthernetConnected = false,
|
||||||
)
|
)
|
||||||
}
|
Timber.i("Lost Ethernet connection")
|
||||||
Timber.i("Lost Ethernet connection")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun watchForWifiConnectivityChanges() {
|
private suspend fun watchForWifiConnectivityChanges() {
|
||||||
withContext(ioDispatcher) {
|
wifiService.networkStatus.collect {
|
||||||
wifiService.networkStatus.collect { status ->
|
when (it) {
|
||||||
when (status) {
|
is NetworkStatus.Available -> {
|
||||||
is NetworkStatus.Available -> {
|
Timber.i("Gained Wi-Fi connection")
|
||||||
Timber.i("Gained Wi-Fi connection")
|
networkEventsFlow.value =
|
||||||
networkEventsFlow.update {
|
networkEventsFlow.value.copy(
|
||||||
it.copy(
|
isWifiConnected = true,
|
||||||
isWifiConnected = true,
|
)
|
||||||
)
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
is NetworkStatus.CapabilitiesChanged -> {
|
is NetworkStatus.CapabilitiesChanged -> {
|
||||||
Timber.i("Wifi capabilities changed")
|
Timber.i("Wifi capabilities changed")
|
||||||
networkEventsFlow.update {
|
networkEventsFlow.value =
|
||||||
it.copy(
|
networkEventsFlow.value.copy(
|
||||||
isWifiConnected = true,
|
isWifiConnected = true,
|
||||||
|
)
|
||||||
|
val ssid = wifiService.getNetworkName(it.networkCapabilities)
|
||||||
|
ssid?.let {
|
||||||
|
if(it.contains(Constants.UNREADABLE_SSID)) {
|
||||||
|
Timber.w("SSID unreadable: missing permissions")
|
||||||
|
} else Timber.i("Detected valid SSID")
|
||||||
|
appDataRepository.appState.setCurrentSsid(ssid)
|
||||||
|
networkEventsFlow.value =
|
||||||
|
networkEventsFlow.value.copy(
|
||||||
|
currentNetworkSSID = ssid,
|
||||||
)
|
)
|
||||||
}
|
} ?: Timber.w("Failed to read ssid")
|
||||||
val ssid = wifiService.getNetworkName(status.networkCapabilities)
|
}
|
||||||
ssid?.let { name ->
|
|
||||||
if (name.contains(Constants.UNREADABLE_SSID)) {
|
|
||||||
Timber.w("SSID unreadable: missing permissions")
|
|
||||||
} else Timber.i("Detected valid SSID")
|
|
||||||
appDataRepository.appState.setCurrentSsid(name)
|
|
||||||
networkEventsFlow.update {
|
|
||||||
it.copy(
|
|
||||||
currentNetworkSSID = name,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} ?: Timber.w("Failed to read ssid")
|
|
||||||
}
|
|
||||||
|
|
||||||
is NetworkStatus.Unavailable -> {
|
is NetworkStatus.Unavailable -> {
|
||||||
networkEventsFlow.update {
|
networkEventsFlow.value =
|
||||||
it.copy(
|
networkEventsFlow.value.copy(
|
||||||
isWifiConnected = false,
|
isWifiConnected = false,
|
||||||
)
|
)
|
||||||
}
|
Timber.i("Lost Wi-Fi connection")
|
||||||
Timber.i("Lost Wi-Fi connection")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -370,84 +352,76 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
|
|||||||
return appDataRepository.tunnels.findByTunnelNetworksName(ssid).firstOrNull()
|
return appDataRepository.tunnels.findByTunnelNetworksName(ssid).firstOrNull()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun isTunnelDown(): Boolean {
|
|
||||||
return vpnService.vpnState.value.status == TunnelState.DOWN
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun manageVpn() {
|
private suspend fun manageVpn() {
|
||||||
val context = this
|
networkEventsFlow.collectLatest { watcherState ->
|
||||||
withContext(ioDispatcher) {
|
val autoTunnel = "Auto-tunnel watcher"
|
||||||
networkEventsFlow.collectLatest { watcherState ->
|
if (!watcherState.settings.isAutoTunnelPaused) {
|
||||||
val autoTunnel = "Auto-tunnel watcher"
|
//delay for rapid network state changes and then collect latest
|
||||||
if (!watcherState.settings.isAutoTunnelPaused) {
|
delay(Constants.WATCHER_COLLECTION_DELAY)
|
||||||
//delay for rapid network state changes and then collect latest
|
when {
|
||||||
delay(Constants.WATCHER_COLLECTION_DELAY)
|
watcherState.isEthernetConditionMet() -> {
|
||||||
val tunnelConfig = vpnService.vpnState.value.tunnelConfig
|
Timber.i("$autoTunnel - tunnel on on ethernet condition met")
|
||||||
when {
|
serviceManager.startVpnServiceForeground(this)
|
||||||
watcherState.isEthernetConditionMet() -> {
|
}
|
||||||
Timber.i("$autoTunnel - tunnel on on ethernet condition met")
|
|
||||||
if (isTunnelDown()) serviceManager.startVpnServiceForeground(context)
|
|
||||||
}
|
|
||||||
|
|
||||||
watcherState.isMobileDataConditionMet() -> {
|
watcherState.isMobileDataConditionMet() -> {
|
||||||
Timber.i("$autoTunnel - tunnel on mobile data condition met")
|
Timber.i("$autoTunnel - tunnel on on mobile data condition met")
|
||||||
val mobileDataTunnel = getMobileDataTunnel()
|
serviceManager.startVpnServiceForeground(this, getMobileDataTunnel()?.id)
|
||||||
val tunnel =
|
}
|
||||||
mobileDataTunnel ?: appDataRepository.getPrimaryOrFirstTunnel()
|
|
||||||
if (isTunnelDown() || tunnelConfig?.isMobileDataTunnel == false) {
|
watcherState.isTunnelNotMobileDataPreferredConditionMet() -> {
|
||||||
serviceManager.startVpnServiceForeground(
|
getMobileDataTunnel()?.let {
|
||||||
context,
|
Timber.i("$autoTunnel - tunnel connected on mobile data is not preferred condition met, switching to preferred")
|
||||||
tunnel?.id,
|
serviceManager.startVpnServiceForeground(
|
||||||
)
|
this,
|
||||||
|
getMobileDataTunnel()?.id,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
watcherState.isTunnelOffOnMobileDataConditionMet() -> {
|
||||||
|
Timber.i("$autoTunnel - tunnel off on mobile data met, turning vpn off")
|
||||||
|
serviceManager.stopVpnServiceForeground(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
watcherState.isTunnelNotWifiNamePreferredMet(watcherState.currentNetworkSSID) -> {
|
||||||
|
Timber.i("$autoTunnel - tunnel on ssid not associated with current tunnel condition met")
|
||||||
|
getSsidTunnel(watcherState.currentNetworkSSID)?.let {
|
||||||
|
Timber.i("Found tunnel associated with this SSID, bringing tunnel up")
|
||||||
|
serviceManager.startVpnServiceForeground(this, it.id)
|
||||||
|
} ?: suspend {
|
||||||
|
Timber.i("No tunnel associated with this SSID, using defaults")
|
||||||
|
if (appDataRepository.getPrimaryOrFirstTunnel()?.name != vpnService.name) {
|
||||||
|
serviceManager.startVpnServiceForeground(this)
|
||||||
}
|
}
|
||||||
}
|
}.invoke()
|
||||||
|
}
|
||||||
|
|
||||||
watcherState.isTunnelOffOnMobileDataConditionMet() -> {
|
watcherState.isUntrustedWifiConditionMet() -> {
|
||||||
Timber.i("$autoTunnel - tunnel off on mobile data met, turning vpn off")
|
Timber.i("$autoTunnel - tunnel on untrusted wifi condition met")
|
||||||
if (!isTunnelDown()) serviceManager.stopVpnServiceForeground(context)
|
serviceManager.startVpnServiceForeground(
|
||||||
}
|
this,
|
||||||
|
getSsidTunnel(watcherState.currentNetworkSSID)?.id,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
watcherState.isUntrustedWifiConditionMet() -> {
|
watcherState.isTrustedWifiConditionMet() -> {
|
||||||
if (tunnelConfig?.tunnelNetworks?.contains(watcherState.currentNetworkSSID) == false ||
|
Timber.i("$autoTunnel - tunnel off on trusted wifi condition met, turning vpn off")
|
||||||
tunnelConfig == null) {
|
serviceManager.stopVpnServiceForeground(this)
|
||||||
Timber.i("$autoTunnel - tunnel on ssid not associated with current tunnel condition met")
|
}
|
||||||
getSsidTunnel(watcherState.currentNetworkSSID)?.let {
|
|
||||||
Timber.i("Found tunnel associated with this SSID, bringing tunnel up: ${it.name}")
|
|
||||||
if (isTunnelDown() || tunnelConfig?.id != it.id) serviceManager.startVpnServiceForeground(
|
|
||||||
context,
|
|
||||||
it.id,
|
|
||||||
)
|
|
||||||
} ?: suspend {
|
|
||||||
Timber.i("No tunnel associated with this SSID, using defaults")
|
|
||||||
val default = appDataRepository.getPrimaryOrFirstTunnel()
|
|
||||||
if (default?.name != vpnService.name) {
|
|
||||||
default?.let {
|
|
||||||
serviceManager.startVpnServiceForeground(context, it.id)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
watcherState.isTunnelOffOnWifiConditionMet() -> {
|
||||||
}.invoke()
|
Timber.i("$autoTunnel - tunnel off on wifi condition met, turning vpn off")
|
||||||
}
|
serviceManager.stopVpnServiceForeground(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
watcherState.isTrustedWifiConditionMet() -> {
|
watcherState.isTunnelOffOnNoConnectivityMet() -> {
|
||||||
Timber.i("$autoTunnel - tunnel off on trusted wifi condition met, turning vpn off")
|
Timber.i("$autoTunnel - tunnel off on no connectivity met, turning vpn off")
|
||||||
if (!isTunnelDown()) serviceManager.stopVpnServiceForeground(context)
|
serviceManager.stopVpnServiceForeground(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
watcherState.isTunnelOffOnWifiConditionMet() -> {
|
else -> {
|
||||||
Timber.i("$autoTunnel - tunnel off on wifi condition met, turning vpn off")
|
Timber.i("$autoTunnel - no condition met")
|
||||||
if (!isTunnelDown()) serviceManager.stopVpnServiceForeground(context)
|
|
||||||
}
|
|
||||||
|
|
||||||
watcherState.isTunnelOffOnNoConnectivityMet() -> {
|
|
||||||
Timber.i("$autoTunnel - tunnel off on no connectivity met, turning vpn off")
|
|
||||||
if (!isTunnelDown()) serviceManager.stopVpnServiceForeground(context)
|
|
||||||
}
|
|
||||||
|
|
||||||
else -> {
|
|
||||||
Timber.i("$autoTunnel - no condition met")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+36
-49
@@ -7,23 +7,20 @@ import androidx.core.app.ServiceCompat
|
|||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import com.zaneschepke.wireguardautotunnel.R
|
import com.zaneschepke.wireguardautotunnel.R
|
||||||
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
|
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
|
||||||
import com.zaneschepke.wireguardautotunnel.module.IoDispatcher
|
|
||||||
import com.zaneschepke.wireguardautotunnel.module.MainImmediateDispatcher
|
|
||||||
import com.zaneschepke.wireguardautotunnel.receiver.NotificationActionReceiver
|
import com.zaneschepke.wireguardautotunnel.receiver.NotificationActionReceiver
|
||||||
import com.zaneschepke.wireguardautotunnel.service.notification.NotificationService
|
import com.zaneschepke.wireguardautotunnel.service.notification.NotificationService
|
||||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.HandshakeStatus
|
import com.zaneschepke.wireguardautotunnel.service.tunnel.HandshakeStatus
|
||||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelState
|
|
||||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnService
|
import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnService
|
||||||
import com.zaneschepke.wireguardautotunnel.util.Constants
|
import com.zaneschepke.wireguardautotunnel.util.Constants
|
||||||
import com.zaneschepke.wireguardautotunnel.util.handshakeStatus
|
import com.zaneschepke.wireguardautotunnel.util.handshakeStatus
|
||||||
import com.zaneschepke.wireguardautotunnel.util.mapPeerStats
|
import com.zaneschepke.wireguardautotunnel.util.mapPeerStats
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import kotlinx.coroutines.CancellationException
|
import kotlinx.coroutines.CancellationException
|
||||||
import kotlinx.coroutines.CoroutineDispatcher
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import org.amnezia.awg.backend.Tunnel
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@@ -40,21 +37,13 @@ class WireGuardTunnelService : ForegroundService() {
|
|||||||
@Inject
|
@Inject
|
||||||
lateinit var notificationService: NotificationService
|
lateinit var notificationService: NotificationService
|
||||||
|
|
||||||
@Inject
|
|
||||||
@MainImmediateDispatcher
|
|
||||||
lateinit var mainImmediateDispatcher: CoroutineDispatcher
|
|
||||||
|
|
||||||
@Inject
|
|
||||||
@IoDispatcher
|
|
||||||
lateinit var ioDispatcher: CoroutineDispatcher
|
|
||||||
|
|
||||||
private var job: Job? = null
|
private var job: Job? = null
|
||||||
|
|
||||||
private var didShowConnected = false
|
private var didShowConnected = false
|
||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
lifecycleScope.launch(mainImmediateDispatcher) {
|
lifecycleScope.launch(Dispatchers.Main) {
|
||||||
//TODO fix this to not launch if AOVPN
|
//TODO fix this to not launch if AOVPN
|
||||||
if (appDataRepository.tunnels.count() != 0) {
|
if (appDataRepository.tunnels.count() != 0) {
|
||||||
launchVpnNotification()
|
launchVpnNotification()
|
||||||
@@ -66,10 +55,10 @@ class WireGuardTunnelService : ForegroundService() {
|
|||||||
super.startService(extras)
|
super.startService(extras)
|
||||||
cancelJob()
|
cancelJob()
|
||||||
job =
|
job =
|
||||||
lifecycleScope.launch {
|
lifecycleScope.launch(Dispatchers.IO) {
|
||||||
launch {
|
launch {
|
||||||
val tunnelId = extras?.getInt(Constants.TUNNEL_EXTRA_KEY)
|
val tunnelId = extras?.getInt(Constants.TUNNEL_EXTRA_KEY)
|
||||||
if (vpnService.getState() == TunnelState.UP) {
|
if (vpnService.getState() == Tunnel.State.UP) {
|
||||||
vpnService.stopTunnel()
|
vpnService.stopTunnel()
|
||||||
}
|
}
|
||||||
vpnService.startTunnel(
|
vpnService.startTunnel(
|
||||||
@@ -86,41 +75,39 @@ class WireGuardTunnelService : ForegroundService() {
|
|||||||
|
|
||||||
//TODO improve tunnel notifications
|
//TODO improve tunnel notifications
|
||||||
private suspend fun handshakeNotifications() {
|
private suspend fun handshakeNotifications() {
|
||||||
withContext(ioDispatcher) {
|
var tunnelName: String? = null
|
||||||
var tunnelName: String? = null
|
vpnService.vpnState.collect { state ->
|
||||||
vpnService.vpnState.collect { state ->
|
state.statistics
|
||||||
state.statistics
|
?.mapPeerStats()
|
||||||
?.mapPeerStats()
|
?.map { it.value?.handshakeStatus() }
|
||||||
?.map { it.value?.handshakeStatus() }
|
.let { statuses ->
|
||||||
.let { statuses ->
|
when {
|
||||||
when {
|
statuses?.all { it == HandshakeStatus.HEALTHY } == true -> {
|
||||||
statuses?.all { it == HandshakeStatus.HEALTHY } == true -> {
|
if (!didShowConnected) {
|
||||||
if (!didShowConnected) {
|
delay(Constants.VPN_CONNECTED_NOTIFICATION_DELAY)
|
||||||
delay(Constants.VPN_CONNECTED_NOTIFICATION_DELAY)
|
tunnelName = state.tunnelConfig?.name
|
||||||
tunnelName = state.tunnelConfig?.name
|
launchVpnNotification(
|
||||||
launchVpnNotification(
|
getString(R.string.tunnel_start_title),
|
||||||
getString(R.string.tunnel_start_title),
|
"${getString(R.string.tunnel_start_text)} - $tunnelName",
|
||||||
"${getString(R.string.tunnel_start_text)} - $tunnelName",
|
)
|
||||||
)
|
didShowConnected = true
|
||||||
didShowConnected = true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
statuses?.any { it == HandshakeStatus.STALE } == true -> {}
|
|
||||||
statuses?.all { it == HandshakeStatus.NOT_STARTED } ==
|
|
||||||
true -> {
|
|
||||||
}
|
|
||||||
|
|
||||||
else -> {}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
statuses?.any { it == HandshakeStatus.STALE } == true -> {}
|
||||||
|
statuses?.all { it == HandshakeStatus.NOT_STARTED } ==
|
||||||
|
true -> {
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> {}
|
||||||
}
|
}
|
||||||
if (state.status == TunnelState.UP && state.tunnelConfig?.name != tunnelName) {
|
|
||||||
tunnelName = state.tunnelConfig?.name
|
|
||||||
launchVpnNotification(
|
|
||||||
getString(R.string.tunnel_start_title),
|
|
||||||
"${getString(R.string.tunnel_start_text)} - $tunnelName",
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
if (state.status == Tunnel.State.UP && state.tunnelConfig?.name != tunnelName) {
|
||||||
|
tunnelName = state.tunnelConfig?.name
|
||||||
|
launchVpnNotification(
|
||||||
|
getString(R.string.tunnel_start_title),
|
||||||
|
"${getString(R.string.tunnel_start_text)} - $tunnelName",
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -134,7 +121,7 @@ class WireGuardTunnelService : ForegroundService() {
|
|||||||
|
|
||||||
override fun stopService() {
|
override fun stopService() {
|
||||||
super.stopService()
|
super.stopService()
|
||||||
lifecycleScope.launch {
|
lifecycleScope.launch(Dispatchers.IO) {
|
||||||
vpnService.stopTunnel()
|
vpnService.stopTunnel()
|
||||||
didShowConnected = false
|
didShowConnected = false
|
||||||
}
|
}
|
||||||
@@ -194,7 +181,7 @@ class WireGuardTunnelService : ForegroundService() {
|
|||||||
private fun cancelJob() {
|
private fun cancelJob() {
|
||||||
try {
|
try {
|
||||||
job?.cancel()
|
job?.cancel()
|
||||||
} catch (e: CancellationException) {
|
} catch (e : CancellationException) {
|
||||||
Timber.i("Tunnel job cancelled")
|
Timber.i("Tunnel job cancelled")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-7
@@ -2,14 +2,14 @@ package com.zaneschepke.wireguardautotunnel.service.shortcut
|
|||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import androidx.activity.ComponentActivity
|
import androidx.activity.ComponentActivity
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
|
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
|
||||||
import com.zaneschepke.wireguardautotunnel.module.ApplicationScope
|
|
||||||
import com.zaneschepke.wireguardautotunnel.service.foreground.Action
|
import com.zaneschepke.wireguardautotunnel.service.foreground.Action
|
||||||
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
|
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
|
||||||
import com.zaneschepke.wireguardautotunnel.service.foreground.WireGuardConnectivityWatcherService
|
import com.zaneschepke.wireguardautotunnel.service.foreground.WireGuardConnectivityWatcherService
|
||||||
import com.zaneschepke.wireguardautotunnel.service.foreground.WireGuardTunnelService
|
import com.zaneschepke.wireguardautotunnel.service.foreground.WireGuardTunnelService
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@@ -22,13 +22,9 @@ class ShortcutsActivity : ComponentActivity() {
|
|||||||
@Inject
|
@Inject
|
||||||
lateinit var serviceManager: ServiceManager
|
lateinit var serviceManager: ServiceManager
|
||||||
|
|
||||||
@Inject
|
|
||||||
@ApplicationScope
|
|
||||||
lateinit var applicationScope: CoroutineScope
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
applicationScope.launch {
|
lifecycleScope.launch(Dispatchers.Main) {
|
||||||
val settings = appDataRepository.settings.getSettings()
|
val settings = appDataRepository.settings.getSettings()
|
||||||
if (settings.isShortcutsEnabled) {
|
if (settings.isShortcutsEnabled) {
|
||||||
when (intent.getStringExtra(CLASS_NAME_EXTRA_KEY)) {
|
when (intent.getStringExtra(CLASS_NAME_EXTRA_KEY)) {
|
||||||
|
|||||||
+30
-20
@@ -4,13 +4,14 @@ import android.os.Build
|
|||||||
import android.service.quicksettings.Tile
|
import android.service.quicksettings.Tile
|
||||||
import android.service.quicksettings.TileService
|
import android.service.quicksettings.TileService
|
||||||
import com.zaneschepke.wireguardautotunnel.R
|
import com.zaneschepke.wireguardautotunnel.R
|
||||||
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
|
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
|
||||||
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
|
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
|
||||||
import com.zaneschepke.wireguardautotunnel.module.ApplicationScope
|
|
||||||
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
|
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.cancel
|
import kotlinx.coroutines.cancel
|
||||||
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
@@ -24,30 +25,29 @@ class AutoTunnelControlTile : TileService() {
|
|||||||
@Inject
|
@Inject
|
||||||
lateinit var serviceManager: ServiceManager
|
lateinit var serviceManager: ServiceManager
|
||||||
|
|
||||||
@Inject
|
private val scope = CoroutineScope(Dispatchers.IO)
|
||||||
@ApplicationScope
|
|
||||||
lateinit var applicationScope: CoroutineScope
|
|
||||||
|
|
||||||
private var manualStartConfig: TunnelConfig? = null
|
private var manualStartConfig: TunnelConfig? = null
|
||||||
|
|
||||||
override fun onStartListening() {
|
override fun onStartListening() {
|
||||||
super.onStartListening()
|
super.onStartListening()
|
||||||
applicationScope.launch {
|
scope.launch {
|
||||||
val settings = appDataRepository.settings.getSettings()
|
appDataRepository.settings.getSettingsFlow().collectLatest {
|
||||||
when (settings.isAutoTunnelEnabled) {
|
when (it.isAutoTunnelEnabled) {
|
||||||
true -> {
|
true -> {
|
||||||
if (settings.isAutoTunnelPaused) {
|
if (it.isAutoTunnelPaused) {
|
||||||
setInactive()
|
setInactive()
|
||||||
setTileDescription(this@AutoTunnelControlTile.getString(R.string.paused))
|
setTileDescription(this@AutoTunnelControlTile.getString(R.string.paused))
|
||||||
} else {
|
} else {
|
||||||
setActive()
|
setActive()
|
||||||
setTileDescription(this@AutoTunnelControlTile.getString(R.string.active))
|
setTileDescription(this@AutoTunnelControlTile.getString(R.string.active))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
false -> {
|
false -> {
|
||||||
setTileDescription(this@AutoTunnelControlTile.getString(R.string.disabled))
|
setTileDescription(this@AutoTunnelControlTile.getString(R.string.disabled))
|
||||||
setUnavailable()
|
setUnavailable()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -58,10 +58,20 @@ class AutoTunnelControlTile : TileService() {
|
|||||||
onStartListening()
|
onStartListening()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onDestroy() {
|
||||||
|
super.onDestroy()
|
||||||
|
scope.cancel()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onTileRemoved() {
|
||||||
|
super.onTileRemoved()
|
||||||
|
scope.cancel()
|
||||||
|
}
|
||||||
|
|
||||||
override fun onClick() {
|
override fun onClick() {
|
||||||
super.onClick()
|
super.onClick()
|
||||||
unlockAndRun {
|
unlockAndRun {
|
||||||
applicationScope.launch {
|
scope.launch {
|
||||||
try {
|
try {
|
||||||
appDataRepository.toggleWatcherServicePause()
|
appDataRepository.toggleWatcherServicePause()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
|||||||
+34
-24
@@ -3,16 +3,16 @@ package com.zaneschepke.wireguardautotunnel.service.tile
|
|||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.service.quicksettings.Tile
|
import android.service.quicksettings.Tile
|
||||||
import android.service.quicksettings.TileService
|
import android.service.quicksettings.TileService
|
||||||
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
|
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
|
||||||
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
|
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
|
||||||
import com.zaneschepke.wireguardautotunnel.module.ApplicationScope
|
|
||||||
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
|
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
|
||||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelState
|
|
||||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnService
|
import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnService
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.cancel
|
import kotlinx.coroutines.cancel
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import org.amnezia.awg.backend.Tunnel
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@@ -28,37 +28,47 @@ class TunnelControlTile : TileService() {
|
|||||||
@Inject
|
@Inject
|
||||||
lateinit var serviceManager: ServiceManager
|
lateinit var serviceManager: ServiceManager
|
||||||
|
|
||||||
@Inject
|
private val scope = CoroutineScope(Dispatchers.IO)
|
||||||
@ApplicationScope
|
|
||||||
lateinit var applicationScope: CoroutineScope
|
|
||||||
|
|
||||||
private var manualStartConfig: TunnelConfig? = null
|
private var manualStartConfig: TunnelConfig? = null
|
||||||
|
|
||||||
override fun onStartListening() {
|
override fun onStartListening() {
|
||||||
super.onStartListening()
|
super.onStartListening()
|
||||||
Timber.d("On start listening called")
|
Timber.d("On start listening called")
|
||||||
applicationScope.launch {
|
scope.launch {
|
||||||
when (vpnService.getState()) {
|
vpnService.vpnState.collect { it ->
|
||||||
TunnelState.UP -> {
|
when (it.status) {
|
||||||
setActive()
|
Tunnel.State.UP -> {
|
||||||
setTileDescription(vpnService.name)
|
setActive()
|
||||||
}
|
it.tunnelConfig?.name?.let { name -> setTileDescription(name) }
|
||||||
|
}
|
||||||
|
|
||||||
TunnelState.DOWN -> {
|
Tunnel.State.DOWN -> {
|
||||||
setInactive()
|
setInactive()
|
||||||
val config = appDataRepository.getStartTunnelConfig()?.also { config ->
|
val config = appDataRepository.getStartTunnelConfig()?.also { config ->
|
||||||
manualStartConfig = config
|
manualStartConfig = config
|
||||||
} ?: appDataRepository.getPrimaryOrFirstTunnel()
|
} ?: appDataRepository.getPrimaryOrFirstTunnel()
|
||||||
config?.let {
|
config?.let {
|
||||||
setTileDescription(it.name)
|
setTileDescription(it.name)
|
||||||
} ?: setUnavailable()
|
} ?: setUnavailable()
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> setInactive()
|
else -> setInactive()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onDestroy() {
|
||||||
|
super.onDestroy()
|
||||||
|
scope.cancel()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onTileRemoved() {
|
||||||
|
super.onTileRemoved()
|
||||||
|
scope.cancel()
|
||||||
|
}
|
||||||
|
|
||||||
override fun onTileAdded() {
|
override fun onTileAdded() {
|
||||||
super.onTileAdded()
|
super.onTileAdded()
|
||||||
onStartListening()
|
onStartListening()
|
||||||
@@ -67,9 +77,9 @@ class TunnelControlTile : TileService() {
|
|||||||
override fun onClick() {
|
override fun onClick() {
|
||||||
super.onClick()
|
super.onClick()
|
||||||
unlockAndRun {
|
unlockAndRun {
|
||||||
applicationScope.launch {
|
scope.launch {
|
||||||
try {
|
try {
|
||||||
if (vpnService.getState() == TunnelState.UP) {
|
if (vpnService.getState() == Tunnel.State.UP) {
|
||||||
serviceManager.stopVpnServiceForeground(
|
serviceManager.stopVpnServiceForeground(
|
||||||
this@TunnelControlTile,
|
this@TunnelControlTile,
|
||||||
isManualStop = true,
|
isManualStop = true,
|
||||||
|
|||||||
@@ -1,43 +0,0 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.service.tunnel
|
|
||||||
|
|
||||||
import com.wireguard.android.backend.Tunnel
|
|
||||||
|
|
||||||
enum class TunnelState {
|
|
||||||
UP,
|
|
||||||
DOWN,
|
|
||||||
TOGGLE;
|
|
||||||
|
|
||||||
fun toWgState(): Tunnel.State {
|
|
||||||
return when (this) {
|
|
||||||
UP -> Tunnel.State.UP
|
|
||||||
DOWN -> Tunnel.State.DOWN
|
|
||||||
TOGGLE -> Tunnel.State.TOGGLE
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun toAmState(): org.amnezia.awg.backend.Tunnel.State {
|
|
||||||
return when (this) {
|
|
||||||
UP -> org.amnezia.awg.backend.Tunnel.State.UP
|
|
||||||
DOWN -> org.amnezia.awg.backend.Tunnel.State.DOWN
|
|
||||||
TOGGLE -> org.amnezia.awg.backend.Tunnel.State.TOGGLE
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
fun from(state: Tunnel.State): TunnelState {
|
|
||||||
return when (state) {
|
|
||||||
Tunnel.State.DOWN -> DOWN
|
|
||||||
Tunnel.State.TOGGLE -> TOGGLE
|
|
||||||
Tunnel.State.UP -> UP
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun from(state: org.amnezia.awg.backend.Tunnel.State): TunnelState {
|
|
||||||
return when (state) {
|
|
||||||
org.amnezia.awg.backend.Tunnel.State.DOWN -> DOWN
|
|
||||||
org.amnezia.awg.backend.Tunnel.State.TOGGLE -> TOGGLE
|
|
||||||
org.amnezia.awg.backend.Tunnel.State.UP -> UP
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,15 +1,15 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.service.tunnel
|
package com.zaneschepke.wireguardautotunnel.service.tunnel
|
||||||
|
|
||||||
import com.wireguard.android.backend.Tunnel
|
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
|
||||||
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
|
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import org.amnezia.awg.backend.Tunnel
|
||||||
|
|
||||||
interface VpnService : Tunnel, org.amnezia.awg.backend.Tunnel {
|
interface VpnService : Tunnel {
|
||||||
suspend fun startTunnel(tunnelConfig: TunnelConfig? = null): TunnelState
|
suspend fun startTunnel(tunnelConfig: TunnelConfig? = null): Tunnel.State
|
||||||
|
|
||||||
suspend fun stopTunnel()
|
suspend fun stopTunnel()
|
||||||
|
|
||||||
val vpnState: StateFlow<VpnState>
|
val vpnState: StateFlow<VpnState>
|
||||||
|
|
||||||
fun getState(): TunnelState
|
fun getState(): Tunnel.State
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.service.tunnel
|
package com.zaneschepke.wireguardautotunnel.service.tunnel
|
||||||
|
|
||||||
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
|
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
|
||||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.statistics.TunnelStatistics
|
import org.amnezia.awg.backend.Statistics
|
||||||
|
import org.amnezia.awg.backend.Tunnel
|
||||||
|
|
||||||
data class VpnState(
|
data class VpnState(
|
||||||
val status: TunnelState = TunnelState.DOWN,
|
val status: Tunnel.State = Tunnel.State.DOWN,
|
||||||
val tunnelConfig: TunnelConfig? = null,
|
val tunnelConfig: TunnelConfig? = null,
|
||||||
val statistics: TunnelStatistics? = null
|
val statistics: Statistics? = null
|
||||||
)
|
)
|
||||||
|
|||||||
+61
-138
@@ -1,132 +1,83 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.service.tunnel
|
package com.zaneschepke.wireguardautotunnel.service.tunnel
|
||||||
|
|
||||||
import com.wireguard.android.backend.Backend
|
|
||||||
import com.wireguard.android.backend.BackendException
|
|
||||||
import com.wireguard.android.backend.Tunnel.State
|
|
||||||
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
|
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
|
||||||
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
|
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
|
||||||
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
|
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
|
||||||
import com.zaneschepke.wireguardautotunnel.module.ApplicationScope
|
|
||||||
import com.zaneschepke.wireguardautotunnel.module.IoDispatcher
|
|
||||||
import com.zaneschepke.wireguardautotunnel.module.Kernel
|
import com.zaneschepke.wireguardautotunnel.module.Kernel
|
||||||
import com.zaneschepke.wireguardautotunnel.module.Userspace
|
import com.zaneschepke.wireguardautotunnel.module.Userspace
|
||||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.statistics.AmneziaStatistics
|
|
||||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.statistics.TunnelStatistics
|
|
||||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.statistics.WireGuardStatistics
|
|
||||||
import com.zaneschepke.wireguardautotunnel.util.Constants
|
import com.zaneschepke.wireguardautotunnel.util.Constants
|
||||||
import kotlinx.coroutines.CancellationException
|
import kotlinx.coroutines.CancellationException
|
||||||
import kotlinx.coroutines.CoroutineDispatcher
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import org.amnezia.awg.backend.Backend
|
||||||
|
import org.amnezia.awg.backend.BackendException
|
||||||
|
import org.amnezia.awg.backend.Statistics
|
||||||
import org.amnezia.awg.backend.Tunnel
|
import org.amnezia.awg.backend.Tunnel
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import javax.inject.Provider
|
|
||||||
|
|
||||||
class WireGuardTunnel
|
class WireGuardTunnel
|
||||||
@Inject
|
@Inject
|
||||||
constructor(
|
constructor(
|
||||||
private val userspaceAmneziaBackend: Provider<org.amnezia.awg.backend.Backend>,
|
@Userspace private val userspaceBackend: Backend,
|
||||||
@Userspace private val userspaceBackend: Provider<Backend>,
|
@Kernel private val kernelBackend: Backend,
|
||||||
@Kernel private val kernelBackend: Provider<Backend>,
|
|
||||||
private val appDataRepository: AppDataRepository,
|
private val appDataRepository: AppDataRepository,
|
||||||
@ApplicationScope private val applicationScope: CoroutineScope,
|
|
||||||
@IoDispatcher private val ioDispatcher: CoroutineDispatcher
|
|
||||||
) : VpnService {
|
) : VpnService {
|
||||||
private val _vpnState = MutableStateFlow(VpnState())
|
private val _vpnState = MutableStateFlow(VpnState())
|
||||||
override val vpnState: StateFlow<VpnState> = _vpnState.asStateFlow()
|
override val vpnState: StateFlow<VpnState> = _vpnState.asStateFlow()
|
||||||
|
|
||||||
|
private val scope = CoroutineScope(Dispatchers.IO)
|
||||||
|
|
||||||
private var statsJob: Job? = null
|
private var statsJob: Job? = null
|
||||||
|
|
||||||
private var backendIsWgUserspace = true
|
private var backend: Backend = userspaceBackend
|
||||||
|
|
||||||
private var backendIsAmneziaUserspace = false
|
private var backendIsUserspace = true
|
||||||
|
|
||||||
init {
|
init {
|
||||||
applicationScope.launch(ioDispatcher) {
|
scope.launch {
|
||||||
appDataRepository.settings.getSettingsFlow().collect {
|
appDataRepository.settings.getSettingsFlow().collect {
|
||||||
if (it.isKernelEnabled && (backendIsWgUserspace || backendIsAmneziaUserspace)) {
|
if (it.isKernelEnabled && backendIsUserspace) {
|
||||||
Timber.i("Setting kernel backend")
|
Timber.d("Setting kernel backend")
|
||||||
backendIsWgUserspace = false
|
backend = kernelBackend
|
||||||
backendIsAmneziaUserspace = false
|
backendIsUserspace = false
|
||||||
} else if (!it.isKernelEnabled && !it.isAmneziaEnabled && !backendIsWgUserspace) {
|
} else if (!it.isKernelEnabled && !backendIsUserspace) {
|
||||||
Timber.i("Setting WireGuard userspace backend")
|
Timber.d("Setting userspace backend")
|
||||||
backendIsWgUserspace = true
|
backend = userspaceBackend
|
||||||
backendIsAmneziaUserspace = false
|
backendIsUserspace = true
|
||||||
} else if (it.isAmneziaEnabled && !backendIsAmneziaUserspace) {
|
|
||||||
Timber.i("Setting Amnezia userspace backend")
|
|
||||||
backendIsAmneziaUserspace = true
|
|
||||||
backendIsWgUserspace = false
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setState(tunnelConfig: TunnelConfig?, tunnelState: TunnelState): TunnelState {
|
override suspend fun startTunnel(tunnelConfig: TunnelConfig?): Tunnel.State {
|
||||||
return if (backendIsAmneziaUserspace) {
|
return try {
|
||||||
Timber.i("Using Amnezia backend")
|
//TODO we need better error handling here
|
||||||
val config = tunnelConfig?.let {
|
val config = tunnelConfig ?: appDataRepository.getPrimaryOrFirstTunnel()
|
||||||
if (it.amQuick != "") TunnelConfig.configFromAmQuick(it.amQuick) else {
|
if (config != null) {
|
||||||
Timber.w("Using backwards compatible wg config, amnezia specific config not found.")
|
emitTunnelConfig(config)
|
||||||
TunnelConfig.configFromAmQuick(it.wgQuick)
|
val wgConfig = TunnelConfig.configFromQuick(config.wgQuick)
|
||||||
}
|
val state =
|
||||||
}
|
backend.setState(
|
||||||
val state =
|
this,
|
||||||
userspaceAmneziaBackend.get().setState(this, tunnelState.toAmState(), config)
|
Tunnel.State.UP,
|
||||||
TunnelState.from(state)
|
wgConfig,
|
||||||
} else {
|
)
|
||||||
Timber.i("Using Wg backend")
|
state
|
||||||
val wgConfig = tunnelConfig?.let { TunnelConfig.configFromWgQuick(it.wgQuick) }
|
} else throw Exception("No tunnels")
|
||||||
val state = backend().setState(
|
} catch (e: BackendException) {
|
||||||
this,
|
Timber.e("Failed to start tunnel with error: ${e.message}")
|
||||||
tunnelState.toWgState(),
|
Tunnel.State.DOWN
|
||||||
wgConfig,
|
|
||||||
)
|
|
||||||
TunnelState.from(state)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun startTunnel(tunnelConfig: TunnelConfig?): TunnelState {
|
private fun emitTunnelState(state: Tunnel.State) {
|
||||||
return withContext(ioDispatcher) {
|
|
||||||
try {
|
|
||||||
//TODO we need better error handling here
|
|
||||||
// need to bubble up these errors to the UI
|
|
||||||
val config = tunnelConfig ?: appDataRepository.getPrimaryOrFirstTunnel()
|
|
||||||
if (config != null) {
|
|
||||||
emitTunnelConfig(config)
|
|
||||||
setState(config, TunnelState.UP)
|
|
||||||
} else throw Exception("No tunnels")
|
|
||||||
} catch (e: BackendException) {
|
|
||||||
Timber.e("Failed to start tunnel with error: ${e.message}")
|
|
||||||
TunnelState.from(State.DOWN)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun backend(): Backend {
|
|
||||||
return when {
|
|
||||||
backendIsWgUserspace -> {
|
|
||||||
userspaceBackend.get()
|
|
||||||
}
|
|
||||||
|
|
||||||
!backendIsWgUserspace && !backendIsAmneziaUserspace -> {
|
|
||||||
kernelBackend.get()
|
|
||||||
}
|
|
||||||
|
|
||||||
else -> {
|
|
||||||
userspaceBackend.get()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun emitTunnelState(state: TunnelState) {
|
|
||||||
_vpnState.tryEmit(
|
_vpnState.tryEmit(
|
||||||
_vpnState.value.copy(
|
_vpnState.value.copy(
|
||||||
status = state,
|
status = state,
|
||||||
@@ -134,7 +85,7 @@ constructor(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun emitBackendStatistics(statistics: TunnelStatistics) {
|
private fun emitBackendStatistics(statistics: Statistics) {
|
||||||
_vpnState.tryEmit(
|
_vpnState.tryEmit(
|
||||||
_vpnState.value.copy(
|
_vpnState.value.copy(
|
||||||
statistics = statistics,
|
statistics = statistics,
|
||||||
@@ -150,73 +101,45 @@ constructor(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun resetVpnState() {
|
|
||||||
_vpnState.tryEmit(VpnState())
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun stopTunnel() {
|
override suspend fun stopTunnel() {
|
||||||
withContext(ioDispatcher) {
|
try {
|
||||||
try {
|
if (getState() == Tunnel.State.UP) {
|
||||||
if (getState() == TunnelState.UP) {
|
val state = backend.setState(this, Tunnel.State.DOWN, null)
|
||||||
val state = setState(null, TunnelState.DOWN)
|
emitTunnelState(state)
|
||||||
resetVpnState()
|
|
||||||
emitTunnelState(state)
|
|
||||||
}
|
|
||||||
} catch (e: BackendException) {
|
|
||||||
Timber.e("Failed to stop wireguard tunnel with error: ${e.message}")
|
|
||||||
} catch (e: org.amnezia.awg.backend.BackendException) {
|
|
||||||
Timber.e("Failed to stop amnezia tunnel with error: ${e.message}")
|
|
||||||
}
|
}
|
||||||
|
} catch (e: BackendException) {
|
||||||
|
Timber.e("Failed to stop tunnel with error: ${e.message}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getState(): TunnelState {
|
override fun getState(): Tunnel.State {
|
||||||
return if (backendIsAmneziaUserspace) TunnelState.from(
|
return backend.getState(this)
|
||||||
userspaceAmneziaBackend.get().getState(this),
|
|
||||||
)
|
|
||||||
else TunnelState.from(backend().getState(this))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getName(): String {
|
override fun getName(): String {
|
||||||
return _vpnState.value.tunnelConfig?.name ?: ""
|
return _vpnState.value.tunnelConfig?.name ?: ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onStateChange(state: Tunnel.State) {
|
||||||
override fun onStateChange(newState: Tunnel.State) {
|
val tunnel = this
|
||||||
handleStateChange(TunnelState.from(newState))
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun handleStateChange(state: TunnelState) {
|
|
||||||
emitTunnelState(state)
|
emitTunnelState(state)
|
||||||
WireGuardAutoTunnel.requestTunnelTileServiceStateUpdate()
|
WireGuardAutoTunnel.requestTunnelTileServiceStateUpdate(WireGuardAutoTunnel.instance)
|
||||||
if (state == TunnelState.UP) {
|
if (state == Tunnel.State.UP) {
|
||||||
statsJob = startTunnelStatisticsJob()
|
statsJob =
|
||||||
|
scope.launch {
|
||||||
|
while (true) {
|
||||||
|
val statistics = backend.getStatistics(tunnel)
|
||||||
|
emitBackendStatistics(statistics)
|
||||||
|
delay(Constants.VPN_STATISTIC_CHECK_INTERVAL)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (state == TunnelState.DOWN) {
|
if (state == Tunnel.State.DOWN) {
|
||||||
try {
|
try {
|
||||||
statsJob?.cancel()
|
statsJob?.cancel()
|
||||||
} catch (e: CancellationException) {
|
} catch (e : CancellationException) {
|
||||||
Timber.i("Stats job cancelled")
|
Timber.i("Stats job cancelled")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun startTunnelStatisticsJob() = applicationScope.launch(ioDispatcher) {
|
|
||||||
while (true) {
|
|
||||||
if (backendIsAmneziaUserspace) {
|
|
||||||
emitBackendStatistics(
|
|
||||||
AmneziaStatistics(
|
|
||||||
userspaceAmneziaBackend.get().getStatistics(this@WireGuardTunnel),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
emitBackendStatistics(WireGuardStatistics(backend().getStatistics(this@WireGuardTunnel)))
|
|
||||||
}
|
|
||||||
delay(Constants.VPN_STATISTIC_CHECK_INTERVAL)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onStateChange(state: State) {
|
|
||||||
handleStateChange(TunnelState.from(state))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
-34
@@ -1,34 +0,0 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.service.tunnel.statistics
|
|
||||||
|
|
||||||
import org.amnezia.awg.backend.Statistics
|
|
||||||
import org.amnezia.awg.crypto.Key
|
|
||||||
|
|
||||||
class AmneziaStatistics(private val statistics: Statistics) : TunnelStatistics() {
|
|
||||||
override fun peerStats(peer: Key): PeerStats? {
|
|
||||||
val key = Key.fromBase64(peer.toBase64())
|
|
||||||
val stats = statistics.peer(key)
|
|
||||||
return stats?.let {
|
|
||||||
PeerStats(
|
|
||||||
rxBytes = stats.rxBytes,
|
|
||||||
txBytes = stats.txBytes,
|
|
||||||
latestHandshakeEpochMillis = stats.latestHandshakeEpochMillis,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun isTunnelStale(): Boolean {
|
|
||||||
return statistics.isStale
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getPeers(): Array<Key> {
|
|
||||||
return statistics.peers()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun rx(): Long {
|
|
||||||
return statistics.totalRx()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun tx(): Long {
|
|
||||||
return statistics.totalTx()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-18
@@ -1,18 +0,0 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.service.tunnel.statistics
|
|
||||||
|
|
||||||
import org.amnezia.awg.crypto.Key
|
|
||||||
|
|
||||||
abstract class TunnelStatistics {
|
|
||||||
@JvmRecord
|
|
||||||
data class PeerStats(val rxBytes: Long, val txBytes: Long, val latestHandshakeEpochMillis: Long)
|
|
||||||
|
|
||||||
abstract fun peerStats(peer: Key): PeerStats?
|
|
||||||
|
|
||||||
abstract fun isTunnelStale(): Boolean
|
|
||||||
|
|
||||||
abstract fun getPeers(): Array<Key>
|
|
||||||
|
|
||||||
abstract fun rx(): Long
|
|
||||||
|
|
||||||
abstract fun tx(): Long
|
|
||||||
}
|
|
||||||
-36
@@ -1,36 +0,0 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.service.tunnel.statistics
|
|
||||||
|
|
||||||
import com.wireguard.android.backend.Statistics
|
|
||||||
import org.amnezia.awg.crypto.Key
|
|
||||||
|
|
||||||
class WireGuardStatistics(private val statistics: Statistics) : TunnelStatistics() {
|
|
||||||
override fun peerStats(peer: Key): PeerStats? {
|
|
||||||
val key = com.wireguard.crypto.Key.fromBase64(peer.toBase64())
|
|
||||||
val peerStats = statistics.peer(key)
|
|
||||||
return peerStats?.let {
|
|
||||||
PeerStats(
|
|
||||||
txBytes = peerStats.txBytes,
|
|
||||||
rxBytes = peerStats.rxBytes,
|
|
||||||
latestHandshakeEpochMillis = peerStats.latestHandshakeEpochMillis,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun isTunnelStale(): Boolean {
|
|
||||||
return statistics.isStale
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getPeers(): Array<Key> {
|
|
||||||
return statistics.peers().map {
|
|
||||||
Key.fromBase64(it.toBase64())
|
|
||||||
}.toTypedArray()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun rx(): Long {
|
|
||||||
return statistics.totalRx()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun tx(): Long {
|
|
||||||
return statistics.totalTx()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,25 +1,35 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.ui
|
package com.zaneschepke.wireguardautotunnel.ui
|
||||||
|
|
||||||
|
import android.app.Application
|
||||||
import android.content.ActivityNotFoundException
|
import android.content.ActivityNotFoundException
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.compose.runtime.mutableStateListOf
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import com.wireguard.android.backend.GoBackend
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import com.zaneschepke.logcatter.Logcatter
|
||||||
|
import com.zaneschepke.logcatter.model.LogMessage
|
||||||
import com.zaneschepke.wireguardautotunnel.R
|
import com.zaneschepke.wireguardautotunnel.R
|
||||||
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
|
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
|
||||||
import com.zaneschepke.wireguardautotunnel.util.Constants
|
import com.zaneschepke.wireguardautotunnel.util.Constants
|
||||||
|
import com.zaneschepke.wireguardautotunnel.util.FileUtils
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
import kotlinx.coroutines.flow.update
|
import kotlinx.coroutines.launch
|
||||||
|
import org.amnezia.awg.backend.GoBackend
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
import java.time.Instant
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
class AppViewModel
|
class AppViewModel
|
||||||
@Inject
|
@Inject
|
||||||
constructor() : ViewModel() {
|
constructor(
|
||||||
|
private val application: Application,
|
||||||
|
) : ViewModel() {
|
||||||
|
|
||||||
val vpnIntent: Intent? = GoBackend.VpnService.prepare(WireGuardAutoTunnel.instance)
|
val vpnIntent: Intent? = GoBackend.VpnService.prepare(WireGuardAutoTunnel.instance)
|
||||||
|
|
||||||
@@ -39,85 +49,100 @@ constructor() : ViewModel() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun requestPermissions() {
|
private fun requestPermissions() {
|
||||||
_appUiState.update {
|
_appUiState.value = _appUiState.value.copy(
|
||||||
it.copy(
|
requestPermissions = true,
|
||||||
requestPermissions = true,
|
)
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun permissionsRequested() {
|
fun permissionsRequested() {
|
||||||
_appUiState.update {
|
_appUiState.value = _appUiState.value.copy(
|
||||||
it.copy(
|
requestPermissions = false,
|
||||||
requestPermissions = false,
|
)
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun openWebPage(url: String, context: Context) {
|
fun openWebPage(url: String) {
|
||||||
try {
|
try {
|
||||||
val webpage: Uri = Uri.parse(url)
|
val webpage: Uri = Uri.parse(url)
|
||||||
val intent = Intent(Intent.ACTION_VIEW, webpage).apply {
|
val intent = Intent(Intent.ACTION_VIEW, webpage).apply {
|
||||||
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||||
}
|
}
|
||||||
context.startActivity(intent)
|
application.startActivity(intent)
|
||||||
} catch (e: ActivityNotFoundException) {
|
} catch (e: ActivityNotFoundException) {
|
||||||
Timber.e(e)
|
Timber.e(e)
|
||||||
showSnackbarMessage(context.getString(R.string.no_browser_detected))
|
showSnackbarMessage(application.getString(R.string.no_browser_detected))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onVpnPermissionAccepted() {
|
fun onVpnPermissionAccepted() {
|
||||||
_appUiState.update {
|
_appUiState.value = _appUiState.value.copy(
|
||||||
it.copy(
|
vpnPermissionAccepted = true,
|
||||||
vpnPermissionAccepted = true,
|
)
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun launchEmail(context: Context) {
|
fun launchEmail() {
|
||||||
try {
|
try {
|
||||||
val intent =
|
val intent =
|
||||||
Intent(Intent.ACTION_SENDTO).apply {
|
Intent(Intent.ACTION_SENDTO).apply {
|
||||||
type = Constants.EMAIL_MIME_TYPE
|
type = Constants.EMAIL_MIME_TYPE
|
||||||
putExtra(Intent.EXTRA_EMAIL, arrayOf(context.getString(R.string.my_email)))
|
putExtra(Intent.EXTRA_EMAIL, arrayOf(application.getString(R.string.my_email)))
|
||||||
putExtra(Intent.EXTRA_SUBJECT, context.getString(R.string.email_subject))
|
putExtra(Intent.EXTRA_SUBJECT, application.getString(R.string.email_subject))
|
||||||
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||||
}
|
}
|
||||||
context.startActivity(
|
application.startActivity(
|
||||||
Intent.createChooser(intent, context.getString(R.string.email_chooser)).apply {
|
Intent.createChooser(intent, application.getString(R.string.email_chooser)).apply {
|
||||||
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
} catch (e: ActivityNotFoundException) {
|
} catch (e: ActivityNotFoundException) {
|
||||||
Timber.e(e)
|
Timber.e(e)
|
||||||
showSnackbarMessage(context.getString(R.string.no_email_detected))
|
showSnackbarMessage(application.getString(R.string.no_email_detected))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun showSnackbarMessage(message: String) {
|
fun showSnackbarMessage(message: String) {
|
||||||
_appUiState.update {
|
_appUiState.value = _appUiState.value.copy(
|
||||||
it.copy(
|
snackbarMessage = message,
|
||||||
snackbarMessage = message,
|
snackbarMessageConsumed = false,
|
||||||
snackbarMessageConsumed = false,
|
)
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun snackbarMessageConsumed() {
|
fun snackbarMessageConsumed() {
|
||||||
_appUiState.update {
|
_appUiState.value = _appUiState.value.copy(
|
||||||
it.copy(
|
snackbarMessage = "",
|
||||||
snackbarMessage = "",
|
snackbarMessageConsumed = true,
|
||||||
snackbarMessageConsumed = true,
|
)
|
||||||
)
|
}
|
||||||
|
|
||||||
|
val logs = mutableStateListOf<LogMessage>()
|
||||||
|
|
||||||
|
fun readLogCatOutput() =
|
||||||
|
viewModelScope.launch(viewModelScope.coroutineContext + Dispatchers.IO) {
|
||||||
|
launch {
|
||||||
|
Logcatter.logs(callback = {
|
||||||
|
logs.add(it)
|
||||||
|
if (logs.size > Constants.LOG_BUFFER_SIZE) {
|
||||||
|
logs.removeRange(0, (logs.size - Constants.LOG_BUFFER_SIZE).toInt())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun clearLogs() {
|
||||||
|
logs.clear()
|
||||||
|
Logcatter.clear()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun saveLogsToFile() {
|
||||||
|
val fileName = "${Constants.BASE_LOG_FILE_NAME}-${Instant.now().epochSecond}.txt"
|
||||||
|
val content = logs.joinToString(separator = "\n")
|
||||||
|
FileUtils.saveFileToDownloads(application.applicationContext, content, fileName)
|
||||||
|
Toast.makeText(application, application.getString(R.string.logs_saved), Toast.LENGTH_SHORT)
|
||||||
|
.show()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setNotificationPermissionAccepted(accepted: Boolean) {
|
fun setNotificationPermissionAccepted(accepted: Boolean) {
|
||||||
_appUiState.update {
|
_appUiState.value = _appUiState.value.copy(
|
||||||
it.copy(
|
notificationPermissionAccepted = accepted,
|
||||||
notificationPermissionAccepted = accepted,
|
)
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
package com.zaneschepke.wireguardautotunnel.ui
|
||||||
|
|
||||||
|
import com.journeyapps.barcodescanner.CaptureActivity
|
||||||
|
|
||||||
|
class CaptureActivityPortrait : CaptureActivity()
|
||||||
@@ -32,24 +32,21 @@ import androidx.compose.ui.unit.dp
|
|||||||
import androidx.hilt.navigation.compose.hiltViewModel
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.navigation.NavType
|
|
||||||
import androidx.navigation.compose.NavHost
|
import androidx.navigation.compose.NavHost
|
||||||
import androidx.navigation.compose.composable
|
import androidx.navigation.compose.composable
|
||||||
import androidx.navigation.compose.currentBackStackEntryAsState
|
import androidx.navigation.compose.currentBackStackEntryAsState
|
||||||
import androidx.navigation.compose.rememberNavController
|
import androidx.navigation.compose.rememberNavController
|
||||||
import androidx.navigation.navArgument
|
|
||||||
import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
||||||
import com.google.accompanist.permissions.isGranted
|
import com.google.accompanist.permissions.isGranted
|
||||||
import com.google.accompanist.permissions.rememberPermissionState
|
import com.google.accompanist.permissions.rememberPermissionState
|
||||||
import com.zaneschepke.wireguardautotunnel.R
|
import com.zaneschepke.wireguardautotunnel.R
|
||||||
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
|
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
|
||||||
import com.zaneschepke.wireguardautotunnel.data.repository.AppStateRepository
|
import com.zaneschepke.wireguardautotunnel.data.datastore.DataStoreManager
|
||||||
import com.zaneschepke.wireguardautotunnel.data.repository.SettingsRepository
|
import com.zaneschepke.wireguardautotunnel.data.repository.SettingsRepository
|
||||||
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
|
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.BottomNavBar
|
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.BottomNavBar
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.common.prompt.CustomSnackBar
|
import com.zaneschepke.wireguardautotunnel.ui.common.prompt.CustomSnackBar
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.screens.config.ConfigScreen
|
import com.zaneschepke.wireguardautotunnel.ui.screens.config.ConfigScreen
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.screens.main.ConfigType
|
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.screens.main.MainScreen
|
import com.zaneschepke.wireguardautotunnel.ui.screens.main.MainScreen
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.screens.options.OptionsScreen
|
import com.zaneschepke.wireguardautotunnel.ui.screens.options.OptionsScreen
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.screens.pinlock.PinLockScreen
|
import com.zaneschepke.wireguardautotunnel.ui.screens.pinlock.PinLockScreen
|
||||||
@@ -61,13 +58,14 @@ import com.zaneschepke.wireguardautotunnel.util.StringValue
|
|||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import xyz.teamgravity.pin_lock_compose.PinManager
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class MainActivity : AppCompatActivity() {
|
class MainActivity : AppCompatActivity() {
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var appStateRepository: AppStateRepository
|
lateinit var dataStoreManager: DataStoreManager
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var settingsRepository: SettingsRepository
|
lateinit var settingsRepository: SettingsRepository
|
||||||
@@ -81,18 +79,17 @@ class MainActivity : AppCompatActivity() {
|
|||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
val isPinLockEnabled = intent.extras?.getBoolean(SplashActivity.IS_PIN_LOCK_ENABLED_KEY)
|
|
||||||
|
|
||||||
enableEdgeToEdge(navigationBarStyle = SystemBarStyle.dark(Color.Transparent.toArgb()))
|
enableEdgeToEdge(navigationBarStyle = SystemBarStyle.dark(Color.Transparent.toArgb()))
|
||||||
|
|
||||||
|
// load preferences into memory and init data
|
||||||
lifecycleScope.launch {
|
lifecycleScope.launch {
|
||||||
WireGuardAutoTunnel.requestTunnelTileServiceStateUpdate()
|
dataStoreManager.init()
|
||||||
|
WireGuardAutoTunnel.requestTunnelTileServiceStateUpdate(this@MainActivity)
|
||||||
val settings = settingsRepository.getSettings()
|
val settings = settingsRepository.getSettings()
|
||||||
if (settings.isAutoTunnelEnabled) {
|
if (settings.isAutoTunnelEnabled) {
|
||||||
serviceManager.startWatcherService(application.applicationContext)
|
serviceManager.startWatcherService(application.applicationContext)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setContent {
|
setContent {
|
||||||
val appViewModel = hiltViewModel<AppViewModel>()
|
val appViewModel = hiltViewModel<AppViewModel>()
|
||||||
val appUiState by appViewModel.appUiState.collectAsStateWithLifecycle()
|
val appUiState by appViewModel.appUiState.collectAsStateWithLifecycle()
|
||||||
@@ -141,11 +138,7 @@ class MainActivity : AppCompatActivity() {
|
|||||||
return@LaunchedEffect notificationPermissionState.launchPermissionRequest()
|
return@LaunchedEffect notificationPermissionState.launchPermissionRequest()
|
||||||
}
|
}
|
||||||
if (!appUiState.vpnPermissionAccepted) {
|
if (!appUiState.vpnPermissionAccepted) {
|
||||||
return@LaunchedEffect appViewModel.vpnIntent?.let {
|
return@LaunchedEffect vpnActivityResultState.launch(appViewModel.vpnIntent)
|
||||||
vpnActivityResultState.launch(
|
|
||||||
it,
|
|
||||||
)
|
|
||||||
}!!
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -155,6 +148,7 @@ class MainActivity : AppCompatActivity() {
|
|||||||
appViewModel.setNotificationPermissionAccepted(
|
appViewModel.setNotificationPermissionAccepted(
|
||||||
notificationPermissionState?.status?.isGranted ?: true,
|
notificationPermissionState?.status?.isGranted ?: true,
|
||||||
)
|
)
|
||||||
|
if (!WireGuardAutoTunnel.isRunningOnAndroidTv()) appViewModel.readLogCatOutput()
|
||||||
}
|
}
|
||||||
|
|
||||||
LaunchedEffect(appUiState.snackbarMessageConsumed) {
|
LaunchedEffect(appUiState.snackbarMessageConsumed) {
|
||||||
@@ -201,8 +195,10 @@ class MainActivity : AppCompatActivity() {
|
|||||||
) { padding ->
|
) { padding ->
|
||||||
NavHost(
|
NavHost(
|
||||||
navController,
|
navController,
|
||||||
startDestination = (if (isPinLockEnabled == true) Screen.Lock.route else Screen.Main.route),
|
startDestination =
|
||||||
modifier = Modifier
|
(if (PinManager.pinExists()) Screen.Lock.route else Screen.Main.route),
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
.padding(padding)
|
.padding(padding)
|
||||||
.fillMaxSize(),
|
.fillMaxSize(),
|
||||||
) {
|
) {
|
||||||
@@ -234,33 +230,16 @@ class MainActivity : AppCompatActivity() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
composable(Screen.Support.Logs.route) {
|
composable(Screen.Support.Logs.route) {
|
||||||
LogsScreen()
|
LogsScreen(appViewModel)
|
||||||
}
|
}
|
||||||
composable(
|
composable("${Screen.Config.route}/{id}") {
|
||||||
"${Screen.Config.route}/{id}?configType={configType}",
|
|
||||||
arguments =
|
|
||||||
listOf(
|
|
||||||
navArgument("id") {
|
|
||||||
type = NavType.StringType
|
|
||||||
defaultValue = "0"
|
|
||||||
},
|
|
||||||
navArgument("configType") {
|
|
||||||
type = NavType.StringType
|
|
||||||
defaultValue = ConfigType.WIREGUARD.name
|
|
||||||
},
|
|
||||||
),
|
|
||||||
) {
|
|
||||||
val id = it.arguments?.getString("id")
|
val id = it.arguments?.getString("id")
|
||||||
val configType = ConfigType.valueOf(
|
|
||||||
it.arguments?.getString("configType") ?: ConfigType.WIREGUARD.name,
|
|
||||||
)
|
|
||||||
if (!id.isNullOrBlank()) {
|
if (!id.isNullOrBlank()) {
|
||||||
ConfigScreen(
|
ConfigScreen(
|
||||||
navController = navController,
|
navController = navController,
|
||||||
tunnelId = id,
|
tunnelId = id,
|
||||||
appViewModel = appViewModel,
|
appViewModel = appViewModel,
|
||||||
focusRequester = focusRequester,
|
focusRequester = focusRequester,
|
||||||
configType = configType,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,9 +4,11 @@ import androidx.compose.material.icons.Icons
|
|||||||
import androidx.compose.material.icons.rounded.Home
|
import androidx.compose.material.icons.rounded.Home
|
||||||
import androidx.compose.material.icons.rounded.QuestionMark
|
import androidx.compose.material.icons.rounded.QuestionMark
|
||||||
import androidx.compose.material.icons.rounded.Settings
|
import androidx.compose.material.icons.rounded.Settings
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
import com.zaneschepke.wireguardautotunnel.R
|
import com.zaneschepke.wireguardautotunnel.R
|
||||||
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
|
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.BottomNavItem
|
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.BottomNavItem
|
||||||
|
import com.zaneschepke.wireguardautotunnel.util.StringValue
|
||||||
|
|
||||||
sealed class Screen(val route: String) {
|
sealed class Screen(val route: String) {
|
||||||
data object Main : Screen("main") {
|
data object Main : Screen("main") {
|
||||||
|
|||||||
@@ -1,68 +0,0 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.ui
|
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import android.content.Intent
|
|
||||||
import android.os.Build
|
|
||||||
import android.os.Bundle
|
|
||||||
import androidx.activity.ComponentActivity
|
|
||||||
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
|
|
||||||
import androidx.lifecycle.Lifecycle
|
|
||||||
import androidx.lifecycle.lifecycleScope
|
|
||||||
import androidx.lifecycle.repeatOnLifecycle
|
|
||||||
import com.zaneschepke.logcatter.LocalLogCollector
|
|
||||||
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
|
|
||||||
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel.Companion.isRunningOnAndroidTv
|
|
||||||
import com.zaneschepke.wireguardautotunnel.data.repository.AppStateRepository
|
|
||||||
import com.zaneschepke.wireguardautotunnel.module.ApplicationScope
|
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import xyz.teamgravity.pin_lock_compose.PinManager
|
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
@SuppressLint("CustomSplashScreen")
|
|
||||||
@AndroidEntryPoint
|
|
||||||
class SplashActivity : ComponentActivity() {
|
|
||||||
|
|
||||||
@Inject
|
|
||||||
lateinit var appStateRepository: AppStateRepository
|
|
||||||
|
|
||||||
@Inject
|
|
||||||
lateinit var localLogCollector: LocalLogCollector
|
|
||||||
|
|
||||||
@Inject
|
|
||||||
@ApplicationScope
|
|
||||||
lateinit var applicationScope: CoroutineScope
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
|
||||||
val splashScreen = installSplashScreen()
|
|
||||||
splashScreen.setKeepOnScreenCondition { true }
|
|
||||||
}
|
|
||||||
super.onCreate(savedInstanceState)
|
|
||||||
|
|
||||||
applicationScope.launch {
|
|
||||||
if (!isRunningOnAndroidTv()) localLogCollector.start()
|
|
||||||
}
|
|
||||||
|
|
||||||
lifecycleScope.launch {
|
|
||||||
repeatOnLifecycle(Lifecycle.State.CREATED) {
|
|
||||||
val pinLockEnabled = appStateRepository.isPinLockEnabled()
|
|
||||||
if (pinLockEnabled) {
|
|
||||||
PinManager.initialize(WireGuardAutoTunnel.instance)
|
|
||||||
}
|
|
||||||
|
|
||||||
val intent = Intent(this@SplashActivity, MainActivity::class.java).apply {
|
|
||||||
putExtra(IS_PIN_LOCK_ENABLED_KEY, pinLockEnabled)
|
|
||||||
}
|
|
||||||
startActivity(intent)
|
|
||||||
finish()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
const val IS_PIN_LOCK_ENABLED_KEY = "is_pin_lock_enabled"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -15,12 +15,11 @@ import androidx.compose.runtime.Composable
|
|||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.statistics.TunnelStatistics
|
|
||||||
import com.zaneschepke.wireguardautotunnel.util.NumberUtils
|
import com.zaneschepke.wireguardautotunnel.util.NumberUtils
|
||||||
import com.zaneschepke.wireguardautotunnel.util.toThreeDecimalPlaceString
|
import com.zaneschepke.wireguardautotunnel.util.toThreeDecimalPlaceString
|
||||||
|
import org.amnezia.awg.backend.Statistics
|
||||||
|
|
||||||
@OptIn(ExperimentalFoundationApi::class)
|
@OptIn(ExperimentalFoundationApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
@@ -31,7 +30,7 @@ fun RowListItem(
|
|||||||
onClick: () -> Unit,
|
onClick: () -> Unit,
|
||||||
rowButton: @Composable () -> Unit,
|
rowButton: @Composable () -> Unit,
|
||||||
expanded: Boolean,
|
expanded: Boolean,
|
||||||
statistics: TunnelStatistics?
|
statistics: Statistics?
|
||||||
) {
|
) {
|
||||||
Box(
|
Box(
|
||||||
modifier =
|
modifier =
|
||||||
@@ -53,15 +52,14 @@ fun RowListItem(
|
|||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
modifier = Modifier.fillMaxWidth(13 / 20f),
|
|
||||||
) {
|
) {
|
||||||
icon()
|
icon()
|
||||||
Text(text, maxLines = 1, overflow = TextOverflow.Ellipsis)
|
Text(text)
|
||||||
}
|
}
|
||||||
rowButton()
|
rowButton()
|
||||||
}
|
}
|
||||||
if (expanded) {
|
if (expanded) {
|
||||||
statistics?.getPeers()?.forEach {
|
statistics?.peers()?.forEach {
|
||||||
Row(
|
Row(
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier
|
||||||
@@ -71,9 +69,9 @@ fun RowListItem(
|
|||||||
horizontalArrangement = Arrangement.SpaceEvenly,
|
horizontalArrangement = Arrangement.SpaceEvenly,
|
||||||
) {
|
) {
|
||||||
//TODO change these to string resources
|
//TODO change these to string resources
|
||||||
val handshakeEpoch = statistics.peerStats(it)!!.latestHandshakeEpochMillis
|
val handshakeEpoch = statistics.peer(it)!!.latestHandshakeEpochMillis
|
||||||
val peerTx = statistics.peerStats(it)!!.txBytes
|
val peerTx = statistics.peer(it)!!.txBytes
|
||||||
val peerRx = statistics.peerStats(it)!!.rxBytes
|
val peerRx = statistics.peer(it)!!.rxBytes
|
||||||
val peerId = it.toBase64().subSequence(0, 3).toString() + "***"
|
val peerId = it.toBase64().subSequence(0, 3).toString() + "***"
|
||||||
val handshakeSec =
|
val handshakeSec =
|
||||||
NumberUtils.getSecondsBetweenTimestampAndNow(handshakeEpoch)
|
NumberUtils.getSecondsBetweenTimestampAndNow(handshakeEpoch)
|
||||||
|
|||||||
@@ -44,21 +44,19 @@ fun SearchBar(onQuery: (queryString: String) -> Unit) {
|
|||||||
onQuery(onQueryChanged)
|
onQuery(onQueryChanged)
|
||||||
},
|
},
|
||||||
leadingIcon = {
|
leadingIcon = {
|
||||||
val icon = Icons.Rounded.Search
|
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = icon,
|
imageVector = Icons.Rounded.Search,
|
||||||
tint = MaterialTheme.colorScheme.onBackground,
|
tint = MaterialTheme.colorScheme.onBackground,
|
||||||
contentDescription = icon.name,
|
contentDescription = stringResource(id = R.string.search_icon),
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
trailingIcon = {
|
trailingIcon = {
|
||||||
if (showClearIcon) {
|
if (showClearIcon) {
|
||||||
IconButton(onClick = { query = "" }) {
|
IconButton(onClick = { query = "" }) {
|
||||||
val icon = Icons.Rounded.Clear
|
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = icon,
|
imageVector = Icons.Rounded.Clear,
|
||||||
tint = MaterialTheme.colorScheme.onBackground,
|
tint = MaterialTheme.colorScheme.onBackground,
|
||||||
contentDescription = icon.name,
|
contentDescription = stringResource(id = R.string.clear_icon),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+6
-9
@@ -28,15 +28,12 @@ fun ConfigurationToggle(
|
|||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(label, textAlign = TextAlign.Start, modifier = Modifier
|
||||||
label, textAlign = TextAlign.Start,
|
.weight(
|
||||||
modifier = Modifier
|
weight = 1.0f,
|
||||||
.weight(
|
fill = false,
|
||||||
weight = 1.0f,
|
),
|
||||||
fill = false,
|
softWrap = true)
|
||||||
),
|
|
||||||
softWrap = true,
|
|
||||||
)
|
|
||||||
Switch(
|
Switch(
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
enabled = enabled,
|
enabled = enabled,
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
package com.zaneschepke.wireguardautotunnel.ui.models
|
||||||
|
|
||||||
|
import org.amnezia.awg.config.Interface
|
||||||
|
|
||||||
|
data class InterfaceProxy(
|
||||||
|
var privateKey: String = "",
|
||||||
|
var publicKey: String = "",
|
||||||
|
var addresses: String = "",
|
||||||
|
var dnsServers: String = "",
|
||||||
|
var listenPort: String = "",
|
||||||
|
var mtu: String = ""
|
||||||
|
) {
|
||||||
|
companion object {
|
||||||
|
fun from(i: Interface): InterfaceProxy {
|
||||||
|
return InterfaceProxy(
|
||||||
|
publicKey = i.keyPair.publicKey.toBase64().trim(),
|
||||||
|
privateKey = i.keyPair.privateKey.toBase64().trim(),
|
||||||
|
addresses = i.addresses.joinToString(", ").trim(),
|
||||||
|
dnsServers = i.dnsServers.joinToString(", ").replace("/", "").trim(),
|
||||||
|
listenPort =
|
||||||
|
if (i.listenPort.isPresent) {
|
||||||
|
i.listenPort.get().toString().trim()
|
||||||
|
} else {
|
||||||
|
""
|
||||||
|
},
|
||||||
|
mtu = if (i.mtu.isPresent) i.mtu.get().toString().trim() else "",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+7
-32
@@ -1,13 +1,13 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.ui.screens.config.model
|
package com.zaneschepke.wireguardautotunnel.ui.models
|
||||||
|
|
||||||
import com.wireguard.config.Peer
|
import org.amnezia.awg.config.Peer
|
||||||
|
|
||||||
data class PeerProxy(
|
data class PeerProxy(
|
||||||
val publicKey: String = "",
|
var publicKey: String = "",
|
||||||
val preSharedKey: String = "",
|
var preSharedKey: String = "",
|
||||||
val persistentKeepalive: String = "",
|
var persistentKeepalive: String = "",
|
||||||
val endpoint: String = "",
|
var endpoint: String = "",
|
||||||
val allowedIps: String = IPV4_WILDCARD.joinToString(", ").trim()
|
var allowedIps: String = IPV4_WILDCARD.joinToString(", ").trim()
|
||||||
) {
|
) {
|
||||||
companion object {
|
companion object {
|
||||||
fun from(peer: Peer): PeerProxy {
|
fun from(peer: Peer): PeerProxy {
|
||||||
@@ -35,31 +35,6 @@ data class PeerProxy(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun from(peer: org.amnezia.awg.config.Peer): PeerProxy {
|
|
||||||
return PeerProxy(
|
|
||||||
publicKey = peer.publicKey.toBase64(),
|
|
||||||
preSharedKey =
|
|
||||||
if (peer.preSharedKey.isPresent) {
|
|
||||||
peer.preSharedKey.get().toBase64().trim()
|
|
||||||
} else {
|
|
||||||
""
|
|
||||||
},
|
|
||||||
persistentKeepalive =
|
|
||||||
if (peer.persistentKeepalive.isPresent) {
|
|
||||||
peer.persistentKeepalive.get().toString().trim()
|
|
||||||
} else {
|
|
||||||
""
|
|
||||||
},
|
|
||||||
endpoint =
|
|
||||||
if (peer.endpoint.isPresent) {
|
|
||||||
peer.endpoint.get().toString().trim()
|
|
||||||
} else {
|
|
||||||
""
|
|
||||||
},
|
|
||||||
allowedIps = peer.allowedIps.joinToString(", ").trim(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
val IPV4_PUBLIC_NETWORKS =
|
val IPV4_PUBLIC_NETWORKS =
|
||||||
setOf(
|
setOf(
|
||||||
"0.0.0.0/5",
|
"0.0.0.0/5",
|
||||||
+14
-127
@@ -79,9 +79,9 @@ import com.zaneschepke.wireguardautotunnel.ui.common.config.ConfigurationTextBox
|
|||||||
import com.zaneschepke.wireguardautotunnel.ui.common.prompt.AuthorizationPrompt
|
import com.zaneschepke.wireguardautotunnel.ui.common.prompt.AuthorizationPrompt
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.common.screen.LoadingScreen
|
import com.zaneschepke.wireguardautotunnel.ui.common.screen.LoadingScreen
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.common.text.SectionTitle
|
import com.zaneschepke.wireguardautotunnel.ui.common.text.SectionTitle
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.screens.main.ConfigType
|
|
||||||
import com.zaneschepke.wireguardautotunnel.util.Constants
|
import com.zaneschepke.wireguardautotunnel.util.Constants
|
||||||
import com.zaneschepke.wireguardautotunnel.util.getMessage
|
import com.zaneschepke.wireguardautotunnel.util.Event
|
||||||
|
import com.zaneschepke.wireguardautotunnel.util.Result
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
|
|
||||||
@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter")
|
@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter")
|
||||||
@@ -94,8 +94,7 @@ fun ConfigScreen(
|
|||||||
focusRequester: FocusRequester,
|
focusRequester: FocusRequester,
|
||||||
navController: NavController,
|
navController: NavController,
|
||||||
appViewModel: AppViewModel,
|
appViewModel: AppViewModel,
|
||||||
tunnelId: String,
|
tunnelId: String
|
||||||
configType: ConfigType
|
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val clipboardManager: ClipboardManager = LocalClipboardManager.current
|
val clipboardManager: ClipboardManager = LocalClipboardManager.current
|
||||||
@@ -149,11 +148,11 @@ fun ConfigScreen(
|
|||||||
},
|
},
|
||||||
onError = {
|
onError = {
|
||||||
showAuthPrompt = false
|
showAuthPrompt = false
|
||||||
appViewModel.showSnackbarMessage(context.getString(R.string.error_authentication_failed))
|
appViewModel.showSnackbarMessage(Event.Error.AuthenticationFailed.message)
|
||||||
},
|
},
|
||||||
onFailure = {
|
onFailure = {
|
||||||
showAuthPrompt = false
|
showAuthPrompt = false
|
||||||
appViewModel.showSnackbarMessage(context.getString(R.string.error_authorization_failed))
|
appViewModel.showSnackbarMessage(Event.Error.AuthorizationFailed.message)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -320,11 +319,15 @@ fun ConfigScreen(
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
onClick = {
|
onClick = {
|
||||||
viewModel.onSaveAllChanges(configType).onSuccess {
|
viewModel.onSaveAllChanges().let {
|
||||||
appViewModel.showSnackbarMessage(context.getString(R.string.config_changes_saved))
|
when (it) {
|
||||||
navController.navigate(Screen.Main.route)
|
is Result.Success -> {
|
||||||
}.onFailure {
|
appViewModel.showSnackbarMessage(it.data.message)
|
||||||
appViewModel.showSnackbarMessage(it.getMessage(context))
|
navController.navigate(Screen.Main.route)
|
||||||
|
}
|
||||||
|
|
||||||
|
is Result.Error -> appViewModel.showSnackbarMessage(it.error.message)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
containerColor = fobColor,
|
containerColor = fobColor,
|
||||||
@@ -483,122 +486,6 @@ fun ConfigScreen(
|
|||||||
modifier = Modifier.width(IntrinsicSize.Min),
|
modifier = Modifier.width(IntrinsicSize.Min),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (configType == ConfigType.AMNEZIA) {
|
|
||||||
ConfigurationTextBox(
|
|
||||||
value = uiState.interfaceProxy.junkPacketCount,
|
|
||||||
onValueChange = { value -> viewModel.onJunkPacketCountChanged(value) },
|
|
||||||
keyboardActions = keyboardActions,
|
|
||||||
label = stringResource(R.string.junk_packet_count),
|
|
||||||
hint = stringResource(R.string.junk_packet_count).lowercase(),
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.focusRequester(focusRequester),
|
|
||||||
)
|
|
||||||
ConfigurationTextBox(
|
|
||||||
value = uiState.interfaceProxy.junkPacketMinSize,
|
|
||||||
onValueChange = { value ->
|
|
||||||
viewModel.onJunkPacketMinSizeChanged(
|
|
||||||
value,
|
|
||||||
)
|
|
||||||
},
|
|
||||||
keyboardActions = keyboardActions,
|
|
||||||
label = stringResource(R.string.junk_packet_minimum_size),
|
|
||||||
hint = stringResource(R.string.junk_packet_minimum_size).lowercase(),
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.focusRequester(focusRequester),
|
|
||||||
)
|
|
||||||
ConfigurationTextBox(
|
|
||||||
value = uiState.interfaceProxy.junkPacketMaxSize,
|
|
||||||
onValueChange = { value ->
|
|
||||||
viewModel.onJunkPacketMaxSizeChanged(
|
|
||||||
value,
|
|
||||||
)
|
|
||||||
},
|
|
||||||
keyboardActions = keyboardActions,
|
|
||||||
label = stringResource(R.string.junk_packet_maximum_size),
|
|
||||||
hint = stringResource(R.string.junk_packet_maximum_size).lowercase(),
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.focusRequester(focusRequester),
|
|
||||||
)
|
|
||||||
ConfigurationTextBox(
|
|
||||||
value = uiState.interfaceProxy.initPacketJunkSize,
|
|
||||||
onValueChange = { value ->
|
|
||||||
viewModel.onInitPacketJunkSizeChanged(
|
|
||||||
value,
|
|
||||||
)
|
|
||||||
},
|
|
||||||
keyboardActions = keyboardActions,
|
|
||||||
label = stringResource(R.string.init_packet_junk_size),
|
|
||||||
hint = stringResource(R.string.init_packet_junk_size).lowercase(),
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.focusRequester(focusRequester),
|
|
||||||
)
|
|
||||||
ConfigurationTextBox(
|
|
||||||
value = uiState.interfaceProxy.responsePacketJunkSize,
|
|
||||||
onValueChange = { value -> viewModel.onResponsePacketJunkSize(value) },
|
|
||||||
keyboardActions = keyboardActions,
|
|
||||||
label = stringResource(R.string.response_packet_junk_size),
|
|
||||||
hint = stringResource(R.string.response_packet_junk_size).lowercase(),
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.focusRequester(focusRequester),
|
|
||||||
)
|
|
||||||
ConfigurationTextBox(
|
|
||||||
value = uiState.interfaceProxy.initPacketMagicHeader,
|
|
||||||
onValueChange = { value -> viewModel.onInitPacketMagicHeader(value) },
|
|
||||||
keyboardActions = keyboardActions,
|
|
||||||
label = stringResource(R.string.init_packet_magic_header),
|
|
||||||
hint = stringResource(R.string.init_packet_magic_header).lowercase(),
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.focusRequester(focusRequester),
|
|
||||||
)
|
|
||||||
ConfigurationTextBox(
|
|
||||||
value = uiState.interfaceProxy.responsePacketMagicHeader,
|
|
||||||
onValueChange = { value ->
|
|
||||||
viewModel.onResponsePacketMagicHeader(
|
|
||||||
value,
|
|
||||||
)
|
|
||||||
},
|
|
||||||
keyboardActions = keyboardActions,
|
|
||||||
label = stringResource(R.string.response_packet_magic_header),
|
|
||||||
hint = stringResource(R.string.response_packet_magic_header).lowercase(),
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.focusRequester(focusRequester),
|
|
||||||
)
|
|
||||||
ConfigurationTextBox(
|
|
||||||
value = uiState.interfaceProxy.underloadPacketMagicHeader,
|
|
||||||
onValueChange = { value ->
|
|
||||||
viewModel.onUnderloadPacketMagicHeader(
|
|
||||||
value,
|
|
||||||
)
|
|
||||||
},
|
|
||||||
keyboardActions = keyboardActions,
|
|
||||||
label = stringResource(R.string.underload_packet_magic_header),
|
|
||||||
hint = stringResource(R.string.underload_packet_magic_header).lowercase(),
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.focusRequester(focusRequester),
|
|
||||||
)
|
|
||||||
ConfigurationTextBox(
|
|
||||||
value = uiState.interfaceProxy.transportPacketMagicHeader,
|
|
||||||
onValueChange = { value ->
|
|
||||||
viewModel.onTransportPacketMagicHeader(
|
|
||||||
value,
|
|
||||||
)
|
|
||||||
},
|
|
||||||
keyboardActions = keyboardActions,
|
|
||||||
label = stringResource(R.string.transport_packet_magic_header),
|
|
||||||
hint = stringResource(R.string.transport_packet_magic_header).lowercase(),
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.focusRequester(focusRequester),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
Row(
|
Row(
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
|
|||||||
+5
-60
@@ -1,9 +1,8 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.ui.screens.config
|
package com.zaneschepke.wireguardautotunnel.ui.screens.config
|
||||||
|
|
||||||
import com.wireguard.config.Config
|
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
|
||||||
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
|
import com.zaneschepke.wireguardautotunnel.ui.models.InterfaceProxy
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.screens.config.model.InterfaceProxy
|
import com.zaneschepke.wireguardautotunnel.ui.models.PeerProxy
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.screens.config.model.PeerProxy
|
|
||||||
import com.zaneschepke.wireguardautotunnel.util.Packages
|
import com.zaneschepke.wireguardautotunnel.util.Packages
|
||||||
|
|
||||||
data class ConfigUiState(
|
data class ConfigUiState(
|
||||||
@@ -15,59 +14,5 @@ data class ConfigUiState(
|
|||||||
val isAllApplicationsEnabled: Boolean = false,
|
val isAllApplicationsEnabled: Boolean = false,
|
||||||
val loading: Boolean = true,
|
val loading: Boolean = true,
|
||||||
val tunnel: TunnelConfig? = null,
|
val tunnel: TunnelConfig? = null,
|
||||||
val tunnelName: String = "",
|
val tunnelName: String = ""
|
||||||
val isAmneziaEnabled: Boolean = false
|
)
|
||||||
) {
|
|
||||||
companion object {
|
|
||||||
fun from(config: Config): ConfigUiState {
|
|
||||||
val proxyPeers = config.peers.map { PeerProxy.from(it) }
|
|
||||||
val proxyInterface = InterfaceProxy.from(config.`interface`)
|
|
||||||
var include = true
|
|
||||||
var isAllApplicationsEnabled = false
|
|
||||||
val checkedPackages =
|
|
||||||
if (config.`interface`.includedApplications.isNotEmpty()) {
|
|
||||||
config.`interface`.includedApplications
|
|
||||||
} else if (config.`interface`.excludedApplications.isNotEmpty()) {
|
|
||||||
include = false
|
|
||||||
config.`interface`.excludedApplications
|
|
||||||
} else {
|
|
||||||
isAllApplicationsEnabled = true
|
|
||||||
emptySet()
|
|
||||||
}
|
|
||||||
return ConfigUiState(
|
|
||||||
proxyPeers,
|
|
||||||
proxyInterface,
|
|
||||||
emptyList(),
|
|
||||||
checkedPackages.toList(),
|
|
||||||
include,
|
|
||||||
isAllApplicationsEnabled,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun from(config: org.amnezia.awg.config.Config): ConfigUiState {
|
|
||||||
//TODO update with new values
|
|
||||||
val proxyPeers = config.peers.map { PeerProxy.from(it) }
|
|
||||||
val proxyInterface = InterfaceProxy.from(config.`interface`)
|
|
||||||
var include = true
|
|
||||||
var isAllApplicationsEnabled = false
|
|
||||||
val checkedPackages =
|
|
||||||
if (config.`interface`.includedApplications.isNotEmpty()) {
|
|
||||||
config.`interface`.includedApplications
|
|
||||||
} else if (config.`interface`.excludedApplications.isNotEmpty()) {
|
|
||||||
include = false
|
|
||||||
config.`interface`.excludedApplications
|
|
||||||
} else {
|
|
||||||
isAllApplicationsEnabled = true
|
|
||||||
emptySet()
|
|
||||||
}
|
|
||||||
return ConfigUiState(
|
|
||||||
proxyPeers,
|
|
||||||
proxyInterface,
|
|
||||||
emptyList(),
|
|
||||||
checkedPackages.toList(),
|
|
||||||
include,
|
|
||||||
isAllApplicationsEnabled,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
+79
-252
@@ -1,36 +1,33 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.ui.screens.config
|
package com.zaneschepke.wireguardautotunnel.ui.screens.config
|
||||||
|
|
||||||
import android.Manifest
|
import android.Manifest
|
||||||
|
import android.app.Application
|
||||||
import android.content.pm.PackageInfo
|
import android.content.pm.PackageInfo
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import com.wireguard.config.Config
|
|
||||||
import com.wireguard.config.Interface
|
|
||||||
import com.wireguard.config.Peer
|
|
||||||
import com.wireguard.crypto.Key
|
|
||||||
import com.wireguard.crypto.KeyPair
|
|
||||||
import com.zaneschepke.wireguardautotunnel.R
|
|
||||||
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
|
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
|
||||||
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
|
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
|
||||||
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
|
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
|
||||||
import com.zaneschepke.wireguardautotunnel.data.repository.SettingsRepository
|
import com.zaneschepke.wireguardautotunnel.ui.models.InterfaceProxy
|
||||||
import com.zaneschepke.wireguardautotunnel.module.IoDispatcher
|
import com.zaneschepke.wireguardautotunnel.ui.models.PeerProxy
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.screens.config.model.PeerProxy
|
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.screens.main.ConfigType
|
|
||||||
import com.zaneschepke.wireguardautotunnel.util.Constants
|
import com.zaneschepke.wireguardautotunnel.util.Constants
|
||||||
|
import com.zaneschepke.wireguardautotunnel.util.Event
|
||||||
import com.zaneschepke.wireguardautotunnel.util.NumberUtils
|
import com.zaneschepke.wireguardautotunnel.util.NumberUtils
|
||||||
import com.zaneschepke.wireguardautotunnel.util.StringValue
|
import com.zaneschepke.wireguardautotunnel.util.Result
|
||||||
import com.zaneschepke.wireguardautotunnel.util.WgTunnelExceptions
|
|
||||||
import com.zaneschepke.wireguardautotunnel.util.removeAt
|
import com.zaneschepke.wireguardautotunnel.util.removeAt
|
||||||
import com.zaneschepke.wireguardautotunnel.util.update
|
import com.zaneschepke.wireguardautotunnel.util.update
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import kotlinx.coroutines.CoroutineDispatcher
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
import kotlinx.coroutines.flow.update
|
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import org.amnezia.awg.config.Config
|
||||||
|
import org.amnezia.awg.config.Interface
|
||||||
|
import org.amnezia.awg.config.Peer
|
||||||
|
import org.amnezia.awg.crypto.Key
|
||||||
|
import org.amnezia.awg.crypto.KeyPair
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@@ -38,36 +35,49 @@ import javax.inject.Inject
|
|||||||
class ConfigViewModel
|
class ConfigViewModel
|
||||||
@Inject
|
@Inject
|
||||||
constructor(
|
constructor(
|
||||||
private val settingsRepository: SettingsRepository,
|
private val application: Application,
|
||||||
private val appDataRepository: AppDataRepository,
|
private val appDataRepository: AppDataRepository
|
||||||
@IoDispatcher private val ioDispatcher: CoroutineDispatcher
|
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
|
|
||||||
private val packageManager = WireGuardAutoTunnel.instance.packageManager
|
private val packageManager = application.packageManager
|
||||||
|
|
||||||
private val _uiState = MutableStateFlow(ConfigUiState())
|
private val _uiState = MutableStateFlow(ConfigUiState())
|
||||||
val uiState = _uiState.asStateFlow()
|
val uiState = _uiState.asStateFlow()
|
||||||
|
|
||||||
fun init(tunnelId: String) =
|
fun init(tunnelId: String) =
|
||||||
viewModelScope.launch(ioDispatcher) {
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
val packages = getQueriedPackages("")
|
val packages = getQueriedPackages("")
|
||||||
val state =
|
val state =
|
||||||
if (tunnelId != Constants.MANUAL_TUNNEL_CONFIG_ID) {
|
if (tunnelId != Constants.MANUAL_TUNNEL_CONFIG_ID) {
|
||||||
val tunnelConfig =
|
val tunnelConfig =
|
||||||
appDataRepository.tunnels.getAll()
|
appDataRepository.tunnels.getAll()
|
||||||
.firstOrNull { it.id.toString() == tunnelId }
|
.firstOrNull { it.id.toString() == tunnelId }
|
||||||
val isAmneziaEnabled = settingsRepository.getSettings().isAmneziaEnabled
|
|
||||||
if (tunnelConfig != null) {
|
if (tunnelConfig != null) {
|
||||||
(if (isAmneziaEnabled) {
|
val config = TunnelConfig.configFromQuick(tunnelConfig.wgQuick)
|
||||||
val amConfig =
|
val proxyPeers = config.peers.map { PeerProxy.from(it) }
|
||||||
if (tunnelConfig.amQuick == "") tunnelConfig.wgQuick else tunnelConfig.amQuick
|
val proxyInterface = InterfaceProxy.from(config.`interface`)
|
||||||
ConfigUiState.from(TunnelConfig.configFromAmQuick(amConfig))
|
var include = true
|
||||||
} else ConfigUiState.from(TunnelConfig.configFromWgQuick(tunnelConfig.wgQuick))).copy(
|
var isAllApplicationsEnabled = false
|
||||||
packages = packages,
|
val checkedPackages =
|
||||||
loading = false,
|
if (config.`interface`.includedApplications.isNotEmpty()) {
|
||||||
tunnel = tunnelConfig,
|
config.`interface`.includedApplications
|
||||||
tunnelName = tunnelConfig.name,
|
} else if (config.`interface`.excludedApplications.isNotEmpty()) {
|
||||||
isAmneziaEnabled = isAmneziaEnabled,
|
include = false
|
||||||
|
config.`interface`.excludedApplications
|
||||||
|
} else {
|
||||||
|
isAllApplicationsEnabled = true
|
||||||
|
emptySet()
|
||||||
|
}
|
||||||
|
ConfigUiState(
|
||||||
|
proxyPeers,
|
||||||
|
proxyInterface,
|
||||||
|
packages,
|
||||||
|
checkedPackages.toList(),
|
||||||
|
include,
|
||||||
|
isAllApplicationsEnabled,
|
||||||
|
false,
|
||||||
|
tunnelConfig,
|
||||||
|
tunnelConfig.name,
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
ConfigUiState(loading = false, packages = packages)
|
ConfigUiState(loading = false, packages = packages)
|
||||||
@@ -111,7 +121,7 @@ constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun getPackageLabel(packageInfo: PackageInfo): String {
|
fun getPackageLabel(packageInfo: PackageInfo): String {
|
||||||
return packageInfo.applicationInfo.loadLabel(packageManager).toString()
|
return packageInfo.applicationInfo.loadLabel(application.packageManager).toString()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getAllInternetCapablePackages(): List<PackageInfo> {
|
private fun getAllInternetCapablePackages(): List<PackageInfo> {
|
||||||
@@ -140,7 +150,7 @@ constructor(
|
|||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
if (tunnelConfig != null) {
|
if (tunnelConfig != null) {
|
||||||
saveConfig(tunnelConfig).join()
|
saveConfig(tunnelConfig).join()
|
||||||
WireGuardAutoTunnel.requestTunnelTileServiceStateUpdate()
|
WireGuardAutoTunnel.requestTunnelTileServiceStateUpdate(application)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -158,20 +168,6 @@ constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun buildAmPeerListFromProxyPeers(): List<org.amnezia.awg.config.Peer> {
|
|
||||||
return _uiState.value.proxyPeers.map {
|
|
||||||
val builder = org.amnezia.awg.config.Peer.Builder()
|
|
||||||
if (it.allowedIps.isNotEmpty()) builder.parseAllowedIPs(it.allowedIps.trim())
|
|
||||||
if (it.publicKey.isNotEmpty()) builder.parsePublicKey(it.publicKey.trim())
|
|
||||||
if (it.preSharedKey.isNotEmpty()) builder.parsePreSharedKey(it.preSharedKey.trim())
|
|
||||||
if (it.endpoint.isNotEmpty()) builder.parseEndpoint(it.endpoint.trim())
|
|
||||||
if (it.persistentKeepalive.isNotEmpty()) {
|
|
||||||
builder.parsePersistentKeepalive(it.persistentKeepalive.trim())
|
|
||||||
}
|
|
||||||
builder.build()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun emptyCheckedPackagesList() {
|
private fun emptyCheckedPackagesList() {
|
||||||
_uiState.value = _uiState.value.copy(checkedPackageNames = emptyList())
|
_uiState.value = _uiState.value.copy(checkedPackageNames = emptyList())
|
||||||
}
|
}
|
||||||
@@ -194,244 +190,147 @@ constructor(
|
|||||||
return builder.build()
|
return builder.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun buildAmInterfaceListFromProxyInterface(): org.amnezia.awg.config.Interface {
|
fun onSaveAllChanges(): Result<Event> {
|
||||||
val builder = org.amnezia.awg.config.Interface.Builder()
|
|
||||||
builder.parsePrivateKey(_uiState.value.interfaceProxy.privateKey.trim())
|
|
||||||
builder.parseAddresses(_uiState.value.interfaceProxy.addresses.trim())
|
|
||||||
if (_uiState.value.interfaceProxy.dnsServers.isNotEmpty()) {
|
|
||||||
builder.parseDnsServers(_uiState.value.interfaceProxy.dnsServers.trim())
|
|
||||||
}
|
|
||||||
if (_uiState.value.interfaceProxy.mtu.isNotEmpty())
|
|
||||||
builder.parseMtu(_uiState.value.interfaceProxy.mtu.trim())
|
|
||||||
if (_uiState.value.interfaceProxy.listenPort.isNotEmpty()) {
|
|
||||||
builder.parseListenPort(_uiState.value.interfaceProxy.listenPort.trim())
|
|
||||||
}
|
|
||||||
if (isAllApplicationsEnabled()) emptyCheckedPackagesList()
|
|
||||||
if (_uiState.value.include) builder.includeApplications(_uiState.value.checkedPackageNames)
|
|
||||||
if (!_uiState.value.include) builder.excludeApplications(_uiState.value.checkedPackageNames)
|
|
||||||
if (_uiState.value.interfaceProxy.junkPacketCount.isNotEmpty()) {
|
|
||||||
builder.setJunkPacketCount(_uiState.value.interfaceProxy.junkPacketCount.trim().toInt())
|
|
||||||
}
|
|
||||||
if (_uiState.value.interfaceProxy.junkPacketMinSize.isNotEmpty()) {
|
|
||||||
builder.setJunkPacketMinSize(
|
|
||||||
_uiState.value.interfaceProxy.junkPacketMinSize.trim().toInt(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
if (_uiState.value.interfaceProxy.junkPacketMaxSize.isNotEmpty()) {
|
|
||||||
builder.setJunkPacketMaxSize(
|
|
||||||
_uiState.value.interfaceProxy.junkPacketMaxSize.trim().toInt(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
if (_uiState.value.interfaceProxy.initPacketJunkSize.isNotEmpty()) {
|
|
||||||
builder.setInitPacketJunkSize(
|
|
||||||
_uiState.value.interfaceProxy.initPacketJunkSize.trim().toInt(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
if (_uiState.value.interfaceProxy.responsePacketJunkSize.isNotEmpty()) {
|
|
||||||
builder.setResponsePacketJunkSize(
|
|
||||||
_uiState.value.interfaceProxy.responsePacketJunkSize.trim().toInt(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
if (_uiState.value.interfaceProxy.initPacketMagicHeader.isNotEmpty()) {
|
|
||||||
builder.setInitPacketMagicHeader(
|
|
||||||
_uiState.value.interfaceProxy.initPacketMagicHeader.trim().toLong(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
if (_uiState.value.interfaceProxy.responsePacketMagicHeader.isNotEmpty()) {
|
|
||||||
builder.setResponsePacketMagicHeader(
|
|
||||||
_uiState.value.interfaceProxy.responsePacketMagicHeader.trim().toLong(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
if (_uiState.value.interfaceProxy.transportPacketMagicHeader.isNotEmpty()) {
|
|
||||||
builder.setTransportPacketMagicHeader(
|
|
||||||
_uiState.value.interfaceProxy.transportPacketMagicHeader.trim().toLong(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
if (_uiState.value.interfaceProxy.underloadPacketMagicHeader.isNotEmpty()) {
|
|
||||||
builder.setUnderloadPacketMagicHeader(
|
|
||||||
_uiState.value.interfaceProxy.underloadPacketMagicHeader.trim().toLong(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
return builder.build()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun buildConfig(): Config {
|
|
||||||
val peerList = buildPeerListFromProxyPeers()
|
|
||||||
val wgInterface = buildInterfaceListFromProxyInterface()
|
|
||||||
return Config.Builder().addPeers(peerList).setInterface(wgInterface).build()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun buildAmConfig(): org.amnezia.awg.config.Config {
|
|
||||||
val peerList = buildAmPeerListFromProxyPeers()
|
|
||||||
val amInterface = buildAmInterfaceListFromProxyInterface()
|
|
||||||
return org.amnezia.awg.config.Config.Builder().addPeers(peerList).setInterface(amInterface)
|
|
||||||
.build()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onSaveAllChanges(configType: ConfigType): Result<Unit> {
|
|
||||||
return try {
|
return try {
|
||||||
val wgQuick = buildConfig().toWgQuickString()
|
val peerList = buildPeerListFromProxyPeers()
|
||||||
val amQuick = if (configType == ConfigType.AMNEZIA) {
|
val wgInterface = buildInterfaceListFromProxyInterface()
|
||||||
buildAmConfig().toAwgQuickString()
|
val config = Config.Builder().addPeers(peerList).setInterface(wgInterface).build()
|
||||||
} else TunnelConfig.AM_QUICK_DEFAULT
|
|
||||||
val tunnelConfig = when (uiState.value.tunnel) {
|
val tunnelConfig = when (uiState.value.tunnel) {
|
||||||
null -> TunnelConfig(
|
null -> TunnelConfig(
|
||||||
name = _uiState.value.tunnelName,
|
name = _uiState.value.tunnelName,
|
||||||
wgQuick = wgQuick,
|
wgQuick = config.toAwgQuickString(),
|
||||||
amQuick = amQuick,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
else -> uiState.value.tunnel!!.copy(
|
else -> uiState.value.tunnel!!.copy(
|
||||||
name = _uiState.value.tunnelName,
|
name = _uiState.value.tunnelName,
|
||||||
wgQuick = wgQuick,
|
wgQuick = config.toAwgQuickString(),
|
||||||
amQuick = amQuick,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
updateTunnelConfig(tunnelConfig)
|
updateTunnelConfig(tunnelConfig)
|
||||||
Result.success(Unit)
|
Result.Success(Event.Message.ConfigSaved)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Timber.e(e)
|
Timber.e(e)
|
||||||
val message = e.message?.substringAfter(":", missingDelimiterValue = "")
|
val message = e.message?.substringAfter(":", missingDelimiterValue = "")
|
||||||
val stringValue = message?.let {
|
Result.Error(Event.Error.ConfigParseError(message ?: ""))
|
||||||
StringValue.DynamicString(message)
|
|
||||||
} ?: StringValue.StringResource(R.string.unknown_error)
|
|
||||||
Result.failure(WgTunnelExceptions.ConfigParseError(stringValue))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onPeerPublicKeyChange(index: Int, value: String) {
|
fun onPeerPublicKeyChange(index: Int, value: String) {
|
||||||
_uiState.update {
|
_uiState.value =
|
||||||
it.copy(
|
_uiState.value.copy(
|
||||||
proxyPeers =
|
proxyPeers =
|
||||||
_uiState.value.proxyPeers.update(
|
_uiState.value.proxyPeers.update(
|
||||||
index,
|
index,
|
||||||
_uiState.value.proxyPeers[index].copy(publicKey = value),
|
_uiState.value.proxyPeers[index].copy(publicKey = value),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onPreSharedKeyChange(index: Int, value: String) {
|
fun onPreSharedKeyChange(index: Int, value: String) {
|
||||||
_uiState.update {
|
_uiState.value =
|
||||||
it.copy(
|
_uiState.value.copy(
|
||||||
proxyPeers =
|
proxyPeers =
|
||||||
_uiState.value.proxyPeers.update(
|
_uiState.value.proxyPeers.update(
|
||||||
index,
|
index,
|
||||||
_uiState.value.proxyPeers[index].copy(preSharedKey = value),
|
_uiState.value.proxyPeers[index].copy(preSharedKey = value),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onEndpointChange(index: Int, value: String) {
|
fun onEndpointChange(index: Int, value: String) {
|
||||||
_uiState.update {
|
_uiState.value =
|
||||||
it.copy(
|
_uiState.value.copy(
|
||||||
proxyPeers =
|
proxyPeers =
|
||||||
_uiState.value.proxyPeers.update(
|
_uiState.value.proxyPeers.update(
|
||||||
index,
|
index,
|
||||||
_uiState.value.proxyPeers[index].copy(endpoint = value),
|
_uiState.value.proxyPeers[index].copy(endpoint = value),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onAllowedIpsChange(index: Int, value: String) {
|
fun onAllowedIpsChange(index: Int, value: String) {
|
||||||
_uiState.update {
|
_uiState.value =
|
||||||
it.copy(
|
_uiState.value.copy(
|
||||||
proxyPeers =
|
proxyPeers =
|
||||||
_uiState.value.proxyPeers.update(
|
_uiState.value.proxyPeers.update(
|
||||||
index,
|
index,
|
||||||
_uiState.value.proxyPeers[index].copy(allowedIps = value),
|
_uiState.value.proxyPeers[index].copy(allowedIps = value),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onPersistentKeepaliveChanged(index: Int, value: String) {
|
fun onPersistentKeepaliveChanged(index: Int, value: String) {
|
||||||
_uiState.update {
|
_uiState.value =
|
||||||
it.copy(
|
_uiState.value.copy(
|
||||||
proxyPeers =
|
proxyPeers =
|
||||||
_uiState.value.proxyPeers.update(
|
_uiState.value.proxyPeers.update(
|
||||||
index,
|
index,
|
||||||
_uiState.value.proxyPeers[index].copy(persistentKeepalive = value),
|
_uiState.value.proxyPeers[index].copy(persistentKeepalive = value),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onDeletePeer(index: Int) {
|
fun onDeletePeer(index: Int) {
|
||||||
_uiState.update {
|
_uiState.value =
|
||||||
it.copy(
|
_uiState.value.copy(
|
||||||
proxyPeers = _uiState.value.proxyPeers.removeAt(index),
|
proxyPeers = _uiState.value.proxyPeers.removeAt(index),
|
||||||
)
|
)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun addEmptyPeer() {
|
fun addEmptyPeer() {
|
||||||
_uiState.update {
|
_uiState.value = _uiState.value.copy(proxyPeers = _uiState.value.proxyPeers + PeerProxy())
|
||||||
it.copy(proxyPeers = _uiState.value.proxyPeers + PeerProxy())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun generateKeyPair() {
|
fun generateKeyPair() {
|
||||||
val keyPair = KeyPair()
|
val keyPair = KeyPair()
|
||||||
_uiState.update {
|
_uiState.value =
|
||||||
it.copy(
|
_uiState.value.copy(
|
||||||
interfaceProxy =
|
interfaceProxy =
|
||||||
_uiState.value.interfaceProxy.copy(
|
_uiState.value.interfaceProxy.copy(
|
||||||
privateKey = keyPair.privateKey.toBase64(),
|
privateKey = keyPair.privateKey.toBase64(),
|
||||||
publicKey = keyPair.publicKey.toBase64(),
|
publicKey = keyPair.publicKey.toBase64(),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onAddressesChanged(value: String) {
|
fun onAddressesChanged(value: String) {
|
||||||
_uiState.update {
|
_uiState.value =
|
||||||
it.copy(
|
_uiState.value.copy(
|
||||||
interfaceProxy = _uiState.value.interfaceProxy.copy(addresses = value),
|
interfaceProxy = _uiState.value.interfaceProxy.copy(addresses = value),
|
||||||
)
|
)
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onListenPortChanged(value: String) {
|
fun onListenPortChanged(value: String) {
|
||||||
_uiState.update {
|
_uiState.value =
|
||||||
it.copy(
|
_uiState.value.copy(
|
||||||
interfaceProxy = _uiState.value.interfaceProxy.copy(listenPort = value),
|
interfaceProxy = _uiState.value.interfaceProxy.copy(listenPort = value),
|
||||||
)
|
)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onDnsServersChanged(value: String) {
|
fun onDnsServersChanged(value: String) {
|
||||||
_uiState.update {
|
_uiState.value =
|
||||||
it.copy(
|
_uiState.value.copy(
|
||||||
interfaceProxy = _uiState.value.interfaceProxy.copy(dnsServers = value),
|
interfaceProxy = _uiState.value.interfaceProxy.copy(dnsServers = value),
|
||||||
)
|
)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onMtuChanged(value: String) {
|
fun onMtuChanged(value: String) {
|
||||||
_uiState.update {
|
_uiState.value =
|
||||||
it.copy(interfaceProxy = _uiState.value.interfaceProxy.copy(mtu = value))
|
_uiState.value.copy(interfaceProxy = _uiState.value.interfaceProxy.copy(mtu = value))
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onInterfacePublicKeyChange(value: String) {
|
private fun onInterfacePublicKeyChange(value: String) {
|
||||||
_uiState.update {
|
_uiState.value =
|
||||||
it.copy(
|
_uiState.value.copy(
|
||||||
interfaceProxy = _uiState.value.interfaceProxy.copy(publicKey = value),
|
interfaceProxy = _uiState.value.interfaceProxy.copy(publicKey = value),
|
||||||
)
|
)
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onPrivateKeyChange(value: String) {
|
fun onPrivateKeyChange(value: String) {
|
||||||
_uiState.update {
|
_uiState.value =
|
||||||
it.copy(
|
_uiState.value.copy(
|
||||||
interfaceProxy = _uiState.value.interfaceProxy.copy(privateKey = value),
|
interfaceProxy = _uiState.value.interfaceProxy.copy(privateKey = value),
|
||||||
)
|
)
|
||||||
}
|
|
||||||
if (NumberUtils.isValidKey(value)) {
|
if (NumberUtils.isValidKey(value)) {
|
||||||
val pair = KeyPair(Key.fromBase64(value))
|
val pair = KeyPair(Key.fromBase64(value))
|
||||||
onInterfacePublicKeyChange(pair.publicKey.toBase64())
|
onInterfacePublicKeyChange(pair.publicKey.toBase64())
|
||||||
@@ -445,78 +344,6 @@ constructor(
|
|||||||
getAllInternetCapablePackages().filter {
|
getAllInternetCapablePackages().filter {
|
||||||
getPackageLabel(it).lowercase().contains(query.lowercase())
|
getPackageLabel(it).lowercase().contains(query.lowercase())
|
||||||
}
|
}
|
||||||
_uiState.update { it.copy(packages = packages) }
|
_uiState.value = _uiState.value.copy(packages = packages)
|
||||||
}
|
|
||||||
|
|
||||||
fun onJunkPacketCountChanged(value: String) {
|
|
||||||
_uiState.update {
|
|
||||||
it.copy(
|
|
||||||
interfaceProxy = _uiState.value.interfaceProxy.copy(junkPacketCount = value),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onJunkPacketMinSizeChanged(value: String) {
|
|
||||||
_uiState.update {
|
|
||||||
it.copy(
|
|
||||||
interfaceProxy = _uiState.value.interfaceProxy.copy(junkPacketMinSize = value),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onJunkPacketMaxSizeChanged(value: String) {
|
|
||||||
_uiState.update {
|
|
||||||
it.copy(
|
|
||||||
interfaceProxy = _uiState.value.interfaceProxy.copy(junkPacketMaxSize = value),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onInitPacketJunkSizeChanged(value: String) {
|
|
||||||
_uiState.update {
|
|
||||||
it.copy(
|
|
||||||
interfaceProxy = _uiState.value.interfaceProxy.copy(initPacketJunkSize = value),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onResponsePacketJunkSize(value: String) {
|
|
||||||
_uiState.update {
|
|
||||||
it.copy(
|
|
||||||
interfaceProxy = _uiState.value.interfaceProxy.copy(responsePacketJunkSize = value),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onInitPacketMagicHeader(value: String) {
|
|
||||||
_uiState.update {
|
|
||||||
it.copy(
|
|
||||||
interfaceProxy = _uiState.value.interfaceProxy.copy(initPacketMagicHeader = value),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onResponsePacketMagicHeader(value: String) {
|
|
||||||
_uiState.update {
|
|
||||||
it.copy(
|
|
||||||
interfaceProxy = _uiState.value.interfaceProxy.copy(responsePacketMagicHeader = value),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onTransportPacketMagicHeader(value: String) {
|
|
||||||
_uiState.update {
|
|
||||||
it.copy(
|
|
||||||
interfaceProxy = _uiState.value.interfaceProxy.copy(transportPacketMagicHeader = value),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onUnderloadPacketMagicHeader(value: String) {
|
|
||||||
_uiState.update {
|
|
||||||
it.copy(
|
|
||||||
interfaceProxy = _uiState.value.interfaceProxy.copy(underloadPacketMagicHeader = value),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
-73
@@ -1,73 +0,0 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.ui.screens.config.model
|
|
||||||
|
|
||||||
import com.wireguard.config.Interface
|
|
||||||
|
|
||||||
data class InterfaceProxy(
|
|
||||||
val privateKey: String = "",
|
|
||||||
val publicKey: String = "",
|
|
||||||
val addresses: String = "",
|
|
||||||
val dnsServers: String = "",
|
|
||||||
val listenPort: String = "",
|
|
||||||
val mtu: String = "",
|
|
||||||
val junkPacketCount: String = "",
|
|
||||||
val junkPacketMinSize: String = "",
|
|
||||||
val junkPacketMaxSize: String = "",
|
|
||||||
val initPacketJunkSize: String = "",
|
|
||||||
val responsePacketJunkSize: String = "",
|
|
||||||
val initPacketMagicHeader: String = "",
|
|
||||||
val responsePacketMagicHeader: String = "",
|
|
||||||
val underloadPacketMagicHeader: String = "",
|
|
||||||
val transportPacketMagicHeader: String = "",
|
|
||||||
) {
|
|
||||||
companion object {
|
|
||||||
fun from(i: Interface): InterfaceProxy {
|
|
||||||
return InterfaceProxy(
|
|
||||||
publicKey = i.keyPair.publicKey.toBase64().trim(),
|
|
||||||
privateKey = i.keyPair.privateKey.toBase64().trim(),
|
|
||||||
addresses = i.addresses.joinToString(", ").trim(),
|
|
||||||
dnsServers = i.dnsServers.joinToString(", ").replace("/", "").trim(),
|
|
||||||
listenPort =
|
|
||||||
if (i.listenPort.isPresent) {
|
|
||||||
i.listenPort.get().toString().trim()
|
|
||||||
} else {
|
|
||||||
""
|
|
||||||
},
|
|
||||||
mtu = if (i.mtu.isPresent) i.mtu.get().toString().trim() else "",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun from(i: org.amnezia.awg.config.Interface): InterfaceProxy {
|
|
||||||
return InterfaceProxy(
|
|
||||||
publicKey = i.keyPair.publicKey.toBase64().trim(),
|
|
||||||
privateKey = i.keyPair.privateKey.toBase64().trim(),
|
|
||||||
addresses = i.addresses.joinToString(", ").trim(),
|
|
||||||
dnsServers = i.dnsServers.joinToString(", ").replace("/", "").trim(),
|
|
||||||
listenPort =
|
|
||||||
if (i.listenPort.isPresent) {
|
|
||||||
i.listenPort.get().toString().trim()
|
|
||||||
} else {
|
|
||||||
""
|
|
||||||
},
|
|
||||||
mtu = if (i.mtu.isPresent) i.mtu.get().toString().trim() else "",
|
|
||||||
junkPacketCount = if (i.junkPacketCount.isPresent) i.junkPacketCount.get()
|
|
||||||
.toString() else "",
|
|
||||||
junkPacketMinSize = if (i.junkPacketMinSize.isPresent) i.junkPacketMinSize.get()
|
|
||||||
.toString() else "",
|
|
||||||
junkPacketMaxSize = if (i.junkPacketMaxSize.isPresent) i.junkPacketMaxSize.get()
|
|
||||||
.toString() else "",
|
|
||||||
initPacketJunkSize = if (i.initPacketJunkSize.isPresent) i.initPacketJunkSize.get()
|
|
||||||
.toString() else "",
|
|
||||||
responsePacketJunkSize = if (i.responsePacketJunkSize.isPresent) i.responsePacketJunkSize.get()
|
|
||||||
.toString() else "",
|
|
||||||
initPacketMagicHeader = if (i.initPacketMagicHeader.isPresent) i.initPacketMagicHeader.get()
|
|
||||||
.toString() else "",
|
|
||||||
responsePacketMagicHeader = if (i.responsePacketMagicHeader.isPresent) i.responsePacketMagicHeader.get()
|
|
||||||
.toString() else "",
|
|
||||||
transportPacketMagicHeader = if (i.transportPacketMagicHeader.isPresent) i.transportPacketMagicHeader.get()
|
|
||||||
.toString() else "",
|
|
||||||
underloadPacketMagicHeader = if (i.underloadPacketMagicHeader.isPresent) i.underloadPacketMagicHeader.get()
|
|
||||||
.toString() else "",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.ui.screens.main
|
|
||||||
|
|
||||||
enum class ConfigType {
|
|
||||||
AMNEZIA,
|
|
||||||
WIREGUARD
|
|
||||||
}
|
|
||||||
+88
-168
@@ -14,7 +14,6 @@ import androidx.compose.animation.slideInVertically
|
|||||||
import androidx.compose.animation.slideOutVertically
|
import androidx.compose.animation.slideOutVertically
|
||||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.focusGroup
|
|
||||||
import androidx.compose.foundation.focusable
|
import androidx.compose.foundation.focusable
|
||||||
import androidx.compose.foundation.gestures.ScrollableDefaults
|
import androidx.compose.foundation.gestures.ScrollableDefaults
|
||||||
import androidx.compose.foundation.gestures.detectTapGestures
|
import androidx.compose.foundation.gestures.detectTapGestures
|
||||||
@@ -30,11 +29,11 @@ import androidx.compose.foundation.lazy.items
|
|||||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
import androidx.compose.foundation.overscroll
|
import androidx.compose.foundation.overscroll
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.foundation.text.ClickableText
|
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.Create
|
import androidx.compose.material.icons.filled.Create
|
||||||
import androidx.compose.material.icons.filled.FileOpen
|
import androidx.compose.material.icons.filled.FileOpen
|
||||||
import androidx.compose.material.icons.filled.QrCode
|
import androidx.compose.material.icons.filled.QrCode
|
||||||
|
import androidx.compose.material.icons.rounded.Add
|
||||||
import androidx.compose.material.icons.rounded.Bolt
|
import androidx.compose.material.icons.rounded.Bolt
|
||||||
import androidx.compose.material.icons.rounded.Circle
|
import androidx.compose.material.icons.rounded.Circle
|
||||||
import androidx.compose.material.icons.rounded.CopyAll
|
import androidx.compose.material.icons.rounded.CopyAll
|
||||||
@@ -46,6 +45,7 @@ import androidx.compose.material.icons.rounded.Star
|
|||||||
import androidx.compose.material3.AlertDialog
|
import androidx.compose.material3.AlertDialog
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.FabPosition
|
import androidx.compose.material3.FabPosition
|
||||||
|
import androidx.compose.material3.FloatingActionButton
|
||||||
import androidx.compose.material3.HorizontalDivider
|
import androidx.compose.material3.HorizontalDivider
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.IconButton
|
import androidx.compose.material3.IconButton
|
||||||
@@ -70,6 +70,7 @@ import androidx.compose.ui.Alignment
|
|||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.focus.FocusRequester
|
import androidx.compose.ui.focus.FocusRequester
|
||||||
import androidx.compose.ui.focus.focusRequester
|
import androidx.compose.ui.focus.focusRequester
|
||||||
|
import androidx.compose.ui.focus.onFocusChanged
|
||||||
import androidx.compose.ui.geometry.Offset
|
import androidx.compose.ui.geometry.Offset
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
|
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
|
||||||
@@ -80,38 +81,35 @@ import androidx.compose.ui.input.pointer.pointerInput
|
|||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.platform.LocalHapticFeedback
|
import androidx.compose.ui.platform.LocalHapticFeedback
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.SpanStyle
|
|
||||||
import androidx.compose.ui.text.buildAnnotatedString
|
import androidx.compose.ui.text.buildAnnotatedString
|
||||||
import androidx.compose.ui.text.font.FontStyle
|
import androidx.compose.ui.text.font.FontStyle
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
|
||||||
import androidx.compose.ui.text.withStyle
|
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.hilt.navigation.compose.hiltViewModel
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import androidx.navigation.NavController
|
import androidx.navigation.NavController
|
||||||
import com.iamageo.multifablibrary.FabIcon
|
|
||||||
import com.iamageo.multifablibrary.FabOption
|
|
||||||
import com.iamageo.multifablibrary.MultiFabItem
|
|
||||||
import com.iamageo.multifablibrary.MultiFloatingActionButton
|
|
||||||
import com.journeyapps.barcodescanner.ScanContract
|
import com.journeyapps.barcodescanner.ScanContract
|
||||||
import com.journeyapps.barcodescanner.ScanOptions
|
import com.journeyapps.barcodescanner.ScanOptions
|
||||||
import com.zaneschepke.wireguardautotunnel.R
|
import com.zaneschepke.wireguardautotunnel.R
|
||||||
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
|
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
|
||||||
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
|
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
|
||||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.HandshakeStatus
|
import com.zaneschepke.wireguardautotunnel.service.tunnel.HandshakeStatus
|
||||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelState
|
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.AppViewModel
|
import com.zaneschepke.wireguardautotunnel.ui.AppViewModel
|
||||||
|
import com.zaneschepke.wireguardautotunnel.ui.CaptureActivityPortrait
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.Screen
|
import com.zaneschepke.wireguardautotunnel.ui.Screen
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.common.RowListItem
|
import com.zaneschepke.wireguardautotunnel.ui.common.RowListItem
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.common.screen.LoadingScreen
|
import com.zaneschepke.wireguardautotunnel.ui.common.screen.LoadingScreen
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.theme.corn
|
import com.zaneschepke.wireguardautotunnel.ui.theme.corn
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.theme.mint
|
import com.zaneschepke.wireguardautotunnel.ui.theme.mint
|
||||||
import com.zaneschepke.wireguardautotunnel.util.Constants
|
import com.zaneschepke.wireguardautotunnel.util.Constants
|
||||||
import com.zaneschepke.wireguardautotunnel.util.getMessage
|
import com.zaneschepke.wireguardautotunnel.util.Event
|
||||||
|
import com.zaneschepke.wireguardautotunnel.util.Result
|
||||||
import com.zaneschepke.wireguardautotunnel.util.handshakeStatus
|
import com.zaneschepke.wireguardautotunnel.util.handshakeStatus
|
||||||
import com.zaneschepke.wireguardautotunnel.util.mapPeerStats
|
import com.zaneschepke.wireguardautotunnel.util.mapPeerStats
|
||||||
|
import com.zaneschepke.wireguardautotunnel.util.truncateWithEllipsis
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import org.amnezia.awg.backend.Tunnel
|
||||||
|
|
||||||
@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter")
|
@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter")
|
||||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class)
|
@OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class)
|
||||||
@@ -125,11 +123,10 @@ fun MainScreen(
|
|||||||
val haptic = LocalHapticFeedback.current
|
val haptic = LocalHapticFeedback.current
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val isVisible = rememberSaveable { mutableStateOf(true) }
|
val isVisible = rememberSaveable { mutableStateOf(true) }
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope { Dispatchers.IO }
|
||||||
|
|
||||||
val sheetState = rememberModalBottomSheetState()
|
val sheetState = rememberModalBottomSheetState()
|
||||||
var showBottomSheet by remember { mutableStateOf(false) }
|
var showBottomSheet by remember { mutableStateOf(false) }
|
||||||
var configType by remember { mutableStateOf(ConfigType.WIREGUARD) }
|
|
||||||
|
|
||||||
// Nested scroll for control FAB
|
// Nested scroll for control FAB
|
||||||
val nestedScrollConnection = remember {
|
val nestedScrollConnection = remember {
|
||||||
@@ -190,7 +187,7 @@ fun MainScreen(
|
|||||||
name.startsWith(Constants.ANDROID_TV_EXPLORER_STUB)
|
name.startsWith(Constants.ANDROID_TV_EXPLORER_STUB)
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
appViewModel.showSnackbarMessage(context.getString(R.string.error_no_file_explorer))
|
appViewModel.showSnackbarMessage(Event.Error.FileExplorerRequired.message)
|
||||||
}
|
}
|
||||||
return intent
|
return intent
|
||||||
}
|
}
|
||||||
@@ -198,8 +195,11 @@ fun MainScreen(
|
|||||||
) { data ->
|
) { data ->
|
||||||
if (data == null) return@rememberLauncherForActivityResult
|
if (data == null) return@rememberLauncherForActivityResult
|
||||||
scope.launch {
|
scope.launch {
|
||||||
viewModel.onTunnelFileSelected(data, configType, context).onFailure {
|
viewModel.onTunnelFileSelected(data).let {
|
||||||
appViewModel.showSnackbarMessage(it.getMessage(context))
|
when (it) {
|
||||||
|
is Result.Error -> appViewModel.showSnackbarMessage(it.error.message)
|
||||||
|
is Result.Success -> {}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -209,8 +209,11 @@ fun MainScreen(
|
|||||||
onResult = {
|
onResult = {
|
||||||
if (it.contents != null) {
|
if (it.contents != null) {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
viewModel.onTunnelQrResult(it.contents, configType).onFailure { error ->
|
viewModel.onTunnelQrResult(it.contents).let { result ->
|
||||||
appViewModel.showSnackbarMessage(error.getMessage(context))
|
when (result) {
|
||||||
|
is Result.Success -> {}
|
||||||
|
is Result.Error -> appViewModel.showSnackbarMessage(result.error.message)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -223,7 +226,7 @@ fun MainScreen(
|
|||||||
confirmButton = {
|
confirmButton = {
|
||||||
TextButton(
|
TextButton(
|
||||||
onClick = {
|
onClick = {
|
||||||
selectedTunnel?.let { viewModel.onDelete(it, context) }
|
selectedTunnel?.let { viewModel.onDelete(it) }
|
||||||
showDeleteTunnelAlertDialog = false
|
showDeleteTunnelAlertDialog = false
|
||||||
selectedTunnel = null
|
selectedTunnel = null
|
||||||
},
|
},
|
||||||
@@ -243,9 +246,7 @@ fun MainScreen(
|
|||||||
|
|
||||||
fun onTunnelToggle(checked: Boolean, tunnel: TunnelConfig) {
|
fun onTunnelToggle(checked: Boolean, tunnel: TunnelConfig) {
|
||||||
if (appViewModel.isRequiredPermissionGranted()) {
|
if (appViewModel.isRequiredPermissionGranted()) {
|
||||||
if (checked) viewModel.onTunnelStart(tunnel, context) else viewModel.onTunnelStop(
|
if (checked) viewModel.onTunnelStart(tunnel) else viewModel.onTunnelStop()
|
||||||
context,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -253,28 +254,14 @@ fun MainScreen(
|
|||||||
return LoadingScreen()
|
return LoadingScreen()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun launchQrScanner() {
|
|
||||||
val scanOptions = ScanOptions()
|
|
||||||
scanOptions.setDesiredBarcodeFormats(ScanOptions.QR_CODE)
|
|
||||||
scanOptions.setOrientationLocked(true)
|
|
||||||
scanOptions.setPrompt(
|
|
||||||
context.getString(R.string.scanning_qr),
|
|
||||||
)
|
|
||||||
scanOptions.setBeepEnabled(false)
|
|
||||||
scanLauncher.launch(scanOptions)
|
|
||||||
}
|
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
modifier =
|
modifier =
|
||||||
Modifier.pointerInput(Unit) {
|
Modifier.pointerInput(Unit) {
|
||||||
if (uiState.tunnels.isNotEmpty()) {
|
detectTapGestures(
|
||||||
detectTapGestures(
|
onTap = {
|
||||||
onTap = {
|
selectedTunnel = null
|
||||||
selectedTunnel = null
|
},
|
||||||
},
|
)
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
},
|
},
|
||||||
floatingActionButtonPosition = FabPosition.End,
|
floatingActionButtonPosition = FabPosition.End,
|
||||||
floatingActionButton = {
|
floatingActionButton = {
|
||||||
@@ -282,77 +269,49 @@ fun MainScreen(
|
|||||||
visible = isVisible.value,
|
visible = isVisible.value,
|
||||||
enter = slideInVertically(initialOffsetY = { it * 2 }),
|
enter = slideInVertically(initialOffsetY = { it * 2 }),
|
||||||
exit = slideOutVertically(targetOffsetY = { it * 2 }),
|
exit = slideOutVertically(targetOffsetY = { it * 2 }),
|
||||||
modifier = Modifier
|
|
||||||
.focusRequester(focusRequester)
|
|
||||||
.focusGroup(),
|
|
||||||
) {
|
) {
|
||||||
val secondaryColor = MaterialTheme.colorScheme.secondary
|
val secondaryColor = MaterialTheme.colorScheme.secondary
|
||||||
val tvFobColor = MaterialTheme.colorScheme.surfaceColorAtElevation(2.dp)
|
val hoverColor = MaterialTheme.colorScheme.surfaceColorAtElevation(2.dp)
|
||||||
val fobColor =
|
var fobColor by remember { mutableStateOf(secondaryColor) }
|
||||||
if (WireGuardAutoTunnel.isRunningOnAndroidTv()) tvFobColor else secondaryColor
|
FloatingActionButton(
|
||||||
val fobIconColor =
|
modifier =
|
||||||
if (WireGuardAutoTunnel.isRunningOnAndroidTv()) Color.White else MaterialTheme.colorScheme.background
|
(if (
|
||||||
MultiFloatingActionButton(
|
WireGuardAutoTunnel.isRunningOnAndroidTv() &&
|
||||||
fabIcon = FabIcon(
|
uiState.tunnels.isEmpty()
|
||||||
iconRes = R.drawable.add,
|
)
|
||||||
iconResAfterRotate = R.drawable.close,
|
Modifier.focusRequester(focusRequester)
|
||||||
iconRotate = 180f,
|
else Modifier)
|
||||||
),
|
.onFocusChanged {
|
||||||
fabOption = FabOption(
|
if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
||||||
iconTint = fobIconColor,
|
fobColor = if (it.isFocused) hoverColor else secondaryColor
|
||||||
backgroundTint = fobColor,
|
}
|
||||||
),
|
},
|
||||||
itemsMultiFab = listOf(
|
onClick = { showBottomSheet = true },
|
||||||
MultiFabItem(
|
containerColor = fobColor,
|
||||||
label = {
|
|
||||||
Text(
|
|
||||||
stringResource(id = R.string.amnezia),
|
|
||||||
color = Color.White,
|
|
||||||
textAlign = TextAlign.Center,
|
|
||||||
modifier = Modifier.padding(end = 10.dp),
|
|
||||||
)
|
|
||||||
},
|
|
||||||
modifier = Modifier
|
|
||||||
.size(40.dp),
|
|
||||||
icon = R.drawable.add,
|
|
||||||
value = ConfigType.AMNEZIA.name,
|
|
||||||
miniFabOption = FabOption(
|
|
||||||
backgroundTint = fobColor,
|
|
||||||
fobIconColor,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
MultiFabItem(
|
|
||||||
label = {
|
|
||||||
Text(
|
|
||||||
stringResource(id = R.string.wireguard),
|
|
||||||
color = Color.White,
|
|
||||||
textAlign = TextAlign.Center,
|
|
||||||
modifier = Modifier.padding(end = 10.dp),
|
|
||||||
)
|
|
||||||
},
|
|
||||||
icon = R.drawable.add,
|
|
||||||
value = ConfigType.WIREGUARD.name,
|
|
||||||
miniFabOption = FabOption(
|
|
||||||
backgroundTint = fobColor,
|
|
||||||
fobIconColor,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
onFabItemClicked = {
|
|
||||||
showBottomSheet = true
|
|
||||||
configType = ConfigType.valueOf(it.value)
|
|
||||||
},
|
|
||||||
shape = RoundedCornerShape(16.dp),
|
shape = RoundedCornerShape(16.dp),
|
||||||
)
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Rounded.Add,
|
||||||
|
contentDescription = stringResource(id = R.string.add_tunnel),
|
||||||
|
tint = Color.DarkGray,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
) {
|
) {
|
||||||
|
AnimatedVisibility(uiState.tunnels.isEmpty(), exit = fadeOut(), enter = fadeIn()) {
|
||||||
|
Column(
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
verticalArrangement = Arrangement.Center,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize(),
|
||||||
|
) {
|
||||||
|
Text(text = stringResource(R.string.no_tunnels), fontStyle = FontStyle.Italic)
|
||||||
|
}
|
||||||
|
}
|
||||||
if (showBottomSheet) {
|
if (showBottomSheet) {
|
||||||
ModalBottomSheet(
|
ModalBottomSheet(
|
||||||
onDismissRequest = {
|
onDismissRequest = { showBottomSheet = false },
|
||||||
showBottomSheet = false
|
|
||||||
|
|
||||||
},
|
|
||||||
sheetState = sheetState,
|
sheetState = sheetState,
|
||||||
) {
|
) {
|
||||||
// Sheet content
|
// Sheet content
|
||||||
@@ -385,7 +344,16 @@ fun MainScreen(
|
|||||||
.clickable {
|
.clickable {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
showBottomSheet = false
|
showBottomSheet = false
|
||||||
launchQrScanner()
|
val scanOptions = ScanOptions()
|
||||||
|
scanOptions.setDesiredBarcodeFormats(ScanOptions.QR_CODE)
|
||||||
|
scanOptions.setOrientationLocked(true)
|
||||||
|
scanOptions.setPrompt(
|
||||||
|
context.getString(R.string.scanning_qr),
|
||||||
|
)
|
||||||
|
scanOptions.setBeepEnabled(false)
|
||||||
|
scanOptions.captureActivity =
|
||||||
|
CaptureActivityPortrait::class.java
|
||||||
|
scanLauncher.launch(scanOptions)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.padding(10.dp),
|
.padding(10.dp),
|
||||||
@@ -409,7 +377,7 @@ fun MainScreen(
|
|||||||
.clickable {
|
.clickable {
|
||||||
showBottomSheet = false
|
showBottomSheet = false
|
||||||
navController.navigate(
|
navController.navigate(
|
||||||
"${Screen.Config.route}/${Constants.MANUAL_TUNNEL_CONFIG_ID}?configType=${configType}",
|
"${Screen.Config.route}/${Constants.MANUAL_TUNNEL_CONFIG_ID}",
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
.padding(10.dp),
|
.padding(10.dp),
|
||||||
@@ -433,60 +401,12 @@ fun MainScreen(
|
|||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.overscroll(ScrollableDefaults.overscrollEffect())
|
.overscroll(ScrollableDefaults.overscrollEffect()).nestedScroll(nestedScrollConnection),
|
||||||
.nestedScroll(nestedScrollConnection),
|
|
||||||
state = rememberLazyListState(0, uiState.tunnels.count()),
|
state = rememberLazyListState(0, uiState.tunnels.count()),
|
||||||
userScrollEnabled = true,
|
userScrollEnabled = true,
|
||||||
reverseLayout = false,
|
reverseLayout = false,
|
||||||
flingBehavior = ScrollableDefaults.flingBehavior(),
|
flingBehavior = ScrollableDefaults.flingBehavior(),
|
||||||
) {
|
) {
|
||||||
item {
|
|
||||||
AnimatedVisibility(
|
|
||||||
uiState.tunnels.isEmpty(), exit = fadeOut(), enter = fadeIn(),
|
|
||||||
) {
|
|
||||||
Column(
|
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
|
||||||
verticalArrangement = Arrangement.Center,
|
|
||||||
modifier = Modifier
|
|
||||||
.padding(top = 100.dp)
|
|
||||||
.fillMaxSize(),
|
|
||||||
) {
|
|
||||||
val gettingStarted = buildAnnotatedString {
|
|
||||||
append(stringResource(id = R.string.see_the))
|
|
||||||
append(" ")
|
|
||||||
pushStringAnnotation(
|
|
||||||
tag = "gettingStarted",
|
|
||||||
annotation = stringResource(id = R.string.getting_started_url),
|
|
||||||
)
|
|
||||||
withStyle(style = SpanStyle(color = MaterialTheme.colorScheme.primary)) {
|
|
||||||
append(stringResource(id = R.string.getting_started_guide))
|
|
||||||
}
|
|
||||||
pop()
|
|
||||||
append(" ")
|
|
||||||
append(stringResource(R.string.unsure_how))
|
|
||||||
append(".")
|
|
||||||
}
|
|
||||||
Text(
|
|
||||||
text = stringResource(R.string.no_tunnels),
|
|
||||||
fontStyle = FontStyle.Italic,
|
|
||||||
)
|
|
||||||
ClickableText(
|
|
||||||
modifier = Modifier
|
|
||||||
.padding(vertical = 10.dp, horizontal = 24.dp),
|
|
||||||
text = gettingStarted,
|
|
||||||
style = MaterialTheme.typography.bodyMedium.copy(
|
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
|
||||||
textAlign = TextAlign.Center,
|
|
||||||
),
|
|
||||||
) {
|
|
||||||
gettingStarted.getStringAnnotations(tag = "gettingStarted", it, it)
|
|
||||||
.firstOrNull()?.let { annotation ->
|
|
||||||
appViewModel.openWebPage(annotation.item, context)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
item {
|
item {
|
||||||
if (uiState.settings.isAutoTunnelEnabled) {
|
if (uiState.settings.isAutoTunnelEnabled) {
|
||||||
val autoTunnelingLabel = buildAnnotatedString {
|
val autoTunnelingLabel = buildAnnotatedString {
|
||||||
@@ -542,7 +462,7 @@ fun MainScreen(
|
|||||||
val leadingIconColor =
|
val leadingIconColor =
|
||||||
(if (
|
(if (
|
||||||
uiState.vpnState.tunnelConfig?.name == tunnel.name &&
|
uiState.vpnState.tunnelConfig?.name == tunnel.name &&
|
||||||
uiState.vpnState.status == TunnelState.UP
|
uiState.vpnState.status == Tunnel.State.UP
|
||||||
) {
|
) {
|
||||||
uiState.vpnState.statistics
|
uiState.vpnState.statistics
|
||||||
?.mapPeerStats()
|
?.mapPeerStats()
|
||||||
@@ -585,13 +505,13 @@ fun MainScreen(
|
|||||||
.size(if (icon == circleIcon) 15.dp else 20.dp),
|
.size(if (icon == circleIcon) 15.dp else 20.dp),
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
text = tunnel.name,
|
text = tunnel.name.truncateWithEllipsis(Constants.ALLOWED_DISPLAY_NAME_LENGTH),
|
||||||
onHold = {
|
onHold = {
|
||||||
if (
|
if (
|
||||||
(uiState.vpnState.status == TunnelState.UP) &&
|
(uiState.vpnState.status == Tunnel.State.UP) &&
|
||||||
(tunnel.name == uiState.vpnState.tunnelConfig?.name)
|
(tunnel.name == uiState.vpnState.tunnelConfig?.name)
|
||||||
) {
|
) {
|
||||||
appViewModel.showSnackbarMessage(context.getString(R.string.turn_off_tunnel))
|
appViewModel.showSnackbarMessage(Event.Message.TunnelOffAction.message)
|
||||||
return@RowListItem
|
return@RowListItem
|
||||||
}
|
}
|
||||||
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
|
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
|
||||||
@@ -600,7 +520,7 @@ fun MainScreen(
|
|||||||
onClick = {
|
onClick = {
|
||||||
if (!WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
if (!WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
||||||
if (
|
if (
|
||||||
uiState.vpnState.status == TunnelState.UP &&
|
uiState.vpnState.status == Tunnel.State.UP &&
|
||||||
(uiState.vpnState.tunnelConfig?.name == tunnel.name)
|
(uiState.vpnState.tunnelConfig?.name == tunnel.name)
|
||||||
) {
|
) {
|
||||||
expanded.value = !expanded.value
|
expanded.value = !expanded.value
|
||||||
@@ -625,7 +545,7 @@ fun MainScreen(
|
|||||||
!uiState.settings.isAutoTunnelPaused
|
!uiState.settings.isAutoTunnelPaused
|
||||||
) {
|
) {
|
||||||
appViewModel.showSnackbarMessage(
|
appViewModel.showSnackbarMessage(
|
||||||
context.getString(R.string.turn_off_tunnel),
|
Event.Message.AutoTunnelOffAction.message,
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
navController.navigate(
|
navController.navigate(
|
||||||
@@ -658,7 +578,7 @@ fun MainScreen(
|
|||||||
} else {
|
} else {
|
||||||
val checked by remember {
|
val checked by remember {
|
||||||
derivedStateOf {
|
derivedStateOf {
|
||||||
(uiState.vpnState.status == TunnelState.UP &&
|
(uiState.vpnState.status == Tunnel.State.UP &&
|
||||||
tunnel.name == uiState.vpnState.tunnelConfig?.name)
|
tunnel.name == uiState.vpnState.tunnelConfig?.name)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -680,7 +600,7 @@ fun MainScreen(
|
|||||||
onClick = {
|
onClick = {
|
||||||
if (uiState.settings.isAutoTunnelEnabled) {
|
if (uiState.settings.isAutoTunnelEnabled) {
|
||||||
appViewModel.showSnackbarMessage(
|
appViewModel.showSnackbarMessage(
|
||||||
context.getString(R.string.turn_off_auto),
|
Event.Message.AutoTunnelOffAction.message,
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
selectedTunnel = tunnel
|
selectedTunnel = tunnel
|
||||||
@@ -700,13 +620,13 @@ fun MainScreen(
|
|||||||
modifier = Modifier.focusRequester(focusRequester),
|
modifier = Modifier.focusRequester(focusRequester),
|
||||||
onClick = {
|
onClick = {
|
||||||
if (
|
if (
|
||||||
uiState.vpnState.status == TunnelState.UP &&
|
uiState.vpnState.status == Tunnel.State.UP &&
|
||||||
(uiState.vpnState.tunnelConfig?.name == tunnel.name)
|
(uiState.vpnState.tunnelConfig?.name == tunnel.name)
|
||||||
) {
|
) {
|
||||||
expanded.value = !expanded.value
|
expanded.value = !expanded.value
|
||||||
} else {
|
} else {
|
||||||
appViewModel.showSnackbarMessage(
|
appViewModel.showSnackbarMessage(
|
||||||
context.getString(R.string.turn_on_tunnel),
|
Event.Message.TunnelOnAction.message,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -723,11 +643,11 @@ fun MainScreen(
|
|||||||
IconButton(
|
IconButton(
|
||||||
onClick = {
|
onClick = {
|
||||||
if (
|
if (
|
||||||
uiState.vpnState.status == TunnelState.UP &&
|
uiState.vpnState.status == Tunnel.State.UP &&
|
||||||
tunnel.name == uiState.vpnState.tunnelConfig?.name
|
tunnel.name == uiState.vpnState.tunnelConfig?.name
|
||||||
) {
|
) {
|
||||||
appViewModel.showSnackbarMessage(
|
appViewModel.showSnackbarMessage(
|
||||||
context.getString(R.string.turn_off_tunnel),
|
Event.Message.TunnelOffAction.message,
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
selectedTunnel = tunnel
|
selectedTunnel = tunnel
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.ui.screens.main
|
package com.zaneschepke.wireguardautotunnel.ui.screens.main
|
||||||
|
|
||||||
import com.zaneschepke.wireguardautotunnel.data.domain.Settings
|
import com.zaneschepke.wireguardautotunnel.data.model.Settings
|
||||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnState
|
import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnState
|
||||||
import com.zaneschepke.wireguardautotunnel.util.TunnelConfigs
|
import com.zaneschepke.wireguardautotunnel.util.TunnelConfigs
|
||||||
|
|
||||||
|
|||||||
+94
-220
@@ -1,30 +1,30 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.ui.screens.main
|
package com.zaneschepke.wireguardautotunnel.ui.screens.main
|
||||||
|
|
||||||
|
import android.app.Application
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.database.Cursor
|
import android.database.Cursor
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.provider.OpenableColumns
|
import android.provider.OpenableColumns
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import com.wireguard.config.Config
|
|
||||||
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
|
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
|
||||||
import com.zaneschepke.wireguardautotunnel.data.domain.Settings
|
import com.zaneschepke.wireguardautotunnel.data.model.Settings
|
||||||
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
|
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
|
||||||
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
|
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
|
||||||
import com.zaneschepke.wireguardautotunnel.module.IoDispatcher
|
|
||||||
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
|
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
|
||||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnService
|
import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnService
|
||||||
import com.zaneschepke.wireguardautotunnel.util.Constants
|
import com.zaneschepke.wireguardautotunnel.util.Constants
|
||||||
|
import com.zaneschepke.wireguardautotunnel.util.Event
|
||||||
import com.zaneschepke.wireguardautotunnel.util.NumberUtils
|
import com.zaneschepke.wireguardautotunnel.util.NumberUtils
|
||||||
import com.zaneschepke.wireguardautotunnel.util.WgTunnelExceptions
|
import com.zaneschepke.wireguardautotunnel.util.Result
|
||||||
import com.zaneschepke.wireguardautotunnel.util.toWgQuickString
|
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import kotlinx.coroutines.CoroutineDispatcher
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.flow.SharingStarted
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
import kotlinx.coroutines.flow.combine
|
import kotlinx.coroutines.flow.combine
|
||||||
import kotlinx.coroutines.flow.stateIn
|
import kotlinx.coroutines.flow.stateIn
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
import org.amnezia.awg.config.Config
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
import java.util.zip.ZipInputStream
|
import java.util.zip.ZipInputStream
|
||||||
@@ -34,10 +34,10 @@ import javax.inject.Inject
|
|||||||
class MainViewModel
|
class MainViewModel
|
||||||
@Inject
|
@Inject
|
||||||
constructor(
|
constructor(
|
||||||
|
private val application: Application,
|
||||||
private val appDataRepository: AppDataRepository,
|
private val appDataRepository: AppDataRepository,
|
||||||
private val serviceManager: ServiceManager,
|
private val serviceManager: ServiceManager,
|
||||||
val vpnService: VpnService,
|
val vpnService: VpnService
|
||||||
@IoDispatcher private val ioDispatcher: CoroutineDispatcher
|
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
|
|
||||||
val uiState =
|
val uiState =
|
||||||
@@ -54,20 +54,21 @@ constructor(
|
|||||||
MainUiState(),
|
MainUiState(),
|
||||||
)
|
)
|
||||||
|
|
||||||
private fun stopWatcherService(context: Context) {
|
private fun stopWatcherService() =
|
||||||
serviceManager.stopWatcherService(context)
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
}
|
serviceManager.stopWatcherService(application.applicationContext)
|
||||||
|
}
|
||||||
|
|
||||||
fun onDelete(tunnel: TunnelConfig, context: Context) {
|
fun onDelete(tunnel: TunnelConfig) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
val settings = appDataRepository.settings.getSettings()
|
val settings = appDataRepository.settings.getSettings()
|
||||||
val isPrimary = tunnel.isPrimaryTunnel
|
val isPrimary = tunnel.isPrimaryTunnel
|
||||||
if (appDataRepository.tunnels.count() == 1 || isPrimary) {
|
if (appDataRepository.tunnels.count() == 1 || isPrimary) {
|
||||||
stopWatcherService(context)
|
stopWatcherService()
|
||||||
resetTunnelSetting(settings)
|
resetTunnelSetting(settings)
|
||||||
}
|
}
|
||||||
appDataRepository.tunnels.delete(tunnel)
|
appDataRepository.tunnels.delete(tunnel)
|
||||||
WireGuardAutoTunnel.requestTunnelTileServiceStateUpdate()
|
WireGuardAutoTunnel.requestTunnelTileServiceStateUpdate(application)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -80,251 +81,124 @@ constructor(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onTunnelStart(tunnelConfig: TunnelConfig, context: Context) =
|
fun onTunnelStart(tunnelConfig: TunnelConfig) =
|
||||||
viewModelScope.launch {
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
Timber.d("On start called!")
|
Timber.d("On start called!")
|
||||||
serviceManager.startVpnService(
|
serviceManager.startVpnService(
|
||||||
context,
|
application.applicationContext,
|
||||||
tunnelConfig.id,
|
tunnelConfig.id,
|
||||||
isManualStart = true,
|
isManualStart = true,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
fun onTunnelStop(context: Context) =
|
fun onTunnelStop() =
|
||||||
viewModelScope.launch {
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
Timber.i("Stopping active tunnel")
|
Timber.i("Stopping active tunnel")
|
||||||
serviceManager.stopVpnService(context, isManualStop = true)
|
serviceManager.stopVpnService(application.applicationContext, isManualStop = true)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun validateConfigString(config: String, configType: ConfigType) {
|
private fun validateConfigString(config: String) {
|
||||||
when (configType) {
|
TunnelConfig.configFromQuick(config)
|
||||||
ConfigType.AMNEZIA -> TunnelConfig.configFromAmQuick(config)
|
}
|
||||||
ConfigType.WIREGUARD -> TunnelConfig.configFromWgQuick(config)
|
|
||||||
|
suspend fun onTunnelQrResult(result: String): Result<Unit> {
|
||||||
|
return try {
|
||||||
|
validateConfigString(result)
|
||||||
|
val tunnelConfig =
|
||||||
|
TunnelConfig(name = NumberUtils.generateRandomTunnelName(), wgQuick = result)
|
||||||
|
addTunnel(tunnelConfig)
|
||||||
|
Result.Success(Unit)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Timber.e(e)
|
||||||
|
Result.Error(Event.Error.InvalidQrCode)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun generateQrCodeDefaultName(config: String, configType: ConfigType): String {
|
private suspend fun saveTunnelConfigFromStream(stream: InputStream, fileName: String) {
|
||||||
return try {
|
val bufferReader = stream.bufferedReader(charset = Charsets.UTF_8)
|
||||||
when (configType) {
|
val config = Config.parse(bufferReader)
|
||||||
ConfigType.AMNEZIA -> {
|
val tunnelName = getNameFromFileName(fileName)
|
||||||
TunnelConfig.configFromAmQuick(config).peers[0].endpoint.get().host
|
addTunnel(TunnelConfig(name = tunnelName, wgQuick = config.toAwgQuickString()))
|
||||||
}
|
withContext(Dispatchers.IO) { stream.close() }
|
||||||
|
}
|
||||||
|
|
||||||
ConfigType.WIREGUARD -> {
|
private fun getInputStreamFromUri(uri: Uri): InputStream? {
|
||||||
TunnelConfig.configFromWgQuick(config).peers[0].endpoint.get().host
|
return application.applicationContext.contentResolver.openInputStream(uri)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun onTunnelFileSelected(uri: Uri): Result<Unit> {
|
||||||
|
try {
|
||||||
|
if (isValidUriContentScheme(uri)) {
|
||||||
|
val fileName = getFileName(application.applicationContext, uri)
|
||||||
|
when (getFileExtensionFromFileName(fileName)) {
|
||||||
|
Constants.CONF_FILE_EXTENSION ->
|
||||||
|
saveTunnelFromConfUri(fileName, uri).let {
|
||||||
|
when (it) {
|
||||||
|
is Result.Error -> return Result.Error(Event.Error.FileReadFailed)
|
||||||
|
is Result.Success -> return it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Constants.ZIP_FILE_EXTENSION -> saveTunnelsFromZipUri(uri)
|
||||||
|
else -> return Result.Error(Event.Error.InvalidFileExtension)
|
||||||
}
|
}
|
||||||
|
return Result.Success(Unit)
|
||||||
|
} else {
|
||||||
|
return Result.Error(Event.Error.InvalidFileExtension)
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Timber.e(e)
|
Timber.e(e)
|
||||||
NumberUtils.generateRandomTunnelName()
|
return Result.Error(Event.Error.FileReadFailed)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun generateQrCodeTunnelName(config: String, configType: ConfigType): String {
|
private suspend fun saveTunnelsFromZipUri(uri: Uri) {
|
||||||
var defaultName = generateQrCodeDefaultName(config, configType)
|
ZipInputStream(getInputStreamFromUri(uri)).use { zip ->
|
||||||
val lines = config.lines().toMutableList()
|
generateSequence { zip.nextEntry }
|
||||||
val linesIterator = lines.iterator()
|
.filterNot {
|
||||||
while (linesIterator.hasNext()) {
|
it.isDirectory ||
|
||||||
val next = linesIterator.next()
|
getFileExtensionFromFileName(it.name) != Constants.CONF_FILE_EXTENSION
|
||||||
if (next.contains(Constants.QR_CODE_NAME_PROPERTY)) {
|
}
|
||||||
defaultName = next.substringAfter(Constants.QR_CODE_NAME_PROPERTY).trim()
|
.forEach {
|
||||||
break
|
val name = getNameFromFileName(it.name)
|
||||||
}
|
val config = Config.parse(zip)
|
||||||
}
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
return defaultName
|
addTunnel(TunnelConfig(name = name, wgQuick = config.toAwgQuickString()))
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun onTunnelQrResult(result: String, configType: ConfigType): Result<Unit> {
|
|
||||||
return withContext(ioDispatcher) {
|
|
||||||
try {
|
|
||||||
validateConfigString(result, configType)
|
|
||||||
val tunnelName = makeTunnelNameUnique(generateQrCodeTunnelName(result, configType))
|
|
||||||
val tunnelConfig = when (configType) {
|
|
||||||
ConfigType.AMNEZIA -> {
|
|
||||||
TunnelConfig(
|
|
||||||
name = tunnelName, amQuick = result,
|
|
||||||
wgQuick = TunnelConfig.configFromAmQuick(result).toWgQuickString(),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ConfigType.WIREGUARD -> TunnelConfig(name = tunnelName, wgQuick = result)
|
|
||||||
}
|
}
|
||||||
addTunnel(tunnelConfig)
|
|
||||||
Result.success(Unit)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Timber.e(e)
|
|
||||||
Result.failure(WgTunnelExceptions.InvalidQrCode())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun makeTunnelNameUnique(name: String): String {
|
private suspend fun saveTunnelFromConfUri(name: String, uri: Uri): Result<Unit> {
|
||||||
return withContext(ioDispatcher) {
|
val stream = getInputStreamFromUri(uri)
|
||||||
val tunnels = appDataRepository.tunnels.getAll()
|
return if (stream != null) {
|
||||||
var tunnelName = name
|
saveTunnelConfigFromStream(stream, name)
|
||||||
var num = 1
|
Result.Success(Unit)
|
||||||
while (tunnels.any { it.name == tunnelName }) {
|
} else {
|
||||||
tunnelName = name + "(${num})"
|
Result.Error(Event.Error.FileReadFailed)
|
||||||
num++
|
|
||||||
}
|
|
||||||
tunnelName
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun saveTunnelConfigFromStream(
|
private suspend fun addTunnel(tunnelConfig: TunnelConfig) {
|
||||||
stream: InputStream,
|
|
||||||
fileName: String,
|
|
||||||
type: ConfigType
|
|
||||||
) {
|
|
||||||
var amQuick: String? = null
|
|
||||||
val wgQuick = stream.use {
|
|
||||||
when (type) {
|
|
||||||
ConfigType.AMNEZIA -> {
|
|
||||||
val config = org.amnezia.awg.config.Config.parse(it)
|
|
||||||
amQuick = config.toAwgQuickString()
|
|
||||||
config.toWgQuickString()
|
|
||||||
}
|
|
||||||
|
|
||||||
ConfigType.WIREGUARD -> {
|
|
||||||
Config.parse(it).toWgQuickString()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
viewModelScope.launch {
|
|
||||||
val tunnelName = makeTunnelNameUnique(getNameFromFileName(fileName))
|
|
||||||
addTunnel(
|
|
||||||
TunnelConfig(
|
|
||||||
name = tunnelName,
|
|
||||||
wgQuick = wgQuick,
|
|
||||||
amQuick = amQuick ?: TunnelConfig.AM_QUICK_DEFAULT,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getInputStreamFromUri(uri: Uri, context: Context): InputStream? {
|
|
||||||
return context.applicationContext.contentResolver.openInputStream(uri)
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun onTunnelFileSelected(
|
|
||||||
uri: Uri,
|
|
||||||
configType: ConfigType,
|
|
||||||
context: Context
|
|
||||||
): Result<Unit> {
|
|
||||||
return withContext(ioDispatcher) {
|
|
||||||
try {
|
|
||||||
if (isValidUriContentScheme(uri)) {
|
|
||||||
val fileName = getFileName(context, uri)
|
|
||||||
return@withContext when (getFileExtensionFromFileName(fileName)) {
|
|
||||||
Constants.CONF_FILE_EXTENSION ->
|
|
||||||
saveTunnelFromConfUri(fileName, uri, configType, context)
|
|
||||||
|
|
||||||
Constants.ZIP_FILE_EXTENSION -> saveTunnelsFromZipUri(
|
|
||||||
uri,
|
|
||||||
configType,
|
|
||||||
context,
|
|
||||||
)
|
|
||||||
|
|
||||||
else -> Result.failure(WgTunnelExceptions.InvalidFileExtension())
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Result.failure(WgTunnelExceptions.InvalidFileExtension())
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Timber.e(e)
|
|
||||||
Result.failure(WgTunnelExceptions.FileReadFailed())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun saveTunnelsFromZipUri(
|
|
||||||
uri: Uri,
|
|
||||||
configType: ConfigType,
|
|
||||||
context: Context
|
|
||||||
): Result<Unit> {
|
|
||||||
return withContext(ioDispatcher) {
|
|
||||||
ZipInputStream(getInputStreamFromUri(uri, context)).use { zip ->
|
|
||||||
generateSequence { zip.nextEntry }
|
|
||||||
.filterNot {
|
|
||||||
it.isDirectory ||
|
|
||||||
getFileExtensionFromFileName(it.name) != Constants.CONF_FILE_EXTENSION
|
|
||||||
}
|
|
||||||
.forEach {
|
|
||||||
val name = getNameFromFileName(it.name)
|
|
||||||
withContext(viewModelScope.coroutineContext) {
|
|
||||||
try {
|
|
||||||
var amQuick: String? = null
|
|
||||||
val wgQuick =
|
|
||||||
when (configType) {
|
|
||||||
ConfigType.AMNEZIA -> {
|
|
||||||
val config = org.amnezia.awg.config.Config.parse(zip)
|
|
||||||
amQuick = config.toAwgQuickString()
|
|
||||||
config.toWgQuickString()
|
|
||||||
}
|
|
||||||
|
|
||||||
ConfigType.WIREGUARD -> {
|
|
||||||
Config.parse(zip).toWgQuickString()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
addTunnel(
|
|
||||||
TunnelConfig(
|
|
||||||
name = makeTunnelNameUnique(name),
|
|
||||||
wgQuick = wgQuick,
|
|
||||||
amQuick = amQuick ?: TunnelConfig.AM_QUICK_DEFAULT,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
Result.success(Unit)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Result.failure(WgTunnelExceptions.FileReadFailed())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Result.success(Unit)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun saveTunnelFromConfUri(
|
|
||||||
name: String,
|
|
||||||
uri: Uri,
|
|
||||||
configType: ConfigType,
|
|
||||||
context: Context
|
|
||||||
): Result<Unit> {
|
|
||||||
return withContext(ioDispatcher) {
|
|
||||||
val stream = getInputStreamFromUri(uri, context)
|
|
||||||
return@withContext if (stream != null) {
|
|
||||||
try {
|
|
||||||
saveTunnelConfigFromStream(stream, name, configType)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
return@withContext Result.failure(WgTunnelExceptions.ConfigParseError())
|
|
||||||
}
|
|
||||||
Result.success(Unit)
|
|
||||||
} else {
|
|
||||||
Result.failure(WgTunnelExceptions.FileReadFailed())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun addTunnel(tunnelConfig: TunnelConfig) = viewModelScope.launch {
|
|
||||||
val firstTunnel = appDataRepository.tunnels.count() == 0
|
val firstTunnel = appDataRepository.tunnels.count() == 0
|
||||||
saveTunnel(tunnelConfig)
|
saveTunnel(tunnelConfig)
|
||||||
if (firstTunnel) WireGuardAutoTunnel.requestTunnelTileServiceStateUpdate()
|
if (firstTunnel) WireGuardAutoTunnel.requestTunnelTileServiceStateUpdate(application)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun pauseAutoTunneling() =
|
fun pauseAutoTunneling() =
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
appDataRepository.settings.save(uiState.value.settings.copy(isAutoTunnelPaused = true))
|
appDataRepository.settings.save(uiState.value.settings.copy(isAutoTunnelPaused = true))
|
||||||
WireGuardAutoTunnel.requestAutoTunnelTileServiceUpdate()
|
WireGuardAutoTunnel.requestAutoTunnelTileServiceUpdate(application)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun resumeAutoTunneling() =
|
fun resumeAutoTunneling() =
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
appDataRepository.settings.save(uiState.value.settings.copy(isAutoTunnelPaused = false))
|
appDataRepository.settings.save(uiState.value.settings.copy(isAutoTunnelPaused = false))
|
||||||
WireGuardAutoTunnel.requestAutoTunnelTileServiceUpdate()
|
WireGuardAutoTunnel.requestAutoTunnelTileServiceUpdate(application)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun saveTunnel(tunnelConfig: TunnelConfig) = viewModelScope.launch {
|
private suspend fun saveTunnel(tunnelConfig: TunnelConfig) {
|
||||||
appDataRepository.tunnels.save(tunnelConfig)
|
appDataRepository.tunnels.save(tunnelConfig)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -365,17 +239,17 @@ constructor(
|
|||||||
return fileName.substring(0, fileName.lastIndexOf('.'))
|
return fileName.substring(0, fileName.lastIndexOf('.'))
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getFileExtensionFromFileName(fileName: String): String? {
|
private fun getFileExtensionFromFileName(fileName: String): String {
|
||||||
return try {
|
return try {
|
||||||
fileName.substring(fileName.lastIndexOf('.'))
|
fileName.substring(fileName.lastIndexOf('.'))
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Timber.e(e)
|
Timber.e(e)
|
||||||
null
|
""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun saveSettings(settings: Settings) =
|
private fun saveSettings(settings: Settings) =
|
||||||
viewModelScope.launch { appDataRepository.settings.save(settings) }
|
viewModelScope.launch(Dispatchers.IO) { appDataRepository.settings.save(settings) }
|
||||||
|
|
||||||
|
|
||||||
fun onCopyTunnel(tunnel: TunnelConfig?) = viewModelScope.launch {
|
fun onCopyTunnel(tunnel: TunnelConfig?) = viewModelScope.launch {
|
||||||
|
|||||||
+192
-264
@@ -1,22 +1,17 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.ui.screens.options
|
package com.zaneschepke.wireguardautotunnel.ui.screens.options
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import androidx.compose.animation.AnimatedVisibility
|
|
||||||
import androidx.compose.animation.slideInVertically
|
|
||||||
import androidx.compose.animation.slideOutVertically
|
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.focusGroup
|
|
||||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
||||||
import androidx.compose.foundation.layout.FlowRow
|
import androidx.compose.foundation.layout.FlowRow
|
||||||
import androidx.compose.foundation.layout.IntrinsicSize
|
import androidx.compose.foundation.layout.IntrinsicSize
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
|
||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.foundation.rememberScrollState
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.foundation.text.KeyboardActions
|
import androidx.compose.foundation.text.KeyboardActions
|
||||||
@@ -29,10 +24,9 @@ import androidx.compose.material3.Icon
|
|||||||
import androidx.compose.material3.IconButton
|
import androidx.compose.material3.IconButton
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.OutlinedTextField
|
import androidx.compose.material3.OutlinedTextField
|
||||||
import androidx.compose.material3.Scaffold
|
|
||||||
import androidx.compose.material3.Surface
|
import androidx.compose.material3.Surface
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.surfaceColorAtElevation
|
import androidx.compose.material3.TextButton
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
@@ -45,21 +39,15 @@ import androidx.compose.ui.Modifier
|
|||||||
import androidx.compose.ui.focus.FocusRequester
|
import androidx.compose.ui.focus.FocusRequester
|
||||||
import androidx.compose.ui.focus.focusRequester
|
import androidx.compose.ui.focus.focusRequester
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.platform.LocalContext
|
|
||||||
import androidx.compose.ui.platform.LocalFocusManager
|
import androidx.compose.ui.platform.LocalFocusManager
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.font.FontStyle
|
import androidx.compose.ui.text.font.FontStyle
|
||||||
import androidx.compose.ui.text.input.ImeAction
|
import androidx.compose.ui.text.input.ImeAction
|
||||||
import androidx.compose.ui.text.input.KeyboardCapitalization
|
import androidx.compose.ui.text.input.KeyboardCapitalization
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.hilt.navigation.compose.hiltViewModel
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import androidx.navigation.NavController
|
import androidx.navigation.NavController
|
||||||
import com.iamageo.multifablibrary.FabIcon
|
|
||||||
import com.iamageo.multifablibrary.FabOption
|
|
||||||
import com.iamageo.multifablibrary.MultiFabItem
|
|
||||||
import com.iamageo.multifablibrary.MultiFloatingActionButton
|
|
||||||
import com.zaneschepke.wireguardautotunnel.R
|
import com.zaneschepke.wireguardautotunnel.R
|
||||||
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
|
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.AppViewModel
|
import com.zaneschepke.wireguardautotunnel.ui.AppViewModel
|
||||||
@@ -67,13 +55,11 @@ import com.zaneschepke.wireguardautotunnel.ui.Screen
|
|||||||
import com.zaneschepke.wireguardautotunnel.ui.common.ClickableIconButton
|
import com.zaneschepke.wireguardautotunnel.ui.common.ClickableIconButton
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.common.config.ConfigurationToggle
|
import com.zaneschepke.wireguardautotunnel.ui.common.config.ConfigurationToggle
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.common.text.SectionTitle
|
import com.zaneschepke.wireguardautotunnel.ui.common.text.SectionTitle
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.screens.main.ConfigType
|
|
||||||
import com.zaneschepke.wireguardautotunnel.util.Constants
|
import com.zaneschepke.wireguardautotunnel.util.Constants
|
||||||
import com.zaneschepke.wireguardautotunnel.util.getMessage
|
import com.zaneschepke.wireguardautotunnel.util.Result
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter")
|
|
||||||
@OptIn(ExperimentalLayoutApi::class)
|
@OptIn(ExperimentalLayoutApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun OptionsScreen(
|
fun OptionsScreen(
|
||||||
@@ -85,8 +71,6 @@ fun OptionsScreen(
|
|||||||
) {
|
) {
|
||||||
val scrollState = rememberScrollState()
|
val scrollState = rememberScrollState()
|
||||||
val uiState by optionsViewModel.uiState.collectAsStateWithLifecycle()
|
val uiState by optionsViewModel.uiState.collectAsStateWithLifecycle()
|
||||||
val context = LocalContext.current
|
|
||||||
|
|
||||||
val interactionSource = remember { MutableInteractionSource() }
|
val interactionSource = remember { MutableInteractionSource() }
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
val focusManager = LocalFocusManager.current
|
val focusManager = LocalFocusManager.current
|
||||||
@@ -106,254 +90,198 @@ fun OptionsScreen(
|
|||||||
fun saveTrustedSSID() {
|
fun saveTrustedSSID() {
|
||||||
if (currentText.isNotEmpty()) {
|
if (currentText.isNotEmpty()) {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
optionsViewModel.onSaveRunSSID(currentText).onSuccess {
|
optionsViewModel.onSaveRunSSID(currentText).let {
|
||||||
currentText = ""
|
when (it) {
|
||||||
}.onFailure {
|
is Result.Success -> currentText = ""
|
||||||
appViewModel.showSnackbarMessage(it.getMessage(context))
|
is Result.Error -> appViewModel.showSnackbarMessage(it.error.message)
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Scaffold(
|
|
||||||
floatingActionButton = {
|
|
||||||
val secondaryColor = MaterialTheme.colorScheme.secondary
|
|
||||||
val tvFobColor = MaterialTheme.colorScheme.surfaceColorAtElevation(2.dp)
|
|
||||||
val fobColor =
|
|
||||||
if (WireGuardAutoTunnel.isRunningOnAndroidTv()) tvFobColor else secondaryColor
|
|
||||||
val fobIconColor =
|
|
||||||
if (WireGuardAutoTunnel.isRunningOnAndroidTv()) Color.White else MaterialTheme.colorScheme.background
|
|
||||||
AnimatedVisibility(
|
|
||||||
visible = true,
|
|
||||||
enter = slideInVertically(initialOffsetY = { it * 2 }),
|
|
||||||
exit = slideOutVertically(targetOffsetY = { it * 2 }),
|
|
||||||
modifier = Modifier
|
|
||||||
.focusRequester(focusRequester)
|
|
||||||
.focusGroup(),
|
|
||||||
) {
|
|
||||||
MultiFloatingActionButton(
|
|
||||||
fabIcon = FabIcon(
|
|
||||||
iconRes = R.drawable.edit,
|
|
||||||
iconResAfterRotate = R.drawable.close,
|
|
||||||
iconRotate = 180f,
|
|
||||||
),
|
|
||||||
fabOption = FabOption(
|
|
||||||
iconTint = fobIconColor,
|
|
||||||
backgroundTint = fobColor,
|
|
||||||
),
|
|
||||||
itemsMultiFab = listOf(
|
|
||||||
MultiFabItem(
|
|
||||||
label = {
|
|
||||||
Text(
|
|
||||||
stringResource(id = R.string.amnezia),
|
|
||||||
color = Color.White,
|
|
||||||
textAlign = TextAlign.Center,
|
|
||||||
modifier = Modifier.padding(end = 10.dp),
|
|
||||||
)
|
|
||||||
},
|
|
||||||
modifier = Modifier
|
|
||||||
.size(40.dp),
|
|
||||||
icon = R.drawable.edit,
|
|
||||||
value = ConfigType.AMNEZIA.name,
|
|
||||||
miniFabOption = FabOption(
|
|
||||||
backgroundTint = fobColor,
|
|
||||||
fobIconColor,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
MultiFabItem(
|
|
||||||
label = {
|
|
||||||
Text(
|
|
||||||
stringResource(id = R.string.wireguard),
|
|
||||||
color = Color.White,
|
|
||||||
textAlign = TextAlign.Center,
|
|
||||||
modifier = Modifier.padding(end = 10.dp),
|
|
||||||
)
|
|
||||||
},
|
|
||||||
icon = R.drawable.edit,
|
|
||||||
value = ConfigType.WIREGUARD.name,
|
|
||||||
miniFabOption = FabOption(
|
|
||||||
backgroundTint = fobColor,
|
|
||||||
fobIconColor,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
onFabItemClicked = {
|
|
||||||
val configType = ConfigType.valueOf(it.value)
|
|
||||||
navController.navigate(
|
|
||||||
"${Screen.Config.route}/${tunnelId}?configType=${configType.name}",
|
|
||||||
)
|
|
||||||
},
|
|
||||||
shape = RoundedCornerShape(16.dp),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
) {
|
|
||||||
Column(
|
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
|
||||||
verticalArrangement = Arrangement.Top,
|
|
||||||
modifier =
|
|
||||||
Modifier
|
|
||||||
.fillMaxSize()
|
|
||||||
.verticalScroll(scrollState)
|
|
||||||
.clickable(
|
|
||||||
indication = null,
|
|
||||||
interactionSource = interactionSource,
|
|
||||||
) {
|
|
||||||
focusManager.clearFocus()
|
|
||||||
},
|
|
||||||
) {
|
|
||||||
Surface(
|
|
||||||
tonalElevation = 2.dp,
|
|
||||||
shadowElevation = 2.dp,
|
|
||||||
shape = RoundedCornerShape(12.dp),
|
|
||||||
color = MaterialTheme.colorScheme.surface,
|
|
||||||
modifier =
|
|
||||||
(if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
|
||||||
Modifier
|
|
||||||
.height(IntrinsicSize.Min)
|
|
||||||
.fillMaxWidth(fillMaxWidth)
|
|
||||||
.padding(top = 10.dp)
|
|
||||||
} else {
|
|
||||||
Modifier
|
|
||||||
.fillMaxWidth(fillMaxWidth)
|
|
||||||
.padding(top = 20.dp)
|
|
||||||
})
|
|
||||||
.padding(bottom = 10.dp),
|
|
||||||
) {
|
|
||||||
Column(
|
|
||||||
horizontalAlignment = Alignment.Start,
|
|
||||||
verticalArrangement = Arrangement.Top,
|
|
||||||
modifier = Modifier.padding(15.dp),
|
|
||||||
) {
|
|
||||||
SectionTitle(
|
|
||||||
title = stringResource(id = R.string.general),
|
|
||||||
padding = screenPadding,
|
|
||||||
)
|
|
||||||
ConfigurationToggle(
|
|
||||||
stringResource(R.string.set_primary_tunnel),
|
|
||||||
enabled = true,
|
|
||||||
checked = uiState.isDefaultTunnel,
|
|
||||||
modifier = Modifier
|
|
||||||
.focusRequester(focusRequester),
|
|
||||||
padding = screenPadding,
|
|
||||||
onCheckChanged = { optionsViewModel.onTogglePrimaryTunnel() },
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Surface(
|
|
||||||
tonalElevation = 2.dp,
|
|
||||||
shadowElevation = 2.dp,
|
|
||||||
shape = RoundedCornerShape(12.dp),
|
|
||||||
color = MaterialTheme.colorScheme.surface,
|
|
||||||
modifier =
|
|
||||||
(if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
|
||||||
Modifier
|
|
||||||
.height(IntrinsicSize.Min)
|
|
||||||
.fillMaxWidth(fillMaxWidth)
|
|
||||||
.padding(top = 10.dp)
|
|
||||||
} else {
|
|
||||||
Modifier
|
|
||||||
.fillMaxWidth(fillMaxWidth)
|
|
||||||
.padding(top = 20.dp)
|
|
||||||
})
|
|
||||||
.padding(bottom = 10.dp),
|
|
||||||
) {
|
|
||||||
Column(
|
|
||||||
horizontalAlignment = Alignment.Start,
|
|
||||||
verticalArrangement = Arrangement.Top,
|
|
||||||
modifier = Modifier.padding(15.dp),
|
|
||||||
) {
|
|
||||||
SectionTitle(
|
|
||||||
title = stringResource(id = R.string.auto_tunneling),
|
|
||||||
padding = screenPadding,
|
|
||||||
)
|
|
||||||
ConfigurationToggle(
|
|
||||||
stringResource(R.string.mobile_data_tunnel),
|
|
||||||
enabled = true,
|
|
||||||
checked = uiState.tunnel?.isMobileDataTunnel == true,
|
|
||||||
padding = screenPadding,
|
|
||||||
onCheckChanged = { optionsViewModel.onToggleIsMobileDataTunnel() },
|
|
||||||
)
|
|
||||||
Column {
|
|
||||||
FlowRow(
|
|
||||||
modifier = Modifier
|
|
||||||
.padding(screenPadding)
|
|
||||||
.fillMaxWidth(),
|
|
||||||
horizontalArrangement = Arrangement.spacedBy(5.dp),
|
|
||||||
) {
|
|
||||||
uiState.tunnel?.tunnelNetworks?.forEach { ssid ->
|
|
||||||
ClickableIconButton(
|
|
||||||
onClick = {
|
|
||||||
if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
|
||||||
focusRequester.requestFocus()
|
|
||||||
optionsViewModel.onDeleteRunSSID(ssid)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onIconClick = {
|
|
||||||
if (WireGuardAutoTunnel.isRunningOnAndroidTv()) focusRequester.requestFocus()
|
|
||||||
optionsViewModel.onDeleteRunSSID(ssid)
|
|
||||||
|
|
||||||
},
|
|
||||||
text = ssid,
|
|
||||||
icon = Icons.Filled.Close,
|
|
||||||
enabled = true,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
if (uiState.tunnel == null || uiState.tunnel?.tunnelNetworks?.isEmpty() == true) {
|
|
||||||
Text(
|
|
||||||
stringResource(R.string.no_wifi_names_configured),
|
|
||||||
fontStyle = FontStyle.Italic,
|
|
||||||
color = Color.Gray,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
OutlinedTextField(
|
|
||||||
enabled = true,
|
|
||||||
value = currentText,
|
|
||||||
onValueChange = { currentText = it },
|
|
||||||
label = { Text(stringResource(id = R.string.use_tunnel_on_wifi_name)) },
|
|
||||||
modifier =
|
|
||||||
Modifier
|
|
||||||
.padding(
|
|
||||||
start = screenPadding,
|
|
||||||
top = 5.dp,
|
|
||||||
bottom = 10.dp,
|
|
||||||
),
|
|
||||||
maxLines = 1,
|
|
||||||
keyboardOptions =
|
|
||||||
KeyboardOptions(
|
|
||||||
capitalization = KeyboardCapitalization.None,
|
|
||||||
imeAction = ImeAction.Done,
|
|
||||||
),
|
|
||||||
keyboardActions = KeyboardActions(onDone = { saveTrustedSSID() }),
|
|
||||||
trailingIcon = {
|
|
||||||
if (currentText != "") {
|
|
||||||
IconButton(onClick = { saveTrustedSSID() }) {
|
|
||||||
Icon(
|
|
||||||
imageVector = Icons.Outlined.Add,
|
|
||||||
contentDescription =
|
|
||||||
if (currentText == "") {
|
|
||||||
stringResource(
|
|
||||||
id =
|
|
||||||
R.string
|
|
||||||
.trusted_ssid_empty_description,
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
stringResource(
|
|
||||||
id =
|
|
||||||
R.string
|
|
||||||
.trusted_ssid_value_description,
|
|
||||||
)
|
|
||||||
},
|
|
||||||
tint = MaterialTheme.colorScheme.primary,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Column(
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
verticalArrangement = Arrangement.Top,
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.verticalScroll(scrollState)
|
||||||
|
.clickable(
|
||||||
|
indication = null,
|
||||||
|
interactionSource = interactionSource,
|
||||||
|
) {
|
||||||
|
focusManager.clearFocus()
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
Surface(
|
||||||
|
tonalElevation = 2.dp,
|
||||||
|
shadowElevation = 2.dp,
|
||||||
|
shape = RoundedCornerShape(12.dp),
|
||||||
|
color = MaterialTheme.colorScheme.surface,
|
||||||
|
modifier =
|
||||||
|
(if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
||||||
|
Modifier
|
||||||
|
.height(IntrinsicSize.Min)
|
||||||
|
.fillMaxWidth(fillMaxWidth)
|
||||||
|
.padding(top = 10.dp)
|
||||||
|
} else {
|
||||||
|
Modifier
|
||||||
|
.fillMaxWidth(fillMaxWidth)
|
||||||
|
.padding(top = 20.dp)
|
||||||
|
})
|
||||||
|
.padding(bottom = 10.dp),
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
horizontalAlignment = Alignment.Start,
|
||||||
|
verticalArrangement = Arrangement.Top,
|
||||||
|
modifier = Modifier.padding(15.dp),
|
||||||
|
) {
|
||||||
|
SectionTitle(
|
||||||
|
title = stringResource(id = R.string.general),
|
||||||
|
padding = screenPadding,
|
||||||
|
)
|
||||||
|
ConfigurationToggle(
|
||||||
|
stringResource(R.string.set_primary_tunnel),
|
||||||
|
enabled = true,
|
||||||
|
checked = uiState.isDefaultTunnel,
|
||||||
|
modifier = Modifier
|
||||||
|
.focusRequester(focusRequester),
|
||||||
|
padding = screenPadding,
|
||||||
|
onCheckChanged = { optionsViewModel.onTogglePrimaryTunnel() },
|
||||||
|
)
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(top = 5.dp),
|
||||||
|
horizontalArrangement = Arrangement.Center,
|
||||||
|
) {
|
||||||
|
TextButton(
|
||||||
|
onClick = {
|
||||||
|
navController.navigate(
|
||||||
|
"${Screen.Config.route}/${tunnelId}",
|
||||||
|
)
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
Text(stringResource(R.string.edit_tunnel))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Surface(
|
||||||
|
tonalElevation = 2.dp,
|
||||||
|
shadowElevation = 2.dp,
|
||||||
|
shape = RoundedCornerShape(12.dp),
|
||||||
|
color = MaterialTheme.colorScheme.surface,
|
||||||
|
modifier =
|
||||||
|
(if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
||||||
|
Modifier
|
||||||
|
.height(IntrinsicSize.Min)
|
||||||
|
.fillMaxWidth(fillMaxWidth)
|
||||||
|
.padding(top = 10.dp)
|
||||||
|
} else {
|
||||||
|
Modifier
|
||||||
|
.fillMaxWidth(fillMaxWidth)
|
||||||
|
.padding(top = 20.dp)
|
||||||
|
})
|
||||||
|
.padding(bottom = 10.dp),
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
horizontalAlignment = Alignment.Start,
|
||||||
|
verticalArrangement = Arrangement.Top,
|
||||||
|
modifier = Modifier.padding(15.dp),
|
||||||
|
) {
|
||||||
|
SectionTitle(
|
||||||
|
title = stringResource(id = R.string.auto_tunneling),
|
||||||
|
padding = screenPadding,
|
||||||
|
)
|
||||||
|
ConfigurationToggle(
|
||||||
|
stringResource(R.string.mobile_data_tunnel),
|
||||||
|
enabled = true,
|
||||||
|
checked = uiState.tunnel?.isMobileDataTunnel == true,
|
||||||
|
padding = screenPadding,
|
||||||
|
onCheckChanged = { optionsViewModel.onToggleIsMobileDataTunnel() },
|
||||||
|
)
|
||||||
|
Column {
|
||||||
|
FlowRow(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(screenPadding)
|
||||||
|
.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(5.dp),
|
||||||
|
) {
|
||||||
|
uiState.tunnel?.tunnelNetworks?.forEach { ssid ->
|
||||||
|
ClickableIconButton(
|
||||||
|
onClick = {
|
||||||
|
if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
||||||
|
focusRequester.requestFocus()
|
||||||
|
optionsViewModel.onDeleteRunSSID(ssid)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onIconClick = {
|
||||||
|
if (WireGuardAutoTunnel.isRunningOnAndroidTv()) focusRequester.requestFocus()
|
||||||
|
optionsViewModel.onDeleteRunSSID(ssid)
|
||||||
|
|
||||||
|
},
|
||||||
|
text = ssid,
|
||||||
|
icon = Icons.Filled.Close,
|
||||||
|
enabled = true,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (uiState.tunnel == null || uiState.tunnel?.tunnelNetworks?.isEmpty() == true) {
|
||||||
|
Text(
|
||||||
|
stringResource(R.string.no_wifi_names_configured),
|
||||||
|
fontStyle = FontStyle.Italic,
|
||||||
|
color = Color.Gray,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
OutlinedTextField(
|
||||||
|
enabled = true,
|
||||||
|
value = currentText,
|
||||||
|
onValueChange = { currentText = it },
|
||||||
|
label = { Text(stringResource(id = R.string.use_tunnel_on_wifi_name)) },
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.padding(
|
||||||
|
start = screenPadding,
|
||||||
|
top = 5.dp,
|
||||||
|
bottom = 10.dp,
|
||||||
|
),
|
||||||
|
maxLines = 1,
|
||||||
|
keyboardOptions =
|
||||||
|
KeyboardOptions(
|
||||||
|
capitalization = KeyboardCapitalization.None,
|
||||||
|
imeAction = ImeAction.Done,
|
||||||
|
),
|
||||||
|
keyboardActions = KeyboardActions(onDone = { saveTrustedSSID() }),
|
||||||
|
trailingIcon = {
|
||||||
|
if (currentText != "") {
|
||||||
|
IconButton(onClick = { saveTrustedSSID() }) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Outlined.Add,
|
||||||
|
contentDescription =
|
||||||
|
if (currentText == "") {
|
||||||
|
stringResource(
|
||||||
|
id =
|
||||||
|
R.string
|
||||||
|
.trusted_ssid_empty_description,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
stringResource(
|
||||||
|
id =
|
||||||
|
R.string
|
||||||
|
.trusted_ssid_value_description,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
tint = MaterialTheme.colorScheme.primary,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.ui.screens.options
|
package com.zaneschepke.wireguardautotunnel.ui.screens.options
|
||||||
|
|
||||||
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
|
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
|
||||||
|
|
||||||
data class OptionsUiState(
|
data class OptionsUiState(
|
||||||
val id: String? = null,
|
val id: String? = null,
|
||||||
|
|||||||
+14
-15
@@ -4,16 +4,17 @@ import androidx.compose.ui.util.fastFirstOrNull
|
|||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
|
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
|
||||||
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
|
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
|
||||||
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
|
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
|
||||||
import com.zaneschepke.wireguardautotunnel.util.Constants
|
import com.zaneschepke.wireguardautotunnel.util.Constants
|
||||||
import com.zaneschepke.wireguardautotunnel.util.WgTunnelExceptions
|
import com.zaneschepke.wireguardautotunnel.util.Event
|
||||||
|
import com.zaneschepke.wireguardautotunnel.util.Result
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.SharingStarted
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
import kotlinx.coroutines.flow.combine
|
import kotlinx.coroutines.flow.combine
|
||||||
import kotlinx.coroutines.flow.stateIn
|
import kotlinx.coroutines.flow.stateIn
|
||||||
import kotlinx.coroutines.flow.update
|
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
@@ -42,14 +43,12 @@ constructor(
|
|||||||
)
|
)
|
||||||
|
|
||||||
fun init(tunnelId: String) {
|
fun init(tunnelId: String) {
|
||||||
_optionState.update {
|
_optionState.value = _optionState.value.copy(
|
||||||
it.copy(
|
id = tunnelId,
|
||||||
id = tunnelId,
|
)
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onDeleteRunSSID(ssid: String) = viewModelScope.launch {
|
fun onDeleteRunSSID(ssid: String) = viewModelScope.launch(Dispatchers.IO) {
|
||||||
uiState.value.tunnel?.let {
|
uiState.value.tunnel?.let {
|
||||||
appDataRepository.tunnels.save(
|
appDataRepository.tunnels.save(
|
||||||
tunnelConfig = it.copy(
|
tunnelConfig = it.copy(
|
||||||
@@ -59,7 +58,7 @@ constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun saveTunnel(tunnelConfig: TunnelConfig?) = viewModelScope.launch {
|
private fun saveTunnel(tunnelConfig: TunnelConfig?) = viewModelScope.launch(Dispatchers.IO) {
|
||||||
tunnelConfig?.let {
|
tunnelConfig?.let {
|
||||||
appDataRepository.tunnels.save(it)
|
appDataRepository.tunnels.save(it)
|
||||||
}
|
}
|
||||||
@@ -74,13 +73,13 @@ constructor(
|
|||||||
tunnelsWithName.isEmpty()) {
|
tunnelsWithName.isEmpty()) {
|
||||||
uiState.value.tunnel?.tunnelNetworks?.add(trimmed)
|
uiState.value.tunnel?.tunnelNetworks?.add(trimmed)
|
||||||
saveTunnel(uiState.value.tunnel)
|
saveTunnel(uiState.value.tunnel)
|
||||||
Result.success(Unit)
|
Result.Success(Unit)
|
||||||
} else {
|
} else {
|
||||||
Result.failure(WgTunnelExceptions.SsidConflict())
|
Result.Error(Event.Error.SsidConflict)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onToggleIsMobileDataTunnel() = viewModelScope.launch {
|
fun onToggleIsMobileDataTunnel() = viewModelScope.launch(Dispatchers.IO) {
|
||||||
uiState.value.tunnel?.let {
|
uiState.value.tunnel?.let {
|
||||||
if (it.isMobileDataTunnel) {
|
if (it.isMobileDataTunnel) {
|
||||||
appDataRepository.tunnels.updateMobileDataTunnel(null)
|
appDataRepository.tunnels.updateMobileDataTunnel(null)
|
||||||
@@ -88,7 +87,7 @@ constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onTogglePrimaryTunnel() = viewModelScope.launch {
|
fun onTogglePrimaryTunnel() = viewModelScope.launch(Dispatchers.IO) {
|
||||||
if (uiState.value.tunnel != null) {
|
if (uiState.value.tunnel != null) {
|
||||||
appDataRepository.tunnels.updatePrimaryTunnel(
|
appDataRepository.tunnels.updatePrimaryTunnel(
|
||||||
when (uiState.value.isDefaultTunnel) {
|
when (uiState.value.isDefaultTunnel) {
|
||||||
@@ -96,7 +95,7 @@ constructor(
|
|||||||
false -> uiState.value.tunnel
|
false -> uiState.value.tunnel
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
WireGuardAutoTunnel.requestTunnelTileServiceStateUpdate()
|
WireGuardAutoTunnel.requestTunnelTileServiceStateUpdate(WireGuardAutoTunnel.instance)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+59
-93
@@ -26,6 +26,7 @@ import androidx.compose.foundation.layout.fillMaxWidth
|
|||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.foundation.rememberScrollState
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.foundation.text.KeyboardActions
|
import androidx.compose.foundation.text.KeyboardActions
|
||||||
@@ -44,7 +45,6 @@ import androidx.compose.material3.Surface
|
|||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TextButton
|
import androidx.compose.material3.TextButton
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
|
||||||
import androidx.compose.runtime.SideEffect
|
import androidx.compose.runtime.SideEffect
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
@@ -73,17 +73,21 @@ import com.google.accompanist.permissions.isGranted
|
|||||||
import com.google.accompanist.permissions.rememberPermissionState
|
import com.google.accompanist.permissions.rememberPermissionState
|
||||||
import com.zaneschepke.wireguardautotunnel.R
|
import com.zaneschepke.wireguardautotunnel.R
|
||||||
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
|
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
|
||||||
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
|
|
||||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelState
|
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.AppViewModel
|
import com.zaneschepke.wireguardautotunnel.ui.AppViewModel
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.Screen
|
import com.zaneschepke.wireguardautotunnel.ui.Screen
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.common.ClickableIconButton
|
import com.zaneschepke.wireguardautotunnel.ui.common.ClickableIconButton
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.common.config.ConfigurationToggle
|
import com.zaneschepke.wireguardautotunnel.ui.common.config.ConfigurationToggle
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.common.prompt.AuthorizationPrompt
|
import com.zaneschepke.wireguardautotunnel.ui.common.prompt.AuthorizationPrompt
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.common.text.SectionTitle
|
import com.zaneschepke.wireguardautotunnel.ui.common.text.SectionTitle
|
||||||
import com.zaneschepke.wireguardautotunnel.util.getMessage
|
import com.zaneschepke.wireguardautotunnel.util.Event
|
||||||
|
import com.zaneschepke.wireguardautotunnel.util.FileUtils
|
||||||
|
import com.zaneschepke.wireguardautotunnel.util.Result
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import org.amnezia.awg.backend.AwgQuickBackend
|
||||||
|
import org.amnezia.awg.backend.Tunnel
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
import xyz.teamgravity.pin_lock_compose.PinManager
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
@OptIn(
|
@OptIn(
|
||||||
@@ -97,15 +101,14 @@ fun SettingsScreen(
|
|||||||
navController: NavController,
|
navController: NavController,
|
||||||
focusRequester: FocusRequester,
|
focusRequester: FocusRequester,
|
||||||
) {
|
) {
|
||||||
|
val scope = rememberCoroutineScope { Dispatchers.IO }
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val focusManager = LocalFocusManager.current
|
val focusManager = LocalFocusManager.current
|
||||||
val scope = rememberCoroutineScope()
|
|
||||||
val scrollState = rememberScrollState()
|
val scrollState = rememberScrollState()
|
||||||
val interactionSource = remember { MutableInteractionSource() }
|
val interactionSource = remember { MutableInteractionSource() }
|
||||||
//val pinExists = remember { mutableStateOf(PinManager.pinExists()) }
|
val pinExists = remember { mutableStateOf(PinManager.pinExists()) }
|
||||||
|
|
||||||
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
|
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
|
||||||
val kernelSupport by viewModel.kernelSupport.collectAsStateWithLifecycle()
|
|
||||||
|
|
||||||
val fineLocationState = rememberPermissionState(Manifest.permission.ACCESS_FINE_LOCATION)
|
val fineLocationState = rememberPermissionState(Manifest.permission.ACCESS_FINE_LOCATION)
|
||||||
var currentText by remember { mutableStateOf("") }
|
var currentText by remember { mutableStateOf("") }
|
||||||
@@ -117,10 +120,6 @@ fun SettingsScreen(
|
|||||||
val screenPadding = 5.dp
|
val screenPadding = 5.dp
|
||||||
val fillMaxWidth = .85f
|
val fillMaxWidth = .85f
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
|
||||||
viewModel.checkKernelSupport()
|
|
||||||
}
|
|
||||||
|
|
||||||
val startForResult =
|
val startForResult =
|
||||||
rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) { result: ActivityResult ->
|
rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) { result: ActivityResult ->
|
||||||
if (result.resultCode == Activity.RESULT_OK) {
|
if (result.resultCode == Activity.RESULT_OK) {
|
||||||
@@ -132,32 +131,16 @@ fun SettingsScreen(
|
|||||||
|
|
||||||
fun exportAllConfigs() {
|
fun exportAllConfigs() {
|
||||||
try {
|
try {
|
||||||
val wgFiles = uiState.tunnels.map { config ->
|
val files = uiState.tunnels.map { File(context.cacheDir, "${it.name}.conf") }
|
||||||
val file = File(context.cacheDir, "${config.name}-wg.conf")
|
files.forEachIndexed { index, file ->
|
||||||
file.outputStream().use {
|
file.outputStream().use { it.write(uiState.tunnels[index].wgQuick.toByteArray()) }
|
||||||
it.write(config.wgQuick.toByteArray())
|
|
||||||
}
|
|
||||||
file
|
|
||||||
}
|
|
||||||
val amFiles = uiState.tunnels.mapNotNull { config ->
|
|
||||||
if (config.amQuick != TunnelConfig.AM_QUICK_DEFAULT) {
|
|
||||||
val file = File(context.cacheDir, "${config.name}-am.conf")
|
|
||||||
file.outputStream().use {
|
|
||||||
it.write(config.amQuick.toByteArray())
|
|
||||||
}
|
|
||||||
file
|
|
||||||
} else null
|
|
||||||
}
|
|
||||||
scope.launch {
|
|
||||||
viewModel.onExportTunnels(wgFiles + amFiles).onFailure {
|
|
||||||
appViewModel.showSnackbarMessage(it.getMessage(context))
|
|
||||||
}.onSuccess {
|
|
||||||
didExportFiles = true
|
|
||||||
appViewModel.showSnackbarMessage(context.getString(R.string.exported_configs_message))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
FileUtils.saveFilesToZip(context, files)
|
||||||
|
didExportFiles = true
|
||||||
|
appViewModel.showSnackbarMessage(Event.Message.ConfigsExported.message)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Timber.e(e)
|
Timber.e(e)
|
||||||
|
appViewModel.showSnackbarMessage(Event.Error.Exception(e).message)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -178,7 +161,7 @@ fun SettingsScreen(
|
|||||||
fun handleAutoTunnelToggle() {
|
fun handleAutoTunnelToggle() {
|
||||||
if (uiState.isBatteryOptimizeDisableShown || isBatteryOptimizationsDisabled()) {
|
if (uiState.isBatteryOptimizeDisableShown || isBatteryOptimizationsDisabled()) {
|
||||||
if (appViewModel.isRequiredPermissionGranted()) {
|
if (appViewModel.isRequiredPermissionGranted()) {
|
||||||
viewModel.onToggleAutoTunnel(context)
|
viewModel.onToggleAutoTunnel()
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
requestBatteryOptimizationsDisabled()
|
requestBatteryOptimizationsDisabled()
|
||||||
@@ -187,18 +170,21 @@ fun SettingsScreen(
|
|||||||
|
|
||||||
fun saveTrustedSSID() {
|
fun saveTrustedSSID() {
|
||||||
if (currentText.isNotEmpty()) {
|
if (currentText.isNotEmpty()) {
|
||||||
viewModel.onSaveTrustedSSID(currentText).onSuccess {
|
viewModel.onSaveTrustedSSID(currentText).let {
|
||||||
currentText = ""
|
when (it) {
|
||||||
}.onFailure {
|
is Result.Success -> currentText = ""
|
||||||
appViewModel.showSnackbarMessage(it.getMessage(context))
|
is Result.Error -> appViewModel.showSnackbarMessage(it.error.message)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun openSettings() {
|
fun openSettings() {
|
||||||
val intentSettings = Intent(ACTION_APPLICATION_DETAILS_SETTINGS)
|
scope.launch {
|
||||||
intentSettings.data = Uri.fromParts("package", context.packageName, null)
|
val intentSettings = Intent(ACTION_APPLICATION_DETAILS_SETTINGS)
|
||||||
context.startActivity(intentSettings)
|
intentSettings.data = Uri.fromParts("package", context.packageName, null)
|
||||||
|
context.startActivity(intentSettings)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun checkFineLocationGranted() {
|
fun checkFineLocationGranted() {
|
||||||
@@ -322,11 +308,11 @@ fun SettingsScreen(
|
|||||||
},
|
},
|
||||||
onError = { _ ->
|
onError = { _ ->
|
||||||
showAuthPrompt = false
|
showAuthPrompt = false
|
||||||
appViewModel.showSnackbarMessage(context.getString(R.string.error_authentication_failed))
|
appViewModel.showSnackbarMessage(Event.Error.AuthenticationFailed.message)
|
||||||
},
|
},
|
||||||
onFailure = {
|
onFailure = {
|
||||||
showAuthPrompt = false
|
showAuthPrompt = false
|
||||||
appViewModel.showSnackbarMessage(context.getString(R.string.error_authorization_failed))
|
appViewModel.showSnackbarMessage(Event.Error.AuthorizationFailed.message)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -534,12 +520,12 @@ fun SettingsScreen(
|
|||||||
when (false) {
|
when (false) {
|
||||||
isBackgroundLocationGranted ->
|
isBackgroundLocationGranted ->
|
||||||
appViewModel.showSnackbarMessage(
|
appViewModel.showSnackbarMessage(
|
||||||
context.getString(R.string.background_location_required),
|
Event.Error.BackgroundLocationRequired.message,
|
||||||
)
|
)
|
||||||
|
|
||||||
fineLocationState.status.isGranted ->
|
fineLocationState.status.isGranted ->
|
||||||
appViewModel.showSnackbarMessage(
|
appViewModel.showSnackbarMessage(
|
||||||
context.getString(R.string.precise_location_required),
|
Event.Error.PreciseLocationRequired.message,
|
||||||
)
|
)
|
||||||
|
|
||||||
viewModel.isLocationEnabled(context) ->
|
viewModel.isLocationEnabled(context) ->
|
||||||
@@ -565,49 +551,38 @@ fun SettingsScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Surface(
|
if (AwgQuickBackend.hasKernelSupport()) {
|
||||||
tonalElevation = 2.dp,
|
Surface(
|
||||||
shadowElevation = 2.dp,
|
tonalElevation = 2.dp,
|
||||||
shape = RoundedCornerShape(12.dp),
|
shadowElevation = 2.dp,
|
||||||
color = MaterialTheme.colorScheme.surface,
|
shape = RoundedCornerShape(12.dp),
|
||||||
modifier = Modifier
|
color = MaterialTheme.colorScheme.surface,
|
||||||
.fillMaxWidth(fillMaxWidth)
|
modifier = Modifier
|
||||||
.padding(vertical = 10.dp),
|
.fillMaxWidth(fillMaxWidth)
|
||||||
) {
|
.padding(vertical = 10.dp),
|
||||||
Column(
|
|
||||||
horizontalAlignment = Alignment.Start,
|
|
||||||
verticalArrangement = Arrangement.Top,
|
|
||||||
modifier = Modifier.padding(15.dp),
|
|
||||||
) {
|
) {
|
||||||
SectionTitle(
|
Column(
|
||||||
title = stringResource(id = R.string.backend),
|
horizontalAlignment = Alignment.Start,
|
||||||
padding = screenPadding,
|
verticalArrangement = Arrangement.Top,
|
||||||
)
|
modifier = Modifier.padding(15.dp),
|
||||||
ConfigurationToggle(
|
) {
|
||||||
stringResource(R.string.use_amnezia),
|
SectionTitle(
|
||||||
enabled =
|
title = stringResource(id = R.string.kernel),
|
||||||
!(uiState.settings.isAutoTunnelEnabled ||
|
padding = screenPadding,
|
||||||
uiState.settings.isAlwaysOnVpnEnabled ||
|
)
|
||||||
(uiState.vpnState.status == TunnelState.UP) || uiState.settings.isKernelEnabled),
|
|
||||||
checked = uiState.settings.isAmneziaEnabled,
|
|
||||||
padding = screenPadding,
|
|
||||||
onCheckChanged = {
|
|
||||||
viewModel.onToggleAmnezia()
|
|
||||||
},
|
|
||||||
)
|
|
||||||
if (kernelSupport) {
|
|
||||||
ConfigurationToggle(
|
ConfigurationToggle(
|
||||||
stringResource(R.string.use_kernel),
|
stringResource(R.string.use_kernel),
|
||||||
enabled =
|
enabled =
|
||||||
!(uiState.settings.isAutoTunnelEnabled ||
|
!(uiState.settings.isAutoTunnelEnabled ||
|
||||||
uiState.settings.isAlwaysOnVpnEnabled ||
|
uiState.settings.isAlwaysOnVpnEnabled ||
|
||||||
(uiState.vpnState.status == TunnelState.UP)),
|
(uiState.vpnState.status == Tunnel.State.UP)),
|
||||||
checked = uiState.settings.isKernelEnabled,
|
checked = uiState.settings.isKernelEnabled,
|
||||||
padding = screenPadding,
|
padding = screenPadding,
|
||||||
onCheckChanged = {
|
onCheckChanged = {
|
||||||
scope.launch {
|
viewModel.onToggleKernelMode().let {
|
||||||
viewModel.onToggleKernelMode().onFailure {
|
when (it) {
|
||||||
appViewModel.showSnackbarMessage(it.getMessage(context))
|
is Result.Error -> appViewModel.showSnackbarMessage(it.error.message)
|
||||||
|
is Result.Success -> {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -651,25 +626,16 @@ fun SettingsScreen(
|
|||||||
onCheckChanged = { viewModel.onToggleShortcutsEnabled() },
|
onCheckChanged = { viewModel.onToggleShortcutsEnabled() },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
ConfigurationToggle(
|
|
||||||
stringResource(R.string.restart_at_boot),
|
|
||||||
enabled = true,
|
|
||||||
checked = uiState.settings.isRestoreOnBootEnabled,
|
|
||||||
padding = screenPadding,
|
|
||||||
onCheckChanged = {
|
|
||||||
viewModel.onToggleRestartAtBoot()
|
|
||||||
},
|
|
||||||
)
|
|
||||||
ConfigurationToggle(
|
ConfigurationToggle(
|
||||||
stringResource(R.string.enable_app_lock),
|
stringResource(R.string.enable_app_lock),
|
||||||
enabled = true,
|
enabled = true,
|
||||||
checked = uiState.isPinLockEnabled,
|
checked = pinExists.value,
|
||||||
padding = screenPadding,
|
padding = screenPadding,
|
||||||
onCheckChanged = {
|
onCheckChanged = {
|
||||||
if (uiState.isPinLockEnabled) {
|
if (pinExists.value) {
|
||||||
viewModel.onPinLockDisabled()
|
PinManager.clearPin()
|
||||||
|
pinExists.value = PinManager.pinExists()
|
||||||
} else {
|
} else {
|
||||||
viewModel.onPinLockEnabled()
|
|
||||||
navController.navigate(Screen.Lock.route)
|
navController.navigate(Screen.Lock.route)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
+3
-4
@@ -1,7 +1,7 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.ui.screens.settings
|
package com.zaneschepke.wireguardautotunnel.ui.screens.settings
|
||||||
|
|
||||||
import com.zaneschepke.wireguardautotunnel.data.domain.Settings
|
import com.zaneschepke.wireguardautotunnel.data.model.Settings
|
||||||
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
|
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
|
||||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnState
|
import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnState
|
||||||
|
|
||||||
data class SettingsUiState(
|
data class SettingsUiState(
|
||||||
@@ -9,6 +9,5 @@ data class SettingsUiState(
|
|||||||
val tunnels: List<TunnelConfig> = emptyList(),
|
val tunnels: List<TunnelConfig> = emptyList(),
|
||||||
val vpnState: VpnState = VpnState(),
|
val vpnState: VpnState = VpnState(),
|
||||||
val isLocationDisclosureShown: Boolean = true,
|
val isLocationDisclosureShown: Boolean = true,
|
||||||
val isBatteryOptimizeDisableShown: Boolean = false,
|
val isBatteryOptimizeDisableShown: Boolean = false
|
||||||
val isPinLockEnabled: Boolean = false
|
|
||||||
)
|
)
|
||||||
|
|||||||
+27
-95
@@ -1,52 +1,39 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.ui.screens.settings
|
package com.zaneschepke.wireguardautotunnel.ui.screens.settings
|
||||||
|
|
||||||
|
import android.app.Application
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.location.LocationManager
|
import android.location.LocationManager
|
||||||
import androidx.core.location.LocationManagerCompat
|
import androidx.core.location.LocationManagerCompat
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import com.wireguard.android.backend.WgQuickBackend
|
|
||||||
import com.wireguard.android.util.RootShell
|
|
||||||
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
|
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
|
||||||
import com.zaneschepke.wireguardautotunnel.data.domain.Settings
|
import com.zaneschepke.wireguardautotunnel.data.model.Settings
|
||||||
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
|
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
|
||||||
import com.zaneschepke.wireguardautotunnel.module.IoDispatcher
|
|
||||||
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
|
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
|
||||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnService
|
import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnService
|
||||||
import com.zaneschepke.wireguardautotunnel.util.Constants
|
import com.zaneschepke.wireguardautotunnel.util.Constants
|
||||||
import com.zaneschepke.wireguardautotunnel.util.FileUtils
|
import com.zaneschepke.wireguardautotunnel.util.Event
|
||||||
import com.zaneschepke.wireguardautotunnel.util.WgTunnelExceptions
|
import com.zaneschepke.wireguardautotunnel.util.Result
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import kotlinx.coroutines.CoroutineDispatcher
|
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
|
||||||
import kotlinx.coroutines.flow.SharingStarted
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
|
||||||
import kotlinx.coroutines.flow.combine
|
import kotlinx.coroutines.flow.combine
|
||||||
import kotlinx.coroutines.flow.stateIn
|
import kotlinx.coroutines.flow.stateIn
|
||||||
import kotlinx.coroutines.flow.update
|
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import org.amnezia.awg.util.RootShell
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import xyz.teamgravity.pin_lock_compose.PinManager
|
|
||||||
import java.io.File
|
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import javax.inject.Provider
|
|
||||||
|
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
class SettingsViewModel
|
class SettingsViewModel
|
||||||
@Inject
|
@Inject
|
||||||
constructor(
|
constructor(
|
||||||
|
private val application: Application,
|
||||||
private val appDataRepository: AppDataRepository,
|
private val appDataRepository: AppDataRepository,
|
||||||
private val serviceManager: ServiceManager,
|
private val serviceManager: ServiceManager,
|
||||||
private val rootShell: Provider<RootShell>,
|
private val rootShell: RootShell,
|
||||||
private val fileUtils: FileUtils,
|
|
||||||
@IoDispatcher private val ioDispatcher: CoroutineDispatcher,
|
|
||||||
vpnService: VpnService
|
vpnService: VpnService
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
|
|
||||||
private val _kernelSupport = MutableStateFlow(false)
|
|
||||||
val kernelSupport = _kernelSupport.asStateFlow()
|
|
||||||
|
|
||||||
val uiState =
|
val uiState =
|
||||||
combine(
|
combine(
|
||||||
appDataRepository.settings.getSettingsFlow(),
|
appDataRepository.settings.getSettingsFlow(),
|
||||||
@@ -58,9 +45,8 @@ constructor(
|
|||||||
settings,
|
settings,
|
||||||
tunnels,
|
tunnels,
|
||||||
tunnelState,
|
tunnelState,
|
||||||
generalState.isLocationDisclosureShown,
|
generalState.locationDisclosureShown,
|
||||||
generalState.isBatteryOptimizationDisableShown,
|
generalState.batteryOptimizationDisableShown,
|
||||||
generalState.isPinLockEnabled,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
.stateIn(
|
.stateIn(
|
||||||
@@ -74,9 +60,9 @@ constructor(
|
|||||||
return if (!uiState.value.settings.trustedNetworkSSIDs.contains(trimmed)) {
|
return if (!uiState.value.settings.trustedNetworkSSIDs.contains(trimmed)) {
|
||||||
uiState.value.settings.trustedNetworkSSIDs.add(trimmed)
|
uiState.value.settings.trustedNetworkSSIDs.add(trimmed)
|
||||||
saveSettings(uiState.value.settings)
|
saveSettings(uiState.value.settings)
|
||||||
Result.success(Unit)
|
Result.Success(Unit)
|
||||||
} else {
|
} else {
|
||||||
Result.failure(WgTunnelExceptions.SsidConflict())
|
Result.Error(Event.Error.SsidConflict)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -107,19 +93,15 @@ constructor(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun onExportTunnels(files: List<File>): Result<Unit> {
|
fun onToggleAutoTunnel() =
|
||||||
return fileUtils.saveFilesToZip(files)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onToggleAutoTunnel(context: Context) =
|
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
val isAutoTunnelEnabled = uiState.value.settings.isAutoTunnelEnabled
|
val isAutoTunnelEnabled = uiState.value.settings.isAutoTunnelEnabled
|
||||||
var isAutoTunnelPaused = uiState.value.settings.isAutoTunnelPaused
|
var isAutoTunnelPaused = uiState.value.settings.isAutoTunnelPaused
|
||||||
|
|
||||||
if (isAutoTunnelEnabled) {
|
if (isAutoTunnelEnabled) {
|
||||||
serviceManager.stopWatcherService(context)
|
serviceManager.stopWatcherService(application)
|
||||||
} else {
|
} else {
|
||||||
serviceManager.startWatcherService(context)
|
serviceManager.startWatcherService(application)
|
||||||
isAutoTunnelPaused = false
|
isAutoTunnelPaused = false
|
||||||
}
|
}
|
||||||
saveSettings(
|
saveSettings(
|
||||||
@@ -128,7 +110,7 @@ constructor(
|
|||||||
isAutoTunnelPaused = isAutoTunnelPaused,
|
isAutoTunnelPaused = isAutoTunnelPaused,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
WireGuardAutoTunnel.requestAutoTunnelTileServiceUpdate()
|
WireGuardAutoTunnel.requestAutoTunnelTileServiceUpdate(application)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onToggleAlwaysOnVPN() =
|
fun onToggleAlwaysOnVPN() =
|
||||||
@@ -180,44 +162,21 @@ constructor(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onToggleAmnezia() = viewModelScope.launch {
|
fun onToggleKernelMode(): Result<Unit> {
|
||||||
if (uiState.value.settings.isKernelEnabled) {
|
if (!uiState.value.settings.isKernelEnabled) {
|
||||||
saveKernelMode(false)
|
try {
|
||||||
}
|
rootShell.start()
|
||||||
saveAmneziaMode(!uiState.value.settings.isAmneziaEnabled)
|
Timber.i("Root shell accepted!")
|
||||||
}
|
saveKernelMode(on = true)
|
||||||
|
} catch (e: RootShell.RootShellException) {
|
||||||
private fun saveAmneziaMode(on: Boolean) {
|
Timber.e(e)
|
||||||
saveSettings(
|
|
||||||
uiState.value.settings.copy(
|
|
||||||
isAmneziaEnabled = on,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun onToggleKernelMode(): Result<Unit> {
|
|
||||||
return withContext(ioDispatcher) {
|
|
||||||
if (!uiState.value.settings.isKernelEnabled) {
|
|
||||||
try {
|
|
||||||
rootShell.get().start()
|
|
||||||
Timber.i("Root shell accepted!")
|
|
||||||
saveSettings(
|
|
||||||
uiState.value.settings.copy(
|
|
||||||
isKernelEnabled = true,
|
|
||||||
isAmneziaEnabled = false,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
} catch (e: RootShell.RootShellException) {
|
|
||||||
Timber.e(e)
|
|
||||||
saveKernelMode(on = false)
|
|
||||||
return@withContext Result.failure(WgTunnelExceptions.RootDenied())
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
saveKernelMode(on = false)
|
saveKernelMode(on = false)
|
||||||
|
return Result.Error(Event.Error.RootDenied)
|
||||||
}
|
}
|
||||||
Result.success(Unit)
|
} else {
|
||||||
|
saveKernelMode(on = false)
|
||||||
}
|
}
|
||||||
|
return Result.Success(Unit)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onToggleRestartOnPing() = viewModelScope.launch {
|
fun onToggleRestartOnPing() = viewModelScope.launch {
|
||||||
@@ -227,31 +186,4 @@ constructor(
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun checkKernelSupport() = viewModelScope.launch {
|
|
||||||
val kernelSupport = withContext(ioDispatcher) {
|
|
||||||
WgQuickBackend.hasKernelSupport()
|
|
||||||
}
|
|
||||||
_kernelSupport.update {
|
|
||||||
kernelSupport
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onPinLockDisabled() = viewModelScope.launch {
|
|
||||||
PinManager.clearPin()
|
|
||||||
appDataRepository.appState.setPinLockEnabled(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onPinLockEnabled() = viewModelScope.launch {
|
|
||||||
PinManager.initialize(WireGuardAutoTunnel.instance)
|
|
||||||
appDataRepository.appState.setPinLockEnabled(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onToggleRestartAtBoot() = viewModelScope.launch {
|
|
||||||
saveSettings(
|
|
||||||
uiState.value.settings.copy(
|
|
||||||
isRestoreOnBootEnabled = !uiState.value.settings.isRestoreOnBootEnabled
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
+8
-26
@@ -107,12 +107,7 @@ fun SupportScreen(
|
|||||||
modifier = Modifier.padding(bottom = 20.dp),
|
modifier = Modifier.padding(bottom = 20.dp),
|
||||||
)
|
)
|
||||||
TextButton(
|
TextButton(
|
||||||
onClick = {
|
onClick = { appViewModel.openWebPage(context.resources.getString(R.string.docs_url)) },
|
||||||
appViewModel.openWebPage(
|
|
||||||
context.resources.getString(R.string.docs_url),
|
|
||||||
context,
|
|
||||||
)
|
|
||||||
},
|
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(vertical = 5.dp)
|
.padding(vertical = 5.dp)
|
||||||
.focusRequester(focusRequester),
|
.focusRequester(focusRequester),
|
||||||
@@ -134,7 +129,7 @@ fun SupportScreen(
|
|||||||
weight = 1.0f,
|
weight = 1.0f,
|
||||||
fill = false,
|
fill = false,
|
||||||
),
|
),
|
||||||
softWrap = true,
|
softWrap = true
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
Icon(
|
Icon(
|
||||||
@@ -148,12 +143,7 @@ fun SupportScreen(
|
|||||||
color = MaterialTheme.colorScheme.onBackground,
|
color = MaterialTheme.colorScheme.onBackground,
|
||||||
)
|
)
|
||||||
TextButton(
|
TextButton(
|
||||||
onClick = {
|
onClick = { appViewModel.openWebPage(context.resources.getString(R.string.discord_url)) },
|
||||||
appViewModel.openWebPage(
|
|
||||||
context.resources.getString(R.string.telegram_url),
|
|
||||||
context,
|
|
||||||
)
|
|
||||||
},
|
|
||||||
modifier = Modifier.padding(vertical = 5.dp),
|
modifier = Modifier.padding(vertical = 5.dp),
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
@@ -162,7 +152,7 @@ fun SupportScreen(
|
|||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
) {
|
) {
|
||||||
Row {
|
Row {
|
||||||
val icon = ImageVector.vectorResource(R.drawable.telegram)
|
val icon = ImageVector.vectorResource(R.drawable.discord)
|
||||||
Icon(
|
Icon(
|
||||||
icon,
|
icon,
|
||||||
icon.name,
|
icon.name,
|
||||||
@@ -185,12 +175,7 @@ fun SupportScreen(
|
|||||||
color = MaterialTheme.colorScheme.onBackground,
|
color = MaterialTheme.colorScheme.onBackground,
|
||||||
)
|
)
|
||||||
TextButton(
|
TextButton(
|
||||||
onClick = {
|
onClick = { appViewModel.openWebPage(context.resources.getString(R.string.github_url)) },
|
||||||
appViewModel.openWebPage(
|
|
||||||
context.resources.getString(R.string.github_url),
|
|
||||||
context,
|
|
||||||
)
|
|
||||||
},
|
|
||||||
modifier = Modifier.padding(vertical = 5.dp),
|
modifier = Modifier.padding(vertical = 5.dp),
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
@@ -222,7 +207,7 @@ fun SupportScreen(
|
|||||||
color = MaterialTheme.colorScheme.onBackground,
|
color = MaterialTheme.colorScheme.onBackground,
|
||||||
)
|
)
|
||||||
TextButton(
|
TextButton(
|
||||||
onClick = { appViewModel.launchEmail(context) },
|
onClick = { appViewModel.launchEmail() },
|
||||||
modifier = Modifier.padding(vertical = 5.dp),
|
modifier = Modifier.padding(vertical = 5.dp),
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
@@ -284,10 +269,7 @@ fun SupportScreen(
|
|||||||
fontSize = 16.sp,
|
fontSize = 16.sp,
|
||||||
modifier =
|
modifier =
|
||||||
Modifier.clickable {
|
Modifier.clickable {
|
||||||
appViewModel.openWebPage(
|
appViewModel.openWebPage(context.resources.getString(R.string.privacy_policy_url))
|
||||||
context.resources.getString(R.string.privacy_policy_url),
|
|
||||||
context,
|
|
||||||
)
|
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
Row(
|
Row(
|
||||||
@@ -303,7 +285,7 @@ fun SupportScreen(
|
|||||||
val mode = buildAnnotatedString {
|
val mode = buildAnnotatedString {
|
||||||
append(stringResource(R.string.mode))
|
append(stringResource(R.string.mode))
|
||||||
append(": ")
|
append(": ")
|
||||||
when (uiState.settings.isKernelEnabled) {
|
when(uiState.settings.isKernelEnabled){
|
||||||
true -> append(stringResource(id = R.string.kernel))
|
true -> append(stringResource(id = R.string.kernel))
|
||||||
false -> append(stringResource(id = R.string.userspace))
|
false -> append(stringResource(id = R.string.userspace))
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.ui.screens.support
|
package com.zaneschepke.wireguardautotunnel.ui.screens.support
|
||||||
|
|
||||||
import com.zaneschepke.wireguardautotunnel.data.domain.Settings
|
import com.zaneschepke.wireguardautotunnel.data.model.Settings
|
||||||
|
|
||||||
data class SupportUiState(val settings: Settings = Settings())
|
data class SupportUiState(val settings: Settings = Settings())
|
||||||
|
|||||||
+8
-24
@@ -1,7 +1,6 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.ui.screens.support.logs
|
package com.zaneschepke.wireguardautotunnel.ui.screens.support.logs
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.widget.Toast
|
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
@@ -9,7 +8,7 @@ import androidx.compose.foundation.layout.Row
|
|||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.itemsIndexed
|
import androidx.compose.foundation.lazy.items
|
||||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
@@ -28,24 +27,21 @@ import androidx.compose.ui.Modifier
|
|||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.platform.ClipboardManager
|
import androidx.compose.ui.platform.ClipboardManager
|
||||||
import androidx.compose.ui.platform.LocalClipboardManager
|
import androidx.compose.ui.platform.LocalClipboardManager
|
||||||
import androidx.compose.ui.platform.LocalContext
|
|
||||||
import androidx.compose.ui.text.AnnotatedString
|
import androidx.compose.ui.text.AnnotatedString
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import androidx.hilt.navigation.compose.hiltViewModel
|
import com.zaneschepke.wireguardautotunnel.ui.AppViewModel
|
||||||
import com.zaneschepke.logcatter.model.LogMessage
|
|
||||||
import com.zaneschepke.wireguardautotunnel.R
|
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.common.text.LogTypeLabel
|
import com.zaneschepke.wireguardautotunnel.ui.common.text.LogTypeLabel
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter")
|
@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter")
|
||||||
@Composable
|
@Composable
|
||||||
fun LogsScreen(viewModel: LogsViewModel = hiltViewModel()) {
|
fun LogsScreen(appViewModel: AppViewModel) {
|
||||||
|
|
||||||
val logs = viewModel.logs
|
val logs = remember {
|
||||||
|
appViewModel.logs
|
||||||
val context = LocalContext.current
|
}
|
||||||
|
|
||||||
val lazyColumnListState = rememberLazyListState()
|
val lazyColumnListState = rememberLazyListState()
|
||||||
val clipboardManager: ClipboardManager = LocalClipboardManager.current
|
val clipboardManager: ClipboardManager = LocalClipboardManager.current
|
||||||
@@ -61,15 +57,7 @@ fun LogsScreen(viewModel: LogsViewModel = hiltViewModel()) {
|
|||||||
floatingActionButton = {
|
floatingActionButton = {
|
||||||
FloatingActionButton(
|
FloatingActionButton(
|
||||||
onClick = {
|
onClick = {
|
||||||
scope.launch {
|
appViewModel.saveLogsToFile()
|
||||||
viewModel.saveLogsToFile().onSuccess {
|
|
||||||
Toast.makeText(
|
|
||||||
context,
|
|
||||||
context.getString(R.string.logs_saved),
|
|
||||||
Toast.LENGTH_SHORT,
|
|
||||||
).show()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
shape = RoundedCornerShape(16.dp),
|
shape = RoundedCornerShape(16.dp),
|
||||||
containerColor = MaterialTheme.colorScheme.primary,
|
containerColor = MaterialTheme.colorScheme.primary,
|
||||||
@@ -91,11 +79,7 @@ fun LogsScreen(viewModel: LogsViewModel = hiltViewModel()) {
|
|||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.padding(horizontal = 24.dp),
|
.padding(horizontal = 24.dp),
|
||||||
) {
|
) {
|
||||||
itemsIndexed(
|
items(logs) {
|
||||||
logs,
|
|
||||||
key = { index, _ -> index },
|
|
||||||
contentType = { _: Int, _: LogMessage -> null },
|
|
||||||
) { _, it ->
|
|
||||||
Row(
|
Row(
|
||||||
horizontalArrangement = Arrangement.spacedBy(5.dp, Alignment.Start),
|
horizontalArrangement = Arrangement.spacedBy(5.dp, Alignment.Start),
|
||||||
verticalAlignment = Alignment.Top,
|
verticalAlignment = Alignment.Top,
|
||||||
|
|||||||
-55
@@ -1,55 +0,0 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.ui.screens.support.logs
|
|
||||||
|
|
||||||
import androidx.compose.runtime.mutableStateListOf
|
|
||||||
import androidx.lifecycle.ViewModel
|
|
||||||
import androidx.lifecycle.viewModelScope
|
|
||||||
import com.zaneschepke.logcatter.LocalLogCollector
|
|
||||||
import com.zaneschepke.logcatter.model.LogMessage
|
|
||||||
import com.zaneschepke.wireguardautotunnel.module.IoDispatcher
|
|
||||||
import com.zaneschepke.wireguardautotunnel.module.MainDispatcher
|
|
||||||
import com.zaneschepke.wireguardautotunnel.util.Constants
|
|
||||||
import com.zaneschepke.wireguardautotunnel.util.FileUtils
|
|
||||||
import com.zaneschepke.wireguardautotunnel.util.chunked
|
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
|
||||||
import kotlinx.coroutines.CoroutineDispatcher
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
import java.time.Duration
|
|
||||||
import java.time.Instant
|
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
@HiltViewModel
|
|
||||||
class LogsViewModel
|
|
||||||
@Inject constructor(
|
|
||||||
private val localLogCollector: LocalLogCollector,
|
|
||||||
private val fileUtils: FileUtils,
|
|
||||||
@IoDispatcher private val ioDispatcher: CoroutineDispatcher,
|
|
||||||
@MainDispatcher private val mainDispatcher: CoroutineDispatcher
|
|
||||||
) : ViewModel() {
|
|
||||||
|
|
||||||
val logs = mutableStateListOf<LogMessage>()
|
|
||||||
|
|
||||||
init {
|
|
||||||
viewModelScope.launch(ioDispatcher) {
|
|
||||||
localLogCollector.bufferedLogs.chunked(500, Duration.ofSeconds(1)).collect {
|
|
||||||
withContext(mainDispatcher) {
|
|
||||||
logs.addAll(it)
|
|
||||||
}
|
|
||||||
if (logs.size > Constants.LOG_BUFFER_SIZE) {
|
|
||||||
withContext(mainDispatcher) {
|
|
||||||
logs.removeRange(0, (logs.size - Constants.LOG_BUFFER_SIZE).toInt())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun saveLogsToFile(): Result<Unit> {
|
|
||||||
val file = localLogCollector.getLogFile().getOrElse {
|
|
||||||
return Result.failure(it)
|
|
||||||
}
|
|
||||||
val fileContent = fileUtils.readBytesFromFile(file)
|
|
||||||
val fileName = "${Constants.BASE_LOG_FILE_NAME}-${Instant.now().epochSecond}.txt"
|
|
||||||
return fileUtils.saveByteArrayToDownloads(fileContent, fileName)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -2,7 +2,7 @@ package com.zaneschepke.wireguardautotunnel.util
|
|||||||
|
|
||||||
object Constants {
|
object Constants {
|
||||||
|
|
||||||
const val BASE_LOG_FILE_NAME = "wg_tunnel_logs"
|
const val BASE_LOG_FILE_NAME = "wgtunnel-logs"
|
||||||
const val LOG_BUFFER_SIZE = 3_000L
|
const val LOG_BUFFER_SIZE = 3_000L
|
||||||
|
|
||||||
const val MANUAL_TUNNEL_CONFIG_ID = "0"
|
const val MANUAL_TUNNEL_CONFIG_ID = "0"
|
||||||
@@ -16,17 +16,16 @@ object Constants {
|
|||||||
const val URI_CONTENT_SCHEME = "content"
|
const val URI_CONTENT_SCHEME = "content"
|
||||||
const val ALLOWED_FILE_TYPES = "*/*"
|
const val ALLOWED_FILE_TYPES = "*/*"
|
||||||
const val TEXT_MIME_TYPE = "text/plain"
|
const val TEXT_MIME_TYPE = "text/plain"
|
||||||
const val ZIP_FILE_MIME_TYPE = "application/zip"
|
|
||||||
const val GOOGLE_TV_EXPLORER_STUB = "com.google.android.tv.frameworkpackagestubs"
|
const val GOOGLE_TV_EXPLORER_STUB = "com.google.android.tv.frameworkpackagestubs"
|
||||||
const val ANDROID_TV_EXPLORER_STUB = "com.android.tv.frameworkpackagestubs"
|
const val ANDROID_TV_EXPLORER_STUB = "com.android.tv.frameworkpackagestubs"
|
||||||
const val ALWAYS_ON_VPN_ACTION = "android.net.VpnService"
|
const val ALWAYS_ON_VPN_ACTION = "android.net.VpnService"
|
||||||
const val EMAIL_MIME_TYPE = "plain/text"
|
const val EMAIL_MIME_TYPE = "message/rfc822"
|
||||||
const val SYSTEM_EXEMPT_SERVICE_TYPE_ID = 1024
|
const val SYSTEM_EXEMPT_SERVICE_TYPE_ID = 1024
|
||||||
|
|
||||||
const val SUBSCRIPTION_TIMEOUT = 5_000L
|
const val SUBSCRIPTION_TIMEOUT = 5_000L
|
||||||
const val FOCUS_REQUEST_DELAY = 500L
|
const val FOCUS_REQUEST_DELAY = 500L
|
||||||
|
|
||||||
const val DEFAULT_PING_IP = "1.1.1.1"
|
const val BACKUP_PING_HOST = "1.1.1.1"
|
||||||
const val PING_TIMEOUT = 5_000L
|
const val PING_TIMEOUT = 5_000L
|
||||||
const val VPN_RESTART_DELAY = 1_000L
|
const val VPN_RESTART_DELAY = 1_000L
|
||||||
const val PING_INTERVAL = 60_000L
|
const val PING_INTERVAL = 60_000L
|
||||||
@@ -38,7 +37,4 @@ object Constants {
|
|||||||
|
|
||||||
const val UNREADABLE_SSID = "<unknown ssid>"
|
const val UNREADABLE_SSID = "<unknown ssid>"
|
||||||
|
|
||||||
val amneziaProperties = listOf("Jc", "Jmin", "Jmax", "S1", "S2", "H1", "H2", "H3", "H4")
|
|
||||||
const val QR_CODE_NAME_PROPERTY = "# Name ="
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,117 @@
|
|||||||
|
package com.zaneschepke.wireguardautotunnel.util
|
||||||
|
|
||||||
|
import com.zaneschepke.wireguardautotunnel.R
|
||||||
|
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
|
||||||
|
|
||||||
|
sealed class Event {
|
||||||
|
|
||||||
|
abstract val message: String
|
||||||
|
|
||||||
|
sealed class Error : Event() {
|
||||||
|
data object None : Error() {
|
||||||
|
override val message: String
|
||||||
|
get() = WireGuardAutoTunnel.instance.getString(R.string.error_none)
|
||||||
|
}
|
||||||
|
|
||||||
|
data object SsidConflict : Error() {
|
||||||
|
override val message: String
|
||||||
|
get() = WireGuardAutoTunnel.instance.getString(R.string.error_ssid_exists)
|
||||||
|
}
|
||||||
|
|
||||||
|
data class ConfigParseError(val appendedMessage: String) : Error() {
|
||||||
|
override val message: String =
|
||||||
|
WireGuardAutoTunnel.instance.getString(R.string.config_parse_error) + (
|
||||||
|
if (appendedMessage != "") ": ${appendedMessage.trim()}" else "")
|
||||||
|
}
|
||||||
|
|
||||||
|
data object RootDenied : Error() {
|
||||||
|
override val message: String
|
||||||
|
get() = WireGuardAutoTunnel.instance.getString(R.string.error_root_denied)
|
||||||
|
}
|
||||||
|
|
||||||
|
data class General(val customMessage: String) : Error() {
|
||||||
|
override val message: String
|
||||||
|
get() = customMessage
|
||||||
|
}
|
||||||
|
|
||||||
|
data class Exception(val exception: kotlin.Exception) : Error() {
|
||||||
|
override val message: String
|
||||||
|
get() =
|
||||||
|
exception.message
|
||||||
|
?: WireGuardAutoTunnel.instance.getString(R.string.unknown_error)
|
||||||
|
}
|
||||||
|
|
||||||
|
data object InvalidQrCode : Error() {
|
||||||
|
override val message: String
|
||||||
|
get() = WireGuardAutoTunnel.instance.getString(R.string.error_invalid_code)
|
||||||
|
}
|
||||||
|
|
||||||
|
data object InvalidFileExtension : Error() {
|
||||||
|
override val message: String
|
||||||
|
get() = WireGuardAutoTunnel.instance.getString(R.string.error_file_extension)
|
||||||
|
}
|
||||||
|
|
||||||
|
data object FileReadFailed : Error() {
|
||||||
|
override val message: String
|
||||||
|
get() = WireGuardAutoTunnel.instance.getString(R.string.error_file_extension)
|
||||||
|
}
|
||||||
|
|
||||||
|
data object AuthenticationFailed : Error() {
|
||||||
|
override val message: String
|
||||||
|
get() = WireGuardAutoTunnel.instance.getString(R.string.error_authentication_failed)
|
||||||
|
}
|
||||||
|
|
||||||
|
data object AuthorizationFailed : Error() {
|
||||||
|
override val message: String
|
||||||
|
get() = WireGuardAutoTunnel.instance.getString(R.string.error_authorization_failed)
|
||||||
|
}
|
||||||
|
|
||||||
|
data object BackgroundLocationRequired : Error() {
|
||||||
|
override val message: String
|
||||||
|
get() =
|
||||||
|
WireGuardAutoTunnel.instance.getString(R.string.background_location_required)
|
||||||
|
}
|
||||||
|
|
||||||
|
data object LocationServicesRequired : Error() {
|
||||||
|
override val message: String
|
||||||
|
get() = WireGuardAutoTunnel.instance.getString(R.string.location_services_required)
|
||||||
|
}
|
||||||
|
|
||||||
|
data object PreciseLocationRequired : Error() {
|
||||||
|
override val message: String
|
||||||
|
get() = WireGuardAutoTunnel.instance.getString(R.string.precise_location_required)
|
||||||
|
}
|
||||||
|
|
||||||
|
data object FileExplorerRequired : Error() {
|
||||||
|
override val message: String
|
||||||
|
get() = WireGuardAutoTunnel.instance.getString(R.string.error_no_file_explorer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sealed class Message : Event() {
|
||||||
|
data object ConfigSaved : Message() {
|
||||||
|
override val message: String
|
||||||
|
get() = WireGuardAutoTunnel.instance.getString(R.string.config_changes_saved)
|
||||||
|
}
|
||||||
|
|
||||||
|
data object ConfigsExported : Message() {
|
||||||
|
override val message: String
|
||||||
|
get() = WireGuardAutoTunnel.instance.getString(R.string.exported_configs_message)
|
||||||
|
}
|
||||||
|
|
||||||
|
data object TunnelOffAction : Message() {
|
||||||
|
override val message: String
|
||||||
|
get() = WireGuardAutoTunnel.instance.getString(R.string.turn_off_tunnel)
|
||||||
|
}
|
||||||
|
|
||||||
|
data object TunnelOnAction : Message() {
|
||||||
|
override val message: String
|
||||||
|
get() = WireGuardAutoTunnel.instance.getString(R.string.turn_on_tunnel)
|
||||||
|
}
|
||||||
|
|
||||||
|
data object AutoTunnelOffAction : Message() {
|
||||||
|
override val message: String
|
||||||
|
get() = WireGuardAutoTunnel.instance.getString(R.string.turn_off_auto)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,30 +1,40 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.util
|
package com.zaneschepke.wireguardautotunnel.util
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.BroadcastReceiver
|
||||||
import android.content.pm.PackageInfo
|
import android.content.pm.PackageInfo
|
||||||
import com.zaneschepke.wireguardautotunnel.R
|
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
|
||||||
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
|
|
||||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.HandshakeStatus
|
import com.zaneschepke.wireguardautotunnel.service.tunnel.HandshakeStatus
|
||||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.statistics.TunnelStatistics
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
import kotlinx.coroutines.GlobalScope
|
||||||
import kotlinx.coroutines.ObsoleteCoroutinesApi
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.channels.ClosedReceiveChannelException
|
import org.amnezia.awg.backend.Statistics
|
||||||
import kotlinx.coroutines.channels.ReceiveChannel
|
import org.amnezia.awg.crypto.Key
|
||||||
import kotlinx.coroutines.channels.produce
|
|
||||||
import kotlinx.coroutines.channels.ticker
|
|
||||||
import kotlinx.coroutines.coroutineScope
|
|
||||||
import kotlinx.coroutines.flow.Flow
|
|
||||||
import kotlinx.coroutines.flow.channelFlow
|
|
||||||
import kotlinx.coroutines.selects.whileSelect
|
|
||||||
import org.amnezia.awg.config.Config
|
|
||||||
import timber.log.Timber
|
|
||||||
import java.math.BigDecimal
|
import java.math.BigDecimal
|
||||||
import java.text.DecimalFormat
|
import java.text.DecimalFormat
|
||||||
import java.time.Duration
|
import kotlin.coroutines.CoroutineContext
|
||||||
import java.util.concurrent.ConcurrentLinkedQueue
|
import kotlin.coroutines.EmptyCoroutineContext
|
||||||
import kotlin.coroutines.cancellation.CancellationException
|
|
||||||
|
fun BroadcastReceiver.goAsync(
|
||||||
|
context: CoroutineContext = EmptyCoroutineContext,
|
||||||
|
block: suspend CoroutineScope.() -> Unit
|
||||||
|
) {
|
||||||
|
val pendingResult = goAsync()
|
||||||
|
@OptIn(DelicateCoroutinesApi::class) // Must run globally; there's no teardown callback.
|
||||||
|
GlobalScope.launch(context) {
|
||||||
|
try {
|
||||||
|
block()
|
||||||
|
} finally {
|
||||||
|
pendingResult.finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun String.truncateWithEllipsis(allowedLength: Int): String {
|
||||||
|
return if (this.length > allowedLength + 3) {
|
||||||
|
this.substring(0, allowedLength) + "***"
|
||||||
|
} else this
|
||||||
|
}
|
||||||
|
|
||||||
fun BigDecimal.toThreeDecimalPlaceString(): String {
|
fun BigDecimal.toThreeDecimalPlaceString(): String {
|
||||||
val df = DecimalFormat("#.###")
|
val df = DecimalFormat("#.###")
|
||||||
@@ -39,15 +49,15 @@ typealias TunnelConfigs = List<TunnelConfig>
|
|||||||
|
|
||||||
typealias Packages = List<PackageInfo>
|
typealias Packages = List<PackageInfo>
|
||||||
|
|
||||||
fun TunnelStatistics.mapPeerStats(): Map<org.amnezia.awg.crypto.Key, TunnelStatistics.PeerStats?> {
|
fun Statistics.mapPeerStats(): Map<Key, Statistics.PeerStats?> {
|
||||||
return this.getPeers().associateWith { key -> (this.peerStats(key)) }
|
return this.peers().associateWith { key -> (this.peer(key)) }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun TunnelStatistics.PeerStats.latestHandshakeSeconds(): Long? {
|
fun Statistics.PeerStats.latestHandshakeSeconds(): Long? {
|
||||||
return NumberUtils.getSecondsBetweenTimestampAndNow(this.latestHandshakeEpochMillis)
|
return NumberUtils.getSecondsBetweenTimestampAndNow(this.latestHandshakeEpochMillis)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun TunnelStatistics.PeerStats.handshakeStatus(): HandshakeStatus {
|
fun Statistics.PeerStats.handshakeStatus(): HandshakeStatus {
|
||||||
// TODO add never connected status after duration
|
// TODO add never connected status after duration
|
||||||
return this.latestHandshakeSeconds().let {
|
return this.latestHandshakeSeconds().let {
|
||||||
when {
|
when {
|
||||||
@@ -60,89 +70,3 @@ fun TunnelStatistics.PeerStats.handshakeStatus(): HandshakeStatus {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun Config.toWgQuickString(): String {
|
|
||||||
val amQuick = toAwgQuickString()
|
|
||||||
val lines = amQuick.lines().toMutableList()
|
|
||||||
val linesIterator = lines.iterator()
|
|
||||||
while (linesIterator.hasNext()) {
|
|
||||||
val next = linesIterator.next()
|
|
||||||
Constants.amneziaProperties.forEach {
|
|
||||||
if (next.startsWith(it, ignoreCase = true)) {
|
|
||||||
linesIterator.remove()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return lines.joinToString(System.lineSeparator())
|
|
||||||
}
|
|
||||||
|
|
||||||
fun Throwable.getMessage(context: Context): String {
|
|
||||||
return when (this) {
|
|
||||||
is WgTunnelExceptions -> this.getMessage(context)
|
|
||||||
else -> this.message ?: StringValue.StringResource(R.string.unknown_error).asString(context)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Chunks based on a time or size threshold.
|
|
||||||
*
|
|
||||||
* Borrowed from this [Stack Overflow question](https://stackoverflow.com/questions/51022533/kotlin-chunk-sequence-based-on-size-and-time).
|
|
||||||
*/
|
|
||||||
@OptIn(ObsoleteCoroutinesApi::class, ExperimentalCoroutinesApi::class)
|
|
||||||
fun <T> ReceiveChannel<T>.chunked(scope: CoroutineScope, size: Int, time: Duration) =
|
|
||||||
scope.produce<List<T>> {
|
|
||||||
while (true) { // this loop goes over each chunk
|
|
||||||
val chunk = ConcurrentLinkedQueue<T>() // current chunk
|
|
||||||
val ticker = ticker(time.toMillis()) // time-limit for this chunk
|
|
||||||
try {
|
|
||||||
whileSelect {
|
|
||||||
ticker.onReceive {
|
|
||||||
false // done with chunk when timer ticks, takes priority over received elements
|
|
||||||
}
|
|
||||||
this@chunked.onReceive {
|
|
||||||
chunk += it
|
|
||||||
chunk.size < size // continue whileSelect if chunk is not full
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e: ClosedReceiveChannelException) {
|
|
||||||
Timber.e(e)
|
|
||||||
return@produce
|
|
||||||
} finally {
|
|
||||||
ticker.cancel()
|
|
||||||
if (chunk.isNotEmpty()) {
|
|
||||||
send(chunk.toList())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@OptIn(DelicateCoroutinesApi::class, ExperimentalCoroutinesApi::class)
|
|
||||||
fun <T> Flow<T>.chunked(size: Int, time: Duration) = channelFlow {
|
|
||||||
coroutineScope {
|
|
||||||
val channel = asChannel(this@chunked).chunked(this, size, time)
|
|
||||||
try {
|
|
||||||
while (!channel.isClosedForReceive) {
|
|
||||||
send(channel.receive())
|
|
||||||
}
|
|
||||||
} catch (e: ClosedReceiveChannelException) {
|
|
||||||
// Channel was closed by the flow completing, nothing to do
|
|
||||||
Timber.w(e)
|
|
||||||
} catch (e: CancellationException) {
|
|
||||||
channel.cancel(e)
|
|
||||||
throw e
|
|
||||||
} catch (e: Exception) {
|
|
||||||
channel.cancel(CancellationException("Closing channel due to flow exception", e))
|
|
||||||
throw e
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@ExperimentalCoroutinesApi
|
|
||||||
fun <T> CoroutineScope.asChannel(flow: Flow<T>): ReceiveChannel<T> = produce {
|
|
||||||
flow.collect { value ->
|
|
||||||
channel.send(value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -6,102 +6,19 @@ import android.os.Build
|
|||||||
import android.os.Environment
|
import android.os.Environment
|
||||||
import android.provider.MediaStore
|
import android.provider.MediaStore
|
||||||
import android.provider.MediaStore.MediaColumns
|
import android.provider.MediaStore.MediaColumns
|
||||||
import kotlinx.coroutines.CoroutineDispatcher
|
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
import timber.log.Timber
|
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.FileInputStream
|
|
||||||
import java.io.FileOutputStream
|
import java.io.FileOutputStream
|
||||||
import java.io.OutputStream
|
import java.io.OutputStream
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
import java.util.zip.ZipEntry
|
import java.util.zip.ZipEntry
|
||||||
import java.util.zip.ZipOutputStream
|
import java.util.zip.ZipOutputStream
|
||||||
|
|
||||||
class FileUtils(
|
object FileUtils {
|
||||||
private val context: Context,
|
private const val ZIP_FILE_MIME_TYPE = "application/zip"
|
||||||
private val ioDispatcher: CoroutineDispatcher,
|
|
||||||
) {
|
|
||||||
|
|
||||||
suspend fun readBytesFromFile(file: File): ByteArray {
|
|
||||||
return withContext(ioDispatcher) {
|
|
||||||
FileInputStream(file).use {
|
|
||||||
it.readBytes()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun readTextFromFileName(fileName: String): String {
|
|
||||||
return withContext(ioDispatcher) {
|
|
||||||
context.assets.open(fileName).use { stream ->
|
|
||||||
stream.bufferedReader(Charsets.UTF_8).use {
|
|
||||||
it.readText()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun saveByteArrayToDownloads(content: ByteArray, fileName: String): Result<Unit> {
|
|
||||||
return withContext(ioDispatcher) {
|
|
||||||
try {
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
|
||||||
val contentValues =
|
|
||||||
ContentValues().apply {
|
|
||||||
put(MediaColumns.DISPLAY_NAME, fileName)
|
|
||||||
put(MediaColumns.MIME_TYPE, Constants.TEXT_MIME_TYPE)
|
|
||||||
put(MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_DOWNLOADS)
|
|
||||||
}
|
|
||||||
val resolver = context.contentResolver
|
|
||||||
val uri =
|
|
||||||
resolver.insert(MediaStore.Downloads.EXTERNAL_CONTENT_URI, contentValues)
|
|
||||||
if (uri != null) {
|
|
||||||
resolver.openOutputStream(uri).use { output ->
|
|
||||||
output?.write(content)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
val target =
|
|
||||||
File(
|
|
||||||
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS),
|
|
||||||
fileName,
|
|
||||||
)
|
|
||||||
FileOutputStream(target).use { output ->
|
|
||||||
output.write(content)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Result.success(Unit)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Result.failure(e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun saveFilesToZip(files: List<File>): Result<Unit> {
|
|
||||||
return withContext(ioDispatcher) {
|
|
||||||
try {
|
|
||||||
val zipOutputStream =
|
|
||||||
createDownloadsFileOutputStream(
|
|
||||||
"wg-export_${Instant.now().epochSecond}.zip",
|
|
||||||
Constants.ZIP_FILE_MIME_TYPE,
|
|
||||||
)
|
|
||||||
ZipOutputStream(zipOutputStream).use { zos ->
|
|
||||||
files.forEach { file ->
|
|
||||||
val entry = ZipEntry(file.name)
|
|
||||||
zos.putNextEntry(entry)
|
|
||||||
if (file.isFile) {
|
|
||||||
file.inputStream().use { fis -> fis.copyTo(zos) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return@withContext Result.success(Unit)
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Timber.e(e)
|
|
||||||
Result.failure(WgTunnelExceptions.ConfigExportFailed())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//TODO issue with android 9
|
//TODO issue with android 9
|
||||||
private fun createDownloadsFileOutputStream(
|
private fun createDownloadsFileOutputStream(
|
||||||
|
context: Context,
|
||||||
fileName: String,
|
fileName: String,
|
||||||
mimeType: String = Constants.ALLOWED_FILE_TYPES
|
mimeType: String = Constants.ALLOWED_FILE_TYPES
|
||||||
): OutputStream? {
|
): OutputStream? {
|
||||||
@@ -127,4 +44,47 @@ class FileUtils(
|
|||||||
}
|
}
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun saveFileToDownloads(context: Context, content: String, fileName: String) {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||||
|
val contentValues = ContentValues().apply {
|
||||||
|
put(MediaColumns.DISPLAY_NAME, fileName)
|
||||||
|
put(MediaColumns.MIME_TYPE, Constants.TEXT_MIME_TYPE)
|
||||||
|
put(MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_DOWNLOADS)
|
||||||
|
}
|
||||||
|
val resolver = context.contentResolver
|
||||||
|
val uri = resolver.insert(MediaStore.Downloads.EXTERNAL_CONTENT_URI, contentValues)
|
||||||
|
if (uri != null) {
|
||||||
|
resolver.openOutputStream(uri).use { output ->
|
||||||
|
output?.write(content.toByteArray())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
val target = File(
|
||||||
|
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS),
|
||||||
|
fileName,
|
||||||
|
)
|
||||||
|
FileOutputStream(target).use { output ->
|
||||||
|
output.write(content.toByteArray())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun saveFilesToZip(context: Context, files: List<File>) {
|
||||||
|
val zipOutputStream =
|
||||||
|
createDownloadsFileOutputStream(
|
||||||
|
context,
|
||||||
|
"wg-export_${Instant.now().epochSecond}.zip",
|
||||||
|
ZIP_FILE_MIME_TYPE,
|
||||||
|
)
|
||||||
|
ZipOutputStream(zipOutputStream).use { zos ->
|
||||||
|
files.forEach { file ->
|
||||||
|
val entry = ZipEntry(file.name)
|
||||||
|
zos.putNextEntry(entry)
|
||||||
|
if (file.isFile) {
|
||||||
|
file.inputStream().use { fis -> fis.copyTo(zos) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
package com.zaneschepke.wireguardautotunnel.util
|
||||||
|
|
||||||
|
import timber.log.Timber
|
||||||
|
|
||||||
|
sealed class Result<T> {
|
||||||
|
class Success<T>(val data: T) : Result<T>()
|
||||||
|
|
||||||
|
class Error<T>(val error: Event.Error) : Result<T>() {
|
||||||
|
init {
|
||||||
|
when (this.error) {
|
||||||
|
is Event.Error.Exception -> Timber.e(this.error.exception)
|
||||||
|
else -> Timber.e(this.error.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,130 +0,0 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.util
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import com.zaneschepke.wireguardautotunnel.R
|
|
||||||
|
|
||||||
sealed class WgTunnelExceptions : Exception() {
|
|
||||||
abstract fun getMessage(context: Context): String
|
|
||||||
data class General(private val userMessage: StringValue) : WgTunnelExceptions() {
|
|
||||||
override fun getMessage(context: Context): String {
|
|
||||||
return userMessage.asString(context)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
data class SsidConflict(private val userMessage: StringValue = StringValue.StringResource(R.string.error_ssid_exists)) :
|
|
||||||
WgTunnelExceptions() {
|
|
||||||
override fun getMessage(context: Context): String {
|
|
||||||
return userMessage.asString(context)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
data class ConfigExportFailed(
|
|
||||||
private val userMessage: StringValue = StringValue.StringResource(
|
|
||||||
R.string.export_configs_failed,
|
|
||||||
)
|
|
||||||
) : WgTunnelExceptions() {
|
|
||||||
override fun getMessage(context: Context): String {
|
|
||||||
return userMessage.asString(context)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
data class ConfigParseError(private val appendMessage: StringValue = StringValue.Empty) :
|
|
||||||
WgTunnelExceptions() {
|
|
||||||
override fun getMessage(context: Context): String {
|
|
||||||
return StringValue.StringResource(R.string.config_parse_error).asString(context) + (
|
|
||||||
if (appendMessage != StringValue.Empty) ": ${appendMessage.asString(context)}" else "")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
data class RootDenied(private val userMessage: StringValue = StringValue.StringResource(R.string.error_root_denied)) :
|
|
||||||
WgTunnelExceptions() {
|
|
||||||
override fun getMessage(context: Context): String {
|
|
||||||
return userMessage.asString(context)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
data class InvalidQrCode(private val userMessage: StringValue = StringValue.StringResource(R.string.error_invalid_code)) :
|
|
||||||
WgTunnelExceptions() {
|
|
||||||
override fun getMessage(context: Context): String {
|
|
||||||
return userMessage.asString(context)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
data class InvalidFileExtension(
|
|
||||||
private val userMessage: StringValue = StringValue.StringResource(
|
|
||||||
R.string.error_file_extension,
|
|
||||||
)
|
|
||||||
) : WgTunnelExceptions() {
|
|
||||||
override fun getMessage(context: Context): String {
|
|
||||||
return userMessage.asString(context)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
data class FileReadFailed(private val userMessage: StringValue = StringValue.StringResource(R.string.error_file_format)) :
|
|
||||||
WgTunnelExceptions() {
|
|
||||||
override fun getMessage(context: Context): String {
|
|
||||||
return userMessage.asString(context)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
data class AuthenticationFailed(
|
|
||||||
private val userMessage: StringValue = StringValue.StringResource(
|
|
||||||
R.string.error_authentication_failed,
|
|
||||||
)
|
|
||||||
) : WgTunnelExceptions() {
|
|
||||||
override fun getMessage(context: Context): String {
|
|
||||||
return userMessage.asString(context)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
data class AuthorizationFailed(
|
|
||||||
private val userMessage: StringValue = StringValue.StringResource(
|
|
||||||
R.string.error_authorization_failed,
|
|
||||||
)
|
|
||||||
) : WgTunnelExceptions() {
|
|
||||||
override fun getMessage(context: Context): String {
|
|
||||||
return userMessage.asString(context)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
data class BackgroundLocationRequired(
|
|
||||||
private val userMessage: StringValue = StringValue.StringResource(
|
|
||||||
R.string.background_location_required,
|
|
||||||
)
|
|
||||||
) : WgTunnelExceptions() {
|
|
||||||
override fun getMessage(context: Context): String {
|
|
||||||
return userMessage.asString(context)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
data class LocationServicesRequired(
|
|
||||||
private val userMessage: StringValue = StringValue.StringResource(
|
|
||||||
R.string.location_services_required,
|
|
||||||
)
|
|
||||||
) : WgTunnelExceptions() {
|
|
||||||
override fun getMessage(context: Context): String {
|
|
||||||
return userMessage.asString(context)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
data class PreciseLocationRequired(
|
|
||||||
private val userMessage: StringValue = StringValue.StringResource(
|
|
||||||
R.string.precise_location_required,
|
|
||||||
)
|
|
||||||
) : WgTunnelExceptions() {
|
|
||||||
override fun getMessage(context: Context): String {
|
|
||||||
return userMessage.asString(context)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
data class FileExplorerRequired(
|
|
||||||
private val userMessage: StringValue = StringValue.StringResource(
|
|
||||||
R.string.error_no_file_explorer,
|
|
||||||
)
|
|
||||||
) : WgTunnelExceptions() {
|
|
||||||
override fun getMessage(context: Context): String {
|
|
||||||
return userMessage.asString(context)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:width="24dp"
|
|
||||||
android:height="24dp"
|
|
||||||
android:viewportWidth="960"
|
|
||||||
android:viewportHeight="960">
|
|
||||||
<path
|
|
||||||
android:fillColor="#e8eaed"
|
|
||||||
android:pathData="M440,520L200,520v-80h240v-240h80v240h240v80L520,520v240h-80v-240Z" />
|
|
||||||
</vector>
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:width="24dp"
|
|
||||||
android:height="24dp"
|
|
||||||
android:viewportWidth="960"
|
|
||||||
android:viewportHeight="960">
|
|
||||||
<path
|
|
||||||
android:fillColor="#e8eaed"
|
|
||||||
android:pathData="m256,760 l-56,-56 224,-224 -224,-224 56,-56 224,224 224,-224 56,56 -224,224 224,224 -56,56 -224,-224 -224,224Z" />
|
|
||||||
</vector>
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:width="24dp"
|
|
||||||
android:height="24dp"
|
|
||||||
android:viewportWidth="960"
|
|
||||||
android:viewportHeight="960">
|
|
||||||
<path
|
|
||||||
android:fillColor="#e8eaed"
|
|
||||||
android:pathData="M200,760h57l391,-391 -57,-57 -391,391v57ZM120,840v-170l528,-527q12,-11 26.5,-17t30.5,-6q16,0 31,6t26,18l55,56q12,11 17.5,26t5.5,30q0,16 -5.5,30.5T817,313L290,840L120,840ZM760,256 L704,200 760,256ZM619,341 L591,312 648,369 619,341Z" />
|
|
||||||
</vector>
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:width="50dp"
|
|
||||||
android:height="50dp"
|
|
||||||
android:viewportWidth="50"
|
|
||||||
android:viewportHeight="50">
|
|
||||||
<path
|
|
||||||
android:fillColor="#FF000000"
|
|
||||||
android:pathData="M25,2c12.703,0 23,10.297 23,23S37.703,48 25,48S2,37.703 2,25S12.297,2 25,2zM32.934,34.375c0.423,-1.298 2.405,-14.234 2.65,-16.783c0.074,-0.772 -0.17,-1.285 -0.648,-1.514c-0.578,-0.278 -1.434,-0.139 -2.427,0.219c-1.362,0.491 -18.774,7.884 -19.78,8.312c-0.954,0.405 -1.856,0.847 -1.856,1.487c0,0.45 0.267,0.703 1.003,0.966c0.766,0.273 2.695,0.858 3.834,1.172c1.097,0.303 2.346,0.04 3.046,-0.395c0.742,-0.461 9.305,-6.191 9.92,-6.693c0.614,-0.502 1.104,0.141 0.602,0.644c-0.502,0.502 -6.38,6.207 -7.155,6.997c-0.941,0.959 -0.273,1.953 0.358,2.351c0.721,0.454 5.906,3.932 6.687,4.49c0.781,0.558 1.573,0.811 2.298,0.811C32.191,36.439 32.573,35.484 32.934,34.375z" />
|
|
||||||
</vector>
|
|
||||||
@@ -1,154 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<resources>
|
|
||||||
<string name="no_tunnels">Žádné tunely ještě nebyly přidány!</string>
|
|
||||||
<string name="watcher_notification_text_paused">Monitorování změn ve stavu sítě: pozastaveno</string>
|
|
||||||
<string name="notification_permission_required">Je vyžadováno oprávnění pro oznámení.</string>
|
|
||||||
<string name="add_trusted_ssid">Přidat název důvěryhodné Wi-Fi</string>
|
|
||||||
<string name="enable_auto_tunnel">Spustit automatické tunelování</string>
|
|
||||||
<string name="tunnel_mobile_data">Tunelování na mobilních datech</string>
|
|
||||||
<string name="one_tunnel_required">Alespoň jeden tunel je vyžadován pro použití této funkce</string>
|
|
||||||
<string name="privacy_policy">Otevřít zásady soukromí</string>
|
|
||||||
<string name="okay">OK</string>
|
|
||||||
<string name="thank_you">Děkujeme za používání WG Tunnel!</string>
|
|
||||||
<string name="add_tunnels_text">Přidat ze souboru nebo zipu</string>
|
|
||||||
<string name="open_file">Otevřít soubor</string>
|
|
||||||
<string name="add_from_qr">Přidat z QR kódu</string>
|
|
||||||
<string name="qr_scan">QR skenování</string>
|
|
||||||
<string name="tunnel_name">Název tunelu</string>
|
|
||||||
<string name="add_tunnel">Přidat tunel</string>
|
|
||||||
<string name="exclude">Vyloučit</string>
|
|
||||||
<string name="include">Zahrnout</string>
|
|
||||||
<string name="save_changes">Uložit</string>
|
|
||||||
<string name="endpoint">Koncový bod</string>
|
|
||||||
<string name="vpn_connection_failed">Připojení selhalo</string>
|
|
||||||
<string name="always_on_vpn_support">Povolit trvalé připojení VPN</string>
|
|
||||||
<string name="location_services_not_detected">Služby polohy nebyly detekovány</string>
|
|
||||||
<string name="hint_search_packages">Hledat balíčky</string>
|
|
||||||
<string name="attempt_connection">Pokus o připojení...</string>
|
|
||||||
<string name="vpn_starting">VPN se spouští</string>
|
|
||||||
<string name="vpn_on">VPN zapnuto</string>
|
|
||||||
<string name="vpn_off">VPN vypnuto</string>
|
|
||||||
<string name="default_vpn_on">Hlavní VPN zapnuto</string>
|
|
||||||
<string name="default_vpn_off">Hlavní VPN vypnuto</string>
|
|
||||||
<string name="create_import">Vytvořit od základu</string>
|
|
||||||
<string name="turn_off_auto">Akce vyžaduje, aby bylo automatické tunelování vypnuté či pozastavené</string>
|
|
||||||
<string name="add_peer">Přidat peer</string>
|
|
||||||
<string name="comma_separated_list">seznam oddělený čárkami</string>
|
|
||||||
<string name="random">(náhodné)</string>
|
|
||||||
<string name="optional">(volitelné)</string>
|
|
||||||
<string name="optional_no_recommend">(volitelné, nedoporučeno)</string>
|
|
||||||
<string name="preshared_key">Předsdílený klíč</string>
|
|
||||||
<string name="seconds">vteřin(y)</string>
|
|
||||||
<string name="cancel">Zrušit</string>
|
|
||||||
<string name="error_authentication_failed">Autentizace selhala</string>
|
|
||||||
<string name="error_authorization_failed">Nepovedlo se autorizovat</string>
|
|
||||||
<string name="enabled_app_shortcuts">Zapnout zkratky</string>
|
|
||||||
<string name="export_configs">Exportovat konfigurace</string>
|
|
||||||
<string name="export_configs_failed">Nepovedlo se exportovat konfigurace</string>
|
|
||||||
<string name="location_services_required">Služby polohy vyžadovány</string>
|
|
||||||
<string name="background_location_required">Oprávnění pro polohu na pozadí vyžadováno</string>
|
|
||||||
<string name="precise_location_required">Oprávnění pro přesnou polohu vyžadováno</string>
|
|
||||||
<string name="unknown_error">Došlo k neznámé chybě</string>
|
|
||||||
<string name="exported_configs_message">Konfigurace exportovány do stažených souborů</string>
|
|
||||||
<string name="tunnel_on_wifi">Tunelovat na nedůvěryhodné Wi-Fi</string>
|
|
||||||
<string name="email_subject">WG Tunnel podpora</string>
|
|
||||||
<string name="email_chooser">Poslat email…</string>
|
|
||||||
<string name="use_kernel">Použít kernel modul</string>
|
|
||||||
<string name="error_ssid_exists">SSID již existuje</string>
|
|
||||||
<string name="error_root_denied">Oprávnění root zamítnuto</string>
|
|
||||||
<string name="error_no_file_explorer">Žádný průzkumník souborů není nainstalován</string>
|
|
||||||
<string name="error_invalid_code">Neplatný QR kód</string>
|
|
||||||
<string name="error_none">Žádná chyba</string>
|
|
||||||
<string name="auto_tunnel_title">Služba automatického tunelování</string>
|
|
||||||
<string name="resume">Obnovit</string>
|
|
||||||
<string name="active">aktivní</string>
|
|
||||||
<string name="open_issue">Otevřít případ</string>
|
|
||||||
<string name="incorrect_pin">PIN je nesprávný</string>
|
|
||||||
<string name="create_pin">Vytvořte PIN</string>
|
|
||||||
<string name="set_primary_tunnel">Nastavit jako hlavní tunel</string>
|
|
||||||
<string name="use_tunnel_on_wifi_name">Použít tunel pro Wi-Fi</string>
|
|
||||||
<string name="edit_tunnel">Upravit tunel</string>
|
|
||||||
<string name="disabled">vypnuto</string>
|
|
||||||
<string name="auto_tun_on">Obnovit automatické tunelování</string>
|
|
||||||
<string name="auto_tun_off">Pozastavit automatické tunelování</string>
|
|
||||||
<string name="version">Verze</string>
|
|
||||||
<string name="mode">Mód</string>
|
|
||||||
<string name="use_amnezia">"Použít Amnezia userspace "</string>
|
|
||||||
<string name="junk_packet_count">Junk packet počet</string>
|
|
||||||
<string name="junk_packet_minimum_size">Junk packet minimální velikost</string>
|
|
||||||
<string name="junk_packet_maximum_size">Junk packet maximální velikost</string>
|
|
||||||
<string name="init_packet_junk_size">Init packet junk velikost</string>
|
|
||||||
<string name="response_packet_junk_size">Response packet junk velikost</string>
|
|
||||||
<string name="unsure_how">pokud si nejste jisti, jak postupovat</string>
|
|
||||||
<string name="see_the">Podívejte se na</string>
|
|
||||||
<string name="getting_started_guide">začátečnickou příručku</string>
|
|
||||||
<string name="error_file_format">Neplatný formát konfigurace</string>
|
|
||||||
<string name="error_file_extension">Soubor není ve formátu .conf nebo .zip</string>
|
|
||||||
<string name="turn_off_tunnel">Akce vyžaduje vypnutí tunelu</string>
|
|
||||||
<string name="watcher_notification_text_active">Monitorování změn ve stavu sítě: aktivní</string>
|
|
||||||
<string name="tunnel_on_ethernet">Tunelovat na ethernetu</string>
|
|
||||||
<string name="tunnel_start_title">VPN připojeno</string>
|
|
||||||
<string name="prominent_background_location_message">Tato funkce vyžaduje oprávnění pro přístup k poloze na pozadí pro zapnutí monitorování Wi-Fi SSID, i když je aplikace zavřená. Pro více detailů, podívejte se prosím na zásady soukromí umístěné v kategorii Podpora.</string>
|
|
||||||
<string name="tunnel_start_text">Připojeno k tunelu</string>
|
|
||||||
<string name="tunnels">Tunely</string>
|
|
||||||
<string name="disable_auto_tunnel">Zastavit automatické tunelování</string>
|
|
||||||
<string name="tunnel_all">Tunelovat všechny aplikace</string>
|
|
||||||
<string name="config_changes_saved">Změny v konfiguraci uloženy.</string>
|
|
||||||
<string name="icon">Ikona</string>
|
|
||||||
<string name="no_thanks">Ne, děkuji</string>
|
|
||||||
<string name="turn_on">Zapnout</string>
|
|
||||||
<string name="map">Mapa</string>
|
|
||||||
<string name="public_key">Veřejný klíč</string>
|
|
||||||
<string name="addresses">Adresy</string>
|
|
||||||
<string name="dns_servers">DNS servery</string>
|
|
||||||
<string name="allowed_ips">Povolené IP adresy</string>
|
|
||||||
<string name="name">Název</string>
|
|
||||||
<string name="restart">Restartovat tunel</string>
|
|
||||||
<string name="scanning_qr">Skenování QR</string>
|
|
||||||
<string name="none">Žádné názvy důvěryhodných Wi-Fi</string>
|
|
||||||
<string name="other">Ostatní</string>
|
|
||||||
<string name="auto_tunneling">Automatické tunelování</string>
|
|
||||||
<string name="turn_on_tunnel">Akce vyžaduje aktivní tunel</string>
|
|
||||||
<string name="interface_">Rozhraní</string>
|
|
||||||
<string name="done">Hotovo</string>
|
|
||||||
<string name="rotate_keys">Rotovat klíče</string>
|
|
||||||
<string name="private_key">Soukromý klíč</string>
|
|
||||||
<string name="copy_public_key">Kopírovat veřejný klíč</string>
|
|
||||||
<string name="base64_key">base64 klíč</string>
|
|
||||||
<string name="docs_description">Přečíst si dokumentaci</string>
|
|
||||||
<string name="discord_description">Přidat se ke komunitě</string>
|
|
||||||
<string name="email_description">Poslat mi email</string>
|
|
||||||
<string name="support_help_text">Pokud máte potíže, nápady pro zlepšení, nebo se chcete jen zapojit, následující prostředky jsou k dispozici:</string>
|
|
||||||
<string name="location_services_missing_message">Aplikace nenašla žádné služby polohy zapnuté na Vašem zařízení. Dle Vašeho zařízení, tohle může způsobit, že funkce nedůvěryhodné Wi-Fi nedokáže přečíst jméno připojené Wi-Fi. Chcete i přesto pokračovat?</string>
|
|
||||||
<string name="delete_tunnel">Smazat tunel</string>
|
|
||||||
<string name="delete_tunnel_message">Jste si jisti, že chcete smazat tento tunel?</string>
|
|
||||||
<string name="yes">Ano</string>
|
|
||||||
<string name="pause">Pozastavit</string>
|
|
||||||
<string name="paused">pozastaveno</string>
|
|
||||||
<string name="tunneling_apps">Tunelování aplikací</string>
|
|
||||||
<string name="all">vše</string>
|
|
||||||
<string name="included">zahrnuto</string>
|
|
||||||
<string name="excluded">vyloučeno</string>
|
|
||||||
<string name="always_on_disabled">Trvalé VPN připojení se pokusilo spustit tunel, ale tato funkce je vypnutá v nastavení.</string>
|
|
||||||
<string name="no_email_detected">Žádná emailová aplikace nebyla nalezena</string>
|
|
||||||
<string name="no_browser_detected">Žádný prohlížeč nebyl nalezen</string>
|
|
||||||
<string name="logs_saved">Logy uloženy do stažených souborů</string>
|
|
||||||
<string name="read_logs">Přečíst si logy</string>
|
|
||||||
<string name="config_parse_error">Nepovedlo se vložit konfiguraci</string>
|
|
||||||
<string name="pin_created">PIN úspěšně vytvořen</string>
|
|
||||||
<string name="enter_pin">Vložte Váš PIN</string>
|
|
||||||
<string name="no_wifi_names_configured">Žádné názvy Wi-Fi nebyly nastaveny pro tento tunel</string>
|
|
||||||
<string name="enable_app_lock">Zapnout zámek aplikace</string>
|
|
||||||
<string name="restart_on_ping">Restartovat při selhání pingu</string>
|
|
||||||
<string name="mobile_data_tunnel">Nastavit jako tunel pro mobilní data</string>
|
|
||||||
<string name="general">Obecné</string>
|
|
||||||
<string name="settings">Nastavení</string>
|
|
||||||
<string name="support">Podpora</string>
|
|
||||||
<string name="app_name">WG Tunnel</string>
|
|
||||||
<string name="mtu">MTU</string>
|
|
||||||
<string name="db_name">wg-tunnel-db</string>
|
|
||||||
<string name="listen_port">Naslouchací port</string>
|
|
||||||
<string name="auto">(automaticky)</string>
|
|
||||||
<string name="kernel">Kernel</string>
|
|
||||||
<string name="backend">Backend</string>
|
|
||||||
</resources>
|
|
||||||
@@ -1,41 +1,42 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources>
|
<resources>
|
||||||
<string name="app_name">WG Tunnel</string>
|
<string name="app_name">WG Tunnel</string>
|
||||||
<string name="error_file_extension">Datei ist keine .conf oder .zip</string>
|
<string name="error_file_extension">Dies ist keine .conf oder .zip Datei</string>
|
||||||
<string name="no_tunnels">Noch keine Tunnel hinzugefügt!</string>
|
<string name="no_tunnels">Noch keine Tunnel hinzugefügt!</string>
|
||||||
<string name="watcher_notification_text_active">Überwachung der Netzwerkänderungen: aktiv</string>
|
<string name="watcher_notification_text_active">Überwachung der Netzwerkänderungen: aktiv</string>
|
||||||
<string name="tunnels">Tunnel</string>
|
<string name="tunnels">Tunnel</string>
|
||||||
<string name="enable_auto_tunnel">Auto-Tunneln starten</string>
|
<string name="enable_auto_tunnel">Starte automatisches Verbinden</string>
|
||||||
<string name="tunnel_mobile_data">Tunnel für mobile Daten</string>
|
<string name="tunnel_mobile_data">Verbinden im Mobilnetzwerk</string>
|
||||||
<string name="one_tunnel_required">Mindestens ein Tunnel wird für diese Funktion benötigt</string>
|
<string name="one_tunnel_required">Mindestens eine Verbindung wird für diese Funktion benötigt</string>
|
||||||
<string name="privacy_policy">Datenschutzbestimmungen anzeigen</string>
|
<string name="privacy_policy">Siehe Privacy Policy</string>
|
||||||
<string name="disable_auto_tunnel">Auto-Tunneln stoppen</string>
|
<string name="disable_auto_tunnel">Stoppe automatisches Verbinden</string>
|
||||||
<string name="okay">Ok</string>
|
<string name="okay">Okay</string>
|
||||||
<string name="tunnel_on_ethernet">Tunnel für Ethernet</string>
|
<string name="tunnel_on_ethernet">Verbindung über Ethernet</string>
|
||||||
<string name="attempt_connection">Verbindungsversuch..</string>
|
<string name="search_icon">Suchsymbol</string>
|
||||||
<string name="auto_tunneling">Auto-Tunneln</string>
|
<string name="attempt_connection">Versuche zu verbinden..</string>
|
||||||
|
<string name="auto_tunneling">Automatisches verbinden</string>
|
||||||
<string name="default_vpn_off">Primärer VPN aus</string>
|
<string name="default_vpn_off">Primärer VPN aus</string>
|
||||||
<string name="turn_on_tunnel">Für diese Aktion muss ein aktiver Tunnel bestehen</string>
|
<string name="turn_on_tunnel">Für diese Altion muss eine Verbindung bestehen</string>
|
||||||
<string name="watcher_notification_text_paused">Überwachung der Netzwerkänderungen: pausiert</string>
|
<string name="watcher_notification_text_paused">Überwachung der Netzwerkänderungen: pausiert</string>
|
||||||
<string name="tunnel_start_title">VPN verbunden</string>
|
<string name="tunnel_start_title">VPN verbunden</string>
|
||||||
<string name="tunnel_start_text">Mit Tunnel verbunden</string>
|
<string name="tunnel_start_text">Mit Tunnel verbunden</string>
|
||||||
<string name="notification_permission_required">Benachrichtigungsberechtigung benötigt.</string>
|
<string name="notification_permission_required">Berechtigung für die Benachrichtigung benötigt.</string>
|
||||||
<string name="add_trusted_ssid">Vertrauenswürdigen WLAN-Namen hinzufügen</string>
|
<string name="add_trusted_ssid">Vertrauenswürdiger WiFi-Name hinzugügen</string>
|
||||||
<string name="prominent_background_location_message">Diese Funktion erfordert die Erlaubnis zur Standortbestimmung im Hintergrund, um die Überwachung der WLAN SSID zu ermöglichen, auch wenn die Anwendung geschlossen ist. Weitere Einzelheiten in den Datenschutzbestimmungen, die auf dem Support-Bildschirm verlinkt sind.</string>
|
<string name="prominent_background_location_message">Diese Funktion benötigt Standortberechtigung im Hintergrund um die WiFi SSIDs auch wenn die Applikation geschlossen ist zu überwachen. Für mehr Infos, siehe Privacy Policy auf der Hilfeseite.</string>
|
||||||
<string name="prominent_background_location_title">Vereinbarung der Standortberechtigung im Hintergrund</string>
|
<string name="prominent_background_location_title">Vereinbarung der Standortberechtigung im Hintergrund</string>
|
||||||
<string name="thank_you">Danke fürs Benutzen von WG Tunnel!</string>
|
<string name="thank_you">Danke fürs benutzen von WG Tunnel!</string>
|
||||||
<string name="trusted_ssid_empty_description">SSID eingeben</string>
|
<string name="trusted_ssid_empty_description">SSID eingeben</string>
|
||||||
<string name="trusted_ssid_value_description">SSID bestätigen</string>
|
<string name="trusted_ssid_value_description">SSID bestätigen</string>
|
||||||
<string name="add_tunnels_text">Von Datei oder ZIP hinzufügen</string>
|
<string name="add_tunnels_text">Von Datei oder ZIP hinzufügen</string>
|
||||||
<string name="open_file">Datei öffnen</string>
|
<string name="open_file">Datei geöffnet</string>
|
||||||
<string name="add_from_qr">Über QR-Code hinzufügen</string>
|
<string name="add_from_qr">Von QR-Code hinzufügen</string>
|
||||||
<string name="qr_scan">Scanne QR</string>
|
<string name="qr_scan">Scanne QR</string>
|
||||||
<string name="tunnel_name">Tunnel Name</string>
|
<string name="tunnel_name">Verbindungsname</string>
|
||||||
<string name="add_tunnel">Tunnel hinzufügen</string>
|
<string name="add_tunnel">Verbindung hinzufügen</string>
|
||||||
<string name="exclude">Ausschließen</string>
|
<string name="exclude">Ausgenommen</string>
|
||||||
<string name="include">Einschließen</string>
|
<string name="include">Eingeschlossen</string>
|
||||||
<string name="tunnel_all">Alle Apps tunneln</string>
|
<string name="tunnel_all">Verbinde alle Apps</string>
|
||||||
<string name="config_changes_saved">Konfigurationsänderungen gespeichert.</string>
|
<string name="config_changes_saved">Änderungen der Konfiguration gespeichert.</string>
|
||||||
<string name="save_changes">Speichern</string>
|
<string name="save_changes">Speichern</string>
|
||||||
<string name="icon">Symbol</string>
|
<string name="icon">Symbol</string>
|
||||||
<string name="no_thanks">Nein danke</string>
|
<string name="no_thanks">Nein danke</string>
|
||||||
@@ -49,123 +50,27 @@
|
|||||||
<string name="allowed_ips">Erlaubte IPs</string>
|
<string name="allowed_ips">Erlaubte IPs</string>
|
||||||
<string name="endpoint">Endpunkt</string>
|
<string name="endpoint">Endpunkt</string>
|
||||||
<string name="name">Name</string>
|
<string name="name">Name</string>
|
||||||
<string name="restart">Tunnel neustarten</string>
|
<string name="restart">Verbindung neustarten</string>
|
||||||
<string name="vpn_connection_failed">Verbindung fehlgeschlagen</string>
|
<string name="vpn_connection_failed">Verbindung fehlgeschlagen</string>
|
||||||
<string name="always_on_vpn_support">Always-On VPN erlauben</string>
|
<string name="always_on_vpn_support">Erlaube Always-On VPN</string>
|
||||||
<string name="location_services_not_detected">Standortdienste nicht erkannt</string>
|
<string name="location_services_not_detected">Standortdienste Nicht Erkannt</string>
|
||||||
<string name="hint_search_packages">Pakete suchen</string>
|
<string name="hint_search_packages">Suche packete</string>
|
||||||
|
<string name="clear_icon">Lösche Symbol</string>
|
||||||
<string name="vpn_starting">VPN startet</string>
|
<string name="vpn_starting">VPN startet</string>
|
||||||
<string name="db_name">wg-tunnel-db</string>
|
<string name="db_name">wg-tunnel-db</string>
|
||||||
<string name="scanning_qr">Scanne nach QR</string>
|
<string name="scanning_qr">Scanne nach QR</string>
|
||||||
<string name="none">Keine vertrauenswürdigen WLAN Namen</string>
|
<string name="none">Keine vertrauenswürdigen WiFi Namen</string>
|
||||||
<string name="other">Sonstige</string>
|
<string name="other">Andere</string>
|
||||||
<string name="vpn_on">VPN an</string>
|
<string name="vpn_on">VPN an</string>
|
||||||
<string name="vpn_off">VPN aus</string>
|
<string name="vpn_off">VPN aus</string>
|
||||||
<string name="default_vpn_on">Primärer VPN an</string>
|
<string name="default_vpn_on">Primärer VPN an</string>
|
||||||
<string name="create_import">Von Grund auf neu erstellen</string>
|
<string name="create_import">Starte von Grund auf neu</string>
|
||||||
<string name="turn_off_auto">Für diese Aktion muss Auto-Tunneln ausgeschaltet oder pausiert sein</string>
|
<string name="turn_off_auto">Für diese Aktion muss automatisches Verbinden ausgeschaltet oder pausiert sein</string>
|
||||||
<string name="add_peer">Peer hinzufügen</string>
|
<string name="add_peer">Peer hinzufügen</string>
|
||||||
<string name="done">Erledigt</string>
|
<string name="done">Erledigt</string>
|
||||||
<string name="rotate_keys">Schlüssel rotieren</string>
|
<string name="rotate_keys">Schlüssel ändern</string>
|
||||||
<string name="private_key">Privater Schlüssel</string>
|
<string name="private_key">Privater Schlüssel</string>
|
||||||
<string name="copy_public_key">Öffentlichen Schlüssel kopieren</string>
|
<string name="copy_public_key">Öffentlicher Schlüssel kopieren</string>
|
||||||
<string name="base64_key">base64-Schlüssel</string>
|
<string name="base64_key">base64-Schlüssel</string>
|
||||||
<string name="comma_separated_list">Kommaseparierte Liste</string>
|
<string name="comma_separated_list">Kommaseparierte Liste</string>
|
||||||
<string name="delete_tunnel">Tunnel löschen</string>
|
|
||||||
<string name="persistent_keepalive">Dauerhaftes Keepalive</string>
|
|
||||||
<string name="background_location_required">Hintergrund Standortdienste erforderlich</string>
|
|
||||||
<string name="enable_app_lock">App-Sperre aktivieren</string>
|
|
||||||
<string name="discord_description">Tritt der Community bei</string>
|
|
||||||
<string name="interface_">Schnittstelle</string>
|
|
||||||
<string name="listen_port">Eingehender Port</string>
|
|
||||||
<string name="random">(zufällig)</string>
|
|
||||||
<string name="optional">(optional)</string>
|
|
||||||
<string name="optional_no_recommend">(Optional, nicht empfohlen)</string>
|
|
||||||
<string name="seconds">Sekunden</string>
|
|
||||||
<string name="cancel">Abbrechen</string>
|
|
||||||
<string name="preshared_key">Geteilter Schlüssel</string>
|
|
||||||
<string name="enabled_app_shortcuts">App-Verknüpfungen aktivieren</string>
|
|
||||||
<string name="exported_configs_message">Konfigurationen in Download Ordner exportiert</string>
|
|
||||||
<string name="tunnel_on_wifi">Tunnel bei nicht vertrauenswürdigem WLAN</string>
|
|
||||||
<string name="email_subject">WG Tunnel Unterstützung</string>
|
|
||||||
<string name="docs_description">Dokumentation lesen</string>
|
|
||||||
<string name="email_description">Sende mir eine E-Mail</string>
|
|
||||||
<string name="support_help_text">Bei Fehlern oder Verbesserungsvorschlägen stehen folgende Ressourcen zur Verfügung:</string>
|
|
||||||
<string name="error_root_denied">Root Shell verweigert</string>
|
|
||||||
<string name="error_no_file_explorer">Kein Datei-Explorer installiert</string>
|
|
||||||
<string name="location_services_missing_message">Die App erkennt keine auf deinem Gerät aktivierten Standortdienste. Je nach Gerät kann dies dazu führen, dass die Funktion \"Nicht vertrauenswürdiges WLAN\" den WLAN-Namen nicht lesen kann. Möchtest du trotzdem fortfahren?</string>
|
|
||||||
<string name="auto_tunnel_title">Auto-Tunnel Service</string>
|
|
||||||
<string name="delete_tunnel_message">Bist du sicher, dass du den Tunnel löschen möchtest?</string>
|
|
||||||
<string name="yes">Ja</string>
|
|
||||||
<string name="resume">Fortsetzen</string>
|
|
||||||
<string name="pause">Pausieren</string>
|
|
||||||
<string name="paused">Pausiert</string>
|
|
||||||
<string name="active">Aktiv</string>
|
|
||||||
<string name="go">Los</string>
|
|
||||||
<string name="excluded">ausgeschlossen</string>
|
|
||||||
<string name="all">Alle</string>
|
|
||||||
<string name="always_on_disabled">Always-on VPN wollte einen Tunnel starten, aber dieses Feature ist in den Einstellungen deaktiviert.</string>
|
|
||||||
<string name="no_browser_detected">Keinen Browser erkannt</string>
|
|
||||||
<string name="open_issue">Issue öffnen</string>
|
|
||||||
<string name="read_logs">Logs lesen</string>
|
|
||||||
<string name="auto">(automatisch)</string>
|
|
||||||
<string name="config_parse_error">Fehler beim Lesen der Konfiguration</string>
|
|
||||||
<string name="incorrect_pin">PIN nicht korrekt</string>
|
|
||||||
<string name="pin_created">PIN erfolgreich erstellt</string>
|
|
||||||
<string name="enter_pin">Deine PIN eingeben</string>
|
|
||||||
<string name="auto_off">Auto-Tunneln pausieren</string>
|
|
||||||
<string name="auto_tun_on">Auto-Tunneln fortsetzen</string>
|
|
||||||
<string name="auto_tun_off">Auto-Tunneln pausieren</string>
|
|
||||||
<string name="version">Version</string>
|
|
||||||
<string name="mode">Modus</string>
|
|
||||||
<string name="userspace">Benutzerfläche</string>
|
|
||||||
<string name="settings">Einstellungen</string>
|
|
||||||
<string name="support">Unterstützung</string>
|
|
||||||
<string name="watcher_channel_id">Wächterkanal</string>
|
|
||||||
<string name="error_authentication_failed">Anmeldung fehlgeschlagen</string>
|
|
||||||
<string name="export_configs">Konfigurationen exportieren</string>
|
|
||||||
<string name="unknown_error">Unbekannter Fehler aufgetreten</string>
|
|
||||||
<string name="email_chooser">Sende eine E-Mail…</string>
|
|
||||||
<string name="error_authorization_failed">Autorisierung fehlgeschlagen</string>
|
|
||||||
<string name="location_services_required">Standortdienste erforderlich</string>
|
|
||||||
<string name="precise_location_required">Genauer Standort erforderlich</string>
|
|
||||||
<string name="error_invalid_code">Ungültiger QR Code</string>
|
|
||||||
<string name="error_none">Kein Fehler</string>
|
|
||||||
<string name="tunneling_apps">Getunnelte Apps</string>
|
|
||||||
<string name="included">eingeschlossen</string>
|
|
||||||
<string name="no_email_detected">Keine E-Mail-App erkannt</string>
|
|
||||||
<string name="logs_saved">Logs im Download Ordner gespeichert</string>
|
|
||||||
<string name="create_pin">PIN erstellen</string>
|
|
||||||
<string name="use_tunnel_on_wifi_name">Tunnel für WLAN-Namen verwenden</string>
|
|
||||||
<string name="no_wifi_names_configured">Keine WLAN-Namen für diesen Tunnel konfiguriert</string>
|
|
||||||
<string name="disabled">Deaktiviert</string>
|
|
||||||
<string name="mobile_data_tunnel">Als Tunnel für Mobile Daten setzen</string>
|
|
||||||
<string name="general">Allgemein</string>
|
|
||||||
<string name="restart_on_ping">Neustart bei PING Fehler (Beta)</string>
|
|
||||||
<string name="edit_tunnel">Tunnel bearbeiten</string>
|
|
||||||
<string name="set_primary_tunnel">Als Primären Tunnel setzen</string>
|
|
||||||
<string name="auto_on">Auto-Tunneln fortsetzen</string>
|
|
||||||
<string name="vpn_channel_id">VPN Kanal</string>
|
|
||||||
<string name="vpn_channel_name">VPN Benachrichtigungskanal</string>
|
|
||||||
<string name="watcher_channel_name">Wächterbenachrichtigungskanal</string>
|
|
||||||
<string name="turn_off_tunnel">Aktion erfordert deaktivierten Tunnel</string>
|
|
||||||
<string name="kernel">Kernel</string>
|
|
||||||
<string name="use_kernel">Kernelmodul verwenden</string>
|
|
||||||
<string name="error_ssid_exists">SSID existiert bereits</string>
|
|
||||||
<string name="use_amnezia">"Amnezia Benutzerumgebung benutzen "</string>
|
|
||||||
<string name="junk_packet_count">Junk-Paket Anzahl</string>
|
|
||||||
<string name="junk_packet_maximum_size">Junk-Paket maximale Grösse</string>
|
|
||||||
<string name="init_packet_junk_size">Initial Junk-Paketgröße</string>
|
|
||||||
<string name="backend">Backend</string>
|
|
||||||
<string name="junk_packet_minimum_size">Junk-Paket minimale Grösse</string>
|
|
||||||
<string name="response_packet_junk_size">Antwort Junk-Paketgröße</string>
|
|
||||||
<string name="init_packet_magic_header">Initialpaket magic header</string>
|
|
||||||
<string name="getting_started_guide">Startanleitung erhalten</string>
|
|
||||||
<string name="transport_packet_magic_header">Transportpaket magic header</string>
|
|
||||||
<string name="underload_packet_magic_header">Unterlastpaket magic header</string>
|
|
||||||
<string name="see_the">Schaue das</string>
|
|
||||||
<string name="unsure_how">Wenn du nicht sicher bist, wie du weiterverfahren sollst</string>
|
|
||||||
<string name="export_configs_failed">Konfigurationsexport fehlgeschlagen</string>
|
|
||||||
<string name="error_file_format">Ungültige Konfiguration Tunnel-Format</string>
|
|
||||||
<string name="response_packet_magic_header">Antwortpaket magic header</string>
|
|
||||||
</resources>
|
</resources>
|
||||||
@@ -72,6 +72,7 @@
|
|||||||
<string name="always_on_vpn_support">Permitir VPN siempre-activada</string>
|
<string name="always_on_vpn_support">Permitir VPN siempre-activada</string>
|
||||||
<string name="location_services_not_detected">Servicios de Ubicación No Detectados</string>
|
<string name="location_services_not_detected">Servicios de Ubicación No Detectados</string>
|
||||||
<string name="hint_search_packages">Buscar paquetes</string>
|
<string name="hint_search_packages">Buscar paquetes</string>
|
||||||
|
<string name="clear_icon">Icono claro</string>
|
||||||
<string name="attempt_connection">Intentando conexión...</string>
|
<string name="attempt_connection">Intentando conexión...</string>
|
||||||
<string name="vpn_starting">Iniciando VPN</string>
|
<string name="vpn_starting">Iniciando VPN</string>
|
||||||
<string name="db_name">wg-tunnel-db</string>
|
<string name="db_name">wg-tunnel-db</string>
|
||||||
@@ -105,6 +106,7 @@
|
|||||||
<string name="listen_port">Puerto de escucha</string>
|
<string name="listen_port">Puerto de escucha</string>
|
||||||
<string name="preshared_key">Clave previamente compartida</string>
|
<string name="preshared_key">Clave previamente compartida</string>
|
||||||
<string name="persistent_keepalive">Keepalive persistente</string>
|
<string name="persistent_keepalive">Keepalive persistente</string>
|
||||||
|
<string name="search_icon">Icono de búsqueda</string>
|
||||||
<string name="docs_description">Leer documentación</string>
|
<string name="docs_description">Leer documentación</string>
|
||||||
<string name="email_description">Envíame un email</string>
|
<string name="email_description">Envíame un email</string>
|
||||||
<string name="support_help_text">Si tienes problemas, ideas para mejoras, o simlemente comprometerte, tienes disponibles los siguientes recursos:</string>
|
<string name="support_help_text">Si tienes problemas, ideas para mejoras, o simlemente comprometerte, tienes disponibles los siguientes recursos:</string>
|
||||||
@@ -135,7 +137,7 @@
|
|||||||
<string name="pin_created">Pin creado con éxito</string>
|
<string name="pin_created">Pin creado con éxito</string>
|
||||||
<string name="enter_pin">Introduce tu pin</string>
|
<string name="enter_pin">Introduce tu pin</string>
|
||||||
<string name="create_pin">Crear pin</string>
|
<string name="create_pin">Crear pin</string>
|
||||||
<string name="enable_app_lock">Activar el bloqueo de aplicaciones</string>
|
<string name="enable_app_lock">Bloqueo de app activado</string>
|
||||||
<string name="restart_on_ping">Reiniciar al fallar ping (beta)</string>
|
<string name="restart_on_ping">Reiniciar al fallar ping (beta)</string>
|
||||||
<string name="set_primary_tunnel">Establecer como túnel Principal</string>
|
<string name="set_primary_tunnel">Establecer como túnel Principal</string>
|
||||||
<string name="no_wifi_names_configured">No hay nombres Wi-Fi configurados para este túnel</string>
|
<string name="no_wifi_names_configured">No hay nombres Wi-Fi configurados para este túnel</string>
|
||||||
@@ -152,9 +154,9 @@
|
|||||||
<string name="watcher_channel_id">Canal del obvervador</string>
|
<string name="watcher_channel_id">Canal del obvervador</string>
|
||||||
<string name="watcher_channel_name">Canal de notificación del obvervador</string>
|
<string name="watcher_channel_name">Canal de notificación del obvervador</string>
|
||||||
<string name="prominent_background_location_message">La monitorización SSID Wi-Fi necesita de permiso de ubicación en segundo plano incluso si la app está cerrada. Mira el enlace a la Política de Privacidad en la pantalla de ayuda para más detalles.</string>
|
<string name="prominent_background_location_message">La monitorización SSID Wi-Fi necesita de permiso de ubicación en segundo plano incluso si la app está cerrada. Mira el enlace a la Política de Privacidad en la pantalla de ayuda para más detalles.</string>
|
||||||
<string name="export_configs_failed">Error al exportar la configuración</string>
|
<string name="privacy_policy_url" translatable="false"></string>
|
||||||
<string name="use_amnezia">"Utilizar el entorno de usuario de Amnezia "</string>
|
<string name="docs_url" translatable="false"></string>
|
||||||
<string name="junk_packet_count">Recuento de paquetes basura</string>
|
<string name="github_url" translatable="false"></string>
|
||||||
<string name="backend">Backend</string>
|
<string name="discord_url" translatable="false"></string>
|
||||||
<string name="junk_packet_minimum_size">Tamaño mínimo del paquete basura</string>
|
<string name="my_email" translatable="false"></string>
|
||||||
</resources>
|
</resources>
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<resources></resources>
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<resources>
|
|
||||||
<string name="tunnels">Tunele</string>
|
|
||||||
<string name="app_name">WG Tunnel</string>
|
|
||||||
</resources>
|
|
||||||
@@ -1,162 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<resources>
|
|
||||||
<string name="watcher_notification_text_active">Monitoramento de mudanças do estado da rede: Ativo</string>
|
|
||||||
<string name="watcher_notification_text_paused">Monitoramento de mudanças do estado da rede: pausado</string>
|
|
||||||
<string name="tunnel_start_title">VPN conectada</string>
|
|
||||||
<string name="tunnel_on_ethernet">Túnel na ethernet</string>
|
|
||||||
<string name="save_changes">Salvar</string>
|
|
||||||
<string name="public_key">Chave pública</string>
|
|
||||||
<string name="addresses">Endereços</string>
|
|
||||||
<string name="dns_servers">Servidores DNS</string>
|
|
||||||
<string name="endpoint">Endpoint</string>
|
|
||||||
<string name="name">Nome</string>
|
|
||||||
<string name="restart">Reiniciar Túnel</string>
|
|
||||||
<string name="create_import">Criar do zero</string>
|
|
||||||
<string name="turn_off_auto">Esta ação precisa do auto-túnel desativado ou pausado</string>
|
|
||||||
<string name="rotate_keys">Revezar chaves</string>
|
|
||||||
<string name="private_key">Chave privada</string>
|
|
||||||
<string name="base64_key">Chave base64</string>
|
|
||||||
<string name="optional_no_recommend">(opcional, não recomendado)</string>
|
|
||||||
<string name="preshared_key">Chave pré-compartilhada</string>
|
|
||||||
<string name="seconds">segundos</string>
|
|
||||||
<string name="export_configs">Exportar configurações</string>
|
|
||||||
<string name="export_configs_failed">Falhou ao exportar configurações</string>
|
|
||||||
<string name="go">ir</string>
|
|
||||||
<string name="error_no_file_explorer">Nenhum explorador de arquivos instalado</string>
|
|
||||||
<string name="error_invalid_code">Código QR inválido</string>
|
|
||||||
<string name="auto_tunnel_title">Serviço de Auto-túnel</string>
|
|
||||||
<string name="excluded">excluído</string>
|
|
||||||
<string name="all">todos</string>
|
|
||||||
<string name="enter_pin">Digite seu pin</string>
|
|
||||||
<string name="use_tunnel_on_wifi_name">Usar túnel em wifi com nome</string>
|
|
||||||
<string name="auto_tun_on">Continuar auto-túnel</string>
|
|
||||||
<string name="auto_tun_off">Pausar auto-túnel</string>
|
|
||||||
<string name="version">Versão</string>
|
|
||||||
<string name="mode">Modo</string>
|
|
||||||
<string name="use_amnezia">"Usar Amnezia em modo usuário "</string>
|
|
||||||
<string name="junk_packet_count">Número de pacotes-lixo</string>
|
|
||||||
<string name="junk_packet_minimum_size">Tamanho mínimo de pacote-lixo</string>
|
|
||||||
<string name="junk_packet_maximum_size">Tamanho máximo de pacote-lixo</string>
|
|
||||||
<string name="init_packet_junk_size">Tamanho de pacote-lixo inicial</string>
|
|
||||||
<string name="response_packet_junk_size">Tamanho de resposta de pacote-lixo</string>
|
|
||||||
<string name="app_name">WG Tunnel</string>
|
|
||||||
<string name="no_tunnels">Nenhum túnel foi adicionado!</string>
|
|
||||||
<string name="error_file_extension">O arquivo não é .conf ou .zip</string>
|
|
||||||
<string name="prominent_background_location_message">Este recurso precisa de permissões de localização em segundo plano para habilitar o monitoramento do SSID da rede Wi-Fi mesmo quando o aplicativo está fechado. Para mais detalhes, por favor veja a Política de Privacidade na tela de Suporte.</string>
|
|
||||||
<string name="turn_off_tunnel">Esta ação só é possível com o túnel inativo</string>
|
|
||||||
<string name="enabled_app_shortcuts">Habilitar atalhos do aplicativo</string>
|
|
||||||
<string name="notification_permission_required">Necessita permissões de notificação.</string>
|
|
||||||
<string name="add_trusted_ssid">Adicionar nome de Wi-Fi confiável</string>
|
|
||||||
<string name="enable_auto_tunnel">Iniciar auto-túnel</string>
|
|
||||||
<string name="tunnels">Túneis</string>
|
|
||||||
<string name="disable_auto_tunnel">Parar auto-túnel</string>
|
|
||||||
<string name="tunnel_start_text">Conectado ao túnel</string>
|
|
||||||
<string name="trusted_ssid_empty_description">Digite o SSID</string>
|
|
||||||
<string name="no_thanks">Não, obrigado</string>
|
|
||||||
<string name="privacy_policy">Ver a Política de Privacidade</string>
|
|
||||||
<string name="okay">OK</string>
|
|
||||||
<string name="tunnel_mobile_data">Túnel em dados móveis</string>
|
|
||||||
<string name="one_tunnel_required">Pelo menos um túnel é necessário para usar este recurso</string>
|
|
||||||
<string name="prominent_background_location_title">Revelar a localização em segundo plano</string>
|
|
||||||
<string name="thank_you">Obrigado por usar o WG Tunnel!</string>
|
|
||||||
<string name="trusted_ssid_value_description">Envie o SSID</string>
|
|
||||||
<string name="open_file">Abrir Arquivo</string>
|
|
||||||
<string name="add_from_qr">Adicionar a partir de código QR</string>
|
|
||||||
<string name="add_tunnels_text">Adicionar a partir de arquivo ou zip</string>
|
|
||||||
<string name="tunnel_all">Todos os aplicativos pelo túnel</string>
|
|
||||||
<string name="icon">Ícone</string>
|
|
||||||
<string name="turn_on">Ligar</string>
|
|
||||||
<string name="qr_scan">Escanear o código QR</string>
|
|
||||||
<string name="tunnel_name">Nome do Túnel</string>
|
|
||||||
<string name="add_tunnel">Adicionar Túnel</string>
|
|
||||||
<string name="config_changes_saved">Mudanças nas configurações salvas.</string>
|
|
||||||
<string name="exclude">Excluir</string>
|
|
||||||
<string name="include">Incluir</string>
|
|
||||||
<string name="map">Mapa</string>
|
|
||||||
<string name="vpn_connection_failed">Falha na conexão</string>
|
|
||||||
<string name="mtu">MTU</string>
|
|
||||||
<string name="always_on_vpn_support">Permitir VPN sempre ligada</string>
|
|
||||||
<string name="allowed_ips">IPs Permitidos</string>
|
|
||||||
<string name="attempt_connection">Tentando conexão..</string>
|
|
||||||
<string name="peer">Par</string>
|
|
||||||
<string name="location_services_not_detected">Serviço de localização não foi detectado</string>
|
|
||||||
<string name="hint_search_packages">Procurar pacotes</string>
|
|
||||||
<string name="vpn_starting">Iniciando VPN</string>
|
|
||||||
<string name="other">Outro</string>
|
|
||||||
<string name="scanning_qr">Escaneando código QR</string>
|
|
||||||
<string name="none">Nenhum nome de Wi-Fi confiável</string>
|
|
||||||
<string name="auto_tunneling">Auto-túnel</string>
|
|
||||||
<string name="default_vpn_on">VPN Principal ligada</string>
|
|
||||||
<string name="vpn_on">VPN ligada</string>
|
|
||||||
<string name="vpn_off">VPN desligada</string>
|
|
||||||
<string name="listen_port">Porta de escuta</string>
|
|
||||||
<string name="default_vpn_off">VPN Principal desligada</string>
|
|
||||||
<string name="turn_on_tunnel">Esta ação precisa um túnel ativo</string>
|
|
||||||
<string name="done">Feito</string>
|
|
||||||
<string name="add_peer">Adicionar par</string>
|
|
||||||
<string name="interface_">Interface</string>
|
|
||||||
<string name="copy_public_key">Copiar chave pública</string>
|
|
||||||
<string name="comma_separated_list">Lista separada por vírgulas</string>
|
|
||||||
<string name="optional">(opcional)</string>
|
|
||||||
<string name="random">(aleatório)</string>
|
|
||||||
<string name="persistent_keepalive">Manter a conexão persistente (keepalive)</string>
|
|
||||||
<string name="cancel">Cancelar</string>
|
|
||||||
<string name="error_authentication_failed">Autenticação falhou</string>
|
|
||||||
<string name="error_authorization_failed">Autorização falhou</string>
|
|
||||||
<string name="restart_on_ping">Reiniciar em falha de ping (beta)</string>
|
|
||||||
<string name="background_location_required">Necessita a localização em segundo plano</string>
|
|
||||||
<string name="location_services_required">Necessita dos serviços de localização</string>
|
|
||||||
<string name="email_description">Me envie um email</string>
|
|
||||||
<string name="error_ssid_exists">SSID já existe</string>
|
|
||||||
<string name="delete_tunnel_message">Tem certeza que você quer apagar este túnel?</string>
|
|
||||||
<string name="yes">Sim</string>
|
|
||||||
<string name="precise_location_required">Necessita da localização precisa</string>
|
|
||||||
<string name="exported_configs_message">Configurações exportadas para downloads</string>
|
|
||||||
<string name="unknown_error">Ocorreu um erro desconhecido</string>
|
|
||||||
<string name="email_subject">Suporte para o WG Tunnel</string>
|
|
||||||
<string name="tunnel_on_wifi">Túnel em Wi-Fi não confiável</string>
|
|
||||||
<string name="error_none">Nenhum erro</string>
|
|
||||||
<string name="delete_tunnel">Apagar túnel</string>
|
|
||||||
<string name="email_chooser">Enviar um email…</string>
|
|
||||||
<string name="use_kernel">Usar o módulo do kernel</string>
|
|
||||||
<string name="discord_description">Juntar-se à comunidade</string>
|
|
||||||
<string name="docs_description">Ler a documentação</string>
|
|
||||||
<string name="support_help_text">Se você enfrentar problemas, tiver ideias para melhorias ou apenas quiser participar, os seguintes recursos estão disponíveis:</string>
|
|
||||||
<string name="error_root_denied">Shell Root negado</string>
|
|
||||||
<string name="location_services_missing_message">O aplicativo não detectou o serviço de localização habilitado no seu dispositivo. Dependendo do dispositivo, isto pode causar que a função de Wi-Fi não confiável falhe em ler o nome do Wi-Fi. Quer continuar mesmo assim?</string>
|
|
||||||
<string name="paused">pausado</string>
|
|
||||||
<string name="included">incluso</string>
|
|
||||||
<string name="resume">Continuar</string>
|
|
||||||
<string name="pause">Pausar</string>
|
|
||||||
<string name="active">ativo</string>
|
|
||||||
<string name="always_on_disabled">VPN sempre ligada tentou iniciar um túnel, mas este recurso está desligado nas configurações.</string>
|
|
||||||
<string name="open_issue">Abrir um problema</string>
|
|
||||||
<string name="tunneling_apps">Aplicativos em túnel</string>
|
|
||||||
<string name="no_email_detected">Nenhum aplicativo de email detectado</string>
|
|
||||||
<string name="no_browser_detected">Nenhum navegador detectado</string>
|
|
||||||
<string name="logs_saved">Registros salvos em downloads</string>
|
|
||||||
<string name="incorrect_pin">O Pin está errado</string>
|
|
||||||
<string name="auto">(automático)</string>
|
|
||||||
<string name="disabled">desligado</string>
|
|
||||||
<string name="read_logs">Ler os registros</string>
|
|
||||||
<string name="config_parse_error">Falha na interpretação das configurações</string>
|
|
||||||
<string name="pin_created">Pin criado com sucesso</string>
|
|
||||||
<string name="auto_on">Continuar auto-túnel</string>
|
|
||||||
<string name="create_pin">Criar um pin</string>
|
|
||||||
<string name="enable_app_lock">Ligar trava de aplicativo</string>
|
|
||||||
<string name="no_wifi_names_configured">Nenhum Wi-Fi configurado para este túnel</string>
|
|
||||||
<string name="edit_tunnel">Editar túnel</string>
|
|
||||||
<string name="mobile_data_tunnel">Selecionar como túnel em dados móveis</string>
|
|
||||||
<string name="set_primary_tunnel">Selecionar como túnel principal</string>
|
|
||||||
<string name="general">Geral</string>
|
|
||||||
<string name="userspace">Modo usuário</string>
|
|
||||||
<string name="support">Suporte</string>
|
|
||||||
<string name="kernel">Kernel</string>
|
|
||||||
<string name="auto_off">Pausar auto-túnel</string>
|
|
||||||
<string name="backend">Backend</string>
|
|
||||||
<string name="settings">Configurações</string>
|
|
||||||
<string name="unsure_how">se não tiver certeza em como continuar</string>
|
|
||||||
<string name="see_the">Veja o</string>
|
|
||||||
<string name="getting_started_guide">guia de início rápido</string>
|
|
||||||
<string name="error_file_format">Formato de arquivo de configuração inválido</string>
|
|
||||||
</resources>
|
|
||||||
@@ -1,171 +1,3 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources>
|
<resources>
|
||||||
<string name="turn_off_tunnel">Действие требует отключения туннеля</string>
|
</resources>
|
||||||
<string name="add_tunnel">Добавить туннель</string>
|
|
||||||
<string name="mtu">MTU</string>
|
|
||||||
<string name="tunnel_name">Имя туннеля</string>
|
|
||||||
<string name="public_key">Публичный ключ</string>
|
|
||||||
<string name="name">Имя</string>
|
|
||||||
<string name="peer">Пир</string>
|
|
||||||
<string name="privacy_policy">Посмотреть политику конфиденциальности</string>
|
|
||||||
<string name="icon">Иконка</string>
|
|
||||||
<string name="turn_on">Включить</string>
|
|
||||||
<string name="add_from_qr">Добавить из QR</string>
|
|
||||||
<string name="qr_scan">Сканировать QR</string>
|
|
||||||
<string name="auto_tunneling">Авто-туннелирование</string>
|
|
||||||
<string name="no_tunnels">Туннели ещё не добавлены!</string>
|
|
||||||
<string name="open_file">Открыть файл</string>
|
|
||||||
<string name="exclude">Исключить</string>
|
|
||||||
<string name="include">Включить</string>
|
|
||||||
<string name="tunnel_all">Туннель для всех приложений</string>
|
|
||||||
<string name="config_changes_saved">Изменения конфигурации сохранены.</string>
|
|
||||||
<string name="save_changes">Сохранить</string>
|
|
||||||
<string name="no_thanks">Нет, спасибо</string>
|
|
||||||
<string name="map">Карта</string>
|
|
||||||
<string name="addresses">Адреса</string>
|
|
||||||
<string name="dns_servers">DNS-серверы</string>
|
|
||||||
<string name="allowed_ips">Разрешённые IP</string>
|
|
||||||
<string name="endpoint">Конечная точка</string>
|
|
||||||
<string name="restart">Перезапустить туннель</string>
|
|
||||||
<string name="vpn_connection_failed">Ошибка соединения</string>
|
|
||||||
<string name="always_on_vpn_support">Разрешить постоянный VPN</string>
|
|
||||||
<string name="hint_search_packages">Поиск приложений</string>
|
|
||||||
<string name="other">Другое</string>
|
|
||||||
<string name="vpn_on">VPN вкл.</string>
|
|
||||||
<string name="vpn_off">VPN откл.</string>
|
|
||||||
<string name="interface_">Интерфейс</string>
|
|
||||||
<string name="optional">(необязательно)</string>
|
|
||||||
<string name="optional_no_recommend">(необязательно, не рекомендуется)</string>
|
|
||||||
<string name="resume">Восстановить</string>
|
|
||||||
<string name="tunneling_apps">Туннелируемые приложения</string>
|
|
||||||
<string name="paused">приостановлено</string>
|
|
||||||
<string name="active">активно</string>
|
|
||||||
<string name="excluded">исключено</string>
|
|
||||||
<string name="all">все</string>
|
|
||||||
<string name="no_email_detected">Приложение для отправки почты не обнаружено</string>
|
|
||||||
<string name="enter_pin">Введите свой PIN-код</string>
|
|
||||||
<string name="enable_app_lock">Включить блокировку приложения</string>
|
|
||||||
<string name="restart_on_ping">Перезапуск при сбое пинга (бета)</string>
|
|
||||||
<string name="settings">Настройки</string>
|
|
||||||
<string name="support">Поддержка</string>
|
|
||||||
<string name="backend">Модуль</string>
|
|
||||||
<string name="use_amnezia">"Использовать модуль Amnezia режима пользователя "</string>
|
|
||||||
<string name="init_packet_junk_size">Начальный размер «мусорного» пакета</string>
|
|
||||||
<string name="response_packet_junk_size">Размер ответного «мусорного» пакета</string>
|
|
||||||
<string name="init_packet_magic_header">Заголовок пакета инициализации</string>
|
|
||||||
<string name="see_the">Смотрите</string>
|
|
||||||
<string name="tunnel_mobile_data">Туннелировать через мобильный интернет</string>
|
|
||||||
<string name="tunnel_on_ethernet">Туннелировать через Ethernet</string>
|
|
||||||
<string name="cancel">Отмена</string>
|
|
||||||
<string name="docs_description">Посмотреть документацию</string>
|
|
||||||
<string name="discord_description">Присоединиться к сообществу</string>
|
|
||||||
<string name="email_chooser">Отправить письмо…</string>
|
|
||||||
<string name="add_trusted_ssid">Добавить доверенное имя сети Wi-Fi</string>
|
|
||||||
<string name="included">включено</string>
|
|
||||||
<string name="vpn_starting">Идёт запуск VPN</string>
|
|
||||||
<string name="auto_tunnel_title">Сервис авто-туннелирования</string>
|
|
||||||
<string name="create_import">Создать с нуля</string>
|
|
||||||
<string name="private_key">Закрытый ключ</string>
|
|
||||||
<string name="pause">Приостановить</string>
|
|
||||||
<string name="logs_saved">Журнал сохранён в Загрузки</string>
|
|
||||||
<string name="db_name">wg-tunnel-db</string>
|
|
||||||
<string name="base64_key">Ключ в base64</string>
|
|
||||||
<string name="error_no_file_explorer">Не найден файловый менеджер</string>
|
|
||||||
<string name="delete_tunnel_message">Вы действительно хотите удалить этот туннель?</string>
|
|
||||||
<string name="version">Версия</string>
|
|
||||||
<string name="unknown_error">Неизвестная ошибка</string>
|
|
||||||
<string name="tunnel_on_wifi">Туннель в недоверенных сетях Wi-Fi</string>
|
|
||||||
<string name="error_file_extension">Файл не имеет расширение .conf или .zip</string>
|
|
||||||
<string name="random">(случайно)</string>
|
|
||||||
<string name="tunnel_start_text">Подключено к туннелю</string>
|
|
||||||
<string name="app_name">WG Tunnel</string>
|
|
||||||
<string name="vpn_channel_id">Канал VPN</string>
|
|
||||||
<string name="vpn_channel_name">Канал уведомлений VPN</string>
|
|
||||||
<string name="watcher_channel_id">Канал наблюдателя</string>
|
|
||||||
<string name="watcher_channel_name">Канал уведомлений наблюдателя</string>
|
|
||||||
<string name="watcher_notification_text_active">Отслеживание состояния сети: активно</string>
|
|
||||||
<string name="notification_permission_required">Требуется разрешение на отображение уведомлений.</string>
|
|
||||||
<string name="tunnels">Туннели</string>
|
|
||||||
<string name="enable_auto_tunnel">Запустить авто-туннель</string>
|
|
||||||
<string name="disable_auto_tunnel">Остановить авто-туннель</string>
|
|
||||||
<string name="one_tunnel_required">Для использования этой функции нужно настроить хотя бы один туннель</string>
|
|
||||||
<string name="okay">Хорошо</string>
|
|
||||||
<string name="prominent_background_location_title">Фоновая передача местоположения</string>
|
|
||||||
<string name="thank_you">Благодарим Вас за использование WG Tunnel!</string>
|
|
||||||
<string name="trusted_ssid_value_description">Отправить SSID</string>
|
|
||||||
<string name="trusted_ssid_empty_description">Введите SSID</string>
|
|
||||||
<string name="add_tunnels_text">Добавить из файла или архива</string>
|
|
||||||
<string name="location_services_not_detected">Сервисы местоположения не обнаружены</string>
|
|
||||||
<string name="attempt_connection">Попытка соединения…</string>
|
|
||||||
<string name="scanning_qr">Поиск QR-кода</string>
|
|
||||||
<string name="none">Нет доверенных имён Wi-Fi</string>
|
|
||||||
<string name="default_vpn_on">Основной VPN вкл.</string>
|
|
||||||
<string name="default_vpn_off">Основной VPN выкл.</string>
|
|
||||||
<string name="turn_off_auto">Необходимо приостановить или отключить авто-туннелирование</string>
|
|
||||||
<string name="turn_on_tunnel">Действие требует наличие активного туннеля</string>
|
|
||||||
<string name="add_peer">Добавить пира</string>
|
|
||||||
<string name="rotate_keys">Обновить ключи</string>
|
|
||||||
<string name="comma_separated_list">разделённый запятыми список</string>
|
|
||||||
<string name="listen_port">Порт прослушивания</string>
|
|
||||||
<string name="preshared_key">Общий ключ</string>
|
|
||||||
<string name="seconds">секунд</string>
|
|
||||||
<string name="persistent_keepalive">Поддержание работы туннеля (keepalive)</string>
|
|
||||||
<string name="error_authentication_failed">Сбой аутентификации</string>
|
|
||||||
<string name="export_configs">Экспорт конфигурации</string>
|
|
||||||
<string name="location_services_required">Необходимо наличие служб местоположения</string>
|
|
||||||
<string name="error_invalid_code">Некорректный QR-код</string>
|
|
||||||
<string name="location_services_missing_message">Приложение не обнаружило сервис местоположения на вашем устройстве. На некоторых устройствах это может привести к невозможности определения имени сети Wi-Fi и сбою функции недоверенной сети. Всё равно продолжить?</string>
|
|
||||||
<string name="error_ssid_exists">SSID уже существует</string>
|
|
||||||
<string name="error_root_denied">Root-доступ запрещён</string>
|
|
||||||
<string name="error_none">Нет ошибки</string>
|
|
||||||
<string name="delete_tunnel">Удалить туннель</string>
|
|
||||||
<string name="yes">Да</string>
|
|
||||||
<string name="use_tunnel_on_wifi_name">Использовать туннель в сети Wi-Fi</string>
|
|
||||||
<string name="disabled">отключено</string>
|
|
||||||
<string name="auto_off">Приостановить авто-туннель</string>
|
|
||||||
<string name="auto_tun_on">Восстановить авто-туннель</string>
|
|
||||||
<string name="watcher_notification_text_paused">Отслеживание состояния сети: приостановлено</string>
|
|
||||||
<string name="tunnel_start_title">VPN подключен</string>
|
|
||||||
<string name="prominent_background_location_message">Эта функция требует фоновый доступ к местоположению для отслеживания имён сетей Wi-Fi, даже когда приложение закрыто. Для получения дополнительной информации, прочтите политику конфиденциальности на экране поддержки.</string>
|
|
||||||
<string name="done">Готово</string>
|
|
||||||
<string name="copy_public_key">Копировать открытый ключ</string>
|
|
||||||
<string name="error_authorization_failed">Не удалось пройти аутентификацию</string>
|
|
||||||
<string name="enabled_app_shortcuts">Включить ярлыки приложения</string>
|
|
||||||
<string name="export_configs_failed">Не удалось экспортировать конфигурацию</string>
|
|
||||||
<string name="precise_location_required">Необходимо точное местоположение</string>
|
|
||||||
<string name="open_issue">Сообщить о проблеме</string>
|
|
||||||
<string name="config_parse_error">Не удалось разобрать файл конфигурации</string>
|
|
||||||
<string name="incorrect_pin">Некорректный PIN-код</string>
|
|
||||||
<string name="pin_created">PIN создан успешно</string>
|
|
||||||
<string name="create_pin">Создать PIN-код</string>
|
|
||||||
<string name="mobile_data_tunnel">Назначить как туннель для мобильного интернета</string>
|
|
||||||
<string name="edit_tunnel">Редактировать туннель</string>
|
|
||||||
<string name="auto_on">Восстановить авто-туннель</string>
|
|
||||||
<string name="auto_tun_off">Приостановить авто-туннель</string>
|
|
||||||
<string name="junk_packet_count">Количество «мусорных» пакетов</string>
|
|
||||||
<string name="junk_packet_maximum_size">Максимальный размер «мусорного» пакета</string>
|
|
||||||
<string name="error_file_format">некорректный формат конфигурации туннеля</string>
|
|
||||||
<string name="background_location_required">Необходим фоновый доступ к местоположению</string>
|
|
||||||
<string name="exported_configs_message">Экспорт конфигурации в Загрузки</string>
|
|
||||||
<string name="email_subject">Поддержка WG Tunnel</string>
|
|
||||||
<string name="go">вперёд</string>
|
|
||||||
<string name="email_description">Отправить письмо автору</string>
|
|
||||||
<string name="support_help_text">Если у Вас возникли проблемы, или появилась идея по улучшению, или Вы просто хотите пообщаться, используйте следующие ресурсы:</string>
|
|
||||||
<string name="use_kernel">Использовать модуль режима ядра</string>
|
|
||||||
<string name="always_on_disabled">Функция всегда доступного VPN попыталась запустить туннель, но эта функция отключена в настройках.</string>
|
|
||||||
<string name="no_browser_detected">Веб-браузер не обнаружен</string>
|
|
||||||
<string name="read_logs">Посмотреть журнал</string>
|
|
||||||
<string name="auto">(авто)</string>
|
|
||||||
<string name="set_primary_tunnel">Назначить как главный туннель</string>
|
|
||||||
<string name="no_wifi_names_configured">Имена сетей Wi-Fi не назначены этому туннелю</string>
|
|
||||||
<string name="general">Общее</string>
|
|
||||||
<string name="mode">Режим</string>
|
|
||||||
<string name="userspace">Пользователя</string>
|
|
||||||
<string name="kernel">Модуль ядра</string>
|
|
||||||
<string name="junk_packet_minimum_size">Минимальный размер «мусорного» пакета</string>
|
|
||||||
<string name="response_packet_magic_header">Заголовок пакета ответа</string>
|
|
||||||
<string name="transport_packet_magic_header">Заголовок транспортного пакета</string>
|
|
||||||
<string name="getting_started_guide">руководство по началу работы</string>
|
|
||||||
<string name="unsure_how">, если не уверены, что делать дальше</string>
|
|
||||||
<string name="underload_packet_magic_header">Заголовок пакета под нагрузкой</string>
|
|
||||||
</resources>
|
|
||||||
@@ -59,6 +59,8 @@
|
|||||||
<string name="always_on_vpn_support">Her Zaman Açık VPN\'e İzin Ver</string>
|
<string name="always_on_vpn_support">Her Zaman Açık VPN\'e İzin Ver</string>
|
||||||
<string name="location_services_not_detected">Konum Hizmetleri Algılanmadı</string>
|
<string name="location_services_not_detected">Konum Hizmetleri Algılanmadı</string>
|
||||||
<string name="hint_search_packages">Uygulama arayın</string>
|
<string name="hint_search_packages">Uygulama arayın</string>
|
||||||
|
<string name="clear_icon">Simgeyi Temizle</string>
|
||||||
|
<string name="search_icon">Simge Ara</string>
|
||||||
<string name="attempt_connection">Bağlantı kurulmaya çalışılıyor..</string>
|
<string name="attempt_connection">Bağlantı kurulmaya çalışılıyor..</string>
|
||||||
<string name="vpn_starting">VPN başlatılıyor</string>
|
<string name="vpn_starting">VPN başlatılıyor</string>
|
||||||
<string name="db_name">wg-tunnel-db</string>
|
<string name="db_name">wg-tunnel-db</string>
|
||||||
|
|||||||
@@ -1,169 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<resources>
|
|
||||||
<string name="comma_separated_list">список розділений комами</string>
|
|
||||||
<string name="no_tunnels">Тунелі ще не додані!</string>
|
|
||||||
<string name="app_name">WG Tunnel</string>
|
|
||||||
<string name="add_trusted_ssid">Додати ім\'я довіреної мережі Wi-Fi</string>
|
|
||||||
<string name="tunnels">Тунелі</string>
|
|
||||||
<string name="enable_auto_tunnel">Запустити авто-тунелі</string>
|
|
||||||
<string name="disable_auto_tunnel">Зупинити авто-тунелі</string>
|
|
||||||
<string name="okay">ОК</string>
|
|
||||||
<string name="tunnel_on_ethernet">Тунелювати Ethernet</string>
|
|
||||||
<string name="prominent_background_location_title">Фонова передача місцезнаходження</string>
|
|
||||||
<string name="trusted_ssid_value_description">Підтвердити SSID</string>
|
|
||||||
<string name="qr_scan">Сканувати QR</string>
|
|
||||||
<string name="tunnel_name">Ім\'я тунелю</string>
|
|
||||||
<string name="add_tunnel">Додати тунель</string>
|
|
||||||
<string name="include">Включити</string>
|
|
||||||
<string name="tunnel_all">Тунель для всіх додатків</string>
|
|
||||||
<string name="config_changes_saved">Зміни налаштувань збережено.</string>
|
|
||||||
<string name="icon">Іконка</string>
|
|
||||||
<string name="no_thanks">Ні, дякую</string>
|
|
||||||
<string name="default_vpn_off">Основний VPN вимк.</string>
|
|
||||||
<string name="turn_on_tunnel">Дія потребує активного тунелю</string>
|
|
||||||
<string name="rotate_keys">Оновити ключі</string>
|
|
||||||
<string name="private_key">Закритий ключ</string>
|
|
||||||
<string name="base64_key">Ключ в base64</string>
|
|
||||||
<string name="random">(випадково)</string>
|
|
||||||
<string name="optional">(необов\'язково)</string>
|
|
||||||
<string name="optional_no_recommend">(необов\'язково, не рекомендується)</string>
|
|
||||||
<string name="cancel">Скасувати</string>
|
|
||||||
<string name="export_configs_failed">Помилка експорту конфігурації</string>
|
|
||||||
<string name="location_services_required">Необхідно сервіси місцезнаходження</string>
|
|
||||||
<string name="precise_location_required">Необхідно доступ до точного місцезнаходження</string>
|
|
||||||
<string name="exported_configs_message">Експорт конфігурації в Завантаження</string>
|
|
||||||
<string name="email_chooser">Надіслати E-Mail…</string>
|
|
||||||
<string name="error_root_denied">Root доступ заборонено</string>
|
|
||||||
<string name="error_invalid_code">Некоректний QR-код</string>
|
|
||||||
<string name="error_none">Нема помилок</string>
|
|
||||||
<string name="logs_saved">Логи збережено в Завантаженнях</string>
|
|
||||||
<string name="config_parse_error">Помилка аналізу файлу конфігурації</string>
|
|
||||||
<string name="incorrect_pin">Невірний PIN-код</string>
|
|
||||||
<string name="use_tunnel_on_wifi_name">Використовувати тунель в мережі Wi-Fi</string>
|
|
||||||
<string name="disabled">відключено</string>
|
|
||||||
<string name="version">Версія</string>
|
|
||||||
<string name="mode">Режим</string>
|
|
||||||
<string name="transport_packet_magic_header">Заголовок транспортного пакету</string>
|
|
||||||
<string name="getting_started_guide">інструкція щодо початку роботи</string>
|
|
||||||
<string name="error_file_format">некоректний формат конфігурації тунелю</string>
|
|
||||||
<string name="watcher_notification_text_active">Моніторинг стану мережі: активний</string>
|
|
||||||
<string name="vpn_channel_name">Канал сповіщення VPN</string>
|
|
||||||
<string name="vpn_channel_id">Канал VPN</string>
|
|
||||||
<string name="error_file_extension">Файл не є .conf або .zip файлом</string>
|
|
||||||
<string name="turn_off_tunnel">Дія потребує вимкнення тунелю</string>
|
|
||||||
<string name="watcher_notification_text_paused">Моніторинг стану мережі: призупинено</string>
|
|
||||||
<string name="tunnel_start_text">Підключення до тунелю</string>
|
|
||||||
<string name="tunnel_start_title">VPN підключено</string>
|
|
||||||
<string name="notification_permission_required">Потрібен дозвіл на відображення сповіщень.</string>
|
|
||||||
<string name="tunnel_mobile_data">Тунелювати мобільні дані</string>
|
|
||||||
<string name="one_tunnel_required">Для використання даної функції потрібно налаштувати мінімум один тунель</string>
|
|
||||||
<string name="privacy_policy">Переглянути політику конфіденційності</string>
|
|
||||||
<string name="thank_you">Спасибі за використання WG Tunnel!</string>
|
|
||||||
<string name="prominent_background_location_message">Дана функція потребує фоновий доступ до служби місцезнаходження для моніторингу назви мереж Wi-Fi навіть коли додаток закрито. Для отримання додаткової інформації прочитайте політику приватності на екрані Підтримки.</string>
|
|
||||||
<string name="trusted_ssid_empty_description">Введіть SSID</string>
|
|
||||||
<string name="add_tunnels_text">Додати з файлу або архіву</string>
|
|
||||||
<string name="open_file">Відкрити файл</string>
|
|
||||||
<string name="exclude">Виключити</string>
|
|
||||||
<string name="add_from_qr">Додати з QR коду</string>
|
|
||||||
<string name="save_changes">Зберегти</string>
|
|
||||||
<string name="turn_on">Увімкнути</string>
|
|
||||||
<string name="map">Карта</string>
|
|
||||||
<string name="public_key">Публічний ключ</string>
|
|
||||||
<string name="addresses">Адреса</string>
|
|
||||||
<string name="allowed_ips">Дозволені IP</string>
|
|
||||||
<string name="dns_servers">DNS-сервери</string>
|
|
||||||
<string name="mtu">MTU</string>
|
|
||||||
<string name="peer">Пір</string>
|
|
||||||
<string name="endpoint">Кінцева точка</string>
|
|
||||||
<string name="hint_search_packages">Пошук програм</string>
|
|
||||||
<string name="name">Ім\'я</string>
|
|
||||||
<string name="vpn_connection_failed">Помилка з\'єднання</string>
|
|
||||||
<string name="restart">Перезапустити тунель</string>
|
|
||||||
<string name="always_on_vpn_support">Дозволили Always-ON VPN</string>
|
|
||||||
<string name="location_services_not_detected">Сервіси місце знаходження не знайдено</string>
|
|
||||||
<string name="db_name">wg-tunnel-db</string>
|
|
||||||
<string name="scanning_qr">Сканування QR коду</string>
|
|
||||||
<string name="attempt_connection">Спроба з\'єднання...</string>
|
|
||||||
<string name="other">Інше</string>
|
|
||||||
<string name="vpn_starting">Запуск VPN</string>
|
|
||||||
<string name="auto_tunneling">Авто-тунелювання</string>
|
|
||||||
<string name="none">Нема довірених мереж Wi-Fi</string>
|
|
||||||
<string name="vpn_on">VPN увімк.</string>
|
|
||||||
<string name="vpn_off">VPN вимк.</string>
|
|
||||||
<string name="default_vpn_on">Основний VPN увімк.</string>
|
|
||||||
<string name="create_import">Створити з нуля</string>
|
|
||||||
<string name="turn_off_auto">Необхідно вимкнути або призупинити авто-тунелювання</string>
|
|
||||||
<string name="add_peer">Додати peer</string>
|
|
||||||
<string name="done">Готово</string>
|
|
||||||
<string name="interface_">Інтерфейс</string>
|
|
||||||
<string name="copy_public_key">Копіювати відкритий ключ</string>
|
|
||||||
<string name="listen_port">Слухати порт</string>
|
|
||||||
<string name="preshared_key">Pre-shared key</string>
|
|
||||||
<string name="seconds">секунд</string>
|
|
||||||
<string name="persistent_keepalive">Persistent keepalive</string>
|
|
||||||
<string name="error_authorization_failed">Не вдалося авторизуватися</string>
|
|
||||||
<string name="enabled_app_shortcuts">Дозволити ярлики</string>
|
|
||||||
<string name="error_authentication_failed">Помилка автентифікації</string>
|
|
||||||
<string name="export_configs">Експорт конфігурації</string>
|
|
||||||
<string name="background_location_required">Необхідний фоновий доступ до місцезнаходження</string>
|
|
||||||
<string name="unknown_error">Невідома помилка</string>
|
|
||||||
<string name="tunnel_on_wifi">Тунелювати недовірені мережі Wi-Fi</string>
|
|
||||||
<string name="email_subject">Підтримка WG-Tunnel</string>
|
|
||||||
<string name="go">вперед</string>
|
|
||||||
<string name="docs_description">Переглянути документацію</string>
|
|
||||||
<string name="email_description">Відправити email автору</string>
|
|
||||||
<string name="discord_description">Приєднатися до спільноти</string>
|
|
||||||
<string name="support_help_text">Якщо у вас виникли проблеми, є ідеї щодо покращення, чи бажання долучитися, скористайтесь наступними ресурсами:</string>
|
|
||||||
<string name="use_kernel">Використовувати модуль режиму ядра</string>
|
|
||||||
<string name="error_ssid_exists">SSID вже існує</string>
|
|
||||||
<string name="error_no_file_explorer">Не знайдено файловий менеджер</string>
|
|
||||||
<string name="auto_tunnel_title">Сервіс авто-тунелювання</string>
|
|
||||||
<string name="delete_tunnel">Видалити тунель</string>
|
|
||||||
<string name="location_services_missing_message">Додаток не знайшов служб місце знаходження на вашому пристрої. На деяких пристроях це може привести до неможливості визначення назви мережі Wi-Fi і помилок функції недовірених Wi-Fi мереж. Все рівно хочете продовжити?</string>
|
|
||||||
<string name="included">включено</string>
|
|
||||||
<string name="delete_tunnel_message">Ви дійсно хочете видалити цей тунель?</string>
|
|
||||||
<string name="yes">Так</string>
|
|
||||||
<string name="active">активно</string>
|
|
||||||
<string name="resume">Відновити</string>
|
|
||||||
<string name="pause">Призупинити</string>
|
|
||||||
<string name="paused">призупинено</string>
|
|
||||||
<string name="tunneling_apps">Тунельовані додатки</string>
|
|
||||||
<string name="excluded">виключено</string>
|
|
||||||
<string name="always_on_disabled">Функція Always-on VPN спробувала запустити тунель, але функція вимкнена в налаштуваннях.</string>
|
|
||||||
<string name="auto">(авто)</string>
|
|
||||||
<string name="all">всі</string>
|
|
||||||
<string name="no_email_detected">Програми для надсилання email не знайдено</string>
|
|
||||||
<string name="open_issue">Повідомити про проблему</string>
|
|
||||||
<string name="read_logs">Переглянути логи</string>
|
|
||||||
<string name="no_browser_detected">Веб браузер не знайдено</string>
|
|
||||||
<string name="pin_created">PIN-код створено успішно</string>
|
|
||||||
<string name="enter_pin">Введіть PIN-код</string>
|
|
||||||
<string name="create_pin">Створити PINhкод</string>
|
|
||||||
<string name="enable_app_lock">Увімкнути блокування додатку</string>
|
|
||||||
<string name="edit_tunnel">Редагувати тунель</string>
|
|
||||||
<string name="auto_on">Відновити авто-тунель</string>
|
|
||||||
<string name="restart_on_ping">Перезапуск при помилці ping (бета)</string>
|
|
||||||
<string name="mobile_data_tunnel">Встановити як тунель для мобільних даних</string>
|
|
||||||
<string name="set_primary_tunnel">Встановити як основний тунель</string>
|
|
||||||
<string name="no_wifi_names_configured">Імена мереж Wi-Fi не налаштовано для цього тунелю</string>
|
|
||||||
<string name="general">Загальне</string>
|
|
||||||
<string name="auto_tun_off">Призупинити авто-тунель</string>
|
|
||||||
<string name="auto_off">Призупинити авто-тунель</string>
|
|
||||||
<string name="auto_tun_on">Відновити авто-тунель</string>
|
|
||||||
<string name="userspace">Користувача</string>
|
|
||||||
<string name="support">Підтримка</string>
|
|
||||||
<string name="settings">Налаштування</string>
|
|
||||||
<string name="use_amnezia">"Використовувати модуль Amnezia режиму користувача "</string>
|
|
||||||
<string name="backend">Модуль</string>
|
|
||||||
<string name="kernel">Модуль ядра</string>
|
|
||||||
<string name="junk_packet_count">Кількість «сміттєвих» пакетів</string>
|
|
||||||
<string name="junk_packet_minimum_size">Мінімальний розмір «сміттєвого» пакету</string>
|
|
||||||
<string name="junk_packet_maximum_size">Максимальний розмір «сміттєвого» пакету</string>
|
|
||||||
<string name="init_packet_junk_size">Початковий розмір «сміттєвого» пакету</string>
|
|
||||||
<string name="response_packet_junk_size">Розмір відповіді «сміттєвого» пакету</string>
|
|
||||||
<string name="init_packet_magic_header">Заголовок пакету ініціалізації</string>
|
|
||||||
<string name="underload_packet_magic_header">Заголовок пакету під навантаженням</string>
|
|
||||||
<string name="response_packet_magic_header">Заголовок пакету відповіді</string>
|
|
||||||
<string name="unsure_how">, якщо не впевнені що робити далі</string>
|
|
||||||
<string name="see_the">Дивіться</string>
|
|
||||||
</resources>
|
|
||||||
@@ -58,6 +58,8 @@
|
|||||||
<string name="always_on_vpn_support">Allow Always-On VPN </string>
|
<string name="always_on_vpn_support">Allow Always-On VPN </string>
|
||||||
<string name="location_services_not_detected">Location Services Not Detected</string>
|
<string name="location_services_not_detected">Location Services Not Detected</string>
|
||||||
<string name="hint_search_packages">Search packages</string>
|
<string name="hint_search_packages">Search packages</string>
|
||||||
|
<string name="clear_icon">Clear Icon</string>
|
||||||
|
<string name="search_icon">Search Icon</string>
|
||||||
<string name="attempt_connection">Attempting connection..</string>
|
<string name="attempt_connection">Attempting connection..</string>
|
||||||
<string name="vpn_starting">VPN starting</string>
|
<string name="vpn_starting">VPN starting</string>
|
||||||
<string name="db_name">wg-tunnel-db</string>
|
<string name="db_name">wg-tunnel-db</string>
|
||||||
@@ -92,7 +94,6 @@
|
|||||||
<string name="error_authorization_failed">Failed to authorize</string>
|
<string name="error_authorization_failed">Failed to authorize</string>
|
||||||
<string name="enabled_app_shortcuts">Enable app shortcuts</string>
|
<string name="enabled_app_shortcuts">Enable app shortcuts</string>
|
||||||
<string name="export_configs">Export configs</string>
|
<string name="export_configs">Export configs</string>
|
||||||
<string name="export_configs_failed">Failed to export configs</string>
|
|
||||||
<string name="location_services_required">Location services required</string>
|
<string name="location_services_required">Location services required</string>
|
||||||
<string name="background_location_required">Background location required</string>
|
<string name="background_location_required">Background location required</string>
|
||||||
<string name="precise_location_required">Precise location required</string>
|
<string name="precise_location_required">Precise location required</string>
|
||||||
@@ -107,6 +108,7 @@
|
|||||||
<string name="discord_description">Join the community</string>
|
<string name="discord_description">Join the community</string>
|
||||||
<string name="email_description">Send me an email</string>
|
<string name="email_description">Send me an email</string>
|
||||||
<string name="support_help_text">If you are experiencing issues, have improvement ideas, or just want to engage, the following resources are available:</string>
|
<string name="support_help_text">If you are experiencing issues, have improvement ideas, or just want to engage, the following resources are available:</string>
|
||||||
|
<string name="kernel">Kernel</string>
|
||||||
<string name="use_kernel">Use kernel module</string>
|
<string name="use_kernel">Use kernel module</string>
|
||||||
<string name="error_ssid_exists">SSID already exists</string>
|
<string name="error_ssid_exists">SSID already exists</string>
|
||||||
<string name="error_root_denied">Root shell denied</string>
|
<string name="error_root_denied">Root shell denied</string>
|
||||||
@@ -138,7 +140,7 @@
|
|||||||
<string name="pin_created">Pin successfully created</string>
|
<string name="pin_created">Pin successfully created</string>
|
||||||
<string name="enter_pin">Enter your pin</string>
|
<string name="enter_pin">Enter your pin</string>
|
||||||
<string name="create_pin">Create pin</string>
|
<string name="create_pin">Create pin</string>
|
||||||
<string name="enable_app_lock">Enable app lock</string>
|
<string name="enable_app_lock">Enabled app lock</string>
|
||||||
<string name="restart_on_ping">Restart on ping fail (beta)</string>
|
<string name="restart_on_ping">Restart on ping fail (beta)</string>
|
||||||
<string name="mobile_data_tunnel">Set as mobile data tunnel</string>
|
<string name="mobile_data_tunnel">Set as mobile data tunnel</string>
|
||||||
<string name="set_primary_tunnel">Set as primary tunnel</string>
|
<string name="set_primary_tunnel">Set as primary tunnel</string>
|
||||||
@@ -156,25 +158,4 @@
|
|||||||
<string name="userspace">Userspace</string>
|
<string name="userspace">Userspace</string>
|
||||||
<string name="settings">Settings</string>
|
<string name="settings">Settings</string>
|
||||||
<string name="support">Support</string>
|
<string name="support">Support</string>
|
||||||
<string name="backend">Backend</string>
|
|
||||||
<string name="kernel">Kernel</string>
|
|
||||||
<string name="use_amnezia">"Use Amnezia userspace "</string>
|
|
||||||
<string name="junk_packet_count">Junk packet count</string>
|
|
||||||
<string name="junk_packet_minimum_size">Junk packet minimum size</string>
|
|
||||||
<string name="junk_packet_maximum_size">Junk packet maximum size</string>
|
|
||||||
<string name="init_packet_junk_size">Init packet junk size</string>
|
|
||||||
<string name="response_packet_junk_size">Response packet junk size</string>
|
|
||||||
<string name="init_packet_magic_header">Init packet magic header</string>
|
|
||||||
<string name="response_packet_magic_header">Response packet magic header</string>
|
|
||||||
<string name="transport_packet_magic_header">Transport packet magic header</string>
|
|
||||||
<string name="underload_packet_magic_header">Underload packet magic header</string>
|
|
||||||
<string name="telegram_url" translatable="false">https://t.me/wgtunnel</string>
|
|
||||||
<string name="unsure_how">if you are unsure how to proceed</string>
|
|
||||||
<string name="see_the">See the</string>
|
|
||||||
<string name="getting_started_url" translatable="false">https://zaneschepke.com/wgtunnel-docs/getting-started.html</string>
|
|
||||||
<string name="getting_started_guide">getting started guide</string>
|
|
||||||
<string name="amnezia" translatable="false">Amnezia</string>
|
|
||||||
<string name="wireguard" translatable="false">WireGuard</string>
|
|
||||||
<string name="error_file_format">Invalid tunnel config format</string>
|
|
||||||
<string name="restart_at_boot">Restart on boot</string>
|
|
||||||
</resources>
|
</resources>
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user