mirror of
https://github.com/wgtunnel/android.git
synced 2026-07-03 14:07:49 +02:00
Compare commits
19 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 34d71a6096 | |||
| ff87bee3b4 | |||
| 9739d35eda | |||
| 11ad494fbb | |||
| 90b006acc5 | |||
| eb7b39c379 | |||
| 0a17593310 | |||
| c0e58125dd | |||
| 3791261f91 | |||
| d1e61be3ae | |||
| afd4fb127f | |||
| e0cce8fba4 | |||
| b70ecbdfff | |||
| 513d08998b | |||
| 79583e0e61 | |||
| 75790ec6d5 | |||
| a1941b7229 | |||
| 37bae82700 | |||
| 77cd328a71 |
@@ -0,0 +1,31 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
title: "[BUG] - Problem with app"
|
||||
labels: bug
|
||||
assignees: zaneschepke
|
||||
|
||||
---
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
**Smartphone (please complete the following information):**
|
||||
- Device: [e.g. iPhone6]
|
||||
- Android Version: [e.g. iOS8.1]
|
||||
- App Version [e.g. 22]
|
||||
|
||||
**To Reproduce**
|
||||
Steps to reproduce the behavior:
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
|
||||
**Expected behavior**
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
**Screenshots (Only if necessary)**
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the problem here.
|
||||
@@ -0,0 +1,20 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
title: "[FEATURE] - New feature request"
|
||||
labels: enhancement
|
||||
assignees: zaneschepke
|
||||
|
||||
---
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
|
||||
**Describe the solution you'd like**
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
|
||||
**Additional context**
|
||||
Add any other context or screenshots about the feature request here.
|
||||
@@ -0,0 +1,17 @@
|
||||
# Support
|
||||
|
||||
If you are experiencing issues with the app, the following resources are available to help you.
|
||||
|
||||
<ol>
|
||||
<li>
|
||||
See the app docs site <a href="https://zaneschepke.com/wgtunnel-docs/overview.html">here</a> (work in progress).
|
||||
</li>
|
||||
<li>
|
||||
Chat with me and our community on Discord <a href="https://discord.gg/rbRRNh6H7V">here</a>, or open an issue on GitHub <a href="https://github.com/zaneschepke/wgtunnel/issues/new/choose">here</a>.
|
||||
</li>
|
||||
<li>
|
||||
Send me an email <a href="mailto:zanecschepke@gmail.com">here</a>.
|
||||
</li>
|
||||
</ol>
|
||||
|
||||
Thank you for using WG Tunnel.
|
||||
@@ -0,0 +1,87 @@
|
||||
# name of the workflow
|
||||
name: Android CI Tag Deployment
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- '*.*.*'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build Signed APK
|
||||
# change to macos because of hilt issues on ubuntu in gradle 8.3
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
env:
|
||||
KEY_STORE_PATH: ${{ secrets.KEY_STORE_PATH }}
|
||||
SIGNING_KEY_ALIAS: ${{ secrets.SIGNING_KEY_ALIAS }}
|
||||
SIGNING_KEY_PASSWORD: ${{ secrets.SIGNING_KEY_PASSWORD }}
|
||||
SIGNING_STORE_PASSWORD: ${{ secrets.SIGNING_STORE_PASSWORD }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Set up JDK 17
|
||||
uses: actions/setup-java@v3
|
||||
with:
|
||||
distribution: 'temurin'
|
||||
java-version: '17'
|
||||
cache: gradle
|
||||
- name: Grant execute permission for gradlew
|
||||
run: chmod +x gradlew
|
||||
|
||||
# Here we need to decode keystore.jks from base64 string and place it
|
||||
# in the folder specified in the release signing configuration
|
||||
- name: Decode Keystore
|
||||
id: decode_keystore
|
||||
uses: timheuer/base64-to-file@v1.2
|
||||
with:
|
||||
fileName: 'android_keystore.jks'
|
||||
fileDir: ${{ github.workspace }}/app/keystore/
|
||||
encodedString: ${{ secrets.KEYSTORE }}
|
||||
|
||||
- name: Create service_account.json
|
||||
id: createServiceAccount
|
||||
run: echo '${{ secrets.SERVICE_ACCOUNT_JSON }}' > service_account.json
|
||||
|
||||
# Build and sign APK ("-x test" argument is used to skip tests)
|
||||
# add fdroid flavor for apk upload
|
||||
- name: Build Fdroid Release APK
|
||||
run: ./gradlew :app:assembleFdroidRelease -x test
|
||||
|
||||
# get fdroid flavor release apk path
|
||||
- name: Get apk path
|
||||
id: apk-path
|
||||
run: echo "path=$(find . -regex '^.*/build/outputs/apk/fdroid/release/.*\.apk$' -type f | head -1)" >> $GITHUB_OUTPUT
|
||||
|
||||
# Save the APK after the Build job is complete to publish it as a Github release in the next job
|
||||
- name: Upload APK
|
||||
uses: actions/upload-artifact@v3.1.2
|
||||
with:
|
||||
name: wgtunnel
|
||||
path: ${{ steps.apk-path.outputs.path }}
|
||||
- name: Download APK from build
|
||||
uses: actions/download-artifact@v1
|
||||
with:
|
||||
name: wgtunnel
|
||||
- name: Create Release with Fastlane changelog notes
|
||||
id: create_release
|
||||
uses: softprops/action-gh-release@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
# fix hardcode changelog file name
|
||||
body_path: ${{ github.workspace }}/fastlane/metadata/android/en-US/changelogs/32300.txt
|
||||
tag_name: ${{ github.ref_name }}
|
||||
name: Release ${{ github.ref_name }}
|
||||
draft: false
|
||||
prerelease: false
|
||||
files: ${{ github.workspace }}/${{ steps.apk-path.outputs.path }}
|
||||
- name: Deploy with fastlane
|
||||
uses: ruby/setup-ruby@v1
|
||||
with:
|
||||
ruby-version: '3.2' # Not needed with a .ruby-version file
|
||||
bundler-cache: true
|
||||
|
||||
- name: Distribute app to Beta track 🚀
|
||||
run: (cd ${{ github.workspace }} && bundle install && bundle exec fastlane beta)
|
||||
|
||||
@@ -69,3 +69,5 @@ lint/tmp/
|
||||
# App Specific cases
|
||||
app/release/output.json
|
||||
.idea/codeStyles/
|
||||
# where we keep our signing secrets locally
|
||||
app/signing.properties
|
||||
|
||||
@@ -47,18 +47,19 @@ This is an alternative Android Application for [WireGuard](https://www.wireguard
|
||||
|
||||
## Inspiration
|
||||
|
||||
The inspiration for this app came from the inconvenience of constantly having to turn VPN off and on while on different networks. With there being no free solution to this problem, this app was created to meet that need.
|
||||
The original inspiration for this app came from the inconvenience of having to manually turn VPN off and on while on different networks. This app was created to offer a free solution to this problem.
|
||||
|
||||
## Features
|
||||
|
||||
* Add tunnels via .conf file
|
||||
* Add tunnels via .conf file, zip, manual entry, or QR code
|
||||
* Auto connect to VPN based on Wi-Fi SSID
|
||||
* Split tunneling by application with search
|
||||
* Always-on VPN for Android support
|
||||
* Quick tile support for vpn toggling
|
||||
* Dynamic shortcuts support for automation integration
|
||||
* Configurable Trusted Network list
|
||||
* Optional auto connect on mobile data
|
||||
* Export tunnels to zip
|
||||
* Quick tile support for VPN toggling
|
||||
* Static shortcuts support for primary tunnel for automation integration
|
||||
* Intent automation support for all tunnels
|
||||
* Optional auto connect on mobile data, ethernet
|
||||
* Automatic service restart after reboot
|
||||
* Service will stay running in background after app has been closed
|
||||
|
||||
@@ -68,7 +69,7 @@ The inspiration for this app came from the inconvenience of constantly having to
|
||||
```
|
||||
$ git clone https://github.com/zaneschepke/wgtunnel
|
||||
$ cd wgtunnel
|
||||
$ ./gradlew assembleRelease
|
||||
$ ./gradlew assembleDebug
|
||||
```
|
||||
|
||||
</span>
|
||||
|
||||
+61
-13
@@ -1,3 +1,5 @@
|
||||
import java.util.Properties
|
||||
|
||||
plugins {
|
||||
alias(libs.plugins.android.application)
|
||||
alias(libs.plugins.kotlin.android)
|
||||
@@ -7,17 +9,19 @@ plugins {
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "com.zaneschepke.wireguardautotunnel"
|
||||
compileSdk = 34
|
||||
namespace = Constants.APP_ID
|
||||
compileSdk = Constants.TARGET_SDK
|
||||
|
||||
defaultConfig {
|
||||
applicationId = "com.zaneschepke.wireguardautotunnel"
|
||||
minSdk = 26
|
||||
targetSdk = 34
|
||||
versionCode = 31200
|
||||
versionName = "3.1.2"
|
||||
applicationId = Constants.APP_ID
|
||||
minSdk = Constants.MIN_SDK
|
||||
targetSdk = Constants.TARGET_SDK
|
||||
versionCode = Constants.VERSION_CODE
|
||||
versionName = Constants.VERSION_NAME
|
||||
|
||||
multiDexEnabled = true
|
||||
ksp {
|
||||
arg("room.schemaLocation", "$projectDir/schemas")
|
||||
}
|
||||
|
||||
resourceConfigurations.addAll(listOf("en"))
|
||||
|
||||
@@ -27,7 +31,38 @@ android {
|
||||
}
|
||||
}
|
||||
|
||||
signingConfigs {
|
||||
create(Constants.RELEASE) {
|
||||
val properties = Properties().apply {
|
||||
//created local file for signing details
|
||||
try {
|
||||
load(file("signing.properties").reader())
|
||||
} catch (_ : Exception) {
|
||||
load(file("signing_template.properties").reader())
|
||||
}
|
||||
}
|
||||
|
||||
//try to get secrets from env first for pipeline build, then properties file for local build
|
||||
storeFile = file(System.getenv().getOrDefault(Constants.KEY_STORE_PATH_VAR, properties.getProperty(Constants.KEY_STORE_PATH_VAR)))
|
||||
storePassword = System.getenv().getOrDefault(Constants.STORE_PASS_VAR, properties.getProperty(Constants.STORE_PASS_VAR))
|
||||
keyAlias = System.getenv().getOrDefault(Constants.KEY_ALIAS_VAR, properties.getProperty(Constants.KEY_ALIAS_VAR))
|
||||
keyPassword = System.getenv().getOrDefault(Constants.KEY_PASS_VAR, properties.getProperty(Constants.KEY_PASS_VAR))
|
||||
}
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
//don't strip
|
||||
packaging.jniLibs.keepDebugSymbols.addAll(listOf("libwg-go.so", "libwg-quick.so", "libwg.so"))
|
||||
|
||||
applicationVariants.all {
|
||||
val variant = this
|
||||
variant.outputs
|
||||
.map { it as com.android.build.gradle.internal.api.BaseVariantOutputImpl }
|
||||
.forEach { output ->
|
||||
val outputFileName = "${Constants.APP_NAME}-${variant.flavorName}-${variant.buildType.name}-${variant.versionName}.apk"
|
||||
output.outputFileName = outputFileName
|
||||
}
|
||||
}
|
||||
release {
|
||||
isDebuggable = false
|
||||
isMinifyEnabled = true
|
||||
@@ -36,18 +71,20 @@ android {
|
||||
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||
"proguard-rules.pro"
|
||||
)
|
||||
signingConfig = signingConfigs.getByName(Constants.RELEASE)
|
||||
}
|
||||
debug {
|
||||
isDebuggable = true
|
||||
}
|
||||
}
|
||||
flavorDimensions.add("type")
|
||||
flavorDimensions.add(Constants.TYPE)
|
||||
productFlavors {
|
||||
create("fdroid") {
|
||||
dimension = "type"
|
||||
dimension = Constants.TYPE
|
||||
proguardFile("fdroid-rules.pro")
|
||||
}
|
||||
create("general") {
|
||||
dimension = "type"
|
||||
dimension = Constants.TYPE
|
||||
if (BuildHelper.isReleaseBuild(gradle) && BuildHelper.isGeneralFlavor(gradle))
|
||||
{
|
||||
apply(plugin = "com.google.gms.google-services")
|
||||
@@ -58,9 +95,10 @@ android {
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_17
|
||||
targetCompatibility = JavaVersion.VERSION_17
|
||||
isCoreLibraryDesugaringEnabled = true
|
||||
}
|
||||
kotlinOptions {
|
||||
jvmTarget = "17"
|
||||
jvmTarget = Constants.JVM_TARGET
|
||||
}
|
||||
buildFeatures {
|
||||
compose = true
|
||||
@@ -102,6 +140,7 @@ dependencies {
|
||||
|
||||
//wg
|
||||
implementation(libs.tunnel)
|
||||
coreLibraryDesugaring(libs.desugar.jdk.libs)
|
||||
|
||||
//logging
|
||||
implementation(libs.timber)
|
||||
@@ -118,7 +157,6 @@ dependencies {
|
||||
implementation(libs.accompanist.systemuicontroller)
|
||||
implementation(libs.accompanist.permissions)
|
||||
implementation(libs.accompanist.flowlayout)
|
||||
implementation(libs.accompanist.navigation.animation)
|
||||
implementation(libs.accompanist.drawablepainter)
|
||||
|
||||
//room
|
||||
@@ -128,6 +166,9 @@ dependencies {
|
||||
|
||||
//lifecycle
|
||||
implementation(libs.lifecycle.runtime.compose)
|
||||
implementation(libs.androidx.lifecycle.runtime.ktx)
|
||||
implementation(libs.androidx.lifecycle.process)
|
||||
|
||||
|
||||
//icons
|
||||
implementation(libs.material.icons.extended)
|
||||
@@ -142,4 +183,11 @@ dependencies {
|
||||
//barcode scanning
|
||||
implementation(libs.zxing.android.embedded)
|
||||
implementation(libs.zxing.core)
|
||||
|
||||
//bio
|
||||
implementation(libs.androidx.biometric.ktx)
|
||||
|
||||
//shortcuts
|
||||
implementation(libs.androidx.core)
|
||||
implementation(libs.androidx.core.google.shortcuts)
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
-dontwarn com.google.errorprone.annotations.**
|
||||
Vendored
+1
-1
@@ -18,4 +18,4 @@
|
||||
|
||||
# If you keep the line number information, uncomment this to
|
||||
# hide the original source file name.
|
||||
#-renamesourcefileattribute SourceFile
|
||||
#-renamesourcefileattribute SourceFile
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
{
|
||||
"formatVersion": 1,
|
||||
"database": {
|
||||
"version": 1,
|
||||
"identityHash": "ba86153e6fb0b823197b987239b03e64",
|
||||
"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, `default_tunnel` TEXT, `is_always_on_vpn_enabled` INTEGER NOT NULL, `is_tunnel_on_ethernet_enabled` INTEGER NOT NULL)",
|
||||
"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": "defaultTunnel",
|
||||
"columnName": "default_tunnel",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "isAlwaysOnVpnEnabled",
|
||||
"columnName": "is_always_on_vpn_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "isTunnelOnEthernetEnabled",
|
||||
"columnName": "is_tunnel_on_ethernet_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"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)",
|
||||
"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
|
||||
}
|
||||
],
|
||||
"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, 'ba86153e6fb0b823197b987239b03e64')"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
{
|
||||
"formatVersion": 1,
|
||||
"database": {
|
||||
"version": 2,
|
||||
"identityHash": "65b1c9efff61712231fa64d1f19f3915",
|
||||
"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, `default_tunnel` TEXT, `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_battery_saver_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": "defaultTunnel",
|
||||
"columnName": "default_tunnel",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"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": "isBatterySaverEnabled",
|
||||
"columnName": "is_battery_saver_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)",
|
||||
"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
|
||||
}
|
||||
],
|
||||
"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, '65b1c9efff61712231fa64d1f19f3915')"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
{
|
||||
"formatVersion": 1,
|
||||
"database": {
|
||||
"version": 3,
|
||||
"identityHash": "6b30daba29bb95f8ddc4d26206329d4f",
|
||||
"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, `default_tunnel` TEXT, `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_battery_saver_enabled` INTEGER NOT NULL DEFAULT false, `is_tunnel_on_wifi_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": "defaultTunnel",
|
||||
"columnName": "default_tunnel",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"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": "isBatterySaverEnabled",
|
||||
"columnName": "is_battery_saver_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isTunnelOnWifiEnabled",
|
||||
"columnName": "is_tunnel_on_wifi_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)",
|
||||
"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
|
||||
}
|
||||
],
|
||||
"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, '6b30daba29bb95f8ddc4d26206329d4f')"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
SIGNING_STORE_PASSWORD=
|
||||
SIGNING_KEY_ALIAS=
|
||||
SIGNING_KEY_PASSWORD=
|
||||
KEY_STORE_PATH=/
|
||||
+2
-4
@@ -1,13 +1,11 @@
|
||||
package com.zaneschepke.wireguardautotunnel
|
||||
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
import org.junit.Assert.*
|
||||
|
||||
/**
|
||||
* Instrumented test, which will execute on an Android device.
|
||||
*
|
||||
|
||||
@@ -4,6 +4,10 @@
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"
|
||||
android:maxSdkVersion="32" />
|
||||
<uses-permission
|
||||
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
|
||||
android:maxSdkVersion="32"
|
||||
tools:ignore="ScopedStorage" />
|
||||
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE"
|
||||
android:maxSdkVersion="30"
|
||||
tools:ignore="LeanbackUsesWifi" />
|
||||
|
||||
@@ -2,16 +2,19 @@ package com.zaneschepke.wireguardautotunnel
|
||||
|
||||
object Constants {
|
||||
const val MANUAL_TUNNEL_CONFIG_ID = "0"
|
||||
const val WATCHER_SERVICE_WAKE_LOCK_TIMEOUT = 10*60*1000L /*10 minute*/
|
||||
const val VPN_CONNECTIVITY_CHECK_INTERVAL = 3000L
|
||||
const val VPN_STATISTIC_CHECK_INTERVAL = 10000L
|
||||
const val VPN_STATISTIC_CHECK_INTERVAL = 1000L
|
||||
const val TOGGLE_TUNNEL_DELAY = 500L
|
||||
const val FADE_IN_ANIMATION_DURATION = 1000
|
||||
const val SLIDE_IN_ANIMATION_DURATION = 500
|
||||
const val SLIDE_IN_TRANSITION_OFFSET = 1000
|
||||
const val VALID_FILE_EXTENSION = ".conf"
|
||||
const val CONF_FILE_EXTENSION = ".conf"
|
||||
const val ZIP_FILE_EXTENSION = ".zip"
|
||||
const val URI_CONTENT_SCHEME = "content"
|
||||
const val URI_PACKAGE_SCHEME = "package"
|
||||
const val ALLOWED_FILE_TYPES = "*/*"
|
||||
const val GOOGLE_TV_EXPLORER_STUB = "com.google.android.tv.frameworkpackagestubs"
|
||||
const val ANDROID_TV_EXPLORER_STUB = "com.android.tv.frameworkpackagestubs"
|
||||
const val EMAIL_MIME_TYPE = "message/rfc822"
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package com.zaneschepke.wireguardautotunnel
|
||||
|
||||
import android.content.BroadcastReceiver
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import java.math.BigDecimal
|
||||
import java.text.DecimalFormat
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
import kotlin.coroutines.EmptyCoroutineContext
|
||||
|
||||
fun BroadcastReceiver.goAsync(
|
||||
context: CoroutineContext = EmptyCoroutineContext,
|
||||
block: suspend CoroutineScope.() -> Unit
|
||||
) {
|
||||
val pendingResult = goAsync()
|
||||
@OptIn(DelicateCoroutinesApi::class) // Must run globally; there's no teardown callback.
|
||||
GlobalScope.launch(context) {
|
||||
try {
|
||||
block()
|
||||
} finally {
|
||||
pendingResult.finish()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun BigDecimal.toThreeDecimalPlaceString() : String {
|
||||
val df = DecimalFormat("#.###")
|
||||
return df.format(this)
|
||||
}
|
||||
@@ -3,11 +3,11 @@ package com.zaneschepke.wireguardautotunnel
|
||||
import android.app.Application
|
||||
import android.content.Context
|
||||
import android.content.pm.PackageManager
|
||||
import androidx.lifecycle.ProcessLifecycleOwner
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.zaneschepke.wireguardautotunnel.repository.SettingsDoa
|
||||
import com.zaneschepke.wireguardautotunnel.repository.model.Settings
|
||||
import dagger.hilt.android.HiltAndroidApp
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
@@ -27,13 +27,17 @@ class WireGuardAutoTunnel : Application() {
|
||||
}
|
||||
|
||||
private fun initSettings() {
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
if(settingsRepo.getAll().isEmpty()) {
|
||||
settingsRepo.save(Settings())
|
||||
with(ProcessLifecycleOwner.get()) {
|
||||
lifecycleScope.launch {
|
||||
if(settingsRepo.getAll().isEmpty()) {
|
||||
settingsRepo.save(Settings())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
companion object {
|
||||
fun isRunningOnAndroidTv(context : Context) : Boolean {
|
||||
return context.packageManager.hasSystemFeature(PackageManager.FEATURE_LEANBACK)
|
||||
|
||||
@@ -3,13 +3,11 @@ package com.zaneschepke.wireguardautotunnel.receiver
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import com.zaneschepke.wireguardautotunnel.goAsync
|
||||
import com.zaneschepke.wireguardautotunnel.repository.SettingsDoa
|
||||
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.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
@AndroidEntryPoint
|
||||
@@ -18,20 +16,18 @@ class BootReceiver : BroadcastReceiver() {
|
||||
@Inject
|
||||
lateinit var settingsRepo : SettingsDoa
|
||||
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
override fun onReceive(context: Context, intent: Intent) = goAsync {
|
||||
if (intent.action == Intent.ACTION_BOOT_COMPLETED) {
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
try {
|
||||
val settings = settingsRepo.getAll()
|
||||
if (settings.isNotEmpty()) {
|
||||
val setting = settings.first()
|
||||
if (setting.isAutoTunnelEnabled && setting.defaultTunnel != null) {
|
||||
ServiceManager.startWatcherService(context, setting.defaultTunnel!!)
|
||||
}
|
||||
try {
|
||||
val settings = settingsRepo.getAll()
|
||||
if (settings.isNotEmpty()) {
|
||||
val setting = settings.first()
|
||||
if (setting.isAutoTunnelEnabled && setting.defaultTunnel != null) {
|
||||
ServiceManager.startWatcherService(context, setting.defaultTunnel!!)
|
||||
}
|
||||
} finally {
|
||||
cancel()
|
||||
}
|
||||
} finally {
|
||||
cancel()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+12
-16
@@ -4,14 +4,12 @@ import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import com.zaneschepke.wireguardautotunnel.Constants
|
||||
import com.zaneschepke.wireguardautotunnel.goAsync
|
||||
import com.zaneschepke.wireguardautotunnel.repository.SettingsDoa
|
||||
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.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
@AndroidEntryPoint
|
||||
@@ -19,21 +17,19 @@ class NotificationActionReceiver : BroadcastReceiver() {
|
||||
|
||||
@Inject
|
||||
lateinit var settingsRepo : SettingsDoa
|
||||
override fun onReceive(context: Context, intent: Intent?) {
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
try {
|
||||
val settings = settingsRepo.getAll()
|
||||
if (settings.isNotEmpty()) {
|
||||
val setting = settings.first()
|
||||
if (setting.defaultTunnel != null) {
|
||||
ServiceManager.stopVpnService(context)
|
||||
delay(Constants.TOGGLE_TUNNEL_DELAY)
|
||||
ServiceManager.startVpnService(context, setting.defaultTunnel.toString())
|
||||
}
|
||||
override fun onReceive(context: Context, intent: Intent?) = goAsync {
|
||||
try {
|
||||
val settings = settingsRepo.getAll()
|
||||
if (settings.isNotEmpty()) {
|
||||
val setting = settings.first()
|
||||
if (setting.defaultTunnel != null) {
|
||||
ServiceManager.stopVpnService(context)
|
||||
delay(Constants.TOGGLE_TUNNEL_DELAY)
|
||||
ServiceManager.startVpnService(context, setting.defaultTunnel.toString())
|
||||
}
|
||||
} finally {
|
||||
cancel()
|
||||
}
|
||||
} finally {
|
||||
cancel()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,15 @@
|
||||
package com.zaneschepke.wireguardautotunnel.repository
|
||||
|
||||
import androidx.room.AutoMigration
|
||||
import androidx.room.Database
|
||||
import androidx.room.RoomDatabase
|
||||
import androidx.room.TypeConverters
|
||||
import com.zaneschepke.wireguardautotunnel.repository.model.Settings
|
||||
import com.zaneschepke.wireguardautotunnel.repository.model.TunnelConfig
|
||||
|
||||
@Database(entities = [Settings::class, TunnelConfig::class], version = 1, exportSchema = false)
|
||||
@Database(entities = [Settings::class, TunnelConfig::class], version = 3, autoMigrations = [
|
||||
AutoMigration(from = 1, to = 2), AutoMigration(from = 2, to = 3)
|
||||
], exportSchema = true)
|
||||
@TypeConverters(DatabaseListConverters::class)
|
||||
abstract class AppDatabase : RoomDatabase() {
|
||||
abstract fun settingDao(): SettingsDoa
|
||||
|
||||
+11
-3
@@ -1,15 +1,23 @@
|
||||
package com.zaneschepke.wireguardautotunnel.repository
|
||||
|
||||
import androidx.room.TypeConverter
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
|
||||
class DatabaseListConverters {
|
||||
@TypeConverter
|
||||
fun listToString(value: MutableList<String>): String {
|
||||
return value.joinToString(",")
|
||||
return Json.encodeToString(value)
|
||||
}
|
||||
@TypeConverter
|
||||
fun <T> stringToList(value: String): MutableList<String> {
|
||||
fun stringToList(value: String): MutableList<String> {
|
||||
if(value.isEmpty()) return mutableListOf()
|
||||
return value.split(",").toMutableList()
|
||||
return try {
|
||||
Json.decodeFromString<MutableList<String>>(value)
|
||||
} catch (e : Exception) {
|
||||
val list = value.split(",").toMutableList()
|
||||
val json = listToString(list)
|
||||
Json.decodeFromString<MutableList<String>>(json)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -13,6 +13,9 @@ data class Settings(
|
||||
@ColumnInfo(name = "default_tunnel") var defaultTunnel : String? = null,
|
||||
@ColumnInfo(name = "is_always_on_vpn_enabled") var isAlwaysOnVpnEnabled : Boolean = false,
|
||||
@ColumnInfo(name = "is_tunnel_on_ethernet_enabled") var isTunnelOnEthernetEnabled : Boolean = false,
|
||||
@ColumnInfo(name = "is_shortcuts_enabled", defaultValue = "false") var isShortcutsEnabled : Boolean = false,
|
||||
@ColumnInfo(name = "is_battery_saver_enabled", defaultValue = "false") var isBatterySaverEnabled : Boolean = false,
|
||||
@ColumnInfo(name = "is_tunnel_on_wifi_enabled", defaultValue = "false") var isTunnelOnWifiEnabled : Boolean = false,
|
||||
) {
|
||||
fun isTunnelConfigDefault(tunnelConfig: TunnelConfig) : Boolean {
|
||||
return if (defaultTunnel != null) {
|
||||
|
||||
-8
@@ -91,14 +91,6 @@ object ServiceManager {
|
||||
WireGuardConnectivityWatcherService::class.java)
|
||||
}
|
||||
|
||||
fun toggleWatcherService(context: Context, tunnelConfig : String) {
|
||||
when(getServiceState( context,
|
||||
WireGuardConnectivityWatcherService::class.java,)) {
|
||||
ServiceState.STARTED -> stopWatcherService(context)
|
||||
ServiceState.STOPPED -> startWatcherService(context, tunnelConfig)
|
||||
}
|
||||
}
|
||||
|
||||
fun toggleWatcherServiceForeground(context: Context, tunnelConfig : String) {
|
||||
when(getServiceState( context,
|
||||
WireGuardConnectivityWatcherService::class.java,)) {
|
||||
|
||||
+89
-56
@@ -21,24 +21,24 @@ import com.zaneschepke.wireguardautotunnel.service.network.WifiService
|
||||
import com.zaneschepke.wireguardautotunnel.service.notification.NotificationService
|
||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnService
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
@AndroidEntryPoint
|
||||
class WireGuardConnectivityWatcherService : ForegroundService() {
|
||||
|
||||
private val foregroundId = 122;
|
||||
private val foregroundId = 122
|
||||
|
||||
@Inject
|
||||
lateinit var wifiService : NetworkService<WifiService>
|
||||
lateinit var wifiService: NetworkService<WifiService>
|
||||
|
||||
@Inject
|
||||
lateinit var mobileDataService : NetworkService<MobileDataService>
|
||||
lateinit var mobileDataService: NetworkService<MobileDataService>
|
||||
|
||||
@Inject
|
||||
lateinit var ethernetService: NetworkService<EthernetService>
|
||||
@@ -47,22 +47,22 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
|
||||
lateinit var settingsRepo: SettingsDoa
|
||||
|
||||
@Inject
|
||||
lateinit var notificationService : NotificationService
|
||||
lateinit var notificationService: NotificationService
|
||||
|
||||
@Inject
|
||||
lateinit var vpnService : VpnService
|
||||
lateinit var vpnService: VpnService
|
||||
|
||||
private var isWifiConnected = false;
|
||||
private var isEthernetConnected = false;
|
||||
private var isMobileDataConnected = false;
|
||||
private var currentNetworkSSID = "";
|
||||
private var isWifiConnected = false
|
||||
private var isEthernetConnected = false
|
||||
private var isMobileDataConnected = false
|
||||
private var currentNetworkSSID = ""
|
||||
|
||||
private lateinit var watcherJob : Job;
|
||||
private lateinit var setting : Settings
|
||||
private lateinit var watcherJob: Job
|
||||
private lateinit var setting: Settings
|
||||
private lateinit var tunnelConfig: String
|
||||
|
||||
private var wakeLock: PowerManager.WakeLock? = null
|
||||
private val tag = this.javaClass.name;
|
||||
private val tag = this.javaClass.name
|
||||
|
||||
|
||||
override fun onCreate() {
|
||||
@@ -80,9 +80,11 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
|
||||
this.tunnelConfig = tunnelId
|
||||
}
|
||||
// we need this lock so our service gets not affected by Doze Mode
|
||||
initWakeLock()
|
||||
lifecycleScope.launch {
|
||||
initWakeLock()
|
||||
}
|
||||
cancelWatcherJob()
|
||||
if(this::tunnelConfig.isInitialized) {
|
||||
if (this::tunnelConfig.isInitialized) {
|
||||
startWatcherJob()
|
||||
} else {
|
||||
stopService(extras)
|
||||
@@ -104,7 +106,9 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
|
||||
val notification = notificationService.createNotification(
|
||||
channelId = getString(R.string.watcher_channel_id),
|
||||
channelName = getString(R.string.watcher_channel_name),
|
||||
description = getString(R.string.watcher_notification_text))
|
||||
description = getString(R.string.watcher_notification_text),
|
||||
vibration = false
|
||||
)
|
||||
super.startForeground(foregroundId, notification)
|
||||
}
|
||||
|
||||
@@ -112,46 +116,59 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
|
||||
override fun onTaskRemoved(rootIntent: Intent) {
|
||||
Timber.d("Task Removed called")
|
||||
val restartServiceIntent = Intent(rootIntent)
|
||||
val restartServicePendingIntent: PendingIntent = PendingIntent.getService(this, 1, restartServiceIntent,
|
||||
PendingIntent.FLAG_ONE_SHOT or PendingIntent.FLAG_IMMUTABLE);
|
||||
applicationContext.getSystemService(Context.ALARM_SERVICE);
|
||||
val alarmService: AlarmManager = applicationContext.getSystemService(Context.ALARM_SERVICE) as AlarmManager;
|
||||
alarmService.set(AlarmManager.ELAPSED_REALTIME, SystemClock.elapsedRealtime() + 1000, restartServicePendingIntent);
|
||||
val restartServicePendingIntent: PendingIntent = PendingIntent.getService(
|
||||
this, 1, restartServiceIntent,
|
||||
PendingIntent.FLAG_ONE_SHOT or PendingIntent.FLAG_IMMUTABLE
|
||||
)
|
||||
applicationContext.getSystemService(Context.ALARM_SERVICE)
|
||||
val alarmService: AlarmManager =
|
||||
applicationContext.getSystemService(Context.ALARM_SERVICE) as AlarmManager
|
||||
alarmService.set(
|
||||
AlarmManager.ELAPSED_REALTIME,
|
||||
SystemClock.elapsedRealtime() + 1000,
|
||||
restartServicePendingIntent
|
||||
)
|
||||
}
|
||||
|
||||
private fun initWakeLock() {
|
||||
private suspend fun initWakeLock() {
|
||||
val isBatterySaverOn = withContext(lifecycleScope.coroutineContext) {
|
||||
settingsRepo.getAll().firstOrNull()?.isBatterySaverEnabled ?: false
|
||||
}
|
||||
wakeLock =
|
||||
(getSystemService(Context.POWER_SERVICE) as PowerManager).run {
|
||||
newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "$tag::lock").apply {
|
||||
//TODO decide what to do here with the wakelock
|
||||
//this is draining battery. Perhaps users only care for VPN to connect when their screen is on
|
||||
//and they are actively using apps
|
||||
acquire()
|
||||
if (isBatterySaverOn) {
|
||||
Timber.d("Initiating wakelock with timeout")
|
||||
acquire(Constants.WATCHER_SERVICE_WAKE_LOCK_TIMEOUT)
|
||||
} else {
|
||||
Timber.d("Initiating wakelock with zero timeout")
|
||||
acquire()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun cancelWatcherJob() {
|
||||
if(this::watcherJob.isInitialized) {
|
||||
if (this::watcherJob.isInitialized) {
|
||||
watcherJob.cancel()
|
||||
}
|
||||
}
|
||||
|
||||
private fun startWatcherJob() {
|
||||
watcherJob = lifecycleScope.launch(Dispatchers.IO) {
|
||||
val settings = settingsRepo.getAll();
|
||||
if(settings.isNotEmpty()) {
|
||||
val settings = settingsRepo.getAll()
|
||||
if (settings.isNotEmpty()) {
|
||||
setting = settings[0]
|
||||
}
|
||||
launch {
|
||||
watchForWifiConnectivityChanges()
|
||||
}
|
||||
if(setting.isTunnelOnMobileDataEnabled) {
|
||||
if (setting.isTunnelOnMobileDataEnabled) {
|
||||
launch {
|
||||
watchForMobileDataConnectivityChanges()
|
||||
}
|
||||
}
|
||||
if(setting.isTunnelOnEthernetEnabled) {
|
||||
if (setting.isTunnelOnEthernetEnabled) {
|
||||
launch {
|
||||
watchForEthernetConnectivityChanges()
|
||||
}
|
||||
@@ -164,15 +181,17 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
|
||||
|
||||
private suspend fun watchForMobileDataConnectivityChanges() {
|
||||
mobileDataService.networkStatus.collect {
|
||||
when(it) {
|
||||
when (it) {
|
||||
is NetworkStatus.Available -> {
|
||||
Timber.d("Gained Mobile data connection")
|
||||
isMobileDataConnected = true
|
||||
}
|
||||
|
||||
is NetworkStatus.CapabilitiesChanged -> {
|
||||
isMobileDataConnected = true
|
||||
Timber.d("Mobile data capabilities changed")
|
||||
}
|
||||
|
||||
is NetworkStatus.Unavailable -> {
|
||||
isMobileDataConnected = false
|
||||
Timber.d("Lost mobile data connection")
|
||||
@@ -188,10 +207,12 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
|
||||
Timber.d("Gained Ethernet connection")
|
||||
isEthernetConnected = true
|
||||
}
|
||||
|
||||
is NetworkStatus.CapabilitiesChanged -> {
|
||||
Timber.d("Ethernet capabilities changed")
|
||||
isEthernetConnected = true
|
||||
}
|
||||
|
||||
is NetworkStatus.Unavailable -> {
|
||||
isEthernetConnected = false
|
||||
Timber.d("Lost Ethernet connection")
|
||||
@@ -202,45 +223,57 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
|
||||
|
||||
private suspend fun watchForWifiConnectivityChanges() {
|
||||
wifiService.networkStatus.collect {
|
||||
when (it) {
|
||||
is NetworkStatus.Available -> {
|
||||
Timber.d("Gained Wi-Fi connection")
|
||||
isWifiConnected = true
|
||||
}
|
||||
is NetworkStatus.CapabilitiesChanged -> {
|
||||
Timber.d("Wifi capabilities changed")
|
||||
isWifiConnected = true
|
||||
currentNetworkSSID = wifiService.getNetworkName(it.networkCapabilities) ?: "";
|
||||
}
|
||||
is NetworkStatus.Unavailable -> {
|
||||
isWifiConnected = false
|
||||
Timber.d("Lost Wi-Fi connection")
|
||||
}
|
||||
when (it) {
|
||||
is NetworkStatus.Available -> {
|
||||
Timber.d("Gained Wi-Fi connection")
|
||||
isWifiConnected = true
|
||||
}
|
||||
|
||||
is NetworkStatus.CapabilitiesChanged -> {
|
||||
Timber.d("Wifi capabilities changed")
|
||||
isWifiConnected = true
|
||||
currentNetworkSSID = wifiService.getNetworkName(it.networkCapabilities) ?: ""
|
||||
}
|
||||
|
||||
is NetworkStatus.Unavailable -> {
|
||||
isWifiConnected = false
|
||||
Timber.d("Lost Wi-Fi connection")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun manageVpn() {
|
||||
while(true) {
|
||||
if(isEthernetConnected && setting.isTunnelOnEthernetEnabled && vpnService.getState() == Tunnel.State.DOWN) {
|
||||
while (true) {
|
||||
if (isEthernetConnected && setting.isTunnelOnEthernetEnabled && vpnService.getState() == Tunnel.State.DOWN) {
|
||||
ServiceManager.startVpnService(this, tunnelConfig)
|
||||
}
|
||||
if(!isEthernetConnected && setting.isTunnelOnMobileDataEnabled &&
|
||||
if (!isEthernetConnected && setting.isTunnelOnMobileDataEnabled &&
|
||||
!isWifiConnected &&
|
||||
isMobileDataConnected
|
||||
&& vpnService.getState() == Tunnel.State.DOWN) {
|
||||
&& vpnService.getState() == Tunnel.State.DOWN
|
||||
) {
|
||||
ServiceManager.startVpnService(this, tunnelConfig)
|
||||
} else if(!isEthernetConnected && !setting.isTunnelOnMobileDataEnabled &&
|
||||
} else if (!isEthernetConnected && !setting.isTunnelOnMobileDataEnabled &&
|
||||
!isWifiConnected &&
|
||||
vpnService.getState() == Tunnel.State.UP) {
|
||||
vpnService.getState() == Tunnel.State.UP
|
||||
) {
|
||||
ServiceManager.stopVpnService(this)
|
||||
} else if(!isEthernetConnected && isWifiConnected &&
|
||||
} else if (!isEthernetConnected && isWifiConnected &&
|
||||
!setting.trustedNetworkSSIDs.contains(currentNetworkSSID) &&
|
||||
(vpnService.getState() != Tunnel.State.UP)) {
|
||||
setting.isTunnelOnWifiEnabled &&
|
||||
(vpnService.getState() != Tunnel.State.UP)
|
||||
) {
|
||||
ServiceManager.startVpnService(this, tunnelConfig)
|
||||
} else if(!isEthernetConnected && (isWifiConnected &&
|
||||
} else if (!isEthernetConnected && (isWifiConnected &&
|
||||
setting.trustedNetworkSSIDs.contains(currentNetworkSSID)) &&
|
||||
(vpnService.getState() == Tunnel.State.UP)) {
|
||||
(vpnService.getState() == Tunnel.State.UP)
|
||||
) {
|
||||
ServiceManager.stopVpnService(this)
|
||||
} else if (!isEthernetConnected && (isWifiConnected &&
|
||||
!setting.isTunnelOnWifiEnabled &&
|
||||
(vpnService.getState() == Tunnel.State.UP)
|
||||
)) {
|
||||
ServiceManager.stopVpnService(this)
|
||||
}
|
||||
delay(Constants.VPN_CONNECTIVITY_CHECK_INTERVAL)
|
||||
|
||||
+25
-20
@@ -7,12 +7,11 @@ import androidx.lifecycle.lifecycleScope
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.receiver.NotificationActionReceiver
|
||||
import com.zaneschepke.wireguardautotunnel.repository.SettingsDoa
|
||||
import com.zaneschepke.wireguardautotunnel.repository.model.TunnelConfig
|
||||
import com.zaneschepke.wireguardautotunnel.service.notification.NotificationService
|
||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.HandshakeStatus
|
||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnService
|
||||
import com.zaneschepke.wireguardautotunnel.repository.model.TunnelConfig
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.launch
|
||||
@@ -22,7 +21,7 @@ import javax.inject.Inject
|
||||
@AndroidEntryPoint
|
||||
class WireGuardTunnelService : ForegroundService() {
|
||||
|
||||
private val foregroundId = 123;
|
||||
private val foregroundId = 123
|
||||
|
||||
@Inject
|
||||
lateinit var vpnService : VpnService
|
||||
@@ -50,24 +49,26 @@ class WireGuardTunnelService : ForegroundService() {
|
||||
val tunnelConfigString = extras?.getString(getString(R.string.tunnel_extras_key))
|
||||
cancelJob()
|
||||
job = lifecycleScope.launch(Dispatchers.IO) {
|
||||
if(tunnelConfigString != null) {
|
||||
try {
|
||||
val tunnelConfig = TunnelConfig.from(tunnelConfigString)
|
||||
tunnelName = tunnelConfig.name
|
||||
vpnService.startTunnel(tunnelConfig)
|
||||
} catch (e : Exception) {
|
||||
Timber.e("Problem starting tunnel: ${e.message}")
|
||||
stopService(extras)
|
||||
}
|
||||
} else {
|
||||
Timber.d("Tunnel config null, starting default tunnel")
|
||||
val settings = settingsRepo.getAll();
|
||||
if(settings.isNotEmpty()) {
|
||||
val setting = settings[0]
|
||||
if(setting.defaultTunnel != null && setting.isAlwaysOnVpnEnabled) {
|
||||
val tunnelConfig = TunnelConfig.from(setting.defaultTunnel!!)
|
||||
launch {
|
||||
if(tunnelConfigString != null) {
|
||||
try {
|
||||
val tunnelConfig = TunnelConfig.from(tunnelConfigString)
|
||||
tunnelName = tunnelConfig.name
|
||||
vpnService.startTunnel(tunnelConfig)
|
||||
} catch (e : Exception) {
|
||||
Timber.e("Problem starting tunnel: ${e.message}")
|
||||
stopService(extras)
|
||||
}
|
||||
} else {
|
||||
Timber.d("Tunnel config null, starting default tunnel")
|
||||
val settings = settingsRepo.getAll()
|
||||
if(settings.isNotEmpty()) {
|
||||
val setting = settings[0]
|
||||
if(setting.defaultTunnel != null && setting.isAlwaysOnVpnEnabled) {
|
||||
val tunnelConfig = TunnelConfig.from(setting.defaultTunnel!!)
|
||||
tunnelName = tunnelConfig.name
|
||||
vpnService.startTunnel(tunnelConfig)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -119,6 +120,7 @@ class WireGuardTunnelService : ForegroundService() {
|
||||
channelName = getString(R.string.vpn_channel_name),
|
||||
title = getString(R.string.tunnel_start_title),
|
||||
onGoing = false,
|
||||
vibration = false,
|
||||
showTimestamp = true,
|
||||
description = "${getString(R.string.tunnel_start_text)} $tunnelName"
|
||||
)
|
||||
@@ -131,6 +133,7 @@ class WireGuardTunnelService : ForegroundService() {
|
||||
channelName = getString(R.string.vpn_channel_name),
|
||||
title = getString(R.string.vpn_starting),
|
||||
onGoing = false,
|
||||
vibration = false,
|
||||
showTimestamp = true,
|
||||
description = getString(R.string.attempt_connection)
|
||||
)
|
||||
@@ -141,10 +144,12 @@ class WireGuardTunnelService : ForegroundService() {
|
||||
val notification = notificationService.createNotification(
|
||||
channelId = getString(R.string.vpn_channel_id),
|
||||
channelName = getString(R.string.vpn_channel_name),
|
||||
action = PendingIntent.getBroadcast(this,0,Intent(this, NotificationActionReceiver::class.java),PendingIntent.FLAG_IMMUTABLE),
|
||||
action = PendingIntent.getBroadcast(this,0,
|
||||
Intent(this, NotificationActionReceiver::class.java),PendingIntent.FLAG_IMMUTABLE),
|
||||
actionText = getString(R.string.restart),
|
||||
title = getString(R.string.vpn_connection_failed),
|
||||
onGoing = false,
|
||||
vibration = true,
|
||||
showTimestamp = true,
|
||||
description = message
|
||||
)
|
||||
|
||||
+3
-4
@@ -4,7 +4,6 @@ import android.net.NetworkCapabilities
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
interface NetworkService<T> {
|
||||
fun getNetworkName(networkCapabilities: NetworkCapabilities) : String?
|
||||
val networkStatus : Flow<NetworkStatus>
|
||||
|
||||
}
|
||||
fun getNetworkName(networkCapabilities: NetworkCapabilities): String?
|
||||
val networkStatus: Flow<NetworkStatus>
|
||||
}
|
||||
|
||||
+1
-1
@@ -14,7 +14,7 @@ interface NotificationService {
|
||||
description: String,
|
||||
showTimestamp : Boolean = false,
|
||||
importance: Int = NotificationManager.IMPORTANCE_HIGH,
|
||||
vibration: Boolean = true,
|
||||
vibration: Boolean = false,
|
||||
onGoing: Boolean = true,
|
||||
lights: Boolean = true
|
||||
): Notification
|
||||
|
||||
+1
-1
@@ -14,7 +14,7 @@ import javax.inject.Inject
|
||||
|
||||
class WireGuardNotification @Inject constructor(@ApplicationContext private val context: Context) : NotificationService {
|
||||
|
||||
private val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager;
|
||||
private val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||
|
||||
override fun createNotification(
|
||||
channelId: String,
|
||||
|
||||
+25
-24
@@ -2,6 +2,7 @@ package com.zaneschepke.wireguardautotunnel.service.shortcut
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.zaneschepke.wireguardautotunnel.repository.SettingsDoa
|
||||
import com.zaneschepke.wireguardautotunnel.repository.TunnelConfigDao
|
||||
import com.zaneschepke.wireguardautotunnel.repository.model.Settings
|
||||
@@ -11,9 +12,7 @@ import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
|
||||
import com.zaneschepke.wireguardautotunnel.service.foreground.WireGuardTunnelService
|
||||
import com.zaneschepke.wireguardautotunnel.util.WgTunnelException
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
@@ -27,10 +26,8 @@ class ShortcutsActivity : ComponentActivity() {
|
||||
@Inject
|
||||
lateinit var tunnelConfigRepo : TunnelConfigDao
|
||||
|
||||
private val scope = CoroutineScope(Dispatchers.Main);
|
||||
|
||||
private fun attemptWatcherServiceToggle(tunnelConfig : String) {
|
||||
scope.launch {
|
||||
lifecycleScope.launch(Dispatchers.Main) {
|
||||
val settings = getSettings()
|
||||
if(settings.isAutoTunnelEnabled) {
|
||||
ServiceManager.toggleWatcherServiceForeground(this@ShortcutsActivity, tunnelConfig)
|
||||
@@ -42,32 +39,35 @@ class ShortcutsActivity : ComponentActivity() {
|
||||
super.onCreate(savedInstanceState)
|
||||
if(intent.getStringExtra(CLASS_NAME_EXTRA_KEY)
|
||||
.equals(WireGuardTunnelService::class.java.simpleName)) {
|
||||
scope.launch {
|
||||
try {
|
||||
val settings = getSettings()
|
||||
val tunnelConfig = if(settings.defaultTunnel == null) {
|
||||
tunnelConfigRepo.getAll().first()
|
||||
} else {
|
||||
TunnelConfig.from(settings.defaultTunnel!!)
|
||||
lifecycleScope.launch(Dispatchers.Main) {
|
||||
val settings = getSettings()
|
||||
if(settings.isShortcutsEnabled) {
|
||||
try {
|
||||
val tunnelName = intent.getStringExtra(TUNNEL_NAME_EXTRA_KEY)
|
||||
val tunnelConfig = if(tunnelName != null) {
|
||||
tunnelConfigRepo.getAll().firstOrNull { it.name == tunnelName }
|
||||
} else {
|
||||
if(settings.defaultTunnel == null) {
|
||||
tunnelConfigRepo.getAll().first()
|
||||
} else {
|
||||
TunnelConfig.from(settings.defaultTunnel!!)
|
||||
}
|
||||
}
|
||||
tunnelConfig ?: return@launch
|
||||
attemptWatcherServiceToggle(tunnelConfig.toString())
|
||||
when(intent.action){
|
||||
Action.STOP.name -> ServiceManager.stopVpnService(this@ShortcutsActivity)
|
||||
Action.START.name -> ServiceManager.startVpnService(this@ShortcutsActivity, tunnelConfig.toString())
|
||||
}
|
||||
} catch (e : Exception) {
|
||||
Timber.e(e.message)
|
||||
}
|
||||
attemptWatcherServiceToggle(tunnelConfig.toString())
|
||||
when(intent.action){
|
||||
Action.STOP.name -> ServiceManager.stopVpnService(this@ShortcutsActivity)
|
||||
Action.START.name -> ServiceManager.startVpnService(this@ShortcutsActivity, tunnelConfig.toString())
|
||||
}
|
||||
} catch (e : Exception) {
|
||||
Timber.e(e.message)
|
||||
}
|
||||
}
|
||||
}
|
||||
finish()
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
scope.cancel()
|
||||
}
|
||||
|
||||
private suspend fun getSettings() : Settings {
|
||||
val settings = settingsRepo.getAll()
|
||||
return if (settings.isNotEmpty()) {
|
||||
@@ -77,6 +77,7 @@ class ShortcutsActivity : ComponentActivity() {
|
||||
}
|
||||
}
|
||||
companion object {
|
||||
const val TUNNEL_NAME_EXTRA_KEY = "tunnelName"
|
||||
const val CLASS_NAME_EXTRA_KEY = "className"
|
||||
}
|
||||
}
|
||||
+25
-29
@@ -31,7 +31,7 @@ class TunnelControlTile : TileService() {
|
||||
@Inject
|
||||
lateinit var vpnService : VpnService
|
||||
|
||||
private val scope = CoroutineScope(Dispatchers.Main);
|
||||
private val scope = CoroutineScope(Dispatchers.Main)
|
||||
|
||||
private lateinit var job : Job
|
||||
|
||||
@@ -42,14 +42,6 @@ class TunnelControlTile : TileService() {
|
||||
super.onStartListening()
|
||||
}
|
||||
|
||||
override fun onTileAdded() {
|
||||
super.onTileAdded()
|
||||
qsTile.contentDescription = this.resources.getString(R.string.toggle_vpn)
|
||||
scope.launch {
|
||||
updateTileState();
|
||||
}
|
||||
}
|
||||
|
||||
override fun onTileRemoved() {
|
||||
super.onTileRemoved()
|
||||
cancelJob()
|
||||
@@ -65,7 +57,7 @@ class TunnelControlTile : TileService() {
|
||||
unlockAndRun {
|
||||
scope.launch {
|
||||
try {
|
||||
val tunnel = determineTileTunnel();
|
||||
val tunnel = determineTileTunnel()
|
||||
if(tunnel != null) {
|
||||
attemptWatcherServiceToggle(tunnel.toString())
|
||||
if(vpnService.getState() == Tunnel.State.UP) {
|
||||
@@ -84,23 +76,23 @@ class TunnelControlTile : TileService() {
|
||||
}
|
||||
|
||||
private suspend fun determineTileTunnel() : TunnelConfig? {
|
||||
var tunnelConfig : TunnelConfig? = null;
|
||||
var tunnelConfig : TunnelConfig? = null
|
||||
val settings = settingsRepo.getAll()
|
||||
if (settings.isNotEmpty()) {
|
||||
val setting = settings.first()
|
||||
tunnelConfig = if (setting.defaultTunnel != null) {
|
||||
TunnelConfig.from(setting.defaultTunnel!!);
|
||||
TunnelConfig.from(setting.defaultTunnel!!)
|
||||
} else {
|
||||
val configs = configRepo.getAll();
|
||||
val configs = configRepo.getAll()
|
||||
val config = if(configs.isNotEmpty()) {
|
||||
configs.first();
|
||||
configs.first()
|
||||
} else {
|
||||
null
|
||||
}
|
||||
config
|
||||
}
|
||||
}
|
||||
return tunnelConfig;
|
||||
return tunnelConfig
|
||||
}
|
||||
|
||||
|
||||
@@ -118,20 +110,24 @@ class TunnelControlTile : TileService() {
|
||||
|
||||
private suspend fun updateTileState() {
|
||||
vpnService.state.collect {
|
||||
when(it) {
|
||||
Tunnel.State.UP -> {
|
||||
qsTile.state = Tile.STATE_ACTIVE
|
||||
}
|
||||
Tunnel.State.DOWN -> {
|
||||
qsTile.state = Tile.STATE_INACTIVE;
|
||||
}
|
||||
else -> {
|
||||
qsTile.state = Tile.STATE_UNAVAILABLE
|
||||
try {
|
||||
when(it) {
|
||||
Tunnel.State.UP -> {
|
||||
qsTile.state = Tile.STATE_ACTIVE
|
||||
}
|
||||
Tunnel.State.DOWN -> {
|
||||
qsTile.state = Tile.STATE_INACTIVE
|
||||
}
|
||||
else -> {
|
||||
qsTile.state = Tile.STATE_UNAVAILABLE
|
||||
}
|
||||
}
|
||||
val config = determineTileTunnel()
|
||||
setTileDescription(config?.name ?: this.resources.getString(R.string.no_tunnel_available))
|
||||
qsTile.updateTile()
|
||||
} catch (e : Exception) {
|
||||
Timber.e("Unable to update tile state")
|
||||
}
|
||||
val config = determineTileTunnel();
|
||||
setTileDescription(config?.name ?: this.resources.getString(R.string.no_tunnel_available))
|
||||
qsTile.updateTile()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -140,13 +136,13 @@ class TunnelControlTile : TileService() {
|
||||
qsTile.subtitle = description
|
||||
}
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
qsTile.stateDescription = description;
|
||||
qsTile.stateDescription = description
|
||||
}
|
||||
}
|
||||
|
||||
private fun cancelJob() {
|
||||
if(this::job.isInitialized) {
|
||||
job.cancel();
|
||||
job.cancel()
|
||||
}
|
||||
}
|
||||
}
|
||||
+18
-13
@@ -11,7 +11,6 @@ import com.zaneschepke.wireguardautotunnel.util.NumberUtils
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.channels.BufferOverflow
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
@@ -24,8 +23,7 @@ import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
|
||||
class WireGuardTunnel @Inject constructor(private val backend : Backend,
|
||||
) : VpnService {
|
||||
class WireGuardTunnel @Inject constructor(private val backend : Backend) : VpnService {
|
||||
|
||||
private val _tunnelName = MutableStateFlow("")
|
||||
override val tunnelName get() = _tunnelName.asStateFlow()
|
||||
@@ -47,28 +45,36 @@ class WireGuardTunnel @Inject constructor(private val backend : Backend,
|
||||
override val handshakeStatus: SharedFlow<HandshakeStatus>
|
||||
get() = _handshakeStatus.asSharedFlow()
|
||||
|
||||
private val scope = CoroutineScope(Dispatchers.IO);
|
||||
private val scope = CoroutineScope(Dispatchers.IO)
|
||||
|
||||
private lateinit var statsJob : Job
|
||||
|
||||
|
||||
override suspend fun startTunnel(tunnelConfig: TunnelConfig) : Tunnel.State{
|
||||
return try {
|
||||
if(getState() == Tunnel.State.UP && _tunnelName.value != tunnelConfig.name) {
|
||||
stopTunnel()
|
||||
}
|
||||
_tunnelName.emit(tunnelConfig.name)
|
||||
stopTunnelOnConfigChange(tunnelConfig)
|
||||
emitTunnelName(tunnelConfig.name)
|
||||
val config = TunnelConfig.configFromQuick(tunnelConfig.wgQuick)
|
||||
val state = backend.setState(
|
||||
this, Tunnel.State.UP, config)
|
||||
_state.emit(state)
|
||||
state;
|
||||
state
|
||||
} catch (e : Exception) {
|
||||
Timber.e("Failed to start tunnel with error: ${e.message}")
|
||||
Tunnel.State.DOWN
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun emitTunnelName(name : String) {
|
||||
_tunnelName.emit(name)
|
||||
}
|
||||
|
||||
private suspend fun stopTunnelOnConfigChange(tunnelConfig: TunnelConfig) {
|
||||
if(getState() == Tunnel.State.UP && _tunnelName.value != tunnelConfig.name) {
|
||||
stopTunnel()
|
||||
}
|
||||
}
|
||||
|
||||
override fun getName(): String {
|
||||
return _tunnelName.value
|
||||
}
|
||||
@@ -78,7 +84,6 @@ class WireGuardTunnel @Inject constructor(private val backend : Backend,
|
||||
if(getState() == Tunnel.State.UP) {
|
||||
val state = backend.setState(this, Tunnel.State.DOWN, null)
|
||||
_state.emit(state)
|
||||
scope.cancel()
|
||||
}
|
||||
} catch (e : BackendException) {
|
||||
Timber.e("Failed to stop tunnel with error: ${e.message}")
|
||||
@@ -90,7 +95,7 @@ class WireGuardTunnel @Inject constructor(private val backend : Backend,
|
||||
}
|
||||
|
||||
override fun onStateChange(state : Tunnel.State) {
|
||||
val tunnel = this;
|
||||
val tunnel = this
|
||||
_state.tryEmit(state)
|
||||
if(state == Tunnel.State.UP) {
|
||||
statsJob = scope.launch {
|
||||
@@ -109,11 +114,11 @@ class WireGuardTunnel @Inject constructor(private val backend : Backend,
|
||||
_handshakeStatus.emit(HandshakeStatus.NOT_STARTED)
|
||||
}
|
||||
if(neverHadHandshakeCounter <= HandshakeStatus.NEVER_CONNECTED_TO_UNHEALTHY_TIME_LIMIT_SEC) {
|
||||
neverHadHandshakeCounter += 10
|
||||
neverHadHandshakeCounter += (1 * Constants.VPN_STATISTIC_CHECK_INTERVAL/1000).toInt()
|
||||
}
|
||||
return@forEach
|
||||
}
|
||||
if(NumberUtils.getSecondsBetweenTimestampAndNow(handshakeEpoch) >= HandshakeStatus.UNHEALTHY_TIME_LIMIT_SEC) {
|
||||
if((NumberUtils.getSecondsBetweenTimestampAndNow(handshakeEpoch) ?: 0L) >= HandshakeStatus.UNHEALTHY_TIME_LIMIT_SEC) {
|
||||
_handshakeStatus.emit(HandshakeStatus.UNHEALTHY)
|
||||
} else {
|
||||
_handshakeStatus.emit(HandshakeStatus.HEALTHY)
|
||||
|
||||
@@ -11,6 +11,7 @@ import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.compose.animation.ExitTransition
|
||||
import androidx.compose.animation.ExperimentalAnimationApi
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.animation.fadeIn
|
||||
@@ -32,27 +33,26 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.input.key.onKeyEvent
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.google.accompanist.navigation.animation.AnimatedNavHost
|
||||
import com.google.accompanist.navigation.animation.composable
|
||||
import com.google.accompanist.navigation.animation.rememberAnimatedNavController
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.navigation.compose.NavHost
|
||||
import androidx.navigation.compose.composable
|
||||
import androidx.navigation.compose.rememberNavController
|
||||
import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
||||
import com.google.accompanist.permissions.isGranted
|
||||
import com.google.accompanist.permissions.rememberPermissionState
|
||||
import com.wireguard.android.backend.GoBackend
|
||||
import com.zaneschepke.wireguardautotunnel.Constants
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.CustomSnackBar
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.PermissionRequestFailedScreen
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.BottomNavBar
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.prompt.CustomSnackBar
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.config.ConfigScreen
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.detail.DetailScreen
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.main.MainScreen
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.SettingsScreen
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.support.SupportScreen
|
||||
import com.zaneschepke.wireguardautotunnel.ui.theme.TransparentSystemBars
|
||||
import com.zaneschepke.wireguardautotunnel.ui.theme.WireguardAutoTunnelTheme
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
@@ -66,7 +66,7 @@ class MainActivity : AppCompatActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContent {
|
||||
val navController = rememberAnimatedNavController()
|
||||
val navController = rememberNavController()
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
|
||||
WireguardAutoTunnelTheme {
|
||||
@@ -99,10 +99,10 @@ class MainActivity : AppCompatActivity() {
|
||||
}
|
||||
|
||||
fun showSnackBarMessage(message : String) {
|
||||
CoroutineScope(Dispatchers.Main).launch {
|
||||
lifecycleScope.launch(Dispatchers.Main) {
|
||||
val result = snackbarHostState.showSnackbar(
|
||||
message = message,
|
||||
actionLabel = "Okay",
|
||||
actionLabel = applicationContext.getString(R.string.okay),
|
||||
duration = SnackbarDuration.Short,
|
||||
)
|
||||
when (result) {
|
||||
@@ -171,7 +171,7 @@ class MainActivity : AppCompatActivity() {
|
||||
return@Scaffold
|
||||
}
|
||||
|
||||
AnimatedNavHost(navController, startDestination = Routes.Main.name) {
|
||||
NavHost(navController, startDestination = Routes.Main.name) {
|
||||
composable(Routes.Main.name, enterTransition = {
|
||||
when (initialState.destination.route) {
|
||||
Routes.Settings.name, Routes.Support.name ->
|
||||
@@ -184,7 +184,10 @@ class MainActivity : AppCompatActivity() {
|
||||
fadeIn(animationSpec = tween(Constants.FADE_IN_ANIMATION_DURATION))
|
||||
}
|
||||
}
|
||||
}) {
|
||||
}, exitTransition = {
|
||||
ExitTransition.None
|
||||
}
|
||||
) {
|
||||
MainScreen(padding = padding, showSnackbarMessage = { message -> showSnackBarMessage(message) }, navController = navController)
|
||||
}
|
||||
composable(Routes.Settings.name, enterTransition = {
|
||||
@@ -226,14 +229,6 @@ class MainActivity : AppCompatActivity() {
|
||||
val id = it.arguments?.getString("id")
|
||||
if(!id.isNullOrBlank()) {
|
||||
ConfigScreen(navController = navController, id = id, showSnackbarMessage = { message -> showSnackBarMessage(message) }, focusRequester = focusRequester)}
|
||||
}
|
||||
composable("${Routes.Detail.name}/{id}", enterTransition = {
|
||||
fadeIn(animationSpec = tween(Constants.FADE_IN_ANIMATION_DURATION))
|
||||
}) {
|
||||
val id = it.arguments?.getString("id")
|
||||
if(!id.isNullOrBlank()) {
|
||||
DetailScreen(padding = padding, focusRequester = focusRequester, id = id)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,8 +10,7 @@ enum class Routes {
|
||||
Main,
|
||||
Settings,
|
||||
Support,
|
||||
Config,
|
||||
Detail;
|
||||
Config;
|
||||
|
||||
|
||||
companion object {
|
||||
|
||||
@@ -1,27 +1,36 @@
|
||||
package com.zaneschepke.wireguardautotunnel.ui.common
|
||||
|
||||
import androidx.compose.animation.animateContentSize
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.wireguard.android.backend.Statistics
|
||||
import com.zaneschepke.wireguardautotunnel.toThreeDecimalPlaceString
|
||||
import com.zaneschepke.wireguardautotunnel.util.NumberUtils
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
fun RowListItem(icon : @Composable() () -> Unit, text : String, onHold : () -> Unit, onClick: () -> Unit, rowButton : @Composable() () -> Unit ) {
|
||||
fun RowListItem(icon : @Composable () -> Unit, text : String, onHold : () -> Unit,
|
||||
onClick: () -> Unit, rowButton : @Composable () -> Unit,
|
||||
expanded : Boolean, statistics: Statistics?
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.animateContentSize()
|
||||
.clip(RoundedCornerShape(30.dp))
|
||||
.combinedClickable(
|
||||
onClick = {
|
||||
onClick()
|
||||
@@ -31,19 +40,45 @@ fun RowListItem(icon : @Composable() () -> Unit, text : String, onHold : () -> U
|
||||
}
|
||||
)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(14.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically,) {
|
||||
icon()
|
||||
Text(text)
|
||||
Column {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 14.dp, vertical = 5.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically,) {
|
||||
icon()
|
||||
Text(text)
|
||||
}
|
||||
rowButton()
|
||||
}
|
||||
if(expanded) {
|
||||
statistics?.peers()?.forEach {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(end = 10.dp, bottom = 10.dp, start = 10.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceEvenly
|
||||
) {
|
||||
val handshakeEpoch = statistics.peer(it)!!.latestHandshakeEpochMillis
|
||||
val peerTx = statistics.peer(it)!!.txBytes
|
||||
val peerRx = statistics.peer(it)!!.rxBytes
|
||||
val peerId = it.toBase64().subSequence(0,3).toString() + "***"
|
||||
val handshakeSec = NumberUtils.getSecondsBetweenTimestampAndNow(handshakeEpoch)
|
||||
val handshake = if(handshakeSec == null) "never" else "$handshakeSec secs ago"
|
||||
val peerTxMB = NumberUtils.bytesToMB(peerTx).toThreeDecimalPlaceString()
|
||||
val peerRxMB = NumberUtils.bytesToMB(peerRx).toThreeDecimalPlaceString()
|
||||
val fontSize = 9.sp
|
||||
Text("peer: $peerId", fontSize = fontSize)
|
||||
Text("handshake: $handshake", fontSize = fontSize)
|
||||
Text("tx: $peerTxMB MB", fontSize = fontSize)
|
||||
Text("rx: $peerRxMB MB", fontSize = fontSize)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
rowButton()
|
||||
}
|
||||
}
|
||||
}
|
||||
-1
@@ -1,6 +1,5 @@
|
||||
package com.zaneschepke.wireguardautotunnel.ui.common.config
|
||||
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
|
||||
+79
@@ -0,0 +1,79 @@
|
||||
package com.zaneschepke.wireguardautotunnel.ui.common.prompt
|
||||
|
||||
import androidx.biometric.BiometricManager
|
||||
import androidx.biometric.BiometricManager.Authenticators.BIOMETRIC_WEAK
|
||||
import androidx.biometric.BiometricManager.Authenticators.DEVICE_CREDENTIAL
|
||||
import androidx.biometric.BiometricPrompt
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
|
||||
@Composable
|
||||
fun AuthorizationPrompt(onSuccess : () -> Unit, onFailure : () -> Unit, onError : (String) -> Unit) {
|
||||
val context = LocalContext.current
|
||||
val biometricManager = BiometricManager.from(context)
|
||||
val bio = biometricManager.canAuthenticate(BIOMETRIC_WEAK or DEVICE_CREDENTIAL)
|
||||
val isBiometricAvailable = remember {
|
||||
when(bio){
|
||||
BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE -> {
|
||||
onError("Biometrics not available")
|
||||
false
|
||||
}
|
||||
BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED -> {
|
||||
onError("Biometrics not created")
|
||||
false
|
||||
}
|
||||
BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE -> {
|
||||
onError("Biometric hardware not found")
|
||||
false
|
||||
}
|
||||
BiometricManager.BIOMETRIC_ERROR_SECURITY_UPDATE_REQUIRED -> {
|
||||
onError("Biometric security update required")
|
||||
false
|
||||
}
|
||||
BiometricManager.BIOMETRIC_ERROR_UNSUPPORTED -> {
|
||||
onError("Biometrics not supported")
|
||||
false
|
||||
}
|
||||
BiometricManager.BIOMETRIC_STATUS_UNKNOWN -> {
|
||||
onError("Biometrics status unknown")
|
||||
false
|
||||
}
|
||||
BiometricManager.BIOMETRIC_SUCCESS -> true
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
if(isBiometricAvailable) {
|
||||
val executor = remember { ContextCompat.getMainExecutor(context) }
|
||||
|
||||
val promptInfo = BiometricPrompt.PromptInfo.Builder()
|
||||
.setAllowedAuthenticators(BIOMETRIC_WEAK or DEVICE_CREDENTIAL)
|
||||
.setTitle("Biometric Authentication")
|
||||
.setSubtitle("Log in using your biometric credential")
|
||||
.build()
|
||||
|
||||
val biometricPrompt = BiometricPrompt(
|
||||
context as FragmentActivity,
|
||||
executor,
|
||||
object : BiometricPrompt.AuthenticationCallback() {
|
||||
override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
|
||||
super.onAuthenticationError(errorCode, errString)
|
||||
onFailure()
|
||||
}
|
||||
|
||||
override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
|
||||
super.onAuthenticationSucceeded(result)
|
||||
onSuccess()
|
||||
}
|
||||
|
||||
override fun onAuthenticationFailed() {
|
||||
super.onAuthenticationFailed()
|
||||
onFailure()
|
||||
}
|
||||
}
|
||||
)
|
||||
biometricPrompt.authenticate(promptInfo)
|
||||
}
|
||||
}
|
||||
+8
-5
@@ -1,10 +1,12 @@
|
||||
package com.zaneschepke.wireguardautotunnel.ui.common
|
||||
package com.zaneschepke.wireguardautotunnel.ui.common.prompt
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.IntrinsicSize
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.rounded.Info
|
||||
@@ -42,14 +44,15 @@ fun CustomSnackBar(
|
||||
if (isRtl) LayoutDirection.Rtl else LayoutDirection.Ltr
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
modifier = Modifier.width(IntrinsicSize.Max).height(IntrinsicSize.Min),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceEvenly
|
||||
horizontalArrangement = Arrangement.Start
|
||||
) {
|
||||
Icon(
|
||||
Icons.Rounded.Info,
|
||||
contentDescription = stringResource(R.string.info),
|
||||
tint = Color.White
|
||||
tint = Color.White,
|
||||
modifier = Modifier.padding(end = 10.dp)
|
||||
)
|
||||
Text(message, color = Color.White, modifier = Modifier.padding(end = 5.dp))
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
package com.zaneschepke.wireguardautotunnel.ui.models
|
||||
|
||||
import com.wireguard.config.Interface
|
||||
import com.wireguard.config.Peer
|
||||
|
||||
data class InterfaceProxy(
|
||||
var privateKey : String = "",
|
||||
@@ -12,15 +11,14 @@ data class InterfaceProxy(
|
||||
var mtu : String = "",
|
||||
){
|
||||
companion object {
|
||||
private fun String.removeWhiteSpaces() = replace("\\s".toRegex(), "")
|
||||
fun from(i : Interface) : InterfaceProxy {
|
||||
return InterfaceProxy(
|
||||
publicKey = i.keyPair.publicKey.toBase64().removeWhiteSpaces(),
|
||||
privateKey = i.keyPair.privateKey.toBase64().removeWhiteSpaces(),
|
||||
addresses = i.addresses.joinToString(",").removeWhiteSpaces(),
|
||||
dnsServers = i.dnsServers.joinToString(",").replace("/", "").removeWhiteSpaces(),
|
||||
listenPort = if(i.listenPort.isPresent) i.listenPort.get().toString().removeWhiteSpaces() else "",
|
||||
mtu = if(i.mtu.isPresent) i.mtu.get().toString().removeWhiteSpaces() else ""
|
||||
publicKey = i.keyPair.publicKey.toBase64().trim(),
|
||||
privateKey = i.keyPair.privateKey.toBase64().trim(),
|
||||
addresses = i.addresses.joinToString(", ").trim(),
|
||||
dnsServers = i.dnsServers.joinToString(", ").replace("/", "").trim(),
|
||||
listenPort = if(i.listenPort.isPresent) i.listenPort.get().toString().trim() else "",
|
||||
mtu = if(i.mtu.isPresent) i.mtu.get().toString().trim() else ""
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,16 +7,16 @@ data class PeerProxy(
|
||||
var preSharedKey : String = "",
|
||||
var persistentKeepalive : String = "",
|
||||
var endpoint : String = "",
|
||||
var allowedIps: String = IPV4_WILDCARD.joinToString(",")
|
||||
var allowedIps: String = IPV4_WILDCARD.joinToString(", ").trim()
|
||||
){
|
||||
companion object {
|
||||
fun from(peer : Peer) : PeerProxy {
|
||||
return PeerProxy(
|
||||
publicKey = peer.publicKey.toBase64(),
|
||||
preSharedKey = if(peer.preSharedKey.isPresent) peer.preSharedKey.get().toString() else "",
|
||||
persistentKeepalive = if(peer.persistentKeepalive.isPresent) peer.persistentKeepalive.get().toString() else "",
|
||||
endpoint = if(peer.endpoint.isPresent) peer.endpoint.get().toString() else "",
|
||||
allowedIps = peer.allowedIps.joinToString(",")
|
||||
preSharedKey = if(peer.preSharedKey.isPresent) peer.preSharedKey.get().toBase64().trim() else "",
|
||||
persistentKeepalive = if(peer.persistentKeepalive.isPresent) peer.persistentKeepalive.get().toString().trim() else "",
|
||||
endpoint = if(peer.endpoint.isPresent) peer.endpoint.get().toString().trim() else "",
|
||||
allowedIps = peer.allowedIps.joinToString(", ").trim()
|
||||
)
|
||||
}
|
||||
val IPV4_PUBLIC_NETWORKS = setOf(
|
||||
|
||||
+34
-20
@@ -3,6 +3,7 @@ package com.zaneschepke.wireguardautotunnel.ui.screens.config
|
||||
import android.annotation.SuppressLint
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.focusGroup
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
@@ -66,6 +67,7 @@ import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
import androidx.compose.ui.text.input.KeyboardCapitalization
|
||||
import androidx.compose.ui.text.input.PasswordVisualTransformation
|
||||
import androidx.compose.ui.text.input.VisualTransformation
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
@@ -77,7 +79,9 @@ import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
|
||||
import com.zaneschepke.wireguardautotunnel.ui.Routes
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.SearchBar
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.config.ConfigurationTextBox
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.prompt.AuthorizationPrompt
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.text.SectionTitle
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
|
||||
@@ -109,6 +113,8 @@ fun ConfigScreen(
|
||||
val proxyPeers by viewModel.proxyPeers.collectAsStateWithLifecycle()
|
||||
val proxyInterface by viewModel.interfaceProxy.collectAsStateWithLifecycle()
|
||||
var showApplicationsDialog by remember { mutableStateOf(false) }
|
||||
var showAuthPrompt by remember { mutableStateOf(false) }
|
||||
var isAuthenticated by remember { mutableStateOf(false) }
|
||||
val baseTextBoxModifier = Modifier.onFocusChanged {
|
||||
if(WireGuardAutoTunnel.isRunningOnAndroidTv(context)) {
|
||||
keyboardController?.hide()
|
||||
@@ -117,18 +123,7 @@ fun ConfigScreen(
|
||||
|
||||
val keyboardActions = KeyboardActions(
|
||||
onDone = {
|
||||
//focusManager.clearFocus()
|
||||
keyboardController?.hide()
|
||||
},
|
||||
onNext = {
|
||||
keyboardController?.hide()
|
||||
},
|
||||
onPrevious = {
|
||||
keyboardController?.hide()
|
||||
},
|
||||
onGo = {
|
||||
keyboardController?.hide(
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
@@ -142,11 +137,13 @@ fun ConfigScreen(
|
||||
val screenPadding = 5.dp
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
try {
|
||||
viewModel.onScreenLoad(id)
|
||||
} catch (e : Exception) {
|
||||
showSnackbarMessage(e.message!!)
|
||||
navController.navigate(Routes.Main.name)
|
||||
scope.launch(Dispatchers.IO) {
|
||||
try {
|
||||
viewModel.onScreenLoad(id)
|
||||
} catch (e : Exception) {
|
||||
showSnackbarMessage(e.message!!)
|
||||
navController.navigate(Routes.Main.name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -156,6 +153,21 @@ fun ConfigScreen(
|
||||
else "${checkedPackages.size} " + (if (include) "included" else "excluded")
|
||||
|
||||
}
|
||||
|
||||
if(showAuthPrompt) {
|
||||
AuthorizationPrompt(onSuccess = {
|
||||
showAuthPrompt = false
|
||||
isAuthenticated = true },
|
||||
onError = { error ->
|
||||
showSnackbarMessage(error)
|
||||
showAuthPrompt = false
|
||||
},
|
||||
onFailure = {
|
||||
showAuthPrompt = false
|
||||
showSnackbarMessage(context.getString(R.string.authentication_failed))
|
||||
})
|
||||
}
|
||||
|
||||
if (showApplicationsDialog) {
|
||||
val sortedPackages = remember(packages) {
|
||||
packages.sortedBy { viewModel.getPackageLabel(it) }
|
||||
@@ -236,7 +248,7 @@ fun ConfigScreen(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
SearchBar(viewModel::emitQueriedPackages);
|
||||
SearchBar(viewModel::emitQueriedPackages)
|
||||
}
|
||||
Spacer(Modifier.padding(5.dp))
|
||||
LazyColumn(
|
||||
@@ -399,10 +411,12 @@ fun ConfigScreen(
|
||||
modifier = baseTextBoxModifier.fillMaxWidth().focusRequester(focusRequester)
|
||||
)
|
||||
OutlinedTextField(
|
||||
modifier = baseTextBoxModifier.fillMaxWidth(),
|
||||
modifier = baseTextBoxModifier.fillMaxWidth().clickable {
|
||||
showAuthPrompt = true
|
||||
},
|
||||
value = proxyInterface.privateKey,
|
||||
visualTransformation = PasswordVisualTransformation(),
|
||||
enabled = id == Constants.MANUAL_TUNNEL_CONFIG_ID,
|
||||
visualTransformation = if((id == Constants.MANUAL_TUNNEL_CONFIG_ID) || isAuthenticated) VisualTransformation.None else PasswordVisualTransformation(),
|
||||
enabled = (id == Constants.MANUAL_TUNNEL_CONFIG_ID) || isAuthenticated,
|
||||
onValueChange = { value ->
|
||||
viewModel.onPrivateKeyChange(value)
|
||||
},
|
||||
|
||||
+13
-20
@@ -27,7 +27,6 @@ import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
@@ -63,14 +62,10 @@ class ConfigViewModel @Inject constructor(private val application : Application,
|
||||
|
||||
private lateinit var tunnelConfig: TunnelConfig
|
||||
|
||||
fun onScreenLoad(id : String) {
|
||||
suspend fun onScreenLoad(id : String) {
|
||||
if(id != Constants.MANUAL_TUNNEL_CONFIG_ID) {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
tunnelConfig = withContext(this.coroutineContext) {
|
||||
getTunnelConfigById(id) ?: throw WgTunnelException("Config not found")
|
||||
}
|
||||
emitScreenData()
|
||||
}
|
||||
tunnelConfig = getTunnelConfigById(id) ?: throw WgTunnelException("Config not found")
|
||||
emitScreenData()
|
||||
} else {
|
||||
emitEmptyScreenData()
|
||||
}
|
||||
@@ -271,22 +266,22 @@ class ConfigViewModel @Inject constructor(private val application : Application,
|
||||
fun buildPeerListFromProxyPeers() : List<Peer> {
|
||||
return _proxyPeers.value.map {
|
||||
val builder = Peer.Builder()
|
||||
if (it.allowedIps.isNotEmpty()) builder.parseAllowedIPs(it.allowedIps.removeWhiteSpaces())
|
||||
if (it.publicKey.isNotEmpty()) builder.parsePublicKey(it.publicKey.removeWhiteSpaces())
|
||||
if (it.preSharedKey.isNotEmpty()) builder.parsePreSharedKey(it.preSharedKey.removeWhiteSpaces())
|
||||
if (it.endpoint.isNotEmpty()) builder.parseEndpoint(it.endpoint.removeWhiteSpaces())
|
||||
if (it.persistentKeepalive.isNotEmpty()) builder.parsePersistentKeepalive(it.persistentKeepalive.removeWhiteSpaces())
|
||||
if (it.allowedIps.isNotEmpty()) builder.parseAllowedIPs(it.allowedIps.trim())
|
||||
if (it.publicKey.isNotEmpty()) builder.parsePublicKey(it.publicKey.trim())
|
||||
if (it.preSharedKey.isNotEmpty()) builder.parsePreSharedKey(it.preSharedKey.trim())
|
||||
if (it.endpoint.isNotEmpty()) builder.parseEndpoint(it.endpoint.trim())
|
||||
if (it.persistentKeepalive.isNotEmpty()) builder.parsePersistentKeepalive(it.persistentKeepalive.trim())
|
||||
builder.build()
|
||||
}
|
||||
}
|
||||
|
||||
fun buildInterfaceListFromProxyInterface() : Interface {
|
||||
val builder = Interface.Builder()
|
||||
builder.parsePrivateKey(_interface.value.privateKey.removeWhiteSpaces())
|
||||
builder.parseAddresses(_interface.value.addresses.removeWhiteSpaces())
|
||||
builder.parseDnsServers(_interface.value.dnsServers.removeWhiteSpaces())
|
||||
if(_interface.value.mtu.isNotEmpty()) builder.parseMtu(_interface.value.mtu.removeWhiteSpaces())
|
||||
if(_interface.value.listenPort.isNotEmpty()) builder.parseListenPort(_interface.value.listenPort.removeWhiteSpaces())
|
||||
builder.parsePrivateKey(_interface.value.privateKey.trim())
|
||||
builder.parseAddresses(_interface.value.addresses.trim())
|
||||
builder.parseDnsServers(_interface.value.dnsServers.trim())
|
||||
if(_interface.value.mtu.isNotEmpty()) builder.parseMtu(_interface.value.mtu.trim())
|
||||
if(_interface.value.listenPort.isNotEmpty()) builder.parseListenPort(_interface.value.listenPort.trim())
|
||||
if(isAllApplicationsEnabled()) _checkedPackages.value.clear()
|
||||
if(_include.value) builder.includeApplications(_checkedPackages.value)
|
||||
if(!_include.value) builder.excludeApplications(_checkedPackages.value)
|
||||
@@ -397,6 +392,4 @@ class ConfigViewModel @Inject constructor(private val application : Application,
|
||||
onInterfacePublicKeyChange("")
|
||||
}
|
||||
}
|
||||
|
||||
private fun String.removeWhiteSpaces() = replace("\\s".toRegex(), "")
|
||||
}
|
||||
-163
@@ -1,163 +0,0 @@
|
||||
package com.zaneschepke.wireguardautotunnel.ui.screens.detail
|
||||
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.focusGroup
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
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.platform.ClipboardManager
|
||||
import androidx.compose.ui.platform.LocalClipboardManager
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.text.font.FontStyle
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
|
||||
import com.zaneschepke.wireguardautotunnel.util.NumberUtils
|
||||
import java.time.Duration
|
||||
import java.time.Instant
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
fun DetailScreen(
|
||||
viewModel: DetailViewModel = hiltViewModel(),
|
||||
focusRequester: FocusRequester,
|
||||
padding: PaddingValues,
|
||||
id : String
|
||||
) {
|
||||
|
||||
val context = LocalContext.current
|
||||
val clipboardManager: ClipboardManager = LocalClipboardManager.current
|
||||
val tunnelStats by viewModel.tunnelStats.collectAsStateWithLifecycle(null)
|
||||
val tunnel by viewModel.tunnel.collectAsStateWithLifecycle(null)
|
||||
val tunnelName by viewModel.tunnelName.collectAsStateWithLifecycle()
|
||||
val lastHandshake by viewModel.lastHandshake.collectAsStateWithLifecycle(emptyMap())
|
||||
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
viewModel.emitConfig(id)
|
||||
}
|
||||
|
||||
if(null != tunnel) {
|
||||
val interfaceKey = tunnel?.`interface`?.keyPair?.publicKey?.toBase64().toString()
|
||||
val addresses = tunnel?.`interface`?.addresses!!.joinToString()
|
||||
val dnsServers = tunnel?.`interface`?.dnsServers!!.joinToString()
|
||||
val optionalMtu = tunnel?.`interface`?.mtu
|
||||
val mtu = if(optionalMtu?.isPresent == true) optionalMtu.get().toString() else stringResource(
|
||||
id = R.string.none
|
||||
)
|
||||
Column(
|
||||
horizontalAlignment = Alignment.Start,
|
||||
verticalArrangement = Arrangement.Top,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.fillMaxHeight(if(WireGuardAutoTunnel.isRunningOnAndroidTv(context)) 4/5f else 1f)
|
||||
.verticalScroll(rememberScrollState())
|
||||
.focusRequester(focusRequester)
|
||||
.padding(padding)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 20.dp, vertical = 7.dp).focusGroup(),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
Column(modifier = Modifier.weight(1f, true)) {
|
||||
Text(stringResource(R.string.config_interface), fontWeight = FontWeight.Bold, fontSize = 20.sp)
|
||||
Text(stringResource(R.string.name), fontStyle = FontStyle.Italic)
|
||||
Text(text = tunnelName, modifier = Modifier.clickable {
|
||||
clipboardManager.setText(AnnotatedString(tunnelName))
|
||||
})
|
||||
Text(stringResource(R.string.public_key), fontStyle = FontStyle.Italic)
|
||||
Text(text = interfaceKey, modifier = Modifier.clickable {
|
||||
clipboardManager.setText(AnnotatedString(interfaceKey))
|
||||
})
|
||||
Text(stringResource(R.string.addresses), fontStyle = FontStyle.Italic)
|
||||
Text(text = addresses, modifier = Modifier.clickable {
|
||||
clipboardManager.setText(AnnotatedString(addresses))
|
||||
})
|
||||
Text(stringResource(R.string.dns_servers), fontStyle = FontStyle.Italic)
|
||||
Text(text = dnsServers, modifier = Modifier.clickable {
|
||||
clipboardManager.setText(AnnotatedString(dnsServers))
|
||||
})
|
||||
Text(stringResource(R.string.mtu), fontStyle = FontStyle.Italic)
|
||||
Text(text = mtu, modifier = Modifier.clickable {
|
||||
clipboardManager.setText(AnnotatedString(mtu))
|
||||
})
|
||||
Box(modifier = Modifier.padding(10.dp))
|
||||
tunnel?.peers?.forEach{
|
||||
val peerKey = it.publicKey.toBase64().toString()
|
||||
val allowedIps = it.allowedIps.joinToString()
|
||||
val endpoint = if(it.endpoint.isPresent) it.endpoint.get().toString() else stringResource(
|
||||
id = R.string.none
|
||||
)
|
||||
Text(stringResource(R.string.peer), fontWeight = FontWeight.Bold, fontSize = 20.sp)
|
||||
Text(stringResource(R.string.public_key), fontStyle = FontStyle.Italic)
|
||||
Text(text = peerKey, modifier = Modifier.clickable {
|
||||
clipboardManager.setText(AnnotatedString(peerKey))
|
||||
})
|
||||
Text(stringResource(id = R.string.allowed_ips), fontStyle = FontStyle.Italic)
|
||||
Text(text = allowedIps, modifier = Modifier.clickable {
|
||||
clipboardManager.setText(AnnotatedString(allowedIps))
|
||||
})
|
||||
Text(stringResource(R.string.endpoint), fontStyle = FontStyle.Italic)
|
||||
Text(text = endpoint, modifier = Modifier.clickable {
|
||||
clipboardManager.setText(AnnotatedString(endpoint))
|
||||
})
|
||||
if (tunnelStats != null) {
|
||||
val totalRx = tunnelStats?.totalRx() ?: 0
|
||||
val totalTx = tunnelStats?.totalTx() ?: 0
|
||||
if((totalRx + totalTx != 0L)) {
|
||||
val rxKB = NumberUtils.bytesToKB(tunnelStats!!.totalRx())
|
||||
val txKB = NumberUtils.bytesToKB(tunnelStats!!.totalTx())
|
||||
Text(stringResource(R.string.transfer), fontStyle = FontStyle.Italic)
|
||||
val transfer = "rx: ${NumberUtils.formatDecimalTwoPlaces(rxKB)} KB tx: ${NumberUtils.formatDecimalTwoPlaces(txKB)} KB"
|
||||
Text(transfer, modifier = Modifier.clickable {
|
||||
clipboardManager.setText(AnnotatedString(transfer))})
|
||||
Text(stringResource(R.string.last_handshake), fontStyle = FontStyle.Italic)
|
||||
val handshakeEpoch = lastHandshake[it.publicKey]
|
||||
if(handshakeEpoch != null) {
|
||||
if(handshakeEpoch == 0L) {
|
||||
Text(stringResource(id = R.string.never), modifier = Modifier.clickable {
|
||||
clipboardManager.setText(AnnotatedString(context.getString(R.string.never)))
|
||||
})
|
||||
} else {
|
||||
val time = Instant.ofEpochMilli(handshakeEpoch)
|
||||
val duration = "${Duration.between(time, Instant.now()).seconds} seconds ago"
|
||||
Text(duration, modifier = Modifier.clickable {
|
||||
clipboardManager.setText(AnnotatedString(duration))
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
-46
@@ -1,46 +0,0 @@
|
||||
package com.zaneschepke.wireguardautotunnel.ui.screens.detail
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.wireguard.config.Config
|
||||
import com.zaneschepke.wireguardautotunnel.repository.TunnelConfigDao
|
||||
import com.zaneschepke.wireguardautotunnel.repository.model.TunnelConfig
|
||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnService
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class DetailViewModel @Inject constructor(private val tunnelRepo : TunnelConfigDao, private val vpnService : VpnService
|
||||
) : ViewModel() {
|
||||
|
||||
private val _tunnel = MutableStateFlow<Config?>(null)
|
||||
val tunnel get() = _tunnel.asStateFlow()
|
||||
|
||||
private val _tunnelName = MutableStateFlow("")
|
||||
val tunnelName = _tunnelName.asStateFlow()
|
||||
val tunnelStats get() = vpnService.statistics
|
||||
val lastHandshake get() = vpnService.lastHandshake
|
||||
|
||||
private suspend fun getTunnelConfigById(id: String): TunnelConfig? {
|
||||
return try {
|
||||
tunnelRepo.getById(id.toLong())
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e.message)
|
||||
null
|
||||
}
|
||||
}
|
||||
fun emitConfig(id: String) {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
val tunnelConfig = getTunnelConfigById(id)
|
||||
if(tunnelConfig != null) {
|
||||
_tunnelName.emit(tunnelConfig.name)
|
||||
_tunnel.emit(TunnelConfig.configFromQuick(tunnelConfig.wgQuick))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+59
-34
@@ -106,7 +106,7 @@ fun MainScreen(
|
||||
val haptic = LocalHapticFeedback.current
|
||||
val context = LocalContext.current
|
||||
val isVisible = rememberSaveable { mutableStateOf(true) }
|
||||
val scope = rememberCoroutineScope()
|
||||
val scope = rememberCoroutineScope { Dispatchers.IO }
|
||||
|
||||
val sheetState = rememberModalBottomSheetState()
|
||||
var showBottomSheet by remember { mutableStateOf(false) }
|
||||
@@ -117,6 +117,7 @@ fun MainScreen(
|
||||
val state by viewModel.state.collectAsStateWithLifecycle(Tunnel.State.DOWN)
|
||||
val tunnelName by viewModel.tunnelName.collectAsStateWithLifecycle("")
|
||||
val settings by viewModel.settings.collectAsStateWithLifecycle()
|
||||
val statistics by viewModel.statistics.collectAsStateWithLifecycle(null)
|
||||
|
||||
// Nested scroll for control FAB
|
||||
val nestedScrollConnection = remember {
|
||||
@@ -150,7 +151,7 @@ fun MainScreen(
|
||||
val name = it.activityInfo.packageName
|
||||
name.startsWith(Constants.GOOGLE_TV_EXPLORER_STUB) || name.startsWith(Constants.ANDROID_TV_EXPLORER_STUB)
|
||||
}) {
|
||||
throw WgTunnelException("No file explorer installed")
|
||||
throw WgTunnelException(context.getString(R.string.no_file_explorer))
|
||||
}
|
||||
return intent
|
||||
}
|
||||
@@ -159,8 +160,8 @@ fun MainScreen(
|
||||
scope.launch(Dispatchers.IO) {
|
||||
try {
|
||||
viewModel.onTunnelFileSelected(data)
|
||||
} catch (e : Exception) {
|
||||
showSnackbarMessage(e.message ?: "Unknown error occurred")
|
||||
} catch (e : WgTunnelException) {
|
||||
showSnackbarMessage(e.message)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -168,10 +169,18 @@ fun MainScreen(
|
||||
val scanLauncher = rememberLauncherForActivityResult(
|
||||
contract = ScanContract(),
|
||||
onResult = {
|
||||
try {
|
||||
viewModel.onTunnelQrResult(it.contents)
|
||||
} catch (e: Exception) {
|
||||
showSnackbarMessage(context.getString(R.string.qr_result_failed))
|
||||
scope.launch {
|
||||
try {
|
||||
viewModel.onTunnelQrResult(it.contents)
|
||||
} catch (e: Exception) {
|
||||
when(e) {
|
||||
is WgTunnelException -> {
|
||||
showSnackbarMessage(e.message)
|
||||
} else -> {
|
||||
showSnackbarMessage("No QR code scanned")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
@@ -198,7 +207,7 @@ fun MainScreen(
|
||||
{ Text(text = stringResource(R.string.cancel)) }
|
||||
},
|
||||
title = { Text(text = stringResource(R.string.primary_tunnel_change)) },
|
||||
text = { Text(text = stringResource(R.string.primary_tunnnel_change_question)) }
|
||||
text = { Text(text = stringResource(R.string.primary_tunnel_change_question)) }
|
||||
)
|
||||
}
|
||||
|
||||
@@ -227,9 +236,12 @@ fun MainScreen(
|
||||
val hoverColor = MaterialTheme.colorScheme.surfaceColorAtElevation(2.dp)
|
||||
var fobColor by remember { mutableStateOf(secondaryColor) }
|
||||
FloatingActionButton(
|
||||
modifier = Modifier.padding(bottom = 90.dp).onFocusChanged {
|
||||
if(WireGuardAutoTunnel.isRunningOnAndroidTv(context)) {
|
||||
fobColor = if (it.isFocused) hoverColor else secondaryColor }
|
||||
modifier = Modifier
|
||||
.padding(bottom = 90.dp)
|
||||
.onFocusChanged {
|
||||
if (WireGuardAutoTunnel.isRunningOnAndroidTv(context)) {
|
||||
fobColor = if (it.isFocused) hoverColor else secondaryColor
|
||||
}
|
||||
}
|
||||
,
|
||||
onClick = {
|
||||
@@ -273,7 +285,7 @@ fun MainScreen(
|
||||
showBottomSheet = false
|
||||
try {
|
||||
tunnelFileImportResultLauncher.launch(Constants.ALLOWED_FILE_TYPES)
|
||||
} catch (e : Exception) {
|
||||
} catch (e: Exception) {
|
||||
showSnackbarMessage(e.message!!)
|
||||
}
|
||||
}
|
||||
@@ -285,7 +297,7 @@ fun MainScreen(
|
||||
modifier = Modifier.padding(10.dp)
|
||||
)
|
||||
Text(
|
||||
stringResource(id = R.string.add_from_file),
|
||||
stringResource(id = R.string.add_tunnels_text),
|
||||
modifier = Modifier.padding(10.dp)
|
||||
)
|
||||
}
|
||||
@@ -360,17 +372,24 @@ fun MainScreen(
|
||||
HandshakeStatus.NEVER_CONNECTED -> brickRed
|
||||
} else {Color.Gray})
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
val expanded = remember {
|
||||
mutableStateOf(false)
|
||||
}
|
||||
RowListItem(icon = {
|
||||
if (settings.isTunnelConfigDefault(tunnel))
|
||||
Icon(
|
||||
Icons.Rounded.Star, "status",
|
||||
Icons.Rounded.Star, stringResource(R.string.status),
|
||||
tint = leadingIconColor,
|
||||
modifier = Modifier.padding(end = 10.dp).size(20.dp)
|
||||
modifier = Modifier
|
||||
.padding(end = 10.dp)
|
||||
.size(20.dp)
|
||||
)
|
||||
else Icon(
|
||||
Icons.Rounded.Circle, "status",
|
||||
Icons.Rounded.Circle, stringResource(R.string.status),
|
||||
tint = leadingIconColor,
|
||||
modifier = Modifier.padding(end = 15.dp).size(15.dp)
|
||||
modifier = Modifier
|
||||
.padding(end = 15.dp)
|
||||
.size(15.dp)
|
||||
)
|
||||
},
|
||||
text = tunnel.name,
|
||||
@@ -384,12 +403,16 @@ fun MainScreen(
|
||||
},
|
||||
onClick = {
|
||||
if (!WireGuardAutoTunnel.isRunningOnAndroidTv(context)) {
|
||||
navController.navigate("${Routes.Detail.name}/${tunnel.id}")
|
||||
if(state == Tunnel.State.UP && (tunnelName == tunnel.name) ) {
|
||||
expanded.value = !expanded.value
|
||||
}
|
||||
} else {
|
||||
selectedTunnel = tunnel
|
||||
focusRequester.requestFocus()
|
||||
}
|
||||
},
|
||||
statistics = statistics,
|
||||
expanded = expanded.value,
|
||||
rowButton = {
|
||||
if (tunnel.id == selectedTunnel?.id && !WireGuardAutoTunnel.isRunningOnAndroidTv(context)) {
|
||||
Row {
|
||||
@@ -417,6 +440,17 @@ fun MainScreen(
|
||||
}
|
||||
}
|
||||
} else {
|
||||
val checked = state == Tunnel.State.UP && tunnel.name == tunnelName
|
||||
if(!checked) expanded.value = false
|
||||
@Composable
|
||||
fun TunnelSwitch() = Switch(
|
||||
modifier = Modifier.focusRequester(focusRequester),
|
||||
checked = checked,
|
||||
onCheckedChange = { checked ->
|
||||
if(!checked) expanded.value = false
|
||||
onTunnelToggle(checked, tunnel)
|
||||
}
|
||||
)
|
||||
if (WireGuardAutoTunnel.isRunningOnAndroidTv(context)) {
|
||||
Row {
|
||||
if(!settings.isTunnelConfigDefault(tunnel)) {
|
||||
@@ -431,9 +465,11 @@ fun MainScreen(
|
||||
IconButton(
|
||||
modifier = Modifier.focusRequester(focusRequester),
|
||||
onClick = {
|
||||
navController.navigate("${Routes.Detail.name}/${tunnel.id}")
|
||||
if(state == Tunnel.State.UP && (tunnelName == tunnel.name) ) {
|
||||
expanded.value = !expanded.value
|
||||
}
|
||||
}) {
|
||||
Icon(Icons.Rounded.Info, "Info")
|
||||
Icon(Icons.Rounded.Info, stringResource(R.string.info))
|
||||
}
|
||||
IconButton(onClick = {
|
||||
if (state == Tunnel.State.UP && tunnel.name == tunnelName)
|
||||
@@ -467,21 +503,10 @@ fun MainScreen(
|
||||
stringResource(id = R.string.delete)
|
||||
)
|
||||
}
|
||||
Switch(
|
||||
modifier = Modifier.focusRequester(focusRequester),
|
||||
checked = (state == Tunnel.State.UP && tunnel.name == tunnelName),
|
||||
onCheckedChange = { checked ->
|
||||
onTunnelToggle(checked, tunnel)
|
||||
}
|
||||
)
|
||||
TunnelSwitch()
|
||||
}
|
||||
} else {
|
||||
Switch(
|
||||
checked = (state == Tunnel.State.UP && tunnel.name == tunnelName),
|
||||
onCheckedChange = { checked ->
|
||||
onTunnelToggle(checked, tunnel)
|
||||
}
|
||||
)
|
||||
TunnelSwitch()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
+48
-37
@@ -7,8 +7,6 @@ import android.net.Uri
|
||||
import android.provider.OpenableColumns
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.wireguard.config.BadConfigException
|
||||
import com.wireguard.config.Config
|
||||
import com.zaneschepke.wireguardautotunnel.Constants
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
@@ -30,7 +28,9 @@ import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.filter
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.io.InputStream
|
||||
import java.util.zip.ZipInputStream
|
||||
import javax.inject.Inject
|
||||
|
||||
|
||||
@@ -49,6 +49,7 @@ class MainViewModel @Inject constructor(
|
||||
val tunnelName get() = vpnService.tunnelName
|
||||
private val _settings = MutableStateFlow(Settings())
|
||||
val settings get() = _settings.asStateFlow()
|
||||
val statistics get() = vpnService.statistics
|
||||
|
||||
init {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
@@ -118,39 +119,26 @@ class MainViewModel @Inject constructor(
|
||||
}
|
||||
|
||||
private fun validateConfigString(config: String) {
|
||||
if (!config.contains(application.getString(R.string.config_validation))) {
|
||||
throw WgTunnelException(application.getString(R.string.config_validation))
|
||||
TunnelConfig.configFromQuick(config)
|
||||
}
|
||||
|
||||
suspend fun onTunnelQrResult(result: String) {
|
||||
try {
|
||||
validateConfigString(result)
|
||||
val tunnelConfig =
|
||||
TunnelConfig(name = NumberUtils.generateRandomTunnelName(), wgQuick = result)
|
||||
addTunnel(tunnelConfig)
|
||||
} catch (e : Exception) {
|
||||
throw WgTunnelException(e)
|
||||
}
|
||||
}
|
||||
|
||||
fun onTunnelQrResult(result: String) {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
try {
|
||||
validateConfigString(result)
|
||||
val tunnelConfig =
|
||||
TunnelConfig(name = NumberUtils.generateRandomTunnelName(), wgQuick = result)
|
||||
addTunnel(tunnelConfig)
|
||||
} catch (e: WgTunnelException) {
|
||||
throw WgTunnelException(
|
||||
e.message ?: application.getString(R.string.unknown_error_message)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun validateFileExtension(fileName: String) {
|
||||
val extension = getFileExtensionFromFileName(fileName)
|
||||
if (extension != Constants.VALID_FILE_EXTENSION) {
|
||||
throw WgTunnelException(application.getString(R.string.file_extension_message))
|
||||
}
|
||||
}
|
||||
|
||||
private fun saveTunnelConfigFromStream(stream: InputStream, fileName: String) {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
val bufferReader = stream.bufferedReader(charset = Charsets.UTF_8)
|
||||
val config = Config.parse(bufferReader)
|
||||
val tunnelName = getNameFromFileName(fileName)
|
||||
addTunnel(TunnelConfig(name = tunnelName, wgQuick = config.toWgQuickString()))
|
||||
private suspend fun saveTunnelConfigFromStream(stream: InputStream, fileName: String) {
|
||||
val bufferReader = stream.bufferedReader(charset = Charsets.UTF_8)
|
||||
val config = Config.parse(bufferReader)
|
||||
val tunnelName = getNameFromFileName(fileName)
|
||||
addTunnel(TunnelConfig(name = tunnelName, wgQuick = config.toWgQuickString()))
|
||||
withContext(Dispatchers.IO) {
|
||||
stream.close()
|
||||
}
|
||||
}
|
||||
@@ -160,17 +148,40 @@ class MainViewModel @Inject constructor(
|
||||
?: throw WgTunnelException(application.getString(R.string.stream_failed))
|
||||
}
|
||||
|
||||
fun onTunnelFileSelected(uri: Uri) {
|
||||
suspend fun onTunnelFileSelected(uri: Uri) {
|
||||
try {
|
||||
val fileName = getFileName(application.applicationContext, uri)
|
||||
validateFileExtension(fileName)
|
||||
val stream = getInputStreamFromUri(uri)
|
||||
saveTunnelConfigFromStream(stream, fileName)
|
||||
val fileExtension = getFileExtensionFromFileName(fileName)
|
||||
when(fileExtension){
|
||||
Constants.CONF_FILE_EXTENSION -> saveTunnelFromConfUri(fileName, uri)
|
||||
Constants.ZIP_FILE_EXTENSION -> saveTunnelsFromZipUri(uri)
|
||||
else -> throw WgTunnelException(application.getString(R.string.file_extension_message))
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
throw WgTunnelException(e.message ?: "Error importing file")
|
||||
throw WgTunnelException(e)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun saveTunnelsFromZipUri(uri: Uri) {
|
||||
ZipInputStream(getInputStreamFromUri(uri)).use { zip ->
|
||||
generateSequence { zip.nextEntry }
|
||||
.filterNot { it.isDirectory ||
|
||||
getFileExtensionFromFileName(it.name) != Constants.CONF_FILE_EXTENSION }
|
||||
.forEach {
|
||||
val name = getNameFromFileName(it.name)
|
||||
val config = Config.parse(zip)
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
addTunnel(TunnelConfig(name = name, wgQuick = config.toWgQuickString()))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun saveTunnelFromConfUri(name : String, uri: Uri) {
|
||||
val stream = getInputStreamFromUri(uri)
|
||||
saveTunnelConfigFromStream(stream, name)
|
||||
}
|
||||
|
||||
private suspend fun addTunnel(tunnelConfig: TunnelConfig) {
|
||||
saveTunnel(tunnelConfig)
|
||||
}
|
||||
|
||||
+238
-125
@@ -5,6 +5,7 @@ import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.provider.Settings
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
@@ -15,7 +16,6 @@ import androidx.compose.foundation.layout.IntrinsicSize
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
@@ -69,8 +69,12 @@ import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.ClickableIconButton
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.config.ConfigurationToggle
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.prompt.AuthorizationPrompt
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.text.SectionTitle
|
||||
import com.zaneschepke.wireguardautotunnel.util.StorageUtil
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import java.io.File
|
||||
|
||||
@OptIn(
|
||||
ExperimentalPermissionsApi::class,
|
||||
@@ -84,7 +88,7 @@ fun SettingsScreen(
|
||||
focusRequester: FocusRequester,
|
||||
) {
|
||||
|
||||
val scope = rememberCoroutineScope()
|
||||
val scope = rememberCoroutineScope { Dispatchers.IO }
|
||||
val context = LocalContext.current
|
||||
val focusManager = LocalFocusManager.current
|
||||
val keyboardController = LocalSoftwareKeyboardController.current
|
||||
@@ -96,13 +100,32 @@ fun SettingsScreen(
|
||||
val fineLocationState = rememberPermissionState(Manifest.permission.ACCESS_FINE_LOCATION)
|
||||
var currentText by remember { mutableStateOf("") }
|
||||
val scrollState = rememberScrollState()
|
||||
var didShowLocationDisclaimer by remember { mutableStateOf(false) }
|
||||
var isLocationDisclaimerNeeded by remember { mutableStateOf(true) }
|
||||
var isBackgroundLocationGranted by remember { mutableStateOf(true) }
|
||||
var showAuthPrompt by remember { mutableStateOf(false) }
|
||||
var didExportFiles by remember { mutableStateOf(false) }
|
||||
|
||||
|
||||
|
||||
val screenPadding = 5.dp
|
||||
val fillMaxHeight = .85f
|
||||
val fillMaxWidth = .85f
|
||||
|
||||
fun exportAllConfigs() {
|
||||
try {
|
||||
val files = tunnels.map { File(context.cacheDir, "${it.name}.conf") }
|
||||
files.forEachIndexed { index, file ->
|
||||
file.outputStream().use {
|
||||
it.write(tunnels[index].wgQuick.toByteArray())
|
||||
}
|
||||
}
|
||||
StorageUtil.saveFilesToZip(context, files)
|
||||
didExportFiles = true
|
||||
showSnackbarMessage(context.getString(R.string.exported_configs_message))
|
||||
} catch (e : Exception) {
|
||||
showSnackbarMessage(e.message!!)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fun saveTrustedSSID() {
|
||||
if (currentText.isNotEmpty()) {
|
||||
@@ -111,7 +134,7 @@ fun SettingsScreen(
|
||||
viewModel.onSaveTrustedSSID(currentText)
|
||||
currentText = ""
|
||||
} catch (e : Exception) {
|
||||
showSnackbarMessage(e.message ?: "Unknown error")
|
||||
showSnackbarMessage(e.message ?: context.getString(R.string.unknown_error))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -121,6 +144,7 @@ fun SettingsScreen(
|
||||
return(isBackgroundLocationGranted && fineLocationState.status.isGranted && !viewModel.isLocationServicesNeeded())
|
||||
}
|
||||
|
||||
|
||||
fun openSettings() {
|
||||
scope.launch {
|
||||
val intentSettings =
|
||||
@@ -136,62 +160,89 @@ fun SettingsScreen(
|
||||
rememberPermissionState(Manifest.permission.ACCESS_BACKGROUND_LOCATION)
|
||||
if(!backgroundLocationState.status.isGranted) {
|
||||
isBackgroundLocationGranted = false
|
||||
if(!didShowLocationDisclaimer) {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Top,
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.verticalScroll(scrollState)
|
||||
.padding(padding)
|
||||
) {
|
||||
Icon(
|
||||
Icons.Rounded.LocationOff,
|
||||
contentDescription = stringResource(id = R.string.map),
|
||||
modifier = Modifier
|
||||
.padding(30.dp)
|
||||
.size(128.dp)
|
||||
)
|
||||
Text(
|
||||
stringResource(R.string.prominent_background_location_title),
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier.padding(30.dp),
|
||||
fontSize = 20.sp
|
||||
)
|
||||
Text(
|
||||
stringResource(R.string.prominent_background_location_message),
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier.padding(30.dp),
|
||||
fontSize = 15.sp
|
||||
)
|
||||
Row(
|
||||
modifier = if (WireGuardAutoTunnel.isRunningOnAndroidTv(context)) Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(10.dp) else Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(30.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceEvenly
|
||||
) {
|
||||
TextButton(onClick = {
|
||||
didShowLocationDisclaimer = true
|
||||
}) {
|
||||
Text(stringResource(id = R.string.no_thanks))
|
||||
}
|
||||
TextButton(modifier = Modifier.focusRequester(focusRequester), onClick = {
|
||||
openSettings()
|
||||
}) {
|
||||
Text(stringResource(id = R.string.turn_on))
|
||||
}
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
} else {
|
||||
isLocationDisclaimerNeeded = false
|
||||
isBackgroundLocationGranted = true
|
||||
}
|
||||
}
|
||||
|
||||
if(Build.VERSION.SDK_INT <= Build.VERSION_CODES.P) {
|
||||
if(!fineLocationState.status.isGranted) {
|
||||
isBackgroundLocationGranted = false
|
||||
} else {
|
||||
isLocationDisclaimerNeeded = false
|
||||
isBackgroundLocationGranted = true
|
||||
}
|
||||
}
|
||||
|
||||
if(isLocationDisclaimerNeeded) {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Top,
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.verticalScroll(scrollState)
|
||||
.padding(padding)
|
||||
) {
|
||||
Icon(
|
||||
Icons.Rounded.LocationOff,
|
||||
contentDescription = stringResource(id = R.string.map),
|
||||
modifier = Modifier
|
||||
.padding(30.dp)
|
||||
.size(128.dp)
|
||||
)
|
||||
Text(
|
||||
stringResource(R.string.prominent_background_location_title),
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier.padding(30.dp),
|
||||
fontSize = 20.sp
|
||||
)
|
||||
Text(
|
||||
stringResource(R.string.prominent_background_location_message),
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier.padding(30.dp),
|
||||
fontSize = 15.sp
|
||||
)
|
||||
Row(
|
||||
modifier = if (WireGuardAutoTunnel.isRunningOnAndroidTv(context)) Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(10.dp) else Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(30.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceEvenly
|
||||
) {
|
||||
TextButton(onClick = {
|
||||
isLocationDisclaimerNeeded = false
|
||||
}) {
|
||||
Text(stringResource(id = R.string.no_thanks))
|
||||
}
|
||||
TextButton(modifier = Modifier.focusRequester(focusRequester), onClick = {
|
||||
openSettings()
|
||||
}) {
|
||||
Text(stringResource(id = R.string.turn_on))
|
||||
}
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
|
||||
if(showAuthPrompt) {
|
||||
AuthorizationPrompt(onSuccess = {
|
||||
showAuthPrompt = false
|
||||
exportAllConfigs() },
|
||||
onError = { error ->
|
||||
showSnackbarMessage(error)
|
||||
showAuthPrompt = false
|
||||
},
|
||||
onFailure = {
|
||||
showAuthPrompt = false
|
||||
showSnackbarMessage(context.getString(R.string.authentication_failed))
|
||||
})
|
||||
}
|
||||
|
||||
if (tunnels.isEmpty()) {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
@@ -229,7 +280,10 @@ fun SettingsScreen(
|
||||
Modifier
|
||||
.height(IntrinsicSize.Min)
|
||||
.fillMaxWidth(fillMaxWidth)
|
||||
else Modifier.fillMaxWidth(fillMaxWidth)).padding(top = 60.dp, bottom = 25.dp)
|
||||
.padding(top = 10.dp)
|
||||
else Modifier
|
||||
.fillMaxWidth(fillMaxWidth)
|
||||
.padding(top = 60.dp)).padding(bottom = 25.dp)
|
||||
) {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.Start,
|
||||
@@ -237,64 +291,77 @@ fun SettingsScreen(
|
||||
modifier = Modifier.padding(15.dp)
|
||||
) {
|
||||
SectionTitle(title = stringResource(id = R.string.auto_tunneling), padding = screenPadding)
|
||||
Text(
|
||||
stringResource(R.string.trusted_ssid),
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier.padding(screenPadding, bottom = 5.dp, top = 5.dp)
|
||||
ConfigurationToggle(
|
||||
stringResource(id = R.string.tunnel_on_wifi),
|
||||
enabled = !(settings.isAutoTunnelEnabled || settings.isAlwaysOnVpnEnabled),
|
||||
checked = settings.isTunnelOnWifiEnabled,
|
||||
padding = screenPadding,
|
||||
onCheckChanged = {
|
||||
scope.launch {
|
||||
viewModel.onToggleTunnelOnWifi()
|
||||
}
|
||||
},
|
||||
modifier = Modifier.focusRequester(focusRequester)
|
||||
)
|
||||
FlowRow(
|
||||
modifier = Modifier.padding(screenPadding),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
verticalArrangement = Arrangement.SpaceEvenly
|
||||
) {
|
||||
trustedSSIDs.forEach { ssid ->
|
||||
ClickableIconButton(
|
||||
onIconClick = {
|
||||
scope.launch {
|
||||
viewModel.onDeleteTrustedSSID(ssid)
|
||||
AnimatedVisibility(visible = settings.isTunnelOnWifiEnabled) {
|
||||
Column {
|
||||
FlowRow(
|
||||
modifier = Modifier.padding(screenPadding),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
verticalArrangement = Arrangement.SpaceEvenly
|
||||
) {
|
||||
trustedSSIDs.forEach { ssid ->
|
||||
ClickableIconButton(
|
||||
onIconClick = {
|
||||
scope.launch {
|
||||
viewModel.onDeleteTrustedSSID(ssid)
|
||||
}
|
||||
},
|
||||
text = ssid,
|
||||
icon = Icons.Filled.Close,
|
||||
enabled = !(settings.isAutoTunnelEnabled || settings.isAlwaysOnVpnEnabled)
|
||||
)
|
||||
}
|
||||
if(trustedSSIDs.isEmpty()) {
|
||||
Text(stringResource(R.string.none), fontStyle = FontStyle.Italic, color = Color.Gray)
|
||||
}
|
||||
}
|
||||
OutlinedTextField(
|
||||
enabled = !(settings.isAutoTunnelEnabled || settings.isAlwaysOnVpnEnabled),
|
||||
value = currentText,
|
||||
onValueChange = { currentText = it },
|
||||
label = { Text(stringResource(R.string.add_trusted_ssid)) },
|
||||
modifier = Modifier
|
||||
.padding(start = screenPadding, top = 5.dp, bottom = 10.dp)
|
||||
.onFocusChanged {
|
||||
if (WireGuardAutoTunnel.isRunningOnAndroidTv(context)) {
|
||||
keyboardController?.hide()
|
||||
}
|
||||
},
|
||||
maxLines = 1,
|
||||
keyboardOptions = KeyboardOptions(
|
||||
capitalization = KeyboardCapitalization.None,
|
||||
imeAction = ImeAction.Done
|
||||
),
|
||||
keyboardActions = KeyboardActions(
|
||||
onDone = {
|
||||
saveTrustedSSID()
|
||||
}
|
||||
),
|
||||
trailingIcon = {
|
||||
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 = if (currentText == "") Color.Transparent else MaterialTheme.colorScheme.primary
|
||||
)
|
||||
}
|
||||
},
|
||||
text = ssid,
|
||||
icon = Icons.Filled.Close,
|
||||
enabled = !(settings.isAutoTunnelEnabled || settings.isAlwaysOnVpnEnabled)
|
||||
)
|
||||
}
|
||||
if(trustedSSIDs.isEmpty()) {
|
||||
Text(stringResource(R.string.none), fontStyle = FontStyle.Italic, color = Color.Gray)
|
||||
}
|
||||
}
|
||||
OutlinedTextField(
|
||||
enabled = !(settings.isAutoTunnelEnabled || settings.isAlwaysOnVpnEnabled),
|
||||
value = currentText,
|
||||
onValueChange = { currentText = it },
|
||||
label = { Text(stringResource(R.string.add_trusted_ssid)) },
|
||||
modifier = Modifier.padding(start = screenPadding, top = 5.dp).focusRequester(focusRequester).onFocusChanged {
|
||||
if(WireGuardAutoTunnel.isRunningOnAndroidTv(context)) {
|
||||
keyboardController?.hide()
|
||||
}
|
||||
},
|
||||
maxLines = 1,
|
||||
keyboardOptions = KeyboardOptions(
|
||||
capitalization = KeyboardCapitalization.None,
|
||||
imeAction = ImeAction.Done
|
||||
),
|
||||
keyboardActions = KeyboardActions(
|
||||
onDone = {
|
||||
saveTrustedSSID()
|
||||
}
|
||||
),
|
||||
trailingIcon = {
|
||||
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 = if (currentText == "") Color.Transparent else MaterialTheme.colorScheme.primary
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
ConfigurationToggle(stringResource(R.string.tunnel_mobile_data),
|
||||
enabled = !(settings.isAutoTunnelEnabled || settings.isAlwaysOnVpnEnabled),
|
||||
checked = settings.isTunnelOnMobileDataEnabled,
|
||||
@@ -315,27 +382,47 @@ fun SettingsScreen(
|
||||
}
|
||||
}
|
||||
)
|
||||
ConfigurationToggle(stringResource(R.string.enable_auto_tunnel),
|
||||
enabled = !settings.isAlwaysOnVpnEnabled,
|
||||
checked = settings.isAutoTunnelEnabled,
|
||||
ConfigurationToggle(
|
||||
stringResource(R.string.battery_saver),
|
||||
enabled = !(settings.isAutoTunnelEnabled || settings.isAlwaysOnVpnEnabled),
|
||||
checked = settings.isBatterySaverEnabled,
|
||||
padding = screenPadding,
|
||||
onCheckChanged = {
|
||||
if(!isAllAutoTunnelPermissionsEnabled()) {
|
||||
val message = if(viewModel.isLocationServicesNeeded()){
|
||||
"Location services required"
|
||||
} else if(!isBackgroundLocationGranted){
|
||||
"Background location required"
|
||||
} else {
|
||||
"Precise location required"
|
||||
}
|
||||
showSnackbarMessage(message)
|
||||
} else scope.launch {
|
||||
viewModel.toggleAutoTunnel()
|
||||
scope.launch {
|
||||
viewModel.onToggleBatterySaver()
|
||||
}
|
||||
}
|
||||
)
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(top = 5.dp),
|
||||
horizontalArrangement = Arrangement.Center
|
||||
) {
|
||||
TextButton(
|
||||
enabled = !settings.isAlwaysOnVpnEnabled,
|
||||
onClick = {
|
||||
//TODO fix logic for mobile only
|
||||
if(!isAllAutoTunnelPermissionsEnabled() && settings.isTunnelOnWifiEnabled) {
|
||||
val message = if(!isBackgroundLocationGranted) {
|
||||
context.getString(R.string.background_location_required)
|
||||
} else if(viewModel.isLocationServicesNeeded()) {
|
||||
context.getString(R.string.location_services_required)
|
||||
} else {
|
||||
context.getString(R.string.precise_location_required)
|
||||
}
|
||||
showSnackbarMessage(message)
|
||||
} else scope.launch {
|
||||
viewModel.toggleAutoTunnel()
|
||||
}
|
||||
}) {
|
||||
val autoTunnelButtonText = if(settings.isAutoTunnelEnabled) stringResource(R.string.disable_auto_tunnel)
|
||||
else stringResource(id = R.string.enable_auto_tunnel)
|
||||
Text(autoTunnelButtonText)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
if(!WireGuardAutoTunnel.isRunningOnAndroidTv(context)) {
|
||||
Surface(
|
||||
@@ -343,7 +430,8 @@ fun SettingsScreen(
|
||||
shadowElevation = 2.dp,
|
||||
shape = RoundedCornerShape(12.dp),
|
||||
color = MaterialTheme.colorScheme.surface,
|
||||
modifier = Modifier.fillMaxWidth(fillMaxWidth)
|
||||
modifier = Modifier
|
||||
.fillMaxWidth(fillMaxWidth)
|
||||
.height(IntrinsicSize.Min)
|
||||
.padding(bottom = 180.dp)
|
||||
) {
|
||||
@@ -363,6 +451,31 @@ fun SettingsScreen(
|
||||
}
|
||||
}
|
||||
)
|
||||
ConfigurationToggle(stringResource(R.string.enabled_app_shortcuts),
|
||||
enabled = true,
|
||||
checked = settings.isShortcutsEnabled,
|
||||
padding = screenPadding,
|
||||
onCheckChanged = {
|
||||
scope.launch {
|
||||
viewModel.onToggleShortcutsEnabled()
|
||||
}
|
||||
}
|
||||
)
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(top = 5.dp),
|
||||
horizontalArrangement = Arrangement.Center
|
||||
) {
|
||||
TextButton(
|
||||
enabled = !didExportFiles,
|
||||
onClick = {
|
||||
showAuthPrompt = true
|
||||
}) {
|
||||
Text(stringResource(R.string.export_configs))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+19
-1
@@ -84,7 +84,7 @@ class SettingsViewModel @Inject constructor(private val application : Applicatio
|
||||
}
|
||||
|
||||
private suspend fun getFirstTunnelConfig() : TunnelConfig {
|
||||
return tunnelRepo.getAll().first();
|
||||
return tunnelRepo.getAll().first()
|
||||
}
|
||||
|
||||
suspend fun onToggleAlwaysOnVPN() {
|
||||
@@ -125,4 +125,22 @@ class SettingsViewModel @Inject constructor(private val application : Applicatio
|
||||
fun isLocationServicesNeeded() : Boolean {
|
||||
return(!isLocationServicesEnabled() && Build.VERSION.SDK_INT > Build.VERSION_CODES.P)
|
||||
}
|
||||
|
||||
suspend fun onToggleShortcutsEnabled() {
|
||||
settingsRepo.save(_settings.value.copy(
|
||||
isShortcutsEnabled = !_settings.value.isShortcutsEnabled
|
||||
))
|
||||
}
|
||||
|
||||
suspend fun onToggleBatterySaver() {
|
||||
settingsRepo.save(_settings.value.copy(
|
||||
isBatterySaverEnabled = !_settings.value.isBatterySaverEnabled
|
||||
))
|
||||
}
|
||||
|
||||
suspend fun onToggleTunnelOnWifi() {
|
||||
settingsRepo.save(_settings.value.copy(
|
||||
isTunnelOnWifiEnabled = !_settings.value.isTunnelOnWifiEnabled
|
||||
))
|
||||
}
|
||||
}
|
||||
+86
-18
@@ -1,22 +1,34 @@
|
||||
package com.zaneschepke.wireguardautotunnel.ui.screens.support
|
||||
|
||||
import android.content.Intent
|
||||
import android.content.Intent.createChooser
|
||||
import android.net.Uri
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.focusable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.IntrinsicSize
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.rounded.ArrowForward
|
||||
import androidx.compose.material.icons.rounded.Book
|
||||
import androidx.compose.material.icons.rounded.Mail
|
||||
import androidx.compose.material3.Divider
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
@@ -31,18 +43,32 @@ import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextDecoration
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.core.content.ContextCompat.startActivity
|
||||
import com.zaneschepke.wireguardautotunnel.BuildConfig
|
||||
import com.zaneschepke.wireguardautotunnel.Constants
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
|
||||
|
||||
@Composable
|
||||
fun SupportScreen(padding : PaddingValues, focusRequester: FocusRequester) {
|
||||
|
||||
val context = LocalContext.current
|
||||
val fillMaxWidth = .85f
|
||||
|
||||
fun openWebPage(url: String) {
|
||||
val webpage: Uri = Uri.parse(url)
|
||||
val intent = Intent(Intent.ACTION_VIEW, webpage)
|
||||
context.startActivity(intent)
|
||||
}
|
||||
|
||||
fun launchEmail() {
|
||||
val intent = Intent(Intent.ACTION_SEND).apply {
|
||||
type = Constants.EMAIL_MIME_TYPE
|
||||
putExtra(Intent.EXTRA_EMAIL, context.getString(R.string.my_email))
|
||||
putExtra(Intent.EXTRA_SUBJECT, context.getString(R.string.email_subject))
|
||||
}
|
||||
startActivity(context,createChooser(intent, context.getString(R.string.email_chooser)),null)
|
||||
}
|
||||
|
||||
Column(horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Top,
|
||||
@@ -51,23 +77,65 @@ fun SupportScreen(padding : PaddingValues, focusRequester: FocusRequester) {
|
||||
.verticalScroll(rememberScrollState())
|
||||
.focusable()
|
||||
.padding(padding)) {
|
||||
Text(stringResource(R.string.support_text), textAlign = TextAlign.Center, modifier = Modifier.padding(30.dp), fontSize = 15.sp)
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(14.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceEvenly
|
||||
Surface(
|
||||
tonalElevation = 2.dp,
|
||||
shadowElevation = 2.dp,
|
||||
shape = RoundedCornerShape(12.dp),
|
||||
color = MaterialTheme.colorScheme.surface,
|
||||
modifier = (if (WireGuardAutoTunnel.isRunningOnAndroidTv(context))
|
||||
Modifier
|
||||
.height(IntrinsicSize.Min)
|
||||
.fillMaxWidth(fillMaxWidth)
|
||||
.padding(top = 10.dp)
|
||||
else Modifier
|
||||
.fillMaxWidth(fillMaxWidth)
|
||||
.padding(top = 20.dp)).padding(bottom = 25.dp)
|
||||
) {
|
||||
IconButton(onClick = {
|
||||
openWebPage(context.resources.getString(R.string.discord_url))
|
||||
}) {
|
||||
Icon(imageVector = ImageVector.vectorResource(R.drawable.discord), "Discord")
|
||||
}
|
||||
IconButton(modifier = Modifier.focusRequester(focusRequester),onClick = {
|
||||
openWebPage(context.resources.getString(R.string.github_url))
|
||||
}) {
|
||||
Icon(imageVector = ImageVector.vectorResource(R.drawable.github), "Github")
|
||||
Column(modifier = Modifier.padding(20.dp)) {
|
||||
Text(stringResource(R.string.thank_you), textAlign = TextAlign.Start, modifier = Modifier.padding(bottom = 20.dp), fontSize = 16.sp)
|
||||
Text(stringResource(id = R.string.support_help_text), textAlign = TextAlign.Start, fontSize = 16.sp, modifier = Modifier.padding(bottom = 20.dp))
|
||||
TextButton(onClick = { openWebPage(context.resources.getString(R.string.docs_url)) }, modifier = Modifier.padding(vertical = 5.dp).focusRequester(focusRequester)) {
|
||||
Row(horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth()) {
|
||||
Row {
|
||||
Icon(Icons.Rounded.Book, stringResource(id = R.string.docs))
|
||||
Text(stringResource(id = R.string.docs_description), textAlign = TextAlign.Justify, modifier = Modifier.padding(start = 10.dp))
|
||||
}
|
||||
Icon(Icons.Rounded.ArrowForward, stringResource(id = R.string.go))
|
||||
}
|
||||
}
|
||||
Divider(color = MaterialTheme.colorScheme.onBackground, thickness = 0.5.dp)
|
||||
TextButton(onClick = { openWebPage(context.resources.getString(R.string.discord_url)) }, modifier = Modifier.padding(vertical = 5.dp)) {
|
||||
Row(horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth()) {
|
||||
Row {
|
||||
Icon(imageVector = ImageVector.vectorResource(R.drawable.discord), stringResource(
|
||||
id = R.string.discord), Modifier.size(25.dp))
|
||||
Text(stringResource(id = R.string.discord_description), textAlign = TextAlign.Justify, modifier = Modifier.padding(start = 10.dp))
|
||||
}
|
||||
Icon(Icons.Rounded.ArrowForward, stringResource(id = R.string.go))
|
||||
}
|
||||
}
|
||||
Divider(color = MaterialTheme.colorScheme.onBackground, thickness = 0.5.dp)
|
||||
TextButton(onClick = { openWebPage(context.resources.getString(R.string.github_url)) }, modifier = Modifier.padding(vertical = 5.dp)) {
|
||||
Row(horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth()) {
|
||||
Row {
|
||||
Icon(imageVector = ImageVector.vectorResource(R.drawable.github), stringResource(
|
||||
id = R.string.github
|
||||
), Modifier.size(25.dp))
|
||||
Text("Open an issue", textAlign = TextAlign.Justify, modifier = Modifier.padding(start = 10.dp))
|
||||
}
|
||||
Icon(Icons.Rounded.ArrowForward, stringResource(id = R.string.go))
|
||||
}
|
||||
}
|
||||
Divider(color = MaterialTheme.colorScheme.onBackground, thickness = 0.5.dp)
|
||||
TextButton(onClick = { launchEmail() }, modifier = Modifier.padding(vertical = 5.dp)) {
|
||||
Row(horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth()) {
|
||||
Row {
|
||||
Icon(Icons.Rounded.Mail, stringResource(id = R.string.email))
|
||||
Text(stringResource(id = R.string.email_description), textAlign = TextAlign.Justify, modifier = Modifier.padding(start = 10.dp))
|
||||
}
|
||||
Icon(Icons.Rounded.ArrowForward, stringResource(id = R.string.go))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
@@ -75,6 +143,6 @@ fun SupportScreen(padding : PaddingValues, focusRequester: FocusRequester) {
|
||||
modifier = Modifier.clickable {
|
||||
openWebPage(context.resources.getString(R.string.privacy_policy_url))
|
||||
})
|
||||
Text("App version: ${com.zaneschepke.wireguardautotunnel.BuildConfig.VERSION_NAME}", Modifier.padding(25.dp))
|
||||
Text("App version: ${BuildConfig.VERSION_NAME}", Modifier.padding(25.dp))
|
||||
}
|
||||
}
|
||||
@@ -1,17 +1,18 @@
|
||||
package com.zaneschepke.wireguardautotunnel.util
|
||||
|
||||
import java.math.BigDecimal
|
||||
import java.text.DecimalFormat
|
||||
import java.time.Duration
|
||||
import java.time.Instant
|
||||
import kotlin.math.pow
|
||||
|
||||
object NumberUtils {
|
||||
|
||||
private const val BYTES_IN_KB = 1024L
|
||||
private const val BYTES_IN_KB = 1024.0
|
||||
private val BYTES_IN_MB = BYTES_IN_KB.pow(2.0)
|
||||
private val keyValidationRegex = """^[A-Za-z0-9+/]{42}[AEIMQUYcgkosw480]=${'$'}""".toRegex()
|
||||
|
||||
fun bytesToKB(bytes : Long) : BigDecimal {
|
||||
return bytes.toBigDecimal().divide(BYTES_IN_KB.toBigDecimal())
|
||||
fun bytesToMB(bytes : Long) : BigDecimal {
|
||||
return bytes.toBigDecimal().divide(BYTES_IN_MB.toBigDecimal())
|
||||
}
|
||||
|
||||
fun isValidKey(key : String) : Boolean {
|
||||
@@ -22,13 +23,12 @@ object NumberUtils {
|
||||
return "tunnel${(Math.random() * 100000).toInt()}"
|
||||
}
|
||||
|
||||
fun formatDecimalTwoPlaces(bigDecimal: BigDecimal) : String {
|
||||
val df = DecimalFormat("#.##")
|
||||
return df.format(bigDecimal)
|
||||
}
|
||||
|
||||
fun getSecondsBetweenTimestampAndNow(epoch : Long) : Long {
|
||||
val time = Instant.ofEpochMilli(epoch)
|
||||
return Duration.between(time, Instant.now()).seconds
|
||||
fun getSecondsBetweenTimestampAndNow(epoch : Long) : Long? {
|
||||
return if (epoch != 0L) {
|
||||
val time = Instant.ofEpochMilli(epoch)
|
||||
return Duration.between(time, Instant.now()).seconds
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package com.zaneschepke.wireguardautotunnel.util
|
||||
|
||||
import android.content.ContentValues
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import android.os.Environment
|
||||
import android.provider.MediaStore
|
||||
import android.provider.MediaStore.MediaColumns
|
||||
import com.zaneschepke.wireguardautotunnel.Constants
|
||||
import java.io.File
|
||||
import java.io.OutputStream
|
||||
import java.time.Instant
|
||||
import java.util.zip.ZipEntry
|
||||
import java.util.zip.ZipOutputStream
|
||||
|
||||
object StorageUtil {
|
||||
private const val ZIP_FILE_MIME_TYPE = "application/zip"
|
||||
private fun createDownloadsFileOutputStream(context: Context, fileName: String, mimeType : String = Constants.ALLOWED_FILE_TYPES) : OutputStream? {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
val resolver = context.contentResolver
|
||||
val contentValues = ContentValues().apply {
|
||||
put(MediaColumns.DISPLAY_NAME, fileName)
|
||||
put(MediaColumns.MIME_TYPE, mimeType)
|
||||
put(MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_DOWNLOADS)
|
||||
}
|
||||
val uri = resolver.insert(MediaStore.Downloads.EXTERNAL_CONTENT_URI, contentValues)
|
||||
if (uri != null) {
|
||||
|
||||
return resolver.openOutputStream(uri)
|
||||
}
|
||||
} else {
|
||||
val target = File(
|
||||
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS),
|
||||
fileName
|
||||
)
|
||||
return target.outputStream()
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
fun saveFilesToZip(context: Context, files : List<File>) {
|
||||
val zipOutputStream = createDownloadsFileOutputStream(context, "wg-export_${Instant.now().epochSecond}.zip", ZIP_FILE_MIME_TYPE)
|
||||
ZipOutputStream(zipOutputStream).use { zos ->
|
||||
files.forEach { file ->
|
||||
val entry = ZipEntry( file.name)
|
||||
zos.putNextEntry(entry)
|
||||
if (file.isFile) {
|
||||
file.inputStream().use { fis -> fis.copyTo(zos) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,15 @@
|
||||
package com.zaneschepke.wireguardautotunnel.util
|
||||
|
||||
class WgTunnelException(message: String) : Exception(message)
|
||||
import com.wireguard.config.BadConfigException
|
||||
|
||||
class WgTunnelException(e: Exception) : Exception() {
|
||||
constructor(message : String) : this(Exception(message))
|
||||
|
||||
override val message: String = generateExceptionMessage(e)
|
||||
private fun generateExceptionMessage(e : Exception) : String {
|
||||
return when(e) {
|
||||
is BadConfigException -> "${e.section.name} ${e.location.name} ${e.reason.name}"
|
||||
else -> e.message ?: "Unknown error occurred"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,9 +6,10 @@
|
||||
<string name="watcher_channel_id">Watcher Channel</string>
|
||||
<string name="watcher_channel_name">Watcher Notification Channel</string>
|
||||
<string name="foreground_file">FOREGROUND_FILE</string>
|
||||
<string name="github_url">https://github.com/zaneschepke/wgtunnel</string>
|
||||
<string name="privacy_policy_url">https://zaneschepke.github.io/wgtunnel/</string>
|
||||
<string name="file_extension_message">File is not a .conf file</string>
|
||||
<string name="github_url">https://github.com/zaneschepke/wgtunnel/issues</string>
|
||||
<string name="docs_url">https://zaneschepke.com/wgtunnel-docs/overview.html</string>
|
||||
<string name="privacy_policy_url">https://zaneschepke.com/wgtunnel-docs/privacypolicy.html</string>
|
||||
<string name="file_extension_message">File is not a .conf or .zip</string>
|
||||
<string name="turn_off_tunnel">Turn off tunnel before editing</string>
|
||||
<string name="no_tunnels">No tunnels added yet!</string>
|
||||
<string name="tunnel_exists">Tunnel name already exists</string>
|
||||
@@ -20,10 +21,10 @@
|
||||
<string name="vpn_permission_required">VPN permission is required for the app to work properly. If this permission is not launching, please disable \"Always-on VPN\" in your phone settings for the official WireGuard mobile app and try again.</string>
|
||||
<string name="notification_permission_required">Notifications permission is required for the app to work properly.</string>
|
||||
<string name="open_settings">Open Settings</string>
|
||||
<string name="add_trusted_ssid">Add Trusted SSID</string>
|
||||
<string name="trusted_ssid">Trusted SSIDs</string>
|
||||
<string name="add_trusted_ssid">Add trusted wifi name</string>
|
||||
<string name="tunnels">Tunnels</string>
|
||||
<string name="enable_auto_tunnel">Enable auto-tunneling</string>
|
||||
<string name="enable_auto_tunnel">Start auto-tunneling</string>
|
||||
<string name="disable_auto_tunnel">Stop auto-tunneling</string>
|
||||
<string name="tunnel_mobile_data">Tunnel on mobile data</string>
|
||||
<string name="background_location_reason">\"Allow all the time\" location permission is required for retrieving Wi-Fi SSID in the background. Permission is needed for this feature.</string>
|
||||
<string name="location_permission_reason">Location permission is required for this feature to work properly.</string>
|
||||
@@ -34,13 +35,13 @@
|
||||
<string name="tunnel_on_ethernet">Tunnel on ethernet</string>
|
||||
<string name="prominent_background_location_message">This feature requires background location permission to enable Wi-Fi SSID monitoring even while the application is closed. For more details, please see the Privacy Policy linked on the Support screen.</string>
|
||||
<string name="prominent_background_location_title">Background Location Disclosure</string>
|
||||
<string name="support_text">Thank you for using WG Tunnel! If you are experiencing issues with the app, please reach out on Discord or create an issue on Github. I will try to address the issue as quickly as possible. Thank you!</string>
|
||||
<string name="thank_you">Thank you for using WG Tunnel!</string>
|
||||
<string name="trusted_ssid_empty_description">Enter SSID</string>
|
||||
<string name="trusted_ssid_value_description">Submit SSID</string>
|
||||
<string name="config_validation">[Interface]</string>
|
||||
<string name="add_from_file">Add tunnel from files</string>
|
||||
<string name="add_tunnels_text">Add from file or zip</string>
|
||||
<string name="open_file">File Open</string>
|
||||
<string name="add_from_qr">Add tunnel from QR code</string>
|
||||
<string name="add_from_qr">Add from QR code</string>
|
||||
<string name="qr_scan">QR Scan</string>
|
||||
<string name="tunnel_edit">Tunnel Edit</string>
|
||||
<string name="tunnel_name">Tunnel Name</string>
|
||||
@@ -92,7 +93,7 @@
|
||||
<string name="db_name">wg-tunnel-db</string>
|
||||
<string name="scanning_qr">Scanning for QR</string>
|
||||
<string name="qr_result_failed">QR scan failed</string>
|
||||
<string name="none">None</string>
|
||||
<string name="none">No trusted wifi names</string>
|
||||
<string name="never">Never</string>
|
||||
<string name="stream_failed">Failed to open file stream.</string>
|
||||
<string name="unknown_error_message">An unknown error occurred.</string>
|
||||
@@ -126,5 +127,30 @@
|
||||
<string name="persistent_keepalive">Persistent keepalive</string>
|
||||
<string name="cancel">Cancel</string>
|
||||
<string name="primary_tunnel_change">Primary tunnel change</string>
|
||||
<string name="primary_tunnnel_change_question">Would you like to make this your primary tunnel?</string>
|
||||
<string name="primary_tunnel_change_question">Would you like to make this your primary tunnel?</string>
|
||||
<string name="authentication_failed">Authentication failed</string>
|
||||
<string name="enabled_app_shortcuts">Enable app shortcuts</string>
|
||||
<string name="export_configs">Export configs</string>
|
||||
<string name="battery_saver">Battery saver (beta)</string>
|
||||
<string name="location_services_required">Location services required</string>
|
||||
<string name="background_location_required">Background location required</string>
|
||||
<string name="precise_location_required">Precise location required</string>
|
||||
<string name="unknown_error">Unknown error occurred</string>
|
||||
<string name="exported_configs_message">Exported configs to downloads</string>
|
||||
<string name="no_file_explorer">No file explorer installed</string>
|
||||
<string name="status">status</string>
|
||||
<string name="tunnel_on_wifi">Tunnel on untrusted wifi</string>
|
||||
<string name="my_email">zanecschepke@gmail.com</string>
|
||||
<string name="email_subject">WG Tunnel Support</string>
|
||||
<string name="email_chooser">Send an email…</string>
|
||||
<string name="go">go</string>
|
||||
<string name="docs_description">Read the docs (WIP)</string>
|
||||
<string name="discord_description">Join the community</string>
|
||||
<string name="discord">Discord</string>
|
||||
<string name="docs">Docs</string>
|
||||
<string name="github">GitHub</string>
|
||||
<string name="email">Email</string>
|
||||
<string name="email_description">Send me an email</string>
|
||||
<string name="support_help_text">If you are experiencing issues, have improvement ideas, or just want to engage, the following resources are available:</string>
|
||||
|
||||
</resources>
|
||||
@@ -1,9 +1,8 @@
|
||||
package com.zaneschepke.wireguardautotunnel
|
||||
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Test
|
||||
|
||||
import org.junit.Assert.*
|
||||
|
||||
/**
|
||||
* Example local unit test, which will execute on the development machine (host).
|
||||
*
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 99 KiB After Width: | Height: | Size: 125 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 89 KiB After Width: | Height: | Size: 99 KiB |
@@ -7,8 +7,6 @@ buildscript {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
plugins {
|
||||
alias(libs.plugins.android.application) apply false
|
||||
alias(libs.plugins.kotlin.android) apply false
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
object Constants {
|
||||
const val VERSION_NAME = "3.2.3"
|
||||
const val JVM_TARGET = "17"
|
||||
const val VERSION_CODE = 32300
|
||||
const val TARGET_SDK = 34
|
||||
const val MIN_SDK = 26
|
||||
const val APP_ID = "com.zaneschepke.wireguardautotunnel"
|
||||
const val APP_NAME = "wgtunnel"
|
||||
|
||||
const val STORE_PASS_VAR = "SIGNING_STORE_PASSWORD"
|
||||
const val KEY_ALIAS_VAR = "SIGNING_KEY_ALIAS"
|
||||
const val KEY_PASS_VAR = "SIGNING_KEY_PASSWORD"
|
||||
const val KEY_STORE_PATH_VAR = "KEY_STORE_PATH"
|
||||
|
||||
const val RELEASE = "release"
|
||||
const val TYPE = "type"
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
json_key_file "service_account.json"
|
||||
package_name "com.zaneschepke.wireguardautotunnel"
|
||||
@@ -0,0 +1,17 @@
|
||||
default_platform(:android)
|
||||
|
||||
platform :android do
|
||||
|
||||
desc "Deploy a beta version to the Google Play"
|
||||
lane :beta do
|
||||
gradle(task: "clean bundleGeneralRelease")
|
||||
upload_to_play_store(track: 'beta')
|
||||
end
|
||||
|
||||
desc "Deploy a new version to the Google Play"
|
||||
lane :production do
|
||||
gradle(task: "clean bundleGeneralRelease")
|
||||
upload_to_play_store
|
||||
end
|
||||
|
||||
end
|
||||
@@ -0,0 +1,3 @@
|
||||
Enhancements:
|
||||
- Fix < Android 9 permission bug
|
||||
- Other optimizations
|
||||
@@ -0,0 +1,5 @@
|
||||
Enhancements:
|
||||
- Add tunnel statistics to main screen
|
||||
- Improve settings screen AndroidTV navigation
|
||||
- Remove notification vibration
|
||||
- Various other bug fixes
|
||||
@@ -0,0 +1,5 @@
|
||||
Enhancements:
|
||||
- Add support for mobile data only auto-tunneling
|
||||
- Improve support screen UI
|
||||
- Update resource links
|
||||
- Various other bug fixes
|
||||
@@ -1,10 +1,13 @@
|
||||
Features
|
||||
|
||||
- Add tunnels via .conf file or QR
|
||||
- Add tunnels via .conf file, zip, manual entry, or QR code
|
||||
- Auto connect to VPN based on Wi-Fi SSID
|
||||
- Split tunneling by application with search
|
||||
- Always-on VPN support
|
||||
- Configurable Trusted Network list
|
||||
- Quick tile and Shortcuts integration
|
||||
- Optional auto connect on mobile data
|
||||
- Automatic service restart after reboot
|
||||
- Always-on VPN for Android support
|
||||
- Export tunnels to zip
|
||||
- Quick tile support for VPN toggling
|
||||
- Static shortcuts support for primary tunnel for automation integration
|
||||
- Intent automation support for all tunnels
|
||||
- Optional auto connect on mobile data, ethernet
|
||||
- Automatic service restart after reboot
|
||||
- Service will stay running in background after app has been closed
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 99 KiB After Width: | Height: | Size: 125 KiB |
+23
-16
@@ -1,45 +1,51 @@
|
||||
[versions]
|
||||
accompanist = "0.31.2-alpha"
|
||||
activityCompose = "1.8.0"
|
||||
accompanist = "0.32.0"
|
||||
activityCompose = "1.8.1"
|
||||
androidx-junit = "1.1.5"
|
||||
appcompat = "1.6.1"
|
||||
biometricKtx = "1.2.0-alpha05"
|
||||
coreGoogleShortcuts = "1.1.0"
|
||||
coreKtx = "1.12.0"
|
||||
desugar_jdk_libs = "2.0.4"
|
||||
espressoCore = "3.5.1"
|
||||
firebase-crashlytics-gradle = "2.9.9"
|
||||
google-services = "4.4.0"
|
||||
hiltAndroid = "2.48"
|
||||
hiltNavigationCompose = "1.0.0"
|
||||
hiltAndroid = "2.49"
|
||||
hiltNavigationCompose = "1.1.0"
|
||||
junit = "4.13.2"
|
||||
kotlinx-serialization-json = "1.5.1"
|
||||
kotlinx-serialization-json = "1.6.2"
|
||||
lifecycle-runtime-compose = "2.6.2"
|
||||
material-icons-extended = "1.5.3"
|
||||
material-icons-extended = "1.5.4"
|
||||
material3 = "1.1.2"
|
||||
navigationCompose = "2.7.4"
|
||||
roomVersion = "2.6.0-rc01"
|
||||
navigationCompose = "2.7.5"
|
||||
roomVersion = "2.6.1"
|
||||
timber = "5.0.1"
|
||||
tunnel = "1.0.20230706"
|
||||
androidGradlePlugin = "8.3.0-alpha06"
|
||||
androidGradlePlugin = "8.2.0"
|
||||
kotlin="1.9.10"
|
||||
ksp="1.9.10-1.0.13"
|
||||
composeBom="2023.10.00"
|
||||
firebaseBom="32.3.1"
|
||||
compose="1.5.3"
|
||||
crashlytics="18.4.3"
|
||||
analytics="21.3.0"
|
||||
composeBom="2023.10.01"
|
||||
firebaseBom= "32.6.0"
|
||||
compose="1.5.4"
|
||||
crashlytics= "18.6.0"
|
||||
analytics="21.5.0"
|
||||
composeCompiler="1.5.3"
|
||||
zxingAndroidEmbedded = "4.3.0"
|
||||
zxingCore = "3.4.1"
|
||||
zxingCore = "3.5.2"
|
||||
|
||||
|
||||
[libraries]
|
||||
# accompanist
|
||||
accompanist-drawablepainter = { module = "com.google.accompanist:accompanist-drawablepainter", version.ref = "accompanist" }
|
||||
accompanist-flowlayout = { module = "com.google.accompanist:accompanist-flowlayout", version.ref = "accompanist" }
|
||||
accompanist-navigation-animation = { module = "com.google.accompanist:accompanist-navigation-animation", version.ref = "accompanist" }
|
||||
accompanist-permissions = { module = "com.google.accompanist:accompanist-permissions", version.ref = "accompanist" }
|
||||
accompanist-systemuicontroller = { module = "com.google.accompanist:accompanist-systemuicontroller", version.ref = "accompanist" }
|
||||
|
||||
#room
|
||||
androidx-biometric-ktx = { module = "androidx.biometric:biometric-ktx", version.ref = "biometricKtx" }
|
||||
androidx-core = { module = "androidx.core:core", version.ref = "coreKtx" }
|
||||
androidx-core-google-shortcuts = { module = "androidx.core:core-google-shortcuts", version.ref = "coreGoogleShortcuts" }
|
||||
androidx-lifecycle-process = { module = "androidx.lifecycle:lifecycle-process", version.ref = "lifecycle-runtime-compose" }
|
||||
androidx-lifecycle-service = { module = "androidx.lifecycle:lifecycle-service", version.ref = "lifecycle-runtime-compose" }
|
||||
androidx-room-compiler = { module = "androidx.room:room-compiler", version.ref = "roomVersion" }
|
||||
androidx-room-ktx = { module = "androidx.room:room-ktx", version.ref = "roomVersion" }
|
||||
@@ -55,6 +61,7 @@ androidx-compose-ui-tooling-preview = { module="androidx.compose.ui:ui-tooling-p
|
||||
androidx-compose-ui = { module="androidx.compose.ui:ui", version.ref="compose" }
|
||||
|
||||
#hilt
|
||||
desugar_jdk_libs = { module = "com.android.tools:desugar_jdk_libs", version.ref = "desugar_jdk_libs" }
|
||||
hilt-android = { module = "com.google.dagger:hilt-android", version.ref = "hiltAndroid" }
|
||||
hilt-android-compiler = { module = "com.google.dagger:hilt-android-compiler", version.ref = "hiltAndroid" }
|
||||
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
#Wed Oct 11 22:39:21 EDT 2023
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.3-bin.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
|
||||
Reference in New Issue
Block a user