mirror of
https://github.com/wgtunnel/android.git
synced 2026-07-03 14:07:49 +02:00
Compare commits
40 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6f48147b3e | |||
| 16979dbb2b | |||
| 5d8190628d | |||
| 6d597a235d | |||
| 61fd2f01b9 | |||
| 914a641977 | |||
| c97a3afbc4 | |||
| fadc2d1562 | |||
| 106ab76b82 | |||
| 48b3f60b37 | |||
| e7f5cee6dd | |||
| 5e01fd6c85 | |||
| dcbd72c6f6 | |||
| f27aa1452a | |||
| 82c3521c58 | |||
| c6535ffe7e | |||
| be5fe94ce6 | |||
| 1e905c1cfb | |||
| a549c111aa | |||
| 5ab344044e | |||
| 7030f68548 | |||
| ecd51b6fe5 | |||
| 3e4f8e0791 | |||
| e940a0dbc5 | |||
| 8e233475df | |||
| 24789826ad | |||
| 787e0d1a71 | |||
| 914ef53ef0 | |||
| a569974beb | |||
| 87bc89b6f1 | |||
| c343220e96 | |||
| 5447ec73f7 | |||
| a2b8eb5b0b | |||
| e37777e662 | |||
| ee3fcabcf1 | |||
| 2a8895ffbc | |||
| b1fdb5b9b2 | |||
| 1d644748e5 | |||
| 61989b596f | |||
| 7ca0db3a40 |
@@ -0,0 +1,22 @@
|
|||||||
|
# Contributor Code of Conduct
|
||||||
|
|
||||||
|
## Pledge
|
||||||
|
|
||||||
|
We as individuals involved in this project, pledge to participate in this
|
||||||
|
community in a respectful, constructive, and civil manner as we work towards a common goal
|
||||||
|
of delivering free, open source, and value adding software for all.
|
||||||
|
|
||||||
|
## Standard
|
||||||
|
|
||||||
|
The standard for this community is the Golden Rule.
|
||||||
|
|
||||||
|
> “Do unto others as you would have them do unto you.”
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
This Code of Conduct applies to all spaces related to WG Tunnel.
|
||||||
|
|
||||||
|
## Incidents or Concerns
|
||||||
|
|
||||||
|
For any incidents or concerns, reach out to Zane at
|
||||||
|
<support@zaneschepke.com>.
|
||||||
@@ -78,7 +78,7 @@ jobs:
|
|||||||
name: wgtunnel
|
name: wgtunnel
|
||||||
- name: Create Release with Fastlane changelog notes
|
- name: Create Release with Fastlane changelog notes
|
||||||
id: create_release
|
id: create_release
|
||||||
uses: softprops/action-gh-release@v1
|
uses: softprops/action-gh-release@v2
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
with:
|
with:
|
||||||
@@ -89,6 +89,32 @@ jobs:
|
|||||||
draft: false
|
draft: false
|
||||||
prerelease: true
|
prerelease: true
|
||||||
files: ${{ github.workspace }}/${{ steps.apk-path.outputs.path }}
|
files: ${{ github.workspace }}/${{ steps.apk-path.outputs.path }}
|
||||||
|
|
||||||
|
- name: Install apksigner
|
||||||
|
run: |
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install -y apksigner
|
||||||
|
|
||||||
|
- name: Get checksum
|
||||||
|
id: checksum
|
||||||
|
run: echo "checksum=$(apksigner verify -print-certs ${{ steps.apk-path.outputs.path }} | grep -Po "(?<=SHA-256 digest:) .*" | tr -d "[:blank:]")" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
- name: Append checksum
|
||||||
|
id: append_checksum
|
||||||
|
uses: softprops/action-gh-release@v2
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
with:
|
||||||
|
body: |
|
||||||
|
|
||||||
|
SHA256 fingerprint:
|
||||||
|
```${{ steps.checksum.outputs.checksum }}```
|
||||||
|
tag_name: ${{ github.ref_name }}
|
||||||
|
name: ${{ github.ref_name }}
|
||||||
|
draft: false
|
||||||
|
prerelease: true
|
||||||
|
append_body: true
|
||||||
|
|
||||||
- name: Deploy with fastlane
|
- name: Deploy with fastlane
|
||||||
uses: ruby/setup-ruby@v1
|
uses: ruby/setup-ruby@v1
|
||||||
with:
|
with:
|
||||||
|
|||||||
@@ -85,7 +85,7 @@ jobs:
|
|||||||
event-type: fdroid-update
|
event-type: fdroid-update
|
||||||
- name: Create Release with Fastlane changelog notes
|
- name: Create Release with Fastlane changelog notes
|
||||||
id: create_release
|
id: create_release
|
||||||
uses: softprops/action-gh-release@v1
|
uses: softprops/action-gh-release@v2
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
with:
|
with:
|
||||||
@@ -95,6 +95,32 @@ jobs:
|
|||||||
draft: false
|
draft: false
|
||||||
prerelease: false
|
prerelease: false
|
||||||
files: ${{ github.workspace }}/${{ steps.apk-path.outputs.path }}
|
files: ${{ github.workspace }}/${{ steps.apk-path.outputs.path }}
|
||||||
|
|
||||||
|
- name: Install apksigner
|
||||||
|
run: |
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install -y apksigner
|
||||||
|
|
||||||
|
- name: Get checksum
|
||||||
|
id: checksum
|
||||||
|
run: echo "checksum=$(apksigner verify -print-certs ${{ steps.apk-path.outputs.path }} | grep -Po "(?<=SHA-256 digest:) .*" | tr -d "[:blank:]")" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
- name: Append checksum
|
||||||
|
id: append_checksum
|
||||||
|
uses: softprops/action-gh-release@v2
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
with:
|
||||||
|
body: |
|
||||||
|
|
||||||
|
SHA256 fingerprint:
|
||||||
|
```${{ steps.checksum.outputs.checksum }}```
|
||||||
|
tag_name: ${{ github.ref_name }}
|
||||||
|
name: ${{ github.ref_name }}
|
||||||
|
draft: false
|
||||||
|
prerelease: false
|
||||||
|
append_body: true
|
||||||
|
|
||||||
- name: Deploy with fastlane
|
- name: Deploy with fastlane
|
||||||
uses: ruby/setup-ruby@v1
|
uses: ruby/setup-ruby@v1
|
||||||
with:
|
with:
|
||||||
|
|||||||
@@ -4,8 +4,9 @@ WG Tunnel
|
|||||||
|
|
||||||
<div align="center">
|
<div align="center">
|
||||||
|
|
||||||
[](https://opensource.org/licenses/MIT)
|
[](https://discord.gg/rbRRNh6H7V)
|
||||||
[](https://discord.gg/rbRRNh6H7V)
|
[](https://twitter.com/i/communities/1780655267685736818)
|
||||||
|
[](https://t.me/wgtunnel)
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -49,23 +50,40 @@ and on while on different networks. This app was created to offer a free solutio
|
|||||||
## Features
|
## Features
|
||||||
|
|
||||||
* Add tunnels via .conf file, zip, manual entry, or QR code
|
* Add tunnels via .conf file, zip, manual entry, or QR code
|
||||||
* Auto connect to VPN 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
|
||||||
* Always-On VPN support
|
* Always-On VPN support
|
||||||
* Export tunnels to zip
|
* Export tunnels to zip
|
||||||
* Quick tile support for VPN toggling
|
* Quick tile support for tunnel toggling, auto-tunneling
|
||||||
* Static shortcuts support for primary tunnel for automation integration
|
* Static shortcuts support for tunnel toggling, auto-tunneling
|
||||||
* Intent automation support for all tunnels
|
* Intent automation support for all tunnels
|
||||||
* Automatic service restart after reboot
|
* Automatic auto-tunneling service restart after reboot
|
||||||
|
* Automatic tunnel restart after reboot
|
||||||
* Battery preservation measures
|
* Battery preservation measures
|
||||||
|
* Restart tunnel on ping failure (beta)
|
||||||
|
|
||||||
## Docs (WIP)
|
## Docs
|
||||||
|
|
||||||
Basic documentation of the feature and behaviors of this app can be found [here](https://zaneschepke.com/wgtunnel-docs/overview.html).
|
Basic documentation of the feature and behaviors of this app can be
|
||||||
|
found [here](https://zaneschepke.com/wgtunnel-docs/overview.html).
|
||||||
|
|
||||||
The repository for these docs can be found [here](https://github.com/zaneschepke/wgtunnel-docs).
|
The repository for these docs can be found [here](https://github.com/zaneschepke/wgtunnel-docs).
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
|
||||||
|
Any contributions in the form of feedback, issues, code, or translations are welcome and much appreciated!
|
||||||
|
|
||||||
|
Please read the [code of conduct](https://github.com/zaneschepke/wgtunnel?tab=coc-ov-file#contributor-code-of-conduct) before contributing.
|
||||||
|
|
||||||
|
## Translation
|
||||||
|
|
||||||
|
This app is using [Weblate](https://weblate.org) to assist with translations.
|
||||||
|
|
||||||
|
Help translate WG Tunnel into your language at [Hosted Weblate](https://hosted.weblate.org/engage/wg-tunnel/).\
|
||||||
|
[](https://hosted.weblate.org/engage/wg-tunnel/)
|
||||||
|
|
||||||
|
|
||||||
## Building
|
## Building
|
||||||
|
|
||||||
```
|
```
|
||||||
@@ -73,28 +91,8 @@ $ git clone https://github.com/zaneschepke/wgtunnel
|
|||||||
$ cd wgtunnel
|
$ cd wgtunnel
|
||||||
```
|
```
|
||||||
|
|
||||||
Create a personal access token (classic) in GitHuv to be able to pull the wireguard-android github dependencies from GitHub packages
|
|
||||||
as documented [here](https://docs.github.com/en/packages/learn-github-packages/introduction-to-github-packages#authenticating-to-github-packages).
|
|
||||||
|
|
||||||
Alternatively, you can clone [wireguard-android](https://github.com/zaneschepke/wireguard-android) and run the following command to publish the dependency to your local maven repository (requires you have maven installed). This is the ideal approach
|
|
||||||
if you intent to make changes to this lib.
|
|
||||||
|
|
||||||
```
|
|
||||||
$ git clone https://github.com/zaneschepke/wireguard-android
|
|
||||||
$ cd wireguard-android
|
|
||||||
$ brew install maven
|
|
||||||
$ ./gradlew publishToMavenLocal
|
|
||||||
```
|
|
||||||
|
|
||||||
The [wireguard-android](https://github.com/zaneschepke/wireguard-android) dependency is a fork of the official [wireguard-android](https://github.com/WireGuard/wireguard-android) library.
|
|
||||||
|
|
||||||
Add the following lines to local.properties file:
|
|
||||||
```
|
|
||||||
GH_USER=<your github username>
|
|
||||||
GH_TOKEN=<the personal access token with read package permission you just created>
|
|
||||||
```
|
|
||||||
|
|
||||||
And then build the app:
|
And then build the app:
|
||||||
|
|
||||||
```
|
```
|
||||||
$ ./gradlew assembleDebug
|
$ ./gradlew assembleDebug
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
# Security Policy
|
||||||
|
|
||||||
|
## Reporting a Vulnerability
|
||||||
|
|
||||||
|
Please report security issues to `support@zaneschepke.com`
|
||||||
@@ -12,6 +12,10 @@ android {
|
|||||||
namespace = Constants.APP_ID
|
namespace = Constants.APP_ID
|
||||||
compileSdk = Constants.TARGET_SDK
|
compileSdk = Constants.TARGET_SDK
|
||||||
|
|
||||||
|
androidResources {
|
||||||
|
generateLocaleConfig = true
|
||||||
|
}
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId = Constants.APP_ID
|
applicationId = Constants.APP_ID
|
||||||
minSdk = Constants.MIN_SDK
|
minSdk = Constants.MIN_SDK
|
||||||
@@ -25,8 +29,6 @@ android {
|
|||||||
getByName("debug").assets.srcDirs(files("$projectDir/schemas")) // Room
|
getByName("debug").assets.srcDirs(files("$projectDir/schemas")) // Room
|
||||||
}
|
}
|
||||||
|
|
||||||
resourceConfigurations.addAll(listOf("en"))
|
|
||||||
|
|
||||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||||
vectorDrawables { useSupportLibrary = true }
|
vectorDrawables { useSupportLibrary = true }
|
||||||
}
|
}
|
||||||
@@ -158,8 +160,8 @@ 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)
|
||||||
coreLibraryDesugaring(libs.desugar.jdk.libs)
|
coreLibraryDesugaring(libs.desugar.jdk.libs)
|
||||||
|
|
||||||
// logging
|
// logging
|
||||||
|
|||||||
@@ -0,0 +1,176 @@
|
|||||||
|
{
|
||||||
|
"formatVersion": 1,
|
||||||
|
"database": {
|
||||||
|
"version": 7,
|
||||||
|
"identityHash": "e65e4e7cf01f50fb03196d47b54288b1",
|
||||||
|
"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)",
|
||||||
|
"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"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"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)",
|
||||||
|
"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"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"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, 'e65e4e7cf01f50fb03196d47b54288b1')"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -23,13 +23,13 @@ class MigrationTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
@Throws(IOException::class)
|
@Throws(IOException::class)
|
||||||
fun migrate4To5() {
|
fun migrate6To7() {
|
||||||
helper.createDatabase(dbName, 4).apply {
|
helper.createDatabase(dbName, 6).apply {
|
||||||
// Database has schema version 1. Insert some data using SQL queries.
|
// Database has schema version 1. Insert some data using SQL queries.
|
||||||
// You can't use DAO classes because they expect the latest schema.
|
// You can't use DAO classes because they expect the latest schema.
|
||||||
execSQL(Queries.createDefaultSettings())
|
execSQL(Queries.createDefaultSettings())
|
||||||
execSQL(
|
execSQL(
|
||||||
"INSERT INTO TunnelConfig (name, wg_quick)" + " VALUES ('hello', 'hello')",
|
Queries.createTunnelConfig(),
|
||||||
)
|
)
|
||||||
// Prepare for the next version.
|
// Prepare for the next version.
|
||||||
close()
|
close()
|
||||||
@@ -37,7 +37,7 @@ class MigrationTest {
|
|||||||
|
|
||||||
// Re-open the database with version 2 and provide
|
// Re-open the database with version 2 and provide
|
||||||
// MIGRATION_1_2 as the migration process.
|
// MIGRATION_1_2 as the migration process.
|
||||||
helper.runMigrationsAndValidate(dbName, 5, true)
|
helper.runMigrationsAndValidate(dbName, 7, true)
|
||||||
// MigrationTestHelper automatically verifies the schema changes,
|
// MigrationTestHelper automatically verifies the schema changes,
|
||||||
// but you need to validate that the data was migrated properly.
|
// but you need to validate that the data was migrated properly.
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:tools="http://schemas.android.com/tools">
|
xmlns:tools="http://schemas.android.com/tools">
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
<uses-permission
|
<uses-permission
|
||||||
android:name="android.permission.READ_EXTERNAL_STORAGE"
|
android:name="android.permission.READ_EXTERNAL_STORAGE"
|
||||||
@@ -51,7 +52,7 @@
|
|||||||
<application
|
<application
|
||||||
android:name=".WireGuardAutoTunnel"
|
android:name=".WireGuardAutoTunnel"
|
||||||
android:allowBackup="true"
|
android:allowBackup="true"
|
||||||
android:banner="@mipmap/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"
|
||||||
android:fullBackupContent="@xml/backup_rules"
|
android:fullBackupContent="@xml/backup_rules"
|
||||||
@@ -101,7 +102,24 @@
|
|||||||
android:name=".service.tile.TunnelControlTile"
|
android:name=".service.tile.TunnelControlTile"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
android:icon="@drawable/ic_launcher"
|
android:icon="@drawable/ic_launcher"
|
||||||
android:label="WG Tunnel"
|
android:label="Tunnel control"
|
||||||
|
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE">
|
||||||
|
<meta-data
|
||||||
|
android:name="android.service.quicksettings.ACTIVE_TILE"
|
||||||
|
android:value="true" />
|
||||||
|
<meta-data
|
||||||
|
android:name="android.service.quicksettings.TOGGLEABLE_TILE"
|
||||||
|
android:value="true" />
|
||||||
|
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.service.quicksettings.action.QS_TILE" />
|
||||||
|
</intent-filter>
|
||||||
|
</service>
|
||||||
|
<service
|
||||||
|
android:name=".service.tile.AutoTunnelControlTile"
|
||||||
|
android:exported="true"
|
||||||
|
android:icon="@drawable/ic_launcher"
|
||||||
|
android:label="Auto-tunnel"
|
||||||
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE">
|
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE">
|
||||||
<meta-data
|
<meta-data
|
||||||
android:name="android.service.quicksettings.ACTIVE_TILE"
|
android:name="android.service.quicksettings.ACTIVE_TILE"
|
||||||
@@ -144,6 +162,7 @@
|
|||||||
android:exported="false">
|
android:exported="false">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<category android:name="android.intent.category.DEFAULT" />
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
|
|
||||||
<action android:name="android.intent.action.BOOT_COMPLETED" />
|
<action android:name="android.intent.action.BOOT_COMPLETED" />
|
||||||
<action android:name="android.intent.action.ACTION_BOOT_COMPLETED" />
|
<action android:name="android.intent.action.ACTION_BOOT_COMPLETED" />
|
||||||
<action android:name="android.intent.action.QUICKBOOT_POWERON" />
|
<action android:name="android.intent.action.QUICKBOOT_POWERON" />
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import android.content.ComponentName
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
import android.service.quicksettings.TileService
|
import android.service.quicksettings.TileService
|
||||||
|
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
|
||||||
@@ -19,6 +20,7 @@ class WireGuardAutoTunnel : Application() {
|
|||||||
if (BuildConfig.DEBUG) Timber.plant(Timber.DebugTree()) else Timber.plant(ReleaseTree())
|
if (BuildConfig.DEBUG) Timber.plant(Timber.DebugTree()) else Timber.plant(ReleaseTree())
|
||||||
PinManager.initialize(this)
|
PinManager.initialize(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
lateinit var instance: WireGuardAutoTunnel
|
lateinit var instance: WireGuardAutoTunnel
|
||||||
private set
|
private set
|
||||||
@@ -27,11 +29,18 @@ class WireGuardAutoTunnel : Application() {
|
|||||||
return instance.packageManager.hasSystemFeature(PackageManager.FEATURE_LEANBACK)
|
return instance.packageManager.hasSystemFeature(PackageManager.FEATURE_LEANBACK)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun requestTileServiceStateUpdate(context : Context) {
|
fun requestTunnelTileServiceStateUpdate(context: Context) {
|
||||||
TileService.requestListeningState(
|
TileService.requestListeningState(
|
||||||
context,
|
context,
|
||||||
ComponentName(instance, TunnelControlTile::class.java),
|
ComponentName(instance, TunnelControlTile::class.java),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun requestAutoTunnelTileServiceUpdate(context: Context) {
|
||||||
|
TileService.requestListeningState(
|
||||||
|
context,
|
||||||
|
ComponentName(instance, AutoTunnelControlTile::class.java),
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,14 +2,16 @@ package com.zaneschepke.wireguardautotunnel.data
|
|||||||
|
|
||||||
import androidx.room.AutoMigration
|
import androidx.room.AutoMigration
|
||||||
import androidx.room.Database
|
import androidx.room.Database
|
||||||
|
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 com.zaneschepke.wireguardautotunnel.data.model.Settings
|
import com.zaneschepke.wireguardautotunnel.data.model.Settings
|
||||||
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
|
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
|
||||||
|
|
||||||
@Database(
|
@Database(
|
||||||
entities = [Settings::class, TunnelConfig::class],
|
entities = [Settings::class, TunnelConfig::class],
|
||||||
version = 6,
|
version = 7,
|
||||||
autoMigrations =
|
autoMigrations =
|
||||||
[
|
[
|
||||||
AutoMigration(from = 1, to = 2),
|
AutoMigration(from = 1, to = 2),
|
||||||
@@ -26,6 +28,11 @@ import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
|
|||||||
from = 5,
|
from = 5,
|
||||||
to = 6,
|
to = 6,
|
||||||
),
|
),
|
||||||
|
AutoMigration(
|
||||||
|
from = 6,
|
||||||
|
to = 7,
|
||||||
|
spec = RemoveLegacySettingColumnsMigration::class,
|
||||||
|
),
|
||||||
],
|
],
|
||||||
exportSchema = true,
|
exportSchema = true,
|
||||||
)
|
)
|
||||||
@@ -35,3 +42,13 @@ abstract class AppDatabase : RoomDatabase() {
|
|||||||
|
|
||||||
abstract fun tunnelConfigDoa(): TunnelConfigDao
|
abstract fun tunnelConfigDoa(): TunnelConfigDao
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@DeleteColumn(
|
||||||
|
tableName = "Settings",
|
||||||
|
columnName = "default_tunnel",
|
||||||
|
)
|
||||||
|
@DeleteColumn(
|
||||||
|
tableName = "Settings",
|
||||||
|
columnName = "is_battery_saver_enabled",
|
||||||
|
)
|
||||||
|
class RemoveLegacySettingColumnsMigration : AutoMigrationSpec
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ class DatabaseCallback : RoomDatabase.Callback() {
|
|||||||
execSQL(Queries.createDefaultSettings())
|
execSQL(Queries.createDefaultSettings())
|
||||||
Timber.i("Bootstrapping settings data")
|
Timber.i("Bootstrapping settings data")
|
||||||
setTransactionSuccessful()
|
setTransactionSuccessful()
|
||||||
} catch (e : Exception) {
|
} catch (e: Exception) {
|
||||||
Timber.e(e)
|
Timber.e(e)
|
||||||
} finally {
|
} finally {
|
||||||
endTransaction()
|
endTransaction()
|
||||||
|
|||||||
+1
-1
@@ -12,7 +12,7 @@ class DatabaseListConverters {
|
|||||||
|
|
||||||
@TypeConverter
|
@TypeConverter
|
||||||
fun stringToList(value: String): MutableList<String> {
|
fun stringToList(value: String): MutableList<String> {
|
||||||
if (value.isEmpty()) return mutableListOf()
|
if (value.isBlank() || value.isEmpty()) return mutableListOf()
|
||||||
return try {
|
return try {
|
||||||
Json.decodeFromString<MutableList<String>>(value)
|
Json.decodeFromString<MutableList<String>>(value)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
|||||||
@@ -1,16 +1,14 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.data
|
package com.zaneschepke.wireguardautotunnel.data
|
||||||
|
|
||||||
object Queries {
|
object Queries {
|
||||||
fun createDefaultSettings() : String {
|
fun createDefaultSettings(): String {
|
||||||
return """
|
return """
|
||||||
INSERT INTO Settings (is_tunnel_enabled,
|
INSERT INTO Settings (is_tunnel_enabled,
|
||||||
is_tunnel_on_mobile_data_enabled,
|
is_tunnel_on_mobile_data_enabled,
|
||||||
trusted_network_ssids,
|
trusted_network_ssids,
|
||||||
default_tunnel,
|
|
||||||
is_always_on_vpn_enabled,
|
is_always_on_vpn_enabled,
|
||||||
is_tunnel_on_ethernet_enabled,
|
is_tunnel_on_ethernet_enabled,
|
||||||
is_shortcuts_enabled,
|
is_shortcuts_enabled,
|
||||||
is_battery_saver_enabled,
|
|
||||||
is_tunnel_on_wifi_enabled,
|
is_tunnel_on_wifi_enabled,
|
||||||
is_kernel_enabled,
|
is_kernel_enabled,
|
||||||
is_restore_on_boot_enabled,
|
is_restore_on_boot_enabled,
|
||||||
@@ -19,8 +17,6 @@ object Queries {
|
|||||||
('false',
|
('false',
|
||||||
'false',
|
'false',
|
||||||
'sampleSSID1,sampleSSID2',
|
'sampleSSID1,sampleSSID2',
|
||||||
NULL,
|
|
||||||
'false',
|
|
||||||
'false',
|
'false',
|
||||||
'false',
|
'false',
|
||||||
'false',
|
'false',
|
||||||
@@ -30,4 +26,10 @@ object Queries {
|
|||||||
'false')
|
'false')
|
||||||
""".trimIndent()
|
""".trimIndent()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun createTunnelConfig(): String {
|
||||||
|
return """
|
||||||
|
INSERT INTO TunnelConfig (name, wg_quick) VALUES ('test', 'test')
|
||||||
|
""".trimIndent()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,19 +10,27 @@ import kotlinx.coroutines.flow.Flow
|
|||||||
|
|
||||||
@Dao
|
@Dao
|
||||||
interface SettingsDao {
|
interface SettingsDao {
|
||||||
@Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun save(t: Settings)
|
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||||
|
suspend fun save(t: Settings)
|
||||||
|
|
||||||
@Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun saveAll(t: List<Settings>)
|
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||||
|
suspend fun saveAll(t: List<Settings>)
|
||||||
|
|
||||||
@Query("SELECT * FROM settings WHERE id=:id") suspend fun getById(id: Long): Settings?
|
@Query("SELECT * FROM settings WHERE id=:id")
|
||||||
|
suspend fun getById(id: Long): Settings?
|
||||||
|
|
||||||
@Query("SELECT * FROM settings") suspend fun getAll(): List<Settings>
|
@Query("SELECT * FROM settings")
|
||||||
|
suspend fun getAll(): List<Settings>
|
||||||
|
|
||||||
@Query("SELECT * FROM settings LIMIT 1") fun getSettingsFlow(): Flow<Settings>
|
@Query("SELECT * FROM settings LIMIT 1")
|
||||||
|
fun getSettingsFlow(): Flow<Settings>
|
||||||
|
|
||||||
@Query("SELECT * FROM settings") fun getAllFlow(): Flow<MutableList<Settings>>
|
@Query("SELECT * FROM settings")
|
||||||
|
fun getAllFlow(): Flow<MutableList<Settings>>
|
||||||
|
|
||||||
@Delete suspend fun delete(t: Settings)
|
@Delete
|
||||||
|
suspend fun delete(t: Settings)
|
||||||
|
|
||||||
@Query("SELECT COUNT('id') FROM settings") suspend fun count(): Long
|
@Query("SELECT COUNT('id') FROM settings")
|
||||||
|
suspend fun count(): Long
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,21 +6,44 @@ import androidx.room.Insert
|
|||||||
import androidx.room.OnConflictStrategy
|
import androidx.room.OnConflictStrategy
|
||||||
import androidx.room.Query
|
import androidx.room.Query
|
||||||
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
|
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
|
||||||
|
import com.zaneschepke.wireguardautotunnel.util.TunnelConfigs
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
||||||
@Dao
|
@Dao
|
||||||
interface TunnelConfigDao {
|
interface TunnelConfigDao {
|
||||||
@Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun save(t: TunnelConfig)
|
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||||
|
suspend fun save(t: TunnelConfig)
|
||||||
|
|
||||||
@Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun saveAll(t: List<TunnelConfig>)
|
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||||
|
suspend fun saveAll(t: TunnelConfigs)
|
||||||
|
|
||||||
@Query("SELECT * FROM TunnelConfig WHERE id=:id") suspend fun getById(id: Long): TunnelConfig?
|
@Query("SELECT * FROM TunnelConfig WHERE id=:id")
|
||||||
|
suspend fun getById(id: Long): TunnelConfig?
|
||||||
|
|
||||||
@Query("SELECT * FROM TunnelConfig") suspend fun getAll(): List<TunnelConfig>
|
@Query("SELECT * FROM TunnelConfig")
|
||||||
|
suspend fun getAll(): TunnelConfigs
|
||||||
|
|
||||||
@Delete suspend fun delete(t: TunnelConfig)
|
@Delete
|
||||||
|
suspend fun delete(t: TunnelConfig)
|
||||||
|
|
||||||
@Query("SELECT COUNT('id') FROM TunnelConfig") suspend fun count(): Long
|
@Query("SELECT COUNT('id') FROM TunnelConfig")
|
||||||
|
suspend fun count(): Long
|
||||||
|
|
||||||
@Query("SELECT * FROM tunnelconfig") fun getAllFlow(): Flow<MutableList<TunnelConfig>>
|
@Query("SELECT * FROM TunnelConfig WHERE tunnel_networks LIKE '%' || :name || '%'")
|
||||||
|
suspend fun findByTunnelNetworkName(name: String): TunnelConfigs
|
||||||
|
|
||||||
|
@Query("UPDATE TunnelConfig SET is_primary_tunnel = 0 WHERE is_primary_tunnel =1")
|
||||||
|
fun resetPrimaryTunnel()
|
||||||
|
|
||||||
|
@Query("UPDATE TunnelConfig SET is_mobile_data_tunnel = 0 WHERE is_mobile_data_tunnel =1")
|
||||||
|
fun resetMobileDataTunnel()
|
||||||
|
|
||||||
|
@Query("SELECT * FROM TUNNELCONFIG WHERE is_primary_tunnel=1")
|
||||||
|
suspend fun findByPrimary(): TunnelConfigs
|
||||||
|
|
||||||
|
@Query("SELECT * FROM TUNNELCONFIG WHERE is_mobile_data_tunnel=1")
|
||||||
|
suspend fun findByMobileDataTunnel(): TunnelConfigs
|
||||||
|
|
||||||
|
@Query("SELECT * FROM tunnelconfig")
|
||||||
|
fun getAllFlow(): Flow<MutableList<TunnelConfig>>
|
||||||
}
|
}
|
||||||
|
|||||||
+31
-4
@@ -4,16 +4,24 @@ import android.content.Context
|
|||||||
import androidx.datastore.preferences.core.Preferences
|
import androidx.datastore.preferences.core.Preferences
|
||||||
import androidx.datastore.preferences.core.booleanPreferencesKey
|
import androidx.datastore.preferences.core.booleanPreferencesKey
|
||||||
import androidx.datastore.preferences.core.edit
|
import androidx.datastore.preferences.core.edit
|
||||||
|
import androidx.datastore.preferences.core.intPreferencesKey
|
||||||
|
import androidx.datastore.preferences.core.stringPreferencesKey
|
||||||
import androidx.datastore.preferences.preferencesDataStore
|
import androidx.datastore.preferences.preferencesDataStore
|
||||||
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 timber.log.Timber
|
||||||
|
import java.io.IOException
|
||||||
|
|
||||||
class DataStoreManager(private val context: Context) {
|
class DataStoreManager(private val context: Context) {
|
||||||
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")
|
||||||
|
val TUNNEL_RUNNING_FROM_MANUAL_START =
|
||||||
|
booleanPreferencesKey("TUNNEL_RUNNING_FROM_MANUAL_START")
|
||||||
|
val ACTIVE_TUNNEL = intPreferencesKey("ACTIVE_TUNNEL")
|
||||||
|
val CURRENT_SSID = stringPreferencesKey("CURRENT_SSID")
|
||||||
}
|
}
|
||||||
|
|
||||||
// preferences
|
// preferences
|
||||||
@@ -24,19 +32,38 @@ class DataStoreManager(private val context: Context) {
|
|||||||
)
|
)
|
||||||
|
|
||||||
suspend fun init() {
|
suspend fun init() {
|
||||||
|
try {
|
||||||
context.dataStore.data.first()
|
context.dataStore.data.first()
|
||||||
|
} catch (e: IOException) {
|
||||||
|
Timber.e(e)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun <T> saveToDataStore(key: Preferences.Key<T>, value: T) =
|
suspend fun <T> saveToDataStore(key: Preferences.Key<T>, value: T) {
|
||||||
|
try {
|
||||||
context.dataStore.edit { it[key] = value }
|
context.dataStore.edit { it[key] = value }
|
||||||
|
} catch (e: IOException) {
|
||||||
|
Timber.e(e)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Timber.e(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
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>) =
|
suspend fun <T> getFromStore(key: Preferences.Key<T>): T? {
|
||||||
context.dataStore.data.map{ it[key] }.first()
|
return try {
|
||||||
|
context.dataStore.data.map { it[key] }.first()
|
||||||
|
} catch (e: IOException) {
|
||||||
|
Timber.e(e)
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
fun <T> getFromStoreBlocking(key: Preferences.Key<T>) = runBlocking {
|
fun <T> getFromStoreBlocking(key: Preferences.Key<T>) = runBlocking {
|
||||||
context.dataStore.data.map{ it[key] }.first()
|
context.dataStore.data.map { it[key] }.first()
|
||||||
}
|
}
|
||||||
|
|
||||||
val preferencesFlow: Flow<Preferences?> = context.dataStore.data
|
val preferencesFlow: Flow<Preferences?> = context.dataStore.data
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,62 +7,47 @@ import androidx.room.PrimaryKey
|
|||||||
@Entity
|
@Entity
|
||||||
data class Settings(
|
data class Settings(
|
||||||
@PrimaryKey(autoGenerate = true) val id: Int = 0,
|
@PrimaryKey(autoGenerate = true) val id: Int = 0,
|
||||||
@ColumnInfo(name = "is_tunnel_enabled") var isAutoTunnelEnabled: Boolean = false,
|
@ColumnInfo(name = "is_tunnel_enabled") val isAutoTunnelEnabled: Boolean = false,
|
||||||
@ColumnInfo(name = "is_tunnel_on_mobile_data_enabled")
|
@ColumnInfo(name = "is_tunnel_on_mobile_data_enabled")
|
||||||
var isTunnelOnMobileDataEnabled: Boolean = false,
|
val isTunnelOnMobileDataEnabled: Boolean = false,
|
||||||
@ColumnInfo(name = "trusted_network_ssids")
|
@ColumnInfo(name = "trusted_network_ssids")
|
||||||
var trustedNetworkSSIDs: MutableList<String> = mutableListOf(),
|
val trustedNetworkSSIDs: MutableList<String> = mutableListOf(),
|
||||||
@ColumnInfo(name = "default_tunnel") var defaultTunnel: String? = null,
|
@ColumnInfo(name = "is_always_on_vpn_enabled") val isAlwaysOnVpnEnabled: Boolean = false,
|
||||||
@ColumnInfo(name = "is_always_on_vpn_enabled") var isAlwaysOnVpnEnabled: Boolean = false,
|
|
||||||
@ColumnInfo(name = "is_tunnel_on_ethernet_enabled")
|
@ColumnInfo(name = "is_tunnel_on_ethernet_enabled")
|
||||||
var isTunnelOnEthernetEnabled: Boolean = false,
|
val isTunnelOnEthernetEnabled: Boolean = false,
|
||||||
@ColumnInfo(
|
@ColumnInfo(
|
||||||
name = "is_shortcuts_enabled",
|
name = "is_shortcuts_enabled",
|
||||||
defaultValue = "false",
|
defaultValue = "false",
|
||||||
)
|
)
|
||||||
var isShortcutsEnabled: Boolean = false,
|
val isShortcutsEnabled: Boolean = false,
|
||||||
@ColumnInfo(
|
|
||||||
name = "is_battery_saver_enabled",
|
|
||||||
defaultValue = "false",
|
|
||||||
)
|
|
||||||
var isBatterySaverEnabled: Boolean = false,
|
|
||||||
@ColumnInfo(
|
@ColumnInfo(
|
||||||
name = "is_tunnel_on_wifi_enabled",
|
name = "is_tunnel_on_wifi_enabled",
|
||||||
defaultValue = "false",
|
defaultValue = "false",
|
||||||
)
|
)
|
||||||
var isTunnelOnWifiEnabled: Boolean = false,
|
val isTunnelOnWifiEnabled: Boolean = false,
|
||||||
@ColumnInfo(
|
@ColumnInfo(
|
||||||
name = "is_kernel_enabled",
|
name = "is_kernel_enabled",
|
||||||
defaultValue = "false",
|
defaultValue = "false",
|
||||||
)
|
)
|
||||||
var isKernelEnabled: Boolean = false,
|
val isKernelEnabled: Boolean = false,
|
||||||
@ColumnInfo(
|
@ColumnInfo(
|
||||||
name = "is_restore_on_boot_enabled",
|
name = "is_restore_on_boot_enabled",
|
||||||
defaultValue = "false",
|
defaultValue = "false",
|
||||||
)
|
)
|
||||||
var isRestoreOnBootEnabled: Boolean = false,
|
val isRestoreOnBootEnabled: Boolean = false,
|
||||||
@ColumnInfo(
|
@ColumnInfo(
|
||||||
name = "is_multi_tunnel_enabled",
|
name = "is_multi_tunnel_enabled",
|
||||||
defaultValue = "false",
|
defaultValue = "false",
|
||||||
)
|
)
|
||||||
var isMultiTunnelEnabled: Boolean = false,
|
val isMultiTunnelEnabled: Boolean = false,
|
||||||
@ColumnInfo(
|
@ColumnInfo(
|
||||||
name = "is_auto_tunnel_paused",
|
name = "is_auto_tunnel_paused",
|
||||||
defaultValue = "false",
|
defaultValue = "false",
|
||||||
)
|
)
|
||||||
var isAutoTunnelPaused: Boolean = false,
|
val isAutoTunnelPaused: Boolean = false,
|
||||||
@ColumnInfo(
|
@ColumnInfo(
|
||||||
name = "is_ping_enabled",
|
name = "is_ping_enabled",
|
||||||
defaultValue = "false",
|
defaultValue = "false",
|
||||||
)
|
)
|
||||||
var isPingEnabled: Boolean = false,
|
val isPingEnabled: Boolean = false,
|
||||||
) {
|
)
|
||||||
fun isTunnelConfigDefault(tunnelConfig: TunnelConfig): Boolean {
|
|
||||||
return if (defaultTunnel != null) {
|
|
||||||
val defaultConfig = TunnelConfig.from(defaultTunnel!!)
|
|
||||||
(tunnelConfig.id == defaultConfig.id)
|
|
||||||
} else {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -4,27 +4,31 @@ 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 kotlinx.serialization.Serializable
|
|
||||||
import kotlinx.serialization.json.Json
|
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
|
|
||||||
@Entity(indices = [Index(value = ["name"], unique = true)])
|
@Entity(indices = [Index(value = ["name"], unique = true)])
|
||||||
@Serializable
|
|
||||||
data class TunnelConfig(
|
data class TunnelConfig(
|
||||||
@PrimaryKey(autoGenerate = true) val id: Int = 0,
|
@PrimaryKey(autoGenerate = true) val id: Int = 0,
|
||||||
@ColumnInfo(name = "name") var name: String,
|
@ColumnInfo(name = "name") val name: String,
|
||||||
@ColumnInfo(name = "wg_quick") var wgQuick: String
|
@ColumnInfo(name = "wg_quick") val wgQuick: String,
|
||||||
|
@ColumnInfo(
|
||||||
|
name = "tunnel_networks",
|
||||||
|
defaultValue = "",
|
||||||
|
)
|
||||||
|
val tunnelNetworks: MutableList<String> = mutableListOf(),
|
||||||
|
@ColumnInfo(
|
||||||
|
name = "is_mobile_data_tunnel",
|
||||||
|
defaultValue = "false",
|
||||||
|
)
|
||||||
|
val isMobileDataTunnel: Boolean = false,
|
||||||
|
@ColumnInfo(
|
||||||
|
name = "is_primary_tunnel",
|
||||||
|
defaultValue = "false",
|
||||||
|
)
|
||||||
|
val isPrimaryTunnel: Boolean = false,
|
||||||
) {
|
) {
|
||||||
override fun toString(): String {
|
|
||||||
return Json.encodeToString(serializer(), this)
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun from(string: String): TunnelConfig {
|
|
||||||
return Json.decodeFromString<TunnelConfig>(string)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun configFromQuick(wgQuick: String): Config {
|
fun configFromQuick(wgQuick: String): Config {
|
||||||
val inputStream: InputStream = wgQuick.byteInputStream()
|
val inputStream: InputStream = wgQuick.byteInputStream()
|
||||||
val reader = inputStream.bufferedReader(Charsets.UTF_8)
|
val reader = inputStream.bufferedReader(Charsets.UTF_8)
|
||||||
|
|||||||
+14
@@ -0,0 +1,14 @@
|
|||||||
|
package com.zaneschepke.wireguardautotunnel.data.repository
|
||||||
|
|
||||||
|
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
|
||||||
|
|
||||||
|
interface AppDataRepository {
|
||||||
|
suspend fun getPrimaryOrFirstTunnel(): TunnelConfig?
|
||||||
|
suspend fun getStartTunnelConfig(): TunnelConfig?
|
||||||
|
|
||||||
|
suspend fun toggleWatcherServicePause()
|
||||||
|
|
||||||
|
val settings: SettingsRepository
|
||||||
|
val tunnels: TunnelConfigRepository
|
||||||
|
val appState: AppStateRepository
|
||||||
|
}
|
||||||
+34
@@ -0,0 +1,34 @@
|
|||||||
|
package com.zaneschepke.wireguardautotunnel.data.repository
|
||||||
|
|
||||||
|
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
class AppDataRoomRepository @Inject constructor(
|
||||||
|
override val settings: SettingsRepository,
|
||||||
|
override val tunnels: TunnelConfigRepository,
|
||||||
|
override val appState: AppStateRepository
|
||||||
|
) : AppDataRepository {
|
||||||
|
override suspend fun getPrimaryOrFirstTunnel(): TunnelConfig? {
|
||||||
|
return tunnels.findPrimary().firstOrNull() ?: tunnels.getAll().firstOrNull()
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun getStartTunnelConfig(): TunnelConfig? {
|
||||||
|
return if (appState.isTunnelRunningFromManualStart()) {
|
||||||
|
appState.getActiveTunnelId()?.let {
|
||||||
|
tunnels.getById(it)
|
||||||
|
}
|
||||||
|
} else null
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun toggleWatcherServicePause() {
|
||||||
|
val settings = settings.getSettings()
|
||||||
|
if (settings.isAutoTunnelEnabled) {
|
||||||
|
val pauseAutoTunnel = !settings.isAutoTunnelPaused
|
||||||
|
this.settings.save(
|
||||||
|
settings.copy(
|
||||||
|
isAutoTunnelPaused = pauseAutoTunnel,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+26
@@ -0,0 +1,26 @@
|
|||||||
|
package com.zaneschepke.wireguardautotunnel.data.repository
|
||||||
|
|
||||||
|
import com.zaneschepke.wireguardautotunnel.data.model.GeneralState
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
||||||
|
interface AppStateRepository {
|
||||||
|
suspend fun isLocationDisclosureShown(): Boolean
|
||||||
|
suspend fun setLocationDisclosureShown(shown: Boolean)
|
||||||
|
|
||||||
|
suspend fun isBatteryOptimizationDisableShown(): Boolean
|
||||||
|
suspend fun setBatteryOptimizationDisableShown(shown: Boolean)
|
||||||
|
|
||||||
|
suspend fun isTunnelRunningFromManualStart(): Boolean
|
||||||
|
suspend fun setTunnelRunningFromManualStart(id: Int)
|
||||||
|
|
||||||
|
suspend fun setManualStop()
|
||||||
|
|
||||||
|
suspend fun getActiveTunnelId(): Int?
|
||||||
|
|
||||||
|
suspend fun getCurrentSsid(): String?
|
||||||
|
|
||||||
|
suspend fun setCurrentSsid(ssid: String)
|
||||||
|
|
||||||
|
val generalStateFlow: Flow<GeneralState>
|
||||||
|
|
||||||
|
}
|
||||||
+81
@@ -0,0 +1,81 @@
|
|||||||
|
package com.zaneschepke.wireguardautotunnel.data.repository
|
||||||
|
|
||||||
|
import com.zaneschepke.wireguardautotunnel.data.datastore.DataStoreManager
|
||||||
|
import com.zaneschepke.wireguardautotunnel.data.model.GeneralState
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
|
import timber.log.Timber
|
||||||
|
|
||||||
|
class DataStoreAppStateRepository(private val dataStoreManager: DataStoreManager) :
|
||||||
|
AppStateRepository {
|
||||||
|
override suspend fun isLocationDisclosureShown(): Boolean {
|
||||||
|
return dataStoreManager.getFromStore(DataStoreManager.LOCATION_DISCLOSURE_SHOWN)
|
||||||
|
?: GeneralState.LOCATION_DISCLOSURE_SHOWN_DEFAULT
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun setLocationDisclosureShown(shown: Boolean) {
|
||||||
|
dataStoreManager.saveToDataStore(DataStoreManager.LOCATION_DISCLOSURE_SHOWN, shown)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun isBatteryOptimizationDisableShown(): Boolean {
|
||||||
|
return dataStoreManager.getFromStore(DataStoreManager.BATTERY_OPTIMIZE_DISABLE_SHOWN)
|
||||||
|
?: GeneralState.BATTERY_OPTIMIZATION_DISABLE_SHOWN_DEFAULT
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun setBatteryOptimizationDisableShown(shown: Boolean) {
|
||||||
|
dataStoreManager.saveToDataStore(DataStoreManager.BATTERY_OPTIMIZE_DISABLE_SHOWN, shown)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun isTunnelRunningFromManualStart(): Boolean {
|
||||||
|
return dataStoreManager.getFromStore(DataStoreManager.TUNNEL_RUNNING_FROM_MANUAL_START)
|
||||||
|
?: GeneralState.TUNNELING_RUNNING_FROM_MANUAL_START_DEFAULT
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun setTunnelRunningFromManualStart(id: Int) {
|
||||||
|
setTunnelRunningFromManualStart(true)
|
||||||
|
setActiveTunnelId(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun setManualStop() {
|
||||||
|
setTunnelRunningFromManualStart(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun setTunnelRunningFromManualStart(running: Boolean) {
|
||||||
|
dataStoreManager.saveToDataStore(DataStoreManager.TUNNEL_RUNNING_FROM_MANUAL_START, running)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun getActiveTunnelId(): Int? {
|
||||||
|
return dataStoreManager.getFromStore(DataStoreManager.ACTIVE_TUNNEL)
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun setActiveTunnelId(id: Int) {
|
||||||
|
dataStoreManager.saveToDataStore(DataStoreManager.ACTIVE_TUNNEL, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun getCurrentSsid(): String? {
|
||||||
|
return dataStoreManager.getFromStore(DataStoreManager.CURRENT_SSID)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun setCurrentSsid(ssid: String) {
|
||||||
|
dataStoreManager.saveToDataStore(DataStoreManager.CURRENT_SSID, ssid)
|
||||||
|
}
|
||||||
|
|
||||||
|
override val generalStateFlow: Flow<GeneralState> =
|
||||||
|
dataStoreManager.preferencesFlow.map { prefs ->
|
||||||
|
prefs?.let { pref ->
|
||||||
|
try {
|
||||||
|
GeneralState(
|
||||||
|
locationDisclosureShown = pref[DataStoreManager.LOCATION_DISCLOSURE_SHOWN]
|
||||||
|
?: GeneralState.LOCATION_DISCLOSURE_SHOWN_DEFAULT,
|
||||||
|
batteryOptimizationDisableShown = pref[DataStoreManager.BATTERY_OPTIMIZE_DISABLE_SHOWN]
|
||||||
|
?: GeneralState.BATTERY_OPTIMIZATION_DISABLE_SHOWN_DEFAULT,
|
||||||
|
tunnelRunningFromManualStart = pref[DataStoreManager.TUNNEL_RUNNING_FROM_MANUAL_START]
|
||||||
|
?: GeneralState.TUNNELING_RUNNING_FROM_MANUAL_START_DEFAULT,
|
||||||
|
)
|
||||||
|
} catch (e: IllegalArgumentException) {
|
||||||
|
Timber.e(e)
|
||||||
|
GeneralState()
|
||||||
|
}
|
||||||
|
} ?: GeneralState()
|
||||||
|
}
|
||||||
|
}
|
||||||
+1
-1
@@ -4,7 +4,7 @@ import com.zaneschepke.wireguardautotunnel.data.SettingsDao
|
|||||||
import com.zaneschepke.wireguardautotunnel.data.model.Settings
|
import com.zaneschepke.wireguardautotunnel.data.model.Settings
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
||||||
class SettingsRepositoryImpl(private val settingsDoa: SettingsDao) : SettingsRepository {
|
class RoomSettingsRepository(private val settingsDoa: SettingsDao) : SettingsRepository {
|
||||||
|
|
||||||
override suspend fun save(settings: Settings) {
|
override suspend fun save(settings: Settings) {
|
||||||
settingsDoa.save(settings)
|
settingsDoa.save(settings)
|
||||||
+68
@@ -0,0 +1,68 @@
|
|||||||
|
package com.zaneschepke.wireguardautotunnel.data.repository
|
||||||
|
|
||||||
|
import com.zaneschepke.wireguardautotunnel.data.TunnelConfigDao
|
||||||
|
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
|
||||||
|
import com.zaneschepke.wireguardautotunnel.util.TunnelConfigs
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
||||||
|
class RoomTunnelConfigRepository(private val tunnelConfigDao: TunnelConfigDao) :
|
||||||
|
TunnelConfigRepository {
|
||||||
|
override fun getTunnelConfigsFlow(): Flow<TunnelConfigs> {
|
||||||
|
return tunnelConfigDao.getAllFlow()
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun getAll(): TunnelConfigs {
|
||||||
|
return tunnelConfigDao.getAll()
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun save(tunnelConfig: TunnelConfig) {
|
||||||
|
tunnelConfigDao.save(tunnelConfig)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun updatePrimaryTunnel(tunnelConfig: TunnelConfig?) {
|
||||||
|
tunnelConfigDao.resetPrimaryTunnel()
|
||||||
|
tunnelConfig?.let {
|
||||||
|
save(
|
||||||
|
it.copy(
|
||||||
|
isPrimaryTunnel = true,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun updateMobileDataTunnel(tunnelConfig: TunnelConfig?) {
|
||||||
|
tunnelConfigDao.resetMobileDataTunnel()
|
||||||
|
tunnelConfig?.let {
|
||||||
|
save(
|
||||||
|
it.copy(
|
||||||
|
isMobileDataTunnel = true,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun delete(tunnelConfig: TunnelConfig) {
|
||||||
|
tunnelConfigDao.delete(tunnelConfig)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun getById(id: Int): TunnelConfig? {
|
||||||
|
return tunnelConfigDao.getById(id.toLong())
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun count(): Int {
|
||||||
|
return tunnelConfigDao.count().toInt()
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun findByTunnelNetworksName(name: String): TunnelConfigs {
|
||||||
|
return tunnelConfigDao.findByTunnelNetworkName(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun findByMobileDataTunnel(): TunnelConfigs {
|
||||||
|
return tunnelConfigDao.findByMobileDataTunnel()
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun findPrimary(): TunnelConfigs {
|
||||||
|
return tunnelConfigDao.findByPrimary()
|
||||||
|
}
|
||||||
|
}
|
||||||
+12
@@ -12,7 +12,19 @@ interface TunnelConfigRepository {
|
|||||||
|
|
||||||
suspend fun save(tunnelConfig: TunnelConfig)
|
suspend fun save(tunnelConfig: TunnelConfig)
|
||||||
|
|
||||||
|
suspend fun updatePrimaryTunnel(tunnelConfig: TunnelConfig?)
|
||||||
|
|
||||||
|
suspend fun updateMobileDataTunnel(tunnelConfig: TunnelConfig?)
|
||||||
|
|
||||||
suspend fun delete(tunnelConfig: TunnelConfig)
|
suspend fun delete(tunnelConfig: TunnelConfig)
|
||||||
|
|
||||||
|
suspend fun getById(id: Int): TunnelConfig?
|
||||||
|
|
||||||
suspend fun count(): Int
|
suspend fun count(): Int
|
||||||
|
|
||||||
|
suspend fun findByTunnelNetworksName(name: String): TunnelConfigs
|
||||||
|
|
||||||
|
suspend fun findByMobileDataTunnel(): TunnelConfigs
|
||||||
|
|
||||||
|
suspend fun findPrimary(): TunnelConfigs
|
||||||
}
|
}
|
||||||
|
|||||||
-29
@@ -1,29 +0,0 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.data.repository
|
|
||||||
|
|
||||||
import com.zaneschepke.wireguardautotunnel.data.TunnelConfigDao
|
|
||||||
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
|
|
||||||
import com.zaneschepke.wireguardautotunnel.util.TunnelConfigs
|
|
||||||
import kotlinx.coroutines.flow.Flow
|
|
||||||
|
|
||||||
class TunnelConfigRepositoryImpl(private val tunnelConfigDao: TunnelConfigDao) :
|
|
||||||
TunnelConfigRepository {
|
|
||||||
override fun getTunnelConfigsFlow(): Flow<TunnelConfigs> {
|
|
||||||
return tunnelConfigDao.getAllFlow()
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun getAll(): TunnelConfigs {
|
|
||||||
return tunnelConfigDao.getAll()
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun save(tunnelConfig: TunnelConfig) {
|
|
||||||
tunnelConfigDao.save(tunnelConfig)
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun delete(tunnelConfig: TunnelConfig) {
|
|
||||||
tunnelConfigDao.delete(tunnelConfig)
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun count(): Int {
|
|
||||||
return tunnelConfigDao.count().toInt()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -2,4 +2,6 @@ package com.zaneschepke.wireguardautotunnel.module
|
|||||||
|
|
||||||
import javax.inject.Qualifier
|
import javax.inject.Qualifier
|
||||||
|
|
||||||
@Qualifier @Retention(AnnotationRetention.BINARY) annotation class Kernel
|
@Qualifier
|
||||||
|
@Retention(AnnotationRetention.BINARY)
|
||||||
|
annotation class Kernel
|
||||||
|
|||||||
@@ -5,10 +5,14 @@ import com.zaneschepke.wireguardautotunnel.data.AppDatabase
|
|||||||
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
|
||||||
|
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
|
||||||
|
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRoomRepository
|
||||||
|
import com.zaneschepke.wireguardautotunnel.data.repository.AppStateRepository
|
||||||
|
import com.zaneschepke.wireguardautotunnel.data.repository.DataStoreAppStateRepository
|
||||||
|
import com.zaneschepke.wireguardautotunnel.data.repository.RoomSettingsRepository
|
||||||
|
import com.zaneschepke.wireguardautotunnel.data.repository.RoomTunnelConfigRepository
|
||||||
import com.zaneschepke.wireguardautotunnel.data.repository.SettingsRepository
|
import com.zaneschepke.wireguardautotunnel.data.repository.SettingsRepository
|
||||||
import com.zaneschepke.wireguardautotunnel.data.repository.SettingsRepositoryImpl
|
|
||||||
import com.zaneschepke.wireguardautotunnel.data.repository.TunnelConfigRepository
|
import com.zaneschepke.wireguardautotunnel.data.repository.TunnelConfigRepository
|
||||||
import com.zaneschepke.wireguardautotunnel.data.repository.TunnelConfigRepositoryImpl
|
|
||||||
import dagger.Module
|
import dagger.Module
|
||||||
import dagger.Provides
|
import dagger.Provides
|
||||||
import dagger.hilt.InstallIn
|
import dagger.hilt.InstallIn
|
||||||
@@ -34,13 +38,13 @@ class RepositoryModule {
|
|||||||
@Singleton
|
@Singleton
|
||||||
@Provides
|
@Provides
|
||||||
fun provideTunnelConfigRepository(tunnelConfigDao: TunnelConfigDao): TunnelConfigRepository {
|
fun provideTunnelConfigRepository(tunnelConfigDao: TunnelConfigDao): TunnelConfigRepository {
|
||||||
return TunnelConfigRepositoryImpl(tunnelConfigDao)
|
return RoomTunnelConfigRepository(tunnelConfigDao)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Singleton
|
@Singleton
|
||||||
@Provides
|
@Provides
|
||||||
fun provideSettingsRepository(settingsDao: SettingsDao): SettingsRepository {
|
fun provideSettingsRepository(settingsDao: SettingsDao): SettingsRepository {
|
||||||
return SettingsRepositoryImpl(settingsDao)
|
return RoomSettingsRepository(settingsDao)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Singleton
|
@Singleton
|
||||||
@@ -48,4 +52,22 @@ class RepositoryModule {
|
|||||||
fun providePreferencesDataStore(@ApplicationContext context: Context): DataStoreManager {
|
fun providePreferencesDataStore(@ApplicationContext context: Context): DataStoreManager {
|
||||||
return DataStoreManager(context)
|
return DataStoreManager(context)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@Singleton
|
||||||
|
fun provideGeneralStateRepository(dataStoreManager: DataStoreManager): AppStateRepository {
|
||||||
|
return DataStoreAppStateRepository(dataStoreManager)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@Singleton
|
||||||
|
fun provideAppDataRepository(
|
||||||
|
settingsRepository: SettingsRepository,
|
||||||
|
tunnelConfigRepository: TunnelConfigRepository,
|
||||||
|
appStateRepository: AppStateRepository
|
||||||
|
): AppDataRepository {
|
||||||
|
return AppDataRoomRepository(settingsRepository, tunnelConfigRepository, appStateRepository)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,8 @@
|
|||||||
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.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
|
||||||
import com.wireguard.android.backend.GoBackend
|
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
|
||||||
import com.wireguard.android.backend.WgQuickBackend
|
|
||||||
import com.wireguard.android.util.RootShell
|
|
||||||
import com.wireguard.android.util.ToolsInstaller
|
|
||||||
import com.zaneschepke.wireguardautotunnel.data.repository.SettingsRepository
|
|
||||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnService
|
import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnService
|
||||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.WireGuardTunnel
|
import com.zaneschepke.wireguardautotunnel.service.tunnel.WireGuardTunnel
|
||||||
import dagger.Module
|
import dagger.Module
|
||||||
@@ -14,6 +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 org.amnezia.awg.backend.AwgQuickBackend
|
||||||
|
import org.amnezia.awg.backend.Backend
|
||||||
|
import org.amnezia.awg.backend.GoBackend
|
||||||
|
import org.amnezia.awg.util.RootShell
|
||||||
|
import org.amnezia.awg.util.ToolsInstaller
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
|
|
||||||
@Module
|
@Module
|
||||||
@@ -36,7 +37,7 @@ 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
|
@Provides
|
||||||
@@ -44,8 +45,14 @@ class TunnelModule {
|
|||||||
fun provideVpnService(
|
fun provideVpnService(
|
||||||
@Userspace userspaceBackend: Backend,
|
@Userspace userspaceBackend: Backend,
|
||||||
@Kernel kernelBackend: Backend,
|
@Kernel kernelBackend: Backend,
|
||||||
settingsRepository: SettingsRepository
|
appDataRepository: AppDataRepository
|
||||||
): VpnService {
|
): VpnService {
|
||||||
return WireGuardTunnel(userspaceBackend, kernelBackend, settingsRepository)
|
return WireGuardTunnel(userspaceBackend, kernelBackend, appDataRepository)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@Singleton
|
||||||
|
fun provideServiceManager(appDataRepository: AppDataRepository): ServiceManager {
|
||||||
|
return ServiceManager(appDataRepository)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,4 +2,6 @@ package com.zaneschepke.wireguardautotunnel.module
|
|||||||
|
|
||||||
import javax.inject.Qualifier
|
import javax.inject.Qualifier
|
||||||
|
|
||||||
@Qualifier @Retention(AnnotationRetention.BINARY) annotation class Userspace
|
@Qualifier
|
||||||
|
@Retention(AnnotationRetention.BINARY)
|
||||||
|
annotation class Userspace
|
||||||
|
|||||||
@@ -3,8 +3,7 @@ package com.zaneschepke.wireguardautotunnel.receiver
|
|||||||
import android.content.BroadcastReceiver
|
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.AppDataRepository
|
||||||
import com.zaneschepke.wireguardautotunnel.data.repository.TunnelConfigRepository
|
|
||||||
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
|
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
|
||||||
import com.zaneschepke.wireguardautotunnel.util.goAsync
|
import com.zaneschepke.wireguardautotunnel.util.goAsync
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
@@ -14,19 +13,33 @@ import javax.inject.Inject
|
|||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class BootReceiver : BroadcastReceiver() {
|
class BootReceiver : BroadcastReceiver() {
|
||||||
|
|
||||||
@Inject lateinit var settingsRepository: SettingsRepository
|
@Inject
|
||||||
|
lateinit var appDataRepository: AppDataRepository
|
||||||
|
|
||||||
@Inject lateinit var tunnelConfigRepository: TunnelConfigRepository
|
@Inject
|
||||||
|
lateinit var serviceManager: ServiceManager
|
||||||
|
|
||||||
override fun onReceive(context: Context?, intent: Intent?) = goAsync {
|
override fun onReceive(context: Context?, intent: Intent?) = goAsync {
|
||||||
if (Intent.ACTION_BOOT_COMPLETED != intent?.action) return@goAsync
|
if (Intent.ACTION_BOOT_COMPLETED != intent?.action) return@goAsync
|
||||||
val settings = settingsRepository.getSettings()
|
context?.run {
|
||||||
|
val settings = appDataRepository.settings.getSettings()
|
||||||
if (settings.isAutoTunnelEnabled) {
|
if (settings.isAutoTunnelEnabled) {
|
||||||
Timber.i("Starting watcher service from boot")
|
Timber.i("Starting watcher service from boot")
|
||||||
ServiceManager.startWatcherServiceForeground(context!!)
|
serviceManager.startWatcherServiceForeground(context)
|
||||||
} else if(settings.isAlwaysOnVpnEnabled) {
|
}
|
||||||
Timber.i("Starting tunnel from boot")
|
if (appDataRepository.appState.isTunnelRunningFromManualStart()) {
|
||||||
ServiceManager.startVpnServicePrimaryTunnel(context!!, settings, tunnelConfigRepository.getAll().firstOrNull())
|
appDataRepository.appState.getActiveTunnelId()?.let {
|
||||||
|
Timber.i("Starting tunnel that was active before reboot")
|
||||||
|
serviceManager.startVpnServiceForeground(
|
||||||
|
context,
|
||||||
|
appDataRepository.tunnels.getById(it)?.id,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (settings.isAlwaysOnVpnEnabled) {
|
||||||
|
Timber.i("Starting vpn service from boot AOVPN")
|
||||||
|
serviceManager.startVpnServiceForeground(context)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+11
-6
@@ -10,20 +10,25 @@ import com.zaneschepke.wireguardautotunnel.util.goAsync
|
|||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import kotlinx.coroutines.cancel
|
import kotlinx.coroutines.cancel
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
|
import timber.log.Timber
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class NotificationActionReceiver : BroadcastReceiver() {
|
class NotificationActionReceiver : BroadcastReceiver() {
|
||||||
@Inject lateinit var settingsRepository: SettingsRepository
|
@Inject
|
||||||
|
lateinit var settingsRepository: SettingsRepository
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var serviceManager: ServiceManager
|
||||||
|
|
||||||
override fun onReceive(context: Context, intent: Intent?) = goAsync {
|
override fun onReceive(context: Context, intent: Intent?) = goAsync {
|
||||||
try {
|
try {
|
||||||
val settings = settingsRepository.getSettings()
|
//TODO fix for manual start changes when enabled
|
||||||
if (settings.defaultTunnel != null) {
|
serviceManager.stopVpnServiceForeground(context)
|
||||||
ServiceManager.stopVpnService(context)
|
|
||||||
delay(Constants.TOGGLE_TUNNEL_DELAY)
|
delay(Constants.TOGGLE_TUNNEL_DELAY)
|
||||||
ServiceManager.startVpnServiceForeground(context, settings.defaultTunnel.toString())
|
serviceManager.startVpnServiceForeground(context)
|
||||||
}
|
} catch (e: Exception) {
|
||||||
|
Timber.e(e)
|
||||||
} finally {
|
} finally {
|
||||||
cancel()
|
cancel()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,5 +3,6 @@ package com.zaneschepke.wireguardautotunnel.service.foreground
|
|||||||
enum class Action {
|
enum class Action {
|
||||||
START,
|
START,
|
||||||
START_FOREGROUND,
|
START_FOREGROUND,
|
||||||
STOP
|
STOP,
|
||||||
|
STOP_FOREGROUND
|
||||||
}
|
}
|
||||||
|
|||||||
+6
-14
@@ -4,6 +4,7 @@ import android.content.Intent
|
|||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.IBinder
|
import android.os.IBinder
|
||||||
import androidx.lifecycle.LifecycleService
|
import androidx.lifecycle.LifecycleService
|
||||||
|
import com.zaneschepke.wireguardautotunnel.util.Constants
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
|
||||||
open class ForegroundService : LifecycleService() {
|
open class ForegroundService : LifecycleService() {
|
||||||
@@ -23,11 +24,12 @@ 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 -> stopService(intent.extras)
|
Action.STOP.name, Action.STOP_FOREGROUND.name -> stopService()
|
||||||
"android.net.VpnService" -> {
|
Constants.ALWAYS_ON_VPN_ACTION -> {
|
||||||
Timber.d("Always-on VPN starting service")
|
Timber.i("Always-on VPN starting service")
|
||||||
startService(intent.extras)
|
startService(intent.extras)
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> Timber.d("This should never happen. No action in the received intent")
|
else -> Timber.d("This should never happen. No action in the received intent")
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -35,29 +37,19 @@ open class ForegroundService : LifecycleService() {
|
|||||||
"with a null intent. It has been probably restarted by the system.",
|
"with a null intent. It has been probably restarted by the system.",
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
// by returning this we make sure the service is restarted if the system kills the service
|
|
||||||
return START_STICKY
|
return START_STICKY
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDestroy() {
|
|
||||||
super.onDestroy()
|
|
||||||
Timber.d("The service has been destroyed")
|
|
||||||
}
|
|
||||||
|
|
||||||
protected open fun startService(extras: Bundle?) {
|
protected open fun startService(extras: Bundle?) {
|
||||||
if (isServiceStarted) return
|
if (isServiceStarted) return
|
||||||
Timber.d("Starting ${this.javaClass.simpleName}")
|
Timber.d("Starting ${this.javaClass.simpleName}")
|
||||||
isServiceStarted = true
|
isServiceStarted = true
|
||||||
}
|
}
|
||||||
|
|
||||||
protected open fun stopService(extras: Bundle?) {
|
protected open fun stopService() {
|
||||||
Timber.d("Stopping ${this.javaClass.simpleName}")
|
Timber.d("Stopping ${this.javaClass.simpleName}")
|
||||||
try {
|
|
||||||
stopForeground(STOP_FOREGROUND_REMOVE)
|
stopForeground(STOP_FOREGROUND_REMOVE)
|
||||||
stopSelf()
|
stopSelf()
|
||||||
} catch (e: Exception) {
|
|
||||||
Timber.e(e)
|
|
||||||
}
|
|
||||||
isServiceStarted = false
|
isServiceStarted = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+43
-42
@@ -3,33 +3,17 @@ package com.zaneschepke.wireguardautotunnel.service.foreground
|
|||||||
import android.app.Service
|
import android.app.Service
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import com.zaneschepke.wireguardautotunnel.R
|
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
|
||||||
import com.zaneschepke.wireguardautotunnel.data.model.Settings
|
import com.zaneschepke.wireguardautotunnel.util.Constants
|
||||||
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
|
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
|
||||||
object ServiceManager {
|
class ServiceManager(private val appDataRepository: AppDataRepository) {
|
||||||
|
|
||||||
// private
|
|
||||||
// fun <T> Context.isServiceRunning(service: Class<T>) =
|
|
||||||
// (getSystemService(ACTIVITY_SERVICE) as ActivityManager)
|
|
||||||
// .runningAppProcesses.any {
|
|
||||||
// it.processName == service.name
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// fun <T : Service> getServiceState(
|
|
||||||
// context: Context,
|
|
||||||
// cls: Class<T>
|
|
||||||
// ): ServiceState {
|
|
||||||
// val isServiceRunning = context.isServiceRunning(cls)
|
|
||||||
// return if (isServiceRunning) ServiceState.STARTED else ServiceState.STOPPED
|
|
||||||
// }
|
|
||||||
|
|
||||||
private fun <T : Service> actionOnService(
|
private fun <T : Service> actionOnService(
|
||||||
action: Action,
|
action: Action,
|
||||||
context: Context,
|
context: Context,
|
||||||
cls: Class<T>,
|
cls: Class<T>,
|
||||||
extras: Map<String, String>? = null
|
extras: Map<String, Int>? = null
|
||||||
) {
|
) {
|
||||||
val intent =
|
val intent =
|
||||||
Intent(context, cls).also {
|
Intent(context, cls).also {
|
||||||
@@ -39,30 +23,41 @@ object ServiceManager {
|
|||||||
intent.component?.javaClass
|
intent.component?.javaClass
|
||||||
try {
|
try {
|
||||||
when (action) {
|
when (action) {
|
||||||
Action.START_FOREGROUND -> {
|
Action.START_FOREGROUND, Action.STOP_FOREGROUND -> context.startForegroundService(intent)
|
||||||
context.startForegroundService(intent)
|
Action.START, Action.STOP -> context.startService(intent)
|
||||||
}
|
|
||||||
Action.START -> {
|
|
||||||
context.startService(intent)
|
|
||||||
}
|
|
||||||
Action.STOP -> context.startService(intent)
|
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Timber.e(e.message)
|
Timber.e(e.message)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun startVpnService(context: Context, tunnelConfig: String) {
|
suspend fun startVpnService(
|
||||||
|
context: Context,
|
||||||
|
tunnelId: Int? = null,
|
||||||
|
isManualStart: Boolean = false
|
||||||
|
) {
|
||||||
|
if (isManualStart) onManualStart(tunnelId)
|
||||||
actionOnService(
|
actionOnService(
|
||||||
Action.START,
|
Action.START,
|
||||||
context,
|
context,
|
||||||
WireGuardTunnelService::class.java,
|
WireGuardTunnelService::class.java,
|
||||||
mapOf(context.getString(R.string.tunnel_extras_key) to tunnelConfig),
|
tunnelId?.let { mapOf(Constants.TUNNEL_EXTRA_KEY to it) },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun stopVpnService(context: Context) {
|
suspend fun stopVpnServiceForeground(context: Context, isManualStop: Boolean = false) {
|
||||||
Timber.d("Stopping vpn service action")
|
if (isManualStop) onManualStop()
|
||||||
|
Timber.i("Stopping vpn service")
|
||||||
|
actionOnService(
|
||||||
|
Action.STOP_FOREGROUND,
|
||||||
|
context,
|
||||||
|
WireGuardTunnelService::class.java,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun stopVpnService(context: Context, isManualStop: Boolean = false) {
|
||||||
|
if (isManualStop) onManualStop()
|
||||||
|
Timber.i("Stopping vpn service")
|
||||||
actionOnService(
|
actionOnService(
|
||||||
Action.STOP,
|
Action.STOP,
|
||||||
context,
|
context,
|
||||||
@@ -70,24 +65,30 @@ object ServiceManager {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun startVpnServiceForeground(context: Context, tunnelConfig: String) {
|
private suspend fun onManualStop() {
|
||||||
|
appDataRepository.appState.setManualStop()
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun onManualStart(tunnelId: Int?) {
|
||||||
|
tunnelId?.let {
|
||||||
|
appDataRepository.appState.setTunnelRunningFromManualStart(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun startVpnServiceForeground(
|
||||||
|
context: Context,
|
||||||
|
tunnelId: Int? = null,
|
||||||
|
isManualStart: Boolean = false
|
||||||
|
) {
|
||||||
|
if (isManualStart) onManualStart(tunnelId)
|
||||||
actionOnService(
|
actionOnService(
|
||||||
Action.START_FOREGROUND,
|
Action.START_FOREGROUND,
|
||||||
context,
|
context,
|
||||||
WireGuardTunnelService::class.java,
|
WireGuardTunnelService::class.java,
|
||||||
mapOf(context.getString(R.string.tunnel_extras_key) to tunnelConfig),
|
tunnelId?.let { mapOf(Constants.TUNNEL_EXTRA_KEY to it) },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun startVpnServicePrimaryTunnel(context: Context, settings: Settings, fallbackTunnel: TunnelConfig? = null) {
|
|
||||||
if(settings.defaultTunnel != null) {
|
|
||||||
return startVpnServiceForeground(context, settings.defaultTunnel!!)
|
|
||||||
}
|
|
||||||
if(fallbackTunnel != null) {
|
|
||||||
startVpnServiceForeground(context, fallbackTunnel.toString())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun startWatcherServiceForeground(
|
fun startWatcherServiceForeground(
|
||||||
context: Context,
|
context: Context,
|
||||||
) {
|
) {
|
||||||
|
|||||||
-6
@@ -1,6 +0,0 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.service.foreground
|
|
||||||
|
|
||||||
enum class ServiceState {
|
|
||||||
STARTED,
|
|
||||||
STOPPED,
|
|
||||||
}
|
|
||||||
+84
@@ -0,0 +1,84 @@
|
|||||||
|
package com.zaneschepke.wireguardautotunnel.service.foreground
|
||||||
|
|
||||||
|
import com.zaneschepke.wireguardautotunnel.data.model.Settings
|
||||||
|
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
|
||||||
|
import org.amnezia.awg.backend.Tunnel
|
||||||
|
|
||||||
|
data class WatcherState(
|
||||||
|
val isWifiConnected: Boolean = false,
|
||||||
|
val config: TunnelConfig? = null,
|
||||||
|
val vpnStatus: Tunnel.State = Tunnel.State.DOWN,
|
||||||
|
val isEthernetConnected: Boolean = false,
|
||||||
|
val isMobileDataConnected: Boolean = false,
|
||||||
|
val currentNetworkSSID: String = "",
|
||||||
|
val settings: Settings = Settings()
|
||||||
|
) {
|
||||||
|
|
||||||
|
private fun isVpnConnected() = vpnStatus == Tunnel.State.UP
|
||||||
|
fun isEthernetConditionMet(): Boolean {
|
||||||
|
return (isEthernetConnected &&
|
||||||
|
settings.isTunnelOnEthernetEnabled &&
|
||||||
|
!isVpnConnected())
|
||||||
|
}
|
||||||
|
|
||||||
|
fun isMobileDataConditionMet(): Boolean {
|
||||||
|
return (!isEthernetConnected &&
|
||||||
|
settings.isTunnelOnMobileDataEnabled &&
|
||||||
|
!isWifiConnected &&
|
||||||
|
isMobileDataConnected &&
|
||||||
|
!isVpnConnected())
|
||||||
|
}
|
||||||
|
|
||||||
|
fun isTunnelNotMobileDataPreferredConditionMet(): Boolean {
|
||||||
|
return (!isEthernetConnected &&
|
||||||
|
settings.isTunnelOnMobileDataEnabled &&
|
||||||
|
!isWifiConnected &&
|
||||||
|
isMobileDataConnected &&
|
||||||
|
config?.isMobileDataTunnel == false && isVpnConnected())
|
||||||
|
}
|
||||||
|
|
||||||
|
fun isTunnelOffOnMobileDataConditionMet(): Boolean {
|
||||||
|
return (!isEthernetConnected &&
|
||||||
|
!settings.isTunnelOnMobileDataEnabled &&
|
||||||
|
isMobileDataConnected &&
|
||||||
|
!isWifiConnected &&
|
||||||
|
isVpnConnected())
|
||||||
|
}
|
||||||
|
|
||||||
|
fun isUntrustedWifiConditionMet(): Boolean {
|
||||||
|
return (!isEthernetConnected &&
|
||||||
|
isWifiConnected &&
|
||||||
|
!settings.trustedNetworkSSIDs.contains(currentNetworkSSID) &&
|
||||||
|
settings.isTunnelOnWifiEnabled
|
||||||
|
&& !isVpnConnected())
|
||||||
|
}
|
||||||
|
|
||||||
|
fun isTunnelNotWifiNamePreferredMet(ssid: String): Boolean {
|
||||||
|
return (!isEthernetConnected &&
|
||||||
|
isWifiConnected &&
|
||||||
|
!settings.trustedNetworkSSIDs.contains(currentNetworkSSID) &&
|
||||||
|
settings.isTunnelOnWifiEnabled && config?.tunnelNetworks?.contains(ssid) == false && isVpnConnected())
|
||||||
|
}
|
||||||
|
|
||||||
|
fun isTrustedWifiConditionMet(): Boolean {
|
||||||
|
return (!isEthernetConnected &&
|
||||||
|
(isWifiConnected &&
|
||||||
|
settings.trustedNetworkSSIDs.contains(currentNetworkSSID)) &&
|
||||||
|
(isVpnConnected()))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun isTunnelOffOnWifiConditionMet(): Boolean {
|
||||||
|
return (!isEthernetConnected &&
|
||||||
|
(isWifiConnected &&
|
||||||
|
!settings.isTunnelOnWifiEnabled &&
|
||||||
|
(isVpnConnected())))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun isTunnelOffOnNoConnectivityMet(): Boolean {
|
||||||
|
return (!isEthernetConnected &&
|
||||||
|
!isWifiConnected &&
|
||||||
|
!isMobileDataConnected &&
|
||||||
|
(isVpnConnected()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
+123
-97
@@ -5,10 +5,9 @@ import android.os.Bundle
|
|||||||
import android.os.PowerManager
|
import android.os.PowerManager
|
||||||
import androidx.core.app.ServiceCompat
|
import androidx.core.app.ServiceCompat
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import com.wireguard.android.backend.Tunnel
|
|
||||||
import com.zaneschepke.wireguardautotunnel.R
|
import com.zaneschepke.wireguardautotunnel.R
|
||||||
import com.zaneschepke.wireguardautotunnel.data.model.Settings
|
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
|
||||||
import com.zaneschepke.wireguardautotunnel.data.repository.SettingsRepository
|
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
|
||||||
import com.zaneschepke.wireguardautotunnel.service.network.EthernetService
|
import com.zaneschepke.wireguardautotunnel.service.network.EthernetService
|
||||||
import com.zaneschepke.wireguardautotunnel.service.network.MobileDataService
|
import com.zaneschepke.wireguardautotunnel.service.network.MobileDataService
|
||||||
import com.zaneschepke.wireguardautotunnel.service.network.NetworkService
|
import com.zaneschepke.wireguardautotunnel.service.network.NetworkService
|
||||||
@@ -18,12 +17,14 @@ import com.zaneschepke.wireguardautotunnel.service.notification.NotificationServ
|
|||||||
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.Dispatchers
|
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.launch
|
import kotlinx.coroutines.launch
|
||||||
|
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
|
||||||
@@ -33,30 +34,30 @@ import javax.inject.Inject
|
|||||||
class WireGuardConnectivityWatcherService : ForegroundService() {
|
class WireGuardConnectivityWatcherService : ForegroundService() {
|
||||||
private val foregroundId = 122
|
private val foregroundId = 122
|
||||||
|
|
||||||
@Inject lateinit var wifiService: NetworkService<WifiService>
|
@Inject
|
||||||
|
lateinit var wifiService: NetworkService<WifiService>
|
||||||
|
|
||||||
@Inject lateinit var mobileDataService: NetworkService<MobileDataService>
|
@Inject
|
||||||
|
lateinit var mobileDataService: NetworkService<MobileDataService>
|
||||||
|
|
||||||
@Inject lateinit var ethernetService: NetworkService<EthernetService>
|
@Inject
|
||||||
|
lateinit var ethernetService: NetworkService<EthernetService>
|
||||||
|
|
||||||
@Inject lateinit var settingsRepository: SettingsRepository
|
@Inject
|
||||||
|
lateinit var appDataRepository: AppDataRepository
|
||||||
|
|
||||||
@Inject lateinit var notificationService: NotificationService
|
@Inject
|
||||||
|
lateinit var notificationService: NotificationService
|
||||||
|
|
||||||
@Inject lateinit var vpnService: VpnService
|
@Inject
|
||||||
|
lateinit var vpnService: VpnService
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var serviceManager: ServiceManager
|
||||||
|
|
||||||
private val networkEventsFlow = MutableStateFlow(WatcherState())
|
private val networkEventsFlow = MutableStateFlow(WatcherState())
|
||||||
|
|
||||||
data class WatcherState(
|
private var watcherJob: Job? = null
|
||||||
val isWifiConnected: Boolean = false,
|
|
||||||
val isVpnConnected: Boolean = false,
|
|
||||||
val isEthernetConnected: Boolean = false,
|
|
||||||
val isMobileDataConnected: Boolean = false,
|
|
||||||
val currentNetworkSSID: String = "",
|
|
||||||
val settings: Settings = Settings()
|
|
||||||
)
|
|
||||||
|
|
||||||
private lateinit var watcherJob: Job
|
|
||||||
|
|
||||||
private var wakeLock: PowerManager.WakeLock? = null
|
private var wakeLock: PowerManager.WakeLock? = null
|
||||||
private val tag = this.javaClass.name
|
private val tag = this.javaClass.name
|
||||||
@@ -65,7 +66,7 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
|
|||||||
super.onCreate()
|
super.onCreate()
|
||||||
lifecycleScope.launch(Dispatchers.Main) {
|
lifecycleScope.launch(Dispatchers.Main) {
|
||||||
try {
|
try {
|
||||||
if (settingsRepository.getSettings().isAutoTunnelPaused) {
|
if (appDataRepository.settings.getSettings().isAutoTunnelPaused) {
|
||||||
launchWatcherPausedNotification()
|
launchWatcherPausedNotification()
|
||||||
} else launchWatcherNotification()
|
} else launchWatcherNotification()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
@@ -86,8 +87,8 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun stopService(extras: Bundle?) {
|
override fun stopService() {
|
||||||
super.stopService(extras)
|
super.stopService()
|
||||||
wakeLock?.let {
|
wakeLock?.let {
|
||||||
if (it.isHeld) {
|
if (it.isHeld) {
|
||||||
it.release()
|
it.release()
|
||||||
@@ -134,15 +135,17 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun cancelWatcherJob() {
|
private fun cancelWatcherJob() {
|
||||||
if (this::watcherJob.isInitialized) {
|
try {
|
||||||
watcherJob.cancel()
|
watcherJob?.cancel()
|
||||||
|
} catch (e : CancellationException) {
|
||||||
|
Timber.i("Watcher job cancelled")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun startWatcherJob() {
|
private fun startWatcherJob() {
|
||||||
watcherJob =
|
watcherJob =
|
||||||
lifecycleScope.launch(Dispatchers.IO) {
|
lifecycleScope.launch(Dispatchers.IO) {
|
||||||
val setting = settingsRepository.getSettings()
|
val setting = appDataRepository.settings.getSettings()
|
||||||
launch {
|
launch {
|
||||||
Timber.i("Starting wifi watcher")
|
Timber.i("Starting wifi watcher")
|
||||||
watchForWifiConnectivityChanges()
|
watchForWifiConnectivityChanges()
|
||||||
@@ -167,7 +170,7 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
|
|||||||
Timber.i("Starting settings watcher")
|
Timber.i("Starting settings watcher")
|
||||||
watchForSettingsChanges()
|
watchForSettingsChanges()
|
||||||
}
|
}
|
||||||
if(setting.isPingEnabled) {
|
if (setting.isPingEnabled) {
|
||||||
launch {
|
launch {
|
||||||
Timber.i("Starting ping watcher")
|
Timber.i("Starting ping watcher")
|
||||||
watchForPingFailure()
|
watchForPingFailure()
|
||||||
@@ -191,6 +194,7 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
|
|||||||
isMobileDataConnected = true,
|
isMobileDataConnected = true,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
is NetworkStatus.CapabilitiesChanged -> {
|
is NetworkStatus.CapabilitiesChanged -> {
|
||||||
networkEventsFlow.value =
|
networkEventsFlow.value =
|
||||||
networkEventsFlow.value.copy(
|
networkEventsFlow.value.copy(
|
||||||
@@ -198,6 +202,7 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
|
|||||||
)
|
)
|
||||||
Timber.i("Mobile data capabilities changed")
|
Timber.i("Mobile data capabilities changed")
|
||||||
}
|
}
|
||||||
|
|
||||||
is NetworkStatus.Unavailable -> {
|
is NetworkStatus.Unavailable -> {
|
||||||
networkEventsFlow.value =
|
networkEventsFlow.value =
|
||||||
networkEventsFlow.value.copy(
|
networkEventsFlow.value.copy(
|
||||||
@@ -212,39 +217,39 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
|
|||||||
private suspend fun watchForPingFailure() {
|
private suspend fun watchForPingFailure() {
|
||||||
try {
|
try {
|
||||||
do {
|
do {
|
||||||
if(vpnService.vpnState.value.status == Tunnel.State.UP) {
|
if (vpnService.vpnState.value.status == Tunnel.State.UP) {
|
||||||
val config = vpnService.vpnState.value.config
|
val tunnelConfig = vpnService.vpnState.value.tunnelConfig
|
||||||
config?.let {
|
tunnelConfig?.let {
|
||||||
val results = it.peers.map { peer ->
|
val config = TunnelConfig.configFromQuick(it.wgQuick)
|
||||||
val host = if(peer.endpoint.isPresent &&
|
val results = config.peers.map { peer ->
|
||||||
|
val host = if (peer.endpoint.isPresent &&
|
||||||
peer.endpoint.get().resolved.isPresent)
|
peer.endpoint.get().resolved.isPresent)
|
||||||
peer.endpoint.get().resolved.get().host
|
peer.endpoint.get().resolved.get().host
|
||||||
else Constants.BACKUP_PING_HOST
|
else Constants.BACKUP_PING_HOST
|
||||||
Timber.i("Checking reachability of: $host")
|
Timber.i("Checking reachability of: $host")
|
||||||
val reachable = InetAddress.getByName(host).isReachable(Constants.PING_TIMEOUT.toInt())
|
val reachable = InetAddress.getByName(host)
|
||||||
|
.isReachable(Constants.PING_TIMEOUT.toInt())
|
||||||
Timber.i("Result: reachable - $reachable")
|
Timber.i("Result: reachable - $reachable")
|
||||||
reachable
|
reachable
|
||||||
}
|
}
|
||||||
if(results.contains(false)) {
|
if (results.contains(false)) {
|
||||||
Timber.i("Restarting VPN for ping failure")
|
Timber.i("Restarting VPN for ping failure")
|
||||||
ServiceManager.stopVpnService(this)
|
serviceManager.stopVpnServiceForeground(this)
|
||||||
delay(Constants.VPN_RESTART_DELAY)
|
delay(Constants.VPN_RESTART_DELAY)
|
||||||
val tunnel = networkEventsFlow.value.settings.defaultTunnel
|
serviceManager.startVpnServiceForeground(this)
|
||||||
ServiceManager.startVpnServiceForeground(this, tunnel!!)
|
|
||||||
delay(Constants.PING_COOLDOWN)
|
delay(Constants.PING_COOLDOWN)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
delay(Constants.PING_INTERVAL)
|
delay(Constants.PING_INTERVAL)
|
||||||
} while (true)
|
} while (true)
|
||||||
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Timber.e(e)
|
Timber.e(e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun watchForSettingsChanges() {
|
private suspend fun watchForSettingsChanges() {
|
||||||
settingsRepository.getSettingsFlow().collect {
|
appDataRepository.settings.getSettingsFlow().collect {
|
||||||
if (networkEventsFlow.value.settings.isAutoTunnelPaused != it.isAutoTunnelPaused) {
|
if (networkEventsFlow.value.settings.isAutoTunnelPaused != it.isAutoTunnelPaused) {
|
||||||
when (it.isAutoTunnelPaused) {
|
when (it.isAutoTunnelPaused) {
|
||||||
true -> launchWatcherPausedNotification()
|
true -> launchWatcherPausedNotification()
|
||||||
@@ -260,19 +265,11 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
|
|||||||
|
|
||||||
private suspend fun watchForVpnConnectivityChanges() {
|
private suspend fun watchForVpnConnectivityChanges() {
|
||||||
vpnService.vpnState.collect {
|
vpnService.vpnState.collect {
|
||||||
when (it.status) {
|
|
||||||
Tunnel.State.DOWN ->
|
|
||||||
networkEventsFlow.value =
|
networkEventsFlow.value =
|
||||||
networkEventsFlow.value.copy(
|
networkEventsFlow.value.copy(
|
||||||
isVpnConnected = false,
|
vpnStatus = it.status,
|
||||||
|
config = it.tunnelConfig,
|
||||||
)
|
)
|
||||||
Tunnel.State.UP ->
|
|
||||||
networkEventsFlow.value =
|
|
||||||
networkEventsFlow.value.copy(
|
|
||||||
isVpnConnected = true,
|
|
||||||
)
|
|
||||||
else -> {}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -286,6 +283,7 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
|
|||||||
isEthernetConnected = true,
|
isEthernetConnected = true,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
is NetworkStatus.CapabilitiesChanged -> {
|
is NetworkStatus.CapabilitiesChanged -> {
|
||||||
Timber.i("Ethernet capabilities changed")
|
Timber.i("Ethernet capabilities changed")
|
||||||
networkEventsFlow.value =
|
networkEventsFlow.value =
|
||||||
@@ -293,6 +291,7 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
|
|||||||
isEthernetConnected = true,
|
isEthernetConnected = true,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
is NetworkStatus.Unavailable -> {
|
is NetworkStatus.Unavailable -> {
|
||||||
networkEventsFlow.value =
|
networkEventsFlow.value =
|
||||||
networkEventsFlow.value.copy(
|
networkEventsFlow.value.copy(
|
||||||
@@ -314,19 +313,26 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
|
|||||||
isWifiConnected = true,
|
isWifiConnected = true,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
is NetworkStatus.CapabilitiesChanged -> {
|
is NetworkStatus.CapabilitiesChanged -> {
|
||||||
Timber.i("Wifi capabilities changed")
|
Timber.i("Wifi capabilities changed")
|
||||||
networkEventsFlow.value =
|
networkEventsFlow.value =
|
||||||
networkEventsFlow.value.copy(
|
networkEventsFlow.value.copy(
|
||||||
isWifiConnected = true,
|
isWifiConnected = true,
|
||||||
)
|
)
|
||||||
val ssid = wifiService.getNetworkName(it.networkCapabilities) ?: ""
|
val ssid = wifiService.getNetworkName(it.networkCapabilities)
|
||||||
Timber.i("Detected SSID: $ssid")
|
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 =
|
||||||
networkEventsFlow.value.copy(
|
networkEventsFlow.value.copy(
|
||||||
currentNetworkSSID = ssid,
|
currentNetworkSSID = ssid,
|
||||||
)
|
)
|
||||||
|
} ?: Timber.w("Failed to read ssid")
|
||||||
}
|
}
|
||||||
|
|
||||||
is NetworkStatus.Unavailable -> {
|
is NetworkStatus.Unavailable -> {
|
||||||
networkEventsFlow.value =
|
networkEventsFlow.value =
|
||||||
networkEventsFlow.value.copy(
|
networkEventsFlow.value.copy(
|
||||||
@@ -338,64 +344,84 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private suspend fun getMobileDataTunnel(): TunnelConfig? {
|
||||||
|
return appDataRepository.tunnels.findByMobileDataTunnel().firstOrNull()
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun getSsidTunnel(ssid: String): TunnelConfig? {
|
||||||
|
return appDataRepository.tunnels.findByTunnelNetworksName(ssid).firstOrNull()
|
||||||
|
}
|
||||||
|
|
||||||
private suspend fun manageVpn() {
|
private suspend fun manageVpn() {
|
||||||
networkEventsFlow.collectLatest {
|
networkEventsFlow.collectLatest { watcherState ->
|
||||||
val autoTunnel = "Auto-tunnel watcher"
|
val autoTunnel = "Auto-tunnel watcher"
|
||||||
if (!it.settings.isAutoTunnelPaused && it.settings.defaultTunnel != null) {
|
if (!watcherState.settings.isAutoTunnelPaused) {
|
||||||
delay(Constants.TOGGLE_TUNNEL_DELAY)
|
//delay for rapid network state changes and then collect latest
|
||||||
|
delay(Constants.WATCHER_COLLECTION_DELAY)
|
||||||
when {
|
when {
|
||||||
((it.isEthernetConnected &&
|
watcherState.isEthernetConditionMet() -> {
|
||||||
it.settings.isTunnelOnEthernetEnabled &&
|
Timber.i("$autoTunnel - tunnel on on ethernet condition met")
|
||||||
!it.isVpnConnected)) -> {
|
serviceManager.startVpnServiceForeground(this)
|
||||||
ServiceManager.startVpnServiceForeground(this, it.settings.defaultTunnel!!)
|
|
||||||
Timber.i("$autoTunnel condition 1 met")
|
|
||||||
}
|
}
|
||||||
(!it.isEthernetConnected &&
|
|
||||||
it.settings.isTunnelOnMobileDataEnabled &&
|
watcherState.isMobileDataConditionMet() -> {
|
||||||
!it.isWifiConnected &&
|
Timber.i("$autoTunnel - tunnel on on mobile data condition met")
|
||||||
it.isMobileDataConnected &&
|
serviceManager.startVpnServiceForeground(this, getMobileDataTunnel()?.id)
|
||||||
!it.isVpnConnected) -> {
|
|
||||||
ServiceManager.startVpnServiceForeground(this, it.settings.defaultTunnel!!)
|
|
||||||
Timber.i("$autoTunnel condition 2 met")
|
|
||||||
}
|
}
|
||||||
(!it.isEthernetConnected &&
|
|
||||||
!it.settings.isTunnelOnMobileDataEnabled &&
|
watcherState.isTunnelNotMobileDataPreferredConditionMet() -> {
|
||||||
!it.isWifiConnected &&
|
getMobileDataTunnel()?.let {
|
||||||
it.isVpnConnected) -> {
|
Timber.i("$autoTunnel - tunnel connected on mobile data is not preferred condition met, switching to preferred")
|
||||||
ServiceManager.stopVpnService(this)
|
serviceManager.startVpnServiceForeground(
|
||||||
Timber.i("$autoTunnel condition 3 met")
|
this,
|
||||||
|
getMobileDataTunnel()?.id,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
(!it.isEthernetConnected &&
|
|
||||||
it.isWifiConnected &&
|
|
||||||
!it.settings.trustedNetworkSSIDs.contains(it.currentNetworkSSID) &&
|
|
||||||
it.settings.isTunnelOnWifiEnabled &&
|
|
||||||
(!it.isVpnConnected)) -> {
|
|
||||||
ServiceManager.startVpnServiceForeground(this, it.settings.defaultTunnel!!)
|
|
||||||
Timber.i("$autoTunnel condition 4 met")
|
|
||||||
}
|
}
|
||||||
(!it.isEthernetConnected &&
|
|
||||||
(it.isWifiConnected &&
|
watcherState.isTunnelOffOnMobileDataConditionMet() -> {
|
||||||
it.settings.trustedNetworkSSIDs.contains(it.currentNetworkSSID)) &&
|
Timber.i("$autoTunnel - tunnel off on mobile data met, turning vpn off")
|
||||||
(it.isVpnConnected)) -> {
|
serviceManager.stopVpnServiceForeground(this)
|
||||||
ServiceManager.stopVpnService(this)
|
|
||||||
Timber.i("$autoTunnel condition 5 met")
|
|
||||||
}
|
}
|
||||||
(!it.isEthernetConnected &&
|
|
||||||
(it.isWifiConnected &&
|
watcherState.isTunnelNotWifiNamePreferredMet(watcherState.currentNetworkSSID) -> {
|
||||||
!it.settings.isTunnelOnWifiEnabled &&
|
Timber.i("$autoTunnel - tunnel on ssid not associated with current tunnel condition met")
|
||||||
(it.isVpnConnected))) -> {
|
getSsidTunnel(watcherState.currentNetworkSSID)?.let {
|
||||||
ServiceManager.stopVpnService(this)
|
Timber.i("Found tunnel associated with this SSID, bringing tunnel up")
|
||||||
Timber.i("$autoTunnel condition 6 met")
|
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)
|
||||||
}
|
}
|
||||||
(!it.isEthernetConnected &&
|
}.invoke()
|
||||||
!it.isWifiConnected &&
|
|
||||||
!it.isMobileDataConnected &&
|
|
||||||
(it.isVpnConnected)) -> {
|
|
||||||
ServiceManager.stopVpnService(this)
|
|
||||||
Timber.i("$autoTunnel condition 7 met")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
watcherState.isUntrustedWifiConditionMet() -> {
|
||||||
|
Timber.i("$autoTunnel - tunnel on untrusted wifi condition met")
|
||||||
|
serviceManager.startVpnServiceForeground(
|
||||||
|
this,
|
||||||
|
getSsidTunnel(watcherState.currentNetworkSSID)?.id,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
watcherState.isTrustedWifiConditionMet() -> {
|
||||||
|
Timber.i("$autoTunnel - tunnel off on trusted wifi condition met, turning vpn off")
|
||||||
|
serviceManager.stopVpnServiceForeground(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
watcherState.isTunnelOffOnWifiConditionMet() -> {
|
||||||
|
Timber.i("$autoTunnel - tunnel off on wifi condition met, turning vpn off")
|
||||||
|
serviceManager.stopVpnServiceForeground(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
watcherState.isTunnelOffOnNoConnectivityMet() -> {
|
||||||
|
Timber.i("$autoTunnel - tunnel off on no connectivity met, turning vpn off")
|
||||||
|
serviceManager.stopVpnServiceForeground(this)
|
||||||
|
}
|
||||||
|
|
||||||
else -> {
|
else -> {
|
||||||
Timber.i("$autoTunnel no condition met")
|
Timber.i("$autoTunnel - no condition met")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+50
-52
@@ -6,9 +6,7 @@ import android.os.Bundle
|
|||||||
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.model.TunnelConfig
|
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
|
||||||
import com.zaneschepke.wireguardautotunnel.data.repository.SettingsRepository
|
|
||||||
import com.zaneschepke.wireguardautotunnel.data.repository.TunnelConfigRepository
|
|
||||||
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
|
||||||
@@ -17,10 +15,12 @@ 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.Dispatchers
|
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 org.amnezia.awg.backend.Tunnel
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@@ -28,23 +28,24 @@ import javax.inject.Inject
|
|||||||
class WireGuardTunnelService : ForegroundService() {
|
class WireGuardTunnelService : ForegroundService() {
|
||||||
private val foregroundId = 123
|
private val foregroundId = 123
|
||||||
|
|
||||||
@Inject lateinit var vpnService: VpnService
|
@Inject
|
||||||
|
lateinit var vpnService: VpnService
|
||||||
|
|
||||||
@Inject lateinit var settingsRepository: SettingsRepository
|
@Inject
|
||||||
|
lateinit var appDataRepository: AppDataRepository
|
||||||
|
|
||||||
@Inject lateinit var tunnelConfigRepository: TunnelConfigRepository
|
@Inject
|
||||||
|
lateinit var notificationService: NotificationService
|
||||||
|
|
||||||
@Inject lateinit var notificationService: NotificationService
|
private var job: Job? = null
|
||||||
|
|
||||||
private lateinit var job: Job
|
|
||||||
|
|
||||||
private var tunnelName: String = ""
|
|
||||||
private var didShowConnected = false
|
private var didShowConnected = false
|
||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
lifecycleScope.launch(Dispatchers.Main) {
|
lifecycleScope.launch(Dispatchers.Main) {
|
||||||
if (tunnelConfigRepository.getAll().isNotEmpty()) {
|
//TODO fix this to not launch if AOVPN
|
||||||
|
if (appDataRepository.tunnels.count() != 0) {
|
||||||
launchVpnNotification()
|
launchVpnNotification()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -53,44 +54,28 @@ class WireGuardTunnelService : ForegroundService() {
|
|||||||
override fun startService(extras: Bundle?) {
|
override fun startService(extras: Bundle?) {
|
||||||
super.startService(extras)
|
super.startService(extras)
|
||||||
cancelJob()
|
cancelJob()
|
||||||
val tunnelConfigString = extras?.getString(getString(R.string.tunnel_extras_key))
|
|
||||||
val tunnelConfig = tunnelConfigString?.let { TunnelConfig.from(it) }
|
|
||||||
tunnelName = tunnelConfig?.name ?: ""
|
|
||||||
job =
|
job =
|
||||||
lifecycleScope.launch(Dispatchers.IO) {
|
lifecycleScope.launch(Dispatchers.IO) {
|
||||||
launch {
|
launch {
|
||||||
if (tunnelConfig != null) {
|
val tunnelId = extras?.getInt(Constants.TUNNEL_EXTRA_KEY)
|
||||||
try {
|
if (vpnService.getState() == Tunnel.State.UP) {
|
||||||
tunnelName = tunnelConfig.name
|
vpnService.stopTunnel()
|
||||||
vpnService.startTunnel(tunnelConfig)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Timber.e("Problem starting tunnel: ${e.message}")
|
|
||||||
stopService(extras)
|
|
||||||
}
|
}
|
||||||
} else {
|
vpnService.startTunnel(
|
||||||
Timber.i("Tunnel config null, starting default tunnel or first")
|
tunnelId?.let {
|
||||||
val settings = settingsRepository.getSettings()
|
appDataRepository.tunnels.getById(it)
|
||||||
val tunnels = tunnelConfigRepository.getAll()
|
},
|
||||||
if (settings.isAlwaysOnVpnEnabled) {
|
)
|
||||||
val tunnel =
|
|
||||||
if (settings.defaultTunnel != null) {
|
|
||||||
TunnelConfig.from(settings.defaultTunnel!!)
|
|
||||||
} else if (tunnels.isNotEmpty()) {
|
|
||||||
tunnels.first()
|
|
||||||
} else {
|
|
||||||
null
|
|
||||||
}
|
}
|
||||||
if (tunnel != null) {
|
|
||||||
tunnelName = tunnel.name
|
|
||||||
vpnService.startTunnel(tunnel)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
launchAlwaysOnDisabledNotification()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// TODO add failed to connect notification
|
|
||||||
launch {
|
launch {
|
||||||
|
handshakeNotifications()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//TODO improve tunnel notifications
|
||||||
|
private suspend fun handshakeNotifications() {
|
||||||
|
var tunnelName: String? = null
|
||||||
vpnService.vpnState.collect { state ->
|
vpnService.vpnState.collect { state ->
|
||||||
state.statistics
|
state.statistics
|
||||||
?.mapPeerStats()
|
?.mapPeerStats()
|
||||||
@@ -100,31 +85,42 @@ class WireGuardTunnelService : ForegroundService() {
|
|||||||
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
|
||||||
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?.any { it == HandshakeStatus.STALE } == true -> {}
|
||||||
statuses?.all { it == HandshakeStatus.NOT_STARTED } ==
|
statuses?.all { it == HandshakeStatus.NOT_STARTED } ==
|
||||||
true -> {}
|
true -> {
|
||||||
|
}
|
||||||
|
|
||||||
else -> {}
|
else -> {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
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",
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun launchAlwaysOnDisabledNotification() {
|
private fun launchAlwaysOnDisabledNotification() {
|
||||||
launchVpnNotification(title = this.getString(R.string.vpn_connection_failed),
|
launchVpnNotification(
|
||||||
description = this.getString(R.string.always_on_disabled))
|
title = this.getString(R.string.vpn_connection_failed),
|
||||||
|
description = this.getString(R.string.always_on_disabled),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun stopService(extras: Bundle?) {
|
override fun stopService() {
|
||||||
super.stopService(extras)
|
super.stopService()
|
||||||
lifecycleScope.launch(Dispatchers.IO) {
|
lifecycleScope.launch(Dispatchers.IO) {
|
||||||
vpnService.stopTunnel()
|
vpnService.stopTunnel()
|
||||||
didShowConnected = false
|
didShowConnected = false
|
||||||
@@ -183,8 +179,10 @@ class WireGuardTunnelService : ForegroundService() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun cancelJob() {
|
private fun cancelJob() {
|
||||||
if (this::job.isInitialized) {
|
try {
|
||||||
job.cancel()
|
job?.cancel()
|
||||||
|
} catch (e : CancellationException) {
|
||||||
|
Timber.i("Tunnel job cancelled")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+1
@@ -53,6 +53,7 @@ abstract class BaseNetworkService<T : BaseNetworkService<T>>(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> {
|
else -> {
|
||||||
object : ConnectivityManager.NetworkCallback() {
|
object : ConnectivityManager.NetworkCallback() {
|
||||||
override fun onAvailable(network: Network) {
|
override fun onAvailable(network: Network) {
|
||||||
|
|||||||
+32
-46
@@ -1,80 +1,66 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.service.shortcut
|
package com.zaneschepke.wireguardautotunnel.service.shortcut
|
||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.View
|
|
||||||
import androidx.activity.ComponentActivity
|
import androidx.activity.ComponentActivity
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
|
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
|
||||||
import com.zaneschepke.wireguardautotunnel.data.repository.SettingsRepository
|
|
||||||
import com.zaneschepke.wireguardautotunnel.data.repository.TunnelConfigRepository
|
|
||||||
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.WireGuardTunnelService
|
import com.zaneschepke.wireguardautotunnel.service.foreground.WireGuardTunnelService
|
||||||
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 timber.log.Timber
|
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class ShortcutsActivity : ComponentActivity() {
|
class ShortcutsActivity : ComponentActivity() {
|
||||||
@Inject lateinit var settingsRepository: SettingsRepository
|
|
||||||
|
|
||||||
@Inject lateinit var tunnelConfigRepository: TunnelConfigRepository
|
@Inject
|
||||||
|
lateinit var appDataRepository: AppDataRepository
|
||||||
|
|
||||||
private suspend fun toggleWatcherServicePause() {
|
@Inject
|
||||||
val settings = settingsRepository.getSettings()
|
lateinit var serviceManager: ServiceManager
|
||||||
if (settings.isAutoTunnelEnabled) {
|
|
||||||
val pauseAutoTunnel = !settings.isAutoTunnelPaused
|
|
||||||
settingsRepository.save(
|
|
||||||
settings.copy(
|
|
||||||
isAutoTunnelPaused = pauseAutoTunnel,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
setContentView(View(this))
|
|
||||||
if (
|
|
||||||
intent
|
|
||||||
.getStringExtra(CLASS_NAME_EXTRA_KEY)
|
|
||||||
.equals(WireGuardTunnelService::class.java.simpleName)
|
|
||||||
) {
|
|
||||||
lifecycleScope.launch(Dispatchers.Main) {
|
lifecycleScope.launch(Dispatchers.Main) {
|
||||||
val settings = settingsRepository.getSettings()
|
val settings = appDataRepository.settings.getSettings()
|
||||||
if (settings.isShortcutsEnabled) {
|
if (settings.isShortcutsEnabled) {
|
||||||
try {
|
when (intent.getStringExtra(CLASS_NAME_EXTRA_KEY)) {
|
||||||
|
WireGuardTunnelService::class.java.simpleName -> {
|
||||||
val tunnelName = intent.getStringExtra(TUNNEL_NAME_EXTRA_KEY)
|
val tunnelName = intent.getStringExtra(TUNNEL_NAME_EXTRA_KEY)
|
||||||
val tunnelConfig =
|
val tunnelConfig = tunnelName?.let {
|
||||||
if (tunnelName != null) {
|
appDataRepository.tunnels.getAll().firstOrNull {
|
||||||
tunnelConfigRepository.getAll().firstOrNull {
|
|
||||||
it.name == tunnelName
|
it.name == tunnelName
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
if (settings.defaultTunnel == null) {
|
|
||||||
tunnelConfigRepository.getAll().first()
|
|
||||||
} else {
|
|
||||||
TunnelConfig.from(settings.defaultTunnel!!)
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
tunnelConfig ?: return@launch
|
|
||||||
toggleWatcherServicePause()
|
|
||||||
when (intent.action) {
|
when (intent.action) {
|
||||||
Action.STOP.name ->
|
Action.START.name -> serviceManager.startVpnServiceForeground(
|
||||||
ServiceManager.stopVpnService(
|
this@ShortcutsActivity, tunnelConfig?.id, isManualStart = true,
|
||||||
this@ShortcutsActivity,
|
|
||||||
)
|
)
|
||||||
Action.START.name ->
|
|
||||||
ServiceManager.startVpnServiceForeground(
|
Action.STOP.name -> serviceManager.stopVpnServiceForeground(
|
||||||
this@ShortcutsActivity,
|
this@ShortcutsActivity,
|
||||||
tunnelConfig.toString(),
|
isManualStop = true,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
WireGuardConnectivityWatcherService::class.java.simpleName -> {
|
||||||
|
when (intent.action) {
|
||||||
|
Action.START.name -> appDataRepository.settings.save(
|
||||||
|
settings.copy(
|
||||||
|
isAutoTunnelPaused = false,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
Action.STOP.name -> appDataRepository.settings.save(
|
||||||
|
settings.copy(
|
||||||
|
isAutoTunnelPaused = true,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
|
||||||
Timber.e(e.message)
|
|
||||||
finish()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+111
@@ -0,0 +1,111 @@
|
|||||||
|
package com.zaneschepke.wireguardautotunnel.service.tile
|
||||||
|
|
||||||
|
import android.os.Build
|
||||||
|
import android.service.quicksettings.Tile
|
||||||
|
import android.service.quicksettings.TileService
|
||||||
|
import com.zaneschepke.wireguardautotunnel.R
|
||||||
|
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
|
||||||
|
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
|
||||||
|
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
|
||||||
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.cancel
|
||||||
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import timber.log.Timber
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
@AndroidEntryPoint
|
||||||
|
class AutoTunnelControlTile : TileService() {
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var appDataRepository: AppDataRepository
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var serviceManager: ServiceManager
|
||||||
|
|
||||||
|
private val scope = CoroutineScope(Dispatchers.IO)
|
||||||
|
|
||||||
|
private var manualStartConfig: TunnelConfig? = null
|
||||||
|
|
||||||
|
override fun onStartListening() {
|
||||||
|
super.onStartListening()
|
||||||
|
scope.launch {
|
||||||
|
appDataRepository.settings.getSettingsFlow().collectLatest {
|
||||||
|
when (it.isAutoTunnelEnabled) {
|
||||||
|
true -> {
|
||||||
|
if (it.isAutoTunnelPaused) {
|
||||||
|
setInactive()
|
||||||
|
setTileDescription(this@AutoTunnelControlTile.getString(R.string.paused))
|
||||||
|
} else {
|
||||||
|
setActive()
|
||||||
|
setTileDescription(this@AutoTunnelControlTile.getString(R.string.active))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
false -> {
|
||||||
|
setTileDescription(this@AutoTunnelControlTile.getString(R.string.disabled))
|
||||||
|
setUnavailable()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onTileAdded() {
|
||||||
|
super.onTileAdded()
|
||||||
|
onStartListening()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroy() {
|
||||||
|
super.onDestroy()
|
||||||
|
scope.cancel()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onTileRemoved() {
|
||||||
|
super.onTileRemoved()
|
||||||
|
scope.cancel()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onClick() {
|
||||||
|
super.onClick()
|
||||||
|
unlockAndRun {
|
||||||
|
scope.launch {
|
||||||
|
try {
|
||||||
|
appDataRepository.toggleWatcherServicePause()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Timber.e(e.message)
|
||||||
|
} finally {
|
||||||
|
cancel()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setActive() {
|
||||||
|
qsTile.state = Tile.STATE_ACTIVE
|
||||||
|
qsTile.updateTile()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setInactive() {
|
||||||
|
qsTile.state = Tile.STATE_INACTIVE
|
||||||
|
qsTile.updateTile()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setUnavailable() {
|
||||||
|
manualStartConfig = null
|
||||||
|
qsTile.state = Tile.STATE_UNAVAILABLE
|
||||||
|
qsTile.updateTile()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setTileDescription(description: String) {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||||
|
qsTile.subtitle = description
|
||||||
|
}
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||||
|
qsTile.stateDescription = description
|
||||||
|
}
|
||||||
|
qsTile.updateTile()
|
||||||
|
}
|
||||||
|
}
|
||||||
+38
-47
@@ -3,10 +3,8 @@ package com.zaneschepke.wireguardautotunnel.service.tile
|
|||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.service.quicksettings.Tile
|
import android.service.quicksettings.Tile
|
||||||
import android.service.quicksettings.TileService
|
import android.service.quicksettings.TileService
|
||||||
import com.wireguard.android.backend.Tunnel
|
|
||||||
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
|
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
|
||||||
import com.zaneschepke.wireguardautotunnel.data.repository.SettingsRepository
|
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
|
||||||
import com.zaneschepke.wireguardautotunnel.data.repository.TunnelConfigRepository
|
|
||||||
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 dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
@@ -14,44 +12,49 @@ import kotlinx.coroutines.CoroutineScope
|
|||||||
import kotlinx.coroutines.Dispatchers
|
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
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class TunnelControlTile() : TileService() {
|
class TunnelControlTile : TileService() {
|
||||||
|
|
||||||
@Inject lateinit var tunnelConfigRepository: TunnelConfigRepository
|
@Inject
|
||||||
|
lateinit var appDataRepository: AppDataRepository
|
||||||
|
|
||||||
@Inject lateinit var settingsRepository: SettingsRepository
|
@Inject
|
||||||
|
lateinit var vpnService: VpnService
|
||||||
|
|
||||||
@Inject lateinit var vpnService: VpnService
|
@Inject
|
||||||
|
lateinit var serviceManager: ServiceManager
|
||||||
|
|
||||||
private val scope = CoroutineScope(Dispatchers.IO)
|
private val scope = CoroutineScope(Dispatchers.IO)
|
||||||
|
|
||||||
private var tunnelName: String? = 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")
|
||||||
scope.launch {
|
scope.launch {
|
||||||
vpnService.vpnState.collect {
|
vpnService.vpnState.collect { it ->
|
||||||
when (it.status) {
|
when (it.status) {
|
||||||
Tunnel.State.UP -> setActive()
|
Tunnel.State.UP -> {
|
||||||
Tunnel.State.DOWN -> setInactive()
|
setActive()
|
||||||
|
it.tunnelConfig?.name?.let { name -> setTileDescription(name) }
|
||||||
|
}
|
||||||
|
|
||||||
|
Tunnel.State.DOWN -> {
|
||||||
|
setInactive()
|
||||||
|
val config = appDataRepository.getStartTunnelConfig()?.also { config ->
|
||||||
|
manualStartConfig = config
|
||||||
|
} ?: appDataRepository.getPrimaryOrFirstTunnel()
|
||||||
|
config?.let {
|
||||||
|
setTileDescription(it.name)
|
||||||
|
} ?: setUnavailable()
|
||||||
|
}
|
||||||
|
|
||||||
else -> setInactive()
|
else -> setInactive()
|
||||||
}
|
}
|
||||||
val tunnels = tunnelConfigRepository.getAll()
|
|
||||||
if (tunnels.isEmpty()) {
|
|
||||||
setUnavailable()
|
|
||||||
return@collect
|
|
||||||
}
|
|
||||||
tunnelName = it.name.run {
|
|
||||||
val settings = settingsRepository.getSettings()
|
|
||||||
if (settings.defaultTunnel != null) {
|
|
||||||
TunnelConfig.from(settings.defaultTunnel!!).name
|
|
||||||
} else tunnels.firstOrNull()?.name
|
|
||||||
}
|
|
||||||
setTileDescription(tunnelName ?: "")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -66,23 +69,24 @@ class TunnelControlTile() : TileService() {
|
|||||||
scope.cancel()
|
scope.cancel()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onTileAdded() {
|
||||||
|
super.onTileAdded()
|
||||||
|
onStartListening()
|
||||||
|
}
|
||||||
|
|
||||||
override fun onClick() {
|
override fun onClick() {
|
||||||
super.onClick()
|
super.onClick()
|
||||||
unlockAndRun {
|
unlockAndRun {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
try {
|
try {
|
||||||
val defaultTunnel = settingsRepository.getSettings().defaultTunnel
|
|
||||||
val config = defaultTunnel ?: run {
|
|
||||||
val tunnelConfigs = tunnelConfigRepository.getAll()
|
|
||||||
return@run tunnelConfigs.find { it.name == tunnelName }
|
|
||||||
}
|
|
||||||
toggleWatcherServicePause()
|
|
||||||
if (vpnService.getState() == Tunnel.State.UP) {
|
if (vpnService.getState() == Tunnel.State.UP) {
|
||||||
ServiceManager.stopVpnService(this@TunnelControlTile)
|
serviceManager.stopVpnServiceForeground(
|
||||||
} else {
|
|
||||||
ServiceManager.startVpnServiceForeground(
|
|
||||||
this@TunnelControlTile,
|
this@TunnelControlTile,
|
||||||
config.toString(),
|
isManualStop = true,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
serviceManager.startVpnServiceForeground(
|
||||||
|
this@TunnelControlTile, manualStartConfig?.id, isManualStart = true,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
@@ -94,20 +98,6 @@ class TunnelControlTile() : TileService() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun toggleWatcherServicePause() {
|
|
||||||
scope.launch {
|
|
||||||
val settings = settingsRepository.getSettings()
|
|
||||||
if (settings.isAutoTunnelEnabled) {
|
|
||||||
val pauseAutoTunnel = !settings.isAutoTunnelPaused
|
|
||||||
settingsRepository.save(
|
|
||||||
settings.copy(
|
|
||||||
isAutoTunnelPaused = pauseAutoTunnel,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun setActive() {
|
private fun setActive() {
|
||||||
qsTile.state = Tile.STATE_ACTIVE
|
qsTile.state = Tile.STATE_ACTIVE
|
||||||
qsTile.updateTile()
|
qsTile.updateTile()
|
||||||
@@ -119,6 +109,7 @@ class TunnelControlTile() : TileService() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun setUnavailable() {
|
private fun setUnavailable() {
|
||||||
|
manualStartConfig = null
|
||||||
qsTile.state = Tile.STATE_UNAVAILABLE
|
qsTile.state = Tile.STATE_UNAVAILABLE
|
||||||
qsTile.updateTile()
|
qsTile.updateTile()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
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.model.TunnelConfig
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import org.amnezia.awg.backend.Tunnel
|
||||||
|
|
||||||
interface VpnService : Tunnel {
|
interface VpnService : Tunnel {
|
||||||
suspend fun startTunnel(tunnelConfig: TunnelConfig): Tunnel.State
|
suspend fun startTunnel(tunnelConfig: TunnelConfig? = null): Tunnel.State
|
||||||
|
|
||||||
suspend fun stopTunnel()
|
suspend fun stopTunnel()
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,11 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.service.tunnel
|
package com.zaneschepke.wireguardautotunnel.service.tunnel
|
||||||
|
|
||||||
import com.wireguard.android.backend.Statistics
|
|
||||||
import com.wireguard.android.backend.Tunnel
|
|
||||||
import com.wireguard.config.Config
|
|
||||||
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
|
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
|
||||||
|
import org.amnezia.awg.backend.Statistics
|
||||||
|
import org.amnezia.awg.backend.Tunnel
|
||||||
|
|
||||||
data class VpnState(
|
data class VpnState(
|
||||||
val status: Tunnel.State = Tunnel.State.DOWN,
|
val status: Tunnel.State = Tunnel.State.DOWN,
|
||||||
val name: String = "",
|
val tunnelConfig: TunnelConfig? = null,
|
||||||
val config: Config? = null,
|
|
||||||
val statistics: Statistics? = null
|
val statistics: Statistics? = null
|
||||||
)
|
)
|
||||||
|
|||||||
+37
-50
@@ -1,16 +1,12 @@
|
|||||||
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.Statistics
|
|
||||||
import com.wireguard.android.backend.Tunnel.State
|
|
||||||
import com.wireguard.config.Config
|
|
||||||
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
|
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
|
||||||
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
|
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
|
||||||
import com.zaneschepke.wireguardautotunnel.data.repository.SettingsRepository
|
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
|
||||||
import com.zaneschepke.wireguardautotunnel.module.Kernel
|
import com.zaneschepke.wireguardautotunnel.module.Kernel
|
||||||
import com.zaneschepke.wireguardautotunnel.module.Userspace
|
import com.zaneschepke.wireguardautotunnel.module.Userspace
|
||||||
import com.zaneschepke.wireguardautotunnel.util.Constants
|
import com.zaneschepke.wireguardautotunnel.util.Constants
|
||||||
|
import kotlinx.coroutines.CancellationException
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
@@ -19,24 +15,26 @@ import kotlinx.coroutines.flow.MutableStateFlow
|
|||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import org.amnezia.awg.backend.Backend
|
||||||
|
import org.amnezia.awg.backend.BackendException
|
||||||
|
import org.amnezia.awg.backend.Statistics
|
||||||
|
import org.amnezia.awg.backend.Tunnel
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
class WireGuardTunnel
|
class WireGuardTunnel
|
||||||
@Inject
|
@Inject
|
||||||
constructor(
|
constructor(
|
||||||
@Userspace private val userspaceBackend : Backend,
|
@Userspace private val userspaceBackend: Backend,
|
||||||
@Kernel private val kernelBackend: Backend,
|
@Kernel private val kernelBackend: Backend,
|
||||||
private val settingsRepository: SettingsRepository
|
private val appDataRepository: AppDataRepository,
|
||||||
) : 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 val scope = CoroutineScope(Dispatchers.IO)
|
||||||
|
|
||||||
private lateinit var statsJob: Job
|
private var statsJob: Job? = null
|
||||||
|
|
||||||
private var config: Config? = null
|
|
||||||
|
|
||||||
private var backend: Backend = userspaceBackend
|
private var backend: Backend = userspaceBackend
|
||||||
|
|
||||||
@@ -44,7 +42,7 @@ constructor(
|
|||||||
|
|
||||||
init {
|
init {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
settingsRepository.getSettingsFlow().collect {
|
appDataRepository.settings.getSettingsFlow().collect {
|
||||||
if (it.isKernelEnabled && backendIsUserspace) {
|
if (it.isKernelEnabled && backendIsUserspace) {
|
||||||
Timber.d("Setting kernel backend")
|
Timber.d("Setting kernel backend")
|
||||||
backend = kernelBackend
|
backend = kernelBackend
|
||||||
@@ -58,27 +56,28 @@ constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun startTunnel(tunnelConfig: TunnelConfig): State {
|
override suspend fun startTunnel(tunnelConfig: TunnelConfig?): Tunnel.State {
|
||||||
return try {
|
return try {
|
||||||
stopTunnelOnConfigChange(tunnelConfig)
|
//TODO we need better error handling here
|
||||||
emitTunnelName(tunnelConfig.name)
|
val config = tunnelConfig ?: appDataRepository.getPrimaryOrFirstTunnel()
|
||||||
config = TunnelConfig.configFromQuick(tunnelConfig.wgQuick)
|
if (config != null) {
|
||||||
emitTunnelConfig(config)
|
emitTunnelConfig(config)
|
||||||
|
val wgConfig = TunnelConfig.configFromQuick(config.wgQuick)
|
||||||
val state =
|
val state =
|
||||||
backend.setState(
|
backend.setState(
|
||||||
this,
|
this,
|
||||||
State.UP,
|
Tunnel.State.UP,
|
||||||
config,
|
wgConfig,
|
||||||
)
|
)
|
||||||
emitTunnelState(state)
|
|
||||||
state
|
state
|
||||||
|
} else throw Exception("No tunnels")
|
||||||
} catch (e: BackendException) {
|
} catch (e: BackendException) {
|
||||||
Timber.e("Failed to start tunnel with error: ${e.message}")
|
Timber.e("Failed to start tunnel with error: ${e.message}")
|
||||||
State.DOWN
|
Tunnel.State.DOWN
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun emitTunnelState(state: State) {
|
private fun emitTunnelState(state: Tunnel.State) {
|
||||||
_vpnState.tryEmit(
|
_vpnState.tryEmit(
|
||||||
_vpnState.value.copy(
|
_vpnState.value.copy(
|
||||||
status = state,
|
status = state,
|
||||||
@@ -94,36 +93,18 @@ constructor(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun emitTunnelName(name: String) {
|
private suspend fun emitTunnelConfig(tunnelConfig: TunnelConfig?) {
|
||||||
_vpnState.emit(
|
_vpnState.emit(
|
||||||
_vpnState.value.copy(
|
_vpnState.value.copy(
|
||||||
name = name,
|
tunnelConfig = tunnelConfig,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun emitTunnelConfig(config : Config?) {
|
|
||||||
_vpnState.emit(
|
|
||||||
_vpnState.value.copy(
|
|
||||||
config = config,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun stopTunnelOnConfigChange(tunnelConfig: TunnelConfig) {
|
|
||||||
if (getState() == State.UP && _vpnState.value.name != tunnelConfig.name) {
|
|
||||||
stopTunnel()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getName(): String {
|
|
||||||
return _vpnState.value.name
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun stopTunnel() {
|
override suspend fun stopTunnel() {
|
||||||
try {
|
try {
|
||||||
if (getState() == State.UP) {
|
if (getState() == Tunnel.State.UP) {
|
||||||
val state = backend.setState(this, State.DOWN, null)
|
val state = backend.setState(this, Tunnel.State.DOWN, null)
|
||||||
emitTunnelState(state)
|
emitTunnelState(state)
|
||||||
}
|
}
|
||||||
} catch (e: BackendException) {
|
} catch (e: BackendException) {
|
||||||
@@ -131,15 +112,19 @@ constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getState(): State {
|
override fun getState(): Tunnel.State {
|
||||||
return backend.getState(this)
|
return backend.getState(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onStateChange(state: State) {
|
override fun getName(): String {
|
||||||
|
return _vpnState.value.tunnelConfig?.name ?: ""
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onStateChange(state: Tunnel.State) {
|
||||||
val tunnel = this
|
val tunnel = this
|
||||||
emitTunnelState(state)
|
emitTunnelState(state)
|
||||||
WireGuardAutoTunnel.requestTileServiceStateUpdate(WireGuardAutoTunnel.instance)
|
WireGuardAutoTunnel.requestTunnelTileServiceStateUpdate(WireGuardAutoTunnel.instance)
|
||||||
if (state == State.UP) {
|
if (state == Tunnel.State.UP) {
|
||||||
statsJob =
|
statsJob =
|
||||||
scope.launch {
|
scope.launch {
|
||||||
while (true) {
|
while (true) {
|
||||||
@@ -149,9 +134,11 @@ constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (state == State.DOWN) {
|
if (state == Tunnel.State.DOWN) {
|
||||||
if (this::statsJob.isInitialized) {
|
try {
|
||||||
statsJob.cancel()
|
statsJob?.cancel()
|
||||||
|
} catch (e : CancellationException) {
|
||||||
|
Timber.i("Stats job cancelled")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,12 +8,10 @@ import android.widget.Toast
|
|||||||
import androidx.compose.runtime.mutableStateListOf
|
import androidx.compose.runtime.mutableStateListOf
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import com.wireguard.android.backend.GoBackend
|
|
||||||
import com.zaneschepke.logcatter.Logcatter
|
import com.zaneschepke.logcatter.Logcatter
|
||||||
import com.zaneschepke.logcatter.model.LogMessage
|
import com.zaneschepke.logcatter.model.LogMessage
|
||||||
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.datastore.DataStoreManager
|
|
||||||
import com.zaneschepke.wireguardautotunnel.util.Constants
|
import com.zaneschepke.wireguardautotunnel.util.Constants
|
||||||
import com.zaneschepke.wireguardautotunnel.util.FileUtils
|
import com.zaneschepke.wireguardautotunnel.util.FileUtils
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
@@ -21,7 +19,7 @@ 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.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import org.amnezia.awg.backend.GoBackend
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
@@ -35,27 +33,30 @@ constructor(
|
|||||||
|
|
||||||
val vpnIntent: Intent? = GoBackend.VpnService.prepare(WireGuardAutoTunnel.instance)
|
val vpnIntent: Intent? = GoBackend.VpnService.prepare(WireGuardAutoTunnel.instance)
|
||||||
|
|
||||||
private val _appUiState = MutableStateFlow(AppUiState(
|
private val _appUiState = MutableStateFlow(
|
||||||
vpnPermissionAccepted = vpnIntent == null
|
AppUiState(
|
||||||
))
|
vpnPermissionAccepted = vpnIntent == null,
|
||||||
|
),
|
||||||
|
)
|
||||||
val appUiState = _appUiState.asStateFlow()
|
val appUiState = _appUiState.asStateFlow()
|
||||||
|
|
||||||
|
|
||||||
fun isRequiredPermissionGranted() : Boolean {
|
fun isRequiredPermissionGranted(): Boolean {
|
||||||
val allAccepted = (_appUiState.value.vpnPermissionAccepted && _appUiState.value.vpnPermissionAccepted)
|
val allAccepted =
|
||||||
if(!allAccepted) requestPermissions()
|
(_appUiState.value.vpnPermissionAccepted && _appUiState.value.vpnPermissionAccepted)
|
||||||
|
if (!allAccepted) requestPermissions()
|
||||||
return allAccepted
|
return allAccepted
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun requestPermissions() {
|
private fun requestPermissions() {
|
||||||
_appUiState.value = _appUiState.value.copy(
|
_appUiState.value = _appUiState.value.copy(
|
||||||
requestPermissions = true
|
requestPermissions = true,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun permissionsRequested() {
|
fun permissionsRequested() {
|
||||||
_appUiState.value = _appUiState.value.copy(
|
_appUiState.value = _appUiState.value.copy(
|
||||||
requestPermissions = false
|
requestPermissions = false,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -74,7 +75,7 @@ constructor(
|
|||||||
|
|
||||||
fun onVpnPermissionAccepted() {
|
fun onVpnPermissionAccepted() {
|
||||||
_appUiState.value = _appUiState.value.copy(
|
_appUiState.value = _appUiState.value.copy(
|
||||||
vpnPermissionAccepted = true
|
vpnPermissionAccepted = true,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -90,35 +91,39 @@ constructor(
|
|||||||
application.startActivity(
|
application.startActivity(
|
||||||
Intent.createChooser(intent, application.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(application.getString(R.string.no_email_detected))
|
showSnackbarMessage(application.getString(R.string.no_email_detected))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fun showSnackbarMessage(message : String) {
|
|
||||||
|
fun showSnackbarMessage(message: String) {
|
||||||
_appUiState.value = _appUiState.value.copy(
|
_appUiState.value = _appUiState.value.copy(
|
||||||
snackbarMessage = message,
|
snackbarMessage = message,
|
||||||
snackbarMessageConsumed = false
|
snackbarMessageConsumed = false,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun snackbarMessageConsumed() {
|
fun snackbarMessageConsumed() {
|
||||||
_appUiState.value = _appUiState.value.copy(
|
_appUiState.value = _appUiState.value.copy(
|
||||||
snackbarMessage = "",
|
snackbarMessage = "",
|
||||||
snackbarMessageConsumed = true
|
snackbarMessageConsumed = true,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
val logs = mutableStateListOf<LogMessage>()
|
val logs = mutableStateListOf<LogMessage>()
|
||||||
|
|
||||||
fun readLogCatOutput() = viewModelScope.launch(viewModelScope.coroutineContext + Dispatchers.IO) {
|
fun readLogCatOutput() =
|
||||||
|
viewModelScope.launch(viewModelScope.coroutineContext + Dispatchers.IO) {
|
||||||
launch {
|
launch {
|
||||||
Logcatter.logs {
|
Logcatter.logs(callback = {
|
||||||
logs.add(it)
|
logs.add(it)
|
||||||
if (logs.size > Constants.LOG_BUFFER_SIZE) {
|
if (logs.size > Constants.LOG_BUFFER_SIZE) {
|
||||||
logs.removeRange(0, (logs.size - Constants.LOG_BUFFER_SIZE).toInt())
|
logs.removeRange(0, (logs.size - Constants.LOG_BUFFER_SIZE).toInt())
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -131,12 +136,13 @@ constructor(
|
|||||||
val fileName = "${Constants.BASE_LOG_FILE_NAME}-${Instant.now().epochSecond}.txt"
|
val fileName = "${Constants.BASE_LOG_FILE_NAME}-${Instant.now().epochSecond}.txt"
|
||||||
val content = logs.joinToString(separator = "\n")
|
val content = logs.joinToString(separator = "\n")
|
||||||
FileUtils.saveFileToDownloads(application.applicationContext, content, fileName)
|
FileUtils.saveFileToDownloads(application.applicationContext, content, fileName)
|
||||||
Toast.makeText(application, application.getString(R.string.logs_saved), Toast.LENGTH_SHORT).show()
|
Toast.makeText(application, application.getString(R.string.logs_saved), Toast.LENGTH_SHORT)
|
||||||
|
.show()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setNotificationPermissionAccepted(accepted: Boolean) {
|
fun setNotificationPermissionAccepted(accepted: Boolean) {
|
||||||
_appUiState.value = _appUiState.value.copy(
|
_appUiState.value = _appUiState.value.copy(
|
||||||
notificationPermissionAccepted = accepted
|
notificationPermissionAccepted = accepted,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,14 +19,10 @@ import androidx.compose.material3.SnackbarDuration
|
|||||||
import androidx.compose.material3.SnackbarHost
|
import androidx.compose.material3.SnackbarHost
|
||||||
import androidx.compose.material3.SnackbarHostState
|
import androidx.compose.material3.SnackbarHostState
|
||||||
import androidx.compose.material3.SnackbarResult
|
import androidx.compose.material3.SnackbarResult
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.material3.surfaceColorAtElevation
|
import androidx.compose.material3.surfaceColorAtElevation
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.saveable.rememberSaveable
|
|
||||||
import androidx.compose.runtime.setValue
|
|
||||||
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.focusProperties
|
import androidx.compose.ui.focus.focusProperties
|
||||||
@@ -52,6 +48,7 @@ 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.MainScreen
|
import com.zaneschepke.wireguardautotunnel.ui.screens.main.MainScreen
|
||||||
|
import com.zaneschepke.wireguardautotunnel.ui.screens.options.OptionsScreen
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.screens.pinlock.PinLockScreen
|
import com.zaneschepke.wireguardautotunnel.ui.screens.pinlock.PinLockScreen
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.SettingsScreen
|
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.SettingsScreen
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.screens.support.SupportScreen
|
import com.zaneschepke.wireguardautotunnel.ui.screens.support.SupportScreen
|
||||||
@@ -61,7 +58,6 @@ 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.PinLock
|
|
||||||
import xyz.teamgravity.pin_lock_compose.PinManager
|
import xyz.teamgravity.pin_lock_compose.PinManager
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@@ -74,6 +70,9 @@ class MainActivity : AppCompatActivity() {
|
|||||||
@Inject
|
@Inject
|
||||||
lateinit var settingsRepository: SettingsRepository
|
lateinit var settingsRepository: SettingsRepository
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var serviceManager: ServiceManager
|
||||||
|
|
||||||
@OptIn(
|
@OptIn(
|
||||||
ExperimentalPermissionsApi::class,
|
ExperimentalPermissionsApi::class,
|
||||||
)
|
)
|
||||||
@@ -85,10 +84,10 @@ class MainActivity : AppCompatActivity() {
|
|||||||
// load preferences into memory and init data
|
// load preferences into memory and init data
|
||||||
lifecycleScope.launch {
|
lifecycleScope.launch {
|
||||||
dataStoreManager.init()
|
dataStoreManager.init()
|
||||||
WireGuardAutoTunnel.requestTileServiceStateUpdate(this@MainActivity)
|
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 {
|
||||||
@@ -101,7 +100,6 @@ class MainActivity : AppCompatActivity() {
|
|||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU)
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU)
|
||||||
rememberPermissionState(Manifest.permission.POST_NOTIFICATIONS) else null
|
rememberPermissionState(Manifest.permission.POST_NOTIFICATIONS) else null
|
||||||
|
|
||||||
val focusRequester = remember { FocusRequester() }
|
|
||||||
val snackbarHostState = remember { SnackbarHostState() }
|
val snackbarHostState = remember { SnackbarHostState() }
|
||||||
|
|
||||||
val vpnActivityResultState =
|
val vpnActivityResultState =
|
||||||
@@ -150,7 +148,7 @@ class MainActivity : AppCompatActivity() {
|
|||||||
appViewModel.setNotificationPermissionAccepted(
|
appViewModel.setNotificationPermissionAccepted(
|
||||||
notificationPermissionState?.status?.isGranted ?: true,
|
notificationPermissionState?.status?.isGranted ?: true,
|
||||||
)
|
)
|
||||||
if(!WireGuardAutoTunnel.isRunningOnAndroidTv()) appViewModel.readLogCatOutput()
|
if (!WireGuardAutoTunnel.isRunningOnAndroidTv()) appViewModel.readLogCatOutput()
|
||||||
}
|
}
|
||||||
|
|
||||||
LaunchedEffect(appUiState.snackbarMessageConsumed) {
|
LaunchedEffect(appUiState.snackbarMessageConsumed) {
|
||||||
@@ -160,6 +158,8 @@ class MainActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val focusRequester = remember { FocusRequester() }
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
snackbarHost = {
|
snackbarHost = {
|
||||||
SnackbarHost(snackbarHostState) { snackbarData: SnackbarData ->
|
SnackbarHost(snackbarHostState) { snackbarData: SnackbarData ->
|
||||||
@@ -176,9 +176,11 @@ class MainActivity : AppCompatActivity() {
|
|||||||
//TODO refactor
|
//TODO refactor
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.focusable()
|
.focusable()
|
||||||
.focusProperties { when(navBackStackEntry?.destination?.route) {
|
.focusProperties {
|
||||||
|
when (navBackStackEntry?.destination?.route) {
|
||||||
Screen.Lock.route -> Unit
|
Screen.Lock.route -> Unit
|
||||||
else -> up = focusRequester }
|
else -> up = focusRequester
|
||||||
|
}
|
||||||
},
|
},
|
||||||
bottomBar = {
|
bottomBar = {
|
||||||
BottomNavBar(
|
BottomNavBar(
|
||||||
@@ -215,7 +217,7 @@ class MainActivity : AppCompatActivity() {
|
|||||||
SettingsScreen(
|
SettingsScreen(
|
||||||
appViewModel = appViewModel,
|
appViewModel = appViewModel,
|
||||||
navController = navController,
|
navController = navController,
|
||||||
focusRequester = focusRequester
|
focusRequester = focusRequester,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
composable(
|
composable(
|
||||||
@@ -235,14 +237,28 @@ class MainActivity : AppCompatActivity() {
|
|||||||
if (!id.isNullOrBlank()) {
|
if (!id.isNullOrBlank()) {
|
||||||
ConfigScreen(
|
ConfigScreen(
|
||||||
navController = navController,
|
navController = navController,
|
||||||
id = id,
|
tunnelId = id,
|
||||||
|
appViewModel = appViewModel,
|
||||||
|
focusRequester = focusRequester,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
composable("${Screen.Option.route}/{id}") {
|
||||||
|
val id = it.arguments?.getString("id")
|
||||||
|
if (!id.isNullOrBlank()) {
|
||||||
|
OptionsScreen(
|
||||||
|
navController = navController,
|
||||||
|
tunnelId = id,
|
||||||
appViewModel = appViewModel,
|
appViewModel = appViewModel,
|
||||||
focusRequester = focusRequester,
|
focusRequester = focusRequester,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
composable(Screen.Lock.route) {
|
composable(Screen.Lock.route) {
|
||||||
PinLockScreen(navController = navController, appViewModel = appViewModel)
|
PinLockScreen(
|
||||||
|
navController = navController,
|
||||||
|
appViewModel = appViewModel,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,13 +4,17 @@ 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.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") {
|
||||||
val navItem =
|
val navItem =
|
||||||
BottomNavItem(
|
BottomNavItem(
|
||||||
name = "Tunnels",
|
name = WireGuardAutoTunnel.instance.getString(R.string.tunnels),
|
||||||
route = route,
|
route = route,
|
||||||
icon = Icons.Rounded.Home,
|
icon = Icons.Rounded.Home,
|
||||||
)
|
)
|
||||||
@@ -19,7 +23,7 @@ sealed class Screen(val route: String) {
|
|||||||
data object Settings : Screen("settings") {
|
data object Settings : Screen("settings") {
|
||||||
val navItem =
|
val navItem =
|
||||||
BottomNavItem(
|
BottomNavItem(
|
||||||
name = "Settings",
|
name = WireGuardAutoTunnel.instance.getString(R.string.settings),
|
||||||
route = route,
|
route = route,
|
||||||
icon = Icons.Rounded.Settings,
|
icon = Icons.Rounded.Settings,
|
||||||
)
|
)
|
||||||
@@ -28,13 +32,16 @@ sealed class Screen(val route: String) {
|
|||||||
data object Support : Screen("support") {
|
data object Support : Screen("support") {
|
||||||
val navItem =
|
val navItem =
|
||||||
BottomNavItem(
|
BottomNavItem(
|
||||||
name = "Support",
|
name = WireGuardAutoTunnel.instance.getString(R.string.support),
|
||||||
route = route,
|
route = route,
|
||||||
icon = Icons.Rounded.QuestionMark,
|
icon = Icons.Rounded.QuestionMark,
|
||||||
)
|
)
|
||||||
|
|
||||||
data object Logs : Screen("support/logs")
|
data object Logs : Screen("support/logs")
|
||||||
}
|
}
|
||||||
|
|
||||||
data object Config : Screen("config")
|
data object Config : Screen("config")
|
||||||
data object Lock : Screen("lock")
|
data object Lock : Screen("lock")
|
||||||
|
|
||||||
|
data object Option : Screen("option")
|
||||||
}
|
}
|
||||||
|
|||||||
+4
-3
@@ -10,8 +10,6 @@ import androidx.compose.material3.TextButton
|
|||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.vector.ImageVector
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
import androidx.compose.ui.res.stringResource
|
|
||||||
import com.zaneschepke.wireguardautotunnel.R
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ClickableIconButton(
|
fun ClickableIconButton(
|
||||||
@@ -31,7 +29,10 @@ fun ClickableIconButton(
|
|||||||
imageVector = icon,
|
imageVector = icon,
|
||||||
contentDescription = icon.name,
|
contentDescription = icon.name,
|
||||||
modifier =
|
modifier =
|
||||||
Modifier.size(ButtonDefaults.IconSize).weight(1f, false).clickable {
|
Modifier
|
||||||
|
.size(ButtonDefaults.IconSize)
|
||||||
|
.weight(1f, false)
|
||||||
|
.clickable {
|
||||||
if (enabled) {
|
if (enabled) {
|
||||||
onIconClick()
|
onIconClick()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,9 +17,9 @@ import androidx.compose.ui.Modifier
|
|||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import com.wireguard.android.backend.Statistics
|
|
||||||
import com.zaneschepke.wireguardautotunnel.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
|
||||||
@@ -34,7 +34,8 @@ fun RowListItem(
|
|||||||
) {
|
) {
|
||||||
Box(
|
Box(
|
||||||
modifier =
|
modifier =
|
||||||
Modifier.animateContentSize()
|
Modifier
|
||||||
|
.animateContentSize()
|
||||||
.clip(RoundedCornerShape(30.dp))
|
.clip(RoundedCornerShape(30.dp))
|
||||||
.combinedClickable(
|
.combinedClickable(
|
||||||
onClick = { onClick() },
|
onClick = { onClick() },
|
||||||
@@ -43,7 +44,9 @@ fun RowListItem(
|
|||||||
) {
|
) {
|
||||||
Column {
|
Column {
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier.fillMaxWidth().padding(horizontal = 15.dp, vertical = 5.dp),
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 15.dp, vertical = 5.dp),
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
) {
|
) {
|
||||||
@@ -59,7 +62,8 @@ fun RowListItem(
|
|||||||
statistics?.peers()?.forEach {
|
statistics?.peers()?.forEach {
|
||||||
Row(
|
Row(
|
||||||
modifier =
|
modifier =
|
||||||
Modifier.fillMaxWidth()
|
Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
.padding(end = 10.dp, bottom = 10.dp, start = 10.dp),
|
.padding(end = 10.dp, bottom = 10.dp, start = 10.dp),
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
horizontalArrangement = Arrangement.SpaceEvenly,
|
horizontalArrangement = Arrangement.SpaceEvenly,
|
||||||
|
|||||||
@@ -73,7 +73,8 @@ fun SearchBar(onQuery: (queryString: String) -> Unit) {
|
|||||||
singleLine = true,
|
singleLine = true,
|
||||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Text),
|
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Text),
|
||||||
modifier =
|
modifier =
|
||||||
Modifier.fillMaxWidth()
|
Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
.background(color = MaterialTheme.colorScheme.background, shape = RectangleShape),
|
.background(color = MaterialTheme.colorScheme.background, shape = RectangleShape),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+10
-2
@@ -9,6 +9,7 @@ import androidx.compose.material3.Text
|
|||||||
import androidx.compose.runtime.Composable
|
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.text.style.TextAlign
|
||||||
import androidx.compose.ui.unit.Dp
|
import androidx.compose.ui.unit.Dp
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@@ -21,11 +22,18 @@ fun ConfigurationToggle(
|
|||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier.fillMaxWidth().padding(padding),
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(padding),
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
) {
|
) {
|
||||||
Text(label)
|
Text(label, textAlign = TextAlign.Start, modifier = Modifier
|
||||||
|
.weight(
|
||||||
|
weight = 1.0f,
|
||||||
|
fill = false,
|
||||||
|
),
|
||||||
|
softWrap = true)
|
||||||
Switch(
|
Switch(
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
enabled = enabled,
|
enabled = enabled,
|
||||||
|
|||||||
+2
-2
@@ -30,9 +30,9 @@ fun BottomNavBar(navController: NavController, bottomNavItems: List<BottomNavIte
|
|||||||
}
|
}
|
||||||
|
|
||||||
NavigationBar(
|
NavigationBar(
|
||||||
containerColor = if(!showBottomBar) Color.Transparent else MaterialTheme.colorScheme.background,
|
containerColor = if (!showBottomBar) Color.Transparent else MaterialTheme.colorScheme.background,
|
||||||
) {
|
) {
|
||||||
if(showBottomBar) bottomNavItems.forEach { item ->
|
if (showBottomBar) bottomNavItems.forEach { item ->
|
||||||
val selected = item.route == backStackEntry.value?.destination?.route
|
val selected = item.route == backStackEntry.value?.destination?.route
|
||||||
|
|
||||||
NavigationBarItem(
|
NavigationBarItem(
|
||||||
|
|||||||
+6
@@ -21,26 +21,32 @@ fun AuthorizationPrompt(onSuccess: () -> Unit, onFailure: () -> Unit, onError: (
|
|||||||
onError("Biometrics not available")
|
onError("Biometrics not available")
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED -> {
|
BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED -> {
|
||||||
onError("Biometrics not created")
|
onError("Biometrics not created")
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE -> {
|
BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE -> {
|
||||||
onError("Biometric hardware not found")
|
onError("Biometric hardware not found")
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
BiometricManager.BIOMETRIC_ERROR_SECURITY_UPDATE_REQUIRED -> {
|
BiometricManager.BIOMETRIC_ERROR_SECURITY_UPDATE_REQUIRED -> {
|
||||||
onError("Biometric security update required")
|
onError("Biometric security update required")
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
BiometricManager.BIOMETRIC_ERROR_UNSUPPORTED -> {
|
BiometricManager.BIOMETRIC_ERROR_UNSUPPORTED -> {
|
||||||
onError("Biometrics not supported")
|
onError("Biometrics not supported")
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
BiometricManager.BIOMETRIC_STATUS_UNKNOWN -> {
|
BiometricManager.BIOMETRIC_STATUS_UNKNOWN -> {
|
||||||
onError("Biometrics status unknown")
|
onError("Biometrics status unknown")
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
BiometricManager.BIOMETRIC_SUCCESS -> true
|
BiometricManager.BIOMETRIC_SUCCESS -> true
|
||||||
else -> false
|
else -> false
|
||||||
}
|
}
|
||||||
|
|||||||
+5
-4
@@ -20,10 +20,8 @@ import androidx.compose.ui.Alignment
|
|||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.platform.LocalLayoutDirection
|
import androidx.compose.ui.platform.LocalLayoutDirection
|
||||||
import androidx.compose.ui.res.stringResource
|
|
||||||
import androidx.compose.ui.unit.LayoutDirection
|
import androidx.compose.ui.unit.LayoutDirection
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import com.zaneschepke.wireguardautotunnel.R
|
|
||||||
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
|
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@@ -35,7 +33,8 @@ fun CustomSnackBar(
|
|||||||
Snackbar(
|
Snackbar(
|
||||||
containerColor = containerColor,
|
containerColor = containerColor,
|
||||||
modifier =
|
modifier =
|
||||||
Modifier.fillMaxWidth(
|
Modifier
|
||||||
|
.fillMaxWidth(
|
||||||
if (WireGuardAutoTunnel.isRunningOnAndroidTv()) 1 / 3f else 2 / 3f,
|
if (WireGuardAutoTunnel.isRunningOnAndroidTv()) 1 / 3f else 2 / 3f,
|
||||||
)
|
)
|
||||||
.padding(bottom = 100.dp),
|
.padding(bottom = 100.dp),
|
||||||
@@ -45,7 +44,9 @@ fun CustomSnackBar(
|
|||||||
LocalLayoutDirection provides if (isRtl) LayoutDirection.Rtl else LayoutDirection.Ltr,
|
LocalLayoutDirection provides if (isRtl) LayoutDirection.Rtl else LayoutDirection.Ltr,
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier.width(IntrinsicSize.Max).height(IntrinsicSize.Min),
|
modifier = Modifier
|
||||||
|
.width(IntrinsicSize.Max)
|
||||||
|
.height(IntrinsicSize.Min),
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
horizontalArrangement = Arrangement.Start,
|
horizontalArrangement = Arrangement.Start,
|
||||||
) {
|
) {
|
||||||
|
|||||||
+4
-1
@@ -16,7 +16,10 @@ fun LoadingScreen() {
|
|||||||
Column(
|
Column(
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
verticalArrangement = Arrangement.Top,
|
verticalArrangement = Arrangement.Top,
|
||||||
modifier = Modifier.fillMaxSize().focusable().padding(),
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.focusable()
|
||||||
|
.padding(),
|
||||||
) {
|
) {
|
||||||
Column(modifier = Modifier.padding(120.dp)) { CircularProgressIndicator() }
|
Column(modifier = Modifier.padding(120.dp)) { CircularProgressIndicator() }
|
||||||
}
|
}
|
||||||
|
|||||||
+7
-2
@@ -12,9 +12,14 @@ import androidx.compose.ui.graphics.Color
|
|||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun LogTypeLabel(color : Color, content: @Composable () -> Unit,) {
|
fun LogTypeLabel(color: Color, content: @Composable () -> Unit) {
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier.size(20.dp).clip(RoundedCornerShape(2.dp)).background(color), contentAlignment = Alignment.Center) {
|
modifier = Modifier
|
||||||
|
.size(20.dp)
|
||||||
|
.clip(RoundedCornerShape(2.dp))
|
||||||
|
.background(color),
|
||||||
|
contentAlignment = Alignment.Center,
|
||||||
|
) {
|
||||||
content()
|
content()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -15,7 +15,7 @@ import androidx.compose.ui.unit.sp
|
|||||||
fun SectionTitle(title: String, padding: Dp) {
|
fun SectionTitle(title: String, padding: Dp) {
|
||||||
Text(
|
Text(
|
||||||
title,
|
title,
|
||||||
textAlign = TextAlign.Center,
|
textAlign = TextAlign.Start,
|
||||||
style = TextStyle(fontSize = 18.sp, fontWeight = FontWeight.ExtraBold),
|
style = TextStyle(fontSize = 18.sp, fontWeight = FontWeight.ExtraBold),
|
||||||
modifier = Modifier.padding(padding, bottom = 5.dp, top = 5.dp),
|
modifier = Modifier.padding(padding, bottom = 5.dp, top = 5.dp),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.ui.models
|
package com.zaneschepke.wireguardautotunnel.ui.models
|
||||||
|
|
||||||
import com.wireguard.config.Interface
|
import org.amnezia.awg.config.Interface
|
||||||
|
|
||||||
data class InterfaceProxy(
|
data class InterfaceProxy(
|
||||||
var privateKey: String = "",
|
var privateKey: String = "",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.ui.models
|
package com.zaneschepke.wireguardautotunnel.ui.models
|
||||||
|
|
||||||
import com.wireguard.config.Peer
|
import org.amnezia.awg.config.Peer
|
||||||
|
|
||||||
data class PeerProxy(
|
data class PeerProxy(
|
||||||
var publicKey: String = "",
|
var publicKey: String = "",
|
||||||
|
|||||||
+10
-7
@@ -94,7 +94,7 @@ fun ConfigScreen(
|
|||||||
focusRequester: FocusRequester,
|
focusRequester: FocusRequester,
|
||||||
navController: NavController,
|
navController: NavController,
|
||||||
appViewModel: AppViewModel,
|
appViewModel: AppViewModel,
|
||||||
id: String
|
tunnelId: String
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val clipboardManager: ClipboardManager = LocalClipboardManager.current
|
val clipboardManager: ClipboardManager = LocalClipboardManager.current
|
||||||
@@ -105,7 +105,7 @@ fun ConfigScreen(
|
|||||||
|
|
||||||
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
|
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
|
||||||
|
|
||||||
LaunchedEffect(Unit) { viewModel.init(id) }
|
LaunchedEffect(Unit) { viewModel.init(tunnelId) }
|
||||||
|
|
||||||
LaunchedEffect(uiState.loading) {
|
LaunchedEffect(uiState.loading) {
|
||||||
if (!uiState.loading && WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
if (!uiState.loading && WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
||||||
@@ -173,8 +173,10 @@ fun ConfigScreen(
|
|||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.fillMaxHeight(if (uiState.isAllApplicationsEnabled) 1 / 5f else 4 / 5f),
|
.fillMaxHeight(if (uiState.isAllApplicationsEnabled) 1 / 5f else 4 / 5f),
|
||||||
) {
|
) {
|
||||||
Column(modifier = Modifier
|
Column(
|
||||||
.fillMaxWidth()) {
|
modifier = Modifier
|
||||||
|
.fillMaxWidth(),
|
||||||
|
) {
|
||||||
Row(
|
Row(
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier
|
||||||
@@ -273,7 +275,7 @@ fun ConfigScreen(
|
|||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
checked =
|
checked =
|
||||||
(uiState.checkedPackageNames.contains(
|
(uiState.checkedPackageNames.contains(
|
||||||
pack.packageName
|
pack.packageName,
|
||||||
)),
|
)),
|
||||||
onCheckedChange = {
|
onCheckedChange = {
|
||||||
if (it) {
|
if (it) {
|
||||||
@@ -323,6 +325,7 @@ fun ConfigScreen(
|
|||||||
appViewModel.showSnackbarMessage(it.data.message)
|
appViewModel.showSnackbarMessage(it.data.message)
|
||||||
navController.navigate(Screen.Main.route)
|
navController.navigate(Screen.Main.route)
|
||||||
}
|
}
|
||||||
|
|
||||||
is Result.Error -> appViewModel.showSnackbarMessage(it.error.message)
|
is Result.Error -> appViewModel.showSnackbarMessage(it.error.message)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -390,10 +393,10 @@ fun ConfigScreen(
|
|||||||
.clickable { showAuthPrompt = true },
|
.clickable { showAuthPrompt = true },
|
||||||
value = uiState.interfaceProxy.privateKey,
|
value = uiState.interfaceProxy.privateKey,
|
||||||
visualTransformation =
|
visualTransformation =
|
||||||
if ((id == Constants.MANUAL_TUNNEL_CONFIG_ID) || isAuthenticated)
|
if ((tunnelId == Constants.MANUAL_TUNNEL_CONFIG_ID) || isAuthenticated)
|
||||||
VisualTransformation.None
|
VisualTransformation.None
|
||||||
else PasswordVisualTransformation(),
|
else PasswordVisualTransformation(),
|
||||||
enabled = (id == Constants.MANUAL_TUNNEL_CONFIG_ID) || isAuthenticated,
|
enabled = (tunnelId == Constants.MANUAL_TUNNEL_CONFIG_ID) || isAuthenticated,
|
||||||
onValueChange = { value -> viewModel.onPrivateKeyChange(value) },
|
onValueChange = { value -> viewModel.onPrivateKeyChange(value) },
|
||||||
trailingIcon = {
|
trailingIcon = {
|
||||||
IconButton(
|
IconButton(
|
||||||
|
|||||||
+25
-35
@@ -7,17 +7,9 @@ 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.BadConfigException
|
|
||||||
import com.wireguard.config.Config
|
|
||||||
import com.wireguard.config.Interface
|
|
||||||
import com.wireguard.config.ParseException
|
|
||||||
import com.wireguard.config.Peer
|
|
||||||
import com.wireguard.crypto.Key
|
|
||||||
import com.wireguard.crypto.KeyPair
|
|
||||||
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
|
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
|
||||||
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
|
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
|
||||||
import com.zaneschepke.wireguardautotunnel.data.repository.SettingsRepository
|
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
|
||||||
import com.zaneschepke.wireguardautotunnel.data.repository.TunnelConfigRepository
|
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.models.InterfaceProxy
|
import com.zaneschepke.wireguardautotunnel.ui.models.InterfaceProxy
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.models.PeerProxy
|
import com.zaneschepke.wireguardautotunnel.ui.models.PeerProxy
|
||||||
import com.zaneschepke.wireguardautotunnel.util.Constants
|
import com.zaneschepke.wireguardautotunnel.util.Constants
|
||||||
@@ -30,8 +22,12 @@ import dagger.hilt.android.lifecycle.HiltViewModel
|
|||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
import kotlinx.coroutines.flow.first
|
|
||||||
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
|
||||||
|
|
||||||
@@ -40,8 +36,7 @@ class ConfigViewModel
|
|||||||
@Inject
|
@Inject
|
||||||
constructor(
|
constructor(
|
||||||
private val application: Application,
|
private val application: Application,
|
||||||
private val tunnelConfigRepository: TunnelConfigRepository,
|
private val appDataRepository: AppDataRepository
|
||||||
private val settingsRepository: SettingsRepository,
|
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
|
|
||||||
private val packageManager = application.packageManager
|
private val packageManager = application.packageManager
|
||||||
@@ -55,7 +50,8 @@ constructor(
|
|||||||
val state =
|
val state =
|
||||||
if (tunnelId != Constants.MANUAL_TUNNEL_CONFIG_ID) {
|
if (tunnelId != Constants.MANUAL_TUNNEL_CONFIG_ID) {
|
||||||
val tunnelConfig =
|
val tunnelConfig =
|
||||||
tunnelConfigRepository.getAll().firstOrNull { it.id.toString() == tunnelId }
|
appDataRepository.tunnels.getAll()
|
||||||
|
.firstOrNull { it.id.toString() == tunnelId }
|
||||||
if (tunnelConfig != null) {
|
if (tunnelConfig != null) {
|
||||||
val config = TunnelConfig.configFromQuick(tunnelConfig.wgQuick)
|
val config = TunnelConfig.configFromQuick(tunnelConfig.wgQuick)
|
||||||
val proxyPeers = config.peers.map { PeerProxy.from(it) }
|
val proxyPeers = config.peers.map { PeerProxy.from(it) }
|
||||||
@@ -103,7 +99,7 @@ constructor(
|
|||||||
fun onAddCheckedPackage(packageName: String) {
|
fun onAddCheckedPackage(packageName: String) {
|
||||||
_uiState.value =
|
_uiState.value =
|
||||||
_uiState.value.copy(
|
_uiState.value.copy(
|
||||||
checkedPackageNames = _uiState.value.checkedPackageNames + packageName
|
checkedPackageNames = _uiState.value.checkedPackageNames + packageName,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -114,7 +110,7 @@ constructor(
|
|||||||
fun onRemoveCheckedPackage(packageName: String) {
|
fun onRemoveCheckedPackage(packageName: String) {
|
||||||
_uiState.value =
|
_uiState.value =
|
||||||
_uiState.value.copy(
|
_uiState.value.copy(
|
||||||
checkedPackageNames = _uiState.value.checkedPackageNames - packageName
|
checkedPackageNames = _uiState.value.checkedPackageNames - packageName,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -148,23 +144,13 @@ constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun saveConfig(tunnelConfig: TunnelConfig) =
|
private fun saveConfig(tunnelConfig: TunnelConfig) =
|
||||||
viewModelScope.launch { tunnelConfigRepository.save(tunnelConfig) }
|
viewModelScope.launch { appDataRepository.tunnels.save(tunnelConfig) }
|
||||||
|
|
||||||
private fun updateTunnelConfig(tunnelConfig: TunnelConfig?) =
|
private fun updateTunnelConfig(tunnelConfig: TunnelConfig?) =
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
if (tunnelConfig != null) {
|
if (tunnelConfig != null) {
|
||||||
saveConfig(tunnelConfig).join()
|
saveConfig(tunnelConfig).join()
|
||||||
WireGuardAutoTunnel.requestTileServiceStateUpdate(application)
|
WireGuardAutoTunnel.requestTunnelTileServiceStateUpdate(application)
|
||||||
updateSettingsDefaultTunnel(tunnelConfig)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun updateSettingsDefaultTunnel(tunnelConfig: TunnelConfig) {
|
|
||||||
val settings = settingsRepository.getSettingsFlow().first()
|
|
||||||
if (settings.defaultTunnel != null) {
|
|
||||||
if (tunnelConfig.id == TunnelConfig.from(settings.defaultTunnel!!).id) {
|
|
||||||
settingsRepository.save(settings.copy(defaultTunnel = tunnelConfig.toString()))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -209,11 +195,15 @@ constructor(
|
|||||||
val peerList = buildPeerListFromProxyPeers()
|
val peerList = buildPeerListFromProxyPeers()
|
||||||
val wgInterface = buildInterfaceListFromProxyInterface()
|
val wgInterface = buildInterfaceListFromProxyInterface()
|
||||||
val config = Config.Builder().addPeers(peerList).setInterface(wgInterface).build()
|
val config = Config.Builder().addPeers(peerList).setInterface(wgInterface).build()
|
||||||
val tunnelConfig = when(uiState.value.tunnel) {
|
val tunnelConfig = when (uiState.value.tunnel) {
|
||||||
null -> TunnelConfig(name = _uiState.value.tunnelName, wgQuick = config.toWgQuickString())
|
null -> TunnelConfig(
|
||||||
|
name = _uiState.value.tunnelName,
|
||||||
|
wgQuick = config.toAwgQuickString(),
|
||||||
|
)
|
||||||
|
|
||||||
else -> uiState.value.tunnel!!.copy(
|
else -> uiState.value.tunnel!!.copy(
|
||||||
name = _uiState.value.tunnelName,
|
name = _uiState.value.tunnelName,
|
||||||
wgQuick = config.toWgQuickString(),
|
wgQuick = config.toAwgQuickString(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
updateTunnelConfig(tunnelConfig)
|
updateTunnelConfig(tunnelConfig)
|
||||||
@@ -306,21 +296,21 @@ constructor(
|
|||||||
fun onAddressesChanged(value: String) {
|
fun onAddressesChanged(value: String) {
|
||||||
_uiState.value =
|
_uiState.value =
|
||||||
_uiState.value.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.value =
|
_uiState.value =
|
||||||
_uiState.value.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.value =
|
_uiState.value =
|
||||||
_uiState.value.copy(
|
_uiState.value.copy(
|
||||||
interfaceProxy = _uiState.value.interfaceProxy.copy(dnsServers = value)
|
interfaceProxy = _uiState.value.interfaceProxy.copy(dnsServers = value),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -332,14 +322,14 @@ constructor(
|
|||||||
private fun onInterfacePublicKeyChange(value: String) {
|
private fun onInterfacePublicKeyChange(value: String) {
|
||||||
_uiState.value =
|
_uiState.value =
|
||||||
_uiState.value.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.value =
|
_uiState.value =
|
||||||
_uiState.value.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))
|
||||||
|
|||||||
+61
-102
@@ -38,8 +38,9 @@ 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
|
||||||
import androidx.compose.material.icons.rounded.Delete
|
import androidx.compose.material.icons.rounded.Delete
|
||||||
import androidx.compose.material.icons.rounded.Edit
|
|
||||||
import androidx.compose.material.icons.rounded.Info
|
import androidx.compose.material.icons.rounded.Info
|
||||||
|
import androidx.compose.material.icons.rounded.Settings
|
||||||
|
import androidx.compose.material.icons.rounded.Smartphone
|
||||||
import androidx.compose.material.icons.rounded.Star
|
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
|
||||||
@@ -70,8 +71,12 @@ 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.focus.onFocusChanged
|
||||||
|
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
|
||||||
|
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
|
||||||
|
import androidx.compose.ui.input.nestedscroll.NestedScrollSource
|
||||||
|
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||||
import androidx.compose.ui.input.pointer.pointerInput
|
import androidx.compose.ui.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
|
||||||
@@ -84,7 +89,6 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
|||||||
import androidx.navigation.NavController
|
import androidx.navigation.NavController
|
||||||
import com.journeyapps.barcodescanner.ScanContract
|
import com.journeyapps.barcodescanner.ScanContract
|
||||||
import com.journeyapps.barcodescanner.ScanOptions
|
import com.journeyapps.barcodescanner.ScanOptions
|
||||||
import com.wireguard.android.backend.Tunnel
|
|
||||||
import com.zaneschepke.wireguardautotunnel.R
|
import com.zaneschepke.wireguardautotunnel.R
|
||||||
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
|
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
|
||||||
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
|
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
|
||||||
@@ -105,6 +109,7 @@ import com.zaneschepke.wireguardautotunnel.util.truncateWithEllipsis
|
|||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import org.amnezia.awg.backend.Tunnel
|
||||||
|
|
||||||
@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter")
|
@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter")
|
||||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class)
|
@OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class)
|
||||||
@@ -123,7 +128,25 @@ fun MainScreen(
|
|||||||
val sheetState = rememberModalBottomSheetState()
|
val sheetState = rememberModalBottomSheetState()
|
||||||
var showBottomSheet by remember { mutableStateOf(false) }
|
var showBottomSheet by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
var showPrimaryChangeAlertDialog by remember { mutableStateOf(false) }
|
// Nested scroll for control FAB
|
||||||
|
val nestedScrollConnection = remember {
|
||||||
|
object : NestedScrollConnection {
|
||||||
|
override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
|
||||||
|
// Hide FAB
|
||||||
|
if (available.y < -1) {
|
||||||
|
isVisible.value = false
|
||||||
|
}
|
||||||
|
// Show FAB
|
||||||
|
if (available.y > 1) {
|
||||||
|
isVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
return Offset.Zero
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
var showDeleteTunnelAlertDialog by remember { mutableStateOf(false) }
|
var showDeleteTunnelAlertDialog by remember { mutableStateOf(false) }
|
||||||
var selectedTunnel by remember { mutableStateOf<TunnelConfig?>(null) }
|
var selectedTunnel by remember { mutableStateOf<TunnelConfig?>(null) }
|
||||||
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
|
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
|
||||||
@@ -197,30 +220,6 @@ fun MainScreen(
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
AnimatedVisibility(showPrimaryChangeAlertDialog) {
|
|
||||||
AlertDialog(
|
|
||||||
onDismissRequest = { showPrimaryChangeAlertDialog = false },
|
|
||||||
confirmButton = {
|
|
||||||
TextButton(
|
|
||||||
onClick = {
|
|
||||||
viewModel.onDefaultTunnelChange(selectedTunnel)
|
|
||||||
showPrimaryChangeAlertDialog = false
|
|
||||||
selectedTunnel = null
|
|
||||||
},
|
|
||||||
) {
|
|
||||||
Text(text = stringResource(R.string.okay))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
dismissButton = {
|
|
||||||
TextButton(onClick = { showPrimaryChangeAlertDialog = false }) {
|
|
||||||
Text(text = stringResource(R.string.cancel))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
title = { Text(text = stringResource(R.string.primary_tunnel_change)) },
|
|
||||||
text = { Text(text = stringResource(R.string.primary_tunnel_change_question)) },
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
AnimatedVisibility(showDeleteTunnelAlertDialog) {
|
AnimatedVisibility(showDeleteTunnelAlertDialog) {
|
||||||
AlertDialog(
|
AlertDialog(
|
||||||
onDismissRequest = { showDeleteTunnelAlertDialog = false },
|
onDismissRequest = { showDeleteTunnelAlertDialog = false },
|
||||||
@@ -246,12 +245,12 @@ 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) else viewModel.onTunnelStop()
|
if (checked) viewModel.onTunnelStart(tunnel) else viewModel.onTunnelStop()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if(uiState.loading) {
|
if (uiState.loading) {
|
||||||
return LoadingScreen()
|
return LoadingScreen()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -401,8 +400,8 @@ fun MainScreen(
|
|||||||
verticalArrangement = Arrangement.Top,
|
verticalArrangement = Arrangement.Top,
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxSize()
|
||||||
.overscroll(ScrollableDefaults.overscrollEffect()),
|
.overscroll(ScrollableDefaults.overscrollEffect()).nestedScroll(nestedScrollConnection),
|
||||||
state = rememberLazyListState(0, uiState.tunnels.count()),
|
state = rememberLazyListState(0, uiState.tunnels.count()),
|
||||||
userScrollEnabled = true,
|
userScrollEnabled = true,
|
||||||
reverseLayout = false,
|
reverseLayout = false,
|
||||||
@@ -426,7 +425,7 @@ fun MainScreen(
|
|||||||
icon,
|
icon,
|
||||||
icon.name,
|
icon.name,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(end = 10.dp)
|
.padding(end = 8.5.dp)
|
||||||
.size(25.dp),
|
.size(25.dp),
|
||||||
tint =
|
tint =
|
||||||
if (uiState.settings.isAutoTunnelPaused) Color.Gray
|
if (uiState.settings.isAutoTunnelPaused) Color.Gray
|
||||||
@@ -462,7 +461,7 @@ fun MainScreen(
|
|||||||
) { tunnel ->
|
) { tunnel ->
|
||||||
val leadingIconColor =
|
val leadingIconColor =
|
||||||
(if (
|
(if (
|
||||||
uiState.vpnState.name == tunnel.name &&
|
uiState.vpnState.tunnelConfig?.name == tunnel.name &&
|
||||||
uiState.vpnState.status == Tunnel.State.UP
|
uiState.vpnState.status == Tunnel.State.UP
|
||||||
) {
|
) {
|
||||||
uiState.vpnState.statistics
|
uiState.vpnState.statistics
|
||||||
@@ -486,31 +485,31 @@ fun MainScreen(
|
|||||||
val expanded = remember { mutableStateOf(false) }
|
val expanded = remember { mutableStateOf(false) }
|
||||||
RowListItem(
|
RowListItem(
|
||||||
icon = {
|
icon = {
|
||||||
if (uiState.settings.isTunnelConfigDefault(tunnel)) {
|
val circleIcon = Icons.Rounded.Circle
|
||||||
Icon(
|
val icon = if (tunnel.isPrimaryTunnel) {
|
||||||
Icons.Rounded.Star,
|
Icons.Rounded.Star
|
||||||
stringResource(R.string.status),
|
} else if (tunnel.isMobileDataTunnel) {
|
||||||
tint = leadingIconColor,
|
Icons.Rounded.Smartphone
|
||||||
modifier = Modifier
|
|
||||||
.padding(end = 10.dp)
|
|
||||||
.size(20.dp),
|
|
||||||
)
|
|
||||||
} else {
|
} else {
|
||||||
|
circleIcon
|
||||||
|
}
|
||||||
Icon(
|
Icon(
|
||||||
Icons.Rounded.Circle,
|
icon,
|
||||||
stringResource(R.string.status),
|
icon.name,
|
||||||
tint = leadingIconColor,
|
tint = leadingIconColor,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(end = 15.dp)
|
.padding(
|
||||||
.size(15.dp),
|
end = if (icon == circleIcon) 12.5.dp else 10.dp,
|
||||||
|
start = if (icon == circleIcon) 2.5.dp else 0.dp,
|
||||||
|
)
|
||||||
|
.size(if (icon == circleIcon) 15.dp else 20.dp),
|
||||||
)
|
)
|
||||||
}
|
|
||||||
},
|
},
|
||||||
text = tunnel.name.truncateWithEllipsis(15),
|
text = tunnel.name.truncateWithEllipsis(Constants.ALLOWED_DISPLAY_NAME_LENGTH),
|
||||||
onHold = {
|
onHold = {
|
||||||
if (
|
if (
|
||||||
(uiState.vpnState.status == Tunnel.State.UP) &&
|
(uiState.vpnState.status == Tunnel.State.UP) &&
|
||||||
(tunnel.name == uiState.vpnState.name)
|
(tunnel.name == uiState.vpnState.tunnelConfig?.name)
|
||||||
) {
|
) {
|
||||||
appViewModel.showSnackbarMessage(Event.Message.TunnelOffAction.message)
|
appViewModel.showSnackbarMessage(Event.Message.TunnelOffAction.message)
|
||||||
return@RowListItem
|
return@RowListItem
|
||||||
@@ -522,7 +521,7 @@ fun MainScreen(
|
|||||||
if (!WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
if (!WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
||||||
if (
|
if (
|
||||||
uiState.vpnState.status == Tunnel.State.UP &&
|
uiState.vpnState.status == Tunnel.State.UP &&
|
||||||
(uiState.vpnState.name == tunnel.name)
|
(uiState.vpnState.tunnelConfig?.name == tunnel.name)
|
||||||
) {
|
) {
|
||||||
expanded.value = !expanded.value
|
expanded.value = !expanded.value
|
||||||
}
|
}
|
||||||
@@ -539,7 +538,6 @@ fun MainScreen(
|
|||||||
!WireGuardAutoTunnel.isRunningOnAndroidTv()
|
!WireGuardAutoTunnel.isRunningOnAndroidTv()
|
||||||
) {
|
) {
|
||||||
Row {
|
Row {
|
||||||
if (!uiState.settings.isTunnelConfigDefault(tunnel)) {
|
|
||||||
IconButton(
|
IconButton(
|
||||||
onClick = {
|
onClick = {
|
||||||
if (
|
if (
|
||||||
@@ -550,38 +548,18 @@ fun MainScreen(
|
|||||||
Event.Message.AutoTunnelOffAction.message,
|
Event.Message.AutoTunnelOffAction.message,
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
showPrimaryChangeAlertDialog = true
|
navController.navigate(
|
||||||
|
"${Screen.Option.route}/${selectedTunnel?.id}",
|
||||||
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
) {
|
) {
|
||||||
val icon = Icons.Rounded.Star
|
val icon = Icons.Rounded.Settings
|
||||||
Icon(
|
Icon(
|
||||||
icon,
|
icon,
|
||||||
icon.name,
|
icon.name,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
IconButton(
|
|
||||||
onClick = {
|
|
||||||
if (
|
|
||||||
uiState.settings.isAutoTunnelEnabled &&
|
|
||||||
uiState.settings.isTunnelConfigDefault(
|
|
||||||
tunnel,
|
|
||||||
) &&
|
|
||||||
!uiState.settings.isAutoTunnelPaused
|
|
||||||
) {
|
|
||||||
appViewModel.showSnackbarMessage(
|
|
||||||
Event.Message.AutoTunnelOffAction.message,
|
|
||||||
)
|
|
||||||
} else
|
|
||||||
navController.navigate(
|
|
||||||
"${Screen.Config.route}/${selectedTunnel?.id}",
|
|
||||||
)
|
|
||||||
},
|
|
||||||
) {
|
|
||||||
val icon = Icons.Rounded.Edit
|
|
||||||
Icon(icon, icon.name)
|
|
||||||
}
|
|
||||||
IconButton(
|
IconButton(
|
||||||
modifier = Modifier.focusable(),
|
modifier = Modifier.focusable(),
|
||||||
onClick = { viewModel.onCopyTunnel(selectedTunnel) },
|
onClick = { viewModel.onCopyTunnel(selectedTunnel) },
|
||||||
@@ -601,7 +579,7 @@ fun MainScreen(
|
|||||||
val checked by remember {
|
val checked by remember {
|
||||||
derivedStateOf {
|
derivedStateOf {
|
||||||
(uiState.vpnState.status == Tunnel.State.UP &&
|
(uiState.vpnState.status == Tunnel.State.UP &&
|
||||||
tunnel.name == uiState.vpnState.name)
|
tunnel.name == uiState.vpnState.tunnelConfig?.name)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!checked) expanded.value = false
|
if (!checked) expanded.value = false
|
||||||
@@ -618,7 +596,6 @@ fun MainScreen(
|
|||||||
)
|
)
|
||||||
if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
||||||
Row {
|
Row {
|
||||||
if (!uiState.settings.isTunnelConfigDefault(tunnel)) {
|
|
||||||
IconButton(
|
IconButton(
|
||||||
onClick = {
|
onClick = {
|
||||||
if (uiState.settings.isAutoTunnelEnabled) {
|
if (uiState.settings.isAutoTunnelEnabled) {
|
||||||
@@ -627,23 +604,24 @@ fun MainScreen(
|
|||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
selectedTunnel = tunnel
|
selectedTunnel = tunnel
|
||||||
showPrimaryChangeAlertDialog = true
|
navController.navigate(
|
||||||
|
"${Screen.Option.route}/${selectedTunnel?.id}",
|
||||||
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
) {
|
) {
|
||||||
val icon = Icons.Rounded.Star
|
val icon = Icons.Rounded.Settings
|
||||||
Icon(
|
Icon(
|
||||||
icon,
|
icon,
|
||||||
icon.name,
|
icon.name,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
IconButton(
|
IconButton(
|
||||||
modifier = Modifier.focusRequester(focusRequester),
|
modifier = Modifier.focusRequester(focusRequester),
|
||||||
onClick = {
|
onClick = {
|
||||||
if (
|
if (
|
||||||
uiState.vpnState.status == Tunnel.State.UP &&
|
uiState.vpnState.status == Tunnel.State.UP &&
|
||||||
(uiState.vpnState.name == tunnel.name)
|
(uiState.vpnState.tunnelConfig?.name == tunnel.name)
|
||||||
) {
|
) {
|
||||||
expanded.value = !expanded.value
|
expanded.value = !expanded.value
|
||||||
} else {
|
} else {
|
||||||
@@ -656,25 +634,6 @@ fun MainScreen(
|
|||||||
val icon = Icons.Rounded.Info
|
val icon = Icons.Rounded.Info
|
||||||
Icon(icon, icon.name)
|
Icon(icon, icon.name)
|
||||||
}
|
}
|
||||||
IconButton(
|
|
||||||
onClick = {
|
|
||||||
if (
|
|
||||||
uiState.vpnState.status == Tunnel.State.UP &&
|
|
||||||
tunnel.name == uiState.vpnState.name
|
|
||||||
) {
|
|
||||||
appViewModel.showSnackbarMessage(
|
|
||||||
Event.Message.TunnelOffAction.message,
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
navController.navigate(
|
|
||||||
"${Screen.Config.route}/${tunnel.id}",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
) {
|
|
||||||
val icon = Icons.Rounded.Edit
|
|
||||||
Icon(icon, icon.name)
|
|
||||||
}
|
|
||||||
IconButton(
|
IconButton(
|
||||||
onClick = { viewModel.onCopyTunnel(tunnel) },
|
onClick = { viewModel.onCopyTunnel(tunnel) },
|
||||||
) {
|
) {
|
||||||
@@ -685,7 +644,7 @@ fun MainScreen(
|
|||||||
onClick = {
|
onClick = {
|
||||||
if (
|
if (
|
||||||
uiState.vpnState.status == Tunnel.State.UP &&
|
uiState.vpnState.status == Tunnel.State.UP &&
|
||||||
tunnel.name == uiState.vpnState.name
|
tunnel.name == uiState.vpnState.tunnelConfig?.name
|
||||||
) {
|
) {
|
||||||
appViewModel.showSnackbarMessage(
|
appViewModel.showSnackbarMessage(
|
||||||
Event.Message.TunnelOffAction.message,
|
Event.Message.TunnelOffAction.message,
|
||||||
@@ -699,7 +658,7 @@ fun MainScreen(
|
|||||||
val icon = Icons.Rounded.Delete
|
val icon = Icons.Rounded.Delete
|
||||||
Icon(
|
Icon(
|
||||||
icon,
|
icon,
|
||||||
icon.name
|
icon.name,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
TunnelSwitch()
|
TunnelSwitch()
|
||||||
|
|||||||
+46
-52
@@ -7,13 +7,10 @@ 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.google.zxing.common.StringUtils
|
|
||||||
import com.wireguard.config.Config
|
|
||||||
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
|
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
|
||||||
import com.zaneschepke.wireguardautotunnel.data.model.Settings
|
import com.zaneschepke.wireguardautotunnel.data.model.Settings
|
||||||
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
|
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
|
||||||
import com.zaneschepke.wireguardautotunnel.data.repository.SettingsRepository
|
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
|
||||||
import com.zaneschepke.wireguardautotunnel.data.repository.TunnelConfigRepository
|
|
||||||
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
|
||||||
@@ -22,13 +19,12 @@ import com.zaneschepke.wireguardautotunnel.util.NumberUtils
|
|||||||
import com.zaneschepke.wireguardautotunnel.util.Result
|
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.Dispatchers
|
||||||
import kotlinx.coroutines.async
|
|
||||||
import kotlinx.coroutines.delay
|
|
||||||
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
|
||||||
@@ -39,15 +35,15 @@ class MainViewModel
|
|||||||
@Inject
|
@Inject
|
||||||
constructor(
|
constructor(
|
||||||
private val application: Application,
|
private val application: Application,
|
||||||
private val tunnelConfigRepository: TunnelConfigRepository,
|
private val appDataRepository: AppDataRepository,
|
||||||
private val settingsRepository: SettingsRepository,
|
private val serviceManager: ServiceManager,
|
||||||
private val vpnService: VpnService
|
val vpnService: VpnService
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
|
|
||||||
val uiState =
|
val uiState =
|
||||||
combine(
|
combine(
|
||||||
settingsRepository.getSettingsFlow(),
|
appDataRepository.settings.getSettingsFlow(),
|
||||||
tunnelConfigRepository.getTunnelConfigsFlow(),
|
appDataRepository.tunnels.getTunnelConfigsFlow(),
|
||||||
vpnService.vpnState,
|
vpnService.vpnState,
|
||||||
) { settings, tunnels, vpnState ->
|
) { settings, tunnels, vpnState ->
|
||||||
MainUiState(settings, tunnels, vpnState, false)
|
MainUiState(settings, tunnels, vpnState, false)
|
||||||
@@ -60,48 +56,46 @@ constructor(
|
|||||||
|
|
||||||
private fun stopWatcherService() =
|
private fun stopWatcherService() =
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
ServiceManager.stopWatcherService(application.applicationContext)
|
serviceManager.stopWatcherService(application.applicationContext)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onDelete(tunnel: TunnelConfig) {
|
fun onDelete(tunnel: TunnelConfig) {
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
val settings = settingsRepository.getSettings()
|
val settings = appDataRepository.settings.getSettings()
|
||||||
val isDefault = settings.isTunnelConfigDefault(tunnel)
|
val isPrimary = tunnel.isPrimaryTunnel
|
||||||
if (tunnelConfigRepository.count() == 1 || isDefault) {
|
if (appDataRepository.tunnels.count() == 1 || isPrimary) {
|
||||||
stopWatcherService()
|
stopWatcherService()
|
||||||
settings.defaultTunnel = null
|
resetTunnelSetting(settings)
|
||||||
settings.isAutoTunnelEnabled = false
|
|
||||||
settings.isAlwaysOnVpnEnabled = false
|
|
||||||
saveSettings(settings)
|
|
||||||
}
|
}
|
||||||
tunnelConfigRepository.delete(tunnel)
|
appDataRepository.tunnels.delete(tunnel)
|
||||||
WireGuardAutoTunnel.requestTileServiceStateUpdate(application)
|
WireGuardAutoTunnel.requestTunnelTileServiceStateUpdate(application)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun resetTunnelSetting(settings: Settings) {
|
||||||
|
saveSettings(
|
||||||
|
settings.copy(
|
||||||
|
isAutoTunnelEnabled = false,
|
||||||
|
isAlwaysOnVpnEnabled = false,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
fun onTunnelStart(tunnelConfig: TunnelConfig) =
|
fun onTunnelStart(tunnelConfig: TunnelConfig) =
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
Timber.d("On start called!")
|
Timber.d("On start called!")
|
||||||
stopActiveTunnel().await()
|
serviceManager.startVpnService(
|
||||||
startTunnel(tunnelConfig)
|
application.applicationContext,
|
||||||
|
tunnelConfig.id,
|
||||||
|
isManualStart = true,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun startTunnel(tunnelConfig: TunnelConfig) =
|
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
|
||||||
Timber.d("Start tunnel via manager")
|
|
||||||
ServiceManager.startVpnService(application.applicationContext, tunnelConfig.toString())
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun stopActiveTunnel() =
|
|
||||||
viewModelScope.async(Dispatchers.IO) {
|
|
||||||
onTunnelStop()
|
|
||||||
delay(Constants.TOGGLE_TUNNEL_DELAY)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onTunnelStop() =
|
fun onTunnelStop() =
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
Timber.i("Stopping active tunnel")
|
Timber.i("Stopping active tunnel")
|
||||||
ServiceManager.stopVpnService(application.applicationContext)
|
serviceManager.stopVpnService(application.applicationContext, isManualStop = true)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun validateConfigString(config: String) {
|
private fun validateConfigString(config: String) {
|
||||||
@@ -125,7 +119,7 @@ constructor(
|
|||||||
val bufferReader = stream.bufferedReader(charset = Charsets.UTF_8)
|
val bufferReader = stream.bufferedReader(charset = Charsets.UTF_8)
|
||||||
val config = Config.parse(bufferReader)
|
val config = Config.parse(bufferReader)
|
||||||
val tunnelName = getNameFromFileName(fileName)
|
val tunnelName = getNameFromFileName(fileName)
|
||||||
addTunnel(TunnelConfig(name = tunnelName, wgQuick = config.toWgQuickString()))
|
addTunnel(TunnelConfig(name = tunnelName, wgQuick = config.toAwgQuickString()))
|
||||||
withContext(Dispatchers.IO) { stream.close() }
|
withContext(Dispatchers.IO) { stream.close() }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -145,6 +139,7 @@ constructor(
|
|||||||
is Result.Success -> return it
|
is Result.Success -> return it
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Constants.ZIP_FILE_EXTENSION -> saveTunnelsFromZipUri(uri)
|
Constants.ZIP_FILE_EXTENSION -> saveTunnelsFromZipUri(uri)
|
||||||
else -> return Result.Error(Event.Error.InvalidFileExtension)
|
else -> return Result.Error(Event.Error.InvalidFileExtension)
|
||||||
}
|
}
|
||||||
@@ -169,7 +164,7 @@ constructor(
|
|||||||
val name = getNameFromFileName(it.name)
|
val name = getNameFromFileName(it.name)
|
||||||
val config = Config.parse(zip)
|
val config = Config.parse(zip)
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
addTunnel(TunnelConfig(name = name, wgQuick = config.toWgQuickString()))
|
addTunnel(TunnelConfig(name = name, wgQuick = config.toAwgQuickString()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -186,23 +181,25 @@ constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun addTunnel(tunnelConfig: TunnelConfig) {
|
private suspend fun addTunnel(tunnelConfig: TunnelConfig) {
|
||||||
val firstTunnel = tunnelConfigRepository.count() == 0
|
val firstTunnel = appDataRepository.tunnels.count() == 0
|
||||||
saveTunnel(tunnelConfig)
|
saveTunnel(tunnelConfig)
|
||||||
if(firstTunnel) WireGuardAutoTunnel.requestTileServiceStateUpdate(application)
|
if (firstTunnel) WireGuardAutoTunnel.requestTunnelTileServiceStateUpdate(application)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun pauseAutoTunneling() =
|
fun pauseAutoTunneling() =
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
settingsRepository.save(uiState.value.settings.copy(isAutoTunnelPaused = true))
|
appDataRepository.settings.save(uiState.value.settings.copy(isAutoTunnelPaused = true))
|
||||||
|
WireGuardAutoTunnel.requestAutoTunnelTileServiceUpdate(application)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun resumeAutoTunneling() =
|
fun resumeAutoTunneling() =
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
settingsRepository.save(uiState.value.settings.copy(isAutoTunnelPaused = false))
|
appDataRepository.settings.save(uiState.value.settings.copy(isAutoTunnelPaused = false))
|
||||||
|
WireGuardAutoTunnel.requestAutoTunnelTileServiceUpdate(application)
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun saveTunnel(tunnelConfig: TunnelConfig) {
|
private suspend fun saveTunnel(tunnelConfig: TunnelConfig) {
|
||||||
tunnelConfigRepository.save(tunnelConfig)
|
appDataRepository.tunnels.save(tunnelConfig)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getFileNameByCursor(context: Context, uri: Uri): String? {
|
private fun getFileNameByCursor(context: Context, uri: Uri): String? {
|
||||||
@@ -252,20 +249,17 @@ constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun saveSettings(settings: Settings) =
|
private fun saveSettings(settings: Settings) =
|
||||||
viewModelScope.launch(Dispatchers.IO) { settingsRepository.save(settings) }
|
viewModelScope.launch(Dispatchers.IO) { appDataRepository.settings.save(settings) }
|
||||||
|
|
||||||
fun onDefaultTunnelChange(selectedTunnel: TunnelConfig?) =
|
|
||||||
viewModelScope.launch {
|
|
||||||
if (selectedTunnel != null) {
|
|
||||||
saveSettings(uiState.value.settings.copy(defaultTunnel = selectedTunnel.toString()))
|
|
||||||
.join()
|
|
||||||
WireGuardAutoTunnel.requestTileServiceStateUpdate(application)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onCopyTunnel(tunnel: TunnelConfig?) = viewModelScope.launch {
|
fun onCopyTunnel(tunnel: TunnelConfig?) = viewModelScope.launch {
|
||||||
tunnel?.let {
|
tunnel?.let {
|
||||||
saveTunnel(TunnelConfig(name = it.name.plus(NumberUtils.randomThree()), wgQuick = it.wgQuick))
|
saveTunnel(
|
||||||
|
TunnelConfig(
|
||||||
|
name = it.name.plus(NumberUtils.randomThree()),
|
||||||
|
wgQuick = it.wgQuick,
|
||||||
|
),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+287
@@ -0,0 +1,287 @@
|
|||||||
|
package com.zaneschepke.wireguardautotunnel.ui.screens.options
|
||||||
|
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
||||||
|
import androidx.compose.foundation.layout.FlowRow
|
||||||
|
import androidx.compose.foundation.layout.IntrinsicSize
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.foundation.text.KeyboardActions
|
||||||
|
import androidx.compose.foundation.text.KeyboardOptions
|
||||||
|
import androidx.compose.foundation.verticalScroll
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.Close
|
||||||
|
import androidx.compose.material.icons.outlined.Add
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.IconButton
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.OutlinedTextField
|
||||||
|
import androidx.compose.material3.Surface
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TextButton
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.focus.FocusRequester
|
||||||
|
import androidx.compose.ui.focus.focusRequester
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.platform.LocalFocusManager
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.font.FontStyle
|
||||||
|
import androidx.compose.ui.text.input.ImeAction
|
||||||
|
import androidx.compose.ui.text.input.KeyboardCapitalization
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
|
import androidx.navigation.NavController
|
||||||
|
import com.zaneschepke.wireguardautotunnel.R
|
||||||
|
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
|
||||||
|
import com.zaneschepke.wireguardautotunnel.ui.AppViewModel
|
||||||
|
import com.zaneschepke.wireguardautotunnel.ui.Screen
|
||||||
|
import com.zaneschepke.wireguardautotunnel.ui.common.ClickableIconButton
|
||||||
|
import com.zaneschepke.wireguardautotunnel.ui.common.config.ConfigurationToggle
|
||||||
|
import com.zaneschepke.wireguardautotunnel.ui.common.text.SectionTitle
|
||||||
|
import com.zaneschepke.wireguardautotunnel.util.Constants
|
||||||
|
import com.zaneschepke.wireguardautotunnel.util.Result
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
@OptIn(ExperimentalLayoutApi::class)
|
||||||
|
@Composable
|
||||||
|
fun OptionsScreen(
|
||||||
|
optionsViewModel: OptionsViewModel = hiltViewModel(),
|
||||||
|
navController: NavController,
|
||||||
|
appViewModel: AppViewModel,
|
||||||
|
focusRequester: FocusRequester,
|
||||||
|
tunnelId: String
|
||||||
|
) {
|
||||||
|
val scrollState = rememberScrollState()
|
||||||
|
val uiState by optionsViewModel.uiState.collectAsStateWithLifecycle()
|
||||||
|
val interactionSource = remember { MutableInteractionSource() }
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
|
val focusManager = LocalFocusManager.current
|
||||||
|
val screenPadding = 5.dp
|
||||||
|
val fillMaxWidth = .85f
|
||||||
|
|
||||||
|
var currentText by remember { mutableStateOf("") }
|
||||||
|
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
optionsViewModel.init(tunnelId)
|
||||||
|
if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
||||||
|
delay(Constants.FOCUS_REQUEST_DELAY)
|
||||||
|
focusRequester.requestFocus()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun saveTrustedSSID() {
|
||||||
|
if (currentText.isNotEmpty()) {
|
||||||
|
scope.launch {
|
||||||
|
optionsViewModel.onSaveRunSSID(currentText).let {
|
||||||
|
when (it) {
|
||||||
|
is Result.Success -> currentText = ""
|
||||||
|
is Result.Error -> appViewModel.showSnackbarMessage(it.error.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+9
@@ -0,0 +1,9 @@
|
|||||||
|
package com.zaneschepke.wireguardautotunnel.ui.screens.options
|
||||||
|
|
||||||
|
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
|
||||||
|
|
||||||
|
data class OptionsUiState(
|
||||||
|
val id: String? = null,
|
||||||
|
val tunnel: TunnelConfig? = null,
|
||||||
|
val isDefaultTunnel: Boolean = false
|
||||||
|
)
|
||||||
+101
@@ -0,0 +1,101 @@
|
|||||||
|
package com.zaneschepke.wireguardautotunnel.ui.screens.options
|
||||||
|
|
||||||
|
import androidx.compose.ui.util.fastFirstOrNull
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
|
||||||
|
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
|
||||||
|
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
|
||||||
|
import com.zaneschepke.wireguardautotunnel.util.Constants
|
||||||
|
import com.zaneschepke.wireguardautotunnel.util.Event
|
||||||
|
import com.zaneschepke.wireguardautotunnel.util.Result
|
||||||
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
|
import kotlinx.coroutines.flow.combine
|
||||||
|
import kotlinx.coroutines.flow.stateIn
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
@HiltViewModel
|
||||||
|
class OptionsViewModel @Inject
|
||||||
|
constructor(
|
||||||
|
private val appDataRepository: AppDataRepository
|
||||||
|
) : ViewModel() {
|
||||||
|
|
||||||
|
private val _optionState = MutableStateFlow(OptionsUiState())
|
||||||
|
|
||||||
|
val uiState = combine(
|
||||||
|
appDataRepository.tunnels.getTunnelConfigsFlow(),
|
||||||
|
_optionState,
|
||||||
|
) { tunnels, optionState ->
|
||||||
|
if (optionState.id != null) {
|
||||||
|
val tunnelConfig = tunnels.fastFirstOrNull { it.id.toString() == optionState.id }
|
||||||
|
val isPrimaryTunnel = tunnelConfig?.isPrimaryTunnel == true
|
||||||
|
OptionsUiState(optionState.id, tunnelConfig, isPrimaryTunnel)
|
||||||
|
} else OptionsUiState()
|
||||||
|
}.stateIn(
|
||||||
|
viewModelScope,
|
||||||
|
SharingStarted.WhileSubscribed(Constants.SUBSCRIPTION_TIMEOUT),
|
||||||
|
OptionsUiState(),
|
||||||
|
)
|
||||||
|
|
||||||
|
fun init(tunnelId: String) {
|
||||||
|
_optionState.value = _optionState.value.copy(
|
||||||
|
id = tunnelId,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onDeleteRunSSID(ssid: String) = viewModelScope.launch(Dispatchers.IO) {
|
||||||
|
uiState.value.tunnel?.let {
|
||||||
|
appDataRepository.tunnels.save(
|
||||||
|
tunnelConfig = it.copy(
|
||||||
|
tunnelNetworks = (uiState.value.tunnel!!.tunnelNetworks - ssid).toMutableList(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun saveTunnel(tunnelConfig: TunnelConfig?) = viewModelScope.launch(Dispatchers.IO) {
|
||||||
|
tunnelConfig?.let {
|
||||||
|
appDataRepository.tunnels.save(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun onSaveRunSSID(ssid: String): Result<Unit> {
|
||||||
|
val trimmed = ssid.trim()
|
||||||
|
val tunnelsWithName = withContext(viewModelScope.coroutineContext) {
|
||||||
|
appDataRepository.tunnels.findByTunnelNetworksName(trimmed)
|
||||||
|
}
|
||||||
|
return if (uiState.value.tunnel?.tunnelNetworks?.contains(trimmed) != true &&
|
||||||
|
tunnelsWithName.isEmpty()) {
|
||||||
|
uiState.value.tunnel?.tunnelNetworks?.add(trimmed)
|
||||||
|
saveTunnel(uiState.value.tunnel)
|
||||||
|
Result.Success(Unit)
|
||||||
|
} else {
|
||||||
|
Result.Error(Event.Error.SsidConflict)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onToggleIsMobileDataTunnel() = viewModelScope.launch(Dispatchers.IO) {
|
||||||
|
uiState.value.tunnel?.let {
|
||||||
|
if (it.isMobileDataTunnel) {
|
||||||
|
appDataRepository.tunnels.updateMobileDataTunnel(null)
|
||||||
|
} else appDataRepository.tunnels.updateMobileDataTunnel(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onTogglePrimaryTunnel() = viewModelScope.launch(Dispatchers.IO) {
|
||||||
|
if (uiState.value.tunnel != null) {
|
||||||
|
appDataRepository.tunnels.updatePrimaryTunnel(
|
||||||
|
when (uiState.value.isDefaultTunnel) {
|
||||||
|
true -> null
|
||||||
|
false -> uiState.value.tunnel
|
||||||
|
},
|
||||||
|
)
|
||||||
|
WireGuardAutoTunnel.requestTunnelTileServiceStateUpdate(WireGuardAutoTunnel.instance)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+8
-4
@@ -27,11 +27,11 @@ fun PinLockScreen(navController: NavController, appViewModel: AppViewModel) {
|
|||||||
color = MaterialTheme.colorScheme.surface,
|
color = MaterialTheme.colorScheme.surface,
|
||||||
onPinCorrect = {
|
onPinCorrect = {
|
||||||
// pin is correct, navigate or hide pin lock
|
// pin is correct, navigate or hide pin lock
|
||||||
if(WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
||||||
navController.navigate(Screen.Main.route)
|
navController.navigate(Screen.Main.route)
|
||||||
} else {
|
} else {
|
||||||
val isPopped = navController.popBackStack()
|
val isPopped = navController.popBackStack()
|
||||||
if(!isPopped) {
|
if (!isPopped) {
|
||||||
navController.navigate(Screen.Main.route)
|
navController.navigate(Screen.Main.route)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -39,11 +39,15 @@ fun PinLockScreen(navController: NavController, appViewModel: AppViewModel) {
|
|||||||
},
|
},
|
||||||
onPinIncorrect = {
|
onPinIncorrect = {
|
||||||
// pin is incorrect, show error
|
// pin is incorrect, show error
|
||||||
appViewModel.showSnackbarMessage(StringValue.StringResource(R.string.incorrect_pin).asString(context))
|
appViewModel.showSnackbarMessage(
|
||||||
|
StringValue.StringResource(R.string.incorrect_pin).asString(context),
|
||||||
|
)
|
||||||
},
|
},
|
||||||
onPinCreated = {
|
onPinCreated = {
|
||||||
// pin created for the first time, navigate or hide pin lock
|
// pin created for the first time, navigate or hide pin lock
|
||||||
appViewModel.showSnackbarMessage(StringValue.StringResource(R.string.pin_created).asString(context))
|
appViewModel.showSnackbarMessage(
|
||||||
|
StringValue.StringResource(R.string.pin_created).asString(context),
|
||||||
|
)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+7
-6
@@ -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
|
||||||
@@ -70,8 +71,6 @@ import androidx.navigation.NavController
|
|||||||
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.wireguard.android.backend.Tunnel
|
|
||||||
import com.wireguard.android.backend.WgQuickBackend
|
|
||||||
import com.zaneschepke.wireguardautotunnel.R
|
import com.zaneschepke.wireguardautotunnel.R
|
||||||
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
|
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.AppViewModel
|
import com.zaneschepke.wireguardautotunnel.ui.AppViewModel
|
||||||
@@ -85,6 +84,8 @@ import com.zaneschepke.wireguardautotunnel.util.FileUtils
|
|||||||
import com.zaneschepke.wireguardautotunnel.util.Result
|
import com.zaneschepke.wireguardautotunnel.util.Result
|
||||||
import kotlinx.coroutines.Dispatchers
|
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 xyz.teamgravity.pin_lock_compose.PinManager
|
||||||
import java.io.File
|
import java.io.File
|
||||||
@@ -160,7 +161,7 @@ fun SettingsScreen(
|
|||||||
fun handleAutoTunnelToggle() {
|
fun handleAutoTunnelToggle() {
|
||||||
if (uiState.isBatteryOptimizeDisableShown || isBatteryOptimizationsDisabled()) {
|
if (uiState.isBatteryOptimizeDisableShown || isBatteryOptimizationsDisabled()) {
|
||||||
if (appViewModel.isRequiredPermissionGranted()) {
|
if (appViewModel.isRequiredPermissionGranted()) {
|
||||||
viewModel.toggleAutoTunnel()
|
viewModel.onToggleAutoTunnel()
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
requestBatteryOptimizationsDisabled()
|
requestBatteryOptimizationsDisabled()
|
||||||
@@ -550,7 +551,7 @@ fun SettingsScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (WgQuickBackend.hasKernelSupport()) {
|
if (AwgQuickBackend.hasKernelSupport()) {
|
||||||
Surface(
|
Surface(
|
||||||
tonalElevation = 2.dp,
|
tonalElevation = 2.dp,
|
||||||
shadowElevation = 2.dp,
|
shadowElevation = 2.dp,
|
||||||
@@ -609,7 +610,7 @@ fun SettingsScreen(
|
|||||||
title = stringResource(id = R.string.other),
|
title = stringResource(id = R.string.other),
|
||||||
padding = screenPadding,
|
padding = screenPadding,
|
||||||
)
|
)
|
||||||
if(!WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
if (!WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
||||||
ConfigurationToggle(
|
ConfigurationToggle(
|
||||||
stringResource(R.string.always_on_vpn_support),
|
stringResource(R.string.always_on_vpn_support),
|
||||||
enabled = !uiState.settings.isAutoTunnelEnabled,
|
enabled = !uiState.settings.isAutoTunnelEnabled,
|
||||||
@@ -639,7 +640,7 @@ fun SettingsScreen(
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
if(!WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
if (!WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
||||||
Row(
|
Row(
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
|
|||||||
+21
-37
@@ -6,11 +6,9 @@ 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.util.RootShell
|
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
|
||||||
import com.zaneschepke.wireguardautotunnel.data.datastore.DataStoreManager
|
|
||||||
import com.zaneschepke.wireguardautotunnel.data.model.Settings
|
import com.zaneschepke.wireguardautotunnel.data.model.Settings
|
||||||
import com.zaneschepke.wireguardautotunnel.data.repository.SettingsRepository
|
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
|
||||||
import com.zaneschepke.wireguardautotunnel.data.repository.TunnelConfigRepository
|
|
||||||
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
|
||||||
@@ -21,6 +19,7 @@ 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 org.amnezia.awg.util.RootShell
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@@ -29,26 +28,25 @@ class SettingsViewModel
|
|||||||
@Inject
|
@Inject
|
||||||
constructor(
|
constructor(
|
||||||
private val application: Application,
|
private val application: Application,
|
||||||
private val tunnelConfigRepository: TunnelConfigRepository,
|
private val appDataRepository: AppDataRepository,
|
||||||
private val settingsRepository: SettingsRepository,
|
private val serviceManager: ServiceManager,
|
||||||
private val dataStoreManager: DataStoreManager,
|
|
||||||
private val rootShell: RootShell,
|
private val rootShell: RootShell,
|
||||||
private val vpnService: VpnService
|
vpnService: VpnService
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
|
|
||||||
val uiState =
|
val uiState =
|
||||||
combine(
|
combine(
|
||||||
settingsRepository.getSettingsFlow(),
|
appDataRepository.settings.getSettingsFlow(),
|
||||||
tunnelConfigRepository.getTunnelConfigsFlow(),
|
appDataRepository.tunnels.getTunnelConfigsFlow(),
|
||||||
vpnService.vpnState,
|
vpnService.vpnState,
|
||||||
dataStoreManager.preferencesFlow,
|
appDataRepository.appState.generalStateFlow,
|
||||||
) { settings, tunnels, tunnelState, preferences ->
|
) { settings, tunnels, tunnelState, generalState ->
|
||||||
SettingsUiState(
|
SettingsUiState(
|
||||||
settings,
|
settings,
|
||||||
tunnels,
|
tunnels,
|
||||||
tunnelState,
|
tunnelState,
|
||||||
preferences?.get(DataStoreManager.LOCATION_DISCLOSURE_SHOWN) ?: false,
|
generalState.locationDisclosureShown,
|
||||||
preferences?.get(DataStoreManager.BATTERY_OPTIMIZE_DISABLE_SHOWN) ?: false,
|
generalState.batteryOptimizationDisableShown,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
.stateIn(
|
.stateIn(
|
||||||
@@ -70,12 +68,12 @@ constructor(
|
|||||||
|
|
||||||
fun setLocationDisclosureShown() =
|
fun setLocationDisclosureShown() =
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
dataStoreManager.saveToDataStore(DataStoreManager.LOCATION_DISCLOSURE_SHOWN, true)
|
appDataRepository.appState.setLocationDisclosureShown(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setBatteryOptimizeDisableShown() =
|
fun setBatteryOptimizeDisableShown() =
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
dataStoreManager.saveToDataStore(DataStoreManager.BATTERY_OPTIMIZE_DISABLE_SHOWN, true)
|
appDataRepository.appState.setBatteryOptimizationDisableShown(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onToggleTunnelOnMobileData() {
|
fun onToggleTunnelOnMobileData() {
|
||||||
@@ -95,43 +93,37 @@ constructor(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun getDefaultTunnelOrFirst(): String {
|
fun onToggleAutoTunnel() =
|
||||||
return uiState.value.settings.defaultTunnel
|
|
||||||
?: tunnelConfigRepository.getAll().first().toString()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun toggleAutoTunnel() =
|
|
||||||
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(application)
|
serviceManager.stopWatcherService(application)
|
||||||
} else {
|
} else {
|
||||||
ServiceManager.startWatcherService(application)
|
serviceManager.startWatcherService(application)
|
||||||
isAutoTunnelPaused = false
|
isAutoTunnelPaused = false
|
||||||
}
|
}
|
||||||
saveSettings(
|
saveSettings(
|
||||||
uiState.value.settings.copy(
|
uiState.value.settings.copy(
|
||||||
isAutoTunnelEnabled = !isAutoTunnelEnabled,
|
isAutoTunnelEnabled = !isAutoTunnelEnabled,
|
||||||
isAutoTunnelPaused = isAutoTunnelPaused,
|
isAutoTunnelPaused = isAutoTunnelPaused,
|
||||||
defaultTunnel = getDefaultTunnelOrFirst(),
|
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
WireGuardAutoTunnel.requestAutoTunnelTileServiceUpdate(application)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onToggleAlwaysOnVPN() =
|
fun onToggleAlwaysOnVPN() =
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
val updatedSettings =
|
saveSettings(
|
||||||
uiState.value.settings.copy(
|
uiState.value.settings.copy(
|
||||||
isAlwaysOnVpnEnabled = !uiState.value.settings.isAlwaysOnVpnEnabled,
|
isAlwaysOnVpnEnabled = !uiState.value.settings.isAlwaysOnVpnEnabled,
|
||||||
defaultTunnel = getDefaultTunnelOrFirst(),
|
),
|
||||||
)
|
)
|
||||||
saveSettings(updatedSettings)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun saveSettings(settings: Settings) =
|
private fun saveSettings(settings: Settings) =
|
||||||
viewModelScope.launch { settingsRepository.save(settings) }
|
viewModelScope.launch { appDataRepository.settings.save(settings) }
|
||||||
|
|
||||||
fun onToggleTunnelOnEthernet() {
|
fun onToggleTunnelOnEthernet() {
|
||||||
saveSettings(
|
saveSettings(
|
||||||
@@ -154,14 +146,6 @@ constructor(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onToggleBatterySaver() {
|
|
||||||
saveSettings(
|
|
||||||
uiState.value.settings.copy(
|
|
||||||
isBatterySaverEnabled = !uiState.value.settings.isBatterySaverEnabled,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun saveKernelMode(on: Boolean) {
|
private fun saveKernelMode(on: Boolean) {
|
||||||
saveSettings(
|
saveSettings(
|
||||||
uiState.value.settings.copy(
|
uiState.value.settings.copy(
|
||||||
|
|||||||
+34
-15
@@ -37,6 +37,7 @@ import androidx.compose.ui.platform.LocalContext
|
|||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.res.vectorResource
|
import androidx.compose.ui.res.vectorResource
|
||||||
import androidx.compose.ui.text.TextStyle
|
import androidx.compose.ui.text.TextStyle
|
||||||
|
import androidx.compose.ui.text.buildAnnotatedString
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.text.style.TextDecoration
|
import androidx.compose.ui.text.style.TextDecoration
|
||||||
@@ -50,7 +51,6 @@ 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
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.Screen
|
import com.zaneschepke.wireguardautotunnel.ui.Screen
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.common.screen.LoadingScreen
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun SupportScreen(
|
fun SupportScreen(
|
||||||
@@ -71,7 +71,7 @@ fun SupportScreen(
|
|||||||
Modifier
|
Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.verticalScroll(rememberScrollState())
|
.verticalScroll(rememberScrollState())
|
||||||
.focusable()
|
.focusable(),
|
||||||
) {
|
) {
|
||||||
Surface(
|
Surface(
|
||||||
tonalElevation = 2.dp,
|
tonalElevation = 2.dp,
|
||||||
@@ -123,18 +123,24 @@ fun SupportScreen(
|
|||||||
Text(
|
Text(
|
||||||
stringResource(id = R.string.docs_description),
|
stringResource(id = R.string.docs_description),
|
||||||
textAlign = TextAlign.Justify,
|
textAlign = TextAlign.Justify,
|
||||||
modifier = Modifier.padding(start = 10.dp),
|
modifier = Modifier
|
||||||
|
.padding(start = 10.dp)
|
||||||
|
.weight(
|
||||||
|
weight = 1.0f,
|
||||||
|
fill = false,
|
||||||
|
),
|
||||||
|
softWrap = true
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
Icon(
|
Icon(
|
||||||
forwardIcon,
|
forwardIcon,
|
||||||
forwardIcon.name
|
forwardIcon.name,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
HorizontalDivider(
|
HorizontalDivider(
|
||||||
thickness = 0.5.dp,
|
thickness = 0.5.dp,
|
||||||
color = MaterialTheme.colorScheme.onBackground
|
color = MaterialTheme.colorScheme.onBackground,
|
||||||
)
|
)
|
||||||
TextButton(
|
TextButton(
|
||||||
onClick = { appViewModel.openWebPage(context.resources.getString(R.string.discord_url)) },
|
onClick = { appViewModel.openWebPage(context.resources.getString(R.string.discord_url)) },
|
||||||
@@ -160,13 +166,13 @@ fun SupportScreen(
|
|||||||
}
|
}
|
||||||
Icon(
|
Icon(
|
||||||
forwardIcon,
|
forwardIcon,
|
||||||
forwardIcon.name
|
forwardIcon.name,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
HorizontalDivider(
|
HorizontalDivider(
|
||||||
thickness = 0.5.dp,
|
thickness = 0.5.dp,
|
||||||
color = MaterialTheme.colorScheme.onBackground
|
color = MaterialTheme.colorScheme.onBackground,
|
||||||
)
|
)
|
||||||
TextButton(
|
TextButton(
|
||||||
onClick = { appViewModel.openWebPage(context.resources.getString(R.string.github_url)) },
|
onClick = { appViewModel.openWebPage(context.resources.getString(R.string.github_url)) },
|
||||||
@@ -192,13 +198,13 @@ fun SupportScreen(
|
|||||||
}
|
}
|
||||||
Icon(
|
Icon(
|
||||||
forwardIcon,
|
forwardIcon,
|
||||||
forwardIcon.name
|
forwardIcon.name,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
HorizontalDivider(
|
HorizontalDivider(
|
||||||
thickness = 0.5.dp,
|
thickness = 0.5.dp,
|
||||||
color = MaterialTheme.colorScheme.onBackground
|
color = MaterialTheme.colorScheme.onBackground,
|
||||||
)
|
)
|
||||||
TextButton(
|
TextButton(
|
||||||
onClick = { appViewModel.launchEmail() },
|
onClick = { appViewModel.launchEmail() },
|
||||||
@@ -220,14 +226,14 @@ fun SupportScreen(
|
|||||||
}
|
}
|
||||||
Icon(
|
Icon(
|
||||||
forwardIcon,
|
forwardIcon,
|
||||||
forwardIcon.name
|
forwardIcon.name,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if(!WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
if (!WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
||||||
HorizontalDivider(
|
HorizontalDivider(
|
||||||
thickness = 0.5.dp,
|
thickness = 0.5.dp,
|
||||||
color = MaterialTheme.colorScheme.onBackground
|
color = MaterialTheme.colorScheme.onBackground,
|
||||||
)
|
)
|
||||||
TextButton(
|
TextButton(
|
||||||
onClick = { navController.navigate(Screen.Support.Logs.route) },
|
onClick = { navController.navigate(Screen.Support.Logs.route) },
|
||||||
@@ -249,7 +255,7 @@ fun SupportScreen(
|
|||||||
}
|
}
|
||||||
Icon(
|
Icon(
|
||||||
Icons.AutoMirrored.Rounded.ArrowForward,
|
Icons.AutoMirrored.Rounded.ArrowForward,
|
||||||
stringResource(id = R.string.go)
|
stringResource(id = R.string.go),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -271,8 +277,21 @@ fun SupportScreen(
|
|||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
modifier = Modifier.padding(25.dp),
|
modifier = Modifier.padding(25.dp),
|
||||||
) {
|
) {
|
||||||
Text("Version: ${BuildConfig.VERSION_NAME}", modifier = Modifier.focusable())
|
val version = buildAnnotatedString {
|
||||||
Text("Mode: ${if (uiState.settings.isKernelEnabled) "Kernel" else "Userspace"}")
|
append(stringResource(id = R.string.version))
|
||||||
|
append(": ")
|
||||||
|
append(BuildConfig.VERSION_NAME)
|
||||||
|
}
|
||||||
|
val mode = buildAnnotatedString {
|
||||||
|
append(stringResource(R.string.mode))
|
||||||
|
append(": ")
|
||||||
|
when(uiState.settings.isKernelEnabled){
|
||||||
|
true -> append(stringResource(id = R.string.kernel))
|
||||||
|
false -> append(stringResource(id = R.string.userspace))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Text(version.text, modifier = Modifier.focusable())
|
||||||
|
Text(mode.text)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -11,7 +11,7 @@ import kotlinx.coroutines.flow.stateIn
|
|||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
class SupportViewModel @Inject constructor(private val settingsRepository: SettingsRepository) :
|
class SupportViewModel @Inject constructor(settingsRepository: SettingsRepository) :
|
||||||
ViewModel() {
|
ViewModel() {
|
||||||
|
|
||||||
val uiState =
|
val uiState =
|
||||||
|
|||||||
+16
-10
@@ -31,7 +31,6 @@ 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.wireguardautotunnel.ui.AppViewModel
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.common.text.LogTypeLabel
|
import com.zaneschepke.wireguardautotunnel.ui.common.text.LogTypeLabel
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
@@ -48,7 +47,7 @@ fun LogsScreen(appViewModel: AppViewModel) {
|
|||||||
val clipboardManager: ClipboardManager = LocalClipboardManager.current
|
val clipboardManager: ClipboardManager = LocalClipboardManager.current
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
|
|
||||||
LaunchedEffect(logs.size){
|
LaunchedEffect(logs.size) {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
lazyColumnListState.animateScrollToItem(logs.size)
|
lazyColumnListState.animateScrollToItem(logs.size)
|
||||||
}
|
}
|
||||||
@@ -61,16 +60,16 @@ fun LogsScreen(appViewModel: AppViewModel) {
|
|||||||
appViewModel.saveLogsToFile()
|
appViewModel.saveLogsToFile()
|
||||||
},
|
},
|
||||||
shape = RoundedCornerShape(16.dp),
|
shape = RoundedCornerShape(16.dp),
|
||||||
containerColor = MaterialTheme.colorScheme.primary
|
containerColor = MaterialTheme.colorScheme.primary,
|
||||||
) {
|
) {
|
||||||
val icon = Icons.Filled.Save
|
val icon = Icons.Filled.Save
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = icon,
|
imageVector = icon,
|
||||||
contentDescription = icon.name,
|
contentDescription = icon.name,
|
||||||
tint = MaterialTheme.colorScheme.onPrimary
|
tint = MaterialTheme.colorScheme.onPrimary,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
) {
|
) {
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
@@ -78,9 +77,12 @@ fun LogsScreen(appViewModel: AppViewModel) {
|
|||||||
state = lazyColumnListState,
|
state = lazyColumnListState,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.padding(horizontal = 24.dp)) {
|
.padding(horizontal = 24.dp),
|
||||||
|
) {
|
||||||
items(logs) {
|
items(logs) {
|
||||||
Row(horizontalArrangement = Arrangement.spacedBy(5.dp, Alignment.Start), verticalAlignment = Alignment.Top,
|
Row(
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(5.dp, Alignment.Start),
|
||||||
|
verticalAlignment = Alignment.Top,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.clickable(
|
.clickable(
|
||||||
@@ -88,13 +90,17 @@ fun LogsScreen(appViewModel: AppViewModel) {
|
|||||||
indication = null,
|
indication = null,
|
||||||
onClick = {
|
onClick = {
|
||||||
clipboardManager.setText(annotatedString = AnnotatedString(it.toString()))
|
clipboardManager.setText(annotatedString = AnnotatedString(it.toString()))
|
||||||
}
|
},
|
||||||
)
|
),
|
||||||
) {
|
) {
|
||||||
val fontSize = 10.sp
|
val fontSize = 10.sp
|
||||||
Text(text = it.tag, modifier = Modifier.fillMaxSize(0.3f), fontSize = fontSize)
|
Text(text = it.tag, modifier = Modifier.fillMaxSize(0.3f), fontSize = fontSize)
|
||||||
LogTypeLabel(color = Color(it.level.color())) {
|
LogTypeLabel(color = Color(it.level.color())) {
|
||||||
Text(text = it.level.signifier, textAlign = TextAlign.Center, fontSize = fontSize)
|
Text(
|
||||||
|
text = it.level.signifier,
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
fontSize = fontSize,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
Text("${it.message} - ${it.time}", fontSize = fontSize)
|
Text("${it.message} - ${it.time}", fontSize = fontSize)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -57,6 +57,7 @@ fun WireguardAutoTunnelTheme(
|
|||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
|
if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
|
||||||
}
|
}
|
||||||
|
|
||||||
darkTheme -> DarkColorScheme
|
darkTheme -> DarkColorScheme
|
||||||
else -> LightColorScheme
|
else -> LightColorScheme
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ object Constants {
|
|||||||
const val VPN_STATISTIC_CHECK_INTERVAL = 1_000L
|
const val VPN_STATISTIC_CHECK_INTERVAL = 1_000L
|
||||||
const val VPN_CONNECTED_NOTIFICATION_DELAY = 3_000L
|
const val VPN_CONNECTED_NOTIFICATION_DELAY = 3_000L
|
||||||
const val TOGGLE_TUNNEL_DELAY = 300L
|
const val TOGGLE_TUNNEL_DELAY = 300L
|
||||||
|
const val WATCHER_COLLECTION_DELAY = 1_000L
|
||||||
const val CONF_FILE_EXTENSION = ".conf"
|
const val CONF_FILE_EXTENSION = ".conf"
|
||||||
const val ZIP_FILE_EXTENSION = ".zip"
|
const val ZIP_FILE_EXTENSION = ".zip"
|
||||||
const val URI_CONTENT_SCHEME = "content"
|
const val URI_CONTENT_SCHEME = "content"
|
||||||
@@ -17,6 +18,7 @@ object Constants {
|
|||||||
const val TEXT_MIME_TYPE = "text/plain"
|
const val TEXT_MIME_TYPE = "text/plain"
|
||||||
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 EMAIL_MIME_TYPE = "message/rfc822"
|
const val EMAIL_MIME_TYPE = "message/rfc822"
|
||||||
const val SYSTEM_EXEMPT_SERVICE_TYPE_ID = 1024
|
const val SYSTEM_EXEMPT_SERVICE_TYPE_ID = 1024
|
||||||
|
|
||||||
@@ -29,4 +31,10 @@ object Constants {
|
|||||||
const val PING_INTERVAL = 60_000L
|
const val PING_INTERVAL = 60_000L
|
||||||
const val PING_COOLDOWN = PING_INTERVAL * 60 //one hour
|
const val PING_COOLDOWN = PING_INTERVAL * 60 //one hour
|
||||||
|
|
||||||
|
const val ALLOWED_DISPLAY_NAME_LENGTH = 20
|
||||||
|
|
||||||
|
const val TUNNEL_EXTRA_KEY = "tunnelId"
|
||||||
|
|
||||||
|
const val UNREADABLE_SSID = "<unknown ssid>"
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ sealed class Event {
|
|||||||
get() = WireGuardAutoTunnel.instance.getString(R.string.error_ssid_exists)
|
get() = WireGuardAutoTunnel.instance.getString(R.string.error_ssid_exists)
|
||||||
}
|
}
|
||||||
|
|
||||||
data class ConfigParseError(val appendedMessage : String) : Error() {
|
data class ConfigParseError(val appendedMessage: String) : Error() {
|
||||||
override val message: String =
|
override val message: String =
|
||||||
WireGuardAutoTunnel.instance.getString(R.string.config_parse_error) + (
|
WireGuardAutoTunnel.instance.getString(R.string.config_parse_error) + (
|
||||||
if (appendedMessage != "") ": ${appendedMessage.trim()}" else "")
|
if (appendedMessage != "") ": ${appendedMessage.trim()}" else "")
|
||||||
|
|||||||
@@ -2,15 +2,14 @@ package com.zaneschepke.wireguardautotunnel.util
|
|||||||
|
|
||||||
import android.content.BroadcastReceiver
|
import android.content.BroadcastReceiver
|
||||||
import android.content.pm.PackageInfo
|
import android.content.pm.PackageInfo
|
||||||
import com.wireguard.android.backend.Statistics
|
|
||||||
import com.wireguard.android.backend.Statistics.PeerStats
|
|
||||||
import com.wireguard.crypto.Key
|
|
||||||
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
|
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
|
||||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.HandshakeStatus
|
import com.zaneschepke.wireguardautotunnel.service.tunnel.HandshakeStatus
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||||
import kotlinx.coroutines.GlobalScope
|
import kotlinx.coroutines.GlobalScope
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import org.amnezia.awg.backend.Statistics
|
||||||
|
import org.amnezia.awg.crypto.Key
|
||||||
import java.math.BigDecimal
|
import java.math.BigDecimal
|
||||||
import java.text.DecimalFormat
|
import java.text.DecimalFormat
|
||||||
import kotlin.coroutines.CoroutineContext
|
import kotlin.coroutines.CoroutineContext
|
||||||
@@ -31,8 +30,8 @@ fun BroadcastReceiver.goAsync(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun String.truncateWithEllipsis(allowedLength : Int) : String {
|
fun String.truncateWithEllipsis(allowedLength: Int): String {
|
||||||
return if(this.length > allowedLength + 3) {
|
return if (this.length > allowedLength + 3) {
|
||||||
this.substring(0, allowedLength) + "***"
|
this.substring(0, allowedLength) + "***"
|
||||||
} else this
|
} else this
|
||||||
}
|
}
|
||||||
@@ -50,15 +49,15 @@ typealias TunnelConfigs = List<TunnelConfig>
|
|||||||
|
|
||||||
typealias Packages = List<PackageInfo>
|
typealias Packages = List<PackageInfo>
|
||||||
|
|
||||||
fun Statistics.mapPeerStats(): Map<Key, PeerStats?> {
|
fun Statistics.mapPeerStats(): Map<Key, Statistics.PeerStats?> {
|
||||||
return this.peers().associateWith { key -> (this.peer(key)) }
|
return this.peers().associateWith { key -> (this.peer(key)) }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun PeerStats.latestHandshakeSeconds(): Long? {
|
fun Statistics.PeerStats.latestHandshakeSeconds(): Long? {
|
||||||
return NumberUtils.getSecondsBetweenTimestampAndNow(this.latestHandshakeEpochMillis)
|
return NumberUtils.getSecondsBetweenTimestampAndNow(this.latestHandshakeEpochMillis)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun 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 {
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import java.util.zip.ZipOutputStream
|
|||||||
object FileUtils {
|
object FileUtils {
|
||||||
private const val ZIP_FILE_MIME_TYPE = "application/zip"
|
private const val ZIP_FILE_MIME_TYPE = "application/zip"
|
||||||
|
|
||||||
|
//TODO issue with android 9
|
||||||
private fun createDownloadsFileOutputStream(
|
private fun createDownloadsFileOutputStream(
|
||||||
context: Context,
|
context: Context,
|
||||||
fileName: String,
|
fileName: String,
|
||||||
@@ -45,11 +46,11 @@ object FileUtils {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun saveFileToDownloads(context: Context, content: String, fileName: String) {
|
fun saveFileToDownloads(context: Context, content: String, fileName: String) {
|
||||||
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||||
val contentValues = ContentValues().apply {
|
val contentValues = ContentValues().apply {
|
||||||
put(MediaStore.MediaColumns.DISPLAY_NAME, fileName)
|
put(MediaColumns.DISPLAY_NAME, fileName)
|
||||||
put(MediaStore.MediaColumns.MIME_TYPE, Constants.TEXT_MIME_TYPE)
|
put(MediaColumns.MIME_TYPE, Constants.TEXT_MIME_TYPE)
|
||||||
put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_DOWNLOADS)
|
put(MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_DOWNLOADS)
|
||||||
}
|
}
|
||||||
val resolver = context.contentResolver
|
val resolver = context.contentResolver
|
||||||
val uri = resolver.insert(MediaStore.Downloads.EXTERNAL_CONTENT_URI, contentValues)
|
val uri = resolver.insert(MediaStore.Downloads.EXTERNAL_CONTENT_URI, contentValues)
|
||||||
@@ -61,7 +62,7 @@ object FileUtils {
|
|||||||
} else {
|
} else {
|
||||||
val target = File(
|
val target = File(
|
||||||
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS),
|
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS),
|
||||||
fileName
|
fileName,
|
||||||
)
|
)
|
||||||
FileOutputStream(target).use { output ->
|
FileOutputStream(target).use { output ->
|
||||||
output.write(content.toByteArray())
|
output.write(content.toByteArray())
|
||||||
|
|||||||
@@ -22,11 +22,11 @@ object NumberUtils {
|
|||||||
return "tunnel${randomFive()}"
|
return "tunnel${randomFive()}"
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun randomFive() : Int {
|
private fun randomFive(): Int {
|
||||||
return (Math.random() * 100000).toInt()
|
return (Math.random() * 100000).toInt()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun randomThree() : Int {
|
fun randomThree(): Int {
|
||||||
return (Math.random() * 1000).toInt()
|
return (Math.random() * 1000).toInt()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,17 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.util
|
package com.zaneschepke.wireguardautotunnel.util
|
||||||
|
|
||||||
import android.util.Log
|
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
|
||||||
class ReleaseTree : Timber.Tree() {
|
class ReleaseTree : Timber.DebugTree() {
|
||||||
override fun log(priority: Int, tag: String?, message: String, t: Throwable?) {
|
override fun d(t: Throwable?) {
|
||||||
when(priority) {
|
return
|
||||||
Log.DEBUG -> return
|
|
||||||
}
|
}
|
||||||
super.log(priority,tag,message,t)
|
|
||||||
|
override fun d(t: Throwable?, message: String?, vararg args: Any?) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun d(message: String?, vararg args: Any?) {
|
||||||
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,17 +1,18 @@
|
|||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
android:width="24dp"
|
android:width="24dp"
|
||||||
android:height="24dp"
|
android:height="24dp"
|
||||||
|
android:tint="#FFFFFF"
|
||||||
android:viewportWidth="640"
|
android:viewportWidth="640"
|
||||||
android:viewportHeight="640"
|
android:viewportHeight="640">
|
||||||
android:tint="#FFFFFF">
|
<group
|
||||||
<group android:scaleX="1.0132159"
|
android:scaleX="1.0132159"
|
||||||
android:scaleY="1.0132159"
|
android:scaleY="1.0132159"
|
||||||
android:translateX="-4.229075"
|
android:translateX="-4.229075"
|
||||||
android:translateY="-4.229075">
|
android:translateY="-4.229075">
|
||||||
<path
|
<path
|
||||||
android:pathData="M316.72,80.15C314.94,80.82 312.08,83.95 308.79,88.84C275.66,138.15 157.88,161.96 119.66,127.08C109.97,118.24 101.21,118.84 98.97,128.5C96.01,141.29 98.49,204.07 103.12,233.5C123.71,364.32 186.77,465.69 303.03,554.88C314.06,563.34 316.63,563.42 326.93,555.61C329.91,553.35 336.21,548.63 340.93,545.13C345.64,541.63 350.87,537.46 352.55,535.88C354.23,534.29 357.92,531.53 360.75,529.74C413.56,496.43 481.74,399.04 510.38,316C527.22,267.19 534.86,236.66 539.96,197.9C547.99,136.74 545.31,124.46 526,134.01C469.85,161.74 361.02,137.16 333.39,90.49C327.63,80.75 322.93,77.84 316.72,80.15M307.5,195.34C282.24,203.76 266.16,237.38 269.85,274.04C270.99,285.39 271.18,285 264.63,285C254.13,285 254.15,284.87 255,352.05C255.64,402.94 250.1,397.02 298.5,398.52C352.54,400.2 377.63,400.23 379.23,398.63C381.67,396.18 381.86,392.86 382.46,339.89C383.09,283.93 383.39,286 374.51,286C367.07,286 367.21,286.26 367.77,273.58C369.89,225.38 338.74,184.92 307.5,195.34M308.93,216.98C294.31,224.7 281.68,270.05 290.33,283.75C291.74,285.99 346.85,285.51 347.78,283.25C359.48,254.75 330.62,205.51 308.93,216.98M309.54,317.1C304.18,321.81 304.39,327.76 310.11,332.99C314.78,337.26 314.82,336.51 309.33,349.89C307.44,354.51 306.13,358.89 306.42,359.64C306.96,361.06 329,361.75 329,360.35C329,359.98 327.42,354.82 325.5,348.86C323.58,342.91 322,337.52 322,336.89C322,336.26 323.58,334 325.5,331.87C335.69,320.59 321.02,307.02 309.54,317.1"
|
|
||||||
android:fillColor="#53bdb6"
|
android:fillColor="#53bdb6"
|
||||||
android:fillType="evenOdd"
|
android:fillType="evenOdd"
|
||||||
android:strokeColor="#00000000"/>
|
android:pathData="M316.72,80.15C314.94,80.82 312.08,83.95 308.79,88.84C275.66,138.15 157.88,161.96 119.66,127.08C109.97,118.24 101.21,118.84 98.97,128.5C96.01,141.29 98.49,204.07 103.12,233.5C123.71,364.32 186.77,465.69 303.03,554.88C314.06,563.34 316.63,563.42 326.93,555.61C329.91,553.35 336.21,548.63 340.93,545.13C345.64,541.63 350.87,537.46 352.55,535.88C354.23,534.29 357.92,531.53 360.75,529.74C413.56,496.43 481.74,399.04 510.38,316C527.22,267.19 534.86,236.66 539.96,197.9C547.99,136.74 545.31,124.46 526,134.01C469.85,161.74 361.02,137.16 333.39,90.49C327.63,80.75 322.93,77.84 316.72,80.15M307.5,195.34C282.24,203.76 266.16,237.38 269.85,274.04C270.99,285.39 271.18,285 264.63,285C254.13,285 254.15,284.87 255,352.05C255.64,402.94 250.1,397.02 298.5,398.52C352.54,400.2 377.63,400.23 379.23,398.63C381.67,396.18 381.86,392.86 382.46,339.89C383.09,283.93 383.39,286 374.51,286C367.07,286 367.21,286.26 367.77,273.58C369.89,225.38 338.74,184.92 307.5,195.34M308.93,216.98C294.31,224.7 281.68,270.05 290.33,283.75C291.74,285.99 346.85,285.51 347.78,283.25C359.48,254.75 330.62,205.51 308.93,216.98M309.54,317.1C304.18,321.81 304.39,327.76 310.11,332.99C314.78,337.26 314.82,336.51 309.33,349.89C307.44,354.51 306.13,358.89 306.42,359.64C306.96,361.06 329,361.75 329,360.35C329,359.98 327.42,354.82 325.5,348.86C323.58,342.91 322,337.52 322,336.89C322,336.26 323.58,334 325.5,331.87C335.69,320.59 321.02,307.02 309.54,317.1"
|
||||||
|
android:strokeColor="#00000000" />
|
||||||
</group>
|
</group>
|
||||||
</vector>
|
</vector>
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 4.2 KiB |
@@ -0,0 +1,9 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="960"
|
||||||
|
android:viewportHeight="960">
|
||||||
|
<path
|
||||||
|
android:fillColor="#FF000000"
|
||||||
|
android:pathData="M360,600v-240h80v240h-80ZM520,600v-240h80v240h-80ZM480,920q-108,0 -202.5,-49.5T120,732v108L40,840v-240h240v80h-98q51,75 129.5,117.5T480,840q115,0 208.5,-66T820,599l78,18q-45,136 -160,219.5T480,920ZM42,440q7,-67 32,-128.5T143,198l57,57q-32,41 -52,87.5T123,440L42,440ZM256,199 L199,142q53,-44 114,-69.5T440,42v80q-51,5 -97,25t-87,52ZM705,199q-41,-32 -87.5,-52T520,122v-80q67,6 128.5,31T762,142l-57,57ZM838,440q-5,-51 -25,-97.5T761,255l57,-57q44,52 69,113.5T918,440h-80Z" />
|
||||||
|
</vector>
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="960"
|
||||||
|
android:viewportHeight="960">
|
||||||
|
<path
|
||||||
|
android:fillColor="#FF000000"
|
||||||
|
android:pathData="M380,660v-360l280,180 -280,180ZM480,920q-108,0 -202.5,-49.5T120,732v108L40,840v-240h240v80h-98q51,75 129.5,117.5T480,840q115,0 208.5,-66T820,599l78,18q-45,136 -160,219.5T480,920ZM42,440q7,-67 32,-128.5T143,198l57,57q-32,41 -52,87.5T123,440L42,440ZM256,199 L199,142q53,-44 114,-69.5T440,42v80q-51,5 -97,25t-87,52ZM705,199q-41,-32 -87.5,-52T520,122v-80q67,6 128.5,31T762,142l-57,57ZM838,440q-5,-51 -25,-97.5T761,255l57,-57q44,52 69,113.5T918,440h-80Z" />
|
||||||
|
</vector>
|
||||||
@@ -3,42 +3,53 @@
|
|||||||
android:height="180dp"
|
android:height="180dp"
|
||||||
android:viewportWidth="640"
|
android:viewportWidth="640"
|
||||||
android:viewportHeight="640">
|
android:viewportHeight="640">
|
||||||
<group android:scaleX="0.6666667"
|
<group
|
||||||
|
android:scaleX="0.6666667"
|
||||||
android:scaleY="0.6666667"
|
android:scaleY="0.6666667"
|
||||||
android:translateX="106.666664"
|
android:translateX="106.666664"
|
||||||
android:translateY="106.666664">
|
android:translateY="106.666664">
|
||||||
<group android:scaleX="0.315"
|
<group
|
||||||
|
android:scaleX="0.315"
|
||||||
android:scaleY="0.56"
|
android:scaleY="0.56"
|
||||||
android:translateX="46.4"
|
android:translateX="46.4"
|
||||||
android:translateY="140.8">
|
android:translateY="140.8">
|
||||||
<path
|
<path
|
||||||
android:pathData="M316.72,80.15C314.94,80.82 312.08,83.95 308.79,88.84C275.66,138.15 157.88,161.96 119.66,127.08C109.97,118.24 101.21,118.84 98.97,128.5C96.01,141.29 98.49,204.07 103.12,233.5C123.71,364.32 186.77,465.69 303.03,554.88C314.06,563.34 316.63,563.42 326.93,555.61C329.91,553.35 336.21,548.63 340.93,545.13C345.64,541.63 350.87,537.46 352.55,535.88C354.23,534.29 357.92,531.53 360.75,529.74C413.56,496.43 481.74,399.04 510.38,316C527.22,267.19 534.86,236.66 539.96,197.9C547.99,136.74 545.31,124.46 526,134.01C469.85,161.74 361.02,137.16 333.39,90.49C327.63,80.75 322.93,77.84 316.72,80.15M307.5,195.34C282.24,203.76 266.16,237.38 269.85,274.04C270.99,285.39 271.18,285 264.63,285C254.13,285 254.15,284.87 255,352.05C255.64,402.94 250.1,397.02 298.5,398.52C352.54,400.2 377.63,400.23 379.23,398.63C381.67,396.18 381.86,392.86 382.46,339.89C383.09,283.93 383.39,286 374.51,286C367.07,286 367.21,286.26 367.77,273.58C369.89,225.38 338.74,184.92 307.5,195.34M308.93,216.98C294.31,224.7 281.68,270.05 290.33,283.75C291.74,285.99 346.85,285.51 347.78,283.25C359.48,254.75 330.62,205.51 308.93,216.98M309.54,317.1C304.18,321.81 304.39,327.76 310.11,332.99C314.78,337.26 314.82,336.51 309.33,349.89C307.44,354.51 306.13,358.89 306.42,359.64C306.96,361.06 329,361.75 329,360.35C329,359.98 327.42,354.82 325.5,348.86C323.58,342.91 322,337.52 322,336.89C322,336.26 323.58,334 325.5,331.87C335.69,320.59 321.02,307.02 309.54,317.1"
|
|
||||||
android:fillColor="#53bdb6"
|
android:fillColor="#53bdb6"
|
||||||
android:fillType="evenOdd"
|
android:fillType="evenOdd"
|
||||||
android:strokeColor="#00000000"/>
|
android:pathData="M316.72,80.15C314.94,80.82 312.08,83.95 308.79,88.84C275.66,138.15 157.88,161.96 119.66,127.08C109.97,118.24 101.21,118.84 98.97,128.5C96.01,141.29 98.49,204.07 103.12,233.5C123.71,364.32 186.77,465.69 303.03,554.88C314.06,563.34 316.63,563.42 326.93,555.61C329.91,553.35 336.21,548.63 340.93,545.13C345.64,541.63 350.87,537.46 352.55,535.88C354.23,534.29 357.92,531.53 360.75,529.74C413.56,496.43 481.74,399.04 510.38,316C527.22,267.19 534.86,236.66 539.96,197.9C547.99,136.74 545.31,124.46 526,134.01C469.85,161.74 361.02,137.16 333.39,90.49C327.63,80.75 322.93,77.84 316.72,80.15M307.5,195.34C282.24,203.76 266.16,237.38 269.85,274.04C270.99,285.39 271.18,285 264.63,285C254.13,285 254.15,284.87 255,352.05C255.64,402.94 250.1,397.02 298.5,398.52C352.54,400.2 377.63,400.23 379.23,398.63C381.67,396.18 381.86,392.86 382.46,339.89C383.09,283.93 383.39,286 374.51,286C367.07,286 367.21,286.26 367.77,273.58C369.89,225.38 338.74,184.92 307.5,195.34M308.93,216.98C294.31,224.7 281.68,270.05 290.33,283.75C291.74,285.99 346.85,285.51 347.78,283.25C359.48,254.75 330.62,205.51 308.93,216.98M309.54,317.1C304.18,321.81 304.39,327.76 310.11,332.99C314.78,337.26 314.82,336.51 309.33,349.89C307.44,354.51 306.13,358.89 306.42,359.64C306.96,361.06 329,361.75 329,360.35C329,359.98 327.42,354.82 325.5,348.86C323.58,342.91 322,337.52 322,336.89C322,336.26 323.58,334 325.5,331.87C335.69,320.59 321.02,307.02 309.54,317.1"
|
||||||
|
android:strokeColor="#00000000" />
|
||||||
</group>
|
</group>
|
||||||
|
|
||||||
<group android:scaleX="0.52644"
|
<group
|
||||||
|
android:scaleX="0.52644"
|
||||||
android:scaleY="0.20707752"
|
android:scaleY="0.20707752"
|
||||||
android:translateX="320.4"
|
android:translateX="320.4"
|
||||||
android:translateY="70.26513">
|
android:translateY="70.26513">
|
||||||
<group android:translateY="132.92308">
|
<group android:translateY="132.92308">
|
||||||
<path android:pathData="M76.06154,-101.68616L87.95077,-101.68616L72.44308,0L55.606155,0L43.93846,-76.72615L32.196922,0L14.990769,0L0.6646154,-101.68616L13.513846,-101.68616L24.36923,-14.104615L36.553844,-89.870766L52.283077,-89.870766L63.876923,-14.104615Z"
|
<path
|
||||||
android:fillColor="#53BDB6"/>
|
android:fillColor="#53BDB6"
|
||||||
<path android:pathData="M135.30154,1.6984615Q123.11692,1.6984615,114.25539,-4.1723075Q105.393845,-10.0430765,100.55692,-21.821539Q95.72,-33.6,95.72,-50.88Q95.72,-67.643074,101.62769,-79.495384Q107.535385,-91.347694,117.246155,-97.32923Q126.956924,-103.31077,138.25539,-103.31077Q147.78154,-103.31077,154.42769,-100.61539Q161.07385,-97.92,167.35077,-92.16L159.59692,-84.11077Q154.94461,-88.46769,149.77539,-90.535385Q144.60616,-92.60307,138.32922,-92.60307Q130.42769,-92.60307,123.92923,-88.504616Q117.43077,-84.40615,113.44308,-75.10154Q109.45538,-65.79692,109.45538,-50.88Q109.45538,-29.095385,116.175385,-19.089231Q122.895386,-9.0830765,136.48308,-9.0830765Q146.7477,-9.0830765,155.83076,-13.9569235L155.83076,-44.97231L135.81847,-44.97231L134.26768,-55.606155L168.5323,-55.606155L168.5323,-7.163077Q160.70462,-2.88,152.98769,-0.59076923Q145.27077,1.6984615,135.30154,1.6984615Z"
|
android:pathData="M76.06154,-101.68616L87.95077,-101.68616L72.44308,0L55.606155,0L43.93846,-76.72615L32.196922,0L14.990769,0L0.6646154,-101.68616L13.513846,-101.68616L24.36923,-14.104615L36.553844,-89.870766L52.283077,-89.870766L63.876923,-14.104615Z" />
|
||||||
android:fillColor="#53BDB6"/>
|
<path
|
||||||
<path android:pathData="M348.30463,-90.683075L317.43692,-90.683075L317.43692,0L304.66153,0L304.66153,-90.683075L272.90768,-90.683075L272.90768,-101.68616L349.63385,-101.68616Z"
|
android:fillColor="#53BDB6"
|
||||||
android:fillColor="#53BDB6"/>
|
android:pathData="M135.30154,1.6984615Q123.11692,1.6984615,114.25539,-4.1723075Q105.393845,-10.0430765,100.55692,-21.821539Q95.72,-33.6,95.72,-50.88Q95.72,-67.643074,101.62769,-79.495384Q107.535385,-91.347694,117.246155,-97.32923Q126.956924,-103.31077,138.25539,-103.31077Q147.78154,-103.31077,154.42769,-100.61539Q161.07385,-97.92,167.35077,-92.16L159.59692,-84.11077Q154.94461,-88.46769,149.77539,-90.535385Q144.60616,-92.60307,138.32922,-92.60307Q130.42769,-92.60307,123.92923,-88.504616Q117.43077,-84.40615,113.44308,-75.10154Q109.45538,-65.79692,109.45538,-50.88Q109.45538,-29.095385,116.175385,-19.089231Q122.895386,-9.0830765,136.48308,-9.0830765Q146.7477,-9.0830765,155.83076,-13.9569235L155.83076,-44.97231L135.81847,-44.97231L134.26768,-55.606155L168.5323,-55.606155L168.5323,-7.163077Q160.70462,-2.88,152.98769,-0.59076923Q145.27077,1.6984615,135.30154,1.6984615Z" />
|
||||||
<path android:pathData="M383.24924,-22.670769Q383.24924,-15.064615,386.3877,-11.630769Q389.52615,-8.196923,396.32,-8.196923Q402.44922,-8.196923,408.24615,-11.741538Q414.0431,-15.286154,417.36615,-20.52923L417.36615,-77.76L429.7723,-77.76L429.7723,0L419.2123,0L418.17847,-10.486154Q413.67386,-4.726154,406.99078,-1.5138462Q400.30768,1.6984615,393.2923,1.6984615Q382.14154,1.6984615,376.4923,-4.246154Q370.84308,-10.190769,370.84308,-21.193846L370.84308,-77.76L383.24924,-77.76Z"
|
<path
|
||||||
android:fillColor="#53BDB6"/>
|
android:fillColor="#53BDB6"
|
||||||
<path android:pathData="M459.84308,0L459.84308,-77.76L470.40308,-77.76L471.36307,-66.97846Q476.01538,-72.81231,483.0677,-76.098465Q490.12,-79.38461,497.06152,-79.38461Q508.2123,-79.38461,513.56616,-73.47692Q518.92,-67.56923,518.92,-56.49231L518.92,0L506.51385,0L506.51385,-47.335384Q506.51385,-56.049232,505.59076,-60.627693Q504.6677,-65.206154,501.82462,-67.42154Q498.98154,-69.636925,493.22153,-69.636925Q486.9446,-69.636925,481.36923,-65.76Q475.79385,-61.883076,472.24924,-56.64L472.24924,0Z"
|
android:pathData="M348.30463,-90.683075L317.43692,-90.683075L317.43692,0L304.66153,0L304.66153,-90.683075L272.90768,-90.683075L272.90768,-101.68616L349.63385,-101.68616Z" />
|
||||||
android:fillColor="#53BDB6"/>
|
<path
|
||||||
<path android:pathData="M548.8431,0L548.8431,-77.76L559.4031,-77.76L560.3631,-66.97846Q565.0154,-72.81231,572.0677,-76.098465Q579.12,-79.38461,586.0615,-79.38461Q597.2123,-79.38461,602.56616,-73.47692Q607.92,-67.56923,607.92,-56.49231L607.92,0L595.51385,0L595.51385,-47.335384Q595.51385,-56.049232,594.59076,-60.627693Q593.66766,-65.206154,590.8246,-67.42154Q587.98157,-69.636925,582.22156,-69.636925Q575.94464,-69.636925,570.3692,-65.76Q564.7938,-61.883076,561.2492,-56.64L561.2492,0Z"
|
android:fillColor="#53BDB6"
|
||||||
android:fillColor="#53BDB6"/>
|
android:pathData="M383.24924,-22.670769Q383.24924,-15.064615,386.3877,-11.630769Q389.52615,-8.196923,396.32,-8.196923Q402.44922,-8.196923,408.24615,-11.741538Q414.0431,-15.286154,417.36615,-20.52923L417.36615,-77.76L429.7723,-77.76L429.7723,0L419.2123,0L418.17847,-10.486154Q413.67386,-4.726154,406.99078,-1.5138462Q400.30768,1.6984615,393.2923,1.6984615Q382.14154,1.6984615,376.4923,-4.246154Q370.84308,-10.190769,370.84308,-21.193846L370.84308,-77.76L383.24924,-77.76Z" />
|
||||||
<path android:pathData="M647.59076,-34.486153Q647.88617,-25.772308,650.9877,-19.975384Q654.08923,-14.178461,659.1108,-11.409231Q664.1323,-8.64,670.3354,-8.64Q676.0954,-8.64,680.9323,-10.338462Q685.7692,-12.036923,691.0862,-15.655385L696.92,-7.4584618Q691.4554,-3.1753845,684.4031,-0.73846155Q677.35077,1.6984615,670.1877,1.6984615Q659.0369,1.6984615,650.9877,-3.323077Q642.9385,-8.344615,638.7662,-17.50154Q634.5939,-26.65846,634.5939,-38.76923Q634.5939,-50.51077,638.8031,-59.74154Q643.0123,-68.972305,650.6923,-74.17846Q658.3723,-79.38461,668.56305,-79.38461Q678.3108,-79.38461,685.43695,-74.80615Q692.56305,-70.22769,696.36615,-61.624615Q700.16925,-53.021538,700.16925,-41.28Q700.16925,-37.883076,699.87384,-34.486153ZM668.71075,-69.19385Q659.70154,-69.19385,654.0523,-62.88Q648.4031,-56.566154,647.6646,-44.086155L688.2062,-44.086155Q687.9846,-56.344616,682.81537,-62.76923Q677.6462,-69.19385,668.71075,-69.19385Z"
|
<path
|
||||||
android:fillColor="#53BDB6"/>
|
android:fillColor="#53BDB6"
|
||||||
<path android:pathData="M757.12,-19.2Q757.12,-13.735385,760.55383,-11.187693Q763.9877,-8.64,769.96924,-8.64Q776.24615,-8.64,783.04,-11.372308L786.3631,-2.2892308Q782.8185,-0.51692307,778.0923,0.59076923Q773.36615,1.6984615,767.90155,1.6984615Q761.0339,1.6984615,755.75385,-0.8861538Q750.4738,-3.4707692,747.5939,-8.344615Q744.71387,-13.218462,744.71387,-19.864614L744.71387,-99.24923L720.8615,-99.24923L720.8615,-109.07077L757.12,-109.07077Z"
|
android:pathData="M459.84308,0L459.84308,-77.76L470.40308,-77.76L471.36307,-66.97846Q476.01538,-72.81231,483.0677,-76.098465Q490.12,-79.38461,497.06152,-79.38461Q508.2123,-79.38461,513.56616,-73.47692Q518.92,-67.56923,518.92,-56.49231L518.92,0L506.51385,0L506.51385,-47.335384Q506.51385,-56.049232,505.59076,-60.627693Q504.6677,-65.206154,501.82462,-67.42154Q498.98154,-69.636925,493.22153,-69.636925Q486.9446,-69.636925,481.36923,-65.76Q475.79385,-61.883076,472.24924,-56.64L472.24924,0Z" />
|
||||||
android:fillColor="#53BDB6"/>
|
<path
|
||||||
|
android:fillColor="#53BDB6"
|
||||||
|
android:pathData="M548.8431,0L548.8431,-77.76L559.4031,-77.76L560.3631,-66.97846Q565.0154,-72.81231,572.0677,-76.098465Q579.12,-79.38461,586.0615,-79.38461Q597.2123,-79.38461,602.56616,-73.47692Q607.92,-67.56923,607.92,-56.49231L607.92,0L595.51385,0L595.51385,-47.335384Q595.51385,-56.049232,594.59076,-60.627693Q593.66766,-65.206154,590.8246,-67.42154Q587.98157,-69.636925,582.22156,-69.636925Q575.94464,-69.636925,570.3692,-65.76Q564.7938,-61.883076,561.2492,-56.64L561.2492,0Z" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#53BDB6"
|
||||||
|
android:pathData="M647.59076,-34.486153Q647.88617,-25.772308,650.9877,-19.975384Q654.08923,-14.178461,659.1108,-11.409231Q664.1323,-8.64,670.3354,-8.64Q676.0954,-8.64,680.9323,-10.338462Q685.7692,-12.036923,691.0862,-15.655385L696.92,-7.4584618Q691.4554,-3.1753845,684.4031,-0.73846155Q677.35077,1.6984615,670.1877,1.6984615Q659.0369,1.6984615,650.9877,-3.323077Q642.9385,-8.344615,638.7662,-17.50154Q634.5939,-26.65846,634.5939,-38.76923Q634.5939,-50.51077,638.8031,-59.74154Q643.0123,-68.972305,650.6923,-74.17846Q658.3723,-79.38461,668.56305,-79.38461Q678.3108,-79.38461,685.43695,-74.80615Q692.56305,-70.22769,696.36615,-61.624615Q700.16925,-53.021538,700.16925,-41.28Q700.16925,-37.883076,699.87384,-34.486153ZM668.71075,-69.19385Q659.70154,-69.19385,654.0523,-62.88Q648.4031,-56.566154,647.6646,-44.086155L688.2062,-44.086155Q687.9846,-56.344616,682.81537,-62.76923Q677.6462,-69.19385,668.71075,-69.19385Z" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#53BDB6"
|
||||||
|
android:pathData="M757.12,-19.2Q757.12,-13.735385,760.55383,-11.187693Q763.9877,-8.64,769.96924,-8.64Q776.24615,-8.64,783.04,-11.372308L786.3631,-2.2892308Q782.8185,-0.51692307,778.0923,0.59076923Q773.36615,1.6984615,767.90155,1.6984615Q761.0339,1.6984615,755.75385,-0.8861538Q750.4738,-3.4707692,747.5939,-8.344615Q744.71387,-13.218462,744.71387,-19.864614L744.71387,-99.24923L720.8615,-99.24923L720.8615,-109.07077L757.12,-109.07077Z" />
|
||||||
</group>
|
</group>
|
||||||
</group>
|
</group>
|
||||||
</group>
|
</group>
|
||||||
|
|||||||
@@ -3,14 +3,15 @@
|
|||||||
android:height="108dp"
|
android:height="108dp"
|
||||||
android:viewportWidth="640"
|
android:viewportWidth="640"
|
||||||
android:viewportHeight="640">
|
android:viewportHeight="640">
|
||||||
<group android:scaleX="0.6"
|
<group
|
||||||
|
android:scaleX="0.6"
|
||||||
android:scaleY="0.6"
|
android:scaleY="0.6"
|
||||||
android:translateX="128"
|
android:translateX="128"
|
||||||
android:translateY="128">
|
android:translateY="128">
|
||||||
<path
|
<path
|
||||||
android:pathData="M316.72,80.15C314.94,80.82 312.08,83.95 308.79,88.84C275.66,138.15 157.88,161.96 119.66,127.08C109.97,118.24 101.21,118.84 98.97,128.5C96.01,141.29 98.49,204.07 103.12,233.5C123.71,364.32 186.77,465.69 303.03,554.88C314.06,563.34 316.63,563.42 326.93,555.61C329.91,553.35 336.21,548.63 340.93,545.13C345.64,541.63 350.87,537.46 352.55,535.88C354.23,534.29 357.92,531.53 360.75,529.74C413.56,496.43 481.74,399.04 510.38,316C527.22,267.19 534.86,236.66 539.96,197.9C547.99,136.74 545.31,124.46 526,134.01C469.85,161.74 361.02,137.16 333.39,90.49C327.63,80.75 322.93,77.84 316.72,80.15M307.5,195.34C282.24,203.76 266.16,237.38 269.85,274.04C270.99,285.39 271.18,285 264.63,285C254.13,285 254.15,284.87 255,352.05C255.64,402.94 250.1,397.02 298.5,398.52C352.54,400.2 377.63,400.23 379.23,398.63C381.67,396.18 381.86,392.86 382.46,339.89C383.09,283.93 383.39,286 374.51,286C367.07,286 367.21,286.26 367.77,273.58C369.89,225.38 338.74,184.92 307.5,195.34M308.93,216.98C294.31,224.7 281.68,270.05 290.33,283.75C291.74,285.99 346.85,285.51 347.78,283.25C359.48,254.75 330.62,205.51 308.93,216.98M309.54,317.1C304.18,321.81 304.39,327.76 310.11,332.99C314.78,337.26 314.82,336.51 309.33,349.89C307.44,354.51 306.13,358.89 306.42,359.64C306.96,361.06 329,361.75 329,360.35C329,359.98 327.42,354.82 325.5,348.86C323.58,342.91 322,337.52 322,336.89C322,336.26 323.58,334 325.5,331.87C335.69,320.59 321.02,307.02 309.54,317.1"
|
|
||||||
android:fillColor="#53bdb6"
|
android:fillColor="#53bdb6"
|
||||||
android:fillType="evenOdd"
|
android:fillType="evenOdd"
|
||||||
android:strokeColor="#00000000"/>
|
android:pathData="M316.72,80.15C314.94,80.82 312.08,83.95 308.79,88.84C275.66,138.15 157.88,161.96 119.66,127.08C109.97,118.24 101.21,118.84 98.97,128.5C96.01,141.29 98.49,204.07 103.12,233.5C123.71,364.32 186.77,465.69 303.03,554.88C314.06,563.34 316.63,563.42 326.93,555.61C329.91,553.35 336.21,548.63 340.93,545.13C345.64,541.63 350.87,537.46 352.55,535.88C354.23,534.29 357.92,531.53 360.75,529.74C413.56,496.43 481.74,399.04 510.38,316C527.22,267.19 534.86,236.66 539.96,197.9C547.99,136.74 545.31,124.46 526,134.01C469.85,161.74 361.02,137.16 333.39,90.49C327.63,80.75 322.93,77.84 316.72,80.15M307.5,195.34C282.24,203.76 266.16,237.38 269.85,274.04C270.99,285.39 271.18,285 264.63,285C254.13,285 254.15,284.87 255,352.05C255.64,402.94 250.1,397.02 298.5,398.52C352.54,400.2 377.63,400.23 379.23,398.63C381.67,396.18 381.86,392.86 382.46,339.89C383.09,283.93 383.39,286 374.51,286C367.07,286 367.21,286.26 367.77,273.58C369.89,225.38 338.74,184.92 307.5,195.34M308.93,216.98C294.31,224.7 281.68,270.05 290.33,283.75C291.74,285.99 346.85,285.51 347.78,283.25C359.48,254.75 330.62,205.51 308.93,216.98M309.54,317.1C304.18,321.81 304.39,327.76 310.11,332.99C314.78,337.26 314.82,336.51 309.33,349.89C307.44,354.51 306.13,358.89 306.42,359.64C306.96,361.06 329,361.75 329,360.35C329,359.98 327.42,354.82 325.5,348.86C323.58,342.91 322,337.52 322,336.89C322,336.26 323.58,334 325.5,331.87C335.69,320.59 321.02,307.02 309.54,317.1"
|
||||||
|
android:strokeColor="#00000000" />
|
||||||
</group>
|
</group>
|
||||||
</vector>
|
</vector>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
<background android:drawable="@color/ic_banner_background"/>
|
<background android:drawable="@color/ic_banner_background" />
|
||||||
<foreground android:drawable="@drawable/ic_banner_foreground"/>
|
<foreground android:drawable="@drawable/ic_banner_foreground" />
|
||||||
</adaptive-icon>
|
</adaptive-icon>
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<background android:drawable="@color/ic_channel_background" />
|
||||||
|
</adaptive-icon>
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
<background android:drawable="@color/ic_launcher_background"/>
|
<background android:drawable="@color/ic_launcher_background" />
|
||||||
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
|
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
||||||
<!-- support for adaptive theme icons -->
|
<!-- support for adaptive theme icons -->
|
||||||
<monochrome android:drawable="@drawable/ic_launcher_foreground"/>
|
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
|
||||||
</adaptive-icon>
|
</adaptive-icon>
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
<background android:drawable="@color/ic_launcher_background"/>
|
<background android:drawable="@color/ic_launcher_background" />
|
||||||
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
|
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
||||||
<!-- support for adaptive theme icons -->
|
<!-- support for adaptive theme icons -->
|
||||||
<monochrome android:drawable="@drawable/ic_launcher_foreground"/>
|
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
|
||||||
</adaptive-icon>
|
</adaptive-icon>
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
unqualifiedResLocale=en-US
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<string name="app_name">WG Tunnel</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="watcher_notification_text_active">Überwachung der Netzwerkänderungen: aktiv</string>
|
||||||
|
<string name="tunnels">Tunnel</string>
|
||||||
|
<string name="enable_auto_tunnel">Starte automatisches Verbinden</string>
|
||||||
|
<string name="tunnel_mobile_data">Verbinden im Mobilnetzwerk</string>
|
||||||
|
<string name="one_tunnel_required">Mindestens eine Verbindung wird für diese Funktion benötigt</string>
|
||||||
|
<string name="privacy_policy">Siehe Privacy Policy</string>
|
||||||
|
<string name="disable_auto_tunnel">Stoppe automatisches Verbinden</string>
|
||||||
|
<string name="okay">Okay</string>
|
||||||
|
<string name="tunnel_on_ethernet">Verbindung über Ethernet</string>
|
||||||
|
<string name="search_icon">Suchsymbol</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="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="tunnel_start_title">VPN verbunden</string>
|
||||||
|
<string name="tunnel_start_text">Mit Tunnel verbunden</string>
|
||||||
|
<string name="notification_permission_required">Berechtigung für die Benachrichtigung benötigt.</string>
|
||||||
|
<string name="add_trusted_ssid">Vertrauenswürdiger WiFi-Name hinzugügen</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="thank_you">Danke fürs benutzen von WG Tunnel!</string>
|
||||||
|
<string name="trusted_ssid_empty_description">SSID eingeben</string>
|
||||||
|
<string name="trusted_ssid_value_description">SSID bestätigen</string>
|
||||||
|
<string name="add_tunnels_text">Von Datei oder ZIP hinzufügen</string>
|
||||||
|
<string name="open_file">Datei geöffnet</string>
|
||||||
|
<string name="add_from_qr">Von QR-Code hinzufügen</string>
|
||||||
|
<string name="qr_scan">Scanne QR</string>
|
||||||
|
<string name="tunnel_name">Verbindungsname</string>
|
||||||
|
<string name="add_tunnel">Verbindung hinzufügen</string>
|
||||||
|
<string name="exclude">Ausgenommen</string>
|
||||||
|
<string name="include">Eingeschlossen</string>
|
||||||
|
<string name="tunnel_all">Verbinde alle Apps</string>
|
||||||
|
<string name="config_changes_saved">Änderungen der Konfiguration gespeichert.</string>
|
||||||
|
<string name="save_changes">Speichern</string>
|
||||||
|
<string name="icon">Symbol</string>
|
||||||
|
<string name="no_thanks">Nein danke</string>
|
||||||
|
<string name="turn_on">Einschalten</string>
|
||||||
|
<string name="map">Karte</string>
|
||||||
|
<string name="public_key">Öffentlicher Schlüssel</string>
|
||||||
|
<string name="addresses">Adressen</string>
|
||||||
|
<string name="dns_servers">DNS-Server</string>
|
||||||
|
<string name="mtu">MTU</string>
|
||||||
|
<string name="peer">Peer</string>
|
||||||
|
<string name="allowed_ips">Erlaubte IPs</string>
|
||||||
|
<string name="endpoint">Endpunkt</string>
|
||||||
|
<string name="name">Name</string>
|
||||||
|
<string name="restart">Verbindung neustarten</string>
|
||||||
|
<string name="vpn_connection_failed">Verbindung fehlgeschlagen</string>
|
||||||
|
<string name="always_on_vpn_support">Erlaube Always-On VPN</string>
|
||||||
|
<string name="location_services_not_detected">Standortdienste Nicht Erkannt</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="db_name">wg-tunnel-db</string>
|
||||||
|
<string name="scanning_qr">Scanne nach QR</string>
|
||||||
|
<string name="none">Keine vertrauenswürdigen WiFi Namen</string>
|
||||||
|
<string name="other">Andere</string>
|
||||||
|
<string name="vpn_on">VPN an</string>
|
||||||
|
<string name="vpn_off">VPN aus</string>
|
||||||
|
<string name="default_vpn_on">Primärer VPN an</string>
|
||||||
|
<string name="create_import">Starte von Grund auf neu</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="done">Erledigt</string>
|
||||||
|
<string name="rotate_keys">Schlüssel ändern</string>
|
||||||
|
<string name="private_key">Privater Schlüssel</string>
|
||||||
|
<string name="copy_public_key">Öffentlicher Schlüssel kopieren</string>
|
||||||
|
<string name="base64_key">base64-Schlüssel</string>
|
||||||
|
<string name="comma_separated_list">Kommaseparierte Liste</string>
|
||||||
|
</resources>
|
||||||
@@ -0,0 +1,162 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<string name="map">Mapa</string>
|
||||||
|
<string name="allowed_ips">Allowed IPs</string>
|
||||||
|
<string name="email_chooser">Enviar un email…</string>
|
||||||
|
<string name="go">ir</string>
|
||||||
|
<string name="discord_description">Únete a la comunidad</string>
|
||||||
|
<string name="kernel">Kernel</string>
|
||||||
|
<string name="restart">Reiniciar túnel</string>
|
||||||
|
<string name="vpn_connection_failed">Error de conexión</string>
|
||||||
|
<string name="error_none">Sin error</string>
|
||||||
|
<string name="other">Otros</string>
|
||||||
|
<string name="add_peer">Añadir peer</string>
|
||||||
|
<string name="done">Hecho</string>
|
||||||
|
<string name="copy_public_key">Copiar public key</string>
|
||||||
|
<string name="base64_key">clave base64</string>
|
||||||
|
<string name="exported_configs_message">Configuración exportada a Descargas</string>
|
||||||
|
<string name="tunnel_on_wifi">Túnel en Wi-Fi no de confianza</string>
|
||||||
|
<string name="location_services_missing_message">La app no detecta activado el servicio de ubicación en tu dispositivo. Dependiendo del dispositivo, esto podría hacer que la característica de Wi-Fi no de confianza falle al leer el nombre Wi-Fi. ¿Quieres continuar de todas formas?</string>
|
||||||
|
<string name="excluded">excluida(s)</string>
|
||||||
|
<string name="all">todas</string>
|
||||||
|
<string name="mobile_data_tunnel">Establecer como túnel en datos móviles</string>
|
||||||
|
<string name="use_tunnel_on_wifi_name">Usar tunnel en nombre Wi-Fi</string>
|
||||||
|
<string name="disabled">desactivado</string>
|
||||||
|
<string name="version">Versión</string>
|
||||||
|
<string name="userspace">Espacio del usuario</string>
|
||||||
|
<string name="mode">Modo</string>
|
||||||
|
<string name="support">Ayuda</string>
|
||||||
|
<string name="private_key">Clave privada</string>
|
||||||
|
<string name="trusted_ssid_value_description">Enviar SSID</string>
|
||||||
|
<string name="trusted_ssid_empty_description">Introducir SSID</string>
|
||||||
|
<string name="add_tunnels_text">Añadir desde archivo o zip</string>
|
||||||
|
<string name="open_file">Abrir archivo</string>
|
||||||
|
<string name="add_from_qr">Añadir mediante código QR</string>
|
||||||
|
<string name="qr_scan">Escanear QR</string>
|
||||||
|
<string name="tunnel_name">Nombre de túnel</string>
|
||||||
|
<string name="add_tunnel">Añadir túnel</string>
|
||||||
|
<string name="exclude">Excluir</string>
|
||||||
|
<string name="include">Incluir</string>
|
||||||
|
<string name="tunnel_all">Todas las apps por el túnel</string>
|
||||||
|
<string name="config_changes_saved">Cambios de configuración guardados.</string>
|
||||||
|
<string name="save_changes">Guardar</string>
|
||||||
|
<string name="icon">Icono</string>
|
||||||
|
<string name="no_thanks">No gracias</string>
|
||||||
|
<string name="turn_on">Activar</string>
|
||||||
|
<string name="mtu">MTU</string>
|
||||||
|
<string name="dns_servers">DNS servers</string>
|
||||||
|
<string name="addresses">Addresses</string>
|
||||||
|
<string name="public_key">Public key</string>
|
||||||
|
<string name="error_file_extension">No es un archivo .conf o .zip</string>
|
||||||
|
<string name="turn_off_tunnel">Desactiva antes el túnel</string>
|
||||||
|
<string name="no_tunnels">¡Ningún túnel añadido aún!</string>
|
||||||
|
<string name="watcher_notification_text_paused">Monitoreando cambios de red: En pausa</string>
|
||||||
|
<string name="watcher_notification_text_active">Monitoreando cambios de red: Activado</string>
|
||||||
|
<string name="tunnel_start_title">VPN conectada</string>
|
||||||
|
<string name="tunnel_start_text">Conectado al túnel</string>
|
||||||
|
<string name="notification_permission_required">Necesita permiso de notificaciones.</string>
|
||||||
|
<string name="add_trusted_ssid">Añadir nombres Wi-Fi de confianza</string>
|
||||||
|
<string name="tunnels">Túneles</string>
|
||||||
|
<string name="enable_auto_tunnel">Iniciar túnel-automático</string>
|
||||||
|
<string name="disable_auto_tunnel">Parar túnel-automático</string>
|
||||||
|
<string name="tunnel_mobile_data">Activar túnel en datos móviles</string>
|
||||||
|
<string name="one_tunnel_required">Esta característica necesita ser usada en almenos por un túnel</string>
|
||||||
|
<string name="privacy_policy">Ver Política de Privacidad</string>
|
||||||
|
<string name="okay">OK</string>
|
||||||
|
<string name="tunnel_on_ethernet">Túnel en ethernet</string>
|
||||||
|
<string name="prominent_background_location_title">Divulgación de la ubicación en segundo plano</string>
|
||||||
|
<string name="thank_you">¡Gracias por usar WG Tunnel!</string>
|
||||||
|
<string name="endpoint">Endpoint</string>
|
||||||
|
<string name="peer">Peer</string>
|
||||||
|
<string name="name">Nombre</string>
|
||||||
|
<string name="always_on_vpn_support">Permitir VPN siempre-activada</string>
|
||||||
|
<string name="location_services_not_detected">Servicios de Ubicación No Detectados</string>
|
||||||
|
<string name="hint_search_packages">Buscar paquetes</string>
|
||||||
|
<string name="clear_icon">Icono claro</string>
|
||||||
|
<string name="attempt_connection">Intentando conexión...</string>
|
||||||
|
<string name="vpn_starting">Iniciando VPN</string>
|
||||||
|
<string name="db_name">wg-tunnel-db</string>
|
||||||
|
<string name="scanning_qr">Escaneando QR</string>
|
||||||
|
<string name="none">Sin nombres Wi-Fi de confianza</string>
|
||||||
|
<string name="auto_tunneling">Túnel-automático</string>
|
||||||
|
<string name="vpn_on">VPN on</string>
|
||||||
|
<string name="vpn_off">VPN off</string>
|
||||||
|
<string name="default_vpn_on">VPN Principal on</string>
|
||||||
|
<string name="default_vpn_off">VPN Principal off</string>
|
||||||
|
<string name="create_import">Crear desde cero</string>
|
||||||
|
<string name="turn_off_auto">La acción necesita que túnel-automático esté desactivado o en pausa</string>
|
||||||
|
<string name="turn_on_tunnel">La acción necesita un túnel activado</string>
|
||||||
|
<string name="rotate_keys">Rotar claves</string>
|
||||||
|
<string name="comma_separated_list">lista separada por comas</string>
|
||||||
|
<string name="random">(aleatorio)</string>
|
||||||
|
<string name="optional">(opcional)</string>
|
||||||
|
<string name="optional_no_recommend">(opcional, no recomendado)</string>
|
||||||
|
<string name="seconds">segundos</string>
|
||||||
|
<string name="cancel">Cancelar</string>
|
||||||
|
<string name="error_authentication_failed">Fallo de autenticación</string>
|
||||||
|
<string name="error_authorization_failed">Fallo de autorización</string>
|
||||||
|
<string name="enabled_app_shortcuts">Habilitar acesos directos de app</string>
|
||||||
|
<string name="export_configs">Exportar configuración</string>
|
||||||
|
<string name="background_location_required">Se necesita ubicación en segundo plano</string>
|
||||||
|
<string name="location_services_required">Se necesita servicio de ubicación</string>
|
||||||
|
<string name="precise_location_required">Necesita ubicación precisa</string>
|
||||||
|
<string name="unknown_error">Error desconocido</string>
|
||||||
|
<string name="email_subject">Ayuda WG Tunnel</string>
|
||||||
|
<string name="interface_">Interfaz</string>
|
||||||
|
<string name="listen_port">Puerto de escucha</string>
|
||||||
|
<string name="preshared_key">Clave previamente compartida</string>
|
||||||
|
<string name="persistent_keepalive">Keepalive persistente</string>
|
||||||
|
<string name="search_icon">Icono de búsqueda</string>
|
||||||
|
<string name="docs_description">Leer documentación</string>
|
||||||
|
<string name="email_description">Envíame un email</string>
|
||||||
|
<string name="support_help_text">Si tienes problemas, ideas para mejoras, o simlemente comprometerte, tienes disponibles los siguientes recursos:</string>
|
||||||
|
<string name="use_kernel">Usar módulo del Kernel</string>
|
||||||
|
<string name="error_ssid_exists">SSID existente</string>
|
||||||
|
<string name="error_root_denied">Shell root denegado</string>
|
||||||
|
<string name="error_no_file_explorer">Explorador de archivos no instalado</string>
|
||||||
|
<string name="error_invalid_code">Código QR no valido</string>
|
||||||
|
<string name="auto_tunnel_title">Servicio túnel-automático</string>
|
||||||
|
<string name="delete_tunnel">Eliminar túnel</string>
|
||||||
|
<string name="delete_tunnel_message">¿Estás seguro de que quieres eliminar este túnel?</string>
|
||||||
|
<string name="yes">Sí</string>
|
||||||
|
<string name="resume">Reanudar</string>
|
||||||
|
<string name="pause">Pausar</string>
|
||||||
|
<string name="active">activado</string>
|
||||||
|
<string name="paused">en pausa</string>
|
||||||
|
<string name="tunneling_apps">Apps por el túnel</string>
|
||||||
|
<string name="included">incluida(s)</string>
|
||||||
|
<string name="always_on_disabled">VPN siempre-activada ha intentado iniciar un túnel, pero está característica está deshabilitada en los ajustes.</string>
|
||||||
|
<string name="no_email_detected">Ninguna app de email detectada</string>
|
||||||
|
<string name="no_browser_detected">Ningún navegador detectado</string>
|
||||||
|
<string name="logs_saved">Registros guardados en Descargas</string>
|
||||||
|
<string name="open_issue">Abrir una incidencia</string>
|
||||||
|
<string name="read_logs">Leer los registros</string>
|
||||||
|
<string name="auto">(automático)</string>
|
||||||
|
<string name="config_parse_error">Fallo al analizar la configuración</string>
|
||||||
|
<string name="incorrect_pin">El pin no es correcto</string>
|
||||||
|
<string name="pin_created">Pin creado con éxito</string>
|
||||||
|
<string name="enter_pin">Introduce tu pin</string>
|
||||||
|
<string name="create_pin">Crear pin</string>
|
||||||
|
<string name="enable_app_lock">Bloqueo de app activado</string>
|
||||||
|
<string name="restart_on_ping">Reiniciar al fallar ping (beta)</string>
|
||||||
|
<string name="set_primary_tunnel">Establecer como túnel Principal</string>
|
||||||
|
<string name="no_wifi_names_configured">No hay nombres Wi-Fi configurados para este túnel</string>
|
||||||
|
<string name="general">General</string>
|
||||||
|
<string name="edit_tunnel">Editar túnel</string>
|
||||||
|
<string name="auto_on">Reanudar túnel-automático</string>
|
||||||
|
<string name="auto_off">Pausar túnel-automático</string>
|
||||||
|
<string name="auto_tun_on">Reanudar el túnel automático</string>
|
||||||
|
<string name="auto_tun_off">Pausa del túnel automático</string>
|
||||||
|
<string name="settings">Ajustes</string>
|
||||||
|
<string name="app_name">WG Tunnel</string>
|
||||||
|
<string name="vpn_channel_id">Canal VPN</string>
|
||||||
|
<string name="vpn_channel_name">Canal de notificación VPN</string>
|
||||||
|
<string name="watcher_channel_id">Canal del obvervador</string>
|
||||||
|
<string name="watcher_channel_name">Canal de notificación del obvervador</string>
|
||||||
|
<string name="prominent_background_location_message">La monitorización SSID Wi-Fi necesita de permiso de ubicación en segundo plano incluso si la app está cerrada. Mira el enlace a la Política de Privacidad en la pantalla de ayuda para más detalles.</string>
|
||||||
|
<string name="privacy_policy_url" translatable="false"></string>
|
||||||
|
<string name="docs_url" translatable="false"></string>
|
||||||
|
<string name="github_url" translatable="false"></string>
|
||||||
|
<string name="discord_url" translatable="false"></string>
|
||||||
|
<string name="my_email" translatable="false"></string>
|
||||||
|
</resources>
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
</resources>
|
||||||
@@ -0,0 +1,162 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<string name="app_name">WG Tunnel</string>
|
||||||
|
<string name="vpn_channel_id">VPN Kanalı</string>
|
||||||
|
<string name="vpn_channel_name">VPN Bildirim Kanalı</string>
|
||||||
|
<string name="watcher_channel_id">İzleyici Kanalı</string>
|
||||||
|
<string name="watcher_channel_name">İzleyici Bildirim Kanalı</string>
|
||||||
|
<string name="github_url" translatable="false">https://github.com/zaneschepke/wgtunnel/issues</string>
|
||||||
|
<string name="docs_url" translatable="false">https://zaneschepke.com/wgtunnel-docs/overview.html</string>
|
||||||
|
<string name="privacy_policy_url" translatable="false">https://zaneschepke.com/wgtunnel-docs/privacypolicy.html</string>
|
||||||
|
<string name="error_file_extension">Dosya bir .conf veya .zip değil</string>
|
||||||
|
<string name="turn_off_tunnel">Eylem tünelin kapalı olmasını gerektiriyor</string>
|
||||||
|
<string name="no_tunnels">Henüz bir tünel ekli değil!</string>
|
||||||
|
<string name="discord_url" translatable="false">https://discord.gg/rbRRNh6H7V</string>
|
||||||
|
<string name="watcher_notification_text_active">Ağ durumu değişikliklerini izleme: etkin</string>
|
||||||
|
<string name="watcher_notification_text_paused">Ağ durumu değişikliklerini izleme: duraklatıldı</string>
|
||||||
|
<string name="tunnel_start_title">VPN bağlandı</string>
|
||||||
|
<string name="tunnel_start_text">Tünele bağlı</string>
|
||||||
|
<string name="notification_permission_required">Bildirim izni gerekli.</string>
|
||||||
|
<string name="add_trusted_ssid">Güvenilir Wi-Fi adı ekle</string>
|
||||||
|
<string name="tunnels">Tüneller</string>
|
||||||
|
<string name="enable_auto_tunnel">Otomatik tünellemeyi başlat</string>
|
||||||
|
<string name="disable_auto_tunnel">Otomatik tünellemeyi durdur</string>
|
||||||
|
<string name="tunnel_mobile_data">Mobil veri üzerinde tünel</string>
|
||||||
|
<string name="one_tunnel_required">Bu özelliği kullanabilmek için en az bir tünel gereklidir.</string>
|
||||||
|
<string name="privacy_policy">Gizlilik Politikası</string>
|
||||||
|
<string name="okay">Tamam</string>
|
||||||
|
<string name="tunnel_on_ethernet">Ethernet üzerinde tünel</string>
|
||||||
|
<string name="prominent_background_location_message">Bu özellik, uygulama kapalıyken bile Wi-Fi SSID izlemesini etkinleştirmek için arka plan konumu izni gerektirir. Daha fazla bilgi için lütfen Destek ekranında bağlantısı verilen Gizlilik Politikasına bakın.</string>
|
||||||
|
<string name="prominent_background_location_title">Arka Plan Konum Açıklaması</string>
|
||||||
|
<string name="thank_you">WG Tunnel\'i kullandığınız için teşekkür ederiz!</string>
|
||||||
|
<string name="trusted_ssid_empty_description">SSID\'yi gir</string>
|
||||||
|
<string name="trusted_ssid_value_description">SSID\'yi gönder</string>
|
||||||
|
<string name="add_tunnels_text">Dosyadan veya ZIP\'ten ekle</string>
|
||||||
|
<string name="open_file">Dosya Aç</string>
|
||||||
|
<string name="add_from_qr">QR kodundan ekle</string>
|
||||||
|
<string name="qr_scan">QR kodu tarat</string>
|
||||||
|
<string name="tunnel_name">Tünel adı</string>
|
||||||
|
<string name="add_tunnel">Tünel ekle</string>
|
||||||
|
<string name="exclude">Hariç tut</string>
|
||||||
|
<string name="include">Dahil et</string>
|
||||||
|
<string name="tunnel_all">Tüm uygulamaları dahil et</string>
|
||||||
|
<string name="config_changes_saved">Yapılandırma değişiklikleri kaydedildi.</string>
|
||||||
|
<string name="save_changes">Kaydet</string>
|
||||||
|
<string name="icon">Simge</string>
|
||||||
|
<string name="no_thanks">Hayır, teşekkürler</string>
|
||||||
|
<string name="turn_on">Aç</string>
|
||||||
|
<string name="map">Harita</string>
|
||||||
|
<string name="public_key">Genel anahtar</string>
|
||||||
|
<string name="addresses">Adresler</string>
|
||||||
|
<string name="dns_servers">DNS sunucuları</string>
|
||||||
|
<string name="mtu">MTU</string>
|
||||||
|
<string name="peer">Eş (peer)</string>
|
||||||
|
<string name="allowed_ips">İzin verilen IP\'ler</string>
|
||||||
|
<string name="endpoint">Uç nokta (endpoint)</string>
|
||||||
|
<string name="name">Ad</string>
|
||||||
|
<string name="restart">Tüneli Yeniden Başlat</string>
|
||||||
|
<string name="vpn_connection_failed">Bağlantı kurulamadı</string>
|
||||||
|
<string name="always_on_vpn_support">Her Zaman Açık VPN\'e İzin Ver</string>
|
||||||
|
<string name="location_services_not_detected">Konum Hizmetleri Algılanmadı</string>
|
||||||
|
<string name="hint_search_packages">Uygulama arayın</string>
|
||||||
|
<string name="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="vpn_starting">VPN başlatılıyor</string>
|
||||||
|
<string name="db_name">wg-tunnel-db</string>
|
||||||
|
<string name="scanning_qr">QR taranıyor</string>
|
||||||
|
<string name="none">Güvenilir Wi-Fi adı yok</string>
|
||||||
|
<string name="other">Diğer</string>
|
||||||
|
<string name="auto_tunneling">Otomatik tünelleme</string>
|
||||||
|
<string name="vpn_on">VPN açık</string>
|
||||||
|
<string name="vpn_off">VPN kapalı</string>
|
||||||
|
<string name="default_vpn_on">Birincil VPN açık</string>
|
||||||
|
<string name="default_vpn_off">Birincil VPN kapalı</string>
|
||||||
|
<string name="create_import">Sıfırdan oluştur</string>
|
||||||
|
<string name="turn_off_auto">Eylem, otomatik tünelin devre dışı bırakılmasını veya duraklatılmasını gerektirir</string>
|
||||||
|
<string name="turn_on_tunnel">Eylem aktif tünel gerektirir</string>
|
||||||
|
<string name="add_peer">Eş (peer) ekle</string>
|
||||||
|
<string name="done">Tamam</string>
|
||||||
|
<string name="interface_">Arayüz</string>
|
||||||
|
<string name="rotate_keys">Tuşları döndür</string>
|
||||||
|
<string name="private_key">Özel anahtar</string>
|
||||||
|
<string name="copy_public_key">Genel anahtarı kopyala</string>
|
||||||
|
<string name="base64_key">Base64 anahtarı</string>
|
||||||
|
<string name="comma_separated_list">Virgül ile ayrılmış liste ekleyin</string>
|
||||||
|
<string name="listen_port">Bağlantı noktası</string>
|
||||||
|
<string name="random">(Rastgele)</string>
|
||||||
|
<string name="optional">(İsteğe bağlı)</string>
|
||||||
|
<string name="optional_no_recommend">(İsteğe bağlı, önerilmez)</string>
|
||||||
|
<string name="preshared_key">Ön paylaşımlı anahtar</string>
|
||||||
|
<string name="seconds">saniye</string>
|
||||||
|
<string name="persistent_keepalive">Kalıcı olarak canlı tutma</string>
|
||||||
|
<string name="cancel">İptal</string>
|
||||||
|
<string name="error_authentication_failed">Kimlik doğrulama başarısız oldu</string>
|
||||||
|
<string name="error_authorization_failed">Yetkilendirilemedi</string>
|
||||||
|
<string name="enabled_app_shortcuts">Uygulama kısayollarını etkinleştir</string>
|
||||||
|
<string name="export_configs">Yapılandırmaları dışa aktar</string>
|
||||||
|
<string name="location_services_required">Gerekli konum hizmetleri</string>
|
||||||
|
<string name="background_location_required">Arka plan konumu gerekli</string>
|
||||||
|
<string name="precise_location_required">Kesin konum gerekli</string>
|
||||||
|
<string name="unknown_error">Bilinmeyen bir hata oluştu</string>
|
||||||
|
<string name="exported_configs_message">Yapılandırmalar indirilenler\'e aktarıldı</string>
|
||||||
|
<string name="tunnel_on_wifi">Güvenilmeyen kablosuz internet bağlantısında tünel</string>
|
||||||
|
<string name="my_email" translatable="false">support@zaneschepke.com</string>
|
||||||
|
<string name="email_subject">WG Tunnel Desteği</string>
|
||||||
|
<string name="email_chooser">Bir e-posta gönderin…</string>
|
||||||
|
<string name="go">git</string>
|
||||||
|
<string name="docs_description">Belgeleri oku</string>
|
||||||
|
<string name="discord_description">Topluluğa katıl</string>
|
||||||
|
<string name="email_description">Bana e-posta gönder</string>
|
||||||
|
<string name="support_help_text">Sorun yaşıyorsanız, iyileştirme fikirleriniz varsa veya sadece etkileşimde bulunmak istiyorsanız, aşağıdaki kaynaklar mevcuttur:</string>
|
||||||
|
<string name="kernel">Çekirdek</string>
|
||||||
|
<string name="use_kernel">Çekirdek modülünü kullan</string>
|
||||||
|
<string name="error_ssid_exists">SSID zaten var</string>
|
||||||
|
<string name="error_root_denied">Kök kabuğu reddedildi</string>
|
||||||
|
<string name="error_no_file_explorer">Yüklü dosya gezgini yok</string>
|
||||||
|
<string name="error_invalid_code">Geçersiz QR kodu</string>
|
||||||
|
<string name="error_none">Hata yok</string>
|
||||||
|
<string name="location_services_missing_message">Uygulama, cihazınızda etkinleştirilen herhangi bir konum hizmeti algılamıyor. Cihaza bağlı olarak bu, güvenilmeyen Wi-Fi özelliğinin Wi-Fi adını okuyamamasına neden olabilir. Yine de devam etmek ister misiniz?</string>
|
||||||
|
<string name="auto_tunnel_title">Otomatik Tünel Hizmeti</string>
|
||||||
|
<string name="delete_tunnel">Tüneli sil</string>
|
||||||
|
<string name="delete_tunnel_message">Bu tüneli silmek istediğinizden emin misiniz?</string>
|
||||||
|
<string name="yes">Evet</string>
|
||||||
|
<string name="resume">Devam et</string>
|
||||||
|
<string name="pause">Duraklat</string>
|
||||||
|
<string name="paused">duraklatıldı</string>
|
||||||
|
<string name="active">aktif</string>
|
||||||
|
<string name="tunneling_apps">Tünellenen uygulamalar</string>
|
||||||
|
<string name="included">dahil edildi</string>
|
||||||
|
<string name="excluded">hariç tutuldu</string>
|
||||||
|
<string name="all">tümü</string>
|
||||||
|
<string name="always_on_disabled">Her zaman açık VPN bir tünel başlatmaya çalıştı, ancak bu özellik ayarlarda devre dışı bırakıldı.</string>
|
||||||
|
<string name="no_email_detected">E-posta uygulaması algılanmadı</string>
|
||||||
|
<string name="no_browser_detected">Tarayıcı algılanmadı</string>
|
||||||
|
<string name="logs_saved">İndirilenlere kaydedilen günlükler</string>
|
||||||
|
<string name="open_issue">Bir sorun bildir</string>
|
||||||
|
<string name="read_logs">Günlükleri oku</string>
|
||||||
|
<string name="auto">(Otomatik)</string>
|
||||||
|
<string name="config_parse_error">Yapılandırma kaydedilemedi</string>
|
||||||
|
<string name="incorrect_pin">PIN kodu yanlış</string>
|
||||||
|
<string name="pin_created">Pin başarıyla oluşturuldu</string>
|
||||||
|
<string name="enter_pin">PIN kodunuzu girin</string>
|
||||||
|
<string name="create_pin">PIN kodu oluştur</string>
|
||||||
|
<string name="enable_app_lock">Uygulama kilidini etkinleştir</string>
|
||||||
|
<string name="restart_on_ping">Ping başarısız olduğunda yeniden başlat (beta)</string>
|
||||||
|
<string name="mobile_data_tunnel">Mobil veri tüneli olarak ayarla</string>
|
||||||
|
<string name="set_primary_tunnel">Birincil tünel olarak ayarla</string>
|
||||||
|
<string name="use_tunnel_on_wifi_name">Wi-Fi adında tünel kullan</string>
|
||||||
|
<string name="no_wifi_names_configured">Bu tünel için yapılandırılmış Wi-Fi adı yok</string>
|
||||||
|
<string name="general">Genel</string>
|
||||||
|
<string name="edit_tunnel">Tüneli düzenle</string>
|
||||||
|
<string name="disabled">Devre dışı</string>
|
||||||
|
<string name="auto_on">Otomatik ayarlamayı sürdür</string>
|
||||||
|
<string name="auto_off">Otomatik ayarlamayı duraklat</string>
|
||||||
|
<string name="auto_tun_on">Otomatik tüneli sürdür</string>
|
||||||
|
<string name="auto_tun_off">Otomatik tüneli duraklat</string>
|
||||||
|
<string name="version">Sürüm</string>
|
||||||
|
<string name="mode">Mod</string>
|
||||||
|
<string name="userspace">Kullanıcı alanı</string>
|
||||||
|
<string name="settings">Ayarlar</string>
|
||||||
|
<string name="support">Destek</string>
|
||||||
|
</resources>
|
||||||
@@ -1,11 +1,4 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources>
|
<resources>
|
||||||
<color name="purple_200">#FFBB86FC</color>
|
|
||||||
<color name="purple_500">#FF6200EE</color>
|
|
||||||
<color name="purple_700">#FF3700B3</color>
|
|
||||||
<color name="teal_200">#FF03DAC5</color>
|
|
||||||
<color name="teal_700">#FF018786</color>
|
|
||||||
<color name="black_background">#FF1c1b20</color>
|
<color name="black_background">#FF1c1b20</color>
|
||||||
<color name="black">#FF000000</color>
|
|
||||||
<color name="white">#FFFFFFFF</color>
|
|
||||||
</resources>
|
</resources>
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<color name="ic_channel_background">#1D1A20</color>
|
||||||
|
</resources>
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user