Compare commits

..

1 Commits

Author SHA1 Message Date
Zane Schepke e395740c71 fix: create config not saving
Fixes bug where creating a config from scratch was failing to save

Closes #93 #96 #89
2024-01-19 20:52:11 -05:00
311 changed files with 2940 additions and 8961 deletions
-22
View File
@@ -1,22 +0,0 @@
# Contributor Code of Conduct
## Pledge
We as individuals involved in this project, pledge to participate in this
community in a respectful, constructive, and civil manner as we work towards a common goal
of delivering free, open source, and value adding software for all.
## Standard
The standard for this community is the Golden Rule.
> “Do unto others as you would have them do unto you.”
## Scope
This Code of Conduct applies to all spaces related to WG Tunnel.
## Incidents or Concerns
For any incidents or concerns, reach out to Zane at
<support@zaneschepke.com>.
-2
View File
@@ -1,2 +0,0 @@
ko_fi: zaneschepke
liberapay: zaneschepke
+87
View File
@@ -0,0 +1,87 @@
# name of the workflow
name: Android CI Tag Deployment
on:
push:
tags:
- '*.*.*'
jobs:
build:
name: Build Signed APK
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@v4
- name: Set up JDK 17
uses: actions/setup-java@v4
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@v4.0.0
with:
name: wgtunnel
path: ${{ steps.apk-path.outputs.path }}
- name: Download APK from build
uses: actions/download-artifact@v4
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/33400.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)
-20
View File
@@ -1,20 +0,0 @@
name: Issue Updates Workflow
on:
issues:
types: [ opened, closed, reopened ]
jobs:
build:
name: Build
runs-on: ubuntu-latest
steps:
- name: Send Telegram Message
run: |
msg_text='${{ github.actor }} updated an issue:
status: ${{ github.event.issue.state }} - #${{ github.event.issue.number }} ${{ github.event.issue.title }}
https://github.com/zaneschepke/wgtunnel/issues/${{ github.event.issue.number }}'
curl -s -X POST 'https://api.telegram.org/bot${{ secrets.TELEGRAM_TOKEN }}/sendMessage' \
-d "chat_id=${{ secrets.TELEGRAM_TO }}&text=${msg_text}&message_thread_id=${{ secrets.TELEGRAM_TOPIC }}"
-21
View File
@@ -1,21 +0,0 @@
name: Release Updates Workflow
on:
release:
types: [ published ]
jobs:
build:
name: Build
runs-on: ubuntu-latest
steps:
- name: Send Telegram Message
run: |
msg_text='${{ github.actor }} published a new release:
Release: ${{ github.event.release.tag_name }}
${{ github.event.release.body }}
https://github.com/zaneschepke/wgtunnel/releases/tag/${{ github.event.release.tag_name }}'
curl -s -X POST 'https://api.telegram.org/bot${{ secrets.TELEGRAM_TOKEN }}/sendMessage' \
-d "chat_id=${{ secrets.TELEGRAM_TO }}&text=${msg_text}&message_thread_id=${{ secrets.TELEGRAM_TOPIC }}"
-211
View File
@@ -1,211 +0,0 @@
name: release-android
on:
schedule:
- cron: "4 3 * * *"
workflow_dispatch:
inputs:
track:
type: choice
description: "Google play release track"
options:
- none
- internal
- alpha
- beta
- production
default: alpha
required: true
release_type:
type: choice
description: "GitHub release type"
options:
- none
- prerelease
- nightly
- release
default: release
required: true
tag_name:
description: "Tag name for release"
required: false
default: nightly
jobs:
build:
name: Build Signed APK
if: ${{ inputs.release_type != 'none' }}
runs-on: ubuntu-latest
env:
SIGNING_KEY_ALIAS: ${{ secrets.SIGNING_KEY_ALIAS }}
SIGNING_KEY_PASSWORD: ${{ secrets.SIGNING_KEY_PASSWORD }}
SIGNING_STORE_PASSWORD: ${{ secrets.SIGNING_STORE_PASSWORD }}
KEY_STORE_FILE: 'android_keystore.jks'
KEY_STORE_LOCATION: ${{ github.workspace }}/app/keystore/
GH_USER: ${{ secrets.GH_USER }}
# GH needed for gh cli
GH_TOKEN: ${{ secrets.GH_TOKEN }}
GH_REPO: ${{ github.repository }}
steps:
- uses: actions/checkout@v4
- name: Set up JDK 17
uses: actions/setup-java@v4
with:
distribution: 'temurin'
java-version: '17'
cache: gradle
- name: Grant execute permission for gradlew
run: chmod +x gradlew
- name: Install system dependencies
run: |
sudo apt update && sudo apt install -y gh apksigner
# 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: ${{ env.KEY_STORE_FILE }}
fileDir: ${{ env.KEY_STORE_LOCATION }}
encodedString: ${{ secrets.KEYSTORE }}
# create keystore path for gradle to read
- name: Create keystore path env var
run: |
store_path=${{ env.KEY_STORE_LOCATION }}${{ env.KEY_STORE_FILE }}
echo "KEY_STORE_PATH=$store_path" >> $GITHUB_ENV
- 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
if: ${{ inputs.release_type != '' && inputs.release_type != 'nightly' }}
run: ./gradlew :app:assembleFdroidRelease -x test
- name: Build Fdroid Nightly APK
if: ${{ inputs.release_type == '' || inputs.release_type == 'nightly' }}
run: ./gradlew :app:assembleFdroidNightly -x test
- if: ${{ inputs.release_type == '' || inputs.release_type == 'nightly' }}
run: echo "APK_PATH=$(find . -regex '^.*/build/outputs/apk/fdroid/nightly/.*\.apk$' -type f | head -1)" >> $GITHUB_ENV
- if: ${{ inputs.release_type != '' && inputs.release_type != 'nightly' }}
run: echo "APK_PATH=$(find . -regex '^.*/build/outputs/apk/fdroid/release/.*\.apk$' -type f | head -1)" >> $GITHUB_ENV
- name: Get version code
if: ${{ inputs.release_type == 'release' || inputs.release_type == 'prerelease' }}
run: |
version_code=$(grep "VERSION_CODE" buildSrc/src/main/kotlin/Constants.kt | awk '{print $5}' | tr -d '\n')
echo "VERSION_CODE=$version_code" >> $GITHUB_ENV
# Save the APK after the Build job is complete to publish it as a Github release in the next job
- name: Upload APK
uses: actions/upload-artifact@v4.3.4
with:
name: wgtunnel
path: ${{ env.APK_PATH }}
- name: Download APK from build
uses: actions/download-artifact@v4
with:
name: wgtunnel
- name: Repository Dispatch for my F-Droid repo
uses: peter-evans/repository-dispatch@v3
if: ${{ inputs.release_type == 'release' }}
with:
token: ${{ secrets.PAT }}
repository: zaneschepke/fdroid
event-type: fdroid-update
- name: Set version release notes
if: ${{ inputs.release_type == 'release' || inputs.release_type == 'prerelease' }}
run: |
RELEASE_NOTES="$(cat ${{ github.workspace }}/fastlane/metadata/android/en-US/changelogs/${{ env.VERSION_CODE }}.txt)"
echo "RELEASE_NOTES<<EOF" >> $GITHUB_ENV
echo "$RELEASE_NOTES" >> $GITHUB_ENV
echo "EOF" >> $GITHUB_ENV
- name: On nightly release
if: ${{ contains(env.TAG_NAME, 'nightly') }}
run: |
echo "RELEASE_NOTES=Nightly build for the latest development version of the app." >> $GITHUB_ENV
gh release delete nightly --yes || true
# Setup TAG_NAME, which is used as a general "name"
- if: github.event_name == 'workflow_dispatch'
run: echo "TAG_NAME=${{ github.event.inputs.tag_name }}" >> $GITHUB_ENV
- if: github.event_name == 'schedule'
run: echo "TAG_NAME=nightly" >> $GITHUB_ENV
- name: On nightly release
if: ${{ contains(env.TAG_NAME, 'nightly') }}
run: |
echo "RELEASE_NOTES=Nightly build of the latest development version of the android client." >> $GITHUB_ENV
gh release delete nightly --yes || true
- name: Get checksum
id: checksum
run: echo "checksum=$(apksigner verify -print-certs ${{ env.APK_PATH }} | grep -Po "(?<=SHA-256 digest:) .*" | tr -d "[:blank:]")" >> $GITHUB_OUTPUT
- name: Create Release with Fastlane changelog notes
id: create_release
uses: softprops/action-gh-release@v2
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
body: |
${{ env.RELEASE_NOTES }}
SHA256 fingerprint:
```${{ steps.checksum.outputs.checksum }}```
tag_name: ${{ env.TAG_NAME }}
name: ${{ env.TAG_NAME }}
draft: false
prerelease: ${{ inputs.release_type == 'prerelease' || inputs.release_type == '' || inputs.release_type == 'nightly' }}
make_latest: ${{ inputs.release_type == 'release' }}
files: ${{ github.workspace }}/${{ env.APK_PATH }}
publish-play:
if: ${{ inputs.track != 'none' && inputs.track != '' }}
name: Publish to Google Play
runs-on: ubuntu-latest
env:
SIGNING_KEY_ALIAS: ${{ secrets.SIGNING_KEY_ALIAS }}
SIGNING_KEY_PASSWORD: ${{ secrets.SIGNING_KEY_PASSWORD }}
SIGNING_STORE_PASSWORD: ${{ secrets.SIGNING_STORE_PASSWORD }}
KEY_STORE_FILE: 'android_keystore.jks'
KEY_STORE_LOCATION: ${{ github.workspace }}/app/keystore/
GH_USER: ${{ secrets.GH_USER }}
GH_TOKEN: ${{ secrets.GH_TOKEN }}
steps:
- uses: actions/checkout@v4
- name: Set up JDK 17
uses: actions/setup-java@v4
with:
distribution: 'temurin'
java-version: '17'
cache: gradle
- name: Grant execute permission for gradlew
run: chmod +x gradlew
- 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 Prod track 🚀
run: (cd ${{ github.workspace }} && bundle install && bundle exec fastlane ${{ inputs.track }})
+18 -44
View File
@@ -4,9 +4,8 @@ WG Tunnel
<div align="center"> <div align="center">
[![Discord](https://img.shields.io/badge/Discord-%235865F2.svg?style=for-the-badge&logo=discord&logoColor=white)](https://discord.gg/rbRRNh6H7V) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
[![X Community](https://img.shields.io/badge/X-000000?style=for-the-badge&logo=x&logoColor=white)](https://twitter.com/i/communities/1780655267685736818) [![Discord Chat](https://img.shields.io/discord/1108285024631001111.svg)](https://discord.gg/rbRRNh6H7V)
[![Telegram](https://img.shields.io/badge/Telegram-2CA5E0?style=for-the-badge&logo=telegram&logoColor=white)](https://t.me/wgtunnel)
</div> </div>
@@ -14,16 +13,22 @@ WG Tunnel
[![Google Play](https://img.shields.io/badge/Google_Play-414141?style=for-the-badge&logo=google-play&logoColor=white)](https://play.google.com/store/apps/details?id=com.zaneschepke.wireguardautotunnel) [![Google Play](https://img.shields.io/badge/Google_Play-414141?style=for-the-badge&logo=google-play&logoColor=white)](https://play.google.com/store/apps/details?id=com.zaneschepke.wireguardautotunnel)
[![Fire TV](https://img.shields.io/badge/fire%20tv-fc3b2d?style=for-the-badge&logo=amazon%20fire%20tv&logoColor=white)](https://www.amazon.com/gp/product/B0CFGGL7WK)
[![F-Droid](https://img.shields.io/static/v1?style=for-the-badge&message=F-Droid&color=1976D2&logo=F-Droid&logoColor=FFFFFF&label=)](https://f-droid.org/packages/com.zaneschepke.wireguardautotunnel/) [![F-Droid](https://img.shields.io/static/v1?style=for-the-badge&message=F-Droid&color=1976D2&logo=F-Droid&logoColor=FFFFFF&label=)](https://f-droid.org/packages/com.zaneschepke.wireguardautotunnel/)
</div>
<div align="center">
[![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/N4N8NMJN2)
</div> </div>
<div align="left"> <div align="left">
This is an alternative Android Application for [WireGuard](https://www.wireguard.com/) This is an alternative Android Application for [WireGuard](https://www.wireguard.com/) with added
and [AmneziaWG](https://docs.amnezia.org/documentation/amnezia-wg/) with added
features. Built using the [wireguard-android](https://github.com/WireGuard/wireguard-android) features. Built using the [wireguard-android](https://github.com/WireGuard/wireguard-android)
library and [Jetpack Compose](https://developer.android.com/jetpack/compose), this application was library and [Jetpack Compose](https://developer.android.com/jetpack/compose), this application was
inspired by the official [WireGuard Android](https://github.com/WireGuard/wireguard-android) app. inspired by the official [WireGuard Android](https://github.com/WireGuard/wireguard-android) app.
@@ -51,60 +56,29 @@ and on while on different networks. This app was created to offer a free solutio
## Features ## Features
* Add tunnels via .conf file, zip, manual entry, or QR code * Add tunnels via .conf file, zip, manual entry, or QR code
* Auto connect to tunnels based on Wi-Fi SSID, ethernet, or mobile data * Auto connect to VPN based on Wi-Fi SSID, ethernet, or mobile data
* Split tunneling by application with search * Split tunneling by application with search
* WireGuard support for kernel and userspace modes * WireGuard support for kernel and userspace modes
* Amnezia support for userspace mode for DPI/censorship protection
* Always-On VPN support * Always-On VPN support
* Export Amnezia and WireGuard tunnels to zip * Export tunnels to zip
* Quick tile support for tunnel toggling, auto-tunneling * Quick tile support for VPN toggling
* Static shortcuts support for tunnel toggling, auto-tunneling * Static shortcuts support for primary tunnel for automation integration
* Intent automation support for all tunnels * Intent automation support for all tunnels
* Automatic auto-tunneling service restart after reboot * Automatic service restart after reboot
* Automatic tunnel restart after reboot
* Battery preservation measures * Battery preservation measures
* Restart tunnel on ping failure (beta)
## Fdroid ## Docs (WIP)
Want updates faster? Basic documentation of the feature and behaviors of this app can be found [here](https://zaneschepke.com/wgtunnel-docs/overview.html).
Check out my personal [fdroid repository](https://github.com/zaneschepke/fdroid) to get updates the
moment they are released.
## Docs
Information about features, behaviors, and answers to common questions can be found in the
app [documentation](https://zaneschepke.com/wgtunnel-docs/overview.html).
The repository for these docs can be found [here](https://github.com/zaneschepke/wgtunnel-docs). The repository for these docs can be found [here](https://github.com/zaneschepke/wgtunnel-docs).
## Translation
This app is using [Weblate](https://weblate.org) to assist with translations.
Help translate WG Tunnel into your language
at [Hosted Weblate](https://hosted.weblate.org/engage/wg-tunnel/).\
[![Translation status](https://hosted.weblate.org/widgets/wg-tunnel/-/multi-auto.svg)](https://hosted.weblate.org/engage/wg-tunnel/)
## Building ## Building
``` ```
$ git clone https://github.com/zaneschepke/wgtunnel $ git clone https://github.com/zaneschepke/wgtunnel
$ cd wgtunnel $ cd wgtunnel
```
And then build the app:
```
$ ./gradlew assembleDebug $ ./gradlew assembleDebug
``` ```
## Contributing </span>
Any contributions in the form of feedback, issues, code, or translations are welcome and much
appreciated!
Please read
the [code of conduct](https://github.com/zaneschepke/wgtunnel?tab=coc-ov-file#contributor-code-of-conduct)
before contributing.
-5
View File
@@ -1,5 +0,0 @@
# Security Policy
## Reporting a Vulnerability
Please report security issues to `support@zaneschepke.com`
+57 -40
View File
@@ -1,21 +1,16 @@
import java.util.Properties
plugins { plugins {
alias(libs.plugins.android.application) alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.android) alias(libs.plugins.kotlin.android)
alias(libs.plugins.hilt.android) alias(libs.plugins.hilt.android)
alias(libs.plugins.kotlinxSerialization) id("org.jetbrains.kotlin.plugin.serialization")
alias(libs.plugins.ksp) alias(libs.plugins.ksp)
alias(libs.plugins.compose.compiler)
alias(libs.plugins.grgit)
} }
android { android {
namespace = Constants.APP_ID namespace = Constants.APP_ID
compileSdk = Constants.TARGET_SDK compileSdk = Constants.TARGET_SDK
compileSdkPreview = "VanillaIceCream"
androidResources {
generateLocaleConfig = true
}
defaultConfig { defaultConfig {
applicationId = Constants.APP_ID applicationId = Constants.APP_ID
@@ -30,16 +25,52 @@ android {
getByName("debug").assets.srcDirs(files("$projectDir/schemas")) // Room getByName("debug").assets.srcDirs(files("$projectDir/schemas")) // Room
} }
resourceConfigurations.addAll(listOf("en"))
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables { useSupportLibrary = true } vectorDrawables { useSupportLibrary = true }
} }
signingConfigs { signingConfigs {
create(Constants.RELEASE) { create(Constants.RELEASE) {
storeFile = getStoreFile() val properties =
storePassword = getSigningProperty(Constants.STORE_PASS_VAR) Properties().apply {
keyAlias = getSigningProperty(Constants.KEY_ALIAS_VAR) // created local file for signing details
keyPassword = getSigningProperty(Constants.KEY_PASS_VAR) 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),
)
} }
} }
@@ -70,12 +101,6 @@ android {
signingConfig = signingConfigs.getByName(Constants.RELEASE) signingConfig = signingConfigs.getByName(Constants.RELEASE)
} }
debug { isDebuggable = true } debug { isDebuggable = true }
create(Constants.NIGHTLY) {
initWith(getByName("release"))
defaultConfig.versionName = nightlyVersionName()
defaultConfig.versionCode = nightlyVersionCode()
}
} }
flavorDimensions.add(Constants.TYPE) flavorDimensions.add(Constants.TYPE)
productFlavors { productFlavors {
@@ -85,6 +110,10 @@ android {
} }
create("general") { create("general") {
dimension = Constants.TYPE dimension = Constants.TYPE
if (BuildHelper.isReleaseBuild(gradle) && BuildHelper.isGeneralFlavor(gradle)) {
apply(plugin = "com.google.gms.google-services")
apply(plugin = "com.google.firebase.crashlytics")
}
} }
} }
compileOptions { compileOptions {
@@ -97,19 +126,16 @@ android {
compose = true compose = true
buildConfig = true buildConfig = true
} }
composeOptions { kotlinCompilerExtensionVersion = Constants.COMPOSE_COMPILER_EXTENSION_VERSION }
packaging { resources { excludes += "/META-INF/{AL2.0,LGPL2.1}" } } packaging { resources { excludes += "/META-INF/{AL2.0,LGPL2.1}" } }
} }
val generalImplementation by configurations val generalImplementation by configurations
dependencies { dependencies {
implementation(project(":logcatter"))
implementation(libs.androidx.core.ktx) implementation(libs.androidx.core.ktx)
implementation(libs.androidx.lifecycle.runtime.ktx) implementation(libs.androidx.lifecycle.runtime.ktx)
// optional - helpers for implementing LifecycleOwner in a Service
// helpers for implementing LifecycleOwner in a Service
implementation(libs.androidx.lifecycle.service) implementation(libs.androidx.lifecycle.service)
implementation(libs.androidx.activity.compose) implementation(libs.androidx.activity.compose)
implementation(platform(libs.androidx.compose.bom)) implementation(platform(libs.androidx.compose.bom))
@@ -130,26 +156,23 @@ dependencies {
debugImplementation(libs.androidx.compose.ui.tooling) debugImplementation(libs.androidx.compose.ui.tooling)
debugImplementation(libs.androidx.compose.manifest) debugImplementation(libs.androidx.compose.manifest)
// get tunnel lib from github packages or mavenLocal // wg
implementation(libs.tunnel) implementation(libs.tunnel)
implementation(libs.amneziawg.android)
coreLibraryDesugaring(libs.desugar.jdk.libs) coreLibraryDesugaring(libs.desugar.jdk.libs)
// logging // logging
implementation(libs.timber) implementation(libs.timber)
// compose navigation // compose navigation
implementation(libs.androidx.navigation.compose) implementation(libs.androidx.navigation.compose)
implementation(libs.androidx.hilt.navigation.compose) implementation(libs.androidx.hilt.navigation.compose)
implementation(libs.zaneschepke.multifab)
// hilt // hilt
implementation(libs.hilt.android) implementation(libs.hilt.android)
ksp(libs.hilt.android.compiler) ksp(libs.hilt.android.compiler)
// accompanist // accompanist
implementation(libs.accompanist.systemuicontroller)
implementation(libs.accompanist.permissions) implementation(libs.accompanist.permissions)
implementation(libs.accompanist.flowlayout) implementation(libs.accompanist.flowlayout)
implementation(libs.accompanist.drawablepainter) implementation(libs.accompanist.drawablepainter)
@@ -170,25 +193,19 @@ dependencies {
// serialization // serialization
implementation(libs.kotlinx.serialization.json) implementation(libs.kotlinx.serialization.json)
// firebase crashlytics
generalImplementation(platform(libs.firebase.bom))
generalImplementation(libs.google.firebase.crashlytics.ktx)
generalImplementation(libs.google.firebase.analytics.ktx)
// barcode scanning // barcode scanning
implementation(libs.zxing.android.embedded) implementation(libs.zxing.android.embedded)
implementation(libs.zxing.core)
// bio // bio
implementation(libs.androidx.biometric.ktx) implementation(libs.androidx.biometric.ktx)
implementation(libs.pin.lock.compose)
// shortcuts // shortcuts
implementation(libs.androidx.core) implementation(libs.androidx.core)
implementation(libs.androidx.core.google.shortcuts) implementation(libs.androidx.core.google.shortcuts)
// splash
implementation(libs.androidx.core.splashscreen)
}
fun nightlyVersionCode() : Int {
return Constants.VERSION_CODE + Constants.NIGHTLY_CODE
}
fun nightlyVersionName() : String {
return Constants.VERSION_NAME + "-${grgitService.service.get().grgit.head().abbreviatedId}"
} }
+39
View File
@@ -0,0 +1,39 @@
{
"project_info": {
"project_number": "328300975830",
"project_id": "wireguard-auto-tunnel",
"storage_bucket": "wireguard-auto-tunnel.appspot.com"
},
"client": [
{
"client_info": {
"mobilesdk_app_id": "1:328300975830:android:82cd774598ccb7234b1b77",
"android_client_info": {
"package_name": "com.zaneschepke.wireguardautotunnel"
}
},
"oauth_client": [
{
"client_id": "328300975830-m72lc3hr69ddhdqh9ngr27rvc8o0jb2d.apps.googleusercontent.com",
"client_type": 3
}
],
"api_key": [
{
"current_key": "AIzaSyBsSMY0LlckizXDnuYBy7nXWGSdl8zZedI"
}
],
"services": {
"appinvite_service": {
"other_platform_oauth_client": [
{
"client_id": "328300975830-m72lc3hr69ddhdqh9ngr27rvc8o0jb2d.apps.googleusercontent.com",
"client_type": 3
}
]
}
}
}
],
"configuration_version": "1"
}
+2
View File
@@ -22,3 +22,5 @@
-keepclassmembers class * extends androidx.datastore.preferences.protobuf.GeneratedMessageLite { -keepclassmembers class * extends androidx.datastore.preferences.protobuf.GeneratedMessageLite {
<fields>; <fields>;
} }
@@ -1,168 +0,0 @@
{
"formatVersion": 1,
"database": {
"version": 6,
"identityHash": "625820076477aca948536f7bccccc7ca",
"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, `is_kernel_enabled` INTEGER NOT NULL DEFAULT false, `is_restore_on_boot_enabled` INTEGER NOT NULL DEFAULT false, `is_multi_tunnel_enabled` INTEGER NOT NULL DEFAULT false, `is_auto_tunnel_paused` INTEGER NOT NULL DEFAULT false, `is_ping_enabled` INTEGER NOT NULL DEFAULT false)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isAutoTunnelEnabled",
"columnName": "is_tunnel_enabled",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isTunnelOnMobileDataEnabled",
"columnName": "is_tunnel_on_mobile_data_enabled",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "trustedNetworkSSIDs",
"columnName": "trusted_network_ssids",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "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"
},
{
"fieldPath": "isKernelEnabled",
"columnName": "is_kernel_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isRestoreOnBootEnabled",
"columnName": "is_restore_on_boot_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isMultiTunnelEnabled",
"columnName": "is_multi_tunnel_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isAutoTunnelPaused",
"columnName": "is_auto_tunnel_paused",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isPingEnabled",
"columnName": "is_ping_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "TunnelConfig",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `wg_quick` TEXT NOT NULL)",
"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, '625820076477aca948536f7bccccc7ca')"
]
}
}
@@ -1,176 +0,0 @@
{
"formatVersion": 1,
"database": {
"version": 7,
"identityHash": "e65e4e7cf01f50fb03196d47b54288b1",
"entities": [
{
"tableName": "Settings",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_tunnel_enabled` INTEGER NOT NULL, `is_tunnel_on_mobile_data_enabled` INTEGER NOT NULL, `trusted_network_ssids` TEXT NOT NULL, `is_always_on_vpn_enabled` INTEGER NOT NULL, `is_tunnel_on_ethernet_enabled` INTEGER NOT NULL, `is_shortcuts_enabled` INTEGER NOT NULL DEFAULT false, `is_tunnel_on_wifi_enabled` INTEGER NOT NULL DEFAULT false, `is_kernel_enabled` INTEGER NOT NULL DEFAULT false, `is_restore_on_boot_enabled` INTEGER NOT NULL DEFAULT false, `is_multi_tunnel_enabled` INTEGER NOT NULL DEFAULT false, `is_auto_tunnel_paused` INTEGER NOT NULL DEFAULT false, `is_ping_enabled` INTEGER NOT NULL DEFAULT false)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isAutoTunnelEnabled",
"columnName": "is_tunnel_enabled",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isTunnelOnMobileDataEnabled",
"columnName": "is_tunnel_on_mobile_data_enabled",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "trustedNetworkSSIDs",
"columnName": "trusted_network_ssids",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "isAlwaysOnVpnEnabled",
"columnName": "is_always_on_vpn_enabled",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isTunnelOnEthernetEnabled",
"columnName": "is_tunnel_on_ethernet_enabled",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isShortcutsEnabled",
"columnName": "is_shortcuts_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isTunnelOnWifiEnabled",
"columnName": "is_tunnel_on_wifi_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isKernelEnabled",
"columnName": "is_kernel_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isRestoreOnBootEnabled",
"columnName": "is_restore_on_boot_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isMultiTunnelEnabled",
"columnName": "is_multi_tunnel_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isAutoTunnelPaused",
"columnName": "is_auto_tunnel_paused",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isPingEnabled",
"columnName": "is_ping_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "TunnelConfig",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `wg_quick` TEXT NOT NULL, `tunnel_networks` TEXT NOT NULL DEFAULT '', `is_mobile_data_tunnel` INTEGER NOT NULL DEFAULT false, `is_primary_tunnel` INTEGER NOT NULL DEFAULT false)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "wgQuick",
"columnName": "wg_quick",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "tunnelNetworks",
"columnName": "tunnel_networks",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "''"
},
{
"fieldPath": "isMobileDataTunnel",
"columnName": "is_mobile_data_tunnel",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isPrimaryTunnel",
"columnName": "is_primary_tunnel",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
},
"indices": [
{
"name": "index_TunnelConfig_name",
"unique": true,
"columnNames": [
"name"
],
"orders": [],
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_TunnelConfig_name` ON `${TABLE_NAME}` (`name`)"
}
],
"foreignKeys": []
}
],
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'e65e4e7cf01f50fb03196d47b54288b1')"
]
}
}
@@ -1,190 +0,0 @@
{
"formatVersion": 1,
"database": {
"version": 8,
"identityHash": "b4d4a7c489f6b2f0d3aa4fa6f37b4935",
"entities": [
{
"tableName": "Settings",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_tunnel_enabled` INTEGER NOT NULL, `is_tunnel_on_mobile_data_enabled` INTEGER NOT NULL, `trusted_network_ssids` TEXT NOT NULL, `is_always_on_vpn_enabled` INTEGER NOT NULL, `is_tunnel_on_ethernet_enabled` INTEGER NOT NULL, `is_shortcuts_enabled` INTEGER NOT NULL DEFAULT false, `is_tunnel_on_wifi_enabled` INTEGER NOT NULL DEFAULT false, `is_kernel_enabled` INTEGER NOT NULL DEFAULT false, `is_restore_on_boot_enabled` INTEGER NOT NULL DEFAULT false, `is_multi_tunnel_enabled` INTEGER NOT NULL DEFAULT false, `is_auto_tunnel_paused` INTEGER NOT NULL DEFAULT false, `is_ping_enabled` INTEGER NOT NULL DEFAULT false, `is_amnezia_enabled` INTEGER NOT NULL DEFAULT false)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isAutoTunnelEnabled",
"columnName": "is_tunnel_enabled",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isTunnelOnMobileDataEnabled",
"columnName": "is_tunnel_on_mobile_data_enabled",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "trustedNetworkSSIDs",
"columnName": "trusted_network_ssids",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "isAlwaysOnVpnEnabled",
"columnName": "is_always_on_vpn_enabled",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isTunnelOnEthernetEnabled",
"columnName": "is_tunnel_on_ethernet_enabled",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isShortcutsEnabled",
"columnName": "is_shortcuts_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isTunnelOnWifiEnabled",
"columnName": "is_tunnel_on_wifi_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isKernelEnabled",
"columnName": "is_kernel_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isRestoreOnBootEnabled",
"columnName": "is_restore_on_boot_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isMultiTunnelEnabled",
"columnName": "is_multi_tunnel_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isAutoTunnelPaused",
"columnName": "is_auto_tunnel_paused",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isPingEnabled",
"columnName": "is_ping_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isAmneziaEnabled",
"columnName": "is_amnezia_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "TunnelConfig",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `wg_quick` TEXT NOT NULL, `tunnel_networks` TEXT NOT NULL DEFAULT '', `is_mobile_data_tunnel` INTEGER NOT NULL DEFAULT false, `is_primary_tunnel` INTEGER NOT NULL DEFAULT false, `am_quick` TEXT NOT NULL DEFAULT '')",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "wgQuick",
"columnName": "wg_quick",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "tunnelNetworks",
"columnName": "tunnel_networks",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "''"
},
{
"fieldPath": "isMobileDataTunnel",
"columnName": "is_mobile_data_tunnel",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isPrimaryTunnel",
"columnName": "is_primary_tunnel",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "amQuick",
"columnName": "am_quick",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "''"
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
},
"indices": [
{
"name": "index_TunnelConfig_name",
"unique": true,
"columnNames": [
"name"
],
"orders": [],
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_TunnelConfig_name` ON `${TABLE_NAME}` (`name`)"
}
],
"foreignKeys": []
}
],
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'b4d4a7c489f6b2f0d3aa4fa6f37b4935')"
]
}
}
@@ -4,7 +4,6 @@ import androidx.room.testing.MigrationTestHelper
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry import androidx.test.platform.app.InstrumentationRegistry
import com.zaneschepke.wireguardautotunnel.data.AppDatabase import com.zaneschepke.wireguardautotunnel.data.AppDatabase
import com.zaneschepke.wireguardautotunnel.data.Queries
import org.junit.Rule import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.junit.runner.RunWith import org.junit.runner.RunWith
@@ -23,13 +22,39 @@ class MigrationTest {
@Test @Test
@Throws(IOException::class) @Throws(IOException::class)
fun migrate6To7() { fun migrate4To5() {
helper.createDatabase(dbName, 6).apply { helper.createDatabase(dbName, 4).apply {
// Database has schema version 1. Insert some data using SQL queries. // Database has schema version 1. Insert some data using SQL queries.
// You can't use DAO classes because they expect the latest schema. // You can't use DAO classes because they expect the latest schema.
execSQL(Queries.createDefaultSettings())
execSQL( execSQL(
Queries.createTunnelConfig(), "INSERT INTO Settings (is_tunnel_enabled," +
"is_tunnel_on_mobile_data_enabled," +
"trusted_network_ssids," +
"default_tunnel," +
"is_always_on_vpn_enabled," +
"is_tunnel_on_ethernet_enabled," +
"is_shortcuts_enabled," +
"is_battery_saver_enabled," +
"is_tunnel_on_wifi_enabled," +
"is_kernel_enabled," +
"is_restore_on_boot_enabled," +
"is_multi_tunnel_enabled)" +
" VALUES " +
"('false'," +
"'false'," +
"'[trustedSSID1,trustedSSID2]'," +
"'defaultTunnel'," +
"'false'," +
"'false'," +
"'false'," +
"'false'," +
"'false'," +
"'false'," +
"'false'," +
"'false')",
)
execSQL(
"INSERT INTO TunnelConfig (name, wg_quick)" + " VALUES ('hello', 'hello')",
) )
// Prepare for the next version. // Prepare for the next version.
close() close()
@@ -37,7 +62,7 @@ class MigrationTest {
// Re-open the database with version 2 and provide // Re-open the database with version 2 and provide
// MIGRATION_1_2 as the migration process. // MIGRATION_1_2 as the migration process.
helper.runMigrationsAndValidate(dbName, 7, true) helper.runMigrationsAndValidate(dbName, 5, true)
// MigrationTestHelper automatically verifies the schema changes, // MigrationTestHelper automatically verifies the schema changes,
// but you need to validate that the data was migrated properly. // but you need to validate that the data was migrated properly.
} }
+17 -38
View File
@@ -23,6 +23,7 @@
<!--foreground service exempt android 14--> <!--foreground service exempt android 14-->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SYSTEM_EXEMPTED" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE_SYSTEM_EXEMPTED" />
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" /> <uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" />
<!--foreground service permissions--> <!--foreground service permissions-->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
@@ -51,8 +52,8 @@
</queries> </queries>
<application <application
android:name=".WireGuardAutoTunnel" android:name=".WireGuardAutoTunnel"
android:allowBackup="false" android:allowBackup="true"
android:banner="@drawable/ic_banner" android:banner="@mipmap/ic_banner"
android:dataExtractionRules="@xml/data_extraction_rules" android:dataExtractionRules="@xml/data_extraction_rules"
android:enableOnBackInvokedCallback="true" android:enableOnBackInvokedCallback="true"
android:fullBackupContent="@xml/backup_rules" android:fullBackupContent="@xml/backup_rules"
@@ -60,35 +61,30 @@
android:label="@string/app_name" android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round" android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true" android:supportsRtl="true"
android:theme="@style/Theme.AppSplashScreen" android:theme="@style/Theme.WireguardAutoTunnel"
tools:targetApi="tiramisu"> tools:targetApi="tiramisu">
<activity <activity
android:name=".ui.SplashActivity" android:name=".ui.MainActivity"
android:exported="true" android:exported="true"
android:theme="@style/Theme.AppSplashScreen"> android:theme="@style/Theme.WireguardAutoTunnel">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN" /> <action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" /> <category android:name="android.intent.category.LAUNCHER" />
<category android:name="android.intent.category.LEANBACK_LAUNCHER" /> <category android:name="android.intent.category.LEANBACK_LAUNCHER" />
<action android:name="android.service.quicksettings.action.QS_TILE_PREFERENCES" />
</intent-filter> </intent-filter>
<meta-data <meta-data
android:name="android.app.shortcuts" android:name="android.app.shortcuts"
android:resource="@xml/shortcuts" /> android:resource="@xml/shortcuts" />
</activity> </activity>
<activity <activity
android:name=".ui.MainActivity" android:name=".ui.CaptureActivityPortrait"
android:exported="true" android:screenOrientation="fullSensor"
android:theme="@style/Theme.WireguardAutoTunnel"> android:stateNotNeeded="true"
<intent-filter> android:theme="@style/zxing_CaptureTheme"
<action android:name="android.service.quicksettings.action.QS_TILE_PREFERENCES" /> android:windowSoftInputMode="stateAlwaysHidden" />
</intent-filter>
</activity>
<activity
android:name="com.journeyapps.barcodescanner.CaptureActivity"
android:screenOrientation="portrait"
tools:replace="screenOrientation" />
<activity <activity
android:name=".service.shortcut.ShortcutsActivity" android:name=".service.shortcut.ShortcutsActivity"
android:enabled="true" android:enabled="true"
@@ -100,30 +96,13 @@
android:name=".service.foreground.ForegroundService" android:name=".service.foreground.ForegroundService"
android:enabled="true" android:enabled="true"
android:exported="false" android:exported="false"
android:foregroundServiceType="systemExempted" android:foregroundServiceType="systemExempted|specialUse"
tools:node="merge" /> tools:node="merge" />
<service <service
android:name=".service.tile.TunnelControlTile" android:name=".service.tile.TunnelControlTile"
android:exported="true" android:exported="true"
android:icon="@drawable/ic_launcher" android:icon="@drawable/shield"
android:label="Tunnel control" android:label="WG Tunnel"
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE">
<meta-data
android:name="android.service.quicksettings.ACTIVE_TILE"
android:value="true" />
<meta-data
android:name="android.service.quicksettings.TOGGLEABLE_TILE"
android:value="true" />
<intent-filter>
<action android:name="android.service.quicksettings.action.QS_TILE" />
</intent-filter>
</service>
<service
android:name=".service.tile.AutoTunnelControlTile"
android:exported="true"
android:icon="@drawable/ic_launcher"
android:label="Auto-tunnel"
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE"> android:permission="android.permission.BIND_QUICK_SETTINGS_TILE">
<meta-data <meta-data
android:name="android.service.quicksettings.ACTIVE_TILE" android:name="android.service.quicksettings.ACTIVE_TILE"
@@ -140,7 +119,7 @@
android:name=".service.foreground.WireGuardTunnelService" android:name=".service.foreground.WireGuardTunnelService"
android:enabled="true" android:enabled="true"
android:exported="false" android:exported="false"
android:foregroundServiceType="systemExempted" android:foregroundServiceType="systemExempted|specialUse"
android:permission="android.permission.BIND_VPN_SERVICE" android:permission="android.permission.BIND_VPN_SERVICE"
android:persistent="true" android:persistent="true"
tools:node="merge"> tools:node="merge">
@@ -155,7 +134,7 @@
android:name=".service.foreground.WireGuardConnectivityWatcherService" android:name=".service.foreground.WireGuardConnectivityWatcherService"
android:enabled="true" android:enabled="true"
android:exported="false" android:exported="false"
android:foregroundServiceType="systemExempted" android:foregroundServiceType="systemExempted|specialUse"
android:persistent="true" android:persistent="true"
android:stopWithTask="false" android:stopWithTask="false"
tools:node="merge" /> tools:node="merge" />
Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 38 KiB

@@ -3,36 +3,20 @@ package com.zaneschepke.wireguardautotunnel
import android.app.Application import android.app.Application
import android.content.ComponentName import android.content.ComponentName
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.os.StrictMode
import android.os.StrictMode.ThreadPolicy
import android.service.quicksettings.TileService import android.service.quicksettings.TileService
import com.zaneschepke.wireguardautotunnel.service.tile.AutoTunnelControlTile
import com.zaneschepke.wireguardautotunnel.service.tile.TunnelControlTile import com.zaneschepke.wireguardautotunnel.service.tile.TunnelControlTile
import com.zaneschepke.wireguardautotunnel.util.ReleaseTree
import dagger.hilt.android.HiltAndroidApp import dagger.hilt.android.HiltAndroidApp
import timber.log.Timber import timber.log.Timber
@HiltAndroidApp @HiltAndroidApp
class WireGuardAutoTunnel : Application() { class WireGuardAutoTunnel : Application() {
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
instance = this instance = this
if (BuildConfig.DEBUG) { if (BuildConfig.DEBUG) Timber.plant(Timber.DebugTree())
Timber.plant(Timber.DebugTree())
StrictMode.setThreadPolicy(
ThreadPolicy.Builder()
.detectDiskReads()
.detectDiskWrites()
.detectNetwork()
.penaltyLog()
.build(),
)
} else Timber.plant(ReleaseTree())
} }
companion object { companion object {
lateinit var instance: WireGuardAutoTunnel lateinit var instance: WireGuardAutoTunnel
private set private set
@@ -40,18 +24,11 @@ class WireGuardAutoTunnel : Application() {
return instance.packageManager.hasSystemFeature(PackageManager.FEATURE_LEANBACK) return instance.packageManager.hasSystemFeature(PackageManager.FEATURE_LEANBACK)
} }
fun requestTunnelTileServiceStateUpdate() { fun requestTileServiceStateUpdate() {
TileService.requestListeningState( TileService.requestListeningState(
instance, instance,
ComponentName(instance, TunnelControlTile::class.java), ComponentName(instance, TunnelControlTile::class.java),
) )
} }
fun requestAutoTunnelTileServiceUpdate() {
TileService.requestListeningState(
instance,
ComponentName(instance, AutoTunnelControlTile::class.java),
)
}
} }
} }
@@ -2,39 +2,27 @@ package com.zaneschepke.wireguardautotunnel.data
import androidx.room.AutoMigration import androidx.room.AutoMigration
import androidx.room.Database import androidx.room.Database
import androidx.room.DeleteColumn
import androidx.room.RoomDatabase import androidx.room.RoomDatabase
import androidx.room.TypeConverters import androidx.room.TypeConverters
import androidx.room.migration.AutoMigrationSpec import com.zaneschepke.wireguardautotunnel.data.model.Settings
import com.zaneschepke.wireguardautotunnel.data.domain.Settings import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
@Database( @Database(
entities = [Settings::class, TunnelConfig::class], entities = [Settings::class, TunnelConfig::class],
version = 8, version = 5,
autoMigrations = autoMigrations =
[ [
AutoMigration(from = 1, to = 2), AutoMigration(from = 1, to = 2),
AutoMigration(from = 2, to = 3), AutoMigration(from = 2, to = 3),
AutoMigration( AutoMigration(
from = 3, from = 3,
to = 4, to = 4,
), ),
AutoMigration( AutoMigration(
from = 4, from = 4,
to = 5, to = 5,
), ),
AutoMigration( ],
from = 5,
to = 6,
),
AutoMigration(
from = 6,
to = 7,
spec = RemoveLegacySettingColumnsMigration::class,
),
AutoMigration(7, 8),
],
exportSchema = true, exportSchema = true,
) )
@TypeConverters(DatabaseListConverters::class) @TypeConverters(DatabaseListConverters::class)
@@ -43,13 +31,3 @@ abstract class AppDatabase : RoomDatabase() {
abstract fun tunnelConfigDoa(): TunnelConfigDao abstract fun tunnelConfigDoa(): TunnelConfigDao
} }
@DeleteColumn(
tableName = "Settings",
columnName = "default_tunnel",
)
@DeleteColumn(
tableName = "Settings",
columnName = "is_battery_saver_enabled",
)
class RemoveLegacySettingColumnsMigration : AutoMigrationSpec
@@ -1,21 +0,0 @@
package com.zaneschepke.wireguardautotunnel.data
import androidx.room.RoomDatabase
import androidx.sqlite.db.SupportSQLiteDatabase
import timber.log.Timber
class DatabaseCallback : RoomDatabase.Callback() {
override fun onCreate(db: SupportSQLiteDatabase) = db.run {
// Notice non-ui thread is here
beginTransaction()
try {
execSQL(Queries.createDefaultSettings())
Timber.i("Bootstrapping settings data")
setTransactionSuccessful()
} catch (e: Exception) {
Timber.e(e)
} finally {
endTransaction()
}
}
}
@@ -12,7 +12,7 @@ class DatabaseListConverters {
@TypeConverter @TypeConverter
fun stringToList(value: String): MutableList<String> { fun stringToList(value: String): MutableList<String> {
if (value.isBlank() || value.isEmpty()) return mutableListOf() if (value.isEmpty()) return mutableListOf()
return try { return try {
Json.decodeFromString<MutableList<String>>(value) Json.decodeFromString<MutableList<String>>(value)
} catch (e: Exception) { } catch (e: Exception) {
@@ -1,35 +0,0 @@
package com.zaneschepke.wireguardautotunnel.data
object Queries {
fun createDefaultSettings(): String {
return """
INSERT INTO Settings (is_tunnel_enabled,
is_tunnel_on_mobile_data_enabled,
trusted_network_ssids,
is_always_on_vpn_enabled,
is_tunnel_on_ethernet_enabled,
is_shortcuts_enabled,
is_tunnel_on_wifi_enabled,
is_kernel_enabled,
is_restore_on_boot_enabled,
is_multi_tunnel_enabled)
VALUES
('false',
'false',
'sampleSSID1,sampleSSID2',
'false',
'false',
'false',
'false',
'false',
'false',
'false')
""".trimIndent()
}
fun createTunnelConfig(): String {
return """
INSERT INTO TunnelConfig (name, wg_quick) VALUES ('test', 'test')
""".trimIndent()
}
}
@@ -5,32 +5,24 @@ import androidx.room.Delete
import androidx.room.Insert import androidx.room.Insert
import androidx.room.OnConflictStrategy import androidx.room.OnConflictStrategy
import androidx.room.Query import androidx.room.Query
import com.zaneschepke.wireguardautotunnel.data.domain.Settings import com.zaneschepke.wireguardautotunnel.data.model.Settings
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
@Dao @Dao
interface SettingsDao { interface SettingsDao {
@Insert(onConflict = OnConflictStrategy.REPLACE) @Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun save(t: Settings)
suspend fun save(t: Settings)
@Insert(onConflict = OnConflictStrategy.REPLACE) @Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun saveAll(t: List<Settings>)
suspend fun saveAll(t: List<Settings>)
@Query("SELECT * FROM settings WHERE id=:id") @Query("SELECT * FROM settings WHERE id=:id") suspend fun getById(id: Long): Settings?
suspend fun getById(id: Long): Settings?
@Query("SELECT * FROM settings") @Query("SELECT * FROM settings") suspend fun getAll(): List<Settings>
suspend fun getAll(): List<Settings>
@Query("SELECT * FROM settings LIMIT 1") @Query("SELECT * FROM settings LIMIT 1") fun getSettingsFlow(): Flow<Settings>
fun getSettingsFlow(): Flow<Settings>
@Query("SELECT * FROM settings") @Query("SELECT * FROM settings") fun getAllFlow(): Flow<MutableList<Settings>>
fun getAllFlow(): Flow<MutableList<Settings>>
@Delete @Delete suspend fun delete(t: Settings)
suspend fun delete(t: Settings)
@Query("SELECT COUNT('id') FROM settings") @Query("SELECT COUNT('id') FROM settings") suspend fun count(): Long
suspend fun count(): Long
} }
@@ -5,48 +5,22 @@ import androidx.room.Delete
import androidx.room.Insert import androidx.room.Insert
import androidx.room.OnConflictStrategy import androidx.room.OnConflictStrategy
import androidx.room.Query import androidx.room.Query
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
import com.zaneschepke.wireguardautotunnel.util.TunnelConfigs
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
@Dao @Dao
interface TunnelConfigDao { interface TunnelConfigDao {
@Insert(onConflict = OnConflictStrategy.REPLACE) @Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun save(t: TunnelConfig)
suspend fun save(t: TunnelConfig)
@Insert(onConflict = OnConflictStrategy.REPLACE) @Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun saveAll(t: List<TunnelConfig>)
suspend fun saveAll(t: TunnelConfigs)
@Query("SELECT * FROM TunnelConfig WHERE id=:id") @Query("SELECT * FROM TunnelConfig WHERE id=:id") suspend fun getById(id: Long): TunnelConfig?
suspend fun getById(id: Long): TunnelConfig?
@Query("SELECT * FROM TunnelConfig WHERE name=:name") @Query("SELECT * FROM TunnelConfig") suspend fun getAll(): List<TunnelConfig>
suspend fun getByName(name: String): TunnelConfig?
@Query("SELECT * FROM TunnelConfig") @Delete suspend fun delete(t: TunnelConfig)
suspend fun getAll(): TunnelConfigs
@Delete @Query("SELECT COUNT('id') FROM TunnelConfig") suspend fun count(): Long
suspend fun delete(t: TunnelConfig)
@Query("SELECT COUNT('id') FROM TunnelConfig") @Query("SELECT * FROM tunnelconfig") fun getAllFlow(): Flow<MutableList<TunnelConfig>>
suspend fun count(): Long
@Query("SELECT * FROM TunnelConfig WHERE tunnel_networks LIKE '%' || :name || '%'")
suspend fun findByTunnelNetworkName(name: String): TunnelConfigs
@Query("UPDATE TunnelConfig SET is_primary_tunnel = 0 WHERE is_primary_tunnel =1")
suspend fun resetPrimaryTunnel()
@Query("UPDATE TunnelConfig SET is_mobile_data_tunnel = 0 WHERE is_mobile_data_tunnel =1")
suspend fun resetMobileDataTunnel()
@Query("SELECT * FROM TUNNELCONFIG WHERE is_primary_tunnel=1")
suspend fun findByPrimary(): TunnelConfigs
@Query("SELECT * FROM TUNNELCONFIG WHERE is_mobile_data_tunnel=1")
suspend fun findByMobileDataTunnel(): TunnelConfigs
@Query("SELECT * FROM tunnelconfig")
fun getAllFlow(): Flow<MutableList<TunnelConfig>>
} }
@@ -4,80 +4,35 @@ import android.content.Context
import androidx.datastore.preferences.core.Preferences import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.booleanPreferencesKey import androidx.datastore.preferences.core.booleanPreferencesKey
import androidx.datastore.preferences.core.edit import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.intPreferencesKey
import androidx.datastore.preferences.core.stringPreferencesKey
import androidx.datastore.preferences.preferencesDataStore import androidx.datastore.preferences.preferencesDataStore
import com.zaneschepke.wireguardautotunnel.module.IoDispatcher
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import timber.log.Timber
import java.io.IOException
class DataStoreManager( class DataStoreManager(private val context: Context) {
private val context: Context,
@IoDispatcher private val ioDispatcher: CoroutineDispatcher
) {
companion object { companion object {
val LOCATION_DISCLOSURE_SHOWN = booleanPreferencesKey("LOCATION_DISCLOSURE_SHOWN") val LOCATION_DISCLOSURE_SHOWN = booleanPreferencesKey("LOCATION_DISCLOSURE_SHOWN")
val BATTERY_OPTIMIZE_DISABLE_SHOWN = booleanPreferencesKey("BATTERY_OPTIMIZE_DISABLE_SHOWN") val BATTERY_OPTIMIZE_DISABLE_SHOWN = booleanPreferencesKey("BATTERY_OPTIMIZE_DISABLE_SHOWN")
val TUNNEL_RUNNING_FROM_MANUAL_START =
booleanPreferencesKey("TUNNEL_RUNNING_FROM_MANUAL_START")
val ACTIVE_TUNNEL = intPreferencesKey("ACTIVE_TUNNEL")
val CURRENT_SSID = stringPreferencesKey("CURRENT_SSID")
val IS_PIN_LOCK_ENABLED = booleanPreferencesKey("PIN_LOCK_ENABLED")
} }
// preferences // preferences
private val preferencesKey = "preferences" private val preferencesKey = "preferences"
private val Context.dataStore by private val Context.dataStore by
preferencesDataStore( preferencesDataStore(
name = preferencesKey, name = preferencesKey,
) )
suspend fun init() { suspend fun init() {
withContext(ioDispatcher) { context.dataStore.data.first()
try {
context.dataStore.data.first()
} catch (e: IOException) {
Timber.e(e)
}
}
}
suspend fun <T> saveToDataStore(key: Preferences.Key<T>, value: T) {
withContext(ioDispatcher) {
try {
context.dataStore.edit { it[key] = value }
} catch (e: IOException) {
Timber.e(e)
} catch (e: Exception) {
Timber.e(e)
}
}
} }
suspend fun <T> saveToDataStore(key: Preferences.Key<T>, value: T) =
context.dataStore.edit { it[key] = value }
fun <T> getFromStoreFlow(key: Preferences.Key<T>) = context.dataStore.data.map { it[key] } fun <T> getFromStoreFlow(key: Preferences.Key<T>) = context.dataStore.data.map { it[key] }
suspend fun <T> getFromStore(key: Preferences.Key<T>): T? { suspend fun <T> getFromStore(key: Preferences.Key<T>) =
return withContext(ioDispatcher) { context.dataStore.data.first { it.contains(key) }[key]
try {
context.dataStore.data.map { it[key] }.first()
} catch (e: IOException) {
Timber.e(e)
null
}
}
}
fun <T> getFromStoreBlocking(key: Preferences.Key<T>) = runBlocking {
context.dataStore.data.map { it[key] }.first()
}
val preferencesFlow: Flow<Preferences?> = context.dataStore.data val preferencesFlow: Flow<Preferences?> = context.dataStore.data
} }
@@ -1,16 +0,0 @@
package com.zaneschepke.wireguardautotunnel.data.domain
data class GeneralState(
val isLocationDisclosureShown: Boolean = LOCATION_DISCLOSURE_SHOWN_DEFAULT,
val isBatteryOptimizationDisableShown: Boolean = BATTERY_OPTIMIZATION_DISABLE_SHOWN_DEFAULT,
val isTunnelRunningFromManualStart: Boolean = TUNNELING_RUNNING_FROM_MANUAL_START_DEFAULT,
val isPinLockEnabled: Boolean = PIN_LOCK_ENABLED_DEFAULT,
val activeTunnelId: Int? = null
) {
companion object {
const val LOCATION_DISCLOSURE_SHOWN_DEFAULT = false
const val BATTERY_OPTIMIZATION_DISABLE_SHOWN_DEFAULT = false
const val TUNNELING_RUNNING_FROM_MANUAL_START_DEFAULT = false
const val PIN_LOCK_ENABLED_DEFAULT = false
}
}
@@ -1,58 +0,0 @@
package com.zaneschepke.wireguardautotunnel.data.domain
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
@Entity
data class Settings(
@PrimaryKey(autoGenerate = true) val id: Int = 0,
@ColumnInfo(name = "is_tunnel_enabled") val isAutoTunnelEnabled: Boolean = false,
@ColumnInfo(name = "is_tunnel_on_mobile_data_enabled")
val isTunnelOnMobileDataEnabled: Boolean = false,
@ColumnInfo(name = "trusted_network_ssids")
val trustedNetworkSSIDs: MutableList<String> = mutableListOf(),
@ColumnInfo(name = "is_always_on_vpn_enabled") val isAlwaysOnVpnEnabled: Boolean = false,
@ColumnInfo(name = "is_tunnel_on_ethernet_enabled")
val isTunnelOnEthernetEnabled: Boolean = false,
@ColumnInfo(
name = "is_shortcuts_enabled",
defaultValue = "false",
)
val isShortcutsEnabled: Boolean = false,
@ColumnInfo(
name = "is_tunnel_on_wifi_enabled",
defaultValue = "false",
)
val isTunnelOnWifiEnabled: Boolean = false,
@ColumnInfo(
name = "is_kernel_enabled",
defaultValue = "false",
)
val isKernelEnabled: Boolean = false,
@ColumnInfo(
name = "is_restore_on_boot_enabled",
defaultValue = "false",
)
val isRestoreOnBootEnabled: Boolean = false,
@ColumnInfo(
name = "is_multi_tunnel_enabled",
defaultValue = "false",
)
val isMultiTunnelEnabled: Boolean = false,
@ColumnInfo(
name = "is_auto_tunnel_paused",
defaultValue = "false",
)
val isAutoTunnelPaused: Boolean = false,
@ColumnInfo(
name = "is_ping_enabled",
defaultValue = "false",
)
val isPingEnabled: Boolean = false,
@ColumnInfo(
name = "is_amnezia_enabled",
defaultValue = "false",
)
val isAmneziaEnabled: Boolean = false,
)
@@ -1,53 +0,0 @@
package com.zaneschepke.wireguardautotunnel.data.domain
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.Index
import androidx.room.PrimaryKey
import com.wireguard.config.Config
import java.io.InputStream
@Entity(indices = [Index(value = ["name"], unique = true)])
data class TunnelConfig(
@PrimaryKey(autoGenerate = true) val id: Int = 0,
@ColumnInfo(name = "name") val name: String,
@ColumnInfo(name = "wg_quick") val wgQuick: String,
@ColumnInfo(
name = "tunnel_networks",
defaultValue = "",
)
val tunnelNetworks: MutableList<String> = mutableListOf(),
@ColumnInfo(
name = "is_mobile_data_tunnel",
defaultValue = "false",
)
val isMobileDataTunnel: Boolean = false,
@ColumnInfo(
name = "is_primary_tunnel",
defaultValue = "false",
)
val isPrimaryTunnel: Boolean = false,
@ColumnInfo(
name = "am_quick",
defaultValue = "",
)
val amQuick: String = AM_QUICK_DEFAULT,
) {
companion object {
fun configFromWgQuick(wgQuick: String): Config {
val inputStream: InputStream = wgQuick.byteInputStream()
return inputStream.bufferedReader(Charsets.UTF_8).use {
Config.parse(it)
}
}
fun configFromAmQuick(amQuick: String): org.amnezia.awg.config.Config {
val inputStream: InputStream = amQuick.byteInputStream()
return inputStream.bufferedReader(Charsets.UTF_8).use {
org.amnezia.awg.config.Config.parse(it)
}
}
const val AM_QUICK_DEFAULT = ""
}
}
@@ -0,0 +1,63 @@
package com.zaneschepke.wireguardautotunnel.data.model
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
@Entity
data class Settings(
@PrimaryKey(autoGenerate = true) val id: Int = 0,
@ColumnInfo(name = "is_tunnel_enabled") var isAutoTunnelEnabled: Boolean = false,
@ColumnInfo(name = "is_tunnel_on_mobile_data_enabled")
var isTunnelOnMobileDataEnabled: Boolean = false,
@ColumnInfo(name = "trusted_network_ssids")
var trustedNetworkSSIDs: MutableList<String> = mutableListOf(),
@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,
@ColumnInfo(
name = "is_kernel_enabled",
defaultValue = "false",
)
var isKernelEnabled: Boolean = false,
@ColumnInfo(
name = "is_restore_on_boot_enabled",
defaultValue = "false",
)
var isRestoreOnBootEnabled: Boolean = false,
@ColumnInfo(
name = "is_multi_tunnel_enabled",
defaultValue = "false",
)
var isMultiTunnelEnabled: Boolean = false,
@ColumnInfo(
name = "is_auto_tunnel_paused",
defaultValue = "false",
)
var isAutoTunnelPaused: Boolean = false,
) {
fun isTunnelConfigDefault(tunnelConfig: TunnelConfig): Boolean {
return if (defaultTunnel != null) {
val defaultConfig = TunnelConfig.from(defaultTunnel!!)
(tunnelConfig.id == defaultConfig.id)
} else {
false
}
}
}
@@ -0,0 +1,34 @@
package com.zaneschepke.wireguardautotunnel.data.model
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.Index
import androidx.room.PrimaryKey
import com.wireguard.config.Config
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import java.io.InputStream
@Entity(indices = [Index(value = ["name"], unique = true)])
@Serializable
data class TunnelConfig(
@PrimaryKey(autoGenerate = true) val id: Int = 0,
@ColumnInfo(name = "name") var name: String,
@ColumnInfo(name = "wg_quick") var wgQuick: String
) {
override fun toString(): String {
return Json.encodeToString(serializer(), this)
}
companion object {
fun from(string: String): TunnelConfig {
return Json.decodeFromString<TunnelConfig>(string)
}
fun configFromQuick(wgQuick: String): Config {
val inputStream: InputStream = wgQuick.byteInputStream()
val reader = inputStream.bufferedReader(Charsets.UTF_8)
return Config.parse(reader)
}
}
}
@@ -1,14 +0,0 @@
package com.zaneschepke.wireguardautotunnel.data.repository
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
interface AppDataRepository {
suspend fun getPrimaryOrFirstTunnel(): TunnelConfig?
suspend fun getStartTunnelConfig(): TunnelConfig?
suspend fun toggleWatcherServicePause()
val settings: SettingsRepository
val tunnels: TunnelConfigRepository
val appState: AppStateRepository
}
@@ -1,34 +0,0 @@
package com.zaneschepke.wireguardautotunnel.data.repository
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
import javax.inject.Inject
class AppDataRoomRepository @Inject constructor(
override val settings: SettingsRepository,
override val tunnels: TunnelConfigRepository,
override val appState: AppStateRepository
) : AppDataRepository {
override suspend fun getPrimaryOrFirstTunnel(): TunnelConfig? {
return tunnels.findPrimary().firstOrNull() ?: tunnels.getAll().firstOrNull()
}
override suspend fun getStartTunnelConfig(): TunnelConfig? {
return if (appState.isTunnelRunningFromManualStart()) {
appState.getActiveTunnelId()?.let {
tunnels.getById(it)
}
} else null
}
override suspend fun toggleWatcherServicePause() {
val settings = settings.getSettings()
if (settings.isAutoTunnelEnabled) {
val pauseAutoTunnel = !settings.isAutoTunnelPaused
this.settings.save(
settings.copy(
isAutoTunnelPaused = pauseAutoTunnel,
),
)
}
}
}
@@ -1,29 +0,0 @@
package com.zaneschepke.wireguardautotunnel.data.repository
import com.zaneschepke.wireguardautotunnel.data.domain.GeneralState
import kotlinx.coroutines.flow.Flow
interface AppStateRepository {
suspend fun isLocationDisclosureShown(): Boolean
suspend fun setLocationDisclosureShown(shown: Boolean)
suspend fun isPinLockEnabled(): Boolean
suspend fun setPinLockEnabled(enabled: Boolean)
suspend fun isBatteryOptimizationDisableShown(): Boolean
suspend fun setBatteryOptimizationDisableShown(shown: Boolean)
suspend fun isTunnelRunningFromManualStart(): Boolean
suspend fun setTunnelRunningFromManualStart(id: Int)
suspend fun setManualStop()
suspend fun getActiveTunnelId(): Int?
suspend fun getCurrentSsid(): String?
suspend fun setCurrentSsid(ssid: String)
val generalStateFlow: Flow<GeneralState>
}
@@ -1,92 +0,0 @@
package com.zaneschepke.wireguardautotunnel.data.repository
import com.zaneschepke.wireguardautotunnel.data.datastore.DataStoreManager
import com.zaneschepke.wireguardautotunnel.data.domain.GeneralState
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import timber.log.Timber
class DataStoreAppStateRepository(private val dataStoreManager: DataStoreManager) :
AppStateRepository {
override suspend fun isLocationDisclosureShown(): Boolean {
return dataStoreManager.getFromStore(DataStoreManager.LOCATION_DISCLOSURE_SHOWN)
?: GeneralState.LOCATION_DISCLOSURE_SHOWN_DEFAULT
}
override suspend fun setLocationDisclosureShown(shown: Boolean) {
dataStoreManager.saveToDataStore(DataStoreManager.LOCATION_DISCLOSURE_SHOWN, shown)
}
override suspend fun isPinLockEnabled(): Boolean {
return dataStoreManager.getFromStore(DataStoreManager.IS_PIN_LOCK_ENABLED)
?: GeneralState.PIN_LOCK_ENABLED_DEFAULT
}
override suspend fun setPinLockEnabled(enabled: Boolean) {
dataStoreManager.saveToDataStore(DataStoreManager.IS_PIN_LOCK_ENABLED, enabled)
}
override suspend fun isBatteryOptimizationDisableShown(): Boolean {
return dataStoreManager.getFromStore(DataStoreManager.BATTERY_OPTIMIZE_DISABLE_SHOWN)
?: GeneralState.BATTERY_OPTIMIZATION_DISABLE_SHOWN_DEFAULT
}
override suspend fun setBatteryOptimizationDisableShown(shown: Boolean) {
dataStoreManager.saveToDataStore(DataStoreManager.BATTERY_OPTIMIZE_DISABLE_SHOWN, shown)
}
override suspend fun isTunnelRunningFromManualStart(): Boolean {
return dataStoreManager.getFromStore(DataStoreManager.TUNNEL_RUNNING_FROM_MANUAL_START)
?: GeneralState.TUNNELING_RUNNING_FROM_MANUAL_START_DEFAULT
}
override suspend fun setTunnelRunningFromManualStart(id: Int) {
setTunnelRunningFromManualStart(true)
setActiveTunnelId(id)
}
override suspend fun setManualStop() {
setTunnelRunningFromManualStart(false)
}
private suspend fun setTunnelRunningFromManualStart(running: Boolean) {
dataStoreManager.saveToDataStore(DataStoreManager.TUNNEL_RUNNING_FROM_MANUAL_START, running)
}
override suspend fun getActiveTunnelId(): Int? {
return dataStoreManager.getFromStore(DataStoreManager.ACTIVE_TUNNEL)
}
private suspend fun setActiveTunnelId(id: Int) {
dataStoreManager.saveToDataStore(DataStoreManager.ACTIVE_TUNNEL, id)
}
override suspend fun getCurrentSsid(): String? {
return dataStoreManager.getFromStore(DataStoreManager.CURRENT_SSID)
}
override suspend fun setCurrentSsid(ssid: String) {
dataStoreManager.saveToDataStore(DataStoreManager.CURRENT_SSID, ssid)
}
override val generalStateFlow: Flow<GeneralState> =
dataStoreManager.preferencesFlow.map { prefs ->
prefs?.let { pref ->
try {
GeneralState(
isLocationDisclosureShown = pref[DataStoreManager.LOCATION_DISCLOSURE_SHOWN]
?: GeneralState.LOCATION_DISCLOSURE_SHOWN_DEFAULT,
isBatteryOptimizationDisableShown = pref[DataStoreManager.BATTERY_OPTIMIZE_DISABLE_SHOWN]
?: GeneralState.BATTERY_OPTIMIZATION_DISABLE_SHOWN_DEFAULT,
isTunnelRunningFromManualStart = pref[DataStoreManager.TUNNEL_RUNNING_FROM_MANUAL_START]
?: GeneralState.TUNNELING_RUNNING_FROM_MANUAL_START_DEFAULT,
isPinLockEnabled = pref[DataStoreManager.IS_PIN_LOCK_ENABLED]
?: GeneralState.TUNNELING_RUNNING_FROM_MANUAL_START_DEFAULT,
)
} catch (e: IllegalArgumentException) {
Timber.e(e)
GeneralState()
}
} ?: GeneralState()
}
}
@@ -1,72 +0,0 @@
package com.zaneschepke.wireguardautotunnel.data.repository
import com.zaneschepke.wireguardautotunnel.data.TunnelConfigDao
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
import com.zaneschepke.wireguardautotunnel.util.TunnelConfigs
import kotlinx.coroutines.flow.Flow
class RoomTunnelConfigRepository(private val tunnelConfigDao: TunnelConfigDao) :
TunnelConfigRepository {
override fun getTunnelConfigsFlow(): Flow<TunnelConfigs> {
return tunnelConfigDao.getAllFlow()
}
override suspend fun getAll(): TunnelConfigs {
return tunnelConfigDao.getAll()
}
override suspend fun save(tunnelConfig: TunnelConfig) {
tunnelConfigDao.save(tunnelConfig)
}
override suspend fun updatePrimaryTunnel(tunnelConfig: TunnelConfig?) {
tunnelConfigDao.resetPrimaryTunnel()
tunnelConfig?.let {
save(
it.copy(
isPrimaryTunnel = true,
),
)
}
}
override suspend fun updateMobileDataTunnel(tunnelConfig: TunnelConfig?) {
tunnelConfigDao.resetMobileDataTunnel()
tunnelConfig?.let {
save(
it.copy(
isMobileDataTunnel = true,
),
)
}
}
override suspend fun delete(tunnelConfig: TunnelConfig) {
tunnelConfigDao.delete(tunnelConfig)
}
override suspend fun getById(id: Int): TunnelConfig? {
return tunnelConfigDao.getById(id.toLong())
}
override suspend fun count(): Int {
return tunnelConfigDao.count().toInt()
}
override suspend fun findByTunnelName(name: String): TunnelConfig? {
return tunnelConfigDao.getByName(name)
}
override suspend fun findByTunnelNetworksName(name: String): TunnelConfigs {
return tunnelConfigDao.findByTunnelNetworkName(name)
}
override suspend fun findByMobileDataTunnel(): TunnelConfigs {
return tunnelConfigDao.findByMobileDataTunnel()
}
override suspend fun findPrimary(): TunnelConfigs {
return tunnelConfigDao.findByPrimary()
}
}
@@ -1,6 +1,6 @@
package com.zaneschepke.wireguardautotunnel.data.repository package com.zaneschepke.wireguardautotunnel.data.repository
import com.zaneschepke.wireguardautotunnel.data.domain.Settings import com.zaneschepke.wireguardautotunnel.data.model.Settings
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
interface SettingsRepository { interface SettingsRepository {
@@ -1,10 +1,10 @@
package com.zaneschepke.wireguardautotunnel.data.repository package com.zaneschepke.wireguardautotunnel.data.repository
import com.zaneschepke.wireguardautotunnel.data.SettingsDao import com.zaneschepke.wireguardautotunnel.data.SettingsDao
import com.zaneschepke.wireguardautotunnel.data.domain.Settings import com.zaneschepke.wireguardautotunnel.data.model.Settings
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
class RoomSettingsRepository(private val settingsDoa: SettingsDao) : SettingsRepository { class SettingsRepositoryImpl(private val settingsDoa: SettingsDao) : SettingsRepository {
override suspend fun save(settings: Settings) { override suspend fun save(settings: Settings) {
settingsDoa.save(settings) settingsDoa.save(settings)
@@ -1,6 +1,6 @@
package com.zaneschepke.wireguardautotunnel.data.repository package com.zaneschepke.wireguardautotunnel.data.repository
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
import com.zaneschepke.wireguardautotunnel.util.TunnelConfigs import com.zaneschepke.wireguardautotunnel.util.TunnelConfigs
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
@@ -12,21 +12,7 @@ interface TunnelConfigRepository {
suspend fun save(tunnelConfig: TunnelConfig) suspend fun save(tunnelConfig: TunnelConfig)
suspend fun updatePrimaryTunnel(tunnelConfig: TunnelConfig?)
suspend fun updateMobileDataTunnel(tunnelConfig: TunnelConfig?)
suspend fun delete(tunnelConfig: TunnelConfig) suspend fun delete(tunnelConfig: TunnelConfig)
suspend fun getById(id: Int): TunnelConfig?
suspend fun count(): Int suspend fun count(): Int
suspend fun findByTunnelName(name: String): TunnelConfig?
suspend fun findByTunnelNetworksName(name: String): TunnelConfigs
suspend fun findByMobileDataTunnel(): TunnelConfigs
suspend fun findPrimary(): TunnelConfigs
} }
@@ -0,0 +1,29 @@
package com.zaneschepke.wireguardautotunnel.data.repository
import com.zaneschepke.wireguardautotunnel.data.TunnelConfigDao
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
import com.zaneschepke.wireguardautotunnel.util.TunnelConfigs
import kotlinx.coroutines.flow.Flow
class TunnelConfigRepositoryImpl(private val tunnelConfigDao: TunnelConfigDao) :
TunnelConfigRepository {
override fun getTunnelConfigsFlow(): Flow<TunnelConfigs> {
return tunnelConfigDao.getAllFlow()
}
override suspend fun getAll(): TunnelConfigs {
return tunnelConfigDao.getAll()
}
override suspend fun save(tunnelConfig: TunnelConfig) {
tunnelConfigDao.save(tunnelConfig)
}
override suspend fun delete(tunnelConfig: TunnelConfig) {
tunnelConfigDao.delete(tunnelConfig)
}
override suspend fun count(): Int {
return tunnelConfigDao.count().toInt()
}
}
@@ -1,30 +0,0 @@
package com.zaneschepke.wireguardautotunnel.module
import android.content.Context
import com.zaneschepke.logcatter.LocalLogCollector
import com.zaneschepke.logcatter.LogcatHelper
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
class AppModule {
@Singleton
@ApplicationScope
@Provides
fun providesApplicationScope(@DefaultDispatcher defaultDispatcher: CoroutineDispatcher): CoroutineScope =
CoroutineScope(SupervisorJob() + defaultDispatcher)
@Singleton
@Provides
fun provideLogCollect(@ApplicationContext context: Context): LocalLogCollector {
return LogcatHelper.init(context = context)
}
}
@@ -1,11 +0,0 @@
package com.zaneschepke.wireguardautotunnel.module
import javax.inject.Qualifier
@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class Kernel
@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class Userspace
@@ -1,27 +0,0 @@
package com.zaneschepke.wireguardautotunnel.module
import javax.inject.Qualifier
@Retention(AnnotationRetention.RUNTIME)
@Qualifier
annotation class DefaultDispatcher
@Retention(AnnotationRetention.RUNTIME)
@Qualifier
annotation class IoDispatcher
@Retention(AnnotationRetention.RUNTIME)
@Qualifier
annotation class MainDispatcher
@Retention(AnnotationRetention.BINARY)
@Qualifier
annotation class MainImmediateDispatcher
@Retention(AnnotationRetention.RUNTIME)
@Qualifier
annotation class ApplicationScope
@Retention(AnnotationRetention.RUNTIME)
@Qualifier
annotation class ServiceScope
@@ -1,28 +0,0 @@
package com.zaneschepke.wireguardautotunnel.module
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers
@Module
@InstallIn(SingletonComponent::class)
object CoroutinesDispatchersModule {
@DefaultDispatcher
@Provides
fun providesDefaultDispatcher(): CoroutineDispatcher = Dispatchers.Default
@IoDispatcher
@Provides
fun providesIoDispatcher(): CoroutineDispatcher = Dispatchers.IO
@MainDispatcher
@Provides
fun providesMainDispatcher(): CoroutineDispatcher = Dispatchers.Main
@MainImmediateDispatcher
@Provides
fun providesMainImmediateDispatcher(): CoroutineDispatcher = Dispatchers.Main.immediate
}
@@ -0,0 +1,28 @@
package com.zaneschepke.wireguardautotunnel.module
import android.content.Context
import androidx.room.Room
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.data.AppDatabase
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
class DatabaseModule {
@Provides
@Singleton
fun provideDatabase(@ApplicationContext context: Context): AppDatabase {
return Room.databaseBuilder(
context,
AppDatabase::class.java,
context.getString(R.string.db_name),
)
.fallbackToDestructiveMigration()
.build()
}
}
@@ -0,0 +1,5 @@
package com.zaneschepke.wireguardautotunnel.module
import javax.inject.Qualifier
@Qualifier @Retention(AnnotationRetention.BINARY) annotation class Kernel
@@ -1,45 +1,24 @@
package com.zaneschepke.wireguardautotunnel.module package com.zaneschepke.wireguardautotunnel.module
import android.content.Context import android.content.Context
import androidx.room.Room
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.data.AppDatabase import com.zaneschepke.wireguardautotunnel.data.AppDatabase
import com.zaneschepke.wireguardautotunnel.data.DatabaseCallback
import com.zaneschepke.wireguardautotunnel.data.SettingsDao import com.zaneschepke.wireguardautotunnel.data.SettingsDao
import com.zaneschepke.wireguardautotunnel.data.TunnelConfigDao import com.zaneschepke.wireguardautotunnel.data.TunnelConfigDao
import com.zaneschepke.wireguardautotunnel.data.datastore.DataStoreManager import com.zaneschepke.wireguardautotunnel.data.datastore.DataStoreManager
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRoomRepository
import com.zaneschepke.wireguardautotunnel.data.repository.AppStateRepository
import com.zaneschepke.wireguardautotunnel.data.repository.DataStoreAppStateRepository
import com.zaneschepke.wireguardautotunnel.data.repository.RoomSettingsRepository
import com.zaneschepke.wireguardautotunnel.data.repository.RoomTunnelConfigRepository
import com.zaneschepke.wireguardautotunnel.data.repository.SettingsRepository import com.zaneschepke.wireguardautotunnel.data.repository.SettingsRepository
import com.zaneschepke.wireguardautotunnel.data.repository.SettingsRepositoryImpl
import com.zaneschepke.wireguardautotunnel.data.repository.TunnelConfigRepository import com.zaneschepke.wireguardautotunnel.data.repository.TunnelConfigRepository
import com.zaneschepke.wireguardautotunnel.data.repository.TunnelConfigRepositoryImpl
import dagger.Module import dagger.Module
import dagger.Provides import dagger.Provides
import dagger.hilt.InstallIn import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent import dagger.hilt.components.SingletonComponent
import kotlinx.coroutines.CoroutineDispatcher
import javax.inject.Singleton import javax.inject.Singleton
@Module @Module
@InstallIn(SingletonComponent::class) @InstallIn(SingletonComponent::class)
class RepositoryModule { class RepositoryModule {
@Provides
@Singleton
fun provideDatabase(@ApplicationContext context: Context): AppDatabase {
return Room.databaseBuilder(
context,
AppDatabase::class.java,
context.getString(R.string.db_name),
)
.fallbackToDestructiveMigration()
.addCallback(DatabaseCallback())
.build()
}
@Singleton @Singleton
@Provides @Provides
fun provideSettingsDoa(appDatabase: AppDatabase): SettingsDao { fun provideSettingsDoa(appDatabase: AppDatabase): SettingsDao {
@@ -55,39 +34,18 @@ class RepositoryModule {
@Singleton @Singleton
@Provides @Provides
fun provideTunnelConfigRepository(tunnelConfigDao: TunnelConfigDao): TunnelConfigRepository { fun provideTunnelConfigRepository(tunnelConfigDao: TunnelConfigDao): TunnelConfigRepository {
return RoomTunnelConfigRepository(tunnelConfigDao) return TunnelConfigRepositoryImpl(tunnelConfigDao)
} }
@Singleton @Singleton
@Provides @Provides
fun provideSettingsRepository(settingsDao: SettingsDao): SettingsRepository { fun provideSettingsRepository(settingsDao: SettingsDao): SettingsRepository {
return RoomSettingsRepository(settingsDao) return SettingsRepositoryImpl(settingsDao)
} }
@Singleton @Singleton
@Provides @Provides
fun providePreferencesDataStore( fun providePreferencesDataStore(@ApplicationContext context: Context): DataStoreManager {
@ApplicationContext context: Context, return DataStoreManager(context)
@IoDispatcher ioDispatcher: CoroutineDispatcher
): DataStoreManager {
return DataStoreManager(context, ioDispatcher)
} }
@Provides
@Singleton
fun provideGeneralStateRepository(dataStoreManager: DataStoreManager): AppStateRepository {
return DataStoreAppStateRepository(dataStoreManager)
}
@Provides
@Singleton
fun provideAppDataRepository(
settingsRepository: SettingsRepository,
tunnelConfigRepository: TunnelConfigRepository,
appStateRepository: AppStateRepository
): AppDataRepository {
return AppDataRoomRepository(settingsRepository, tunnelConfigRepository, appStateRepository)
}
} }
@@ -6,8 +6,7 @@ import com.wireguard.android.backend.GoBackend
import com.wireguard.android.backend.WgQuickBackend import com.wireguard.android.backend.WgQuickBackend
import com.wireguard.android.util.RootShell import com.wireguard.android.util.RootShell
import com.wireguard.android.util.ToolsInstaller import com.wireguard.android.util.ToolsInstaller
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository import com.zaneschepke.wireguardautotunnel.data.repository.SettingsRepository
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnService import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnService
import com.zaneschepke.wireguardautotunnel.service.tunnel.WireGuardTunnel import com.zaneschepke.wireguardautotunnel.service.tunnel.WireGuardTunnel
import dagger.Module import dagger.Module
@@ -15,9 +14,6 @@ import dagger.Provides
import dagger.hilt.InstallIn import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent import dagger.hilt.components.SingletonComponent
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import javax.inject.Provider
import javax.inject.Singleton import javax.inject.Singleton
@Module @Module
@@ -43,38 +39,13 @@ class TunnelModule {
return WgQuickBackend(context, rootShell, ToolsInstaller(context, rootShell)) return WgQuickBackend(context, rootShell, ToolsInstaller(context, rootShell))
} }
@Provides
@Singleton
fun provideAmneziaBackend(@ApplicationContext context: Context): org.amnezia.awg.backend.Backend {
return org.amnezia.awg.backend.GoBackend(context)
}
@Provides @Provides
@Singleton @Singleton
fun provideVpnService( fun provideVpnService(
amneziaBackend: Provider<org.amnezia.awg.backend.Backend>, @Userspace userspaceBackend: Backend,
@Userspace userspaceBackend: Provider<Backend>, @Kernel kernelBackend: Backend,
@Kernel kernelBackend: Provider<Backend>, settingsRepository: SettingsRepository
appDataRepository: AppDataRepository,
@ApplicationScope applicationScope: CoroutineScope,
@IoDispatcher ioDispatcher: CoroutineDispatcher
): VpnService { ): VpnService {
return WireGuardTunnel( return WireGuardTunnel(userspaceBackend, kernelBackend, settingsRepository)
amneziaBackend,
userspaceBackend,
kernelBackend,
appDataRepository,
applicationScope,
ioDispatcher,
)
}
@Provides
@Singleton
fun provideServiceManager(
appDataRepository: AppDataRepository,
@IoDispatcher ioDispatcher: CoroutineDispatcher
): ServiceManager {
return ServiceManager(appDataRepository, ioDispatcher)
} }
} }
@@ -0,0 +1,5 @@
package com.zaneschepke.wireguardautotunnel.module
import javax.inject.Qualifier
@Qualifier @Retention(AnnotationRetention.BINARY) annotation class Userspace
@@ -1,25 +0,0 @@
package com.zaneschepke.wireguardautotunnel.module
import android.content.Context
import com.zaneschepke.wireguardautotunnel.util.FileUtils
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.components.ViewModelComponent
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.android.scopes.ViewModelScoped
import kotlinx.coroutines.CoroutineDispatcher
@Module
@InstallIn(ViewModelComponent::class)
class ViewModelModule {
@ViewModelScoped
@Provides
fun provideFileUtils(
@ApplicationContext context: Context,
@IoDispatcher ioDispatcher: CoroutineDispatcher
): FileUtils {
return FileUtils(context, ioDispatcher)
}
}
@@ -3,54 +3,21 @@ package com.zaneschepke.wireguardautotunnel.receiver
import android.content.BroadcastReceiver import android.content.BroadcastReceiver
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository import com.zaneschepke.wireguardautotunnel.data.repository.SettingsRepository
import com.zaneschepke.wireguardautotunnel.module.ApplicationScope
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
import com.zaneschepke.wireguardautotunnel.util.goAsync
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
@AndroidEntryPoint @AndroidEntryPoint
class BootReceiver : BroadcastReceiver() { class BootReceiver : BroadcastReceiver() {
@Inject @Inject lateinit var settingsRepository: SettingsRepository
lateinit var appDataRepository: AppDataRepository
@Inject override fun onReceive(context: Context?, intent: Intent?) = goAsync {
lateinit var serviceManager: ServiceManager if (Intent.ACTION_BOOT_COMPLETED != intent?.action) return@goAsync
if (settingsRepository.getSettings().isAutoTunnelEnabled) {
@Inject ServiceManager.startWatcherServiceForeground(context!!)
@ApplicationScope
lateinit var applicationScope: CoroutineScope
override fun onReceive(context: Context?, intent: Intent?) {
if (Intent.ACTION_BOOT_COMPLETED != intent?.action) return
context?.run {
applicationScope.launch {
val settings = appDataRepository.settings.getSettings()
if(settings.isRestoreOnBootEnabled) {
if (settings.isAutoTunnelEnabled) {
Timber.i("Starting watcher service from boot")
serviceManager.startWatcherServiceForeground(context)
}
if (appDataRepository.appState.isTunnelRunningFromManualStart()) {
appDataRepository.appState.getActiveTunnelId()?.let {
Timber.i("Starting tunnel that was active before reboot")
serviceManager.startVpnServiceForeground(
context,
appDataRepository.tunnels.getById(it)?.id,
)
return@launch
}
}
if (settings.isAlwaysOnVpnEnabled) {
Timber.i("Starting vpn service from boot AOVPN")
serviceManager.startVpnServiceForeground(context)
}
}
}
} }
} }
} }
@@ -4,41 +4,28 @@ import android.content.BroadcastReceiver
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import com.zaneschepke.wireguardautotunnel.data.repository.SettingsRepository import com.zaneschepke.wireguardautotunnel.data.repository.SettingsRepository
import com.zaneschepke.wireguardautotunnel.module.ApplicationScope
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
import com.zaneschepke.wireguardautotunnel.util.Constants import com.zaneschepke.wireguardautotunnel.util.Constants
import com.zaneschepke.wireguardautotunnel.util.goAsync
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.cancel import kotlinx.coroutines.cancel
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
@AndroidEntryPoint @AndroidEntryPoint
class NotificationActionReceiver : BroadcastReceiver() { class NotificationActionReceiver : BroadcastReceiver() {
@Inject @Inject lateinit var settingsRepository: SettingsRepository
lateinit var settingsRepository: SettingsRepository
@Inject override fun onReceive(context: Context, intent: Intent?) = goAsync {
lateinit var serviceManager: ServiceManager try {
val settings = settingsRepository.getSettings()
@Inject if (settings.defaultTunnel != null) {
@ApplicationScope ServiceManager.stopVpnService(context)
lateinit var applicationScope: CoroutineScope
override fun onReceive(context: Context, intent: Intent?) {
applicationScope.launch {
try {
//TODO fix for manual start changes when enabled
serviceManager.stopVpnServiceForeground(context)
delay(Constants.TOGGLE_TUNNEL_DELAY) delay(Constants.TOGGLE_TUNNEL_DELAY)
serviceManager.startVpnServiceForeground(context) ServiceManager.startVpnServiceForeground(context, settings.defaultTunnel.toString())
} catch (e: Exception) {
Timber.e(e)
} finally {
cancel()
} }
} finally {
cancel()
} }
} }
} }
@@ -3,6 +3,5 @@ package com.zaneschepke.wireguardautotunnel.service.foreground
enum class Action { enum class Action {
START, START,
START_FOREGROUND, START_FOREGROUND,
STOP, STOP
STOP_FOREGROUND
} }
@@ -4,7 +4,6 @@ import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.os.IBinder import android.os.IBinder
import androidx.lifecycle.LifecycleService import androidx.lifecycle.LifecycleService
import com.zaneschepke.wireguardautotunnel.util.Constants
import timber.log.Timber import timber.log.Timber
open class ForegroundService : LifecycleService() { open class ForegroundService : LifecycleService() {
@@ -24,13 +23,11 @@ open class ForegroundService : LifecycleService() {
when (action) { when (action) {
Action.START.name, Action.START.name,
Action.START_FOREGROUND.name -> startService(intent.extras) Action.START_FOREGROUND.name -> startService(intent.extras)
Action.STOP.name -> stopService(intent.extras)
Action.STOP.name, Action.STOP_FOREGROUND.name -> stopService() "android.net.VpnService" -> {
Constants.ALWAYS_ON_VPN_ACTION -> { Timber.d("Always-on VPN starting service")
Timber.i("Always-on VPN starting service")
startService(intent.extras) startService(intent.extras)
} }
else -> Timber.d("This should never happen. No action in the received intent") else -> Timber.d("This should never happen. No action in the received intent")
} }
} else { } else {
@@ -38,19 +35,29 @@ open class ForegroundService : LifecycleService() {
"with a null intent. It has been probably restarted by the system.", "with a null intent. It has been probably restarted by the system.",
) )
} }
// by returning this we make sure the service is restarted if the system kills the service
return START_STICKY return START_STICKY
} }
override fun onDestroy() {
super.onDestroy()
Timber.d("The service has been destroyed")
}
protected open fun startService(extras: Bundle?) { protected open fun startService(extras: Bundle?) {
if (isServiceStarted) return if (isServiceStarted) return
Timber.d("Starting ${this.javaClass.simpleName}") Timber.d("Starting ${this.javaClass.simpleName}")
isServiceStarted = true isServiceStarted = true
} }
protected open fun stopService() { protected open fun stopService(extras: Bundle?) {
Timber.d("Stopping ${this.javaClass.simpleName}") Timber.d("Stopping ${this.javaClass.simpleName}")
stopForeground(STOP_FOREGROUND_REMOVE) try {
stopSelf() stopForeground(STOP_FOREGROUND_REMOVE)
stopSelf()
} catch (e: Exception) {
Timber.d("Service stopped without being started: ${e.message}")
}
isServiceStarted = false isServiceStarted = false
} }
} }
@@ -3,23 +3,31 @@ package com.zaneschepke.wireguardautotunnel.service.foreground
import android.app.Service import android.app.Service
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.module.IoDispatcher
import com.zaneschepke.wireguardautotunnel.util.Constants
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.withContext
import timber.log.Timber import timber.log.Timber
class ServiceManager( object ServiceManager {
private val appDataRepository: AppDataRepository,
@IoDispatcher private val ioDispatcher: CoroutineDispatcher // private
) { // fun <T> Context.isServiceRunning(service: Class<T>) =
// (getSystemService(ACTIVITY_SERVICE) as ActivityManager)
// .runningAppProcesses.any {
// it.processName == service.name
// }
//
// fun <T : Service> getServiceState(
// context: Context,
// cls: Class<T>
// ): ServiceState {
// val isServiceRunning = context.isServiceRunning(cls)
// return if (isServiceRunning) ServiceState.STARTED else ServiceState.STOPPED
// }
private fun <T : Service> actionOnService( private fun <T : Service> actionOnService(
action: Action, action: Action,
context: Context, context: Context,
cls: Class<T>, cls: Class<T>,
extras: Map<String, Int>? = null extras: Map<String, String>? = null
) { ) {
val intent = val intent =
Intent(context, cls).also { Intent(context, cls).also {
@@ -29,79 +37,44 @@ class ServiceManager(
intent.component?.javaClass intent.component?.javaClass
try { try {
when (action) { when (action) {
Action.START_FOREGROUND, Action.STOP_FOREGROUND -> context.startForegroundService( Action.START_FOREGROUND -> {
intent, context.startForegroundService(intent)
) }
Action.START -> {
Action.START, Action.STOP -> context.startService(intent) context.startService(intent)
}
Action.STOP -> context.startService(intent)
} }
} catch (e: Exception) { } catch (e: Exception) {
Timber.e(e.message) Timber.e(e.message)
} }
} }
suspend fun startVpnService( fun startVpnService(context: Context, tunnelConfig: String) {
context: Context,
tunnelId: Int? = null,
isManualStart: Boolean = false
) {
if (isManualStart) onManualStart(tunnelId)
actionOnService( actionOnService(
Action.START, Action.START,
context, context,
WireGuardTunnelService::class.java, WireGuardTunnelService::class.java,
tunnelId?.let { mapOf(Constants.TUNNEL_EXTRA_KEY to it) }, mapOf(context.getString(R.string.tunnel_extras_key) to tunnelConfig),
) )
} }
suspend fun stopVpnServiceForeground(context: Context, isManualStop: Boolean = false) { fun stopVpnService(context: Context) {
withContext(ioDispatcher) { Timber.d("Stopping vpn service action")
if (isManualStop) onManualStop() actionOnService(
Timber.i("Stopping vpn service") Action.STOP,
actionOnService( context,
Action.STOP_FOREGROUND, WireGuardTunnelService::class.java,
context, )
WireGuardTunnelService::class.java,
)
}
} }
suspend fun stopVpnService(context: Context, isManualStop: Boolean = false) { fun startVpnServiceForeground(context: Context, tunnelConfig: String) {
withContext(ioDispatcher) { actionOnService(
if (isManualStop) onManualStop() Action.START_FOREGROUND,
Timber.i("Stopping vpn service") context,
actionOnService( WireGuardTunnelService::class.java,
Action.STOP, mapOf(context.getString(R.string.tunnel_extras_key) to tunnelConfig),
context, )
WireGuardTunnelService::class.java,
)
}
}
private suspend fun onManualStop() {
appDataRepository.appState.setManualStop()
}
private suspend fun onManualStart(tunnelId: Int?) {
tunnelId?.let {
appDataRepository.appState.setTunnelRunningFromManualStart(it)
}
}
suspend fun startVpnServiceForeground(
context: Context,
tunnelId: Int? = null,
isManualStart: Boolean = false
) {
withContext(ioDispatcher) {
if (isManualStart) onManualStart(tunnelId)
actionOnService(
Action.START_FOREGROUND,
context,
WireGuardTunnelService::class.java,
tunnelId?.let { mapOf(Constants.TUNNEL_EXTRA_KEY to it) },
)
}
} }
fun startWatcherServiceForeground( fun startWatcherServiceForeground(
@@ -0,0 +1,6 @@
package com.zaneschepke.wireguardautotunnel.service.foreground
enum class ServiceState {
STARTED,
STOPPED,
}
@@ -1,56 +0,0 @@
package com.zaneschepke.wireguardautotunnel.service.foreground
import com.zaneschepke.wireguardautotunnel.data.domain.Settings
data class WatcherState(
val isWifiConnected: Boolean = false,
val isEthernetConnected: Boolean = false,
val isMobileDataConnected: Boolean = false,
val currentNetworkSSID: String = "",
val settings: Settings = Settings()
) {
fun isEthernetConditionMet(): Boolean {
return (isEthernetConnected &&
settings.isTunnelOnEthernetEnabled)
}
fun isMobileDataConditionMet(): Boolean {
return (!isEthernetConnected &&
settings.isTunnelOnMobileDataEnabled &&
!isWifiConnected &&
isMobileDataConnected)
}
fun isTunnelOffOnMobileDataConditionMet(): Boolean {
return (!isEthernetConnected &&
!settings.isTunnelOnMobileDataEnabled &&
isMobileDataConnected &&
!isWifiConnected)
}
fun isUntrustedWifiConditionMet(): Boolean {
return (!isEthernetConnected &&
isWifiConnected &&
!settings.trustedNetworkSSIDs.contains(currentNetworkSSID) &&
settings.isTunnelOnWifiEnabled)
}
fun isTrustedWifiConditionMet(): Boolean {
return (!isEthernetConnected &&
(isWifiConnected &&
settings.trustedNetworkSSIDs.contains(currentNetworkSSID)))
}
fun isTunnelOffOnWifiConditionMet(): Boolean {
return (!isEthernetConnected &&
(isWifiConnected &&
!settings.isTunnelOnWifiEnabled))
}
fun isTunnelOffOnNoConnectivityMet(): Boolean {
return (!isEthernetConnected &&
!isWifiConnected &&
!isMobileDataConnected)
}
}
@@ -1,84 +1,74 @@
package com.zaneschepke.wireguardautotunnel.service.foreground package com.zaneschepke.wireguardautotunnel.service.foreground
import android.app.AlarmManager
import android.app.PendingIntent
import android.content.Context import android.content.Context
import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.os.PowerManager import android.os.PowerManager
import android.os.SystemClock
import androidx.core.app.ServiceCompat import androidx.core.app.ServiceCompat
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import com.wireguard.android.backend.Tunnel
import com.zaneschepke.wireguardautotunnel.R import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig import com.zaneschepke.wireguardautotunnel.data.model.Settings
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository import com.zaneschepke.wireguardautotunnel.data.repository.SettingsRepository
import com.zaneschepke.wireguardautotunnel.module.IoDispatcher
import com.zaneschepke.wireguardautotunnel.module.MainImmediateDispatcher
import com.zaneschepke.wireguardautotunnel.service.network.EthernetService import com.zaneschepke.wireguardautotunnel.service.network.EthernetService
import com.zaneschepke.wireguardautotunnel.service.network.MobileDataService import com.zaneschepke.wireguardautotunnel.service.network.MobileDataService
import com.zaneschepke.wireguardautotunnel.service.network.NetworkService import com.zaneschepke.wireguardautotunnel.service.network.NetworkService
import com.zaneschepke.wireguardautotunnel.service.network.NetworkStatus import com.zaneschepke.wireguardautotunnel.service.network.NetworkStatus
import com.zaneschepke.wireguardautotunnel.service.network.WifiService import com.zaneschepke.wireguardautotunnel.service.network.WifiService
import com.zaneschepke.wireguardautotunnel.service.notification.NotificationService import com.zaneschepke.wireguardautotunnel.service.notification.NotificationService
import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelState
import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnService import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnService
import com.zaneschepke.wireguardautotunnel.util.Constants import com.zaneschepke.wireguardautotunnel.util.Constants
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.CancellationException import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import timber.log.Timber import timber.log.Timber
import java.net.InetAddress
import javax.inject.Inject import javax.inject.Inject
@AndroidEntryPoint @AndroidEntryPoint
class WireGuardConnectivityWatcherService : ForegroundService() { class WireGuardConnectivityWatcherService : ForegroundService() {
private val foregroundId = 122 private val foregroundId = 122
@Inject @Inject lateinit var wifiService: NetworkService<WifiService>
lateinit var wifiService: NetworkService<WifiService>
@Inject @Inject lateinit var mobileDataService: NetworkService<MobileDataService>
lateinit var mobileDataService: NetworkService<MobileDataService>
@Inject @Inject lateinit var ethernetService: NetworkService<EthernetService>
lateinit var ethernetService: NetworkService<EthernetService>
@Inject @Inject lateinit var settingsRepository: SettingsRepository
lateinit var appDataRepository: AppDataRepository
@Inject @Inject lateinit var notificationService: NotificationService
lateinit var notificationService: NotificationService
@Inject @Inject lateinit var vpnService: VpnService
lateinit var vpnService: VpnService
@Inject
lateinit var serviceManager: ServiceManager
@Inject
@IoDispatcher
lateinit var ioDispatcher: CoroutineDispatcher
@Inject
@MainImmediateDispatcher
lateinit var mainImmediateDispatcher: CoroutineDispatcher
private val networkEventsFlow = MutableStateFlow(WatcherState()) private val networkEventsFlow = MutableStateFlow(WatcherState())
private var watcherJob: Job? = null data class WatcherState(
val isWifiConnected: Boolean = false,
val isVpnConnected: Boolean = false,
val isEthernetConnected: Boolean = false,
val isMobileDataConnected: Boolean = false,
val currentNetworkSSID: String = "",
val settings: Settings = Settings()
)
private lateinit var watcherJob: Job
private var wakeLock: PowerManager.WakeLock? = null private var wakeLock: PowerManager.WakeLock? = null
private val tag = this.javaClass.name private val tag = this.javaClass.name
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
lifecycleScope.launch(mainImmediateDispatcher) { lifecycleScope.launch(Dispatchers.Main) {
try { try {
if (appDataRepository.settings.getSettings().isAutoTunnelPaused) { if (settingsRepository.getSettings().isAutoTunnelPaused) {
launchWatcherPausedNotification() launchWatcherPausedNotification()
} else launchWatcherNotification() } else launchWatcherNotification()
} catch (e: Exception) { } catch (e: Exception) {
@@ -99,8 +89,8 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
} }
} }
override fun stopService() { override fun stopService(extras: Bundle?) {
super.stopService() super.stopService(extras)
wakeLock?.let { wakeLock?.let {
if (it.isHeld) { if (it.isHeld) {
it.release() it.release()
@@ -132,322 +122,270 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
launchWatcherNotification(getString(R.string.watcher_notification_text_paused)) launchWatcherNotification(getString(R.string.watcher_notification_text_paused))
} }
private fun initWakeLock() { // TODO could this be restarting service in a bad state?
// try to start task again if killed
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,
)
}
private suspend fun initWakeLock() {
val isBatterySaverOn =
withContext(lifecycleScope.coroutineContext) {
settingsRepository.getSettings().isBatterySaverEnabled
}
wakeLock = wakeLock =
(getSystemService(Context.POWER_SERVICE) as PowerManager).run { (getSystemService(Context.POWER_SERVICE) as PowerManager).run {
newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "$tag::lock").apply { newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "$tag::lock").apply {
try { if (isBatterySaverOn) {
Timber.i("Initiating wakelock with 10 min timeout") Timber.d("Initiating wakelock with timeout")
acquire(Constants.BATTERY_SAVER_WATCHER_WAKE_LOCK_TIMEOUT) acquire(Constants.BATTERY_SAVER_WATCHER_WAKE_LOCK_TIMEOUT)
} finally { } else {
release() Timber.d("Initiating wakelock with zero timeout")
acquire(Constants.DEFAULT_WATCHER_WAKE_LOCK_TIMEOUT)
} }
} }
} }
} }
private fun cancelWatcherJob() { private fun cancelWatcherJob() {
try { if (this::watcherJob.isInitialized) {
watcherJob?.cancel() watcherJob.cancel()
} catch (e: CancellationException) {
Timber.i("Watcher job cancelled")
} }
} }
private fun startWatcherJob() { private fun startWatcherJob() {
watcherJob = watcherJob =
lifecycleScope.launch { lifecycleScope.launch(Dispatchers.IO) {
val setting = appDataRepository.settings.getSettings() val setting = settingsRepository.getSettings()
launch { launch {
Timber.i("Starting wifi watcher") Timber.d("Starting wifi watcher")
watchForWifiConnectivityChanges() watchForWifiConnectivityChanges()
} }
if (setting.isTunnelOnMobileDataEnabled) { if (setting.isTunnelOnMobileDataEnabled) {
launch { launch {
Timber.i("Starting mobile data watcher") Timber.d("Starting mobile data watcher")
watchForMobileDataConnectivityChanges() watchForMobileDataConnectivityChanges()
} }
} }
if (setting.isTunnelOnEthernetEnabled) { if (setting.isTunnelOnEthernetEnabled) {
launch { launch {
Timber.i("Starting ethernet data watcher") Timber.d("Starting ethernet data watcher")
watchForEthernetConnectivityChanges() watchForEthernetConnectivityChanges()
} }
} }
launch { launch {
Timber.i("Starting settings watcher") Timber.d("Starting vpn state watcher")
watchForSettingsChanges() watchForVpnConnectivityChanges()
}
if (setting.isPingEnabled) {
launch {
Timber.i("Starting ping watcher")
watchForPingFailure()
}
} }
launch { launch {
Timber.i("Starting management watcher") Timber.d("Starting settings watcher")
watchForSettingsChanges()
}
launch {
Timber.d("Starting management watcher")
manageVpn() manageVpn()
} }
} }
} }
private suspend fun watchForMobileDataConnectivityChanges() { private suspend fun watchForMobileDataConnectivityChanges() {
withContext(ioDispatcher) { mobileDataService.networkStatus.collect {
mobileDataService.networkStatus.collect { status -> when (it) {
when (status) { is NetworkStatus.Available -> {
is NetworkStatus.Available -> { Timber.d("Gained Mobile data connection")
Timber.i("Gained Mobile data connection") networkEventsFlow.value =
networkEventsFlow.update { networkEventsFlow.value.copy(
it.copy( isMobileDataConnected = true,
isMobileDataConnected = true, )
) }
} is NetworkStatus.CapabilitiesChanged -> {
} networkEventsFlow.value =
networkEventsFlow.value.copy(
is NetworkStatus.CapabilitiesChanged -> { isMobileDataConnected = true,
networkEventsFlow.update { )
it.copy( Timber.d("Mobile data capabilities changed")
isMobileDataConnected = true, }
) is NetworkStatus.Unavailable -> {
} networkEventsFlow.value =
Timber.i("Mobile data capabilities changed") networkEventsFlow.value.copy(
} isMobileDataConnected = false,
)
is NetworkStatus.Unavailable -> { Timber.d("Lost mobile data connection")
networkEventsFlow.update {
it.copy(
isMobileDataConnected = false,
)
}
Timber.i("Lost mobile data connection")
}
} }
}
}
}
private suspend fun watchForPingFailure() {
val context = this
withContext(ioDispatcher) {
try {
do {
if (vpnService.vpnState.value.status == TunnelState.UP) {
val tunnelConfig = vpnService.vpnState.value.tunnelConfig
tunnelConfig?.let {
val config = TunnelConfig.configFromWgQuick(it.wgQuick)
val results = config.peers.map { peer ->
val host = if (peer.endpoint.isPresent &&
peer.endpoint.get().resolved.isPresent)
peer.endpoint.get().resolved.get().host
else Constants.DEFAULT_PING_IP
Timber.i("Checking reachability of: $host")
val reachable = InetAddress.getByName(host)
.isReachable(Constants.PING_TIMEOUT.toInt())
Timber.i("Result: reachable - $reachable")
reachable
}
if (results.contains(false)) {
Timber.i("Restarting VPN for ping failure")
serviceManager.stopVpnServiceForeground(context)
delay(Constants.VPN_RESTART_DELAY)
serviceManager.startVpnServiceForeground(context, it.id)
delay(Constants.PING_COOLDOWN)
}
}
}
delay(Constants.PING_INTERVAL)
} while (true)
} catch (e: Exception) {
Timber.e(e)
} }
} }
} }
private suspend fun watchForSettingsChanges() { private suspend fun watchForSettingsChanges() {
appDataRepository.settings.getSettingsFlow().collect { settings -> settingsRepository.getSettingsFlow().collect {
if (networkEventsFlow.value.settings.isAutoTunnelPaused != settings.isAutoTunnelPaused) { if (networkEventsFlow.value.settings.isAutoTunnelPaused != it.isAutoTunnelPaused) {
when (settings.isAutoTunnelPaused) { when (it.isAutoTunnelPaused) {
true -> launchWatcherPausedNotification() true -> launchWatcherPausedNotification()
false -> launchWatcherNotification() false -> launchWatcherNotification()
} }
} }
networkEventsFlow.update { networkEventsFlow.value =
it.copy( networkEventsFlow.value.copy(
settings = settings, settings = it,
) )
}
}
private suspend fun watchForVpnConnectivityChanges() {
vpnService.vpnState.collect {
when (it.status) {
Tunnel.State.DOWN ->
networkEventsFlow.value =
networkEventsFlow.value.copy(
isVpnConnected = false,
)
Tunnel.State.UP ->
networkEventsFlow.value =
networkEventsFlow.value.copy(
isVpnConnected = true,
)
else -> {}
} }
} }
} }
private suspend fun watchForEthernetConnectivityChanges() { private suspend fun watchForEthernetConnectivityChanges() {
withContext(ioDispatcher) { ethernetService.networkStatus.collect {
ethernetService.networkStatus.collect { status -> when (it) {
when (status) { is NetworkStatus.Available -> {
is NetworkStatus.Available -> { Timber.d("Gained Ethernet connection")
Timber.i("Gained Ethernet connection") networkEventsFlow.value =
networkEventsFlow.update { networkEventsFlow.value.copy(
it.copy( isEthernetConnected = true,
isEthernetConnected = true, )
) }
} is NetworkStatus.CapabilitiesChanged -> {
} Timber.d("Ethernet capabilities changed")
networkEventsFlow.value =
is NetworkStatus.CapabilitiesChanged -> { networkEventsFlow.value.copy(
Timber.i("Ethernet capabilities changed") isEthernetConnected = true,
networkEventsFlow.update { )
it.copy( }
isEthernetConnected = true, is NetworkStatus.Unavailable -> {
) networkEventsFlow.value =
} networkEventsFlow.value.copy(
} isEthernetConnected = false,
)
is NetworkStatus.Unavailable -> { Timber.d("Lost Ethernet connection")
networkEventsFlow.update {
it.copy(
isEthernetConnected = false,
)
}
Timber.i("Lost Ethernet connection")
}
} }
} }
} }
} }
private suspend fun watchForWifiConnectivityChanges() { private suspend fun watchForWifiConnectivityChanges() {
withContext(ioDispatcher) { wifiService.networkStatus.collect {
wifiService.networkStatus.collect { status -> when (it) {
when (status) { is NetworkStatus.Available -> {
is NetworkStatus.Available -> { Timber.d("Gained Wi-Fi connection")
Timber.i("Gained Wi-Fi connection") networkEventsFlow.value =
networkEventsFlow.update { networkEventsFlow.value.copy(
it.copy( isWifiConnected = true,
isWifiConnected = true, )
) }
} is NetworkStatus.CapabilitiesChanged -> {
} Timber.d("Wifi capabilities changed")
networkEventsFlow.value =
is NetworkStatus.CapabilitiesChanged -> { networkEventsFlow.value.copy(
Timber.i("Wifi capabilities changed") isWifiConnected = true,
networkEventsFlow.update { )
it.copy( val ssid = wifiService.getNetworkName(it.networkCapabilities) ?: ""
isWifiConnected = true, Timber.d("Detected SSID: $ssid")
) networkEventsFlow.value =
} networkEventsFlow.value.copy(
val ssid = wifiService.getNetworkName(status.networkCapabilities) currentNetworkSSID = ssid,
ssid?.let { name -> )
if (name.contains(Constants.UNREADABLE_SSID)) { }
Timber.w("SSID unreadable: missing permissions") is NetworkStatus.Unavailable -> {
} else Timber.i("Detected valid SSID") networkEventsFlow.value =
appDataRepository.appState.setCurrentSsid(name) networkEventsFlow.value.copy(
networkEventsFlow.update { isWifiConnected = false,
it.copy( )
currentNetworkSSID = name, Timber.d("Lost Wi-Fi connection")
)
}
} ?: Timber.w("Failed to read ssid")
}
is NetworkStatus.Unavailable -> {
networkEventsFlow.update {
it.copy(
isWifiConnected = false,
)
}
Timber.i("Lost Wi-Fi connection")
}
} }
} }
} }
} }
private suspend fun getMobileDataTunnel(): TunnelConfig? { // TODO clean this up
return appDataRepository.tunnels.findByMobileDataTunnel().firstOrNull()
}
private suspend fun getSsidTunnel(ssid: String): TunnelConfig? {
return appDataRepository.tunnels.findByTunnelNetworksName(ssid).firstOrNull()
}
private fun isTunnelDown(): Boolean {
return vpnService.vpnState.value.status == TunnelState.DOWN
}
private suspend fun manageVpn() { private suspend fun manageVpn() {
val context = this networkEventsFlow.collectLatest {
withContext(ioDispatcher) { Timber.i("New watcher state: $it")
networkEventsFlow.collectLatest { watcherState -> if (!it.settings.isAutoTunnelPaused && it.settings.defaultTunnel != null) {
val autoTunnel = "Auto-tunnel watcher" delay(Constants.TOGGLE_TUNNEL_DELAY)
if (!watcherState.settings.isAutoTunnelPaused) { when {
//delay for rapid network state changes and then collect latest ((it.isEthernetConnected &&
delay(Constants.WATCHER_COLLECTION_DELAY) it.settings.isTunnelOnEthernetEnabled &&
val tunnelConfig = vpnService.vpnState.value.tunnelConfig !it.isVpnConnected)) -> {
when { ServiceManager.startVpnServiceForeground(this, it.settings.defaultTunnel!!)
watcherState.isEthernetConditionMet() -> { Timber.i("Condition 1 met")
Timber.i("$autoTunnel - tunnel on on ethernet condition met") }
if (isTunnelDown()) serviceManager.startVpnServiceForeground(context) (!it.isEthernetConnected &&
} it.settings.isTunnelOnMobileDataEnabled &&
!it.isWifiConnected &&
watcherState.isMobileDataConditionMet() -> { it.isMobileDataConnected &&
Timber.i("$autoTunnel - tunnel on mobile data condition met") !it.isVpnConnected) -> {
val mobileDataTunnel = getMobileDataTunnel() ServiceManager.startVpnServiceForeground(this, it.settings.defaultTunnel!!)
val tunnel = Timber.i("Condition 2 met")
mobileDataTunnel ?: appDataRepository.getPrimaryOrFirstTunnel() }
if (isTunnelDown() || tunnelConfig?.isMobileDataTunnel == false) { (!it.isEthernetConnected &&
serviceManager.startVpnServiceForeground( !it.settings.isTunnelOnMobileDataEnabled &&
context, !it.isWifiConnected &&
tunnel?.id, it.isVpnConnected) -> {
) ServiceManager.stopVpnService(this)
} Timber.i("Condition 3 met")
} }
(!it.isEthernetConnected &&
watcherState.isTunnelOffOnMobileDataConditionMet() -> { it.isWifiConnected &&
Timber.i("$autoTunnel - tunnel off on mobile data met, turning vpn off") !it.settings.trustedNetworkSSIDs.contains(it.currentNetworkSSID) &&
if (!isTunnelDown()) serviceManager.stopVpnServiceForeground(context) it.settings.isTunnelOnWifiEnabled &&
} (!it.isVpnConnected)) -> {
ServiceManager.startVpnServiceForeground(this, it.settings.defaultTunnel!!)
watcherState.isUntrustedWifiConditionMet() -> { Timber.i("Condition 4 met")
if (tunnelConfig?.tunnelNetworks?.contains(watcherState.currentNetworkSSID) == false || }
tunnelConfig == null) { (!it.isEthernetConnected &&
Timber.i("$autoTunnel - tunnel on ssid not associated with current tunnel condition met") (it.isWifiConnected &&
getSsidTunnel(watcherState.currentNetworkSSID)?.let { it.settings.trustedNetworkSSIDs.contains(it.currentNetworkSSID)) &&
Timber.i("Found tunnel associated with this SSID, bringing tunnel up: ${it.name}") (it.isVpnConnected)) -> {
if (isTunnelDown() || tunnelConfig?.id != it.id) serviceManager.startVpnServiceForeground( ServiceManager.stopVpnService(this)
context, Timber.i("Condition 5 met")
it.id, }
) (!it.isEthernetConnected &&
} ?: suspend { (it.isWifiConnected &&
Timber.i("No tunnel associated with this SSID, using defaults") !it.settings.isTunnelOnWifiEnabled &&
val default = appDataRepository.getPrimaryOrFirstTunnel() (it.isVpnConnected))) -> {
if (default?.name != vpnService.name) { ServiceManager.stopVpnService(this)
default?.let { Timber.i("Condition 6 met")
serviceManager.startVpnServiceForeground(context, it.id) }
} (!it.isEthernetConnected &&
!it.isWifiConnected &&
} !it.isMobileDataConnected &&
}.invoke() (it.isVpnConnected)) -> {
} ServiceManager.stopVpnService(this)
} Timber.i("Condition 7 met")
}
watcherState.isTrustedWifiConditionMet() -> { else -> {
Timber.i("$autoTunnel - tunnel off on trusted wifi condition met, turning vpn off") Timber.i("No condition met")
if (!isTunnelDown()) serviceManager.stopVpnServiceForeground(context)
}
watcherState.isTunnelOffOnWifiConditionMet() -> {
Timber.i("$autoTunnel - tunnel off on wifi condition met, turning vpn off")
if (!isTunnelDown()) serviceManager.stopVpnServiceForeground(context)
}
watcherState.isTunnelOffOnNoConnectivityMet() -> {
Timber.i("$autoTunnel - tunnel off on no connectivity met, turning vpn off")
if (!isTunnelDown()) serviceManager.stopVpnServiceForeground(context)
}
else -> {
Timber.i("$autoTunnel - no condition met")
}
} }
} }
} }
@@ -6,24 +6,21 @@ import android.os.Bundle
import androidx.core.app.ServiceCompat import androidx.core.app.ServiceCompat
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import com.zaneschepke.wireguardautotunnel.R import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
import com.zaneschepke.wireguardautotunnel.module.IoDispatcher import com.zaneschepke.wireguardautotunnel.data.repository.SettingsRepository
import com.zaneschepke.wireguardautotunnel.module.MainImmediateDispatcher import com.zaneschepke.wireguardautotunnel.data.repository.TunnelConfigRepository
import com.zaneschepke.wireguardautotunnel.receiver.NotificationActionReceiver import com.zaneschepke.wireguardautotunnel.receiver.NotificationActionReceiver
import com.zaneschepke.wireguardautotunnel.service.notification.NotificationService import com.zaneschepke.wireguardautotunnel.service.notification.NotificationService
import com.zaneschepke.wireguardautotunnel.service.tunnel.HandshakeStatus import com.zaneschepke.wireguardautotunnel.service.tunnel.HandshakeStatus
import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelState
import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnService import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnService
import com.zaneschepke.wireguardautotunnel.util.Constants import com.zaneschepke.wireguardautotunnel.util.Constants
import com.zaneschepke.wireguardautotunnel.util.handshakeStatus import com.zaneschepke.wireguardautotunnel.util.handshakeStatus
import com.zaneschepke.wireguardautotunnel.util.mapPeerStats import com.zaneschepke.wireguardautotunnel.util.mapPeerStats
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.CancellationException import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
@@ -31,32 +28,23 @@ import javax.inject.Inject
class WireGuardTunnelService : ForegroundService() { class WireGuardTunnelService : ForegroundService() {
private val foregroundId = 123 private val foregroundId = 123
@Inject @Inject lateinit var vpnService: VpnService
lateinit var vpnService: VpnService
@Inject @Inject lateinit var settingsRepository: SettingsRepository
lateinit var appDataRepository: AppDataRepository
@Inject @Inject lateinit var tunnelConfigRepository: TunnelConfigRepository
lateinit var notificationService: NotificationService
@Inject @Inject lateinit var notificationService: NotificationService
@MainImmediateDispatcher
lateinit var mainImmediateDispatcher: CoroutineDispatcher
@Inject private lateinit var job: Job
@IoDispatcher
lateinit var ioDispatcher: CoroutineDispatcher
private var job: Job? = null
private var tunnelName: String = ""
private var didShowConnected = false private var didShowConnected = false
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
lifecycleScope.launch(mainImmediateDispatcher) { lifecycleScope.launch(Dispatchers.Main) {
//TODO fix this to not launch if AOVPN if (tunnelConfigRepository.getAll().isNotEmpty()) {
if (appDataRepository.tunnels.count() != 0) {
launchVpnNotification() launchVpnNotification()
} }
} }
@@ -65,76 +53,72 @@ class WireGuardTunnelService : ForegroundService() {
override fun startService(extras: Bundle?) { override fun startService(extras: Bundle?) {
super.startService(extras) super.startService(extras)
cancelJob() cancelJob()
val tunnelConfigString = extras?.getString(getString(R.string.tunnel_extras_key))
val tunnelConfig = tunnelConfigString?.let { TunnelConfig.from(it) }
tunnelName = tunnelConfig?.name ?: ""
job = job =
lifecycleScope.launch { lifecycleScope.launch(Dispatchers.IO) {
launch { launch {
val tunnelId = extras?.getInt(Constants.TUNNEL_EXTRA_KEY) if (tunnelConfig != null) {
if (vpnService.getState() == TunnelState.UP) { try {
vpnService.stopTunnel() tunnelName = tunnelConfig.name
} vpnService.startTunnel(tunnelConfig)
vpnService.startTunnel( } catch (e: Exception) {
tunnelId?.let { Timber.e("Problem starting tunnel: ${e.message}")
appDataRepository.tunnels.getById(it) stopService(extras)
}, }
) } else {
} Timber.d("Tunnel config null, starting default tunnel or first")
launch { val settings = settingsRepository.getSettings()
handshakeNotifications() val tunnels = tunnelConfigRepository.getAll()
} if (settings.isAlwaysOnVpnEnabled) {
} val tunnel =
} if (settings.defaultTunnel != null) {
TunnelConfig.from(settings.defaultTunnel!!)
//TODO improve tunnel notifications } else if (tunnels.isNotEmpty()) {
private suspend fun handshakeNotifications() { tunnels.first()
withContext(ioDispatcher) { } else {
var tunnelName: String? = null null
vpnService.vpnState.collect { state ->
state.statistics
?.mapPeerStats()
?.map { it.value?.handshakeStatus() }
.let { statuses ->
when {
statuses?.all { it == HandshakeStatus.HEALTHY } == true -> {
if (!didShowConnected) {
delay(Constants.VPN_CONNECTED_NOTIFICATION_DELAY)
tunnelName = state.tunnelConfig?.name
launchVpnNotification(
getString(R.string.tunnel_start_title),
"${getString(R.string.tunnel_start_text)} - $tunnelName",
)
didShowConnected = true
} }
if (tunnel != null) {
tunnelName = tunnel.name
vpnService.startTunnel(tunnel)
} }
statuses?.any { it == HandshakeStatus.STALE } == true -> {}
statuses?.all { it == HandshakeStatus.NOT_STARTED } ==
true -> {
}
else -> {}
} }
} }
if (state.status == TunnelState.UP && state.tunnelConfig?.name != tunnelName) { }
tunnelName = state.tunnelConfig?.name // TODO add failed to connect notification
launchVpnNotification( launch {
getString(R.string.tunnel_start_title), vpnService.vpnState.collect { state ->
"${getString(R.string.tunnel_start_text)} - $tunnelName", state.statistics
) ?.mapPeerStats()
?.map { it.value?.handshakeStatus() }
.let { statuses ->
when {
statuses?.all { it == HandshakeStatus.HEALTHY } == true -> {
if (!didShowConnected) {
delay(Constants.VPN_CONNECTED_NOTIFICATION_DELAY)
launchVpnNotification(
getString(R.string.tunnel_start_title),
"${getString(R.string.tunnel_start_text)} $tunnelName",
)
didShowConnected = true
}
}
statuses?.any { it == HandshakeStatus.STALE } == true -> {}
statuses?.all { it == HandshakeStatus.NOT_STARTED } ==
true -> {}
else -> {}
}
}
}
} }
} }
}
} }
private fun launchAlwaysOnDisabledNotification() { override fun stopService(extras: Bundle?) {
launchVpnNotification( super.stopService(extras)
title = this.getString(R.string.vpn_connection_failed), lifecycleScope.launch(Dispatchers.IO) {
description = this.getString(R.string.always_on_disabled),
)
}
override fun stopService() {
super.stopService()
lifecycleScope.launch {
vpnService.stopTunnel() vpnService.stopTunnel()
didShowConnected = false didShowConnected = false
} }
@@ -170,12 +154,12 @@ class WireGuardTunnelService : ForegroundService() {
channelId = getString(R.string.vpn_channel_id), channelId = getString(R.string.vpn_channel_id),
channelName = getString(R.string.vpn_channel_name), channelName = getString(R.string.vpn_channel_name),
action = action =
PendingIntent.getBroadcast( PendingIntent.getBroadcast(
this, this,
0, 0,
Intent(this, NotificationActionReceiver::class.java), Intent(this, NotificationActionReceiver::class.java),
PendingIntent.FLAG_IMMUTABLE, PendingIntent.FLAG_IMMUTABLE,
), ),
actionText = getString(R.string.restart), actionText = getString(R.string.restart),
title = getString(R.string.vpn_connection_failed), title = getString(R.string.vpn_connection_failed),
onGoing = false, onGoing = false,
@@ -192,10 +176,8 @@ class WireGuardTunnelService : ForegroundService() {
} }
private fun cancelJob() { private fun cancelJob() {
try { if (this::job.isInitialized) {
job?.cancel() job.cancel()
} catch (e: CancellationException) {
Timber.i("Tunnel job cancelled")
} }
} }
} }
@@ -53,7 +53,6 @@ abstract class BaseNetworkService<T : BaseNetworkService<T>>(
} }
} }
} }
else -> { else -> {
object : ConnectivityManager.NetworkCallback() { object : ConnectivityManager.NetworkCallback() {
override fun onAvailable(network: Network) { override fun onAvailable(network: Network) {
@@ -118,7 +117,7 @@ inline fun <Result> Flow<NetworkStatus>.map(
crossinline onUnavailable: suspend (network: Network) -> Result, crossinline onUnavailable: suspend (network: Network) -> Result,
crossinline onAvailable: suspend (network: Network) -> Result, crossinline onAvailable: suspend (network: Network) -> Result,
crossinline onCapabilitiesChanged: crossinline onCapabilitiesChanged:
suspend (network: Network, networkCapabilities: NetworkCapabilities) -> Result suspend (network: Network, networkCapabilities: NetworkCapabilities) -> Result
): Flow<Result> = map { status -> ): Flow<Result> = map { status ->
when (status) { when (status) {
is NetworkStatus.Unavailable -> onUnavailable(status.network) is NetworkStatus.Unavailable -> onUnavailable(status.network)
@@ -10,7 +10,6 @@ import android.graphics.Color
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
import com.zaneschepke.wireguardautotunnel.R import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.ui.MainActivity import com.zaneschepke.wireguardautotunnel.ui.MainActivity
import com.zaneschepke.wireguardautotunnel.ui.SplashActivity
import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.qualifiers.ApplicationContext
import javax.inject.Inject import javax.inject.Inject
@@ -46,10 +45,10 @@ class WireGuardNotification @Inject constructor(@ApplicationContext private val
): Notification { ): Notification {
val channel = val channel =
NotificationChannel( NotificationChannel(
channelId, channelId,
channelName, channelName,
importance, importance,
) )
.let { .let {
it.description = title it.description = title
it.enableLights(lights) it.enableLights(lights)
@@ -60,7 +59,7 @@ class WireGuardNotification @Inject constructor(@ApplicationContext private val
} }
notificationManager.createNotificationChannel(channel) notificationManager.createNotificationChannel(channel)
val pendingIntent: PendingIntent = val pendingIntent: PendingIntent =
Intent(context, SplashActivity::class.java).let { notificationIntent -> Intent(context, MainActivity::class.java).let { notificationIntent ->
PendingIntent.getActivity( PendingIntent.getActivity(
context, context,
0, 0,
@@ -95,7 +94,7 @@ class WireGuardNotification @Inject constructor(@ApplicationContext private val
.setOngoing(onGoing) .setOngoing(onGoing)
.setPriority(NotificationCompat.PRIORITY_HIGH) .setPriority(NotificationCompat.PRIORITY_HIGH)
.setShowWhen(showTimestamp) .setShowWhen(showTimestamp)
.setSmallIcon(R.drawable.ic_launcher) .setSmallIcon(R.mipmap.ic_launcher_foreground)
.build() .build()
} }
} }
@@ -1,70 +1,80 @@
package com.zaneschepke.wireguardautotunnel.service.shortcut package com.zaneschepke.wireguardautotunnel.service.shortcut
import android.os.Bundle import android.os.Bundle
import android.view.View
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository import androidx.lifecycle.lifecycleScope
import com.zaneschepke.wireguardautotunnel.module.ApplicationScope import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
import com.zaneschepke.wireguardautotunnel.data.repository.SettingsRepository
import com.zaneschepke.wireguardautotunnel.data.repository.TunnelConfigRepository
import com.zaneschepke.wireguardautotunnel.service.foreground.Action import com.zaneschepke.wireguardautotunnel.service.foreground.Action
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
import com.zaneschepke.wireguardautotunnel.service.foreground.WireGuardConnectivityWatcherService
import com.zaneschepke.wireguardautotunnel.service.foreground.WireGuardTunnelService import com.zaneschepke.wireguardautotunnel.service.foreground.WireGuardTunnelService
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
@AndroidEntryPoint @AndroidEntryPoint
class ShortcutsActivity : ComponentActivity() { class ShortcutsActivity : ComponentActivity() {
@Inject lateinit var settingsRepository: SettingsRepository
@Inject @Inject lateinit var tunnelConfigRepository: TunnelConfigRepository
lateinit var appDataRepository: AppDataRepository
@Inject private suspend fun toggleWatcherServicePause() {
lateinit var serviceManager: ServiceManager val settings = settingsRepository.getSettings()
if (settings.isAutoTunnelEnabled) {
@Inject val pauseAutoTunnel = !settings.isAutoTunnelPaused
@ApplicationScope settingsRepository.save(
lateinit var applicationScope: CoroutineScope settings.copy(
isAutoTunnelPaused = pauseAutoTunnel,
),
)
}
}
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
applicationScope.launch { setContentView(View(this))
val settings = appDataRepository.settings.getSettings() if (
if (settings.isShortcutsEnabled) { intent
when (intent.getStringExtra(CLASS_NAME_EXTRA_KEY)) { .getStringExtra(CLASS_NAME_EXTRA_KEY)
WireGuardTunnelService::class.java.simpleName -> { .equals(WireGuardTunnelService::class.java.simpleName)
) {
lifecycleScope.launch(Dispatchers.Main) {
val settings = settingsRepository.getSettings()
if (settings.isShortcutsEnabled) {
try {
val tunnelName = intent.getStringExtra(TUNNEL_NAME_EXTRA_KEY) val tunnelName = intent.getStringExtra(TUNNEL_NAME_EXTRA_KEY)
val tunnelConfig = tunnelName?.let { val tunnelConfig =
appDataRepository.tunnels.getAll().firstOrNull { if (tunnelName != null) {
it.name == tunnelName tunnelConfigRepository.getAll().firstOrNull {
it.name == tunnelName
}
} else {
if (settings.defaultTunnel == null) {
tunnelConfigRepository.getAll().first()
} else {
TunnelConfig.from(settings.defaultTunnel!!)
}
} }
} tunnelConfig ?: return@launch
toggleWatcherServicePause()
when (intent.action) { when (intent.action) {
Action.START.name -> serviceManager.startVpnServiceForeground( Action.STOP.name ->
this@ShortcutsActivity, tunnelConfig?.id, isManualStart = true, ServiceManager.stopVpnService(
) this@ShortcutsActivity,
)
Action.STOP.name -> serviceManager.stopVpnServiceForeground( Action.START.name ->
this@ShortcutsActivity, ServiceManager.startVpnServiceForeground(
isManualStop = true, this@ShortcutsActivity,
) tunnelConfig.toString(),
} )
}
WireGuardConnectivityWatcherService::class.java.simpleName -> {
when (intent.action) {
Action.START.name -> appDataRepository.settings.save(
settings.copy(
isAutoTunnelPaused = false,
),
)
Action.STOP.name -> appDataRepository.settings.save(
settings.copy(
isAutoTunnelPaused = true,
),
)
} }
} catch (e: Exception) {
Timber.e(e.message)
finish()
} }
} }
} }
@@ -1,107 +0,0 @@
package com.zaneschepke.wireguardautotunnel.service.tile
import android.os.Build
import android.service.quicksettings.Tile
import android.service.quicksettings.TileService
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.ServiceLifecycleDispatcher
import androidx.lifecycle.lifecycleScope
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
import com.zaneschepke.wireguardautotunnel.module.ApplicationScope
import com.zaneschepke.wireguardautotunnel.module.ServiceScope
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject
@AndroidEntryPoint
class AutoTunnelControlTile : TileService(), LifecycleOwner {
@Inject
lateinit var appDataRepository: AppDataRepository
@Inject
lateinit var serviceManager: ServiceManager
private val dispatcher = ServiceLifecycleDispatcher(this)
private var manualStartConfig: TunnelConfig? = null
override fun onStartListening() {
super.onStartListening()
lifecycleScope.launch {
val settings = appDataRepository.settings.getSettings()
when (settings.isAutoTunnelEnabled) {
true -> {
if (settings.isAutoTunnelPaused) {
setInactive()
setTileDescription(this@AutoTunnelControlTile.getString(R.string.paused))
} else {
setActive()
setTileDescription(this@AutoTunnelControlTile.getString(R.string.active))
}
}
false -> {
setTileDescription(this@AutoTunnelControlTile.getString(R.string.disabled))
setUnavailable()
}
}
}
}
override fun onTileAdded() {
super.onTileAdded()
onStartListening()
}
override fun onClick() {
super.onClick()
unlockAndRun {
lifecycleScope.launch {
try {
appDataRepository.toggleWatcherServicePause()
onStartListening()
} catch (e: Exception) {
Timber.e(e.message)
} finally {
cancel()
}
}
}
}
private fun setActive() {
qsTile.state = Tile.STATE_ACTIVE
qsTile.updateTile()
}
private fun setInactive() {
qsTile.state = Tile.STATE_INACTIVE
qsTile.updateTile()
}
private fun setUnavailable() {
manualStartConfig = null
qsTile.state = Tile.STATE_UNAVAILABLE
qsTile.updateTile()
}
private fun setTileDescription(description: String) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
qsTile.subtitle = description
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
qsTile.stateDescription = description
}
qsTile.updateTile()
}
override val lifecycle: Lifecycle
get() = dispatcher.lifecycle
}
@@ -3,83 +3,84 @@ package com.zaneschepke.wireguardautotunnel.service.tile
import android.os.Build import android.os.Build
import android.service.quicksettings.Tile import android.service.quicksettings.Tile
import android.service.quicksettings.TileService import android.service.quicksettings.TileService
import androidx.lifecycle.Lifecycle import com.wireguard.android.backend.Tunnel
import androidx.lifecycle.LifecycleOwner import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
import androidx.lifecycle.LifecycleService import com.zaneschepke.wireguardautotunnel.data.repository.SettingsRepository
import androidx.lifecycle.ServiceLifecycleDispatcher import com.zaneschepke.wireguardautotunnel.data.repository.TunnelConfigRepository
import androidx.lifecycle.lifecycleScope
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
import com.zaneschepke.wireguardautotunnel.module.ApplicationScope
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelState
import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnService import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnService
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.cancel import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
@AndroidEntryPoint @AndroidEntryPoint
class TunnelControlTile : TileService(), LifecycleOwner { class TunnelControlTile() : TileService() {
@Inject @Inject lateinit var tunnelConfigRepository: TunnelConfigRepository
lateinit var appDataRepository: AppDataRepository
@Inject @Inject lateinit var settingsRepository: SettingsRepository
lateinit var vpnService: VpnService
@Inject @Inject lateinit var vpnService: VpnService
lateinit var serviceManager: ServiceManager
private val dispatcher = ServiceLifecycleDispatcher(this) private val scope = CoroutineScope(Dispatchers.IO)
private var manualStartConfig: TunnelConfig? = null private var tunnelName: String? = null
override fun onStartListening() { override fun onStartListening() {
super.onStartListening() super.onStartListening()
Timber.d("On start listening called") Timber.d("On start listening called")
lifecycleScope.launch { scope.launch {
when (vpnService.getState()) { vpnService.vpnState.collect {
TunnelState.UP -> { when (it.status) {
setActive() Tunnel.State.UP -> setActive()
setTileDescription(vpnService.name) Tunnel.State.DOWN -> setInactive()
else -> setInactive()
} }
val tunnels = tunnelConfigRepository.getAll()
TunnelState.DOWN -> { if (tunnels.isEmpty()) {
setInactive() setUnavailable()
val config = appDataRepository.getStartTunnelConfig()?.also { config -> return@collect
manualStartConfig = config
} ?: appDataRepository.getPrimaryOrFirstTunnel()
config?.let {
setTileDescription(it.name)
} ?: setUnavailable()
} }
tunnelName =
else -> setInactive() it.name.ifBlank {
val settings = settingsRepository.getSettings()
if (settings.defaultTunnel != null) {
TunnelConfig.from(settings.defaultTunnel!!).name
} else tunnels.firstOrNull()?.name
}
setTileDescription(tunnelName ?: "")
} }
} }
} }
override fun onTileAdded() { override fun onDestroy() {
super.onTileAdded() super.onDestroy()
onStartListening() scope.cancel()
}
override fun onTileRemoved() {
super.onTileRemoved()
scope.cancel()
} }
override fun onClick() { override fun onClick() {
super.onClick() super.onClick()
unlockAndRun { unlockAndRun {
lifecycleScope.launch { scope.launch {
try { try {
if (vpnService.getState() == TunnelState.UP) { val tunnelConfig =
serviceManager.stopVpnServiceForeground( tunnelConfigRepository.getAll().first { it.name == tunnelName }
this@TunnelControlTile, toggleWatcherServicePause()
isManualStop = true, if (vpnService.getState() == Tunnel.State.UP) {
) ServiceManager.stopVpnService(this@TunnelControlTile)
} else { } else {
serviceManager.startVpnServiceForeground( ServiceManager.startVpnServiceForeground(
this@TunnelControlTile, manualStartConfig?.id, isManualStart = true, this@TunnelControlTile,
tunnelConfig.toString(),
) )
} }
} catch (e: Exception) { } catch (e: Exception) {
@@ -91,6 +92,20 @@ class TunnelControlTile : TileService(), LifecycleOwner {
} }
} }
private fun toggleWatcherServicePause() {
scope.launch {
val settings = settingsRepository.getSettings()
if (settings.isAutoTunnelEnabled) {
val pauseAutoTunnel = !settings.isAutoTunnelPaused
settingsRepository.save(
settings.copy(
isAutoTunnelPaused = pauseAutoTunnel,
),
)
}
}
}
private fun setActive() { private fun setActive() {
qsTile.state = Tile.STATE_ACTIVE qsTile.state = Tile.STATE_ACTIVE
qsTile.updateTile() qsTile.updateTile()
@@ -102,7 +117,6 @@ class TunnelControlTile : TileService(), LifecycleOwner {
} }
private fun setUnavailable() { private fun setUnavailable() {
manualStartConfig = null
qsTile.state = Tile.STATE_UNAVAILABLE qsTile.state = Tile.STATE_UNAVAILABLE
qsTile.updateTile() qsTile.updateTile()
} }
@@ -116,7 +130,4 @@ class TunnelControlTile : TileService(), LifecycleOwner {
} }
qsTile.updateTile() qsTile.updateTile()
} }
override val lifecycle: Lifecycle
get() = dispatcher.lifecycle
} }
@@ -1,43 +0,0 @@
package com.zaneschepke.wireguardautotunnel.service.tunnel
import com.wireguard.android.backend.Tunnel
enum class TunnelState {
UP,
DOWN,
TOGGLE;
fun toWgState(): Tunnel.State {
return when (this) {
UP -> Tunnel.State.UP
DOWN -> Tunnel.State.DOWN
TOGGLE -> Tunnel.State.TOGGLE
}
}
fun toAmState(): org.amnezia.awg.backend.Tunnel.State {
return when (this) {
UP -> org.amnezia.awg.backend.Tunnel.State.UP
DOWN -> org.amnezia.awg.backend.Tunnel.State.DOWN
TOGGLE -> org.amnezia.awg.backend.Tunnel.State.TOGGLE
}
}
companion object {
fun from(state: Tunnel.State): TunnelState {
return when (state) {
Tunnel.State.DOWN -> DOWN
Tunnel.State.TOGGLE -> TOGGLE
Tunnel.State.UP -> UP
}
}
fun from(state: org.amnezia.awg.backend.Tunnel.State): TunnelState {
return when (state) {
org.amnezia.awg.backend.Tunnel.State.DOWN -> DOWN
org.amnezia.awg.backend.Tunnel.State.TOGGLE -> TOGGLE
org.amnezia.awg.backend.Tunnel.State.UP -> UP
}
}
}
}
@@ -1,15 +1,15 @@
package com.zaneschepke.wireguardautotunnel.service.tunnel package com.zaneschepke.wireguardautotunnel.service.tunnel
import com.wireguard.android.backend.Tunnel import com.wireguard.android.backend.Tunnel
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
interface VpnService : Tunnel, org.amnezia.awg.backend.Tunnel { interface VpnService : Tunnel {
suspend fun startTunnel(tunnelConfig: TunnelConfig? = null): TunnelState suspend fun startTunnel(tunnelConfig: TunnelConfig): Tunnel.State
suspend fun stopTunnel() suspend fun stopTunnel()
val vpnState: StateFlow<VpnState> val vpnState: StateFlow<VpnState>
fun getState(): TunnelState fun getState(): Tunnel.State
} }
@@ -1,10 +1,10 @@
package com.zaneschepke.wireguardautotunnel.service.tunnel package com.zaneschepke.wireguardautotunnel.service.tunnel
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig import com.wireguard.android.backend.Statistics
import com.zaneschepke.wireguardautotunnel.service.tunnel.statistics.TunnelStatistics import com.wireguard.android.backend.Tunnel
data class VpnState( data class VpnState(
val status: TunnelState = TunnelState.DOWN, val status: Tunnel.State = Tunnel.State.DOWN,
val tunnelConfig: TunnelConfig? = null, val name: String = "",
val statistics: TunnelStatistics? = null val statistics: Statistics? = null
) )
@@ -2,131 +2,82 @@ package com.zaneschepke.wireguardautotunnel.service.tunnel
import com.wireguard.android.backend.Backend import com.wireguard.android.backend.Backend
import com.wireguard.android.backend.BackendException import com.wireguard.android.backend.BackendException
import com.wireguard.android.backend.Statistics
import com.wireguard.android.backend.Tunnel.State import com.wireguard.android.backend.Tunnel.State
import com.wireguard.config.Config
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository import com.zaneschepke.wireguardautotunnel.data.repository.SettingsRepository
import com.zaneschepke.wireguardautotunnel.module.ApplicationScope
import com.zaneschepke.wireguardautotunnel.module.IoDispatcher
import com.zaneschepke.wireguardautotunnel.module.Kernel import com.zaneschepke.wireguardautotunnel.module.Kernel
import com.zaneschepke.wireguardautotunnel.module.Userspace import com.zaneschepke.wireguardautotunnel.module.Userspace
import com.zaneschepke.wireguardautotunnel.service.tunnel.statistics.AmneziaStatistics
import com.zaneschepke.wireguardautotunnel.service.tunnel.statistics.TunnelStatistics
import com.zaneschepke.wireguardautotunnel.service.tunnel.statistics.WireGuardStatistics
import com.zaneschepke.wireguardautotunnel.util.Constants import com.zaneschepke.wireguardautotunnel.util.Constants
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.amnezia.awg.backend.Tunnel
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Provider
class WireGuardTunnel class WireGuardTunnel
@Inject @Inject
constructor( constructor(
private val userspaceAmneziaBackend: Provider<org.amnezia.awg.backend.Backend>, @Userspace private val userspaceBackend: Backend,
@Userspace private val userspaceBackend: Provider<Backend>, @Kernel private val kernelBackend: Backend,
@Kernel private val kernelBackend: Provider<Backend>, private val settingsRepository: SettingsRepository
private val appDataRepository: AppDataRepository,
@ApplicationScope private val applicationScope: CoroutineScope,
@IoDispatcher private val ioDispatcher: CoroutineDispatcher
) : VpnService { ) : VpnService {
private val _vpnState = MutableStateFlow(VpnState()) private val _vpnState = MutableStateFlow(VpnState())
override val vpnState: StateFlow<VpnState> = _vpnState.asStateFlow() override val vpnState: StateFlow<VpnState> = _vpnState.asStateFlow()
private val scope = CoroutineScope(Dispatchers.IO)
private var statsJob: Job? = null private lateinit var statsJob: Job
private var backendIsWgUserspace = true private var config: Config? = null
private var backendIsAmneziaUserspace = false private var backend: Backend = userspaceBackend
private var backendIsUserspace = true
init { init {
applicationScope.launch(ioDispatcher) { scope.launch {
appDataRepository.settings.getSettingsFlow().collect { settingsRepository.getSettingsFlow().collect {
if (it.isKernelEnabled && (backendIsWgUserspace || backendIsAmneziaUserspace)) { if (it.isKernelEnabled && backendIsUserspace) {
Timber.i("Setting kernel backend") Timber.d("Setting kernel backend")
backendIsWgUserspace = false backend = kernelBackend
backendIsAmneziaUserspace = false backendIsUserspace = false
} else if (!it.isKernelEnabled && !it.isAmneziaEnabled && !backendIsWgUserspace) { } else if (!it.isKernelEnabled && !backendIsUserspace) {
Timber.i("Setting WireGuard userspace backend") Timber.d("Setting userspace backend")
backendIsWgUserspace = true backend = userspaceBackend
backendIsAmneziaUserspace = false backendIsUserspace = true
} else if (it.isAmneziaEnabled && !backendIsAmneziaUserspace) {
Timber.i("Setting Amnezia userspace backend")
backendIsAmneziaUserspace = true
backendIsWgUserspace = false
} }
} }
} }
} }
private fun setState(tunnelConfig: TunnelConfig?, tunnelState: TunnelState): TunnelState { override suspend fun startTunnel(tunnelConfig: TunnelConfig): State {
return if (backendIsAmneziaUserspace) { return try {
Timber.i("Using Amnezia backend") stopTunnelOnConfigChange(tunnelConfig)
val config = tunnelConfig?.let { emitTunnelName(tunnelConfig.name)
if (it.amQuick != "") TunnelConfig.configFromAmQuick(it.amQuick) else { config = TunnelConfig.configFromQuick(tunnelConfig.wgQuick)
Timber.w("Using backwards compatible wg config, amnezia specific config not found.")
TunnelConfig.configFromAmQuick(it.wgQuick)
}
}
val state = val state =
userspaceAmneziaBackend.get().setState(this, tunnelState.toAmState(), config) backend.setState(
TunnelState.from(state) this,
} else { State.UP,
Timber.i("Using Wg backend") config,
val wgConfig = tunnelConfig?.let { TunnelConfig.configFromWgQuick(it.wgQuick) } )
val state = backend().setState( emitTunnelState(state)
this, state
tunnelState.toWgState(), } catch (e: Exception) {
wgConfig, Timber.e("Failed to start tunnel with error: ${e.message}")
) State.DOWN
TunnelState.from(state)
} }
} }
override suspend fun startTunnel(tunnelConfig: TunnelConfig?): TunnelState { private fun emitTunnelState(state: State) {
return withContext(ioDispatcher) {
try {
//TODO we need better error handling here
// need to bubble up these errors to the UI
val config = tunnelConfig ?: appDataRepository.getPrimaryOrFirstTunnel()
if (config != null) {
emitTunnelConfig(config)
setState(config, TunnelState.UP)
} else throw Exception("No tunnels")
} catch (e: BackendException) {
Timber.e("Failed to start tunnel with error: ${e.message}")
TunnelState.from(State.DOWN)
}
}
}
private fun backend(): Backend {
return when {
backendIsWgUserspace -> {
userspaceBackend.get()
}
!backendIsWgUserspace && !backendIsAmneziaUserspace -> {
kernelBackend.get()
}
else -> {
userspaceBackend.get()
}
}
}
private fun emitTunnelState(state: TunnelState) {
_vpnState.tryEmit( _vpnState.tryEmit(
_vpnState.value.copy( _vpnState.value.copy(
status = state, status = state,
@@ -134,7 +85,7 @@ constructor(
) )
} }
private fun emitBackendStatistics(statistics: TunnelStatistics) { private fun emitBackendStatistics(statistics: Statistics) {
_vpnState.tryEmit( _vpnState.tryEmit(
_vpnState.value.copy( _vpnState.value.copy(
statistics = statistics, statistics = statistics,
@@ -142,81 +93,57 @@ constructor(
) )
} }
private suspend fun emitTunnelConfig(tunnelConfig: TunnelConfig?) { private suspend fun emitTunnelName(name: String) {
_vpnState.emit( _vpnState.emit(
_vpnState.value.copy( _vpnState.value.copy(
tunnelConfig = tunnelConfig, name = name,
), ),
) )
} }
private fun resetVpnState() { private suspend fun stopTunnelOnConfigChange(tunnelConfig: TunnelConfig) {
_vpnState.tryEmit(VpnState()) if (getState() == State.UP && _vpnState.value.name != tunnelConfig.name) {
} stopTunnel()
override suspend fun stopTunnel() {
withContext(ioDispatcher) {
try {
if (getState() == TunnelState.UP) {
val state = setState(null, TunnelState.DOWN)
resetVpnState()
emitTunnelState(state)
}
} catch (e: BackendException) {
Timber.e("Failed to stop wireguard tunnel with error: ${e.message}")
} catch (e: org.amnezia.awg.backend.BackendException) {
Timber.e("Failed to stop amnezia tunnel with error: ${e.message}")
}
} }
} }
override fun getState(): TunnelState {
return if (backendIsAmneziaUserspace) TunnelState.from(
userspaceAmneziaBackend.get().getState(this),
)
else TunnelState.from(backend().getState(this))
}
override fun getName(): String { override fun getName(): String {
return _vpnState.value.tunnelConfig?.name ?: "" return _vpnState.value.name
} }
override suspend fun stopTunnel() {
override fun onStateChange(newState: Tunnel.State) { try {
handleStateChange(TunnelState.from(newState)) if (getState() == State.UP) {
} val state = backend.setState(this, State.DOWN, null)
emitTunnelState(state)
private fun handleStateChange(state: TunnelState) {
emitTunnelState(state)
WireGuardAutoTunnel.requestTunnelTileServiceStateUpdate()
if (state == TunnelState.UP) {
statsJob = startTunnelStatisticsJob()
}
if (state == TunnelState.DOWN) {
try {
statsJob?.cancel()
} catch (e: CancellationException) {
Timber.i("Stats job cancelled")
} }
} catch (e: BackendException) {
Timber.e("Failed to stop tunnel with error: ${e.message}")
} }
} }
private fun startTunnelStatisticsJob() = applicationScope.launch(ioDispatcher) { override fun getState(): State {
while (true) { return backend.getState(this)
if (backendIsAmneziaUserspace) {
emitBackendStatistics(
AmneziaStatistics(
userspaceAmneziaBackend.get().getStatistics(this@WireGuardTunnel),
),
)
} else {
emitBackendStatistics(WireGuardStatistics(backend().getStatistics(this@WireGuardTunnel)))
}
delay(Constants.VPN_STATISTIC_CHECK_INTERVAL)
}
} }
override fun onStateChange(state: State) { override fun onStateChange(state: State) {
handleStateChange(TunnelState.from(state)) val tunnel = this
emitTunnelState(state)
WireGuardAutoTunnel.requestTileServiceStateUpdate()
if (state == State.UP) {
statsJob =
scope.launch {
while (true) {
val statistics = backend.getStatistics(tunnel)
emitBackendStatistics(statistics)
delay(Constants.VPN_STATISTIC_CHECK_INTERVAL)
}
}
}
if (state == State.DOWN) {
if (this::statsJob.isInitialized) {
statsJob.cancel()
}
}
} }
} }
@@ -1,34 +0,0 @@
package com.zaneschepke.wireguardautotunnel.service.tunnel.statistics
import org.amnezia.awg.backend.Statistics
import org.amnezia.awg.crypto.Key
class AmneziaStatistics(private val statistics: Statistics) : TunnelStatistics() {
override fun peerStats(peer: Key): PeerStats? {
val key = Key.fromBase64(peer.toBase64())
val stats = statistics.peer(key)
return stats?.let {
PeerStats(
rxBytes = stats.rxBytes,
txBytes = stats.txBytes,
latestHandshakeEpochMillis = stats.latestHandshakeEpochMillis,
)
}
}
override fun isTunnelStale(): Boolean {
return statistics.isStale
}
override fun getPeers(): Array<Key> {
return statistics.peers()
}
override fun rx(): Long {
return statistics.totalRx()
}
override fun tx(): Long {
return statistics.totalTx()
}
}
@@ -1,18 +0,0 @@
package com.zaneschepke.wireguardautotunnel.service.tunnel.statistics
import org.amnezia.awg.crypto.Key
abstract class TunnelStatistics {
@JvmRecord
data class PeerStats(val rxBytes: Long, val txBytes: Long, val latestHandshakeEpochMillis: Long)
abstract fun peerStats(peer: Key): PeerStats?
abstract fun isTunnelStale(): Boolean
abstract fun getPeers(): Array<Key>
abstract fun rx(): Long
abstract fun tx(): Long
}
@@ -1,36 +0,0 @@
package com.zaneschepke.wireguardautotunnel.service.tunnel.statistics
import com.wireguard.android.backend.Statistics
import org.amnezia.awg.crypto.Key
class WireGuardStatistics(private val statistics: Statistics) : TunnelStatistics() {
override fun peerStats(peer: Key): PeerStats? {
val key = com.wireguard.crypto.Key.fromBase64(peer.toBase64())
val peerStats = statistics.peer(key)
return peerStats?.let {
PeerStats(
txBytes = peerStats.txBytes,
rxBytes = peerStats.rxBytes,
latestHandshakeEpochMillis = peerStats.latestHandshakeEpochMillis,
)
}
}
override fun isTunnelStale(): Boolean {
return statistics.isStale
}
override fun getPeers(): Array<Key> {
return statistics.peers().map {
Key.fromBase64(it.toBase64())
}.toTypedArray()
}
override fun rx(): Long {
return statistics.totalRx()
}
override fun tx(): Long {
return statistics.totalTx()
}
}
@@ -0,0 +1,13 @@
package com.zaneschepke.wireguardautotunnel.ui
import androidx.lifecycle.ViewModel
import com.zaneschepke.wireguardautotunnel.data.SettingsDao
import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject
@HiltViewModel
class ActivityViewModel
@Inject
constructor(
private val settingsRepo: SettingsDao,
) : ViewModel() {}
@@ -1,9 +0,0 @@
package com.zaneschepke.wireguardautotunnel.ui
data class AppUiState(
val snackbarMessage: String = "",
val snackbarMessageConsumed: Boolean = true,
val vpnPermissionAccepted: Boolean = false,
val notificationPermissionAccepted: Boolean = false,
val requestPermissions: Boolean = false
)
@@ -1,123 +0,0 @@
package com.zaneschepke.wireguardautotunnel.ui
import android.content.ActivityNotFoundException
import android.content.Context
import android.content.Intent
import android.net.Uri
import androidx.lifecycle.ViewModel
import com.wireguard.android.backend.GoBackend
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
import com.zaneschepke.wireguardautotunnel.util.Constants
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import timber.log.Timber
import javax.inject.Inject
@HiltViewModel
class AppViewModel
@Inject
constructor() : ViewModel() {
val vpnIntent: Intent? = GoBackend.VpnService.prepare(WireGuardAutoTunnel.instance)
private val _appUiState = MutableStateFlow(
AppUiState(
vpnPermissionAccepted = vpnIntent == null,
),
)
val appUiState = _appUiState.asStateFlow()
fun isRequiredPermissionGranted(): Boolean {
val allAccepted =
(_appUiState.value.vpnPermissionAccepted && _appUiState.value.vpnPermissionAccepted)
if (!allAccepted) requestPermissions()
return allAccepted
}
private fun requestPermissions() {
_appUiState.update {
it.copy(
requestPermissions = true,
)
}
}
fun permissionsRequested() {
_appUiState.update {
it.copy(
requestPermissions = false,
)
}
}
fun openWebPage(url: String, context: Context) {
try {
val webpage: Uri = Uri.parse(url)
val intent = Intent(Intent.ACTION_VIEW, webpage).apply {
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
}
context.startActivity(intent)
} catch (e: ActivityNotFoundException) {
Timber.e(e)
showSnackbarMessage(context.getString(R.string.no_browser_detected))
}
}
fun onVpnPermissionAccepted() {
_appUiState.update {
it.copy(
vpnPermissionAccepted = true,
)
}
}
fun launchEmail(context: Context) {
try {
val intent =
Intent(Intent.ACTION_SENDTO).apply {
type = Constants.EMAIL_MIME_TYPE
putExtra(Intent.EXTRA_EMAIL, arrayOf(context.getString(R.string.my_email)))
putExtra(Intent.EXTRA_SUBJECT, context.getString(R.string.email_subject))
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
}
context.startActivity(
Intent.createChooser(intent, context.getString(R.string.email_chooser)).apply {
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
},
)
} catch (e: ActivityNotFoundException) {
Timber.e(e)
showSnackbarMessage(context.getString(R.string.no_email_detected))
}
}
fun showSnackbarMessage(message: String) {
_appUiState.update {
it.copy(
snackbarMessage = message,
snackbarMessageConsumed = false,
)
}
}
fun snackbarMessageConsumed() {
_appUiState.update {
it.copy(
snackbarMessage = "",
snackbarMessageConsumed = true,
)
}
}
fun setNotificationPermissionAccepted(accepted: Boolean) {
_appUiState.update {
it.copy(
notificationPermissionAccepted = accepted,
)
}
}
}
@@ -0,0 +1,5 @@
package com.zaneschepke.wireguardautotunnel.ui
import com.journeyapps.barcodescanner.CaptureActivity
class CaptureActivityPortrait : CaptureActivity()
@@ -1,17 +1,16 @@
package com.zaneschepke.wireguardautotunnel.ui package com.zaneschepke.wireguardautotunnel.ui
import android.Manifest import android.Manifest
import android.content.Intent
import android.net.Uri
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import androidx.activity.SystemBarStyle import android.provider.Settings
import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.compose.setContent import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.compose.foundation.focusable import androidx.compose.foundation.focusable
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.material3.SnackbarData import androidx.compose.material3.SnackbarData
@@ -22,149 +21,126 @@ import androidx.compose.material3.SnackbarResult
import androidx.compose.material3.surfaceColorAtElevation import androidx.compose.material3.surfaceColorAtElevation
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusProperties import androidx.compose.ui.focus.focusProperties
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.ProcessLifecycleOwner
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.navigation.NavType
import androidx.navigation.compose.NavHost import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable import androidx.navigation.compose.composable
import androidx.navigation.compose.currentBackStackEntryAsState
import androidx.navigation.compose.rememberNavController import androidx.navigation.compose.rememberNavController
import androidx.navigation.navArgument
import com.google.accompanist.permissions.ExperimentalPermissionsApi import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.google.accompanist.permissions.isGranted import com.google.accompanist.permissions.isGranted
import com.google.accompanist.permissions.rememberPermissionState import com.google.accompanist.permissions.rememberPermissionState
import com.wireguard.android.backend.GoBackend
import com.zaneschepke.wireguardautotunnel.R import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
import com.zaneschepke.wireguardautotunnel.data.repository.AppStateRepository import com.zaneschepke.wireguardautotunnel.data.datastore.DataStoreManager
import com.zaneschepke.wireguardautotunnel.data.repository.SettingsRepository import com.zaneschepke.wireguardautotunnel.data.repository.SettingsRepository
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager import com.zaneschepke.wireguardautotunnel.ui.common.PermissionRequestFailedScreen
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.BottomNavBar import com.zaneschepke.wireguardautotunnel.ui.common.navigation.BottomNavBar
import com.zaneschepke.wireguardautotunnel.ui.common.prompt.CustomSnackBar import com.zaneschepke.wireguardautotunnel.ui.common.prompt.CustomSnackBar
import com.zaneschepke.wireguardautotunnel.ui.screens.config.ConfigScreen import com.zaneschepke.wireguardautotunnel.ui.screens.config.ConfigScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.main.ConfigType
import com.zaneschepke.wireguardautotunnel.ui.screens.main.MainScreen import com.zaneschepke.wireguardautotunnel.ui.screens.main.MainScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.options.OptionsScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.pinlock.PinLockScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.SettingsScreen import com.zaneschepke.wireguardautotunnel.ui.screens.settings.SettingsScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.support.SupportScreen import com.zaneschepke.wireguardautotunnel.ui.screens.support.SupportScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.support.logs.LogsScreen import com.zaneschepke.wireguardautotunnel.ui.theme.TransparentSystemBars
import com.zaneschepke.wireguardautotunnel.ui.theme.WireguardAutoTunnelTheme import com.zaneschepke.wireguardautotunnel.ui.theme.WireguardAutoTunnelTheme
import com.zaneschepke.wireguardautotunnel.util.StringValue import com.zaneschepke.wireguardautotunnel.util.Constants
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import timber.log.Timber
import java.io.IOException
import javax.inject.Inject import javax.inject.Inject
@AndroidEntryPoint @AndroidEntryPoint
class MainActivity : AppCompatActivity() { class MainActivity : AppCompatActivity() {
@Inject @Inject
lateinit var appStateRepository: AppStateRepository lateinit var dataStoreManager: DataStoreManager
@Inject
lateinit var settingsRepository: SettingsRepository
@Inject
lateinit var serviceManager: ServiceManager
@Inject lateinit var settingsRepository: SettingsRepository
@OptIn( @OptIn(
ExperimentalPermissionsApi::class, ExperimentalPermissionsApi::class,
) )
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
val isPinLockEnabled = intent.extras?.getBoolean(SplashActivity.IS_PIN_LOCK_ENABLED_KEY) // load preferences into memory and init data
enableEdgeToEdge(navigationBarStyle = SystemBarStyle.dark(Color.Transparent.toArgb()))
lifecycleScope.launch { lifecycleScope.launch {
WireGuardAutoTunnel.requestTunnelTileServiceStateUpdate() try {
val settings = settingsRepository.getSettings() dataStoreManager.init()
if (settings.isAutoTunnelEnabled) { if (settingsRepository.getAll().isEmpty()) {
serviceManager.startWatcherService(application.applicationContext) settingsRepository.save(com.zaneschepke.wireguardautotunnel.data.model.Settings())
}
WireGuardAutoTunnel.requestTileServiceStateUpdate()
} catch (e: IOException) {
Timber.e("Failed to load preferences")
} }
} }
setContent { setContent {
val appViewModel = hiltViewModel<AppViewModel>() // val activityViewModel = hiltViewModel<ActivityViewModel>()
val appUiState by appViewModel.appUiState.collectAsStateWithLifecycle()
val navController = rememberNavController() val navController = rememberNavController()
val navBackStackEntry by navController.currentBackStackEntryAsState() val focusRequester = remember { FocusRequester() }
val notificationPermissionState =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU)
rememberPermissionState(Manifest.permission.POST_NOTIFICATIONS) else null
val snackbarHostState = remember { SnackbarHostState() }
val vpnActivityResultState =
rememberLauncherForActivityResult(
ActivityResultContracts.StartActivityForResult(),
onResult = {
val accepted = (it.resultCode == RESULT_OK)
if (accepted) {
appViewModel.onVpnPermissionAccepted()
}
},
)
fun showSnackBarMessage(message: StringValue) {
lifecycleScope.launch(Dispatchers.Main) {
val result =
snackbarHostState.showSnackbar(
message = message.asString(this@MainActivity),
duration = SnackbarDuration.Short,
)
when (result) {
SnackbarResult.ActionPerformed,
SnackbarResult.Dismissed -> {
snackbarHostState.currentSnackbarData?.dismiss()
}
}
}
}
LaunchedEffect(appUiState.requestPermissions) {
if (appUiState.requestPermissions) {
appViewModel.permissionsRequested()
if (notificationPermissionState != null && !notificationPermissionState.status.isGranted
) {
showSnackBarMessage(StringValue.StringResource(R.string.notification_permission_required))
return@LaunchedEffect notificationPermissionState.launchPermissionRequest()
}
if (!appUiState.vpnPermissionAccepted) {
return@LaunchedEffect appViewModel.vpnIntent?.let {
vpnActivityResultState.launch(
it,
)
}!!
}
}
}
WireguardAutoTunnelTheme { WireguardAutoTunnelTheme {
LaunchedEffect(Unit) { TransparentSystemBars()
appViewModel.setNotificationPermissionAccepted(
notificationPermissionState?.status?.isGranted ?: true,
)
}
LaunchedEffect(appUiState.snackbarMessageConsumed) { val snackbarHostState = remember { SnackbarHostState() }
if (!appUiState.snackbarMessageConsumed) {
showSnackBarMessage(StringValue.DynamicString(appUiState.snackbarMessage)) val notificationPermissionState =
appViewModel.snackbarMessageConsumed() rememberPermissionState(Manifest.permission.POST_NOTIFICATIONS)
fun requestNotificationPermission() {
if (
!notificationPermissionState.status.isGranted &&
Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU
) {
notificationPermissionState.launchPermissionRequest()
} }
} }
val focusRequester = remember { FocusRequester() } var vpnIntent by remember { mutableStateOf(GoBackend.VpnService.prepare(this)) }
val vpnActivityResultState =
rememberLauncherForActivityResult(
ActivityResultContracts.StartActivityForResult(),
onResult = {
val accepted = (it.resultCode == RESULT_OK)
if (accepted) {
vpnIntent = null
}
},
)
LaunchedEffect(vpnIntent) {
if (vpnIntent != null) {
vpnActivityResultState.launch(vpnIntent)
} else {
requestNotificationPermission()
}
}
fun showSnackBarMessage(message: String) {
lifecycleScope.launch(Dispatchers.Main) {
val result =
snackbarHostState.showSnackbar(
message = message,
actionLabel = applicationContext.getString(R.string.okay),
duration = SnackbarDuration.Short,
)
when (result) {
SnackbarResult.ActionPerformed,
SnackbarResult.Dismissed -> {
snackbarHostState.currentSnackbarData?.dismiss()
}
}
}
}
Scaffold( Scaffold(
snackbarHost = { snackbarHost = {
@@ -173,45 +149,65 @@ class MainActivity : AppCompatActivity() {
snackbarData.visuals.message, snackbarData.visuals.message,
isRtl = false, isRtl = false,
containerColor = containerColor =
MaterialTheme.colorScheme.surfaceColorAtElevation( MaterialTheme.colorScheme.surfaceColorAtElevation(
2.dp, 2.dp,
), ),
) )
} }
}, },
//TODO refactor modifier = Modifier.focusable().focusProperties { up = focusRequester },
modifier = Modifier bottomBar =
.focusable() if (vpnIntent == null && notificationPermissionState.status.isGranted) {
.focusProperties { {
when (navBackStackEntry?.destination?.route) { BottomNavBar(
Screen.Lock.route -> Unit navController,
else -> up = focusRequester listOf(
Screen.Main.navItem,
Screen.Settings.navItem,
Screen.Support.navItem,
),
)
} }
} else {
{}
}, },
bottomBar = {
BottomNavBar(
navController,
listOf(
Screen.Main.navItem,
Screen.Settings.navItem,
Screen.Support.navItem,
),
)
},
) { padding -> ) { padding ->
NavHost( if (vpnIntent != null) {
navController, PermissionRequestFailedScreen(
startDestination = (if (isPinLockEnabled == true) Screen.Lock.route else Screen.Main.route), padding = padding,
modifier = Modifier onRequestAgain = { vpnActivityResultState.launch(vpnIntent) },
.padding(padding) message = getString(R.string.vpn_permission_required),
.fillMaxSize(), getString(R.string.retry),
) { )
return@Scaffold
}
if (!notificationPermissionState.status.isGranted) {
PermissionRequestFailedScreen(
padding = padding,
onRequestAgain = {
val intentSettings =
Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
intentSettings.data =
Uri.fromParts(
Constants.URI_PACKAGE_SCHEME,
this.packageName,
null,
)
startActivity(intentSettings)
},
message = getString(R.string.notification_permission_required),
getString(R.string.open_settings),
)
return@Scaffold
}
NavHost(navController, startDestination = Screen.Main.route) {
composable( composable(
Screen.Main.route, Screen.Main.route,
) { ) {
MainScreen( MainScreen(
padding = padding,
focusRequester = focusRequester, focusRequester = focusRequester,
appViewModel = appViewModel, showSnackbarMessage = { message -> showSnackBarMessage(message) },
navController = navController, navController = navController,
) )
} }
@@ -219,8 +215,8 @@ class MainActivity : AppCompatActivity() {
Screen.Settings.route, Screen.Settings.route,
) { ) {
SettingsScreen( SettingsScreen(
appViewModel = appViewModel, padding = padding,
navController = navController, showSnackbarMessage = { message -> showSnackBarMessage(message) },
focusRequester = focusRequester, focusRequester = focusRequester,
) )
} }
@@ -228,59 +224,25 @@ class MainActivity : AppCompatActivity() {
Screen.Support.route, Screen.Support.route,
) { ) {
SupportScreen( SupportScreen(
padding = padding,
focusRequester = focusRequester, focusRequester = focusRequester,
appViewModel = appViewModel, showSnackbarMessage = { message -> showSnackBarMessage(message) },
navController = navController,
) )
} }
composable(Screen.Support.Logs.route) { composable("${Screen.Config.route}/{id}") {
LogsScreen()
}
composable(
"${Screen.Config.route}/{id}?configType={configType}",
arguments =
listOf(
navArgument("id") {
type = NavType.StringType
defaultValue = "0"
},
navArgument("configType") {
type = NavType.StringType
defaultValue = ConfigType.WIREGUARD.name
},
),
) {
val id = it.arguments?.getString("id") val id = it.arguments?.getString("id")
val configType = ConfigType.valueOf(
it.arguments?.getString("configType") ?: ConfigType.WIREGUARD.name,
)
if (!id.isNullOrBlank()) { if (!id.isNullOrBlank()) {
// https://dagger.dev/hilt/view-model#assisted-injection
ConfigScreen( ConfigScreen(
navController = navController, navController = navController,
tunnelId = id, id = id,
appViewModel = appViewModel, showSnackbarMessage = { message ->
focusRequester = focusRequester, showSnackBarMessage(message)
configType = configType, },
)
}
}
composable("${Screen.Option.route}/{id}") {
val id = it.arguments?.getString("id")
if (!id.isNullOrBlank()) {
OptionsScreen(
navController = navController,
tunnelId = id,
appViewModel = appViewModel,
focusRequester = focusRequester, focusRequester = focusRequester,
) )
} }
} }
composable(Screen.Lock.route) {
PinLockScreen(
navController = navController,
appViewModel = appViewModel,
)
}
} }
} }
} }
@@ -4,15 +4,13 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.Home import androidx.compose.material.icons.rounded.Home
import androidx.compose.material.icons.rounded.QuestionMark import androidx.compose.material.icons.rounded.QuestionMark
import androidx.compose.material.icons.rounded.Settings import androidx.compose.material.icons.rounded.Settings
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.BottomNavItem import com.zaneschepke.wireguardautotunnel.ui.common.navigation.BottomNavItem
sealed class Screen(val route: String) { sealed class Screen(val route: String) {
data object Main : Screen("main") { data object Main : Screen("main") {
val navItem = val navItem =
BottomNavItem( BottomNavItem(
name = WireGuardAutoTunnel.instance.getString(R.string.tunnels), name = "Tunnels",
route = route, route = route,
icon = Icons.Rounded.Home, icon = Icons.Rounded.Home,
) )
@@ -21,7 +19,7 @@ sealed class Screen(val route: String) {
data object Settings : Screen("settings") { data object Settings : Screen("settings") {
val navItem = val navItem =
BottomNavItem( BottomNavItem(
name = WireGuardAutoTunnel.instance.getString(R.string.settings), name = "Settings",
route = route, route = route,
icon = Icons.Rounded.Settings, icon = Icons.Rounded.Settings,
) )
@@ -30,16 +28,11 @@ sealed class Screen(val route: String) {
data object Support : Screen("support") { data object Support : Screen("support") {
val navItem = val navItem =
BottomNavItem( BottomNavItem(
name = WireGuardAutoTunnel.instance.getString(R.string.support), name = "Support",
route = route, route = route,
icon = Icons.Rounded.QuestionMark, icon = Icons.Rounded.QuestionMark,
) )
data object Logs : Screen("support/logs")
} }
data object Config : Screen("config") data object Config : Screen("config")
data object Lock : Screen("lock")
data object Option : Screen("option")
} }
@@ -1,68 +0,0 @@
package com.zaneschepke.wireguardautotunnel.ui
import android.annotation.SuppressLint
import android.content.Intent
import android.os.Build
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import com.zaneschepke.logcatter.LocalLogCollector
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel.Companion.isRunningOnAndroidTv
import com.zaneschepke.wireguardautotunnel.data.repository.AppStateRepository
import com.zaneschepke.wireguardautotunnel.module.ApplicationScope
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import xyz.teamgravity.pin_lock_compose.PinManager
import javax.inject.Inject
@SuppressLint("CustomSplashScreen")
@AndroidEntryPoint
class SplashActivity : ComponentActivity() {
@Inject
lateinit var appStateRepository: AppStateRepository
@Inject
lateinit var localLogCollector: LocalLogCollector
@Inject
@ApplicationScope
lateinit var applicationScope: CoroutineScope
override fun onCreate(savedInstanceState: Bundle?) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
val splashScreen = installSplashScreen()
splashScreen.setKeepOnScreenCondition { true }
}
super.onCreate(savedInstanceState)
applicationScope.launch {
if (!isRunningOnAndroidTv()) localLogCollector.start()
}
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.CREATED) {
val pinLockEnabled = appStateRepository.isPinLockEnabled()
if (pinLockEnabled) {
PinManager.initialize(WireGuardAutoTunnel.instance)
}
val intent = Intent(this@SplashActivity, MainActivity::class.java).apply {
putExtra(IS_PIN_LOCK_ENABLED_KEY, pinLockEnabled)
}
startActivity(intent)
finish()
}
}
}
companion object {
const val IS_PIN_LOCK_ENABLED_KEY = "is_pin_lock_enabled"
}
}
@@ -10,6 +10,8 @@ import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.stringResource
import com.zaneschepke.wireguardautotunnel.R
@Composable @Composable
fun ClickableIconButton( fun ClickableIconButton(
@@ -27,12 +29,9 @@ fun ClickableIconButton(
Spacer(modifier = Modifier.size(ButtonDefaults.IconSpacing)) Spacer(modifier = Modifier.size(ButtonDefaults.IconSpacing))
Icon( Icon(
imageVector = icon, imageVector = icon,
contentDescription = icon.name, contentDescription = stringResource(R.string.delete),
modifier = modifier =
Modifier Modifier.size(ButtonDefaults.IconSize).weight(1f, false).clickable {
.size(ButtonDefaults.IconSize)
.weight(1f, false)
.clickable {
if (enabled) { if (enabled) {
onIconClick() onIconClick()
} }
@@ -0,0 +1,38 @@
package com.zaneschepke.wireguardautotunnel.ui.common
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Button
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.launch
@Composable
fun PermissionRequestFailedScreen(
padding: PaddingValues,
onRequestAgain: () -> Unit,
message: String,
buttonText: String
) {
val scope = rememberCoroutineScope()
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center,
modifier = Modifier.fillMaxSize().padding(padding),
) {
Text(message, textAlign = TextAlign.Center, modifier = Modifier.padding(15.dp))
Button(
onClick = { scope.launch { onRequestAgain() } },
) {
Text(buttonText)
}
}
}
@@ -15,12 +15,9 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import com.zaneschepke.wireguardautotunnel.service.tunnel.statistics.TunnelStatistics import com.wireguard.android.backend.Statistics
import com.zaneschepke.wireguardautotunnel.util.NumberUtils import com.zaneschepke.wireguardautotunnel.util.NumberUtils
import com.zaneschepke.wireguardautotunnel.util.toThreeDecimalPlaceString import com.zaneschepke.wireguardautotunnel.util.toThreeDecimalPlaceString
@@ -33,50 +30,44 @@ fun RowListItem(
onClick: () -> Unit, onClick: () -> Unit,
rowButton: @Composable () -> Unit, rowButton: @Composable () -> Unit,
expanded: Boolean, expanded: Boolean,
statistics: TunnelStatistics?, statistics: Statistics?
focusRequester: FocusRequester,
) { ) {
Box( Box(
modifier = modifier =
Modifier.focusRequester(focusRequester) Modifier.animateContentSize()
.animateContentSize() .clip(RoundedCornerShape(30.dp))
.clip(RoundedCornerShape(30.dp)) .combinedClickable(
.combinedClickable( onClick = { onClick() },
onClick = { onClick() }, onLongClick = { onHold() },
onLongClick = { onHold() }, ),
),
) { ) {
Column { Column {
Row( Row(
modifier = Modifier modifier = Modifier.fillMaxWidth().padding(horizontal = 15.dp, vertical = 5.dp),
.fillMaxWidth()
.padding(horizontal = 15.dp, vertical = 5.dp),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween, horizontalArrangement = Arrangement.SpaceBetween,
) { ) {
Row( Row(
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.fillMaxWidth(13 / 20f), modifier = Modifier.fillMaxWidth(.60f),
) { ) {
icon() icon()
Text(text, maxLines = 1, overflow = TextOverflow.Ellipsis) Text(text)
} }
rowButton() rowButton()
} }
if (expanded) { if (expanded) {
statistics?.getPeers()?.forEach { statistics?.peers()?.forEach {
Row( Row(
modifier = modifier =
Modifier Modifier.fillMaxWidth()
.fillMaxWidth() .padding(end = 10.dp, bottom = 10.dp, start = 10.dp),
.padding(end = 10.dp, bottom = 10.dp, start = 10.dp),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceEvenly, horizontalArrangement = Arrangement.SpaceEvenly,
) { ) {
//TODO change these to string resources val handshakeEpoch = statistics.peer(it)!!.latestHandshakeEpochMillis
val handshakeEpoch = statistics.peerStats(it)!!.latestHandshakeEpochMillis val peerTx = statistics.peer(it)!!.txBytes
val peerTx = statistics.peerStats(it)!!.txBytes val peerRx = statistics.peer(it)!!.rxBytes
val peerRx = statistics.peerStats(it)!!.rxBytes
val peerId = it.toBase64().subSequence(0, 3).toString() + "***" val peerId = it.toBase64().subSequence(0, 3).toString() + "***"
val handshakeSec = val handshakeSec =
NumberUtils.getSecondsBetweenTimestampAndNow(handshakeEpoch) NumberUtils.getSecondsBetweenTimestampAndNow(handshakeEpoch)
@@ -44,39 +44,36 @@ fun SearchBar(onQuery: (queryString: String) -> Unit) {
onQuery(onQueryChanged) onQuery(onQueryChanged)
}, },
leadingIcon = { leadingIcon = {
val icon = Icons.Rounded.Search
Icon( Icon(
imageVector = icon, imageVector = Icons.Rounded.Search,
tint = MaterialTheme.colorScheme.onBackground, tint = MaterialTheme.colorScheme.onBackground,
contentDescription = icon.name, contentDescription = stringResource(id = R.string.search_icon),
) )
}, },
trailingIcon = { trailingIcon = {
if (showClearIcon) { if (showClearIcon) {
IconButton(onClick = { query = "" }) { IconButton(onClick = { query = "" }) {
val icon = Icons.Rounded.Clear
Icon( Icon(
imageVector = icon, imageVector = Icons.Rounded.Clear,
tint = MaterialTheme.colorScheme.onBackground, tint = MaterialTheme.colorScheme.onBackground,
contentDescription = icon.name, contentDescription = stringResource(id = R.string.clear_icon),
) )
} }
} }
}, },
maxLines = 1, maxLines = 1,
colors = colors =
TextFieldDefaults.colors( TextFieldDefaults.colors(
focusedContainerColor = Color.Transparent, focusedContainerColor = Color.Transparent,
unfocusedContainerColor = Color.Transparent, unfocusedContainerColor = Color.Transparent,
disabledContainerColor = Color.Transparent, disabledContainerColor = Color.Transparent,
), ),
placeholder = { Text(text = stringResource(R.string.hint_search_packages)) }, placeholder = { Text(text = stringResource(R.string.hint_search_packages)) },
textStyle = MaterialTheme.typography.bodySmall, textStyle = MaterialTheme.typography.bodySmall,
singleLine = true, singleLine = true,
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Text), keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Text),
modifier = modifier =
Modifier Modifier.fillMaxWidth()
.fillMaxWidth() .background(color = MaterialTheme.colorScheme.background, shape = RectangleShape),
.background(color = MaterialTheme.colorScheme.background, shape = RectangleShape),
) )
} }
@@ -27,10 +27,10 @@ fun ConfigurationTextBox(
maxLines = 1, maxLines = 1,
placeholder = { Text(hint) }, placeholder = { Text(hint) },
keyboardOptions = keyboardOptions =
KeyboardOptions( KeyboardOptions(
capitalization = KeyboardCapitalization.None, capitalization = KeyboardCapitalization.None,
imeAction = ImeAction.Done, imeAction = ImeAction.Done,
), ),
keyboardActions = keyboardActions, keyboardActions = keyboardActions,
) )
} }
@@ -9,7 +9,6 @@ import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.Dp
@Composable @Composable
@@ -22,21 +21,11 @@ fun ConfigurationToggle(
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
Row( Row(
modifier = Modifier modifier = Modifier.fillMaxWidth().padding(padding),
.fillMaxWidth()
.padding(padding),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween, horizontalArrangement = Arrangement.SpaceBetween,
) { ) {
Text( Text(label)
label, textAlign = TextAlign.Start,
modifier = Modifier
.weight(
weight = 1.0f,
fill = false,
),
softWrap = true,
)
Switch( Switch(
modifier = modifier, modifier = modifier,
enabled = enabled, enabled = enabled,
@@ -6,33 +6,18 @@ import androidx.compose.material3.NavigationBar
import androidx.compose.material3.NavigationBarItem import androidx.compose.material3.NavigationBarItem
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.navigation.NavController import androidx.navigation.NavController
import androidx.navigation.compose.currentBackStackEntryAsState import androidx.navigation.compose.currentBackStackEntryAsState
import com.zaneschepke.wireguardautotunnel.ui.Screen
@Composable @Composable
fun BottomNavBar(navController: NavController, bottomNavItems: List<BottomNavItem>) { fun BottomNavBar(navController: NavController, bottomNavItems: List<BottomNavItem>) {
val backStackEntry = navController.currentBackStackEntryAsState() val backStackEntry = navController.currentBackStackEntryAsState()
var showBottomBar by rememberSaveable { mutableStateOf(true) }
val navBackStackEntry by navController.currentBackStackEntryAsState()
//TODO find a better way to hide nav bar
showBottomBar = when (navBackStackEntry?.destination?.route) {
Screen.Lock.route -> false
else -> true
}
NavigationBar( NavigationBar(
containerColor = if (!showBottomBar) Color.Transparent else MaterialTheme.colorScheme.background, containerColor = MaterialTheme.colorScheme.background,
) { ) {
if (showBottomBar) bottomNavItems.forEach { item -> bottomNavItems.forEach { item ->
val selected = item.route == backStackEntry.value?.destination?.route val selected = item.route == backStackEntry.value?.destination?.route
NavigationBarItem( NavigationBarItem(
@@ -21,32 +21,26 @@ fun AuthorizationPrompt(onSuccess: () -> Unit, onFailure: () -> Unit, onError: (
onError("Biometrics not available") onError("Biometrics not available")
false false
} }
BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED -> { BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED -> {
onError("Biometrics not created") onError("Biometrics not created")
false false
} }
BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE -> { BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE -> {
onError("Biometric hardware not found") onError("Biometric hardware not found")
false false
} }
BiometricManager.BIOMETRIC_ERROR_SECURITY_UPDATE_REQUIRED -> { BiometricManager.BIOMETRIC_ERROR_SECURITY_UPDATE_REQUIRED -> {
onError("Biometric security update required") onError("Biometric security update required")
false false
} }
BiometricManager.BIOMETRIC_ERROR_UNSUPPORTED -> { BiometricManager.BIOMETRIC_ERROR_UNSUPPORTED -> {
onError("Biometrics not supported") onError("Biometrics not supported")
false false
} }
BiometricManager.BIOMETRIC_STATUS_UNKNOWN -> { BiometricManager.BIOMETRIC_STATUS_UNKNOWN -> {
onError("Biometrics status unknown") onError("Biometrics status unknown")
false false
} }
BiometricManager.BIOMETRIC_SUCCESS -> true BiometricManager.BIOMETRIC_SUCCESS -> true
else -> false else -> false
} }
@@ -19,9 +19,12 @@ import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
@Composable @Composable
@@ -30,30 +33,27 @@ fun CustomSnackBar(
isRtl: Boolean = true, isRtl: Boolean = true,
containerColor: Color = MaterialTheme.colorScheme.surface containerColor: Color = MaterialTheme.colorScheme.surface
) { ) {
val context = LocalContext.current
Snackbar( Snackbar(
containerColor = containerColor, containerColor = containerColor,
modifier = modifier =
Modifier Modifier.fillMaxWidth(
.fillMaxWidth( if (WireGuardAutoTunnel.isRunningOnAndroidTv()) 1 / 3f else 2 / 3f,
if (WireGuardAutoTunnel.isRunningOnAndroidTv()) 1 / 3f else 2 / 3f, )
) .padding(bottom = 100.dp),
.padding(bottom = 100.dp),
shape = RoundedCornerShape(16.dp), shape = RoundedCornerShape(16.dp),
) { ) {
CompositionLocalProvider( CompositionLocalProvider(
LocalLayoutDirection provides if (isRtl) LayoutDirection.Rtl else LayoutDirection.Ltr, LocalLayoutDirection provides if (isRtl) LayoutDirection.Rtl else LayoutDirection.Ltr,
) { ) {
Row( Row(
modifier = Modifier modifier = Modifier.width(IntrinsicSize.Max).height(IntrinsicSize.Min),
.width(IntrinsicSize.Max)
.height(IntrinsicSize.Min),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Start, horizontalArrangement = Arrangement.Start,
) { ) {
val icon = Icons.Rounded.Info
Icon( Icon(
icon, Icons.Rounded.Info,
contentDescription = icon.name, contentDescription = stringResource(R.string.info),
tint = Color.White, tint = Color.White,
modifier = Modifier.padding(end = 10.dp), modifier = Modifier.padding(end = 10.dp),
) )
@@ -16,10 +16,7 @@ fun LoadingScreen() {
Column( Column(
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Top, verticalArrangement = Arrangement.Top,
modifier = Modifier modifier = Modifier.fillMaxSize().focusable().padding(),
.fillMaxSize()
.focusable()
.padding(),
) { ) {
Column(modifier = Modifier.padding(120.dp)) { CircularProgressIndicator() } Column(modifier = Modifier.padding(120.dp)) { CircularProgressIndicator() }
} }
@@ -1,25 +0,0 @@
package com.zaneschepke.wireguardautotunnel.ui.common.text
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
@Composable
fun LogTypeLabel(color: Color, content: @Composable () -> Unit) {
Box(
modifier = Modifier
.size(20.dp)
.clip(RoundedCornerShape(2.dp))
.background(color),
contentAlignment = Alignment.Center,
) {
content()
}
}
@@ -15,7 +15,7 @@ import androidx.compose.ui.unit.sp
fun SectionTitle(title: String, padding: Dp) { fun SectionTitle(title: String, padding: Dp) {
Text( Text(
title, title,
textAlign = TextAlign.Start, textAlign = TextAlign.Center,
style = TextStyle(fontSize = 18.sp, fontWeight = FontWeight.ExtraBold), style = TextStyle(fontSize = 18.sp, fontWeight = FontWeight.ExtraBold),
modifier = Modifier.padding(padding, bottom = 5.dp, top = 5.dp), modifier = Modifier.padding(padding, bottom = 5.dp, top = 5.dp),
) )
@@ -0,0 +1,30 @@
package com.zaneschepke.wireguardautotunnel.ui.models
import com.wireguard.config.Interface
data class InterfaceProxy(
var privateKey: String = "",
var publicKey: String = "",
var addresses: String = "",
var dnsServers: String = "",
var listenPort: String = "",
var mtu: String = ""
) {
companion object {
fun from(i: Interface): InterfaceProxy {
return InterfaceProxy(
publicKey = i.keyPair.publicKey.toBase64().trim(),
privateKey = i.keyPair.privateKey.toBase64().trim(),
addresses = i.addresses.joinToString(", ").trim(),
dnsServers = i.dnsServers.joinToString(", ").replace("/", "").trim(),
listenPort =
if (i.listenPort.isPresent) {
i.listenPort.get().toString().trim()
} else {
""
},
mtu = if (i.mtu.isPresent) i.mtu.get().toString().trim() else "",
)
}
}
}
@@ -0,0 +1,73 @@
package com.zaneschepke.wireguardautotunnel.ui.models
import com.wireguard.config.Peer
data class PeerProxy(
var publicKey: String = "",
var preSharedKey: String = "",
var persistentKeepalive: String = "",
var endpoint: String = "",
var allowedIps: String = IPV4_WILDCARD.joinToString(", ").trim()
) {
companion object {
fun from(peer: Peer): PeerProxy {
return PeerProxy(
publicKey = peer.publicKey.toBase64(),
preSharedKey =
if (peer.preSharedKey.isPresent) {
peer.preSharedKey.get().toBase64().trim()
} else {
""
},
persistentKeepalive =
if (peer.persistentKeepalive.isPresent) {
peer.persistentKeepalive.get().toString().trim()
} else {
""
},
endpoint =
if (peer.endpoint.isPresent) {
peer.endpoint.get().toString().trim()
} else {
""
},
allowedIps = peer.allowedIps.joinToString(", ").trim(),
)
}
val IPV4_PUBLIC_NETWORKS =
setOf(
"0.0.0.0/5",
"8.0.0.0/7",
"11.0.0.0/8",
"12.0.0.0/6",
"16.0.0.0/4",
"32.0.0.0/3",
"64.0.0.0/2",
"128.0.0.0/3",
"160.0.0.0/5",
"168.0.0.0/6",
"172.0.0.0/12",
"172.32.0.0/11",
"172.64.0.0/10",
"172.128.0.0/9",
"173.0.0.0/8",
"174.0.0.0/7",
"176.0.0.0/4",
"192.0.0.0/9",
"192.128.0.0/11",
"192.160.0.0/13",
"192.169.0.0/16",
"192.170.0.0/15",
"192.172.0.0/14",
"192.176.0.0/12",
"192.192.0.0/10",
"193.0.0.0/8",
"194.0.0.0/7",
"196.0.0.0/6",
"200.0.0.0/5",
"208.0.0.0/4",
)
val IPV4_WILDCARD = setOf("0.0.0.0/0")
}
}
@@ -1,6 +1,7 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.config package com.zaneschepke.wireguardautotunnel.ui.screens.config
import android.annotation.SuppressLint import android.annotation.SuppressLint
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.Image import androidx.compose.foundation.Image
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.focusGroup import androidx.compose.foundation.focusGroup
@@ -28,7 +29,7 @@ import androidx.compose.material.icons.rounded.ContentCopy
import androidx.compose.material.icons.rounded.Delete import androidx.compose.material.icons.rounded.Delete
import androidx.compose.material.icons.rounded.Refresh import androidx.compose.material.icons.rounded.Refresh
import androidx.compose.material.icons.rounded.Save import androidx.compose.material.icons.rounded.Save
import androidx.compose.material3.BasicAlertDialog import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Checkbox import androidx.compose.material3.Checkbox
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.FabPosition import androidx.compose.material3.FabPosition
@@ -50,6 +51,7 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.focus.focusRequester
@@ -61,7 +63,6 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.PasswordVisualTransformation import androidx.compose.ui.text.input.PasswordVisualTransformation
import androidx.compose.ui.text.input.VisualTransformation import androidx.compose.ui.text.input.VisualTransformation
@@ -72,30 +73,30 @@ import androidx.navigation.NavController
import com.google.accompanist.drawablepainter.DrawablePainter import com.google.accompanist.drawablepainter.DrawablePainter
import com.zaneschepke.wireguardautotunnel.R import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
import com.zaneschepke.wireguardautotunnel.ui.AppViewModel
import com.zaneschepke.wireguardautotunnel.ui.Screen import com.zaneschepke.wireguardautotunnel.ui.Screen
import com.zaneschepke.wireguardautotunnel.ui.common.SearchBar import com.zaneschepke.wireguardautotunnel.ui.common.SearchBar
import com.zaneschepke.wireguardautotunnel.ui.common.config.ConfigurationTextBox import com.zaneschepke.wireguardautotunnel.ui.common.config.ConfigurationTextBox
import com.zaneschepke.wireguardautotunnel.ui.common.prompt.AuthorizationPrompt import com.zaneschepke.wireguardautotunnel.ui.common.prompt.AuthorizationPrompt
import com.zaneschepke.wireguardautotunnel.ui.common.screen.LoadingScreen import com.zaneschepke.wireguardautotunnel.ui.common.screen.LoadingScreen
import com.zaneschepke.wireguardautotunnel.ui.common.text.SectionTitle import com.zaneschepke.wireguardautotunnel.ui.common.text.SectionTitle
import com.zaneschepke.wireguardautotunnel.ui.screens.main.ConfigType
import com.zaneschepke.wireguardautotunnel.util.Constants import com.zaneschepke.wireguardautotunnel.util.Constants
import com.zaneschepke.wireguardautotunnel.util.getMessage import com.zaneschepke.wireguardautotunnel.util.Event
import com.zaneschepke.wireguardautotunnel.util.Result
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter") @SuppressLint("UnusedMaterial3ScaffoldPaddingParameter")
@OptIn( @OptIn(
ExperimentalComposeUiApi::class,
ExperimentalMaterial3Api::class, ExperimentalMaterial3Api::class,
ExperimentalFoundationApi::class,
) )
@Composable @Composable
fun ConfigScreen( fun ConfigScreen(
viewModel: ConfigViewModel = hiltViewModel(), viewModel: ConfigViewModel = hiltViewModel(),
focusRequester: FocusRequester, focusRequester: FocusRequester,
navController: NavController, navController: NavController,
appViewModel: AppViewModel, showSnackbarMessage: (String) -> Unit,
tunnelId: String, id: String
configType: ConfigType
) { ) {
val context = LocalContext.current val context = LocalContext.current
val clipboardManager: ClipboardManager = LocalClipboardManager.current val clipboardManager: ClipboardManager = LocalClipboardManager.current
@@ -106,7 +107,7 @@ fun ConfigScreen(
val uiState by viewModel.uiState.collectAsStateWithLifecycle() val uiState by viewModel.uiState.collectAsStateWithLifecycle()
LaunchedEffect(Unit) { viewModel.init(tunnelId) } LaunchedEffect(Unit) { viewModel.init(id) }
LaunchedEffect(uiState.loading) { LaunchedEffect(uiState.loading) {
if (!uiState.loading && WireGuardAutoTunnel.isRunningOnAndroidTv()) { if (!uiState.loading && WireGuardAutoTunnel.isRunningOnAndroidTv()) {
@@ -128,17 +129,14 @@ fun ConfigScreen(
val fillMaxWidth = .85f val fillMaxWidth = .85f
val screenPadding = 5.dp val screenPadding = 5.dp
val applicationButtonText = buildAnnotatedString { val applicationButtonText = {
append(stringResource(id = R.string.tunneling_apps)) "Tunneling apps: " +
append(": ") if (uiState.isAllApplicationsEnabled) {
if (uiState.isAllApplicationsEnabled) { "all"
append(stringResource(id = R.string.all)) } else {
} else { "${uiState.checkedPackageNames.size} " +
append("${uiState.checkedPackageNames.size} ") (if (uiState.include) "included" else "excluded")
(if (uiState.include) append(stringResource(id = R.string.included)) else append( }
stringResource(id = R.string.excluded),
))
}
} }
if (showAuthPrompt) { if (showAuthPrompt) {
@@ -147,13 +145,13 @@ fun ConfigScreen(
showAuthPrompt = false showAuthPrompt = false
isAuthenticated = true isAuthenticated = true
}, },
onError = { onError = { error ->
showAuthPrompt = false showAuthPrompt = false
appViewModel.showSnackbarMessage(context.getString(R.string.error_authentication_failed)) showSnackbarMessage(Event.Error.AuthenticationFailed.message)
}, },
onFailure = { onFailure = {
showAuthPrompt = false showAuthPrompt = false
appViewModel.showSnackbarMessage(context.getString(R.string.error_authorization_failed)) showSnackbarMessage(Event.Error.AuthorizationFailed.message)
}, },
) )
} }
@@ -163,26 +161,20 @@ fun ConfigScreen(
remember(uiState.packages) { remember(uiState.packages) {
uiState.packages.sortedBy { viewModel.getPackageLabel(it) } uiState.packages.sortedBy { viewModel.getPackageLabel(it) }
} }
BasicAlertDialog(onDismissRequest = { showApplicationsDialog = false }) { AlertDialog(onDismissRequest = { showApplicationsDialog = false }) {
Surface( Surface(
tonalElevation = 2.dp, tonalElevation = 2.dp,
shadowElevation = 2.dp, shadowElevation = 2.dp,
shape = RoundedCornerShape(12.dp), shape = RoundedCornerShape(12.dp),
color = MaterialTheme.colorScheme.surface, color = MaterialTheme.colorScheme.surface,
modifier = modifier =
Modifier Modifier.fillMaxWidth()
.fillMaxWidth() .fillMaxHeight(if (uiState.isAllApplicationsEnabled) 1 / 5f else 4 / 5f),
.fillMaxHeight(if (uiState.isAllApplicationsEnabled) 1 / 5f else 4 / 5f),
) { ) {
Column( Column(modifier = Modifier.fillMaxWidth()) {
modifier = Modifier
.fillMaxWidth(),
) {
Row( Row(
modifier = modifier =
Modifier Modifier.fillMaxWidth().padding(horizontal = 20.dp, vertical = 7.dp),
.fillMaxWidth()
.padding(horizontal = 20.dp, vertical = 7.dp),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween, horizontalArrangement = Arrangement.SpaceBetween,
) { ) {
@@ -195,9 +187,8 @@ fun ConfigScreen(
if (!uiState.isAllApplicationsEnabled) { if (!uiState.isAllApplicationsEnabled) {
Row( Row(
modifier = modifier =
Modifier Modifier.fillMaxWidth()
.fillMaxWidth() .padding(horizontal = 20.dp, vertical = 7.dp),
.padding(horizontal = 20.dp, vertical = 7.dp),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween, horizontalArrangement = Arrangement.SpaceBetween,
) { ) {
@@ -228,9 +219,8 @@ fun ConfigScreen(
} }
Row( Row(
modifier = modifier =
Modifier Modifier.fillMaxWidth()
.fillMaxWidth() .padding(horizontal = 20.dp, vertical = 7.dp),
.padding(horizontal = 20.dp, vertical = 7.dp),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween, horizontalArrangement = Arrangement.SpaceBetween,
) { ) {
@@ -246,9 +236,7 @@ fun ConfigScreen(
Row( Row(
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween, horizontalArrangement = Arrangement.SpaceBetween,
modifier = Modifier modifier = Modifier.fillMaxSize().padding(5.dp),
.fillMaxSize()
.padding(5.dp),
) { ) {
Row(modifier = Modifier.fillMaxWidth(fillMaxWidth)) { Row(modifier = Modifier.fillMaxWidth(fillMaxWidth)) {
val drawable = val drawable =
@@ -260,10 +248,9 @@ fun ConfigScreen(
modifier = Modifier.size(50.dp, 50.dp), modifier = Modifier.size(50.dp, 50.dp),
) )
} else { } else {
val icon = Icons.Rounded.Android
Icon( Icon(
icon, Icons.Rounded.Android,
icon.name, stringResource(id = R.string.edit),
modifier = Modifier.size(50.dp, 50.dp), modifier = Modifier.size(50.dp, 50.dp),
) )
} }
@@ -275,9 +262,9 @@ fun ConfigScreen(
Checkbox( Checkbox(
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
checked = checked =
(uiState.checkedPackageNames.contains( (uiState.checkedPackageNames.contains(
pack.packageName, pack.packageName
)), )),
onCheckedChange = { onCheckedChange = {
if (it) { if (it) {
viewModel.onAddCheckedPackage(pack.packageName) viewModel.onAddCheckedPackage(pack.packageName)
@@ -292,9 +279,7 @@ fun ConfigScreen(
} }
Row( Row(
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
modifier = Modifier modifier = Modifier.fillMaxSize().padding(top = 5.dp),
.fillMaxSize()
.padding(top = 5.dp),
horizontalArrangement = Arrangement.Center, horizontalArrangement = Arrangement.Center,
) { ) {
TextButton(onClick = { showApplicationsDialog = false }) { TextButton(onClick = { showApplicationsDialog = false }) {
@@ -314,17 +299,20 @@ fun ConfigScreen(
var fobColor by remember { mutableStateOf(secondaryColor) } var fobColor by remember { mutableStateOf(secondaryColor) }
FloatingActionButton( FloatingActionButton(
modifier = modifier =
Modifier.onFocusChanged { Modifier.padding(bottom = 90.dp).onFocusChanged {
if (WireGuardAutoTunnel.isRunningOnAndroidTv()) { if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
fobColor = if (it.isFocused) hoverColor else secondaryColor fobColor = if (it.isFocused) hoverColor else secondaryColor
} }
}, },
onClick = { onClick = {
viewModel.onSaveAllChanges(configType).onSuccess { viewModel.onSaveAllChanges().let {
appViewModel.showSnackbarMessage(context.getString(R.string.config_changes_saved)) when (it) {
navController.navigate(Screen.Main.route) is Result.Success -> {
}.onFailure { showSnackbarMessage(it.data.message)
appViewModel.showSnackbarMessage(it.getMessage(context)) navController.navigate(Screen.Main.route)
}
is Result.Error -> showSnackbarMessage(it.error.message)
}
} }
}, },
containerColor = fobColor, containerColor = fobColor,
@@ -343,10 +331,7 @@ fun ConfigScreen(
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Top, verticalArrangement = Arrangement.Top,
modifier = modifier =
Modifier Modifier.verticalScroll(rememberScrollState()).weight(1f, true).fillMaxSize(),
.verticalScroll(rememberScrollState())
.weight(1f, true)
.fillMaxSize(),
) { ) {
Surface( Surface(
tonalElevation = 2.dp, tonalElevation = 2.dp,
@@ -354,21 +339,17 @@ fun ConfigScreen(
shape = RoundedCornerShape(12.dp), shape = RoundedCornerShape(12.dp),
color = MaterialTheme.colorScheme.surface, color = MaterialTheme.colorScheme.surface,
modifier = modifier =
(if (WireGuardAutoTunnel.isRunningOnAndroidTv()) { (if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
Modifier Modifier.fillMaxHeight(fillMaxHeight).fillMaxWidth(fillMaxWidth)
.fillMaxHeight(fillMaxHeight) } else {
.fillMaxWidth(fillMaxWidth) Modifier.fillMaxWidth(fillMaxWidth)
} else { })
Modifier.fillMaxWidth(fillMaxWidth) .padding(top = 50.dp, bottom = 10.dp),
})
.padding(bottom = 10.dp),
) { ) {
Column( Column(
horizontalAlignment = Alignment.Start, horizontalAlignment = Alignment.Start,
verticalArrangement = Arrangement.Top, verticalArrangement = Arrangement.Top,
modifier = Modifier modifier = Modifier.padding(15.dp).focusGroup(),
.padding(15.dp)
.focusGroup(),
) { ) {
SectionTitle( SectionTitle(
stringResource(R.string.interface_), stringResource(R.string.interface_),
@@ -380,20 +361,16 @@ fun ConfigScreen(
keyboardActions = keyboardActions, keyboardActions = keyboardActions,
label = stringResource(R.string.name), label = stringResource(R.string.name),
hint = stringResource(R.string.tunnel_name).lowercase(), hint = stringResource(R.string.tunnel_name).lowercase(),
modifier = Modifier modifier = Modifier.fillMaxWidth().focusRequester(focusRequester),
.fillMaxWidth()
.focusRequester(focusRequester),
) )
OutlinedTextField( OutlinedTextField(
modifier = Modifier modifier = Modifier.fillMaxWidth().clickable { showAuthPrompt = true },
.fillMaxWidth()
.clickable { showAuthPrompt = true },
value = uiState.interfaceProxy.privateKey, value = uiState.interfaceProxy.privateKey,
visualTransformation = visualTransformation =
if ((tunnelId == Constants.MANUAL_TUNNEL_CONFIG_ID) || isAuthenticated) if ((id == Constants.MANUAL_TUNNEL_CONFIG_ID) || isAuthenticated)
VisualTransformation.None VisualTransformation.None
else PasswordVisualTransformation(), else PasswordVisualTransformation(),
enabled = (tunnelId == Constants.MANUAL_TUNNEL_CONFIG_ID) || isAuthenticated, enabled = (id == Constants.MANUAL_TUNNEL_CONFIG_ID) || isAuthenticated,
onValueChange = { value -> viewModel.onPrivateKeyChange(value) }, onValueChange = { value -> viewModel.onPrivateKeyChange(value) },
trailingIcon = { trailingIcon = {
IconButton( IconButton(
@@ -415,9 +392,7 @@ fun ConfigScreen(
) )
OutlinedTextField( OutlinedTextField(
modifier = modifier =
Modifier Modifier.fillMaxWidth().focusRequester(FocusRequester.Default),
.fillMaxWidth()
.focusRequester(FocusRequester.Default),
value = uiState.interfaceProxy.publicKey, value = uiState.interfaceProxy.publicKey,
enabled = false, enabled = false,
onValueChange = {}, onValueChange = {},
@@ -450,9 +425,7 @@ fun ConfigScreen(
keyboardActions = keyboardActions, keyboardActions = keyboardActions,
label = stringResource(R.string.addresses), label = stringResource(R.string.addresses),
hint = stringResource(R.string.comma_separated_list), hint = stringResource(R.string.comma_separated_list),
modifier = Modifier modifier = Modifier.fillMaxWidth(3 / 5f).padding(end = 5.dp),
.fillMaxWidth(3 / 5f)
.padding(end = 5.dp),
) )
ConfigurationTextBox( ConfigurationTextBox(
value = uiState.interfaceProxy.listenPort, value = uiState.interfaceProxy.listenPort,
@@ -470,9 +443,7 @@ fun ConfigScreen(
keyboardActions = keyboardActions, keyboardActions = keyboardActions,
label = stringResource(R.string.dns_servers), label = stringResource(R.string.dns_servers),
hint = stringResource(R.string.comma_separated_list), hint = stringResource(R.string.comma_separated_list),
modifier = Modifier modifier = Modifier.fillMaxWidth(3 / 5f).padding(end = 5.dp),
.fillMaxWidth(3 / 5f)
.padding(end = 5.dp),
) )
ConfigurationTextBox( ConfigurationTextBox(
value = uiState.interfaceProxy.mtu, value = uiState.interfaceProxy.mtu,
@@ -483,131 +454,13 @@ fun ConfigScreen(
modifier = Modifier.width(IntrinsicSize.Min), modifier = Modifier.width(IntrinsicSize.Min),
) )
} }
if (configType == ConfigType.AMNEZIA) {
ConfigurationTextBox(
value = uiState.interfaceProxy.junkPacketCount,
onValueChange = { value -> viewModel.onJunkPacketCountChanged(value) },
keyboardActions = keyboardActions,
label = stringResource(R.string.junk_packet_count),
hint = stringResource(R.string.junk_packet_count).lowercase(),
modifier = Modifier
.fillMaxWidth()
.focusRequester(focusRequester),
)
ConfigurationTextBox(
value = uiState.interfaceProxy.junkPacketMinSize,
onValueChange = { value ->
viewModel.onJunkPacketMinSizeChanged(
value,
)
},
keyboardActions = keyboardActions,
label = stringResource(R.string.junk_packet_minimum_size),
hint = stringResource(R.string.junk_packet_minimum_size).lowercase(),
modifier = Modifier
.fillMaxWidth()
.focusRequester(focusRequester),
)
ConfigurationTextBox(
value = uiState.interfaceProxy.junkPacketMaxSize,
onValueChange = { value ->
viewModel.onJunkPacketMaxSizeChanged(
value,
)
},
keyboardActions = keyboardActions,
label = stringResource(R.string.junk_packet_maximum_size),
hint = stringResource(R.string.junk_packet_maximum_size).lowercase(),
modifier = Modifier
.fillMaxWidth()
.focusRequester(focusRequester),
)
ConfigurationTextBox(
value = uiState.interfaceProxy.initPacketJunkSize,
onValueChange = { value ->
viewModel.onInitPacketJunkSizeChanged(
value,
)
},
keyboardActions = keyboardActions,
label = stringResource(R.string.init_packet_junk_size),
hint = stringResource(R.string.init_packet_junk_size).lowercase(),
modifier = Modifier
.fillMaxWidth()
.focusRequester(focusRequester),
)
ConfigurationTextBox(
value = uiState.interfaceProxy.responsePacketJunkSize,
onValueChange = { value -> viewModel.onResponsePacketJunkSize(value) },
keyboardActions = keyboardActions,
label = stringResource(R.string.response_packet_junk_size),
hint = stringResource(R.string.response_packet_junk_size).lowercase(),
modifier = Modifier
.fillMaxWidth()
.focusRequester(focusRequester),
)
ConfigurationTextBox(
value = uiState.interfaceProxy.initPacketMagicHeader,
onValueChange = { value -> viewModel.onInitPacketMagicHeader(value) },
keyboardActions = keyboardActions,
label = stringResource(R.string.init_packet_magic_header),
hint = stringResource(R.string.init_packet_magic_header).lowercase(),
modifier = Modifier
.fillMaxWidth()
.focusRequester(focusRequester),
)
ConfigurationTextBox(
value = uiState.interfaceProxy.responsePacketMagicHeader,
onValueChange = { value ->
viewModel.onResponsePacketMagicHeader(
value,
)
},
keyboardActions = keyboardActions,
label = stringResource(R.string.response_packet_magic_header),
hint = stringResource(R.string.response_packet_magic_header).lowercase(),
modifier = Modifier
.fillMaxWidth()
.focusRequester(focusRequester),
)
ConfigurationTextBox(
value = uiState.interfaceProxy.underloadPacketMagicHeader,
onValueChange = { value ->
viewModel.onUnderloadPacketMagicHeader(
value,
)
},
keyboardActions = keyboardActions,
label = stringResource(R.string.underload_packet_magic_header),
hint = stringResource(R.string.underload_packet_magic_header).lowercase(),
modifier = Modifier
.fillMaxWidth()
.focusRequester(focusRequester),
)
ConfigurationTextBox(
value = uiState.interfaceProxy.transportPacketMagicHeader,
onValueChange = { value ->
viewModel.onTransportPacketMagicHeader(
value,
)
},
keyboardActions = keyboardActions,
label = stringResource(R.string.transport_packet_magic_header),
hint = stringResource(R.string.transport_packet_magic_header).lowercase(),
modifier = Modifier
.fillMaxWidth()
.focusRequester(focusRequester),
)
}
Row( Row(
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
modifier = Modifier modifier = Modifier.fillMaxSize().padding(top = 5.dp),
.fillMaxSize()
.padding(top = 5.dp),
horizontalArrangement = Arrangement.Center, horizontalArrangement = Arrangement.Center,
) { ) {
TextButton(onClick = { showApplicationsDialog = true }) { TextButton(onClick = { showApplicationsDialog = true }) {
Text(applicationButtonText.text) Text(applicationButtonText())
} }
} }
} }
@@ -619,36 +472,29 @@ fun ConfigScreen(
shape = RoundedCornerShape(12.dp), shape = RoundedCornerShape(12.dp),
color = MaterialTheme.colorScheme.surface, color = MaterialTheme.colorScheme.surface,
modifier = modifier =
(if (WireGuardAutoTunnel.isRunningOnAndroidTv()) { (if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
Modifier Modifier.fillMaxHeight(fillMaxHeight).fillMaxWidth(fillMaxWidth)
.fillMaxHeight(fillMaxHeight) } else {
.fillMaxWidth(fillMaxWidth) Modifier.fillMaxWidth(fillMaxWidth)
} else { })
Modifier.fillMaxWidth(fillMaxWidth) .padding(top = 10.dp, bottom = 10.dp),
})
.padding(top = 10.dp, bottom = 10.dp),
) { ) {
Column( Column(
horizontalAlignment = Alignment.Start, horizontalAlignment = Alignment.Start,
verticalArrangement = Arrangement.Top, verticalArrangement = Arrangement.Top,
modifier = Modifier modifier = Modifier.padding(horizontal = 15.dp).padding(bottom = 10.dp),
.padding(horizontal = 15.dp)
.padding(bottom = 10.dp),
) { ) {
Row( Row(
horizontalArrangement = Arrangement.SpaceBetween, horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
modifier = Modifier modifier = Modifier.fillMaxWidth().padding(horizontal = 5.dp),
.fillMaxWidth()
.padding(horizontal = 5.dp),
) { ) {
SectionTitle( SectionTitle(
stringResource(R.string.peer), stringResource(R.string.peer),
padding = screenPadding, padding = screenPadding,
) )
IconButton(onClick = { viewModel.onDeletePeer(index) }) { IconButton(onClick = { viewModel.onDeletePeer(index) }) {
val icon = Icons.Rounded.Delete Icon(Icons.Rounded.Delete, stringResource(R.string.delete))
Icon(icon, icon.name)
} }
} }
@@ -724,9 +570,7 @@ fun ConfigScreen(
Row( Row(
horizontalArrangement = Arrangement.SpaceEvenly, horizontalArrangement = Arrangement.SpaceEvenly,
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
modifier = Modifier modifier = Modifier.fillMaxSize().padding(bottom = 140.dp),
.fillMaxSize()
.padding(bottom = 140.dp),
) { ) {
Row( Row(
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
@@ -1,9 +1,8 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.config package com.zaneschepke.wireguardautotunnel.ui.screens.config
import com.wireguard.config.Config import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig import com.zaneschepke.wireguardautotunnel.ui.models.InterfaceProxy
import com.zaneschepke.wireguardautotunnel.ui.screens.config.model.InterfaceProxy import com.zaneschepke.wireguardautotunnel.ui.models.PeerProxy
import com.zaneschepke.wireguardautotunnel.ui.screens.config.model.PeerProxy
import com.zaneschepke.wireguardautotunnel.util.Packages import com.zaneschepke.wireguardautotunnel.util.Packages
data class ConfigUiState( data class ConfigUiState(
@@ -15,59 +14,5 @@ data class ConfigUiState(
val isAllApplicationsEnabled: Boolean = false, val isAllApplicationsEnabled: Boolean = false,
val loading: Boolean = true, val loading: Boolean = true,
val tunnel: TunnelConfig? = null, val tunnel: TunnelConfig? = null,
val tunnelName: String = "", val tunnelName: String = ""
val isAmneziaEnabled: Boolean = false )
) {
companion object {
fun from(config: Config): ConfigUiState {
val proxyPeers = config.peers.map { PeerProxy.from(it) }
val proxyInterface = InterfaceProxy.from(config.`interface`)
var include = true
var isAllApplicationsEnabled = false
val checkedPackages =
if (config.`interface`.includedApplications.isNotEmpty()) {
config.`interface`.includedApplications
} else if (config.`interface`.excludedApplications.isNotEmpty()) {
include = false
config.`interface`.excludedApplications
} else {
isAllApplicationsEnabled = true
emptySet()
}
return ConfigUiState(
proxyPeers,
proxyInterface,
emptyList(),
checkedPackages.toList(),
include,
isAllApplicationsEnabled,
)
}
fun from(config: org.amnezia.awg.config.Config): ConfigUiState {
//TODO update with new values
val proxyPeers = config.peers.map { PeerProxy.from(it) }
val proxyInterface = InterfaceProxy.from(config.`interface`)
var include = true
var isAllApplicationsEnabled = false
val checkedPackages =
if (config.`interface`.includedApplications.isNotEmpty()) {
config.`interface`.includedApplications
} else if (config.`interface`.excludedApplications.isNotEmpty()) {
include = false
config.`interface`.excludedApplications
} else {
isAllApplicationsEnabled = true
emptySet()
}
return ConfigUiState(
proxyPeers,
proxyInterface,
emptyList(),
checkedPackages.toList(),
include,
isAllApplicationsEnabled,
)
}
}
}
@@ -1,6 +1,7 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.config package com.zaneschepke.wireguardautotunnel.ui.screens.config
import android.Manifest import android.Manifest
import android.app.Application
import android.content.pm.PackageInfo import android.content.pm.PackageInfo
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.os.Build import android.os.Build
@@ -11,63 +12,73 @@ import com.wireguard.config.Interface
import com.wireguard.config.Peer import com.wireguard.config.Peer
import com.wireguard.crypto.Key import com.wireguard.crypto.Key
import com.wireguard.crypto.KeyPair import com.wireguard.crypto.KeyPair
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
import com.zaneschepke.wireguardautotunnel.data.repository.SettingsRepository import com.zaneschepke.wireguardautotunnel.data.repository.SettingsRepository
import com.zaneschepke.wireguardautotunnel.module.IoDispatcher import com.zaneschepke.wireguardautotunnel.data.repository.TunnelConfigRepository
import com.zaneschepke.wireguardautotunnel.ui.screens.config.model.PeerProxy import com.zaneschepke.wireguardautotunnel.ui.models.InterfaceProxy
import com.zaneschepke.wireguardautotunnel.ui.screens.main.ConfigType import com.zaneschepke.wireguardautotunnel.ui.models.PeerProxy
import com.zaneschepke.wireguardautotunnel.util.Constants import com.zaneschepke.wireguardautotunnel.util.Constants
import com.zaneschepke.wireguardautotunnel.util.Event
import com.zaneschepke.wireguardautotunnel.util.NumberUtils import com.zaneschepke.wireguardautotunnel.util.NumberUtils
import com.zaneschepke.wireguardautotunnel.util.StringValue import com.zaneschepke.wireguardautotunnel.util.Result
import com.zaneschepke.wireguardautotunnel.util.WgTunnelExceptions
import com.zaneschepke.wireguardautotunnel.util.removeAt import com.zaneschepke.wireguardautotunnel.util.removeAt
import com.zaneschepke.wireguardautotunnel.util.update import com.zaneschepke.wireguardautotunnel.util.update
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
@HiltViewModel @HiltViewModel
class ConfigViewModel class ConfigViewModel
@Inject @Inject
constructor( constructor(
private val application: Application,
private val tunnelConfigRepository: TunnelConfigRepository,
private val settingsRepository: SettingsRepository, private val settingsRepository: SettingsRepository,
private val appDataRepository: AppDataRepository,
@IoDispatcher private val ioDispatcher: CoroutineDispatcher
) : ViewModel() { ) : ViewModel() {
private val packageManager = WireGuardAutoTunnel.instance.packageManager private val packageManager = application.packageManager
private val _uiState = MutableStateFlow(ConfigUiState()) private val _uiState = MutableStateFlow(ConfigUiState())
val uiState = _uiState.asStateFlow() val uiState = _uiState.asStateFlow()
fun init(tunnelId: String) = fun init(tunnelId: String) =
viewModelScope.launch(ioDispatcher) { viewModelScope.launch(Dispatchers.IO) {
val packages = getQueriedPackages("") val packages = getQueriedPackages("")
val state = val state =
if (tunnelId != Constants.MANUAL_TUNNEL_CONFIG_ID) { if (tunnelId != Constants.MANUAL_TUNNEL_CONFIG_ID) {
val tunnelConfig = val tunnelConfig =
appDataRepository.tunnels.getAll() tunnelConfigRepository.getAll().firstOrNull { it.id.toString() == tunnelId }
.firstOrNull { it.id.toString() == tunnelId }
val isAmneziaEnabled = settingsRepository.getSettings().isAmneziaEnabled
if (tunnelConfig != null) { if (tunnelConfig != null) {
(if (isAmneziaEnabled) { val config = TunnelConfig.configFromQuick(tunnelConfig.wgQuick)
val amConfig = val proxyPeers = config.peers.map { PeerProxy.from(it) }
if (tunnelConfig.amQuick == "") tunnelConfig.wgQuick else tunnelConfig.amQuick val proxyInterface = InterfaceProxy.from(config.`interface`)
ConfigUiState.from(TunnelConfig.configFromAmQuick(amConfig)) var include = true
} else ConfigUiState.from(TunnelConfig.configFromWgQuick(tunnelConfig.wgQuick))).copy( var isAllApplicationsEnabled = false
packages = packages, val checkedPackages =
loading = false, if (config.`interface`.includedApplications.isNotEmpty()) {
tunnel = tunnelConfig, config.`interface`.includedApplications
tunnelName = tunnelConfig.name, } else if (config.`interface`.excludedApplications.isNotEmpty()) {
isAmneziaEnabled = isAmneziaEnabled, include = false
config.`interface`.excludedApplications
} else {
isAllApplicationsEnabled = true
emptySet()
}
ConfigUiState(
proxyPeers,
proxyInterface,
packages,
checkedPackages.toList(),
include,
isAllApplicationsEnabled,
false,
tunnelConfig,
tunnelConfig.name,
) )
} else { } else {
ConfigUiState(loading = false, packages = packages) ConfigUiState(loading = false, packages = packages)
@@ -89,7 +100,7 @@ constructor(
fun onAddCheckedPackage(packageName: String) { fun onAddCheckedPackage(packageName: String) {
_uiState.value = _uiState.value =
_uiState.value.copy( _uiState.value.copy(
checkedPackageNames = _uiState.value.checkedPackageNames + packageName, checkedPackageNames = _uiState.value.checkedPackageNames + packageName
) )
} }
@@ -100,7 +111,7 @@ constructor(
fun onRemoveCheckedPackage(packageName: String) { fun onRemoveCheckedPackage(packageName: String) {
_uiState.value = _uiState.value =
_uiState.value.copy( _uiState.value.copy(
checkedPackageNames = _uiState.value.checkedPackageNames - packageName, checkedPackageNames = _uiState.value.checkedPackageNames - packageName
) )
} }
@@ -111,7 +122,7 @@ constructor(
} }
fun getPackageLabel(packageInfo: PackageInfo): String { fun getPackageLabel(packageInfo: PackageInfo): String {
return packageInfo.applicationInfo.loadLabel(packageManager).toString() return packageInfo.applicationInfo.loadLabel(application.packageManager).toString()
} }
private fun getAllInternetCapablePackages(): List<PackageInfo> { private fun getAllInternetCapablePackages(): List<PackageInfo> {
@@ -134,16 +145,26 @@ constructor(
} }
private fun saveConfig(tunnelConfig: TunnelConfig) = private fun saveConfig(tunnelConfig: TunnelConfig) =
viewModelScope.launch { appDataRepository.tunnels.save(tunnelConfig) } viewModelScope.launch { tunnelConfigRepository.save(tunnelConfig) }
private fun updateTunnelConfig(tunnelConfig: TunnelConfig?) = private fun updateTunnelConfig(tunnelConfig: TunnelConfig?) =
viewModelScope.launch { viewModelScope.launch {
if (tunnelConfig != null) { if (tunnelConfig != null) {
saveConfig(tunnelConfig).join() saveConfig(tunnelConfig).join()
WireGuardAutoTunnel.requestTunnelTileServiceStateUpdate() WireGuardAutoTunnel.requestTileServiceStateUpdate()
updateSettingsDefaultTunnel(tunnelConfig)
} }
} }
private suspend fun updateSettingsDefaultTunnel(tunnelConfig: TunnelConfig) {
val settings = settingsRepository.getSettingsFlow().first()
if (settings.defaultTunnel != null) {
if (tunnelConfig.id == TunnelConfig.from(settings.defaultTunnel!!).id) {
settingsRepository.save(settings.copy(defaultTunnel = tunnelConfig.toString()))
}
}
}
private fun buildPeerListFromProxyPeers(): List<Peer> { private fun buildPeerListFromProxyPeers(): List<Peer> {
return _uiState.value.proxyPeers.map { return _uiState.value.proxyPeers.map {
val builder = Peer.Builder() val builder = Peer.Builder()
@@ -158,20 +179,6 @@ constructor(
} }
} }
private fun buildAmPeerListFromProxyPeers(): List<org.amnezia.awg.config.Peer> {
return _uiState.value.proxyPeers.map {
val builder = org.amnezia.awg.config.Peer.Builder()
if (it.allowedIps.isNotEmpty()) builder.parseAllowedIPs(it.allowedIps.trim())
if (it.publicKey.isNotEmpty()) builder.parsePublicKey(it.publicKey.trim())
if (it.preSharedKey.isNotEmpty()) builder.parsePreSharedKey(it.preSharedKey.trim())
if (it.endpoint.isNotEmpty()) builder.parseEndpoint(it.endpoint.trim())
if (it.persistentKeepalive.isNotEmpty()) {
builder.parsePersistentKeepalive(it.persistentKeepalive.trim())
}
builder.build()
}
}
private fun emptyCheckedPackagesList() { private fun emptyCheckedPackagesList() {
_uiState.value = _uiState.value.copy(checkedPackageNames = emptyList()) _uiState.value = _uiState.value.copy(checkedPackageNames = emptyList())
} }
@@ -194,244 +201,141 @@ constructor(
return builder.build() return builder.build()
} }
private fun buildAmInterfaceListFromProxyInterface(): org.amnezia.awg.config.Interface { fun onSaveAllChanges(): Result<Event> {
val builder = org.amnezia.awg.config.Interface.Builder()
builder.parsePrivateKey(_uiState.value.interfaceProxy.privateKey.trim())
builder.parseAddresses(_uiState.value.interfaceProxy.addresses.trim())
if (_uiState.value.interfaceProxy.dnsServers.isNotEmpty()) {
builder.parseDnsServers(_uiState.value.interfaceProxy.dnsServers.trim())
}
if (_uiState.value.interfaceProxy.mtu.isNotEmpty())
builder.parseMtu(_uiState.value.interfaceProxy.mtu.trim())
if (_uiState.value.interfaceProxy.listenPort.isNotEmpty()) {
builder.parseListenPort(_uiState.value.interfaceProxy.listenPort.trim())
}
if (isAllApplicationsEnabled()) emptyCheckedPackagesList()
if (_uiState.value.include) builder.includeApplications(_uiState.value.checkedPackageNames)
if (!_uiState.value.include) builder.excludeApplications(_uiState.value.checkedPackageNames)
if (_uiState.value.interfaceProxy.junkPacketCount.isNotEmpty()) {
builder.setJunkPacketCount(_uiState.value.interfaceProxy.junkPacketCount.trim().toInt())
}
if (_uiState.value.interfaceProxy.junkPacketMinSize.isNotEmpty()) {
builder.setJunkPacketMinSize(
_uiState.value.interfaceProxy.junkPacketMinSize.trim().toInt(),
)
}
if (_uiState.value.interfaceProxy.junkPacketMaxSize.isNotEmpty()) {
builder.setJunkPacketMaxSize(
_uiState.value.interfaceProxy.junkPacketMaxSize.trim().toInt(),
)
}
if (_uiState.value.interfaceProxy.initPacketJunkSize.isNotEmpty()) {
builder.setInitPacketJunkSize(
_uiState.value.interfaceProxy.initPacketJunkSize.trim().toInt(),
)
}
if (_uiState.value.interfaceProxy.responsePacketJunkSize.isNotEmpty()) {
builder.setResponsePacketJunkSize(
_uiState.value.interfaceProxy.responsePacketJunkSize.trim().toInt(),
)
}
if (_uiState.value.interfaceProxy.initPacketMagicHeader.isNotEmpty()) {
builder.setInitPacketMagicHeader(
_uiState.value.interfaceProxy.initPacketMagicHeader.trim().toLong(),
)
}
if (_uiState.value.interfaceProxy.responsePacketMagicHeader.isNotEmpty()) {
builder.setResponsePacketMagicHeader(
_uiState.value.interfaceProxy.responsePacketMagicHeader.trim().toLong(),
)
}
if (_uiState.value.interfaceProxy.transportPacketMagicHeader.isNotEmpty()) {
builder.setTransportPacketMagicHeader(
_uiState.value.interfaceProxy.transportPacketMagicHeader.trim().toLong(),
)
}
if (_uiState.value.interfaceProxy.underloadPacketMagicHeader.isNotEmpty()) {
builder.setUnderloadPacketMagicHeader(
_uiState.value.interfaceProxy.underloadPacketMagicHeader.trim().toLong(),
)
}
return builder.build()
}
private fun buildConfig(): Config {
val peerList = buildPeerListFromProxyPeers()
val wgInterface = buildInterfaceListFromProxyInterface()
return Config.Builder().addPeers(peerList).setInterface(wgInterface).build()
}
private fun buildAmConfig(): org.amnezia.awg.config.Config {
val peerList = buildAmPeerListFromProxyPeers()
val amInterface = buildAmInterfaceListFromProxyInterface()
return org.amnezia.awg.config.Config.Builder().addPeers(peerList).setInterface(amInterface)
.build()
}
fun onSaveAllChanges(configType: ConfigType): Result<Unit> {
return try { return try {
val wgQuick = buildConfig().toWgQuickString() val peerList = buildPeerListFromProxyPeers()
val amQuick = if (configType == ConfigType.AMNEZIA) { val wgInterface = buildInterfaceListFromProxyInterface()
buildAmConfig().toAwgQuickString() val config = Config.Builder().addPeers(peerList).setInterface(wgInterface).build()
} else TunnelConfig.AM_QUICK_DEFAULT val tunnelConfig = when(uiState.value.tunnel) {
val tunnelConfig = when (uiState.value.tunnel) { null -> TunnelConfig(name = _uiState.value.tunnelName, wgQuick = config.toWgQuickString())
null -> TunnelConfig(
name = _uiState.value.tunnelName,
wgQuick = wgQuick,
amQuick = amQuick,
)
else -> uiState.value.tunnel!!.copy( else -> uiState.value.tunnel!!.copy(
name = _uiState.value.tunnelName, name = _uiState.value.tunnelName,
wgQuick = wgQuick, wgQuick = config.toWgQuickString(),
amQuick = amQuick,
) )
} }
updateTunnelConfig(tunnelConfig) updateTunnelConfig(tunnelConfig)
Result.success(Unit) Result.Success(Event.Message.ConfigSaved)
} catch (e: Exception) { } catch (e: Exception) {
Timber.e(e) Result.Error(Event.Error.Exception(e))
val message = e.message?.substringAfter(":", missingDelimiterValue = "")
val stringValue = message?.let {
StringValue.DynamicString(message)
} ?: StringValue.StringResource(R.string.unknown_error)
Result.failure(WgTunnelExceptions.ConfigParseError(stringValue))
} }
} }
fun onPeerPublicKeyChange(index: Int, value: String) { fun onPeerPublicKeyChange(index: Int, value: String) {
_uiState.update { _uiState.value =
it.copy( _uiState.value.copy(
proxyPeers = proxyPeers =
_uiState.value.proxyPeers.update( _uiState.value.proxyPeers.update(
index, index,
_uiState.value.proxyPeers[index].copy(publicKey = value), _uiState.value.proxyPeers[index].copy(publicKey = value),
), ),
) )
}
} }
fun onPreSharedKeyChange(index: Int, value: String) { fun onPreSharedKeyChange(index: Int, value: String) {
_uiState.update { _uiState.value =
it.copy( _uiState.value.copy(
proxyPeers = proxyPeers =
_uiState.value.proxyPeers.update( _uiState.value.proxyPeers.update(
index, index,
_uiState.value.proxyPeers[index].copy(preSharedKey = value), _uiState.value.proxyPeers[index].copy(preSharedKey = value),
), ),
) )
}
} }
fun onEndpointChange(index: Int, value: String) { fun onEndpointChange(index: Int, value: String) {
_uiState.update { _uiState.value =
it.copy( _uiState.value.copy(
proxyPeers = proxyPeers =
_uiState.value.proxyPeers.update( _uiState.value.proxyPeers.update(
index, index,
_uiState.value.proxyPeers[index].copy(endpoint = value), _uiState.value.proxyPeers[index].copy(endpoint = value),
), ),
) )
}
} }
fun onAllowedIpsChange(index: Int, value: String) { fun onAllowedIpsChange(index: Int, value: String) {
_uiState.update { _uiState.value =
it.copy( _uiState.value.copy(
proxyPeers = proxyPeers =
_uiState.value.proxyPeers.update( _uiState.value.proxyPeers.update(
index, index,
_uiState.value.proxyPeers[index].copy(allowedIps = value), _uiState.value.proxyPeers[index].copy(allowedIps = value),
), ),
) )
}
} }
fun onPersistentKeepaliveChanged(index: Int, value: String) { fun onPersistentKeepaliveChanged(index: Int, value: String) {
_uiState.update { _uiState.value =
it.copy( _uiState.value.copy(
proxyPeers = proxyPeers =
_uiState.value.proxyPeers.update( _uiState.value.proxyPeers.update(
index, index,
_uiState.value.proxyPeers[index].copy(persistentKeepalive = value), _uiState.value.proxyPeers[index].copy(persistentKeepalive = value),
), ),
) )
}
} }
fun onDeletePeer(index: Int) { fun onDeletePeer(index: Int) {
_uiState.update { _uiState.value =
it.copy( _uiState.value.copy(
proxyPeers = _uiState.value.proxyPeers.removeAt(index), proxyPeers = _uiState.value.proxyPeers.removeAt(index),
) )
}
} }
fun addEmptyPeer() { fun addEmptyPeer() {
_uiState.update { _uiState.value = _uiState.value.copy(proxyPeers = _uiState.value.proxyPeers + PeerProxy())
it.copy(proxyPeers = _uiState.value.proxyPeers + PeerProxy())
}
} }
fun generateKeyPair() { fun generateKeyPair() {
val keyPair = KeyPair() val keyPair = KeyPair()
_uiState.update { _uiState.value =
it.copy( _uiState.value.copy(
interfaceProxy = interfaceProxy =
_uiState.value.interfaceProxy.copy( _uiState.value.interfaceProxy.copy(
privateKey = keyPair.privateKey.toBase64(), privateKey = keyPair.privateKey.toBase64(),
publicKey = keyPair.publicKey.toBase64(), publicKey = keyPair.publicKey.toBase64(),
), ),
) )
}
} }
fun onAddressesChanged(value: String) { fun onAddressesChanged(value: String) {
_uiState.update { _uiState.value =
it.copy( _uiState.value.copy(
interfaceProxy = _uiState.value.interfaceProxy.copy(addresses = value), interfaceProxy = _uiState.value.interfaceProxy.copy(addresses = value)
) )
}
} }
fun onListenPortChanged(value: String) { fun onListenPortChanged(value: String) {
_uiState.update { _uiState.value =
it.copy( _uiState.value.copy(
interfaceProxy = _uiState.value.interfaceProxy.copy(listenPort = value), interfaceProxy = _uiState.value.interfaceProxy.copy(listenPort = value)
) )
}
} }
fun onDnsServersChanged(value: String) { fun onDnsServersChanged(value: String) {
_uiState.update { _uiState.value =
it.copy( _uiState.value.copy(
interfaceProxy = _uiState.value.interfaceProxy.copy(dnsServers = value), interfaceProxy = _uiState.value.interfaceProxy.copy(dnsServers = value)
) )
}
} }
fun onMtuChanged(value: String) { fun onMtuChanged(value: String) {
_uiState.update { _uiState.value =
it.copy(interfaceProxy = _uiState.value.interfaceProxy.copy(mtu = value)) _uiState.value.copy(interfaceProxy = _uiState.value.interfaceProxy.copy(mtu = value))
}
} }
private fun onInterfacePublicKeyChange(value: String) { private fun onInterfacePublicKeyChange(value: String) {
_uiState.update { _uiState.value =
it.copy( _uiState.value.copy(
interfaceProxy = _uiState.value.interfaceProxy.copy(publicKey = value), interfaceProxy = _uiState.value.interfaceProxy.copy(publicKey = value)
) )
}
} }
fun onPrivateKeyChange(value: String) { fun onPrivateKeyChange(value: String) {
_uiState.update { _uiState.value =
it.copy( _uiState.value.copy(
interfaceProxy = _uiState.value.interfaceProxy.copy(privateKey = value), interfaceProxy = _uiState.value.interfaceProxy.copy(privateKey = value)
) )
}
if (NumberUtils.isValidKey(value)) { if (NumberUtils.isValidKey(value)) {
val pair = KeyPair(Key.fromBase64(value)) val pair = KeyPair(Key.fromBase64(value))
onInterfacePublicKeyChange(pair.publicKey.toBase64()) onInterfacePublicKeyChange(pair.publicKey.toBase64())
@@ -445,78 +349,6 @@ constructor(
getAllInternetCapablePackages().filter { getAllInternetCapablePackages().filter {
getPackageLabel(it).lowercase().contains(query.lowercase()) getPackageLabel(it).lowercase().contains(query.lowercase())
} }
_uiState.update { it.copy(packages = packages) } _uiState.value = _uiState.value.copy(packages = packages)
}
fun onJunkPacketCountChanged(value: String) {
_uiState.update {
it.copy(
interfaceProxy = _uiState.value.interfaceProxy.copy(junkPacketCount = value),
)
}
}
fun onJunkPacketMinSizeChanged(value: String) {
_uiState.update {
it.copy(
interfaceProxy = _uiState.value.interfaceProxy.copy(junkPacketMinSize = value),
)
}
}
fun onJunkPacketMaxSizeChanged(value: String) {
_uiState.update {
it.copy(
interfaceProxy = _uiState.value.interfaceProxy.copy(junkPacketMaxSize = value),
)
}
}
fun onInitPacketJunkSizeChanged(value: String) {
_uiState.update {
it.copy(
interfaceProxy = _uiState.value.interfaceProxy.copy(initPacketJunkSize = value),
)
}
}
fun onResponsePacketJunkSize(value: String) {
_uiState.update {
it.copy(
interfaceProxy = _uiState.value.interfaceProxy.copy(responsePacketJunkSize = value),
)
}
}
fun onInitPacketMagicHeader(value: String) {
_uiState.update {
it.copy(
interfaceProxy = _uiState.value.interfaceProxy.copy(initPacketMagicHeader = value),
)
}
}
fun onResponsePacketMagicHeader(value: String) {
_uiState.update {
it.copy(
interfaceProxy = _uiState.value.interfaceProxy.copy(responsePacketMagicHeader = value),
)
}
}
fun onTransportPacketMagicHeader(value: String) {
_uiState.update {
it.copy(
interfaceProxy = _uiState.value.interfaceProxy.copy(transportPacketMagicHeader = value),
)
}
}
fun onUnderloadPacketMagicHeader(value: String) {
_uiState.update {
it.copy(
interfaceProxy = _uiState.value.interfaceProxy.copy(underloadPacketMagicHeader = value),
)
}
} }
} }
@@ -1,73 +0,0 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.config.model
import com.wireguard.config.Interface
data class InterfaceProxy(
val privateKey: String = "",
val publicKey: String = "",
val addresses: String = "",
val dnsServers: String = "",
val listenPort: String = "",
val mtu: String = "",
val junkPacketCount: String = "",
val junkPacketMinSize: String = "",
val junkPacketMaxSize: String = "",
val initPacketJunkSize: String = "",
val responsePacketJunkSize: String = "",
val initPacketMagicHeader: String = "",
val responsePacketMagicHeader: String = "",
val underloadPacketMagicHeader: String = "",
val transportPacketMagicHeader: String = "",
) {
companion object {
fun from(i: Interface): InterfaceProxy {
return InterfaceProxy(
publicKey = i.keyPair.publicKey.toBase64().trim(),
privateKey = i.keyPair.privateKey.toBase64().trim(),
addresses = i.addresses.joinToString(", ").trim(),
dnsServers = i.dnsServers.joinToString(", ").replace("/", "").trim(),
listenPort =
if (i.listenPort.isPresent) {
i.listenPort.get().toString().trim()
} else {
""
},
mtu = if (i.mtu.isPresent) i.mtu.get().toString().trim() else "",
)
}
fun from(i: org.amnezia.awg.config.Interface): InterfaceProxy {
return InterfaceProxy(
publicKey = i.keyPair.publicKey.toBase64().trim(),
privateKey = i.keyPair.privateKey.toBase64().trim(),
addresses = i.addresses.joinToString(", ").trim(),
dnsServers = i.dnsServers.joinToString(", ").replace("/", "").trim(),
listenPort =
if (i.listenPort.isPresent) {
i.listenPort.get().toString().trim()
} else {
""
},
mtu = if (i.mtu.isPresent) i.mtu.get().toString().trim() else "",
junkPacketCount = if (i.junkPacketCount.isPresent) i.junkPacketCount.get()
.toString() else "",
junkPacketMinSize = if (i.junkPacketMinSize.isPresent) i.junkPacketMinSize.get()
.toString() else "",
junkPacketMaxSize = if (i.junkPacketMaxSize.isPresent) i.junkPacketMaxSize.get()
.toString() else "",
initPacketJunkSize = if (i.initPacketJunkSize.isPresent) i.initPacketJunkSize.get()
.toString() else "",
responsePacketJunkSize = if (i.responsePacketJunkSize.isPresent) i.responsePacketJunkSize.get()
.toString() else "",
initPacketMagicHeader = if (i.initPacketMagicHeader.isPresent) i.initPacketMagicHeader.get()
.toString() else "",
responsePacketMagicHeader = if (i.responsePacketMagicHeader.isPresent) i.responsePacketMagicHeader.get()
.toString() else "",
transportPacketMagicHeader = if (i.transportPacketMagicHeader.isPresent) i.transportPacketMagicHeader.get()
.toString() else "",
underloadPacketMagicHeader = if (i.underloadPacketMagicHeader.isPresent) i.underloadPacketMagicHeader.get()
.toString() else "",
)
}
}
}
@@ -1,98 +0,0 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.config.model
import com.wireguard.config.Peer
data class PeerProxy(
val publicKey: String = "",
val preSharedKey: String = "",
val persistentKeepalive: String = "",
val endpoint: String = "",
val allowedIps: String = IPV4_WILDCARD.joinToString(", ").trim()
) {
companion object {
fun from(peer: Peer): PeerProxy {
return PeerProxy(
publicKey = peer.publicKey.toBase64(),
preSharedKey =
if (peer.preSharedKey.isPresent) {
peer.preSharedKey.get().toBase64().trim()
} else {
""
},
persistentKeepalive =
if (peer.persistentKeepalive.isPresent) {
peer.persistentKeepalive.get().toString().trim()
} else {
""
},
endpoint =
if (peer.endpoint.isPresent) {
peer.endpoint.get().toString().trim()
} else {
""
},
allowedIps = peer.allowedIps.joinToString(", ").trim(),
)
}
fun from(peer: org.amnezia.awg.config.Peer): PeerProxy {
return PeerProxy(
publicKey = peer.publicKey.toBase64(),
preSharedKey =
if (peer.preSharedKey.isPresent) {
peer.preSharedKey.get().toBase64().trim()
} else {
""
},
persistentKeepalive =
if (peer.persistentKeepalive.isPresent) {
peer.persistentKeepalive.get().toString().trim()
} else {
""
},
endpoint =
if (peer.endpoint.isPresent) {
peer.endpoint.get().toString().trim()
} else {
""
},
allowedIps = peer.allowedIps.joinToString(", ").trim(),
)
}
val IPV4_PUBLIC_NETWORKS =
setOf(
"0.0.0.0/5",
"8.0.0.0/7",
"11.0.0.0/8",
"12.0.0.0/6",
"16.0.0.0/4",
"32.0.0.0/3",
"64.0.0.0/2",
"128.0.0.0/3",
"160.0.0.0/5",
"168.0.0.0/6",
"172.0.0.0/12",
"172.32.0.0/11",
"172.64.0.0/10",
"172.128.0.0/9",
"173.0.0.0/8",
"174.0.0.0/7",
"176.0.0.0/4",
"192.0.0.0/9",
"192.128.0.0/11",
"192.160.0.0/13",
"192.169.0.0/16",
"192.170.0.0/15",
"192.172.0.0/14",
"192.176.0.0/12",
"192.192.0.0/10",
"193.0.0.0/8",
"194.0.0.0/7",
"196.0.0.0/6",
"200.0.0.0/5",
"208.0.0.0/4",
)
val IPV4_WILDCARD = setOf("0.0.0.0/0")
}
}
@@ -1,6 +0,0 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.main
enum class ConfigType {
AMNEZIA,
WIREGUARD
}
@@ -14,47 +14,50 @@ import androidx.compose.animation.slideInVertically
import androidx.compose.animation.slideOutVertically import androidx.compose.animation.slideOutVertically
import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.focusGroup
import androidx.compose.foundation.focusable import androidx.compose.foundation.focusable
import androidx.compose.foundation.gestures.ScrollableDefaults import androidx.compose.foundation.gestures.ScrollableDefaults
import androidx.compose.foundation.gestures.detectTapGestures import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.requiredWidth
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.overscroll import androidx.compose.foundation.overscroll
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.ClickableText
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Create import androidx.compose.material.icons.filled.Create
import androidx.compose.material.icons.filled.FileOpen import androidx.compose.material.icons.filled.FileOpen
import androidx.compose.material.icons.filled.QrCode import androidx.compose.material.icons.filled.QrCode
import androidx.compose.material.icons.rounded.Add
import androidx.compose.material.icons.rounded.Bolt import androidx.compose.material.icons.rounded.Bolt
import androidx.compose.material.icons.rounded.Circle import androidx.compose.material.icons.rounded.Circle
import androidx.compose.material.icons.rounded.CopyAll
import androidx.compose.material.icons.rounded.Delete import androidx.compose.material.icons.rounded.Delete
import androidx.compose.material.icons.rounded.Edit
import androidx.compose.material.icons.rounded.Info import androidx.compose.material.icons.rounded.Info
import androidx.compose.material.icons.rounded.Settings
import androidx.compose.material.icons.rounded.Smartphone
import androidx.compose.material.icons.rounded.Star import androidx.compose.material.icons.rounded.Star
import androidx.compose.material3.AlertDialog import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Divider
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.FabPosition import androidx.compose.material3.FabPosition
import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.FloatingActionButton
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.MaterialTheme.typography
import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.material3.Switch import androidx.compose.material3.Switch
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextButton import androidx.compose.material3.TextButton
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.material3.surfaceColorAtElevation import androidx.compose.material3.surfaceColorAtElevation
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
@@ -70,97 +73,77 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.geometry.Offset import androidx.compose.ui.focus.onFocusChanged
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.hapticfeedback.HapticFeedbackType import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
import androidx.compose.ui.input.nestedscroll.NestedScrollSource
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalHapticFeedback import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.font.FontStyle import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.withStyle
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.navigation.NavController import androidx.navigation.NavController
import com.iamageo.multifablibrary.FabIcon
import com.iamageo.multifablibrary.FabOption
import com.iamageo.multifablibrary.MultiFabItem
import com.iamageo.multifablibrary.MultiFloatingActionButton
import com.journeyapps.barcodescanner.ScanContract import com.journeyapps.barcodescanner.ScanContract
import com.journeyapps.barcodescanner.ScanOptions import com.journeyapps.barcodescanner.ScanOptions
import com.wireguard.android.backend.Tunnel
import com.zaneschepke.wireguardautotunnel.R import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
import com.zaneschepke.wireguardautotunnel.service.tunnel.HandshakeStatus import com.zaneschepke.wireguardautotunnel.service.tunnel.HandshakeStatus
import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelState import com.zaneschepke.wireguardautotunnel.ui.CaptureActivityPortrait
import com.zaneschepke.wireguardautotunnel.ui.AppViewModel
import com.zaneschepke.wireguardautotunnel.ui.Screen import com.zaneschepke.wireguardautotunnel.ui.Screen
import com.zaneschepke.wireguardautotunnel.ui.common.RowListItem import com.zaneschepke.wireguardautotunnel.ui.common.RowListItem
import com.zaneschepke.wireguardautotunnel.ui.common.screen.LoadingScreen import com.zaneschepke.wireguardautotunnel.ui.common.screen.LoadingScreen
import com.zaneschepke.wireguardautotunnel.ui.theme.corn import com.zaneschepke.wireguardautotunnel.ui.theme.corn
import com.zaneschepke.wireguardautotunnel.ui.theme.mint import com.zaneschepke.wireguardautotunnel.ui.theme.mint
import com.zaneschepke.wireguardautotunnel.util.Constants import com.zaneschepke.wireguardautotunnel.util.Constants
import com.zaneschepke.wireguardautotunnel.util.getMessage import com.zaneschepke.wireguardautotunnel.util.Event
import com.zaneschepke.wireguardautotunnel.util.Result
import com.zaneschepke.wireguardautotunnel.util.handshakeStatus import com.zaneschepke.wireguardautotunnel.util.handshakeStatus
import com.zaneschepke.wireguardautotunnel.util.mapPeerStats import com.zaneschepke.wireguardautotunnel.util.mapPeerStats
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import timber.log.Timber
@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter") @SuppressLint("UnusedMaterial3ScaffoldPaddingParameter")
@OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class) @OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class)
@Composable @Composable
fun MainScreen( fun MainScreen(
viewModel: MainViewModel = hiltViewModel(), viewModel: MainViewModel = hiltViewModel(),
appViewModel: AppViewModel, padding: PaddingValues,
focusRequester: FocusRequester, focusRequester: FocusRequester,
showSnackbarMessage: (String) -> Unit,
navController: NavController navController: NavController
) { ) {
val haptic = LocalHapticFeedback.current val haptic = LocalHapticFeedback.current
val context = LocalContext.current val context = LocalContext.current
val isVisible = rememberSaveable { mutableStateOf(true) } val isVisible = rememberSaveable { mutableStateOf(true) }
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope { Dispatchers.IO }
val sheetState = rememberModalBottomSheetState() val sheetState = rememberModalBottomSheetState()
var showBottomSheet by remember { mutableStateOf(false) } var showBottomSheet by remember { mutableStateOf(false) }
var configType by remember { mutableStateOf(ConfigType.WIREGUARD) }
// Nested scroll for control FAB
val nestedScrollConnection = remember {
object : NestedScrollConnection {
override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
// Hide FAB
if (available.y < -1) {
isVisible.value = false
}
// Show FAB
if (available.y > 1) {
isVisible.value = true
}
return Offset.Zero
}
}
}
var showPrimaryChangeAlertDialog by remember { mutableStateOf(false) }
var showDeleteTunnelAlertDialog by remember { mutableStateOf(false) } var showDeleteTunnelAlertDialog by remember { mutableStateOf(false) }
var selectedTunnel by remember { mutableStateOf<TunnelConfig?>(null) } var selectedTunnel by remember { mutableStateOf<TunnelConfig?>(null) }
val uiState by viewModel.uiState.collectAsStateWithLifecycle() val uiState by viewModel.uiState.collectAsStateWithLifecycle()
LaunchedEffect(Unit) { LaunchedEffect(uiState.loading) {
if (WireGuardAutoTunnel.isRunningOnAndroidTv()) { if (!uiState.loading && WireGuardAutoTunnel.isRunningOnAndroidTv()) {
delay(Constants.FOCUS_REQUEST_DELAY) delay(Constants.FOCUS_REQUEST_DELAY)
focusRequester.requestFocus() focusRequester.requestFocus()
} }
} }
if (uiState.loading) {
LoadingScreen()
return
}
val tunnelFileImportResultLauncher = val tunnelFileImportResultLauncher =
rememberLauncherForActivityResult( rememberLauncherForActivityResult(
object : ActivityResultContracts.GetContent() { object : ActivityResultContracts.GetContent() {
@@ -190,7 +173,7 @@ fun MainScreen(
name.startsWith(Constants.ANDROID_TV_EXPLORER_STUB) name.startsWith(Constants.ANDROID_TV_EXPLORER_STUB)
} }
) { ) {
appViewModel.showSnackbarMessage(context.getString(R.string.error_no_file_explorer)) showSnackbarMessage(Event.Error.FileExplorerRequired.message)
} }
return intent return intent
} }
@@ -198,8 +181,11 @@ fun MainScreen(
) { data -> ) { data ->
if (data == null) return@rememberLauncherForActivityResult if (data == null) return@rememberLauncherForActivityResult
scope.launch { scope.launch {
viewModel.onTunnelFileSelected(data, configType, context).onFailure { viewModel.onTunnelFileSelected(data).let {
appViewModel.showSnackbarMessage(it.getMessage(context)) when (it) {
is Result.Error -> showSnackbarMessage(it.error.message)
is Result.Success -> {}
}
} }
} }
} }
@@ -209,21 +195,48 @@ fun MainScreen(
onResult = { onResult = {
if (it.contents != null) { if (it.contents != null) {
scope.launch { scope.launch {
viewModel.onTunnelQrResult(it.contents, configType).onFailure { error -> viewModel.onTunnelQrResult(it.contents).let { result ->
appViewModel.showSnackbarMessage(error.getMessage(context)) when (result) {
is Result.Success -> {}
is Result.Error -> showSnackbarMessage(result.error.message)
}
} }
} }
} }
}, },
) )
AnimatedVisibility(showPrimaryChangeAlertDialog) {
AlertDialog(
onDismissRequest = { showPrimaryChangeAlertDialog = false },
confirmButton = {
TextButton(
onClick = {
viewModel.onDefaultTunnelChange(selectedTunnel)
showPrimaryChangeAlertDialog = false
selectedTunnel = null
},
) {
Text(text = stringResource(R.string.okay))
}
},
dismissButton = {
TextButton(onClick = { showPrimaryChangeAlertDialog = false }) {
Text(text = stringResource(R.string.cancel))
}
},
title = { Text(text = stringResource(R.string.primary_tunnel_change)) },
text = { Text(text = stringResource(R.string.primary_tunnel_change_question)) },
)
}
AnimatedVisibility(showDeleteTunnelAlertDialog) { AnimatedVisibility(showDeleteTunnelAlertDialog) {
AlertDialog( AlertDialog(
onDismissRequest = { showDeleteTunnelAlertDialog = false }, onDismissRequest = { showDeleteTunnelAlertDialog = false },
confirmButton = { confirmButton = {
TextButton( TextButton(
onClick = { onClick = {
selectedTunnel?.let { viewModel.onDelete(it, context) } selectedTunnel?.let { viewModel.onDelete(it) }
showDeleteTunnelAlertDialog = false showDeleteTunnelAlertDialog = false
selectedTunnel = null selectedTunnel = null
}, },
@@ -242,129 +255,122 @@ fun MainScreen(
} }
fun onTunnelToggle(checked: Boolean, tunnel: TunnelConfig) { fun onTunnelToggle(checked: Boolean, tunnel: TunnelConfig) {
if (appViewModel.isRequiredPermissionGranted()) { if (checked) viewModel.onTunnelStart(tunnel) else viewModel.onTunnelStop()
if (checked) viewModel.onTunnelStart(tunnel, context) else viewModel.onTunnelStop(
context,
)
}
}
if (uiState.loading) {
return LoadingScreen()
}
fun launchQrScanner() {
val scanOptions = ScanOptions()
scanOptions.setDesiredBarcodeFormats(ScanOptions.QR_CODE)
scanOptions.setOrientationLocked(true)
scanOptions.setPrompt(
context.getString(R.string.scanning_qr),
)
scanOptions.setBeepEnabled(false)
scanLauncher.launch(scanOptions)
} }
Scaffold( Scaffold(
modifier = modifier =
Modifier.pointerInput(Unit) { Modifier.pointerInput(Unit) {
if (uiState.tunnels.isNotEmpty()) {
detectTapGestures( detectTapGestures(
onTap = { onTap = {
selectedTunnel = null if (!WireGuardAutoTunnel.isRunningOnAndroidTv()) selectedTunnel = null
}, },
) )
} },
},
floatingActionButtonPosition = FabPosition.End, floatingActionButtonPosition = FabPosition.End,
topBar = {
if (uiState.settings.isAutoTunnelEnabled)
TopAppBar(
title = {
Row(
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
modifier =
Modifier.requiredWidth(LocalConfiguration.current.screenWidthDp.dp)
.padding(end = 5.dp),
) {
Row {
Icon(
Icons.Rounded.Bolt,
stringResource(id = R.string.auto),
modifier = Modifier.size(25.dp),
tint =
if (uiState.settings.isAutoTunnelPaused) Color.Gray
else mint,
)
Text(
"Auto-tunneling: ${if (uiState.settings.isAutoTunnelPaused) "paused" else "active"}",
style = typography.bodyLarge,
modifier = Modifier.padding(start = 10.dp),
)
}
if (uiState.settings.isAutoTunnelPaused)
TextButton(
onClick = { viewModel.resumeAutoTunneling() },
modifier = Modifier.padding(end = 10.dp),
) {
Text("Resume")
}
else
TextButton(
onClick = { viewModel.pauseAutoTunneling() },
modifier = Modifier.padding(end = 10.dp),
) {
Text("Pause")
}
}
},
)
},
floatingActionButton = { floatingActionButton = {
AnimatedVisibility( AnimatedVisibility(
visible = isVisible.value, visible = isVisible.value,
enter = slideInVertically(initialOffsetY = { it * 2 }), enter = slideInVertically(initialOffsetY = { it * 2 }),
exit = slideOutVertically(targetOffsetY = { it * 2 }), exit = slideOutVertically(targetOffsetY = { it * 2 }),
modifier = Modifier
.focusRequester(focusRequester)
.focusGroup(),
) { ) {
val secondaryColor = MaterialTheme.colorScheme.secondary val secondaryColor = MaterialTheme.colorScheme.secondary
val tvFobColor = MaterialTheme.colorScheme.surfaceColorAtElevation(2.dp) val hoverColor = MaterialTheme.colorScheme.surfaceColorAtElevation(2.dp)
val fobColor = var fobColor by remember { mutableStateOf(secondaryColor) }
if (WireGuardAutoTunnel.isRunningOnAndroidTv()) tvFobColor else secondaryColor FloatingActionButton(
val fobIconColor = modifier =
if (WireGuardAutoTunnel.isRunningOnAndroidTv()) Color.White else MaterialTheme.colorScheme.background (if (
MultiFloatingActionButton( WireGuardAutoTunnel.isRunningOnAndroidTv() &&
fabIcon = FabIcon( uiState.tunnels.isEmpty()
iconRes = R.drawable.add, )
iconResAfterRotate = R.drawable.close, Modifier.focusRequester(focusRequester)
iconRotate = 180f, else Modifier)
), .padding(bottom = 90.dp)
fabOption = FabOption( .onFocusChanged {
iconTint = fobIconColor, if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
backgroundTint = fobColor, fobColor = if (it.isFocused) hoverColor else secondaryColor
), }
itemsMultiFab = listOf(
MultiFabItem(
label = {
Text(
stringResource(id = R.string.amnezia),
color = Color.White,
textAlign = TextAlign.Center,
modifier = Modifier.padding(end = 10.dp),
)
}, },
modifier = Modifier onClick = { showBottomSheet = true },
.size(40.dp), containerColor = fobColor,
icon = R.drawable.add,
value = ConfigType.AMNEZIA.name,
miniFabOption = FabOption(
backgroundTint = fobColor,
fobIconColor,
),
),
MultiFabItem(
label = {
Text(
stringResource(id = R.string.wireguard),
color = Color.White,
textAlign = TextAlign.Center,
modifier = Modifier.padding(end = 10.dp),
)
},
icon = R.drawable.add,
value = ConfigType.WIREGUARD.name,
miniFabOption = FabOption(
backgroundTint = fobColor,
fobIconColor,
),
),
),
onFabItemClicked = {
showBottomSheet = true
configType = ConfigType.valueOf(it.value)
},
shape = RoundedCornerShape(16.dp), shape = RoundedCornerShape(16.dp),
) ) {
Icon(
imageVector = Icons.Rounded.Add,
contentDescription = stringResource(id = R.string.add_tunnel),
tint = Color.DarkGray,
)
}
} }
}, },
) { ) { innerPadding ->
AnimatedVisibility(uiState.tunnels.isEmpty(), exit = fadeOut(), enter = fadeIn()) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center,
modifier = Modifier.fillMaxSize().padding(padding),
) {
Text(text = stringResource(R.string.no_tunnels), fontStyle = FontStyle.Italic)
}
}
if (showBottomSheet) { if (showBottomSheet) {
ModalBottomSheet( ModalBottomSheet(
onDismissRequest = { onDismissRequest = { showBottomSheet = false },
showBottomSheet = false
},
sheetState = sheetState, sheetState = sheetState,
) { ) {
// Sheet content // Sheet content
Row( Row(
modifier = modifier =
Modifier Modifier.fillMaxWidth()
.fillMaxWidth() .clickable {
.clickable { showBottomSheet = false
showBottomSheet = false tunnelFileImportResultLauncher.launch(Constants.ALLOWED_FILE_TYPES)
tunnelFileImportResultLauncher.launch(Constants.ALLOWED_FILE_TYPES) }
} .padding(10.dp),
.padding(10.dp),
) { ) {
Icon( Icon(
Icons.Filled.FileOpen, Icons.Filled.FileOpen,
@@ -377,18 +383,26 @@ fun MainScreen(
) )
} }
if (!WireGuardAutoTunnel.isRunningOnAndroidTv()) { if (!WireGuardAutoTunnel.isRunningOnAndroidTv()) {
HorizontalDivider() Divider()
Row( Row(
modifier = modifier =
Modifier Modifier.fillMaxWidth()
.fillMaxWidth() .clickable {
.clickable { scope.launch {
scope.launch { showBottomSheet = false
showBottomSheet = false val scanOptions = ScanOptions()
launchQrScanner() scanOptions.setDesiredBarcodeFormats(ScanOptions.QR_CODE)
scanOptions.setOrientationLocked(true)
scanOptions.setPrompt(
context.getString(R.string.scanning_qr)
)
scanOptions.setBeepEnabled(false)
scanOptions.captureActivity =
CaptureActivityPortrait::class.java
scanLauncher.launch(scanOptions)
}
} }
} .padding(10.dp),
.padding(10.dp),
) { ) {
Icon( Icon(
Icons.Filled.QrCode, Icons.Filled.QrCode,
@@ -401,18 +415,17 @@ fun MainScreen(
) )
} }
} }
HorizontalDivider() Divider()
Row( Row(
modifier = modifier =
Modifier Modifier.fillMaxWidth()
.fillMaxWidth() .clickable {
.clickable { showBottomSheet = false
showBottomSheet = false navController.navigate(
navController.navigate( "${Screen.Config.route}/${Constants.MANUAL_TUNNEL_CONFIG_ID}",
"${Screen.Config.route}/${Constants.MANUAL_TUNNEL_CONFIG_ID}?configType=${configType}", )
) }
} .padding(10.dp),
.padding(10.dp),
) { ) {
Icon( Icon(
Icons.Filled.Create, Icons.Filled.Create,
@@ -431,126 +444,23 @@ fun MainScreen(
horizontalAlignment = Alignment.Start, horizontalAlignment = Alignment.Start,
verticalArrangement = Arrangement.Top, verticalArrangement = Arrangement.Top,
modifier = modifier =
Modifier Modifier.fillMaxWidth()
.fillMaxSize() .fillMaxHeight(.90f)
.overscroll(ScrollableDefaults.overscrollEffect()) .overscroll(ScrollableDefaults.overscrollEffect())
.nestedScroll(nestedScrollConnection), .padding(innerPadding),
state = rememberLazyListState(0, uiState.tunnels.count()), state = rememberLazyListState(0, uiState.tunnels.count()),
userScrollEnabled = true, userScrollEnabled = true,
reverseLayout = false, reverseLayout = true,
flingBehavior = ScrollableDefaults.flingBehavior(), flingBehavior = ScrollableDefaults.flingBehavior(),
) { ) {
item {
AnimatedVisibility(
uiState.tunnels.isEmpty(), exit = fadeOut(), enter = fadeIn(),
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center,
modifier = Modifier
.padding(top = 100.dp)
.fillMaxSize(),
) {
val gettingStarted = buildAnnotatedString {
append(stringResource(id = R.string.see_the))
append(" ")
pushStringAnnotation(
tag = "gettingStarted",
annotation = stringResource(id = R.string.getting_started_url),
)
withStyle(style = SpanStyle(color = MaterialTheme.colorScheme.primary)) {
append(stringResource(id = R.string.getting_started_guide))
}
pop()
append(" ")
append(stringResource(R.string.unsure_how))
append(".")
}
Text(
text = stringResource(R.string.no_tunnels),
fontStyle = FontStyle.Italic,
)
ClickableText(
modifier = Modifier
.padding(vertical = 10.dp, horizontal = 24.dp),
text = gettingStarted,
style = MaterialTheme.typography.bodyMedium.copy(
color = MaterialTheme.colorScheme.onSurfaceVariant,
textAlign = TextAlign.Center,
),
) {
gettingStarted.getStringAnnotations(tag = "gettingStarted", it, it)
.firstOrNull()?.let { annotation ->
appViewModel.openWebPage(annotation.item, context)
}
}
}
}
}
item {
if (uiState.settings.isAutoTunnelEnabled) {
val itemFocusRequester = remember { FocusRequester() }
val autoTunnelingLabel = buildAnnotatedString {
append(stringResource(id = R.string.auto_tunneling))
append(": ")
if (uiState.settings.isAutoTunnelPaused) append(
stringResource(id = R.string.paused),
) else append(
stringResource(id = R.string.active),
)
}
RowListItem(
icon = {
val icon = Icons.Rounded.Bolt
Icon(
icon,
icon.name,
modifier = Modifier
.padding(end = 8.5.dp)
.size(25.dp),
tint =
if (uiState.settings.isAutoTunnelPaused) Color.Gray
else mint,
)
},
text = autoTunnelingLabel.text,
rowButton = {
if (uiState.settings.isAutoTunnelPaused) {
TextButton(
modifier = Modifier.focusRequester(itemFocusRequester),
onClick = { viewModel.resumeAutoTunneling() },
) {
Text(stringResource(id = R.string.resume))
}
} else {
TextButton(
modifier = Modifier.focusRequester(itemFocusRequester),
onClick = { viewModel.pauseAutoTunneling() },
) {
Text(stringResource(id = R.string.pause))
}
}
},
onClick = {
if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
itemFocusRequester.requestFocus()
}
},
onHold = {},
expanded = false,
statistics = null,
focusRequester = focusRequester
)
}
}
items( items(
uiState.tunnels, uiState.tunnels,
key = { tunnel -> tunnel.id }, key = { tunnel -> tunnel.id },
) { tunnel -> ) { tunnel ->
val leadingIconColor = val leadingIconColor =
(if ( (if (
uiState.vpnState.tunnelConfig?.name == tunnel.name && uiState.vpnState.name == tunnel.name &&
uiState.vpnState.status == TunnelState.UP uiState.vpnState.status == Tunnel.State.UP
) { ) {
uiState.vpnState.statistics uiState.vpnState.statistics
?.mapPeerStats() ?.mapPeerStats()
@@ -561,7 +471,6 @@ fun MainScreen(
statuses?.any { it == HandshakeStatus.STALE } == true -> corn statuses?.any { it == HandshakeStatus.STALE } == true -> corn
statuses?.all { it == HandshakeStatus.NOT_STARTED } == true -> statuses?.all { it == HandshakeStatus.NOT_STARTED } == true ->
Color.Gray Color.Gray
else -> { else -> {
Color.Gray Color.Gray
} }
@@ -570,37 +479,32 @@ fun MainScreen(
} else { } else {
Color.Gray Color.Gray
}) })
val itemFocusRequester = remember { FocusRequester() }
val expanded = remember { mutableStateOf(false) } val expanded = remember { mutableStateOf(false) }
RowListItem( RowListItem(
icon = { icon = {
val circleIcon = Icons.Rounded.Circle if (uiState.settings.isTunnelConfigDefault(tunnel)) {
val icon = if (tunnel.isPrimaryTunnel) { Icon(
Icons.Rounded.Star Icons.Rounded.Star,
} else if (tunnel.isMobileDataTunnel) { stringResource(R.string.status),
Icons.Rounded.Smartphone tint = leadingIconColor,
modifier = Modifier.padding(end = 10.dp).size(20.dp),
)
} else { } else {
circleIcon Icon(
Icons.Rounded.Circle,
stringResource(R.string.status),
tint = leadingIconColor,
modifier = Modifier.padding(end = 15.dp).size(15.dp),
)
} }
Icon(
icon,
icon.name,
tint = leadingIconColor,
modifier = Modifier
.padding(
end = if (icon == circleIcon) 12.5.dp else 10.dp,
start = if (icon == circleIcon) 2.5.dp else 0.dp,
)
.size(if (icon == circleIcon) 15.dp else 20.dp),
)
}, },
text = tunnel.name, text = tunnel.name,
onHold = { onHold = {
if ( if (
(uiState.vpnState.status == TunnelState.UP) && (uiState.vpnState.status == Tunnel.State.UP) &&
(tunnel.name == uiState.vpnState.tunnelConfig?.name) (tunnel.name == uiState.vpnState.name)
) { ) {
appViewModel.showSnackbarMessage(context.getString(R.string.turn_off_tunnel)) showSnackbarMessage(Event.Message.TunnelOffAction.message)
return@RowListItem return@RowListItem
} }
haptic.performHapticFeedback(HapticFeedbackType.LongPress) haptic.performHapticFeedback(HapticFeedbackType.LongPress)
@@ -609,67 +513,77 @@ fun MainScreen(
onClick = { onClick = {
if (!WireGuardAutoTunnel.isRunningOnAndroidTv()) { if (!WireGuardAutoTunnel.isRunningOnAndroidTv()) {
if ( if (
uiState.vpnState.status == TunnelState.UP && uiState.vpnState.status == Tunnel.State.UP &&
(uiState.vpnState.tunnelConfig?.name == tunnel.name) (uiState.vpnState.name == tunnel.name)
) { ) {
expanded.value = !expanded.value expanded.value = !expanded.value
} }
} else { } else {
selectedTunnel = tunnel selectedTunnel = tunnel
itemFocusRequester.requestFocus() focusRequester.requestFocus()
} }
}, },
statistics = uiState.vpnState.statistics, statistics = uiState.vpnState.statistics,
expanded = expanded.value, expanded = expanded.value,
focusRequester = focusRequester,
rowButton = { rowButton = {
if ( if (
tunnel.id == selectedTunnel?.id && tunnel.id == selectedTunnel?.id &&
!WireGuardAutoTunnel.isRunningOnAndroidTv() !WireGuardAutoTunnel.isRunningOnAndroidTv()
) { ) {
Row { Row {
if (!uiState.settings.isTunnelConfigDefault(tunnel)) {
IconButton(
onClick = {
if (
uiState.settings.isAutoTunnelEnabled &&
!uiState.settings.isAutoTunnelPaused
) {
showSnackbarMessage(
Event.Message.AutoTunnelOffAction.message,
)
} else {
showPrimaryChangeAlertDialog = true
}
},
) {
Icon(
Icons.Rounded.Star,
stringResource(id = R.string.set_primary),
)
}
}
IconButton( IconButton(
onClick = { onClick = {
if ( if (
uiState.settings.isAutoTunnelEnabled && uiState.settings.isAutoTunnelEnabled &&
!uiState.settings.isAutoTunnelPaused uiState.settings.isTunnelConfigDefault(
tunnel,
) &&
!uiState.settings.isAutoTunnelPaused
) { ) {
appViewModel.showSnackbarMessage( showSnackbarMessage(
context.getString(R.string.turn_off_tunnel), Event.Message.AutoTunnelOffAction.message,
) )
} else { } else
navController.navigate( navController.navigate(
"${Screen.Option.route}/${selectedTunnel?.id}", "${Screen.Config.route}/${selectedTunnel?.id}",
) )
}
}, },
) { ) {
val icon = Icons.Rounded.Settings Icon(Icons.Rounded.Edit, stringResource(id = R.string.edit))
Icon(
icon,
icon.name,
)
}
IconButton(
modifier = Modifier.focusable(),
onClick = { viewModel.onCopyTunnel(selectedTunnel) },
) {
val icon = Icons.Rounded.CopyAll
Icon(icon, icon.name)
} }
IconButton( IconButton(
modifier = Modifier.focusable(), modifier = Modifier.focusable(),
onClick = { showDeleteTunnelAlertDialog = true }, onClick = { showDeleteTunnelAlertDialog = true },
) { ) {
val icon = Icons.Rounded.Delete Icon(Icons.Rounded.Delete, stringResource(id = R.string.delete))
Icon(icon, icon.name)
} }
} }
} else { } else {
val checked by remember { val checked by remember {
derivedStateOf { derivedStateOf {
(uiState.vpnState.status == TunnelState.UP && (uiState.vpnState.status == Tunnel.State.UP &&
tunnel.name == uiState.vpnState.tunnelConfig?.name) tunnel.name == uiState.vpnState.name)
} }
} }
if (!checked) expanded.value = false if (!checked) expanded.value = false
@@ -677,7 +591,7 @@ fun MainScreen(
@Composable @Composable
fun TunnelSwitch() = fun TunnelSwitch() =
Switch( Switch(
modifier = Modifier.focusRequester(itemFocusRequester), modifier = Modifier.focusRequester(focusRequester),
checked = checked, checked = checked,
onCheckedChange = { checked -> onCheckedChange = { checked ->
if (!checked) expanded.value = false if (!checked) expanded.value = false
@@ -686,69 +600,77 @@ fun MainScreen(
) )
if (WireGuardAutoTunnel.isRunningOnAndroidTv()) { if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
Row { Row {
IconButton( if (!uiState.settings.isTunnelConfigDefault(tunnel)) {
onClick = { IconButton(
if (uiState.settings.isAutoTunnelEnabled && !uiState.settings.isAutoTunnelPaused) { onClick = {
appViewModel.showSnackbarMessage( if (uiState.settings.isAutoTunnelEnabled) {
context.getString(R.string.turn_off_auto), showSnackbarMessage(
) Event.Message.AutoTunnelOffAction.message,
} else { )
selectedTunnel = tunnel } else {
navController.navigate( selectedTunnel = tunnel
"${Screen.Option.route}/${selectedTunnel?.id}", showPrimaryChangeAlertDialog = true
) }
} },
}, ) {
) { Icon(
val icon = Icons.Rounded.Settings Icons.Rounded.Star,
Icon( stringResource(id = R.string.set_primary),
icon, )
icon.name, }
)
} }
IconButton( IconButton(
modifier = Modifier.focusRequester(focusRequester), modifier = Modifier.focusRequester(focusRequester),
onClick = { onClick = {
if ( if (
uiState.vpnState.status == TunnelState.UP && uiState.vpnState.status == Tunnel.State.UP &&
(uiState.vpnState.tunnelConfig?.name == tunnel.name) (uiState.vpnState.name == tunnel.name)
) { ) {
expanded.value = !expanded.value expanded.value = !expanded.value
} else { } else {
appViewModel.showSnackbarMessage( showSnackbarMessage(
context.getString(R.string.turn_on_tunnel), Event.Message.TunnelOnAction.message
) )
} }
}, },
) { ) {
val icon = Icons.Rounded.Info Icon(Icons.Rounded.Info, stringResource(R.string.info))
Icon(icon, icon.name)
}
IconButton(
onClick = { viewModel.onCopyTunnel(tunnel) },
) {
val icon = Icons.Rounded.CopyAll
Icon(icon, icon.name)
} }
IconButton( IconButton(
onClick = { onClick = {
if ( if (
uiState.vpnState.status == TunnelState.UP && uiState.vpnState.status == Tunnel.State.UP &&
tunnel.name == uiState.vpnState.tunnelConfig?.name tunnel.name == uiState.vpnState.name
) { ) {
appViewModel.showSnackbarMessage( showSnackbarMessage(
context.getString(R.string.turn_off_tunnel), Event.Message.TunnelOffAction.message
)
} else {
navController.navigate(
"${Screen.Config.route}/${tunnel.id}",
)
}
},
) {
Icon(Icons.Rounded.Edit, stringResource(id = R.string.edit))
}
IconButton(
onClick = {
if (
uiState.vpnState.status == Tunnel.State.UP &&
tunnel.name == uiState.vpnState.name
) {
showSnackbarMessage(
Event.Message.TunnelOffAction.message
) )
} else { } else {
selectedTunnel = tunnel
showDeleteTunnelAlertDialog = true showDeleteTunnelAlertDialog = true
} }
}, },
) { ) {
val icon = Icons.Rounded.Delete
Icon( Icon(
icon, Icons.Rounded.Delete,
icon.name, stringResource(id = R.string.delete),
) )
} }
TunnelSwitch() TunnelSwitch()
@@ -1,6 +1,6 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.main package com.zaneschepke.wireguardautotunnel.ui.screens.main
import com.zaneschepke.wireguardautotunnel.data.domain.Settings import com.zaneschepke.wireguardautotunnel.data.model.Settings
import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnState import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnState
import com.zaneschepke.wireguardautotunnel.util.TunnelConfigs import com.zaneschepke.wireguardautotunnel.util.TunnelConfigs
@@ -1,5 +1,6 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.main package com.zaneschepke.wireguardautotunnel.ui.screens.main
import android.app.Application
import android.content.Context import android.content.Context
import android.database.Cursor import android.database.Cursor
import android.net.Uri import android.net.Uri
@@ -8,18 +9,20 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.wireguard.config.Config import com.wireguard.config.Config
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
import com.zaneschepke.wireguardautotunnel.data.domain.Settings import com.zaneschepke.wireguardautotunnel.data.model.Settings
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository import com.zaneschepke.wireguardautotunnel.data.repository.SettingsRepository
import com.zaneschepke.wireguardautotunnel.module.IoDispatcher import com.zaneschepke.wireguardautotunnel.data.repository.TunnelConfigRepository
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnService import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnService
import com.zaneschepke.wireguardautotunnel.util.Constants import com.zaneschepke.wireguardautotunnel.util.Constants
import com.zaneschepke.wireguardautotunnel.util.Event
import com.zaneschepke.wireguardautotunnel.util.NumberUtils import com.zaneschepke.wireguardautotunnel.util.NumberUtils
import com.zaneschepke.wireguardautotunnel.util.WgTunnelExceptions import com.zaneschepke.wireguardautotunnel.util.Result
import com.zaneschepke.wireguardautotunnel.util.toWgQuickString
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.stateIn
@@ -34,298 +37,175 @@ import javax.inject.Inject
class MainViewModel class MainViewModel
@Inject @Inject
constructor( constructor(
private val appDataRepository: AppDataRepository, private val application: Application,
private val serviceManager: ServiceManager, private val tunnelConfigRepository: TunnelConfigRepository,
val vpnService: VpnService, private val settingsRepository: SettingsRepository,
@IoDispatcher private val ioDispatcher: CoroutineDispatcher private val vpnService: VpnService
) : ViewModel() { ) : ViewModel() {
val uiState = val uiState =
combine( combine(
appDataRepository.settings.getSettingsFlow(), settingsRepository.getSettingsFlow(),
appDataRepository.tunnels.getTunnelConfigsFlow(), tunnelConfigRepository.getTunnelConfigsFlow(),
vpnService.vpnState, vpnService.vpnState,
) { settings, tunnels, vpnState -> ) { settings, tunnels, vpnState ->
MainUiState(settings, tunnels, vpnState, false) validateWatcherServiceState(settings)
} MainUiState(settings, tunnels, vpnState, false)
}
.stateIn( .stateIn(
viewModelScope, viewModelScope,
SharingStarted.WhileSubscribed(Constants.SUBSCRIPTION_TIMEOUT), SharingStarted.WhileSubscribed(Constants.SUBSCRIPTION_TIMEOUT),
MainUiState(), MainUiState(),
) )
private fun stopWatcherService(context: Context) { private fun validateWatcherServiceState(settings: Settings) =
serviceManager.stopWatcherService(context) viewModelScope.launch(Dispatchers.IO) {
} if (settings.isAutoTunnelEnabled) {
ServiceManager.startWatcherService(application.applicationContext)
fun onDelete(tunnel: TunnelConfig, context: Context) {
viewModelScope.launch {
val settings = appDataRepository.settings.getSettings()
val isPrimary = tunnel.isPrimaryTunnel
if (appDataRepository.tunnels.count() == 1 || isPrimary) {
stopWatcherService(context)
resetTunnelSetting(settings)
} }
appDataRepository.tunnels.delete(tunnel) }
WireGuardAutoTunnel.requestTunnelTileServiceStateUpdate()
private fun stopWatcherService() =
viewModelScope.launch(Dispatchers.IO) {
ServiceManager.stopWatcherService(application.applicationContext)
}
fun onDelete(tunnel: TunnelConfig) {
viewModelScope.launch(Dispatchers.IO) {
if (tunnelConfigRepository.count() == 1) {
stopWatcherService()
val settings = settingsRepository.getSettings()
settings.defaultTunnel = null
settings.isAutoTunnelEnabled = false
settings.isAlwaysOnVpnEnabled = false
saveSettings(settings)
}
tunnelConfigRepository.delete(tunnel)
WireGuardAutoTunnel.requestTileServiceStateUpdate()
} }
} }
private fun resetTunnelSetting(settings: Settings) { fun onTunnelStart(tunnelConfig: TunnelConfig) =
saveSettings( viewModelScope.launch(Dispatchers.IO) {
settings.copy(
isAutoTunnelEnabled = false,
isAlwaysOnVpnEnabled = false,
),
)
}
fun onTunnelStart(tunnelConfig: TunnelConfig, context: Context) =
viewModelScope.launch {
Timber.d("On start called!") Timber.d("On start called!")
serviceManager.startVpnService( stopActiveTunnel().await()
context, startTunnel(tunnelConfig)
tunnelConfig.id,
isManualStart = true,
)
} }
private fun startTunnel(tunnelConfig: TunnelConfig) =
fun onTunnelStop(context: Context) = viewModelScope.launch(Dispatchers.IO) {
viewModelScope.launch { Timber.d("Start tunnel via manager")
Timber.i("Stopping active tunnel") ServiceManager.startVpnService(application.applicationContext, tunnelConfig.toString())
serviceManager.stopVpnService(context, isManualStop = true)
} }
private fun validateConfigString(config: String, configType: ConfigType) { private fun stopActiveTunnel() =
when (configType) { viewModelScope.async(Dispatchers.IO) {
ConfigType.AMNEZIA -> TunnelConfig.configFromAmQuick(config) onTunnelStop()
ConfigType.WIREGUARD -> TunnelConfig.configFromWgQuick(config) delay(Constants.TOGGLE_TUNNEL_DELAY)
} }
fun onTunnelStop() =
viewModelScope.launch(Dispatchers.IO) {
Timber.d("Stopping active tunnel")
ServiceManager.stopVpnService(application.applicationContext)
}
private fun validateConfigString(config: String) {
TunnelConfig.configFromQuick(config)
} }
private fun generateQrCodeDefaultName(config: String, configType: ConfigType): String { suspend fun onTunnelQrResult(result: String): Result<Unit> {
return try { return try {
when (configType) { validateConfigString(result)
ConfigType.AMNEZIA -> { val tunnelConfig =
TunnelConfig.configFromAmQuick(config).peers[0].endpoint.get().host TunnelConfig(name = NumberUtils.generateRandomTunnelName(), wgQuick = result)
} addTunnel(tunnelConfig)
Result.Success(Unit)
ConfigType.WIREGUARD -> {
TunnelConfig.configFromWgQuick(config).peers[0].endpoint.get().host
}
}
} catch (e: Exception) { } catch (e: Exception) {
Timber.e(e) Result.Error(Event.Error.InvalidQrCode)
NumberUtils.generateRandomTunnelName()
} }
} }
private fun generateQrCodeTunnelName(config: String, configType: ConfigType): String { private suspend fun saveTunnelConfigFromStream(stream: InputStream, fileName: String) {
var defaultName = generateQrCodeDefaultName(config, configType) val bufferReader = stream.bufferedReader(charset = Charsets.UTF_8)
val lines = config.lines().toMutableList() val config = Config.parse(bufferReader)
val linesIterator = lines.iterator() val tunnelName = getNameFromFileName(fileName)
while (linesIterator.hasNext()) { addTunnel(TunnelConfig(name = tunnelName, wgQuick = config.toWgQuickString()))
val next = linesIterator.next() withContext(Dispatchers.IO) { stream.close() }
if (next.contains(Constants.QR_CODE_NAME_PROPERTY)) {
defaultName = next.substringAfter(Constants.QR_CODE_NAME_PROPERTY).trim()
break
}
}
return defaultName
} }
suspend fun onTunnelQrResult(result: String, configType: ConfigType): Result<Unit> { private fun getInputStreamFromUri(uri: Uri): InputStream? {
return withContext(ioDispatcher) { return application.applicationContext.contentResolver.openInputStream(uri)
try {
validateConfigString(result, configType)
val tunnelName = makeTunnelNameUnique(generateQrCodeTunnelName(result, configType))
val tunnelConfig = when (configType) {
ConfigType.AMNEZIA -> {
TunnelConfig(
name = tunnelName, amQuick = result,
wgQuick = TunnelConfig.configFromAmQuick(result).toWgQuickString(),
)
}
ConfigType.WIREGUARD -> TunnelConfig(name = tunnelName, wgQuick = result)
}
addTunnel(tunnelConfig)
Result.success(Unit)
} catch (e: Exception) {
Timber.e(e)
Result.failure(WgTunnelExceptions.InvalidQrCode())
}
}
} }
private suspend fun makeTunnelNameUnique(name: String): String { suspend fun onTunnelFileSelected(uri: Uri): Result<Unit> {
return withContext(ioDispatcher) { try {
val tunnels = appDataRepository.tunnels.getAll() if (isValidUriContentScheme(uri)) {
var tunnelName = name val fileName = getFileName(application.applicationContext, uri)
var num = 1 when (getFileExtensionFromFileName(fileName)) {
while (tunnels.any { it.name == tunnelName }) { Constants.CONF_FILE_EXTENSION ->
tunnelName = name + "(${num})" saveTunnelFromConfUri(fileName, uri).let {
num++ when (it) {
} is Result.Error -> return Result.Error(Event.Error.FileReadFailed)
tunnelName is Result.Success -> return it
}
}
private fun saveTunnelConfigFromStream(
stream: InputStream,
fileName: String,
type: ConfigType
) {
var amQuick: String? = null
val wgQuick = stream.use {
when (type) {
ConfigType.AMNEZIA -> {
val config = org.amnezia.awg.config.Config.parse(it)
amQuick = config.toAwgQuickString()
config.toWgQuickString()
}
ConfigType.WIREGUARD -> {
Config.parse(it).toWgQuickString()
}
}
}
viewModelScope.launch {
val tunnelName = makeTunnelNameUnique(getNameFromFileName(fileName))
addTunnel(
TunnelConfig(
name = tunnelName,
wgQuick = wgQuick,
amQuick = amQuick ?: TunnelConfig.AM_QUICK_DEFAULT,
),
)
}
}
private fun getInputStreamFromUri(uri: Uri, context: Context): InputStream? {
return context.applicationContext.contentResolver.openInputStream(uri)
}
suspend fun onTunnelFileSelected(
uri: Uri,
configType: ConfigType,
context: Context
): Result<Unit> {
return withContext(ioDispatcher) {
try {
if (isValidUriContentScheme(uri)) {
val fileName = getFileName(context, uri)
return@withContext when (getFileExtensionFromFileName(fileName)) {
Constants.CONF_FILE_EXTENSION ->
saveTunnelFromConfUri(fileName, uri, configType, context)
Constants.ZIP_FILE_EXTENSION -> saveTunnelsFromZipUri(
uri,
configType,
context,
)
else -> Result.failure(WgTunnelExceptions.InvalidFileExtension())
}
} else {
Result.failure(WgTunnelExceptions.InvalidFileExtension())
}
} catch (e: Exception) {
Timber.e(e)
Result.failure(WgTunnelExceptions.FileReadFailed())
}
}
}
private suspend fun saveTunnelsFromZipUri(
uri: Uri,
configType: ConfigType,
context: Context
): Result<Unit> {
return withContext(ioDispatcher) {
ZipInputStream(getInputStreamFromUri(uri, context)).use { zip ->
generateSequence { zip.nextEntry }
.filterNot {
it.isDirectory ||
getFileExtensionFromFileName(it.name) != Constants.CONF_FILE_EXTENSION
}
.forEach {
val name = getNameFromFileName(it.name)
withContext(viewModelScope.coroutineContext) {
try {
var amQuick: String? = null
val wgQuick =
when (configType) {
ConfigType.AMNEZIA -> {
val config = org.amnezia.awg.config.Config.parse(zip)
amQuick = config.toAwgQuickString()
config.toWgQuickString()
}
ConfigType.WIREGUARD -> {
Config.parse(zip).toWgQuickString()
}
}
addTunnel(
TunnelConfig(
name = makeTunnelNameUnique(name),
wgQuick = wgQuick,
amQuick = amQuick ?: TunnelConfig.AM_QUICK_DEFAULT,
),
)
Result.success(Unit)
} catch (e: Exception) {
Result.failure(WgTunnelExceptions.FileReadFailed())
} }
} }
} Constants.ZIP_FILE_EXTENSION -> saveTunnelsFromZipUri(uri)
Result.success(Unit) else -> return Result.Error(Event.Error.InvalidFileExtension)
}
}
}
private suspend fun saveTunnelFromConfUri(
name: String,
uri: Uri,
configType: ConfigType,
context: Context
): Result<Unit> {
return withContext(ioDispatcher) {
val stream = getInputStreamFromUri(uri, context)
return@withContext if (stream != null) {
try {
saveTunnelConfigFromStream(stream, name, configType)
} catch (e: Exception) {
return@withContext Result.failure(WgTunnelExceptions.ConfigParseError())
} }
Result.success(Unit) return Result.Success(Unit)
} else { } else {
Result.failure(WgTunnelExceptions.FileReadFailed()) return Result.Error(Event.Error.InvalidFileExtension)
} }
} catch (e: Exception) {
return Result.Error(Event.Error.FileReadFailed)
} }
} }
private fun addTunnel(tunnelConfig: TunnelConfig) = viewModelScope.launch { private suspend fun saveTunnelsFromZipUri(uri: Uri) {
val firstTunnel = appDataRepository.tunnels.count() == 0 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): Result<Unit> {
val stream = getInputStreamFromUri(uri)
return if (stream != null) {
saveTunnelConfigFromStream(stream, name)
Result.Success(Unit)
} else {
Result.Error(Event.Error.FileReadFailed)
}
}
private suspend fun addTunnel(tunnelConfig: TunnelConfig) {
saveTunnel(tunnelConfig) saveTunnel(tunnelConfig)
if (firstTunnel) WireGuardAutoTunnel.requestTunnelTileServiceStateUpdate() WireGuardAutoTunnel.requestTileServiceStateUpdate()
} }
fun pauseAutoTunneling() = fun pauseAutoTunneling() =
viewModelScope.launch { viewModelScope.launch {
appDataRepository.settings.save(uiState.value.settings.copy(isAutoTunnelPaused = true)) settingsRepository.save(uiState.value.settings.copy(isAutoTunnelPaused = true))
WireGuardAutoTunnel.requestAutoTunnelTileServiceUpdate()
} }
fun resumeAutoTunneling() = fun resumeAutoTunneling() =
viewModelScope.launch { viewModelScope.launch {
appDataRepository.settings.save(uiState.value.settings.copy(isAutoTunnelPaused = false)) settingsRepository.save(uiState.value.settings.copy(isAutoTunnelPaused = false))
WireGuardAutoTunnel.requestAutoTunnelTileServiceUpdate()
} }
private fun saveTunnel(tunnelConfig: TunnelConfig) = viewModelScope.launch { private suspend fun saveTunnel(tunnelConfig: TunnelConfig) {
appDataRepository.tunnels.save(tunnelConfig) tunnelConfigRepository.save(tunnelConfig)
} }
private fun getFileNameByCursor(context: Context, uri: Uri): String? { private fun getFileNameByCursor(context: Context, uri: Uri): String? {
@@ -365,27 +245,23 @@ constructor(
return fileName.substring(0, fileName.lastIndexOf('.')) return fileName.substring(0, fileName.lastIndexOf('.'))
} }
private fun getFileExtensionFromFileName(fileName: String): String? { private fun getFileExtensionFromFileName(fileName: String): String {
return try { return try {
fileName.substring(fileName.lastIndexOf('.')) fileName.substring(fileName.lastIndexOf('.'))
} catch (e: Exception) { } catch (e: Exception) {
Timber.e(e) ""
null
} }
} }
private fun saveSettings(settings: Settings) = private fun saveSettings(settings: Settings) =
viewModelScope.launch { appDataRepository.settings.save(settings) } viewModelScope.launch(Dispatchers.IO) { settingsRepository.save(settings) }
fun onDefaultTunnelChange(selectedTunnel: TunnelConfig?) =
fun onCopyTunnel(tunnel: TunnelConfig?) = viewModelScope.launch { viewModelScope.launch {
tunnel?.let { if (selectedTunnel != null) {
saveTunnel( saveSettings(uiState.value.settings.copy(defaultTunnel = selectedTunnel.toString()))
TunnelConfig( .join()
name = it.name.plus(NumberUtils.randomThree()), WireGuardAutoTunnel.requestTileServiceStateUpdate()
wgQuick = it.wgQuick, }
),
)
} }
}
} }

Some files were not shown because too many files have changed in this diff Show More