Compare commits

..

40 Commits

Author SHA1 Message Date
Zane Schepke 6f48147b3e feat: add amneziawg support 2024-04-27 21:44:41 -04:00
Zane Schepke 16979dbb2b Update ic_channel.xml 2024-04-26 01:50:08 -04:00
Hosted Weblate 5d8190628d Update translation files
Updated by "Remove blank strings" hook in Weblate.

Translation: WG Tunnel/strings
Translate-URL: https://hosted.weblate.org/projects/wg-tunnel/strings/
2024-04-26 01:50:08 -04:00
Anonymous 6d597a235d Translated using Weblate (Spanish)
Currently translated at 100.0% (159 of 159 strings)

Translation: WG Tunnel/strings
Translate-URL: https://hosted.weblate.org/projects/wg-tunnel/strings/es/
2024-04-26 01:49:49 -04:00
Anonymous 61fd2f01b9 Translated using Weblate (German)
Currently translated at 49.0% (78 of 159 strings)

Translation: WG Tunnel/strings
Translate-URL: https://hosted.weblate.org/projects/wg-tunnel/strings/de/
2024-04-26 01:49:08 -04:00
Anonymous 914a641977 Translated using Weblate (Russian)
Currently translated at 3.1% (5 of 159 strings)

Translation: WG Tunnel/strings
Translate-URL: https://hosted.weblate.org/projects/wg-tunnel/strings/ru/
2024-04-26 01:49:08 -04:00
Anonymous c97a3afbc4 Translated using Weblate (Turkish)
Currently translated at 100.0% (159 of 159 strings)

Translation: WG Tunnel/strings
Translate-URL: https://hosted.weblate.org/projects/wg-tunnel/strings/tr/
2024-04-26 01:49:08 -04:00
gallegonovato fadc2d1562 Translated using Weblate (Spanish)
Currently translated at 100.0% (159 of 159 strings)

Translation: WG Tunnel/strings
Translate-URL: https://hosted.weblate.org/projects/wg-tunnel/strings/es/
2024-04-26 01:49:08 -04:00
Languages add-on 106ab76b82 Added translation using Weblate (Spanish) 2024-04-26 01:48:39 -04:00
Weblate (bot) 48b3f60b37 Translations update from Hosted Weblate (#177)
Co-authored-by: Languages add-on <noreply-addon-languages@weblate.org>
Co-authored-by: Gabriel Franz <gabrielfranz31@gmail.com>
Co-authored-by: Saratoga79 <ordizi79@gmail.com>
2024-04-24 03:49:57 -04:00
WINZORT e7f5cee6dd Add fastlane tr (#173) 2024-04-22 23:21:19 -04:00
Zane Schepke 5e01fd6c85 Merge pull request #175 from zaneschepke/i18n
Translations update from Hosted Weblate
2024-04-22 23:18:46 -04:00
Dominik Thalhammer dcbd72c6f6 Translated using Weblate (German)
Currently translated at 40.9% (9 of 22 strings)

Translation: WG Tunnel/fastlane
Translate-URL: https://hosted.weblate.org/projects/wg-tunnel/fastlane/de/
2024-04-22 13:18:21 +02:00
Languages add-on f27aa1452a Added translation using Weblate (German) 2024-04-22 07:52:45 +02:00
Languages add-on 82c3521c58 Added translation using Weblate (Russian) 2024-04-21 06:19:53 +02:00
Zane Schepke c6535ffe7e chore: update chats 2024-04-20 18:36:40 -04:00
Zane Schepke be5fe94ce6 chore: cleanup unused strings 2024-04-20 17:42:14 -04:00
Zane Schepke 1e905c1cfb chore: update README.md 2024-04-20 17:09:36 -04:00
Zane Schepke a549c111aa Merge pull request #170 from yurtpage/i18n
i18n
2024-04-20 16:28:15 -04:00
Zane Schepke 5ab344044e chore: add Weblate details to README 2024-04-20 14:30:21 -04:00
Zane Schepke 7030f68548 chore: update README 2024-04-20 13:57:33 -04:00
Zane Schepke ecd51b6fe5 chore: add code of conduct 2024-04-20 13:52:28 -04:00
Yurt Page 3e4f8e0791 fastlane i18n ru
Signed-off-by: Yurt Page <yurtpage@gmail.com>
2024-04-19 18:28:58 +03:00
Zane Schepke e940a0dbc5 Merge pull request #167 from mikropsoft/main
Improve tr locales
2024-04-18 22:12:21 -04:00
WINZORT 8e233475df Update strings.xml 2024-04-17 17:41:49 +03:00
WINZORT 24789826ad Update strings.xml 2024-04-17 17:38:00 +03:00
WINZORT 787e0d1a71 Update strings.xml 2024-04-17 17:37:22 +03:00
WINZORT 914ef53ef0 Improve tr locales 2024-04-17 17:23:27 +03:00
Zane Schepke a569974beb add Turkish localization 2024-04-16 16:13:41 -04:00
Zane Schepke 87bc89b6f1 Merge branch 'main' of https://github.com/zaneschepke/wgtunnel 2024-04-16 00:50:43 -04:00
Zane Schepke c343220e96 Merge pull request #156 from mikropsoft/main
Add tr locales
2024-04-16 00:41:14 -04:00
Zane Schepke 5447ec73f7 fix: stop tunnel regression
Fixes regression where tunnel is stuck in on state after x amount of toggles. Closes #163

Adds obfuscation of potentially sensitive data from logs.
Closes #160

Adds hiding of FAB on scroll to allow users to toggle tunnels when they have many tunnel configs.
Closes #161
2024-04-16 00:14:06 -04:00
Zane Schepke a2b8eb5b0b ci: update release wording 2024-04-06 11:06:19 -04:00
Zane Schepke e37777e662 fix: improve auto-tunnel reliability
Improved service manager stop service command to improve reliability
Closes #148

Added assets required by Google Play for AndroidTV to fix Google Play release

Added apk fingerprint hash to release to allow verification of apk signature via apksigner

Improve tile sync when tiles are first added

Bump versions
2024-04-06 10:43:00 -04:00
WINZORT ee3fcabcf1 Add tr locales 2024-04-06 15:39:46 +03:00
Zane Schepke 2a8895ffbc docs: update readme, add security 2024-03-30 23:16:19 -04:00
Zane Schepke b1fdb5b9b2 feat: auto-tunneling flexibility
Added tunnel settings feature where users can configure a tunnel to be used on certain SSID or with mobile data.
Closes #50

Added feature where if a tunnel was active when phone restarted, the app will start that tunnel on boot.

Removed automatic auto-tunnel toggling/override from the tunnel tile and app shortcuts as it can cause undesirable behavior.

Added second tile to control auto-tunneling pause/resume state from a tile.

Added two additional static shortcuts to be able to control auto-tunneling pause/resume state from shortcuts.

Fixed bug where crashes can happen from serializing and deserializing tunnel configs by removing the need for serialization of tunnel configs.

Refactored logic of watcher and tunnel service to make state more predictable.
#127

Fixed bug where rapidly toggling tunnels can cause crashes.
Closes #145

Improved how tunnels are manually toggled from one to another.

Improved logic/storage around primary tunnel behavior.

Fixes issue where info level logs were not populating on release builds.

Increase allowed name length displayed in UI.
Closes #143

Fixes bug where androidTV could crash in certain situations.

Bump versions.

Updated screenshots.
2024-03-30 00:00:35 -04:00
Zane Schepke 1d644748e5 Update README.md 2024-03-18 23:48:38 -04:00
Zane Schepke 61989b596f Merge pull request #135 from zaneschepke/dependabot/github_actions/softprops/action-gh-release-2
build(deps): bump softprops/action-gh-release from 1 to 2
2024-03-18 23:40:46 -04:00
dependabot[bot] 7ca0db3a40 build(deps): bump softprops/action-gh-release from 1 to 2
Bumps [softprops/action-gh-release](https://github.com/softprops/action-gh-release) from 1 to 2.
- [Release notes](https://github.com/softprops/action-gh-release/releases)
- [Changelog](https://github.com/softprops/action-gh-release/blob/master/CHANGELOG.md)
- [Commits](https://github.com/softprops/action-gh-release/compare/v1...v2)

---
updated-dependencies:
- dependency-name: softprops/action-gh-release
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-03-11 13:36:55 +00:00
147 changed files with 3155 additions and 1333 deletions
+22
View File
@@ -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>.
+27 -1
View File
@@ -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:
+27 -1
View File
@@ -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:
+27 -29
View File
@@ -4,8 +4,9 @@ WG Tunnel
<div align="center"> <div align="center">
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) [![Discord](https://img.shields.io/badge/Discord-%235865F2.svg?style=for-the-badge&logo=discord&logoColor=white)](https://discord.gg/rbRRNh6H7V)
[![Discord Chat](https://img.shields.io/discord/1108285024631001111.svg)](https://discord.gg/rbRRNh6H7V) [![X Community](https://img.shields.io/badge/X-000000?style=for-the-badge&logo=x&logoColor=white)](https://twitter.com/i/communities/1780655267685736818)
[![Telegram](https://img.shields.io/badge/Telegram-2CA5E0?style=for-the-badge&logo=telegram&logoColor=white)](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/).\
[![Translation status](https://hosted.weblate.org/widgets/wg-tunnel/-/multi-auto.svg)](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
``` ```
+5
View File
@@ -0,0 +1,5 @@
# Security Policy
## Reporting a Vulnerability
Please report security issues to `support@zaneschepke.com`
+6 -4
View File
@@ -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.
} }
+21 -2
View File
@@ -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()
@@ -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>>
} }
@@ -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)
@@ -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
}
@@ -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,
),
)
}
}
}
@@ -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>
}
@@ -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()
}
}
@@ -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)
@@ -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,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
} }
@@ -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)
}
} }
} }
} }
@@ -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
} }
@@ -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
} }
} }
@@ -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,
) { ) {
@@ -1,6 +0,0 @@
package com.zaneschepke.wireguardautotunnel.service.foreground
enum class ServiceState {
STARTED,
STOPPED,
}
@@ -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()))
}
}
@@ -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")
} }
} }
} }
@@ -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")
} }
} }
} }
@@ -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) {
@@ -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()
} }
} }
} }
@@ -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()
}
}
@@ -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
) )
@@ -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")
} }
@@ -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),
) )
} }
@@ -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,
@@ -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(
@@ -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
} }
@@ -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,
) { ) {
@@ -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() }
} }
@@ -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()
} }
} }
@@ -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 = "",
@@ -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(
@@ -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))
@@ -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()
@@ -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,
),
)
} }
} }
} }
@@ -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,
)
}
}
},
)
}
}
}
}
}
@@ -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
)
@@ -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)
}
}
}
@@ -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),
)
}, },
) )
} }
@@ -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
@@ -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(
@@ -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)
} }
} }
} }
@@ -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 =
@@ -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

+9
View File
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android: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>
+9
View File
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android: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>
+1
View File
@@ -0,0 +1 @@
unqualifiedResLocale=en-US
+76
View File
@@ -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>
+162
View File
@@ -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"></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>
+3
View File
@@ -0,0 +1,3 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
</resources>
+162
View File
@@ -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"></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>
-7
View File
@@ -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