Compare commits

..

1 Commits

Author SHA1 Message Date
dependabot[bot] f2d228653e build(deps): bump androidGradlePlugin from 8.8.0-rc01 to 8.9.0-alpha04
Bumps `androidGradlePlugin` from 8.8.0-rc01 to 8.9.0-alpha04.

Updates `com.android.application` from 8.8.0-rc01 to 8.9.0-alpha04

Updates `com.android.library` from 8.8.0-rc01 to 8.9.0-alpha04

---
updated-dependencies:
- dependency-name: com.android.application
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: com.android.library
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-12-09 13:49:41 +00:00
276 changed files with 5653 additions and 8455 deletions
-125
View File
@@ -1,125 +0,0 @@
name: build
on:
workflow_dispatch:
inputs:
build_type:
type: choice
description: "Build type"
required: true
default: debug
options:
- debug
- prerelease
- nightly
- release
secrets:
SIGNING_KEY_ALIAS:
required: false
SIGNING_KEY_PASSWORD:
required: false
SIGNING_STORE_PASSWORD:
required: false
SERVICE_ACCOUNT_JSON:
required: false
KEYSTORE:
required: false
workflow_call:
inputs:
build_type:
type: string
description: "Build type"
required: true
default: debug
secrets:
SIGNING_KEY_ALIAS:
required: false
SIGNING_KEY_PASSWORD:
required: false
SIGNING_STORE_PASSWORD:
required: false
SERVICE_ACCOUNT_JSON:
required: false
KEYSTORE:
required: false
env:
UPLOAD_DIR_ANDROID: android_artifacts
jobs:
build:
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/
outputs:
UPLOAD_DIR_ANDROID: ${{ env.UPLOAD_DIR_ANDROID }}
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: ${{ 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
if: ${{ inputs.build_type != 'debug' }}
run: |
store_path=${{ env.KEY_STORE_LOCATION }}${{ env.KEY_STORE_FILE }}
echo "KEY_STORE_PATH=$store_path" >> $GITHUB_ENV
- name: Create service_account.json
if: ${{ inputs.build_type != 'debug' }}
id: createServiceAccount
run: echo '${{ secrets.ANDROID_SERVICE_ACCOUNT_JSON }}' > service_account.json
- name: Build Fdroid Release APK
if: ${{ inputs.build_type == 'release' }}
run: ./gradlew :app:assembleFdroidRelease --info
- name: Build Fdroid Prerelease APK
if: ${{ inputs.build_type == 'prerelease' }}
run: ./gradlew :app:assembleFdroidPrerelease --info
- name: Build Fdroid Nightly APK
if: ${{ inputs.build_type == 'nightly' }}
run: ./gradlew :app:assembleFdroidNightly --info
- name: Build Debug APK
if: ${{ inputs.build_type == 'debug' }}
run: ./gradlew :app:assembleFdroidDebug --stacktrace
# bump versionCode for nightly and prerelease builds
- name: Commit and push versionCode changes
if: ${{ inputs.build_type == 'nightly' || inputs.build_type == 'prerelease' }}
run: |
git config --global user.name 'GitHub Actions'
git config --global user.email 'actions@github.com'
git add versionCode.txt
git commit -m "Automated build update"
- name: Get release apk path
id: apk-path
run: echo "path=$(find . -regex '^.*/build/outputs/apk/fdroid/${{ inputs.build_type }}/.*\.apk$' -type f | head -1 | tail -c+2)" >> $GITHUB_OUTPUT
- name: Upload release apk
uses: actions/upload-artifact@v4
with:
name: ${{ env.UPLOAD_DIR_ANDROID }}
path: ${{github.workspace}}/${{ steps.apk-path.outputs.path }}
retention-days: 1
@@ -1,11 +1,11 @@
name: on-pr
name: ci-android
on:
workflow_dispatch:
pull_request:
jobs:
format_check:
format:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
@@ -1,4 +1,4 @@
name: publish
name: release-android
on:
schedule:
@@ -31,8 +31,6 @@ on:
required: false
default: nightly
workflow_call:
env:
UPLOAD_DIR_ANDROID: android_artifacts
jobs:
check_commits:
@@ -55,22 +53,17 @@ jobs:
# This script checks for commits newer than 23 hours ago
NEW_COMMITS=$(git rev-list --count --after="$(date -Iseconds -d '23 hours ago')" ${{ github.sha }})
echo "new_commits=$NEW_COMMITS" >> $GITHUB_OUTPUT
build:
if: ${{ inputs.release_type != 'none' }}
uses: ./.github/workflows/build.yml
secrets: inherit
with:
build_type: ${{ inputs.release_type == '' && 'nightly' || inputs.release_type }}
publish:
needs:
- check_commits
- build
needs: check_commits
if: ${{ needs.check_commits.outputs.has_new_commits > 0 && inputs.release_type != 'none' }}
name: publish-github
name: Build Signed APK
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 }}
@@ -78,10 +71,29 @@ jobs:
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 }}
# update latest tag
- name: Set latest tag
uses: rickstaa/action-create-tag@v1
@@ -108,12 +120,51 @@ jobs:
fromTag: "latest"
writeToFile: false # we won't write to file, just output
# 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 == 'release' }}
run: ./gradlew :app:assembleFdroidRelease -x test
- name: Build Fdroid Prerelease APK
if: ${{ inputs.release_type != '' && inputs.release_type == 'prerelease' }}
run: ./gradlew :app:assembleFdroidPrerelease -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 == 'release' }}
run: echo "APK_PATH=$(find . -regex '^.*/build/outputs/apk/fdroid/release/.*\.apk$' -type f | head -1)" >> $GITHUB_ENV
- if: ${{ inputs.release_type != '' && inputs.release_type == 'prerelease' }}
run: echo "APK_PATH=$(find . -regex '^.*/build/outputs/apk/fdroid/prerelease/.*\.apk$' -type f | head -1)" >> $GITHUB_ENV
- name: Get version code
if: ${{ inputs.release_type == 'release' }}
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
- name: Commit and push versionCode changes
if: ${{ inputs.release_type == '' || inputs.release_type == 'nightly' || inputs.release_type == 'prerelease' }}
run: |
git config --global user.name 'GitHub Actions'
git config --global user.email 'actions@github.com'
git add versionCode.txt
git commit -m "Automated build update"
- name: Push changes
if: ${{ inputs.release_type == '' || inputs.release_type == 'nightly' || inputs.release_type == 'prerelease' }}
uses: ad-m/github-push-action@master
@@ -121,14 +172,25 @@ jobs:
github_token: ${{ secrets.GITHUB_TOKEN }}
branch: ${{ github.ref }}
- name: Make download dir
run: mkdir ${{ github.workspace }}/temp
# 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.4.3
with:
name: wgtunnel
path: ${{ env.APK_PATH }}
- name: Download artifacts
- name: Download APK from build
uses: actions/download-artifact@v4
with:
name: ${{ env.UPLOAD_DIR_ANDROID }}
path: ${{ github.workspace }}/temp
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
# Setup TAG_NAME, which is used as a general "name"
- if: github.event_name == 'workflow_dispatch'
@@ -159,9 +221,7 @@ jobs:
- name: Get checksum
id: checksum
run: |
file_path=$(find ${{ github.workspace }}/temp -type f -iname "*.apk" | tail -n1)
echo "checksum=$(apksigner verify -print-certs $file_path | grep -Po "(?<=SHA-256 digest:) .*" | tr -d "[:blank:]")" >> $GITHUB_OUTPUT
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
@@ -173,15 +233,8 @@ jobs:
body: |
${{ env.RELEASE_NOTES }}
SHA-256 fingerprint for the 4096-bit signing certificate:
```sh
${{ steps.checksum.outputs.checksum }}
```
To verify fingerprint:
```sh
apksigner verify --print-certs [path to APK file] | grep SHA-256
```
SHA256 fingerprint:
```${{ steps.checksum.outputs.checksum }}```
### Changelog
${{ steps.changelog.outputs.changes }}
@@ -190,21 +243,7 @@ jobs:
draft: false
prerelease: ${{ inputs.release_type == 'prerelease' || inputs.release_type == '' || inputs.release_type == 'nightly' }}
make_latest: ${{ inputs.release_type == 'release' }}
files: |
${{ github.workspace }}/temp/*
publish-fdroid:
runs-on: ubuntu-latest
needs:
- build
if: inputs.release_type == 'release'
steps:
- name: Dispatch update for fdroid repo
uses: peter-evans/repository-dispatch@v3
with:
token: ${{ secrets.PAT }}
repository: zaneschepke/fdroid
event-type: fdroid-update
files: ${{ github.workspace }}/${{ env.APK_PATH }}
publish-play:
if: ${{ inputs.track != 'none' && inputs.track != '' }}
+53 -68
View File
@@ -4,107 +4,79 @@ WG Tunnel
<div align="center">
An alternative Android client app for [WireGuard®](https://www.wireguard.com/)
and [AmneziaWG](https://docs.amnezia.org/documentation/amnezia-wg/)
<br />
<br />
<a href="https://github.com/zaneschepke/wgtunnel/issues/new?assignees=zaneschepke&labels=bug&projects=&template=bug_report.md&title=%5BBUG%5D+-+Problem+with+app">Report a Bug</a>
·
<a href="https://github.com/zaneschepke/wgtunnel/issues/new?assignees=zaneschepke&labels=enhancement&projects=&template=feature_request.md&title=%5BFEATURE%5D+-+New+feature+request">Request a Feature</a>
·
<a href="https://github.com/zaneschepke/wgtunnel/discussions">Ask a Question</a>
</div>
<br/>
<div align="center">
[![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)
[![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/)
[![Personal](https://img.shields.io/static/v1?style=for-the-badge&message=Personal&color=1976D2&logo=F-Droid&logoColor=FFFFFF&label=)](https://github.com/zaneschepke/fdroid)
</div>
<div align="center">
[![Discord](https://img.shields.io/badge/Discord-%235865F2.svg?style=for-the-badge&logo=discord&logoColor=white)](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>
<details open="open">
<summary>Table of Contents</summary>
<div align="center">
- [About](#about)
- [Acknowledgements](#acknowledgements)
- [Screenshots](#screenshots)
- [Features](#features)
- [Building](#building)
- [Translation](#translation)
- [Contributing](#contributing)
</details>
[![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)
[![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 style="text-align: left;">
## About
Inspired by the official [wireguard-android](https://github.com/WireGuard/wireguard-android) app, WG Tunnel was created to address features and support missing from the official app. This app combines support for both [WireGuard®](https://www.wireguard.com/)
and [AmneziaWG](https://docs.amnezia.org/documentation/amnezia-wg/), with its primary feature of auto-tunneling (on-demand tunneling).
</div>
<div style="text-align: left;">
## Acknowledgements
<div align="left">
Thank you to the following:
This is an alternative Android Application for [WireGuard](https://www.wireguard.com/)
and [AmneziaWG](https://docs.amnezia.org/documentation/amnezia-wg/) with added
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
inspired by the official [WireGuard Android](https://github.com/WireGuard/wireguard-android) app.
- All of the users that have helped contribute to the project with ideas, translations, feedback, bug reports, testing, and donations.
- [WireGuard®](https://www.wireguard.com/) - © Jason A. Donenfeld (https://github.com/WireGuard/wireguard-android)
</div>
- [AmneziaWG](https://docs.amnezia.org/documentation/amnezia-wg/) - Amnezia Team (https://github.com/amnezia-vpn/amneziawg-android)
<div align="center">
## Screenshots
</div>
<div style="display: flex; flex-wrap: wrap; justify-content: center; gap: 10px;">
<img label="Main" src="fastlane/metadata/android/en-US/images/phoneScreenshots/main_screen.png" width="200" />
<img label="Settings" src="fastlane/metadata/android/en-US/images/phoneScreenshots/settings_screen.png" width="200" />
<img label="Auto" src="fastlane/metadata/android/en-US/images/phoneScreenshots/auto_screen.png" width="200" />
<img label="Config" src="fastlane/metadata/android/en-US/images/phoneScreenshots/config_screen.png" width="200" />
</div>
<p float="center">
<img label="Main" style="padding-right:25px" src="fastlane/metadata/android/en-US/images/phoneScreenshots/main_screen.png" width="200" />
<img label="Config" style="padding-left:25px" src="fastlane/metadata/android/en-US/images/phoneScreenshots/config_screen.png" width="200" />
<img label="Settings" style="padding-left:25px" src="fastlane/metadata/android/en-US/images/phoneScreenshots/settings_screen.png" width="200" />
<img label="Support" style="padding-left:25px" src="fastlane/metadata/android/en-US/images/phoneScreenshots/support_screen.png" width="200" />
</p>
<div style="text-align: left;">
<div align="left">
## Inspiration
The original inspiration for this app came from the inconvenience of having to manually turn VPN off
and on while on different networks. This app was created to offer a free solution to this problem.
## Features
* Add tunnels via .conf file, zip, manual entry, clipboard, or QR code
* Auto-tunnel based on Wi-Fi SSID, ethernet, or mobile data
* Add tunnels via .conf file, zip, manual entry, or QR code
* Auto connect to tunnels based on Wi-Fi SSID, ethernet, or mobile data
* Split tunneling by application with search
* Support for kernel and userspace modes
* WireGuard support for kernel and userspace modes
* Amnezia support for userspace mode for DPI/censorship protection
* Pre/Post Up/Down scripts support for all modes on a rooted device
* Always-On VPN support
* Export tunnels to zip
* Export Amnezia and WireGuard tunnels to zip
* Quick tile support for tunnel toggling, auto-tunneling
* Shortcuts support for tunnel toggling, auto-tunneling
* Static shortcuts support for tunnel toggling, auto-tunneling
* Intent automation support for all tunnels
* In app VPN kill switch with LAN bypass
* Automatic auto-tunneling service and/or tunnel restart after reboot or app update
* Battery preservation measures
* Restart tunnel on ping failure
* Restart tunnel on ping failure (beta)
## Building
## Fdroid
```sh
git clone https://github.com/zaneschepke/wgtunnel
cd wgtunnel
```
Want updates faster?
```sh
./gradlew assembleDebug
```
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).
## Translation
@@ -114,6 +86,19 @@ 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
```
$ git clone https://github.com/zaneschepke/wgtunnel
$ cd wgtunnel
```
And then build the app:
```
$ ./gradlew assembleDebug
```
## Contributing
Any contributions in the form of feedback, issues, code, or translations are welcome and much
+5 -15
View File
@@ -14,7 +14,7 @@ val versionCodeIncrement = with(getBuildTaskName().lowercase()) {
when {
this.contains(Constants.NIGHTLY) || this.contains(Constants.PRERELEASE) -> {
if (versionFile.exists()) {
versionFile.readText().trim().toInt() + 1
versionFile.readText().toInt() + 1
} else {
1
}
@@ -31,14 +31,6 @@ android {
generateLocaleConfig = true
}
// reproducibility
dependenciesInfo {
// Disables dependency metadata when building APKs.
includeInApk = false
// Disables dependency metadata when building Android App Bundles.
includeInBundle = false
}
defaultConfig {
applicationId = Constants.APP_ID
minSdk = Constants.MIN_SDK
@@ -172,7 +164,8 @@ dependencies {
debugImplementation(libs.androidx.compose.ui.tooling)
debugImplementation(libs.androidx.compose.manifest)
// tunnel
// get tunnel lib from github packages or mavenLocal
// implementation(fileTree(mapOf("dir" to "libs", "include" to listOf("*.aar"))))
implementation(libs.tunnel)
implementation(libs.amneziawg.android)
coreLibraryDesugaring(libs.desugar.jdk.libs)
@@ -187,10 +180,10 @@ dependencies {
// hilt
implementation(libs.hilt.android)
ksp(libs.hilt.android.compiler)
ksp(libs.androidx.hilt.compiler)
// accompanist
implementation(libs.accompanist.permissions)
implementation(libs.accompanist.flowlayout)
implementation(libs.accompanist.drawablepainter)
// storage
@@ -218,13 +211,10 @@ dependencies {
// shortcuts
implementation(libs.androidx.core)
implementation(libs.androidx.core.google.shortcuts)
// splash
implementation(libs.androidx.core.splashscreen)
// worker
implementation(libs.androidx.work.runtime)
implementation(libs.androidx.hilt.work)
}
fun determineVersionName(): String {
+1 -35
View File
@@ -2,38 +2,4 @@
-keepclassmembers class * extends androidx.datastore.preferences.protobuf.GeneratedMessageLite {
<fields>;
}
# Keep all classes in the org.xbill.DNS package and subpackages
-keep class org.xbill.DNS.** { *; }
-dontwarn org.xbill.DNS.**
# Preserve JNA classes if used (e.g., for IPHlpAPI on Windows)
-keep class com.sun.jna.** { *; }
-dontwarn com.sun.jna.**
# Keep DNS resolver configuration classes that might be loaded dynamically
-keep class org.xbill.DNS.config.** { *; }
-dontwarn org.xbill.DNS.config.**
-keep class org.xbill.DNS.** { *; }
# Prevent optimization issues with native or reflection-based calls
-dontoptimize
-dontshrink
# Uncomment the above if errors persist, but use sparingly as theyre broad
# Suppress warnings about missing classes if not all features are used
-dontwarn java.lang.management.**
-dontwarn sun.nio.ch.**
-keep class com.google.api.client.http.** { *; }
-dontwarn com.google.api.client.http.**
# Keep Joda-Time classes used by Tink
-keep class org.joda.time.** { *; }
-dontwarn org.joda.time.**
-keep class org.slf4j.** { *; }
-dontwarn org.slf4j.**
}
+1 -34
View File
@@ -21,37 +21,4 @@
#-renamesourcefileattribute SourceFile
-keepclassmembers class * extends androidx.datastore.preferences.protobuf.GeneratedMessageLite {
<fields>;
}
# Keep all classes in the org.xbill.DNS package and subpackages
-keep class org.xbill.DNS.** { *; }
-dontwarn org.xbill.DNS.**
# Preserve JNA classes if used (e.g., for IPHlpAPI on Windows)
-keep class com.sun.jna.** { *; }
-dontwarn com.sun.jna.**
# Keep DNS resolver configuration classes that might be loaded dynamically
-keep class org.xbill.DNS.config.** { *; }
-dontwarn org.xbill.DNS.config.**
-keep class org.xbill.DNS.** { *; }
# Prevent optimization issues with native or reflection-based calls
-dontoptimize
-dontshrink
# Uncomment the above if errors persist, but use sparingly as theyre broad
# Suppress warnings about missing classes if not all features are used
-dontwarn java.lang.management.**
-dontwarn sun.nio.ch.**
-keep class com.google.api.client.http.** { *; }
-dontwarn com.google.api.client.http.**
# Keep Joda-Time classes used by Tink
-keep class org.joda.time.** { *; }
-dontwarn org.joda.time.**
-keep class org.slf4j.** { *; }
-dontwarn org.slf4j.**
}
@@ -1,274 +0,0 @@
{
"formatVersion": 1,
"database": {
"version": 14,
"identityHash": "f2b260c389fb2e53216de40e4b1047f3",
"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_ping_enabled` INTEGER NOT NULL DEFAULT false, `is_amnezia_enabled` INTEGER NOT NULL DEFAULT false, `is_wildcards_enabled` INTEGER NOT NULL DEFAULT false, `is_wifi_by_shell_enabled` INTEGER NOT NULL DEFAULT false, `is_stop_on_no_internet_enabled` INTEGER NOT NULL DEFAULT false, `is_vpn_kill_switch_enabled` INTEGER NOT NULL DEFAULT false, `is_kernel_kill_switch_enabled` INTEGER NOT NULL DEFAULT false, `is_lan_on_kill_switch_enabled` INTEGER NOT NULL DEFAULT false, `debounce_delay_seconds` INTEGER NOT NULL DEFAULT 3)",
"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": "isPingEnabled",
"columnName": "is_ping_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isAmneziaEnabled",
"columnName": "is_amnezia_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isWildcardsEnabled",
"columnName": "is_wildcards_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isWifiNameByShellEnabled",
"columnName": "is_wifi_by_shell_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isStopOnNoInternetEnabled",
"columnName": "is_stop_on_no_internet_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isVpnKillSwitchEnabled",
"columnName": "is_vpn_kill_switch_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isKernelKillSwitchEnabled",
"columnName": "is_kernel_kill_switch_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isLanOnKillSwitchEnabled",
"columnName": "is_lan_on_kill_switch_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "debounceDelaySeconds",
"columnName": "debounce_delay_seconds",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "3"
}
],
"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 '', `is_Active` INTEGER NOT NULL DEFAULT false, `is_ping_enabled` INTEGER NOT NULL DEFAULT false, `ping_interval` INTEGER DEFAULT null, `ping_cooldown` INTEGER DEFAULT null, `ping_ip` TEXT DEFAULT null, `is_ethernet_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"
},
{
"fieldPath": "amQuick",
"columnName": "am_quick",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "''"
},
{
"fieldPath": "isActive",
"columnName": "is_Active",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isPingEnabled",
"columnName": "is_ping_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "pingInterval",
"columnName": "ping_interval",
"affinity": "INTEGER",
"notNull": false,
"defaultValue": "null"
},
{
"fieldPath": "pingCooldown",
"columnName": "ping_cooldown",
"affinity": "INTEGER",
"notNull": false,
"defaultValue": "null"
},
{
"fieldPath": "pingIp",
"columnName": "ping_ip",
"affinity": "TEXT",
"notNull": false,
"defaultValue": "null"
},
{
"fieldPath": "isEthernetTunnel",
"columnName": "is_ethernet_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, 'f2b260c389fb2e53216de40e4b1047f3')"
]
}
}
@@ -1,281 +0,0 @@
{
"formatVersion": 1,
"database": {
"version": 15,
"identityHash": "4827f3b1ab5a4e5aa35937a0925d50e4",
"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_ping_enabled` INTEGER NOT NULL DEFAULT false, `is_amnezia_enabled` INTEGER NOT NULL DEFAULT false, `is_wildcards_enabled` INTEGER NOT NULL DEFAULT false, `is_wifi_by_shell_enabled` INTEGER NOT NULL DEFAULT false, `is_stop_on_no_internet_enabled` INTEGER NOT NULL DEFAULT false, `is_vpn_kill_switch_enabled` INTEGER NOT NULL DEFAULT false, `is_kernel_kill_switch_enabled` INTEGER NOT NULL DEFAULT false, `is_lan_on_kill_switch_enabled` INTEGER NOT NULL DEFAULT false, `debounce_delay_seconds` INTEGER NOT NULL DEFAULT 3, `is_disable_kill_switch_on_trusted_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": "isPingEnabled",
"columnName": "is_ping_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isAmneziaEnabled",
"columnName": "is_amnezia_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isWildcardsEnabled",
"columnName": "is_wildcards_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isWifiNameByShellEnabled",
"columnName": "is_wifi_by_shell_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isStopOnNoInternetEnabled",
"columnName": "is_stop_on_no_internet_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isVpnKillSwitchEnabled",
"columnName": "is_vpn_kill_switch_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isKernelKillSwitchEnabled",
"columnName": "is_kernel_kill_switch_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isLanOnKillSwitchEnabled",
"columnName": "is_lan_on_kill_switch_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "debounceDelaySeconds",
"columnName": "debounce_delay_seconds",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "3"
},
{
"fieldPath": "isDisableKillSwitchOnTrustedEnabled",
"columnName": "is_disable_kill_switch_on_trusted_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 '', `is_Active` INTEGER NOT NULL DEFAULT false, `is_ping_enabled` INTEGER NOT NULL DEFAULT false, `ping_interval` INTEGER DEFAULT null, `ping_cooldown` INTEGER DEFAULT null, `ping_ip` TEXT DEFAULT null, `is_ethernet_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"
},
{
"fieldPath": "amQuick",
"columnName": "am_quick",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "''"
},
{
"fieldPath": "isActive",
"columnName": "is_Active",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isPingEnabled",
"columnName": "is_ping_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "pingInterval",
"columnName": "ping_interval",
"affinity": "INTEGER",
"notNull": false,
"defaultValue": "null"
},
{
"fieldPath": "pingCooldown",
"columnName": "ping_cooldown",
"affinity": "INTEGER",
"notNull": false,
"defaultValue": "null"
},
{
"fieldPath": "pingIp",
"columnName": "ping_ip",
"affinity": "TEXT",
"notNull": false,
"defaultValue": "null"
},
{
"fieldPath": "isEthernetTunnel",
"columnName": "is_ethernet_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, '4827f3b1ab5a4e5aa35937a0925d50e4')"
]
}
}
@@ -1,288 +0,0 @@
{
"formatVersion": 1,
"database": {
"version": 16,
"identityHash": "ae51793c4d09ea3194ecd26f0606f35c",
"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_ping_enabled` INTEGER NOT NULL DEFAULT false, `is_amnezia_enabled` INTEGER NOT NULL DEFAULT false, `is_wildcards_enabled` INTEGER NOT NULL DEFAULT false, `is_wifi_by_shell_enabled` INTEGER NOT NULL DEFAULT false, `is_stop_on_no_internet_enabled` INTEGER NOT NULL DEFAULT false, `is_vpn_kill_switch_enabled` INTEGER NOT NULL DEFAULT false, `is_kernel_kill_switch_enabled` INTEGER NOT NULL DEFAULT false, `is_lan_on_kill_switch_enabled` INTEGER NOT NULL DEFAULT false, `debounce_delay_seconds` INTEGER NOT NULL DEFAULT 3, `is_disable_kill_switch_on_trusted_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": "isPingEnabled",
"columnName": "is_ping_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isAmneziaEnabled",
"columnName": "is_amnezia_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isWildcardsEnabled",
"columnName": "is_wildcards_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isWifiNameByShellEnabled",
"columnName": "is_wifi_by_shell_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isStopOnNoInternetEnabled",
"columnName": "is_stop_on_no_internet_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isVpnKillSwitchEnabled",
"columnName": "is_vpn_kill_switch_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isKernelKillSwitchEnabled",
"columnName": "is_kernel_kill_switch_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isLanOnKillSwitchEnabled",
"columnName": "is_lan_on_kill_switch_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "debounceDelaySeconds",
"columnName": "debounce_delay_seconds",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "3"
},
{
"fieldPath": "isDisableKillSwitchOnTrustedEnabled",
"columnName": "is_disable_kill_switch_on_trusted_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 '', `is_Active` INTEGER NOT NULL DEFAULT false, `is_ping_enabled` INTEGER NOT NULL DEFAULT false, `ping_interval` INTEGER DEFAULT null, `ping_cooldown` INTEGER DEFAULT null, `ping_ip` TEXT DEFAULT null, `is_ethernet_tunnel` INTEGER NOT NULL DEFAULT false, `is_ipv4_preferred` INTEGER NOT NULL DEFAULT true)",
"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": "''"
},
{
"fieldPath": "isActive",
"columnName": "is_Active",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isPingEnabled",
"columnName": "is_ping_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "pingInterval",
"columnName": "ping_interval",
"affinity": "INTEGER",
"notNull": false,
"defaultValue": "null"
},
{
"fieldPath": "pingCooldown",
"columnName": "ping_cooldown",
"affinity": "INTEGER",
"notNull": false,
"defaultValue": "null"
},
{
"fieldPath": "pingIp",
"columnName": "ping_ip",
"affinity": "TEXT",
"notNull": false,
"defaultValue": "null"
},
{
"fieldPath": "isEthernetTunnel",
"columnName": "is_ethernet_tunnel",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isIpv4Preferred",
"columnName": "is_ipv4_preferred",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "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, 'ae51793c4d09ea3194ecd26f0606f35c')"
]
}
}
+30 -26
View File
@@ -66,20 +66,21 @@
android:theme="@style/Theme.App.Start"
tools:targetApi="tiramisu">
<activity
android:name=".MainActivity"
android:name=".ui.MainActivity"
android:exported="true"
android:windowSoftInputMode="adjustResize"
android:theme="@style/Theme.WireguardAutoTunnel"
android:configChanges="orientation|screenSize|keyboardHidden"
>
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<action android:name="android.intent.action.SHOW_APP_INFO" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.LAUNCHER" />
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
<action android:name="android.service.quicksettings.action.QS_TILE_PREFERENCES" />
</intent-filter>
<meta-data
android:name="android.app.shortcuts"
android:resource="@xml/shortcuts" />
</activity>
<activity
android:name="com.journeyapps.barcodescanner.CaptureActivity"
@@ -87,7 +88,7 @@
tools:replace="screenOrientation" />
<activity
android:name=".core.shortcut.ShortcutsActivity"
android:name=".service.shortcut.ShortcutsActivity"
android:enabled="true"
android:exported="true"
android:noHistory="true"
@@ -106,18 +107,11 @@
android:resource="@xml/file_paths" />
</provider>
<provider
android:name="androidx.startup.InitializationProvider"
android:authorities="${applicationId}.androidx-startup"
android:multiprocess="true"
tools:node="remove">
</provider>
<service
android:name=".core.service.tile.TunnelControlTile"
android:name=".service.tile.TunnelControlTile"
android:exported="true"
android:icon="@drawable/ic_launcher"
android:label="@string/tunnel_control"
android:label="Tunnel control"
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE">
<meta-data
android:name="android.service.quicksettings.ACTIVE_TILE"
@@ -131,10 +125,10 @@
</intent-filter>
</service>
<service
android:name=".core.service.tile.AutoTunnelControlTile"
android:name=".service.tile.AutoTunnelControlTile"
android:exported="true"
android:icon="@drawable/ic_launcher"
android:label="@string/auto_tunnel"
android:label="Auto-tunnel"
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE">
<meta-data
android:name="android.service.quicksettings.ACTIVE_TILE"
@@ -147,8 +141,23 @@
<action android:name="android.service.quicksettings.action.QS_TILE" />
</intent-filter>
</service>
<service
android:name=".service.tunnel.AlwaysOnVpnService"
android:enabled="true"
android:exported="false"
android:foregroundServiceType="systemExempted"
android:permission="android.permission.BIND_VPN_SERVICE"
android:persistent="true"
tools:node="merge">
<intent-filter>
<action android:name="android.net.VpnService" />
</intent-filter>
<meta-data
android:name="android.net.VpnService.SUPPORTS_ALWAYS_ON"
android:value="true" />
</service>
<service
android:name=".core.service.autotunnel.AutoTunnelService"
android:name=".service.foreground.autotunnel.AutoTunnelService"
android:enabled="true"
android:exported="false"
android:foregroundServiceType="systemExempted"
@@ -157,7 +166,7 @@
tools:node="merge" />
<service
android:name=".core.service.TunnelForegroundService"
android:name=".service.foreground.TunnelBackgroundService"
android:exported="false"
android:persistent="true"
android:foregroundServiceType="systemExempted"
@@ -168,7 +177,7 @@
</service>
<receiver
android:name=".core.broadcast.BootReceiver"
android:name=".receiver.BootReceiver"
android:enabled="true"
android:exported="false">
<intent-filter>
@@ -181,24 +190,19 @@
</intent-filter>
</receiver>
<receiver
android:name=".core.broadcast.AppUpdateReceiver"
android:name=".receiver.AppUpdateReceiver"
android:exported="false">
<intent-filter>
<action android:name="android.intent.action.MY_PACKAGE_REPLACED" />
</intent-filter>
</receiver>
<receiver
android:name=".core.broadcast.KernelReceiver"
android:name=".receiver.KernelReceiver"
android:exported="false"
android:permission="${applicationId}.permission.CONTROL_TUNNELS">
<intent-filter>
<action android:name="com.wireguard.android.action.REFRESH_TUNNEL_STATES" />
</intent-filter>
</receiver>
<receiver
android:name=".core.broadcast.NotificationActionReceiver"
android:exported="false"
android:permission="${applicationId}.permission.CONTROL_TUNNELS">
</receiver>
</application>
</manifest>
@@ -3,19 +3,13 @@ package com.zaneschepke.wireguardautotunnel
import android.app.Application
import android.os.StrictMode
import android.os.StrictMode.ThreadPolicy
import androidx.hilt.work.HiltWorkerFactory
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.ProcessLifecycleOwner
import androidx.work.Configuration
import com.wireguard.android.backend.GoBackend
import com.zaneschepke.logcatter.LogReader
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager
import com.zaneschepke.wireguardautotunnel.di.ApplicationScope
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
import com.zaneschepke.wireguardautotunnel.di.MainDispatcher
import com.zaneschepke.wireguardautotunnel.domain.enums.BackendState
import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository
import com.zaneschepke.wireguardautotunnel.data.repository.AppStateRepository
import com.zaneschepke.wireguardautotunnel.data.repository.SettingsRepository
import com.zaneschepke.wireguardautotunnel.module.ApplicationScope
import com.zaneschepke.wireguardautotunnel.module.IoDispatcher
import com.zaneschepke.wireguardautotunnel.service.tunnel.BackendState
import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelService
import com.zaneschepke.wireguardautotunnel.util.LocaleUtil
import com.zaneschepke.wireguardautotunnel.util.ReleaseTree
import com.zaneschepke.wireguardautotunnel.util.extensions.isRunningOnTv
@@ -23,20 +17,11 @@ import dagger.hilt.android.HiltAndroidApp
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import timber.log.Timber
import javax.inject.Inject
@HiltAndroidApp
class WireGuardAutoTunnel : Application(), Configuration.Provider {
@Inject
lateinit var workerFactory: HiltWorkerFactory
override val workManagerConfiguration: Configuration
get() = Configuration.Builder()
.setWorkerFactory(workerFactory)
.build()
class WireGuardAutoTunnel : Application() {
@Inject
@ApplicationScope
@@ -46,23 +31,21 @@ class WireGuardAutoTunnel : Application(), Configuration.Provider {
lateinit var logReader: LogReader
@Inject
lateinit var appDataRepository: AppDataRepository
lateinit var appStateRepository: AppStateRepository
@Inject
lateinit var settingsRepository: SettingsRepository
@Inject
lateinit var tunnelService: TunnelService
@Inject
@IoDispatcher
lateinit var ioDispatcher: CoroutineDispatcher
@Inject
@MainDispatcher
lateinit var mainDispatcher: CoroutineDispatcher
@Inject
lateinit var tunnelManager: TunnelManager
override fun onCreate() {
super.onCreate()
instance = this
ProcessLifecycleOwner.get().lifecycle.addObserver(AppLifecycleObserver())
if (BuildConfig.DEBUG) {
Timber.plant(Timber.DebugTree())
StrictMode.setThreadPolicy(
@@ -76,31 +59,19 @@ class WireGuardAutoTunnel : Application(), Configuration.Provider {
} else {
Timber.plant(ReleaseTree())
}
GoBackend.setAlwaysOnCallback {
applicationScope.launch {
val settings = appDataRepository.settings.get()
if (settings.isAlwaysOnVpnEnabled) {
val tunnel = appDataRepository.getPrimaryOrFirstTunnel()
tunnel?.let {
tunnelManager.startTunnel(it)
}
} else {
Timber.Forest.w("Always-on VPN is not enabled in app settings")
}
applicationScope.launch {
if (!settingsRepository.getSettings().isKernelEnabled) {
tunnelService.setBackendState(BackendState.SERVICE_ACTIVE, emptyList())
}
appStateRepository.getLocale()?.let {
LocaleUtil.changeLocale(it)
}
}
applicationScope.launch {
withContext(mainDispatcher) {
if (appDataRepository.appState.isLocalLogsEnabled() && !isRunningOnTv()) logReader.initialize()
}
if (!appDataRepository.settings.get().isKernelEnabled) {
tunnelManager.setBackendState(BackendState.SERVICE_ACTIVE, emptyList())
}
appDataRepository.appState.getLocale()?.let {
withContext(mainDispatcher) {
LocaleUtil.changeLocale(it)
if (!isRunningOnTv()) {
applicationScope.launch(ioDispatcher) {
if (appStateRepository.isLocalLogsEnabled()) {
Timber.d("Starting logger")
logReader.start()
}
}
}
@@ -108,30 +79,12 @@ class WireGuardAutoTunnel : Application(), Configuration.Provider {
override fun onTerminate() {
applicationScope.launch {
tunnelManager.setBackendState(BackendState.SERVICE_ACTIVE, emptyList())
tunnelService.setBackendState(BackendState.INACTIVE, emptyList())
}
super.onTerminate()
}
class AppLifecycleObserver : DefaultLifecycleObserver {
override fun onStart(owner: LifecycleOwner) {
Timber.d("Application entered foreground")
foreground = true
}
override fun onPause(owner: LifecycleOwner) {
Timber.d("Application entered background")
foreground = false
}
}
companion object {
private var foreground = false
fun isForeground(): Boolean {
return foreground
}
lateinit var instance: WireGuardAutoTunnel
private set
}
@@ -1,45 +0,0 @@
package com.zaneschepke.wireguardautotunnel.core.broadcast
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository
import com.zaneschepke.wireguardautotunnel.di.ApplicationScope
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import javax.inject.Inject
@AndroidEntryPoint
class AppUpdateReceiver : BroadcastReceiver() {
@Inject
@ApplicationScope
lateinit var applicationScope: CoroutineScope
@Inject
lateinit var appDataRepository: AppDataRepository
@Inject
lateinit var serviceManager: ServiceManager
@Inject
lateinit var tunnelManager: TunnelManager
override fun onReceive(context: Context, intent: Intent) {
if (intent.action != Intent.ACTION_MY_PACKAGE_REPLACED) return
serviceManager.updateTunnelTile()
serviceManager.updateAutoTunnelTile()
applicationScope.launch {
with(appDataRepository.settings.get()) {
if (isRestoreOnBootEnabled) {
// If auto tunnel is enabled, just start it and let auto tunnel start appropriate tun
if (isAutoTunnelEnabled && !serviceManager.autoTunnelActive.value) return@launch serviceManager.startAutoTunnel(true)
tunnelManager.restorePreviousState()
}
}
}
}
}
@@ -1,44 +0,0 @@
package com.zaneschepke.wireguardautotunnel.core.broadcast
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository
import com.zaneschepke.wireguardautotunnel.di.ApplicationScope
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import javax.inject.Inject
@AndroidEntryPoint
class BootReceiver : BroadcastReceiver() {
@Inject
lateinit var appDataRepository: AppDataRepository
@Inject
@ApplicationScope
lateinit var applicationScope: CoroutineScope
@Inject
lateinit var serviceManager: ServiceManager
@Inject
lateinit var tunnelManager: TunnelManager
override fun onReceive(context: Context, intent: Intent) {
if (Intent.ACTION_BOOT_COMPLETED != intent.action) return
serviceManager.updateTunnelTile()
serviceManager.updateAutoTunnelTile()
applicationScope.launch {
with(appDataRepository.settings.get()) {
if (isRestoreOnBootEnabled) {
// If auto tunnel is enabled, just start it and let auto tunnel start appropriate tun
if (isAutoTunnelEnabled && !serviceManager.autoTunnelActive.value) return@launch serviceManager.startAutoTunnel(true)
tunnelManager.restorePreviousState()
}
}
}
}
}
@@ -1,49 +0,0 @@
package com.zaneschepke.wireguardautotunnel.core.broadcast
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository
import com.zaneschepke.wireguardautotunnel.di.ApplicationScope
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import javax.inject.Inject
@AndroidEntryPoint
class KernelReceiver : BroadcastReceiver() {
@Inject
@ApplicationScope
lateinit var applicationScope: CoroutineScope
@Inject
lateinit var tunnelRepository: TunnelRepository
@Inject
lateinit var serviceManager: ServiceManager
@Inject
lateinit var tunnelManager: TunnelManager
override fun onReceive(context: Context, intent: Intent) {
val action = intent.action ?: return
applicationScope.launch {
if (action == REFRESH_TUNNELS_ACTION) {
tunnelManager.runningTunnelNames().forEach { name ->
val tunnel = tunnelRepository.findByTunnelName(name)
tunnel?.let {
tunnelRepository.save(it.copy(isActive = true))
}
}
serviceManager.updateTunnelTile()
}
}
}
companion object {
const val REFRESH_TUNNELS_ACTION = "com.wireguard.android.action.REFRESH_TUNNEL_STATES"
}
}
@@ -1,46 +0,0 @@
package com.zaneschepke.wireguardautotunnel.core.broadcast
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationManager
import com.zaneschepke.wireguardautotunnel.di.ApplicationScope
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager
import com.zaneschepke.wireguardautotunnel.domain.enums.NotificationAction
import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import javax.inject.Inject
@AndroidEntryPoint
class NotificationActionReceiver : BroadcastReceiver() {
@Inject
lateinit var serviceManager: ServiceManager
@Inject
lateinit var tunnelManager: TunnelManager
@Inject
lateinit var tunnelRepository: TunnelRepository
@Inject
@ApplicationScope
lateinit var applicationScope: CoroutineScope
override fun onReceive(context: Context, intent: Intent) {
applicationScope.launch {
when (intent.action) {
NotificationAction.AUTO_TUNNEL_OFF.name -> serviceManager.stopAutoTunnel()
NotificationAction.TUNNEL_OFF.name -> {
val tunnelId = intent.getIntExtra(NotificationManager.EXTRA_ID, 0)
if (tunnelId == 0) return@launch tunnelManager.stopTunnel()
val tunnel = tunnelRepository.getById(tunnelId)
tunnelManager.stopTunnel(tunnel)
}
}
}
}
}
@@ -1,161 +0,0 @@
package com.zaneschepke.wireguardautotunnel.core.network
import android.content.Context
import android.net.ConnectivityManager
import android.net.Network
import android.net.NetworkCapabilities
import android.net.NetworkRequest
import android.net.wifi.SupplicantState
import android.net.wifi.WifiManager
import android.os.Build
import com.zaneschepke.wireguardautotunnel.domain.state.ConnectivityState
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.flowOn
import javax.inject.Inject
class InternetConnectivityMonitor
@Inject
constructor(
@ApplicationContext private val context: Context,
@IoDispatcher private val ioDispatcher: CoroutineDispatcher,
) : NetworkMonitor {
private val connectivityManager =
context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
@get:Synchronized @set:Synchronized
private var wifiCapabilities: NetworkCapabilities? = null
@get:Synchronized @set:Synchronized
private var wifiNetworkChanged: Boolean = false
override val didWifiChangeSinceLastCapabilitiesQuery: Boolean
get() = wifiNetworkChanged
override val status = callbackFlow {
var wifiState: Boolean = false
var ethernetState: Boolean = false
var cellularState: Boolean = false
fun emitState() {
trySend(ConnectivityState(wifiState, ethernetState, cellularState))
}
val currentNetwork = connectivityManager.activeNetwork
if (currentNetwork == null) {
emitState()
}
fun updateCapabilityState(up: Boolean, network: Network) {
with(connectivityManager.getNetworkCapabilities(network)) {
when {
this == null -> return
hasTransport(NetworkCapabilities.TRANSPORT_WIFI) -> wifiState = up
hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) ->
cellularState = up
hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET) ->
ethernetState = up
}
}
}
fun onWifiChange(network: Network, callback: () -> Unit) {
if (connectivityManager.getNetworkCapabilities(network)?.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) == true) {
callback()
}
}
fun onAvailable(network: Network) {
onWifiChange(network) {
wifiNetworkChanged = true
}
}
fun onCapabilitiesChanged(network: Network, networkCapabilities: NetworkCapabilities) {
onWifiChange(network) {
wifiCapabilities = networkCapabilities
}
updateCapabilityState(true, network)
emitState()
}
val networkStatusCallback =
when (Build.VERSION.SDK_INT) {
in Build.VERSION_CODES.S..Int.MAX_VALUE -> {
object :
ConnectivityManager.NetworkCallback(
FLAG_INCLUDE_LOCATION_INFO,
) {
override fun onAvailable(network: Network) {
onAvailable(network)
}
override fun onLost(network: Network) {
updateCapabilityState(false, network)
emitState()
}
override fun onCapabilitiesChanged(network: Network, networkCapabilities: NetworkCapabilities) {
onCapabilitiesChanged(network, networkCapabilities)
}
}
}
else -> {
object : ConnectivityManager.NetworkCallback() {
override fun onAvailable(network: Network) {
onAvailable(network)
}
override fun onLost(network: Network) {
updateCapabilityState(false, network)
emitState()
}
override fun onCapabilitiesChanged(network: Network, networkCapabilities: NetworkCapabilities) {
onCapabilitiesChanged(network, networkCapabilities)
}
}
}
}
val request =
NetworkRequest.Builder()
.addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
.addTransportType(NetworkCapabilities.TRANSPORT_ETHERNET)
.addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR)
.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
.addCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED)
.build()
connectivityManager.registerNetworkCallback(request, networkStatusCallback)
awaitClose { connectivityManager.unregisterNetworkCallback(networkStatusCallback) }
}.flowOn(ioDispatcher)
override fun getWifiCapabilities(): NetworkCapabilities? {
wifiNetworkChanged = false
return wifiCapabilities
}
companion object {
fun getNetworkName(networkCapabilities: NetworkCapabilities, context: Context): String? {
var ssid = networkCapabilities.getWifiName()
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.S) {
val wifiManager =
context.applicationContext.getSystemService(Context.WIFI_SERVICE) as WifiManager
@Suppress("DEPRECATION")
val info = wifiManager.connectionInfo
if (info.supplicantState === SupplicantState.COMPLETED) {
ssid = info.ssid
}
}
return ssid?.trim('"')
}
}
}
@@ -1,13 +0,0 @@
package com.zaneschepke.wireguardautotunnel.core.network
import android.net.NetworkCapabilities
import com.zaneschepke.wireguardautotunnel.domain.state.ConnectivityState
import kotlinx.coroutines.flow.Flow
interface NetworkMonitor {
val status: Flow<ConnectivityState>
// util to help limit location queries
val didWifiChangeSinceLastCapabilitiesQuery: Boolean
fun getWifiCapabilities(): NetworkCapabilities?
}
@@ -1,47 +0,0 @@
package com.zaneschepke.wireguardautotunnel.core.notification
import android.app.Notification
import android.app.NotificationManager
import android.content.Context
import androidx.core.app.NotificationCompat
import com.zaneschepke.wireguardautotunnel.domain.enums.NotificationAction
import com.zaneschepke.wireguardautotunnel.core.notification.WireGuardNotification.NotificationChannels
import com.zaneschepke.wireguardautotunnel.util.StringValue
interface NotificationManager {
val context: Context
fun createNotification(
channel: NotificationChannels,
title: String = "",
actions: Collection<NotificationCompat.Action> = emptyList(),
description: String = "",
showTimestamp: Boolean = false,
importance: Int = NotificationManager.IMPORTANCE_HIGH,
onGoing: Boolean = true,
onlyAlertOnce: Boolean = true,
): Notification
fun createNotification(
channel: NotificationChannels,
title: StringValue,
actions: Collection<NotificationCompat.Action> = emptyList(),
description: StringValue,
showTimestamp: Boolean = false,
importance: Int = NotificationManager.IMPORTANCE_HIGH,
onGoing: Boolean = true,
onlyAlertOnce: Boolean = true,
): Notification
fun createNotificationAction(notificationAction: NotificationAction, extraId: Int? = null): NotificationCompat.Action
fun remove(notificationId: Int)
fun show(notificationId: Int, notification: Notification)
companion object {
const val KERNEL_SERVICE_NOTIFICATION_ID = 123
const val AUTO_TUNNEL_NOTIFICATION_ID = 122
const val VPN_NOTIFICATION_ID = 100
const val EXTRA_ID = "id"
}
}
@@ -1,169 +0,0 @@
package com.zaneschepke.wireguardautotunnel.core.notification
import android.Manifest
import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.graphics.Color
import androidx.core.app.ActivityCompat
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.core.broadcast.NotificationActionReceiver
import com.zaneschepke.wireguardautotunnel.MainActivity
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationManager.Companion.EXTRA_ID
import com.zaneschepke.wireguardautotunnel.domain.enums.NotificationAction
import com.zaneschepke.wireguardautotunnel.util.StringValue
import dagger.hilt.android.qualifiers.ApplicationContext
import javax.inject.Inject
class WireGuardNotification
@Inject
constructor(
@ApplicationContext override val context: Context,
) : com.zaneschepke.wireguardautotunnel.core.notification.NotificationManager {
enum class NotificationChannels {
VPN,
AUTO_TUNNEL,
}
private val notificationManager = NotificationManagerCompat.from(context)
override fun createNotification(
channel: NotificationChannels,
title: String,
actions: Collection<NotificationCompat.Action>,
description: String,
showTimestamp: Boolean,
importance: Int,
onGoing: Boolean,
onlyAlertOnce: Boolean,
): Notification {
notificationManager.createNotificationChannel(channel.asChannel())
return channel.asBuilder().apply {
actions.forEach {
addAction(it)
}
setContentTitle(title)
setContentIntent(
PendingIntent.getActivity(
context,
0,
Intent(context, MainActivity::class.java),
PendingIntent.FLAG_IMMUTABLE,
),
)
setContentText(description)
setOnlyAlertOnce(onlyAlertOnce)
setOngoing(onGoing)
setPriority(NotificationCompat.PRIORITY_HIGH)
setShowWhen(showTimestamp)
setSmallIcon(R.drawable.ic_launcher)
}.build()
}
override fun createNotification(
channel: NotificationChannels,
title: StringValue,
actions: Collection<NotificationCompat.Action>,
description: StringValue,
showTimestamp: Boolean,
importance: Int,
onGoing: Boolean,
onlyAlertOnce: Boolean,
): Notification {
return createNotification(
channel,
title.asString(context),
actions,
description.asString(context),
showTimestamp,
importance,
onGoing,
onlyAlertOnce,
)
}
override fun createNotificationAction(notificationAction: NotificationAction, extraId: Int?): NotificationCompat.Action {
val pendingIntent = PendingIntent.getBroadcast(
context,
0,
Intent(context, NotificationActionReceiver::class.java).apply {
action = notificationAction.name
if (extraId != null) putExtra(EXTRA_ID, extraId)
},
PendingIntent.FLAG_IMMUTABLE,
)
return NotificationCompat.Action.Builder(
R.drawable.ic_launcher,
notificationAction.title(context).uppercase(),
pendingIntent,
).build()
}
override fun remove(notificationId: Int) {
notificationManager.cancel(notificationId)
}
override fun show(notificationId: Int, notification: Notification) {
with(notificationManager) {
if (ActivityCompat.checkSelfPermission(context, Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) {
return
}
notify(notificationId, notification)
}
}
private fun NotificationChannels.asBuilder(): NotificationCompat.Builder {
return when (this) {
NotificationChannels.AUTO_TUNNEL -> {
NotificationCompat.Builder(
context,
context.getString(R.string.auto_tunnel_channel_id),
)
}
NotificationChannels.VPN -> {
NotificationCompat.Builder(
context,
context.getString(R.string.vpn_channel_id),
)
}
}
}
private fun NotificationChannels.asChannel(): NotificationChannel {
return when (this) {
NotificationChannels.VPN -> {
NotificationChannel(
context.getString(R.string.vpn_channel_id),
context.getString(R.string.vpn_channel_name),
NotificationManager.IMPORTANCE_HIGH,
).apply {
description = context.getString(R.string.vpn_channel_description)
enableLights(true)
lightColor = Color.WHITE
enableVibration(false)
vibrationPattern = longArrayOf(100, 200, 300)
}
}
NotificationChannels.AUTO_TUNNEL -> {
NotificationChannel(
context.getString(R.string.auto_tunnel_channel_id),
context.getString(R.string.auto_tunnel_channel_name),
NotificationManager.IMPORTANCE_HIGH,
).apply {
description = context.getString(R.string.auto_tunnel_channel_description)
enableLights(true)
lightColor = Color.WHITE
enableVibration(false)
vibrationPattern = longArrayOf(100, 200, 300)
}
}
}
}
}
@@ -1,119 +0,0 @@
package com.zaneschepke.wireguardautotunnel.core.service
import android.app.Service
import android.content.Context
import android.content.Intent
import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository
import com.zaneschepke.wireguardautotunnel.core.service.autotunnel.AutoTunnelService
import com.zaneschepke.wireguardautotunnel.core.service.tile.AutoTunnelControlTile
import com.zaneschepke.wireguardautotunnel.core.service.tile.TunnelControlTile
import com.zaneschepke.wireguardautotunnel.domain.entity.TunnelConf
import com.zaneschepke.wireguardautotunnel.util.extensions.requestAutoTunnelTileServiceUpdate
import com.zaneschepke.wireguardautotunnel.util.extensions.requestTunnelTileServiceStateUpdate
import jakarta.inject.Inject
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.withContext
import timber.log.Timber
@OptIn(ExperimentalCoroutinesApi::class)
class ServiceManager
@Inject constructor(private val context: Context, private val ioDispatcher: CoroutineDispatcher, private val appDataRepository: AppDataRepository) {
private val _autoTunnelActive = MutableStateFlow(false)
val autoTunnelActive = _autoTunnelActive.asStateFlow()
var autoTunnelService = CompletableDeferred<AutoTunnelService>()
var backgroundService = CompletableDeferred<TunnelForegroundService>()
var autoTunnelTile = CompletableDeferred<AutoTunnelControlTile>()
var tunnelControlTile = CompletableDeferred<TunnelControlTile>()
private fun <T : Service> startService(cls: Class<T>, background: Boolean) {
runCatching {
val intent = Intent(context, cls)
if (background) {
context.startForegroundService(intent)
} else {
context.startService(intent)
}
}.onFailure { Timber.e(it) }
}
suspend fun startAutoTunnel(background: Boolean) {
val settings = appDataRepository.settings.get()
appDataRepository.settings.save(settings.copy(isAutoTunnelEnabled = true))
if (autoTunnelService.isCompleted) return _autoTunnelActive.update { true }
runCatching {
startService(AutoTunnelService::class.java, background)
autoTunnelService.await()
autoTunnelService.getCompleted().start()
_autoTunnelActive.update { true }
updateAutoTunnelTile()
}.onFailure {
Timber.e(it)
}
}
suspend fun startBackgroundService(tunnelConf: TunnelConf) {
if (backgroundService.isCompleted) return
runCatching {
startService(TunnelForegroundService::class.java, true)
backgroundService.await()
backgroundService.getCompleted().start(tunnelConf)
}.onFailure {
Timber.e(it)
}
}
fun stopBackgroundService() {
if (!backgroundService.isCompleted) return
runCatching {
backgroundService.getCompleted().stop()
}.onFailure {
Timber.e(it)
}
}
suspend fun toggleAutoTunnel(background: Boolean) {
withContext(ioDispatcher) {
if (_autoTunnelActive.value) return@withContext stopAutoTunnel()
startAutoTunnel(background)
}
}
fun updateAutoTunnelTile() {
if (autoTunnelTile.isCompleted) {
autoTunnelTile.getCompleted().updateTileState()
} else {
context.requestAutoTunnelTileServiceUpdate()
}
}
fun updateTunnelTile() {
if (tunnelControlTile.isCompleted) {
tunnelControlTile.getCompleted().updateTileState()
} else {
context.requestTunnelTileServiceStateUpdate()
}
}
suspend fun stopAutoTunnel() {
withContext(ioDispatcher) {
val settings = appDataRepository.settings.get()
appDataRepository.settings.save(settings.copy(isAutoTunnelEnabled = false))
if (!autoTunnelService.isCompleted) return@withContext
runCatching {
autoTunnelService.getCompleted().stop()
_autoTunnelActive.update { false }
updateAutoTunnelTile()
}.onFailure {
Timber.e(it)
}
}
}
}
@@ -1,254 +0,0 @@
package com.zaneschepke.wireguardautotunnel.core.service.autotunnel
import android.content.Intent
import android.net.NetworkCapabilities
import android.os.IBinder
import android.os.PowerManager
import androidx.core.app.ServiceCompat
import androidx.lifecycle.LifecycleService
import androidx.lifecycle.lifecycleScope
import com.wireguard.android.util.RootShell
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository
import com.zaneschepke.wireguardautotunnel.di.AppShell
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
import com.zaneschepke.wireguardautotunnel.di.MainImmediateDispatcher
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
import com.zaneschepke.wireguardautotunnel.domain.state.AutoTunnelState
import com.zaneschepke.wireguardautotunnel.domain.state.NetworkState
import com.zaneschepke.wireguardautotunnel.core.network.InternetConnectivityMonitor
import com.zaneschepke.wireguardautotunnel.core.network.NetworkMonitor
import com.zaneschepke.wireguardautotunnel.domain.state.ConnectivityState
import com.zaneschepke.wireguardautotunnel.domain.enums.NotificationAction
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationManager
import com.zaneschepke.wireguardautotunnel.core.notification.WireGuardNotification
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager
import com.zaneschepke.wireguardautotunnel.domain.entity.AppSettings
import com.zaneschepke.wireguardautotunnel.domain.enums.BackendState
import com.zaneschepke.wireguardautotunnel.domain.events.AutoTunnelEvent
import com.zaneschepke.wireguardautotunnel.domain.events.KillSwitchEvent
import com.zaneschepke.wireguardautotunnel.util.Constants
import com.zaneschepke.wireguardautotunnel.util.extensions.Tunnels
import com.zaneschepke.wireguardautotunnel.util.extensions.getCurrentWifiName
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject
import javax.inject.Provider
@AndroidEntryPoint
class AutoTunnelService : LifecycleService() {
@Inject
@AppShell
lateinit var rootShell: Provider<RootShell>
@Inject
lateinit var networkMonitor: NetworkMonitor
@Inject
lateinit var appDataRepository: Provider<AppDataRepository>
@Inject
lateinit var notificationManager: NotificationManager
@Inject
@IoDispatcher
lateinit var ioDispatcher: CoroutineDispatcher
@Inject
lateinit var serviceManager: ServiceManager
@Inject
@MainImmediateDispatcher
lateinit var mainImmediateDispatcher: CoroutineDispatcher
@Inject
lateinit var tunnelManager: TunnelManager
private val defaultState = AutoTunnelState()
private val autoTunnelStateFlow = MutableStateFlow(defaultState)
private var wakeLock: PowerManager.WakeLock? = null
override fun onCreate() {
super.onCreate()
serviceManager.autoTunnelService.complete(this)
lifecycleScope.launch(mainImmediateDispatcher) {
runCatching {
launchWatcherNotification()
}.onFailure {
Timber.e(it)
}
}
}
override fun onBind(intent: Intent): IBinder? {
super.onBind(intent)
return null
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
Timber.d("onStartCommand executed with startId: $startId")
serviceManager.autoTunnelService.complete(this)
return super.onStartCommand(intent, flags, startId)
}
fun start() {
kotlin.runCatching {
lifecycleScope.launch(mainImmediateDispatcher) {
launchWatcherNotification()
initWakeLock()
}
startAutoTunnelJob()
startAutoTunnelStateJob()
startKillSwitchJob()
}.onFailure {
Timber.e(it)
}
}
fun stop() {
wakeLock?.let { if (it.isHeld) it.release() }
stopSelf()
}
override fun onDestroy() {
serviceManager.autoTunnelService = CompletableDeferred()
super.onDestroy()
}
private fun launchWatcherNotification(description: String = getString(R.string.monitoring_state_changes)) {
val notification =
notificationManager.createNotification(
WireGuardNotification.NotificationChannels.AUTO_TUNNEL,
title = getString(R.string.auto_tunnel_title),
description = description,
actions = listOf(
notificationManager.createNotificationAction(NotificationAction.AUTO_TUNNEL_OFF),
),
)
ServiceCompat.startForeground(
this,
NotificationManager.AUTO_TUNNEL_NOTIFICATION_ID,
notification,
Constants.SYSTEM_EXEMPT_SERVICE_TYPE_ID,
)
}
private fun initWakeLock() {
wakeLock = (getSystemService(POWER_SERVICE) as PowerManager).run {
val tag = this.javaClass.name
newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "$tag::lock").apply {
try {
Timber.i("Initiating wakelock with 10 min timeout")
acquire(Constants.BATTERY_SAVER_WATCHER_WAKE_LOCK_TIMEOUT)
} finally {
release()
}
}
}
}
private suspend fun buildNetworkState(connectivityState: ConnectivityState): NetworkState {
return with(autoTunnelStateFlow.value.networkState) {
val wifiName = when {
connectivityState.wifiAvailable &&
(wifiName == null || wifiName == Constants.UNREADABLE_SSID || networkMonitor.didWifiChangeSinceLastCapabilitiesQuery) -> {
networkMonitor.getWifiCapabilities()?.let { getWifiName(it) } ?: wifiName
}
!connectivityState.wifiAvailable -> null
else -> wifiName
}
copy(
isWifiConnected = connectivityState.wifiAvailable,
isMobileDataConnected = connectivityState.cellularAvailable,
isEthernetConnected = isEthernetConnected,
wifiName = wifiName,
)
}
}
private fun startAutoTunnelStateJob() = lifecycleScope.launch(ioDispatcher) {
combine(
combineSettings(),
networkMonitor.status.map {
buildNetworkState(it)
}.distinctUntilChanged(),
) { double, networkState ->
AutoTunnelState(tunnelManager.activeTunnels.value, networkState, double.first, double.second)
}.collect { state ->
autoTunnelStateFlow.update {
it.copy(activeTunnels = state.activeTunnels, networkState = state.networkState, settings = state.settings, tunnels = state.tunnels)
}
}
}
private suspend fun getWifiName(wifiCapabilities: NetworkCapabilities): String? {
val setting = appDataRepository.get().settings.get()
return if (setting.isWifiNameByShellEnabled) {
rootShell.get().getCurrentWifiName()
} else {
InternetConnectivityMonitor.getNetworkName(wifiCapabilities, this@AutoTunnelService)
}
}
private fun combineSettings(): Flow<Pair<AppSettings, Tunnels>> {
return combine(
appDataRepository.get().settings.flow,
appDataRepository.get().tunnels.flow.map { tunnels ->
// isActive is ignored for equality checks so user can manually toggle off tunnel with auto-tunnel
tunnels.map { it.copy(isActive = false) }
},
) { settings, tunnels ->
Pair(settings, tunnels)
}.distinctUntilChanged()
}
private fun startKillSwitchJob() = lifecycleScope.launch(ioDispatcher) {
autoTunnelStateFlow.collect {
if (it == defaultState) return@collect
when (val event = it.asKillSwitchEvent()) {
KillSwitchEvent.DoNothing -> Unit
is KillSwitchEvent.Start -> {
Timber.d("Starting kill switch")
tunnelManager.setBackendState(BackendState.KILL_SWITCH_ACTIVE, event.allowedIps)
}
KillSwitchEvent.Stop -> {
Timber.d("Stopping kill switch")
tunnelManager.setBackendState(BackendState.SERVICE_ACTIVE, emptySet())
}
}
}
}
@OptIn(FlowPreview::class)
private fun startAutoTunnelJob() = lifecycleScope.launch(ioDispatcher) {
Timber.i("Starting auto-tunnel network event watcher")
val settings = appDataRepository.get().settings.get()
Timber.d("Starting with debounce delay of: ${settings.debounceDelaySeconds} seconds")
autoTunnelStateFlow.debounce(settings.debounceDelayMillis()).collect { watcherState ->
if (watcherState == defaultState) return@collect
Timber.d("New auto tunnel state emitted")
when (val event = watcherState.asAutoTunnelEvent()) {
is AutoTunnelEvent.Start -> (event.tunnelConf ?: appDataRepository.get().getPrimaryOrFirstTunnel())?.let {
tunnelManager.startTunnel(it)
}
// TODO improve this to target specific tunnels to better support multi-tunnel
is AutoTunnelEvent.Stop -> tunnelManager.stopTunnel()
AutoTunnelEvent.DoNothing -> Timber.i("Auto-tunneling: no condition met")
}
}
}
}
@@ -1,135 +0,0 @@
package com.zaneschepke.wireguardautotunnel.core.service.tile
import android.content.Intent
import android.os.Build
import android.os.IBinder
import android.service.quicksettings.Tile
import android.service.quicksettings.TileService
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager
import com.zaneschepke.wireguardautotunnel.di.ApplicationScope
import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject
@AndroidEntryPoint
class TunnelControlTile : TileService() {
@Inject
lateinit var appDataRepository: AppDataRepository
@Inject
@ApplicationScope
lateinit var applicationScope: CoroutineScope
@Inject
lateinit var serviceManager: ServiceManager
@Inject
lateinit var tunnelManager: TunnelManager
override fun onCreate() {
super.onCreate()
serviceManager.tunnelControlTile.complete(this)
}
override fun onDestroy() {
super.onDestroy()
serviceManager.tunnelControlTile = CompletableDeferred()
}
override fun onStartListening() {
super.onStartListening()
Timber.d("Start listening called")
serviceManager.tunnelControlTile.complete(this)
applicationScope.launch {
updateTileState()
}
}
fun updateTileState() = applicationScope.launch {
val tunnels = appDataRepository.tunnels.getAll()
if (tunnels.isEmpty()) return@launch setUnavailable()
with(tunnelManager.activeTunnels.value) {
if (isNotEmpty()) if (size == 1) {
tunnels.firstOrNull { it.id == keys.first() }?.let { return@launch updateTile(it.tunName, true) }
} else {
return@launch updateTile(getString(R.string.multiple), true)
}
}
appDataRepository.getStartTunnelConfig()?.let {
updateTile(it.tunName, false)
}
}
override fun onClick() {
super.onClick()
unlockAndRun {
applicationScope.launch {
if (tunnelManager.activeTunnels.value.isNotEmpty()) return@launch tunnelManager.stopTunnel()
appDataRepository.getStartTunnelConfig()?.let {
tunnelManager.startTunnel(it)
}
}
}
}
private fun setActive() {
runCatching {
qsTile.state = Tile.STATE_ACTIVE
qsTile.updateTile()
}
}
private fun setInactive() {
runCatching {
qsTile.state = Tile.STATE_INACTIVE
qsTile.updateTile()
}
}
private fun setUnavailable() {
runCatching {
qsTile.state = Tile.STATE_UNAVAILABLE
setTileDescription("")
qsTile.updateTile()
}
}
private fun setTileDescription(description: String) {
runCatching {
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()
}
}
/* This works around an annoying unsolved frameworks bug some people are hitting. */
override fun onBind(intent: Intent): IBinder? {
var ret: IBinder? = null
try {
ret = super.onBind(intent)
} catch (_: Throwable) {
Timber.e("Failed to bind to TunnelControlTile")
}
return ret
}
private fun updateTile(name: String, active: Boolean) {
runCatching {
setTileDescription(name)
if (active) return setActive()
setInactive()
}.onFailure {
Timber.e(it)
}
}
}
@@ -1,79 +0,0 @@
package com.zaneschepke.wireguardautotunnel.core.shortcut
import android.content.Context
import android.content.Intent
import androidx.core.content.pm.ShortcutInfoCompat
import androidx.core.content.pm.ShortcutManagerCompat
import androidx.core.graphics.drawable.IconCompat
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.withContext
class DynamicShortcutManager(private val context: Context, @IoDispatcher private val ioDispatcher: CoroutineDispatcher) : ShortcutManager {
override suspend fun addShortcuts() {
withContext(ioDispatcher) {
ShortcutManagerCompat.setDynamicShortcuts(context, createShortcuts())
}
}
override suspend fun removeShortcuts() {
withContext(ioDispatcher) {
ShortcutManagerCompat.removeDynamicShortcuts(context, createShortcuts().map { it.id })
}
}
private fun createShortcuts(): List<ShortcutInfoCompat> {
return listOf(
buildShortcut(
context.getString(R.string.vpn_off),
context.getString(R.string.vpn_off),
context.getString(R.string.vpn_off),
intent = Intent(context, ShortcutsActivity::class.java).apply {
putExtra("className", "WireGuardTunnelService")
action = ShortcutsActivity.Action.STOP.name
},
shortcutIcon = R.drawable.vpn_off,
),
buildShortcut(
context.getString(R.string.vpn_on),
context.getString(R.string.vpn_on),
context.getString(R.string.vpn_on),
intent = Intent(context, ShortcutsActivity::class.java).apply {
putExtra("className", "WireGuardTunnelService")
action = ShortcutsActivity.Action.START.name
},
shortcutIcon = R.drawable.vpn_on,
),
buildShortcut(
context.getString(R.string.start_auto),
context.getString(R.string.start_auto),
context.getString(R.string.start_auto),
intent = Intent(context, ShortcutsActivity::class.java).apply {
putExtra("className", "WireGuardConnectivityWatcherService")
action = ShortcutsActivity.Action.START.name
},
shortcutIcon = R.drawable.auto_play,
),
buildShortcut(
context.getString(R.string.stop_auto),
context.getString(R.string.stop_auto),
context.getString(R.string.stop_auto),
intent = Intent(context, ShortcutsActivity::class.java).apply {
putExtra("className", "WireGuardConnectivityWatcherService")
action = ShortcutsActivity.Action.STOP.name
},
shortcutIcon = R.drawable.auto_pause,
),
)
}
private fun buildShortcut(id: String, shortLabel: String, longLabel: String, intent: Intent, shortcutIcon: Int): ShortcutInfoCompat {
return ShortcutInfoCompat.Builder(context, id)
.setShortLabel(shortLabel)
.setLongLabel(longLabel)
.setIntent(intent)
.setIcon(IconCompat.createWithResource(context, shortcutIcon))
.build()
}
}
@@ -1,6 +0,0 @@
package com.zaneschepke.wireguardautotunnel.core.shortcut
interface ShortcutManager {
suspend fun addShortcuts()
suspend fun removeShortcuts()
}
@@ -1,234 +0,0 @@
package com.zaneschepke.wireguardautotunnel.core.tunnel
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
import com.zaneschepke.wireguardautotunnel.core.network.NetworkMonitor
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationManager
import com.zaneschepke.wireguardautotunnel.core.notification.WireGuardNotification
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelProvider.Companion.CHECK_INTERVAL
import com.zaneschepke.wireguardautotunnel.di.ApplicationScope
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
import com.zaneschepke.wireguardautotunnel.domain.entity.TunnelConf
import com.zaneschepke.wireguardautotunnel.domain.enums.BackendError
import com.zaneschepke.wireguardautotunnel.domain.enums.BackendState
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelStatus
import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelState
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelStatistics
import com.zaneschepke.wireguardautotunnel.ui.common.snackbar.SnackbarController
import com.zaneschepke.wireguardautotunnel.util.Constants
import com.zaneschepke.wireguardautotunnel.util.StringValue
import com.zaneschepke.wireguardautotunnel.util.extensions.cancelWithMessage
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import timber.log.Timber
import java.util.concurrent.atomic.AtomicBoolean
open class BaseTunnel(
@IoDispatcher private val ioDispatcher: CoroutineDispatcher,
@ApplicationScope private val applicationScope: CoroutineScope,
private val networkMonitor: NetworkMonitor,
private val appDataRepository: AppDataRepository,
private val serviceManager: ServiceManager,
private val notificationManager: NotificationManager,
) : TunnelProvider {
internal val tunnels = MutableStateFlow<List<TunnelConf>>(emptyList())
private val _activeTunnels = MutableStateFlow<Map<Int, TunnelState>>(emptyMap())
private val tunnelJobs = mutableMapOf<TunnelConf, Job>()
private val isNetworkAvailable = AtomicBoolean(false)
init {
applicationScope.launch(ioDispatcher) {
launch {
startNetworkJob()
}
tunnels.collect { tuns ->
val previousTuns = tunnelJobs.keys.toSet()
val newTuns = tuns - previousTuns
val removedItems = previousTuns - tuns.toSet()
newTuns.forEach { tun ->
Timber.d("Starting tunnel jobs for tun ${tun.name}")
tunnelJobs[tun] = startTunnelJobs(tun)
}
removedItems.forEach { tun ->
tunnelJobs[tun]?.cancelWithMessage("Canceling tunnel jobs for tunnel: ${tun.name}")
tunnelJobs.remove(tun)
_activeTunnels.update { it - tun.id }
serviceManager.updateTunnelTile()
}
}
}
}
private fun startTunnelJobs(tunnel: TunnelConf) = applicationScope.launch(ioDispatcher) {
launch {
startTunnelStatisticsJob(tunnel)
}
launch {
startPingJob(tunnel)
}
launch {
startTunnelConfigChangeJob(tunnel)
}
launch {
startStateJob(tunnel)
}
}
override suspend fun bounceTunnel(tunnelConf: TunnelConf) {
if (tunnels.value.any { it.id == tunnelConf.id }) {
toggleTunnel(tunnelConf, TunnelStatus.DOWN)
toggleTunnel(tunnelConf, TunnelStatus.UP)
}
}
override suspend fun setBackendState(backendState: BackendState, allowedIps: Collection<String>) {
}
override suspend fun runningTunnelNames(): Set<String> {
return emptySet()
}
override val activeTunnels: StateFlow<Map<Int, TunnelState>>
get() = _activeTunnels.asStateFlow()
override suspend fun startTunnel(tunnelConf: TunnelConf) {
serviceManager.startBackgroundService(tunnelConf)
appDataRepository.tunnels.save(tunnelConf.copy(isActive = true))
addToActiveTunnels(tunnelConf)
}
override suspend fun stopTunnel(tunnelConf: TunnelConf?) {
}
open suspend fun toggleTunnel(tunnelConf: TunnelConf, state: TunnelStatus) {
}
open suspend fun getStatistics(tunnelConf: TunnelConf): TunnelStatistics {
throw NotImplementedError("Get statistics not implemented in base class")
}
internal suspend fun onTunnelStop(tunnelConf: TunnelConf) {
appDataRepository.tunnels.save(tunnelConf.copy(isActive = false))
removeFromActiveTunnels(tunnelConf)
if (tunnels.value.isEmpty()) serviceManager.stopBackgroundService()
}
internal suspend fun stopAllTunnels() {
tunnels.value.forEach {
stopTunnel(it)
}
}
private fun addToActiveTunnels(conf: TunnelConf) {
tunnels.update {
it.toMutableList().apply {
add(conf)
}
}
}
private fun removeFromActiveTunnels(conf: TunnelConf) {
tunnels.update {
it.toMutableList().apply {
remove(conf)
}
}
}
private suspend fun startNetworkJob() = coroutineScope {
networkMonitor.status.distinctUntilChanged().collect {
isNetworkAvailable.set(!it.allOffline)
}
}
private suspend fun startStateJob(tunnel: TunnelConf) {
tunnel.state.collect { state ->
_activeTunnels.update {
it + (tunnel.id to state)
}
serviceManager.updateTunnelTile()
}
}
private suspend fun startPingJob(tunnel: TunnelConf) = coroutineScope {
while (isActive) {
if (isNetworkAvailable.get() && tunnel.isActive) {
val pingResult = tunnel.pingTunnel(ioDispatcher)
handlePingResult(tunnel, pingResult)
}
delay(tunnel.pingInterval ?: Constants.PING_INTERVAL)
}
}
private suspend fun handlePingResult(tunnel: TunnelConf, pingResult: List<Boolean>) {
if (pingResult.contains(false)) {
if (isNetworkAvailable.get()) {
Timber.i("Ping result: target was not reachable, bouncing the tunnel")
bounceTunnel(tunnel)
delay(tunnel.pingCooldown ?: Constants.PING_COOLDOWN)
} else {
Timber.i("Ping result: target was not reachable, but no network available")
}
} else {
Timber.i("Ping result: all ping targets were reached successfully")
}
}
internal fun handleBackendThrowable(backendError: BackendError) {
val message = when (backendError) {
BackendError.Config -> StringValue.StringResource(R.string.start_failed_config)
BackendError.DNS -> StringValue.StringResource(R.string.dns_error)
BackendError.Unauthorized -> StringValue.StringResource(R.string.unauthorized)
}
if (WireGuardAutoTunnel.isForeground()) {
SnackbarController.showMessage(message)
} else {
notificationManager.show(
NotificationManager.VPN_NOTIFICATION_ID,
notificationManager.createNotification(
WireGuardNotification.NotificationChannels.VPN,
title = StringValue.StringResource(R.string.tunne_start_failed_title),
description = message,
),
)
}
}
private suspend fun startTunnelConfigChangeJob(tunnel: TunnelConf) = coroutineScope {
appDataRepository.tunnels.flow.collect { storageTuns ->
storageTuns.firstOrNull { it.id == tunnel.id }?.let { storageTun ->
if (!tunnel.isQuickConfigMatching(storageTun) || !tunnel.isPingConfigMatching(storageTun)) {
bounceTunnel(tunnel)
}
}
}
}
private suspend fun startTunnelStatisticsJob(tunnel: TunnelConf) = coroutineScope {
while (isActive) {
val stats = getStatistics(tunnel)
tunnel.state.update {
it.copy(statistics = stats)
}
delay(CHECK_INTERVAL)
}
}
}
@@ -1,83 +0,0 @@
package com.zaneschepke.wireguardautotunnel.core.tunnel
import com.wireguard.android.backend.Backend
import com.wireguard.android.backend.BackendException
import com.wireguard.android.backend.Tunnel
import com.zaneschepke.wireguardautotunnel.core.network.NetworkMonitor
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationManager
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
import com.zaneschepke.wireguardautotunnel.di.ApplicationScope
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
import com.zaneschepke.wireguardautotunnel.domain.entity.TunnelConf
import com.zaneschepke.wireguardautotunnel.domain.enums.BackendState
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelStatus
import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelStatistics
import com.zaneschepke.wireguardautotunnel.domain.state.WireGuardStatistics
import com.zaneschepke.wireguardautotunnel.util.extensions.toBackendError
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.withContext
import timber.log.Timber
import javax.inject.Inject
class KernelTunnel @Inject constructor(
@IoDispatcher private val ioDispatcher: CoroutineDispatcher,
@ApplicationScope private val applicationScope: CoroutineScope,
serviceManager: ServiceManager,
appDataRepository: AppDataRepository,
notificationManager: NotificationManager,
private val backend: Backend,
networkMonitor: NetworkMonitor,
) : BaseTunnel(ioDispatcher, applicationScope, networkMonitor, appDataRepository, serviceManager, notificationManager) {
override suspend fun startTunnel(tunnelConf: TunnelConf) {
withContext(ioDispatcher) {
if (tunnels.value.any { it.id == tunnelConf.id }) return@withContext Timber.w("Tunnel already running")
runCatching {
backend.setState(tunnelConf, Tunnel.State.UP, tunnelConf.toWgConfig())
super.startTunnel(tunnelConf)
}.onFailure {
onTunnelStop(tunnelConf)
if (it is BackendException) {
handleBackendThrowable(it.toBackendError())
} else {
Timber.e(it)
}
}
}
}
override suspend fun getStatistics(tunnelConf: TunnelConf): TunnelStatistics {
return WireGuardStatistics(backend.getStatistics(tunnelConf))
}
override suspend fun stopTunnel(tunnelConf: TunnelConf?) {
withContext(ioDispatcher) {
val tunnel = tunnels.value.firstOrNull { it.id == tunnelConf?.id }
runCatching {
tunnel?.let {
backend.setState(it, Tunnel.State.DOWN, it.toWgConfig())
onTunnelStop(it)
} ?: stopAllTunnels()
}.onFailure {
Timber.e(it)
}
}
}
override suspend fun toggleTunnel(tunnelConf: TunnelConf, state: TunnelStatus) {
when (state) {
TunnelStatus.UP -> backend.setState(tunnelConf, Tunnel.State.UP, tunnelConf.toWgConfig())
TunnelStatus.DOWN -> backend.setState(tunnelConf, Tunnel.State.DOWN, tunnelConf.toWgConfig())
}
}
override suspend fun setBackendState(backendState: BackendState, allowedIps: Collection<String>) {
Timber.w("Not yet implemented for kernel")
}
override suspend fun runningTunnelNames(): Set<String> {
return backend.runningTunnelNames
}
}
@@ -1,108 +0,0 @@
package com.zaneschepke.wireguardautotunnel.core.tunnel
import com.zaneschepke.wireguardautotunnel.di.ApplicationScope
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
import com.zaneschepke.wireguardautotunnel.di.Kernel
import com.zaneschepke.wireguardautotunnel.di.Userspace
import com.zaneschepke.wireguardautotunnel.domain.entity.AppSettings
import com.zaneschepke.wireguardautotunnel.domain.entity.TunnelConf
import com.zaneschepke.wireguardautotunnel.domain.enums.BackendState
import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository
import com.zaneschepke.wireguardautotunnel.util.extensions.withData
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.plus
import kotlinx.coroutines.withContext
import javax.inject.Inject
class TunnelManager @Inject constructor(
@Kernel private val kernelTunnel: TunnelProvider,
@Userspace private val userspaceTunnel: TunnelProvider,
private val appDataRepository: AppDataRepository,
@ApplicationScope private val applicationScope: CoroutineScope,
@IoDispatcher private val ioDispatcher: CoroutineDispatcher,
) : TunnelProvider {
val appSettings: StateFlow<AppSettings?> = appDataRepository.settings.flow.stateIn(
scope = applicationScope.plus(ioDispatcher),
started = SharingStarted.Eagerly,
initialValue = null,
)
@OptIn(ExperimentalCoroutinesApi::class)
override val activeTunnels = appSettings
.filterNotNull()
.flatMapLatest { settings ->
if (settings.isKernelEnabled) {
kernelTunnel.activeTunnels
} else {
userspaceTunnel.activeTunnels
}
}
.stateIn(
scope = applicationScope,
started = SharingStarted.Eagerly,
initialValue = emptyMap(),
)
override suspend fun startTunnel(tunnelConf: TunnelConf) {
appSettings.withData {
if (it.isKernelEnabled) return@withData kernelTunnel.startTunnel(tunnelConf)
userspaceTunnel.startTunnel(tunnelConf)
}
}
override suspend fun stopTunnel(tunnelConf: TunnelConf?) {
appSettings.withData {
if (it.isKernelEnabled) return@withData kernelTunnel.stopTunnel(tunnelConf)
userspaceTunnel.stopTunnel(tunnelConf)
}
}
override suspend fun bounceTunnel(tunnelConf: TunnelConf) {
appSettings.withData {
if (it.isKernelEnabled) return@withData kernelTunnel.stopTunnel(tunnelConf)
userspaceTunnel.stopTunnel(tunnelConf)
}
}
override suspend fun setBackendState(backendState: BackendState, allowedIps: Collection<String>) {
appSettings.withData {
if (it.isKernelEnabled) return@withData kernelTunnel.setBackendState(backendState, allowedIps)
userspaceTunnel.setBackendState(backendState, allowedIps)
}
}
override suspend fun runningTunnelNames(): Set<String> {
appSettings.filterNotNull().first().let {
if (it.isKernelEnabled) return kernelTunnel.runningTunnelNames()
return userspaceTunnel.runningTunnelNames()
}
}
suspend fun restorePreviousState() {
withContext(ioDispatcher) {
with(appDataRepository.settings.get()) {
if (isRestoreOnBootEnabled) {
val previouslyActiveTuns = appDataRepository.tunnels.getActive()
// handle kernel mode
val tunsToStart = previouslyActiveTuns.filterNot { tun -> activeTunnels.value.any { tun.id == it.key } }
if (isKernelEnabled) {
return@withContext tunsToStart.forEach {
startTunnel(it)
}
}
// handle userspace
if (activeTunnels.value.isEmpty()) tunsToStart.firstOrNull()?.let { startTunnel(it) }
}
}
}
}
}
@@ -1,25 +0,0 @@
package com.zaneschepke.wireguardautotunnel.core.tunnel
import com.zaneschepke.wireguardautotunnel.domain.entity.TunnelConf
import com.zaneschepke.wireguardautotunnel.domain.enums.BackendState
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelState
import kotlinx.coroutines.flow.StateFlow
interface TunnelProvider {
val activeTunnels: StateFlow<Map<Int, TunnelState>>
suspend fun startTunnel(tunnelConf: TunnelConf)
suspend fun stopTunnel(tunnelConf: TunnelConf? = null)
suspend fun bounceTunnel(tunnelConf: TunnelConf)
suspend fun setBackendState(backendState: BackendState, allowedIps: Collection<String>)
suspend fun runningTunnelNames(): Set<String>
companion object {
const val CHECK_INTERVAL = 1_000L
}
}
@@ -1,86 +0,0 @@
package com.zaneschepke.wireguardautotunnel.core.tunnel
import com.wireguard.android.backend.BackendException
import com.zaneschepke.wireguardautotunnel.core.network.NetworkMonitor
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationManager
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
import com.zaneschepke.wireguardautotunnel.di.ApplicationScope
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
import com.zaneschepke.wireguardautotunnel.domain.entity.TunnelConf
import com.zaneschepke.wireguardautotunnel.domain.enums.BackendState
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelStatus
import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository
import com.zaneschepke.wireguardautotunnel.domain.state.AmneziaStatistics
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelStatistics
import com.zaneschepke.wireguardautotunnel.util.extensions.asAmBackendState
import com.zaneschepke.wireguardautotunnel.util.extensions.toBackendError
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.withContext
import org.amnezia.awg.backend.Backend
import org.amnezia.awg.backend.Tunnel
import timber.log.Timber
import javax.inject.Inject
class UserspaceTunnel @Inject constructor(
@IoDispatcher private val ioDispatcher: CoroutineDispatcher,
@ApplicationScope private val applicationScope: CoroutineScope,
serviceManager: ServiceManager,
appDataRepository: AppDataRepository,
notificationManager: NotificationManager,
private val backend: Backend,
networkMonitor: NetworkMonitor,
) : TunnelProvider, BaseTunnel(ioDispatcher, applicationScope, networkMonitor, appDataRepository, serviceManager, notificationManager) {
override suspend fun startTunnel(tunnelConf: TunnelConf) {
withContext(ioDispatcher) {
if (tunnels.value.any { it.id == tunnelConf.id }) return@withContext Timber.w("Tunnel already running")
if (tunnels.value.isNotEmpty()) {
stopAllTunnels()
}
runCatching {
backend.setState(tunnelConf, Tunnel.State.UP, tunnelConf.toAmConfig())
super.startTunnel(tunnelConf)
}.onFailure {
onTunnelStop(tunnelConf)
if (it is BackendException) {
handleBackendThrowable(it.toBackendError())
} else {
Timber.e(it)
}
}
}
}
override suspend fun getStatistics(tunnelConf: TunnelConf): TunnelStatistics {
return AmneziaStatistics(backend.getStatistics(tunnelConf))
}
override suspend fun toggleTunnel(tunnelConf: TunnelConf, status: TunnelStatus) {
when (status) {
TunnelStatus.UP -> backend.setState(tunnelConf, Tunnel.State.UP, tunnelConf.toAmConfig())
TunnelStatus.DOWN -> backend.setState(tunnelConf, Tunnel.State.DOWN, tunnelConf.toAmConfig())
}
}
override suspend fun stopTunnel(tunnelConf: TunnelConf?) {
withContext(ioDispatcher) {
runCatching {
tunnels.value.firstOrNull { it.id == tunnelConf?.id }?.let {
backend.setState(it, Tunnel.State.DOWN, it.toAmConfig())
onTunnelStop(it)
} ?: stopAllTunnels()
}.onFailure {
Timber.e(it)
}
}
}
override suspend fun setBackendState(backendState: BackendState, allowedIps: Collection<String>) {
backend.setBackendState(backendState.asAmBackendState(), allowedIps)
}
override suspend fun runningTunnelNames(): Set<String> {
return backend.runningTunnelNames
}
}
@@ -1,60 +0,0 @@
package com.zaneschepke.wireguardautotunnel.core.worker
import android.content.Context
import androidx.hilt.work.HiltWorker
import androidx.work.CoroutineWorker
import androidx.work.ExistingPeriodicWorkPolicy
import androidx.work.PeriodicWorkRequestBuilder
import androidx.work.WorkManager
import androidx.work.WorkerParameters
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.withContext
import timber.log.Timber
import java.util.concurrent.TimeUnit
@HiltWorker
class ServiceWorker @AssistedInject constructor(
@Assisted private val context: Context,
@Assisted private val params: WorkerParameters,
private val serviceManager: ServiceManager,
private val appDataRepository: AppDataRepository,
@IoDispatcher private val ioDispatcher: CoroutineDispatcher,
private val tunnelManager: TunnelManager,
) : CoroutineWorker(context, params) {
companion object {
private const val TAG = "service_worker"
fun stop(context: Context) {
WorkManager.getInstance(context).cancelAllWorkByTag(TAG)
}
fun start(context: Context) {
val periodicWorkRequest = PeriodicWorkRequestBuilder<ServiceWorker>(
repeatInterval = 15,
repeatIntervalTimeUnit = TimeUnit.MINUTES,
).build()
WorkManager.getInstance(context)
.enqueueUniquePeriodicWork(
TAG,
ExistingPeriodicWorkPolicy.KEEP,
periodicWorkRequest,
)
}
}
override suspend fun doWork(): Result = withContext(ioDispatcher) {
Timber.i("Service worker started")
with(appDataRepository.settings.get()) {
if (isAutoTunnelEnabled && !serviceManager.autoTunnelActive.value) return@with serviceManager.startAutoTunnel(true)
if (tunnelManager.activeTunnels.value.isEmpty()) tunnelManager.restorePreviousState()
}
Result.success()
}
}
@@ -6,14 +6,12 @@ import androidx.room.DeleteColumn
import androidx.room.RoomDatabase
import androidx.room.TypeConverters
import androidx.room.migration.AutoMigrationSpec
import com.zaneschepke.wireguardautotunnel.data.dao.SettingsDao
import com.zaneschepke.wireguardautotunnel.data.dao.TunnelConfigDao
import com.zaneschepke.wireguardautotunnel.data.model.Settings
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
import com.zaneschepke.wireguardautotunnel.data.domain.Settings
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
@Database(
entities = [Settings::class, TunnelConfig::class],
version = 16,
version = 13,
autoMigrations =
[
AutoMigration(from = 1, to = 2),
@@ -51,18 +49,6 @@ import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
from = 12,
to = 13,
),
AutoMigration(
from = 13,
to = 14,
),
AutoMigration(
from = 14,
to = 15,
),
AutoMigration(
from = 15,
to = 16,
),
],
exportSchema = true,
)
@@ -1,11 +1,11 @@
package com.zaneschepke.wireguardautotunnel.data.dao
package com.zaneschepke.wireguardautotunnel.data
import androidx.room.Dao
import androidx.room.Delete
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import com.zaneschepke.wireguardautotunnel.data.model.Settings
import com.zaneschepke.wireguardautotunnel.data.domain.Settings
import kotlinx.coroutines.flow.Flow
@Dao
@@ -1,11 +1,11 @@
package com.zaneschepke.wireguardautotunnel.data.dao
package com.zaneschepke.wireguardautotunnel.data
import androidx.room.Dao
import androidx.room.Delete
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
import com.zaneschepke.wireguardautotunnel.util.extensions.TunnelConfigs
import kotlinx.coroutines.flow.Flow
@@ -1,4 +1,4 @@
package com.zaneschepke.wireguardautotunnel.data
package com.zaneschepke.wireguardautotunnel.data.datastore
import android.content.Context
import androidx.datastore.preferences.core.Preferences
@@ -6,11 +6,10 @@ import androidx.datastore.preferences.core.booleanPreferencesKey
import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.stringPreferencesKey
import androidx.datastore.preferences.preferencesDataStore
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
import com.zaneschepke.wireguardautotunnel.module.IoDispatcher
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
@@ -44,7 +43,7 @@ class DataStoreManager(
try {
context.dataStore.data.first()
} catch (e: IOException) {
Timber.Forest.e(e)
Timber.e(e)
}
}
}
@@ -54,9 +53,9 @@ class DataStoreManager(
try {
context.dataStore.edit { it[key] = value }
} catch (e: IOException) {
Timber.Forest.e(e)
Timber.e(e)
} catch (e: Exception) {
Timber.Forest.e(e)
Timber.e(e)
}
}
}
@@ -66,9 +65,9 @@ class DataStoreManager(
try {
context.dataStore.edit { it.remove(key) }
} catch (e: IOException) {
Timber.Forest.e(e)
Timber.e(e)
} catch (e: Exception) {
Timber.Forest.e(e)
Timber.e(e)
}
}
}
@@ -80,7 +79,7 @@ class DataStoreManager(
try {
context.dataStore.data.map { it[key] }.first()
} catch (e: IOException) {
Timber.Forest.e(e)
Timber.e(e)
null
}
}
@@ -90,5 +89,5 @@ class DataStoreManager(
context.dataStore.data.map { it[key] }.first()
}
val preferencesFlow: Flow<Preferences?> = context.dataStore.data.flowOn(ioDispatcher)
val preferencesFlow: Flow<Preferences?> = context.dataStore.data
}
@@ -1,6 +1,5 @@
package com.zaneschepke.wireguardautotunnel.data.model
package com.zaneschepke.wireguardautotunnel.data.domain
import com.zaneschepke.wireguardautotunnel.domain.entity.AppState
import com.zaneschepke.wireguardautotunnel.ui.theme.Theme
data class GeneralState(
@@ -12,31 +11,7 @@ data class GeneralState(
val locale: String? = null,
val theme: Theme = Theme.AUTOMATIC,
) {
fun toAppState(): AppState = AppState(
isLocationDisclosureShown,
isBatteryOptimizationDisableShown,
isPinLockEnabled,
isTunnelStatsExpanded,
isLocationDisclosureShown,
locale,
theme,
)
companion object {
fun from(appState: AppState): GeneralState {
return with(appState) {
GeneralState(
isLocationDisclosureShown,
isBatteryOptimizationDisableShown,
isPinLockEnabled,
isTunnelStatsExpanded,
isLocationDisclosureShown,
locale,
theme,
)
}
}
const val LOCATION_DISCLOSURE_SHOWN_DEFAULT = false
const val BATTERY_OPTIMIZATION_DISABLE_SHOWN_DEFAULT = false
const val PIN_LOCK_ENABLED_DEFAULT = false
@@ -1,9 +1,8 @@
package com.zaneschepke.wireguardautotunnel.data.model
package com.zaneschepke.wireguardautotunnel.data.domain
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
import com.zaneschepke.wireguardautotunnel.domain.entity.AppSettings
@Entity
data class Settings(
@@ -81,38 +80,4 @@ data class Settings(
defaultValue = "false",
)
val isLanOnKillSwitchEnabled: Boolean = false,
@ColumnInfo(
name = "debounce_delay_seconds",
defaultValue = "3",
)
val debounceDelaySeconds: Int = 3,
@ColumnInfo(
name = "is_disable_kill_switch_on_trusted_enabled",
defaultValue = "false",
)
val isDisableKillSwitchOnTrustedEnabled: Boolean = false,
) {
fun toAppSettings(): AppSettings {
return AppSettings(
id, isAutoTunnelEnabled, isTunnelOnMobileDataEnabled, trustedNetworkSSIDs, isAlwaysOnVpnEnabled, isTunnelOnEthernetEnabled,
isShortcutsEnabled, isTunnelOnWifiEnabled, isKernelEnabled, isRestoreOnBootEnabled, isMultiTunnelEnabled, isPingEnabled,
isAmneziaEnabled, isWildcardsEnabled, isWifiNameByShellEnabled, isStopOnNoInternetEnabled, isVpnKillSwitchEnabled,
isKernelKillSwitchEnabled, isLanOnKillSwitchEnabled, debounceDelaySeconds, isDisableKillSwitchOnTrustedEnabled,
)
}
companion object {
fun from(appSettings: AppSettings): Settings {
return with(appSettings) {
Settings(
id, isAutoTunnelEnabled, isTunnelOnMobileDataEnabled, trustedNetworkSSIDs.toMutableList(), isAlwaysOnVpnEnabled,
isTunnelOnEthernetEnabled, isShortcutsEnabled, isTunnelOnWifiEnabled, isKernelEnabled, isRestoreOnBootEnabled,
isMultiTunnelEnabled, isPingEnabled, isAmneziaEnabled, isWildcardsEnabled, isWifiNameByShellEnabled,
isStopOnNoInternetEnabled, isVpnKillSwitchEnabled, isKernelKillSwitchEnabled, isLanOnKillSwitchEnabled,
debounceDelaySeconds, isDisableKillSwitchOnTrustedEnabled,
)
}
}
}
}
)
@@ -0,0 +1,105 @@
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 com.zaneschepke.wireguardautotunnel.util.extensions.toWgQuickString
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,
@ColumnInfo(
name = "is_Active",
defaultValue = "false",
)
val isActive: Boolean = false,
@ColumnInfo(
name = "is_ping_enabled",
defaultValue = "false",
)
val isPingEnabled: Boolean = false,
@ColumnInfo(
name = "ping_interval",
defaultValue = "null",
)
val pingInterval: Long? = null,
@ColumnInfo(
name = "ping_cooldown",
defaultValue = "null",
)
val pingCooldown: Long? = null,
@ColumnInfo(
name = "ping_ip",
defaultValue = "null",
)
var pingIp: String? = null,
@ColumnInfo(
name = "is_ethernet_tunnel",
defaultValue = "false",
)
var isEthernetTunnel: Boolean = false,
) {
fun toAmConfig(): org.amnezia.awg.config.Config {
return configFromAmQuick(if (amQuick != "") amQuick else wgQuick)
}
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)
}
}
fun tunnelConfigFromAmConfig(config: org.amnezia.awg.config.Config, name: String): TunnelConfig {
val amQuick = config.toAwgQuickString(true)
val wgQuick = config.toWgQuickString()
return TunnelConfig(name = name, wgQuick = wgQuick, amQuick = amQuick)
}
const val AM_QUICK_DEFAULT = ""
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",
)
}
}
@@ -1,93 +0,0 @@
package com.zaneschepke.wireguardautotunnel.data.model
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.Index
import androidx.room.PrimaryKey
import com.zaneschepke.wireguardautotunnel.domain.entity.TunnelConf
@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,
@ColumnInfo(
name = "is_Active",
defaultValue = "false",
)
val isActive: Boolean = false,
@ColumnInfo(
name = "is_ping_enabled",
defaultValue = "false",
)
val isPingEnabled: Boolean = false,
@ColumnInfo(
name = "ping_interval",
defaultValue = "null",
)
val pingInterval: Long? = null,
@ColumnInfo(
name = "ping_cooldown",
defaultValue = "null",
)
val pingCooldown: Long? = null,
@ColumnInfo(
name = "ping_ip",
defaultValue = "null",
)
var pingIp: String? = null,
@ColumnInfo(
name = "is_ethernet_tunnel",
defaultValue = "false",
)
var isEthernetTunnel: Boolean = false,
@ColumnInfo(
name = "is_ipv4_preferred",
defaultValue = "true",
)
var isIpv4Preferred: Boolean = true,
) {
fun toTunnel(): TunnelConf {
return TunnelConf(
id, name, wgQuick, tunnelNetworks, isMobileDataTunnel,
isPrimaryTunnel, amQuick, isActive, isPingEnabled, pingInterval,
pingCooldown, pingIp, isEthernetTunnel, isIpv4Preferred,
)
}
companion object {
const val AM_QUICK_DEFAULT = ""
fun from(tunnelConf: TunnelConf): TunnelConfig {
return with(tunnelConf) {
return TunnelConfig(
id, tunName, wgQuick, tunnelNetworks.toMutableList(), isMobileDataTunnel,
isPrimaryTunnel, amQuick, isActive, isPingEnabled, pingInterval,
pingCooldown, pingIp, isEthernetTunnel, isIpv4Preferred,
)
}
}
}
}
@@ -0,0 +1,13 @@
package com.zaneschepke.wireguardautotunnel.data.repository
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
interface AppDataRepository {
suspend fun getPrimaryOrFirstTunnel(): TunnelConfig?
suspend fun getStartTunnelConfig(): TunnelConfig?
val settings: SettingsRepository
val tunnels: TunnelConfigRepository
val appState: AppStateRepository
}
@@ -1,25 +1,20 @@
package com.zaneschepke.wireguardautotunnel.data.repository
import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository
import com.zaneschepke.wireguardautotunnel.domain.repository.AppStateRepository
import com.zaneschepke.wireguardautotunnel.domain.repository.AppSettingRepository
import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository
import com.zaneschepke.wireguardautotunnel.domain.entity.TunnelConf
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
import javax.inject.Inject
class AppDataRoomRepository
@Inject
constructor(
override val settings: AppSettingRepository,
override val tunnels: TunnelRepository,
override val settings: SettingsRepository,
override val tunnels: TunnelConfigRepository,
override val appState: AppStateRepository,
) : AppDataRepository {
override suspend fun getPrimaryOrFirstTunnel(): TunnelConf? {
override suspend fun getPrimaryOrFirstTunnel(): TunnelConfig? {
return tunnels.findPrimary().firstOrNull() ?: tunnels.getAll().firstOrNull()
}
override suspend fun getStartTunnelConfig(): TunnelConf? {
override suspend fun getStartTunnelConfig(): TunnelConfig? {
tunnels.getActive().let {
if (it.isNotEmpty()) return it.first()
return getPrimaryOrFirstTunnel()
@@ -1,6 +1,6 @@
package com.zaneschepke.wireguardautotunnel.domain.repository
package com.zaneschepke.wireguardautotunnel.data.repository
import com.zaneschepke.wireguardautotunnel.data.model.GeneralState
import com.zaneschepke.wireguardautotunnel.data.domain.GeneralState
import com.zaneschepke.wireguardautotunnel.ui.theme.Theme
import kotlinx.coroutines.flow.Flow
@@ -33,5 +33,5 @@ interface AppStateRepository {
suspend fun getLocale(): String?
val flow: Flow<GeneralState>
val generalStateFlow: Flow<GeneralState>
}
@@ -1,8 +1,7 @@
package com.zaneschepke.wireguardautotunnel.data.repository
import com.zaneschepke.wireguardautotunnel.data.DataStoreManager
import com.zaneschepke.wireguardautotunnel.domain.repository.AppStateRepository
import com.zaneschepke.wireguardautotunnel.data.model.GeneralState
import com.zaneschepke.wireguardautotunnel.data.datastore.DataStoreManager
import com.zaneschepke.wireguardautotunnel.data.domain.GeneralState
import com.zaneschepke.wireguardautotunnel.ui.theme.Theme
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
@@ -78,7 +77,7 @@ class DataStoreAppStateRepository(
return dataStoreManager.getFromStore(DataStoreManager.locale)
}
override val flow: Flow<GeneralState> =
override val generalStateFlow: Flow<GeneralState> =
dataStoreManager.preferencesFlow.map { prefs ->
prefs?.let { pref ->
try {
@@ -1,31 +1,30 @@
package com.zaneschepke.wireguardautotunnel.data.repository
import com.zaneschepke.wireguardautotunnel.data.dao.SettingsDao
import com.zaneschepke.wireguardautotunnel.domain.repository.AppSettingRepository
import com.zaneschepke.wireguardautotunnel.data.model.Settings
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
import com.zaneschepke.wireguardautotunnel.domain.entity.AppSettings
import com.zaneschepke.wireguardautotunnel.data.SettingsDao
import com.zaneschepke.wireguardautotunnel.data.domain.Settings
import com.zaneschepke.wireguardautotunnel.module.IoDispatcher
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.withContext
class RoomSettingsRepository(
private val settingsDoa: SettingsDao,
@IoDispatcher private val ioDispatcher: CoroutineDispatcher,
) : AppSettingRepository {
override suspend fun save(appSettings: AppSettings) {
class RoomSettingsRepository(private val settingsDoa: SettingsDao, @IoDispatcher private val ioDispatcher: CoroutineDispatcher) : SettingsRepository {
override suspend fun save(settings: Settings) {
withContext(ioDispatcher) {
settingsDoa.save(Settings.from(appSettings))
settingsDoa.save(settings)
}
}
override val flow = settingsDoa.getSettingsFlow().flowOn(ioDispatcher).map { it.toAppSettings() }
override fun getSettingsFlow(): Flow<Settings> {
return settingsDoa.getSettingsFlow()
}
override suspend fun get(): AppSettings {
override suspend fun getSettings(): Settings {
return withContext(ioDispatcher) {
(settingsDoa.getAll().firstOrNull() ?: Settings()).toAppSettings()
settingsDoa.getAll().firstOrNull() ?: Settings()
}
}
override suspend fun getAll(): List<Settings> {
return withContext(ioDispatcher) { settingsDoa.getAll() }
}
}
@@ -0,0 +1,104 @@
package com.zaneschepke.wireguardautotunnel.data.repository
import com.zaneschepke.wireguardautotunnel.data.TunnelConfigDao
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
import com.zaneschepke.wireguardautotunnel.module.IoDispatcher
import com.zaneschepke.wireguardautotunnel.util.extensions.TunnelConfigs
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.withContext
class RoomTunnelConfigRepository(
private val tunnelConfigDao: TunnelConfigDao,
@IoDispatcher private val ioDispatcher: CoroutineDispatcher,
) :
TunnelConfigRepository {
override fun getTunnelConfigsFlow(): Flow<TunnelConfigs> {
return tunnelConfigDao.getAllFlow()
}
override suspend fun getAll(): TunnelConfigs {
return withContext(ioDispatcher) { tunnelConfigDao.getAll() }
}
override suspend fun save(tunnelConfig: TunnelConfig) {
withContext(ioDispatcher) {
tunnelConfigDao.save(tunnelConfig)
}
}
override suspend fun updatePrimaryTunnel(tunnelConfig: TunnelConfig?) {
withContext(ioDispatcher) {
tunnelConfigDao.resetPrimaryTunnel()
tunnelConfig?.let {
save(
it.copy(
isPrimaryTunnel = true,
),
)
}
}
}
override suspend fun updateMobileDataTunnel(tunnelConfig: TunnelConfig?) {
withContext(ioDispatcher) {
tunnelConfigDao.resetMobileDataTunnel()
tunnelConfig?.let {
save(
it.copy(
isMobileDataTunnel = true,
),
)
}
}
}
override suspend fun updateEthernetTunnel(tunnelConfig: TunnelConfig?) {
withContext(ioDispatcher) {
tunnelConfigDao.resetEthernetTunnel()
tunnelConfig?.let {
save(
it.copy(
isEthernetTunnel = true,
),
)
}
}
}
override suspend fun delete(tunnelConfig: TunnelConfig) {
withContext(ioDispatcher) {
tunnelConfigDao.delete(tunnelConfig)
}
}
override suspend fun getById(id: Int): TunnelConfig? {
return withContext(ioDispatcher) { tunnelConfigDao.getById(id.toLong()) }
}
override suspend fun getActive(): TunnelConfigs {
return withContext(ioDispatcher) {
tunnelConfigDao.getActive()
}
}
override suspend fun count(): Int {
return withContext(ioDispatcher) { tunnelConfigDao.count().toInt() }
}
override suspend fun findByTunnelName(name: String): TunnelConfig? {
return withContext(ioDispatcher) { tunnelConfigDao.getByName(name) }
}
override suspend fun findByTunnelNetworksName(name: String): TunnelConfigs {
return withContext(ioDispatcher) { tunnelConfigDao.findByTunnelNetworkName(name) }
}
override suspend fun findByMobileDataTunnel(): TunnelConfigs {
return withContext(ioDispatcher) { tunnelConfigDao.findByMobileDataTunnel() }
}
override suspend fun findPrimary(): TunnelConfigs {
return withContext(ioDispatcher) { tunnelConfigDao.findByPrimary() }
}
}
@@ -1,107 +0,0 @@
package com.zaneschepke.wireguardautotunnel.data.repository
import com.zaneschepke.wireguardautotunnel.data.dao.TunnelConfigDao
import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
import com.zaneschepke.wireguardautotunnel.domain.entity.TunnelConf
import com.zaneschepke.wireguardautotunnel.util.extensions.Tunnels
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.withContext
class RoomTunnelRepository(
private val tunnelConfigDao: TunnelConfigDao,
@IoDispatcher private val ioDispatcher: CoroutineDispatcher,
) : TunnelRepository {
override val flow = tunnelConfigDao.getAllFlow().flowOn(ioDispatcher).map { it.map { it.toTunnel() } }
override suspend fun getAll(): Tunnels {
return withContext(ioDispatcher) {
tunnelConfigDao.getAll().map { it.toTunnel() }
}
}
override suspend fun save(tunnelConf: TunnelConf) {
withContext(ioDispatcher) {
tunnelConfigDao.save(TunnelConfig.from(tunnelConf))
}
}
override suspend fun updatePrimaryTunnel(tunnelConf: TunnelConf?) {
withContext(ioDispatcher) {
tunnelConfigDao.resetPrimaryTunnel()
tunnelConf?.let {
save(
it.copy(
isPrimaryTunnel = true,
),
)
}
}
}
override suspend fun updateMobileDataTunnel(tunnelConf: TunnelConf?) {
withContext(ioDispatcher) {
tunnelConfigDao.resetMobileDataTunnel()
tunnelConf?.let {
save(
it.copy(
isMobileDataTunnel = true,
),
)
}
}
}
override suspend fun updateEthernetTunnel(tunnelConf: TunnelConf?) {
withContext(ioDispatcher) {
tunnelConfigDao.resetEthernetTunnel()
tunnelConf?.let {
save(
it.copy(
isEthernetTunnel = true,
),
)
}
}
}
override suspend fun delete(tunnelConf: TunnelConf) {
withContext(ioDispatcher) {
tunnelConfigDao.delete(TunnelConfig.from(tunnelConf))
}
}
override suspend fun getById(id: Int): TunnelConf? {
return withContext(ioDispatcher) { tunnelConfigDao.getById(id.toLong())?.toTunnel() }
}
override suspend fun getActive(): Tunnels {
return withContext(ioDispatcher) {
tunnelConfigDao.getActive().map { it.toTunnel() }
}
}
override suspend fun count(): Int {
return withContext(ioDispatcher) { tunnelConfigDao.count().toInt() }
}
override suspend fun findByTunnelName(name: String): TunnelConf? {
return withContext(ioDispatcher) { tunnelConfigDao.getByName(name)?.toTunnel() }
}
override suspend fun findByTunnelNetworksName(name: String): Tunnels {
return withContext(ioDispatcher) { tunnelConfigDao.findByTunnelNetworkName(name).map { it.toTunnel() } }
}
override suspend fun findByMobileDataTunnel(): Tunnels {
return withContext(ioDispatcher) { tunnelConfigDao.findByMobileDataTunnel().map { it.toTunnel() } }
}
override suspend fun findPrimary(): Tunnels {
return withContext(ioDispatcher) { tunnelConfigDao.findByPrimary().map { it.toTunnel() } }
}
}
@@ -0,0 +1,14 @@
package com.zaneschepke.wireguardautotunnel.data.repository
import com.zaneschepke.wireguardautotunnel.data.domain.Settings
import kotlinx.coroutines.flow.Flow
interface SettingsRepository {
suspend fun save(settings: Settings)
fun getSettingsFlow(): Flow<Settings>
suspend fun getSettings(): Settings
suspend fun getAll(): List<Settings>
}
@@ -0,0 +1,35 @@
package com.zaneschepke.wireguardautotunnel.data.repository
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
import com.zaneschepke.wireguardautotunnel.util.extensions.TunnelConfigs
import kotlinx.coroutines.flow.Flow
interface TunnelConfigRepository {
fun getTunnelConfigsFlow(): Flow<TunnelConfigs>
suspend fun getAll(): TunnelConfigs
suspend fun save(tunnelConfig: TunnelConfig)
suspend fun updatePrimaryTunnel(tunnelConfig: TunnelConfig?)
suspend fun updateMobileDataTunnel(tunnelConfig: TunnelConfig?)
suspend fun updateEthernetTunnel(tunnelConfig: TunnelConfig?)
suspend fun delete(tunnelConfig: TunnelConfig)
suspend fun getById(id: Int): TunnelConfig?
suspend fun getActive(): TunnelConfigs
suspend fun count(): Int
suspend fun findByTunnelName(name: String): TunnelConfig?
suspend fun findByTunnelNetworksName(name: String): TunnelConfigs
suspend fun findByMobileDataTunnel(): TunnelConfigs
suspend fun findPrimary(): TunnelConfigs
}
@@ -1,46 +0,0 @@
package com.zaneschepke.wireguardautotunnel.di
import android.content.Context
import com.zaneschepke.logcatter.LogReader
import com.zaneschepke.logcatter.LogcatReader
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationManager
import com.zaneschepke.wireguardautotunnel.core.notification.WireGuardNotification
import com.zaneschepke.wireguardautotunnel.core.shortcut.DynamicShortcutManager
import com.zaneschepke.wireguardautotunnel.core.shortcut.ShortcutManager
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): LogReader {
return LogcatReader.init(storageDir = context.filesDir.absolutePath)
}
@Singleton
@Provides
fun provideNotificationService(@ApplicationContext context: Context): NotificationManager {
return WireGuardNotification(context)
}
@Singleton
@Provides
fun provideShortcutManager(@ApplicationContext context: Context, @IoDispatcher ioDispatcher: CoroutineDispatcher): ShortcutManager {
return DynamicShortcutManager(context, ioDispatcher)
}
}
@@ -1,16 +0,0 @@
package com.zaneschepke.wireguardautotunnel.di
import com.zaneschepke.wireguardautotunnel.core.network.InternetConnectivityMonitor
import com.zaneschepke.wireguardautotunnel.core.network.NetworkMonitor
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
@Module
@InstallIn(SingletonComponent::class)
abstract class ServiceModule {
@Binds
abstract fun provideInternetConnectivityService(wifiService: InternetConnectivityMonitor): NetworkMonitor
}
@@ -1,110 +0,0 @@
package com.zaneschepke.wireguardautotunnel.di
import android.content.Context
import com.wireguard.android.backend.WgQuickBackend
import com.wireguard.android.util.RootShell
import com.wireguard.android.util.ToolsInstaller
import com.zaneschepke.wireguardautotunnel.core.network.NetworkMonitor
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationManager
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
import com.zaneschepke.wireguardautotunnel.core.tunnel.KernelTunnel
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelProvider
import com.zaneschepke.wireguardautotunnel.core.tunnel.UserspaceTunnel
import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository
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 org.amnezia.awg.backend.Backend
import org.amnezia.awg.backend.GoBackend
import org.amnezia.awg.backend.RootTunnelActionHandler
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
class TunnelModule {
@Provides
@Singleton
@TunnelShell
fun provideTunnelRootShell(@ApplicationContext context: Context): RootShell {
return RootShell(context)
}
@Provides
@Singleton
@AppShell
fun provideAppRootShell(@ApplicationContext context: Context): RootShell {
return RootShell(context)
}
@Provides
@Singleton
fun provideAmneziaBackend(@ApplicationContext context: Context): Backend {
return GoBackend(context, RootTunnelActionHandler(org.amnezia.awg.util.RootShell(context)))
}
@Provides
@Singleton
fun provideKernelBackend(@ApplicationContext context: Context, @TunnelShell shell: RootShell): com.wireguard.android.backend.Backend {
return WgQuickBackend(context, shell, ToolsInstaller(context, shell), com.wireguard.android.backend.RootTunnelActionHandler(shell)).also {
it.setMultipleTunnels(true)
}
}
@Provides
@Singleton
@Kernel
fun provideKernelProvider(
@IoDispatcher ioDispatcher: CoroutineDispatcher,
@ApplicationScope applicationScope: CoroutineScope,
serviceManager: ServiceManager,
appDataRepository: AppDataRepository,
networkMonitor: NetworkMonitor,
notificationManager: NotificationManager,
backend: com.wireguard.android.backend.Backend,
): TunnelProvider {
return KernelTunnel(ioDispatcher, applicationScope, serviceManager, appDataRepository, notificationManager, backend, networkMonitor)
}
@Provides
@Singleton
@Userspace
fun provideUserspaceProvider(
@IoDispatcher ioDispatcher: CoroutineDispatcher,
@ApplicationScope applicationScope: CoroutineScope,
serviceManager: ServiceManager,
appDataRepository: AppDataRepository,
notificationManager: NotificationManager,
networkMonitor: NetworkMonitor,
backend: Backend,
): TunnelProvider {
return UserspaceTunnel(ioDispatcher, applicationScope, serviceManager, appDataRepository, notificationManager, backend, networkMonitor)
}
@Provides
@Singleton
fun provideTunnelManager(
@Kernel kernelTunnel: TunnelProvider,
@Userspace userspaceTunnel: TunnelProvider,
appDataRepository: AppDataRepository,
@IoDispatcher ioDispatcher: CoroutineDispatcher,
@ApplicationScope applicationScope: CoroutineScope,
): TunnelManager {
return TunnelManager(kernelTunnel, userspaceTunnel, appDataRepository, applicationScope, ioDispatcher)
}
@Singleton
@Provides
fun provideServiceManager(
@ApplicationContext context: Context,
@IoDispatcher ioDispatcher: CoroutineDispatcher,
appDataRepository: AppDataRepository,
): ServiceManager {
return ServiceManager(context, ioDispatcher, appDataRepository)
}
}
@@ -1,29 +0,0 @@
package com.zaneschepke.wireguardautotunnel.domain.entity
data class AppSettings(
val id: Int = 0,
val isAutoTunnelEnabled: Boolean = false,
val isTunnelOnMobileDataEnabled: Boolean = false,
val trustedNetworkSSIDs: List<String> = emptyList(),
val isAlwaysOnVpnEnabled: Boolean = false,
val isTunnelOnEthernetEnabled: Boolean = false,
val isShortcutsEnabled: Boolean = false,
val isTunnelOnWifiEnabled: Boolean = false,
val isKernelEnabled: Boolean = false,
val isRestoreOnBootEnabled: Boolean = false,
val isMultiTunnelEnabled: Boolean = false,
val isPingEnabled: Boolean = false,
val isAmneziaEnabled: Boolean = false,
val isWildcardsEnabled: Boolean = false,
val isWifiNameByShellEnabled: Boolean = false,
val isStopOnNoInternetEnabled: Boolean = false,
val isVpnKillSwitchEnabled: Boolean = false,
val isKernelKillSwitchEnabled: Boolean = false,
val isLanOnKillSwitchEnabled: Boolean = false,
val debounceDelaySeconds: Int = 3,
val isDisableKillSwitchOnTrustedEnabled: Boolean = false,
) {
fun debounceDelayMillis(): Long {
return debounceDelaySeconds * 1000L
}
}
@@ -1,13 +0,0 @@
package com.zaneschepke.wireguardautotunnel.domain.entity
import com.zaneschepke.wireguardautotunnel.ui.theme.Theme
data class AppState(
val isLocationDisclosureShown: Boolean,
val isBatteryOptimizationDisableShown: Boolean,
val isPinLockEnabled: Boolean,
val isTunnelStatsExpanded: Boolean,
val isLocalLogsEnabled: Boolean,
val locale: String?,
val theme: Theme,
)
@@ -1,126 +0,0 @@
package com.zaneschepke.wireguardautotunnel.domain.entity
import com.wireguard.config.Config
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelState
import com.zaneschepke.wireguardautotunnel.util.Constants
import com.zaneschepke.wireguardautotunnel.util.extensions.asTunnelState
import com.zaneschepke.wireguardautotunnel.util.extensions.isReachable
import com.zaneschepke.wireguardautotunnel.util.extensions.toWgQuickString
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.withContext
import org.amnezia.awg.backend.Tunnel
import timber.log.Timber
import java.io.InputStream
import java.net.InetAddress
import kotlin.coroutines.CoroutineContext
data class TunnelConf(
val id: Int = 0,
val tunName: String,
val wgQuick: String,
val tunnelNetworks: List<String> = emptyList(),
val isMobileDataTunnel: Boolean = false,
val isPrimaryTunnel: Boolean = false,
val amQuick: String,
val isActive: Boolean = false,
val isPingEnabled: Boolean = false,
val pingInterval: Long? = null,
val pingCooldown: Long? = null,
val pingIp: String? = null,
val isEthernetTunnel: Boolean = false,
val isIpv4Preferred: Boolean = false,
) : Tunnel, com.wireguard.android.backend.Tunnel {
val state = MutableStateFlow(TunnelState())
fun toAmConfig(): org.amnezia.awg.config.Config {
return configFromAmQuick(amQuick.ifBlank { wgQuick })
}
fun toWgConfig(): Config {
return configFromWgQuick(wgQuick)
}
override fun getName(): String {
return tunName
}
override fun isIpv4ResolutionPreferred(): Boolean {
return isIpv4Preferred
}
override fun onStateChange(newState: Tunnel.State) {
state.update {
it.copy(state = newState.asTunnelState())
}
}
override fun onStateChange(newState: com.wireguard.android.backend.Tunnel.State) {
state.update {
it.copy(state = newState.asTunnelState())
}
}
fun isQuickConfigMatching(updatedConf: TunnelConf): Boolean {
return updatedConf.wgQuick == wgQuick ||
updatedConf.amQuick == amQuick
}
fun isPingConfigMatching(updatedConf: TunnelConf): Boolean {
return updatedConf.isPingEnabled == isPingEnabled &&
pingIp == updatedConf.pingIp &&
updatedConf.pingCooldown == pingCooldown &&
updatedConf.pingInterval == pingInterval
}
suspend fun pingTunnel(context: CoroutineContext): List<Boolean> {
return withContext(context) {
val config = toWgConfig()
if (pingIp != null) {
Timber.i("Pinging custom ip")
listOf(InetAddress.getByName(pingIp).isReachable(Constants.PING_TIMEOUT.toInt()))
} else {
Timber.i("Pinging all peers")
config.peers.map { peer ->
peer.isReachable(isIpv4Preferred)
}
}
}
}
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)
}
}
fun tunnelConfigFromAmConfig(config: org.amnezia.awg.config.Config, name: String): TunnelConf {
val amQuick = config.toAwgQuickString(true)
val wgQuick = config.toWgQuickString()
return TunnelConf(tunName = name, wgQuick = wgQuick, amQuick = amQuick)
}
private const val IPV6_ALL_NETWORKS = "::/0"
private const val IPV4_ALL_NETWORKS = "0.0.0.0/0"
val ALL_IPS = listOf(IPV4_ALL_NETWORKS, IPV6_ALL_NETWORKS)
private val IPV4_PUBLIC_NETWORKS = listOf(
"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 LAN_BYPASS_ALLOWED_IPS = listOf(IPV6_ALL_NETWORKS) + IPV4_PUBLIC_NETWORKS
}
}
@@ -1,7 +0,0 @@
package com.zaneschepke.wireguardautotunnel.domain.enums
sealed class BackendError() {
data object DNS : BackendError()
data object Unauthorized : BackendError()
data object Config : BackendError()
}
@@ -1,6 +0,0 @@
package com.zaneschepke.wireguardautotunnel.domain.enums
enum class ConfigType {
AMNEZIA,
WG,
}
@@ -1,17 +0,0 @@
package com.zaneschepke.wireguardautotunnel.domain.enums
import android.content.Context
import com.zaneschepke.wireguardautotunnel.R
enum class NotificationAction {
TUNNEL_OFF,
AUTO_TUNNEL_OFF,
;
fun title(context: Context): String {
return when (this) {
TUNNEL_OFF -> context.getString(R.string.stop)
AUTO_TUNNEL_OFF -> context.getString(R.string.stop)
}
}
}
@@ -1,15 +0,0 @@
package com.zaneschepke.wireguardautotunnel.domain.enums
enum class TunnelStatus {
UP,
DOWN,
;
fun isDown(): Boolean {
return this == DOWN
}
fun isUp(): Boolean {
return this == UP
}
}
@@ -1,9 +0,0 @@
package com.zaneschepke.wireguardautotunnel.domain.events
import com.zaneschepke.wireguardautotunnel.domain.entity.TunnelConf
sealed class AutoTunnelEvent {
data class Start(val tunnelConf: TunnelConf? = null) : AutoTunnelEvent()
data object Stop : AutoTunnelEvent()
data object DoNothing : AutoTunnelEvent()
}
@@ -1,7 +0,0 @@
package com.zaneschepke.wireguardautotunnel.domain.events
sealed class KillSwitchEvent {
data class Start(val allowedIps: List<String>) : KillSwitchEvent()
data object Stop : KillSwitchEvent()
data object DoNothing : KillSwitchEvent()
}
@@ -1,13 +0,0 @@
package com.zaneschepke.wireguardautotunnel.domain.repository
import com.zaneschepke.wireguardautotunnel.domain.entity.TunnelConf
interface AppDataRepository {
suspend fun getPrimaryOrFirstTunnel(): TunnelConf?
suspend fun getStartTunnelConfig(): TunnelConf?
val settings: AppSettingRepository
val tunnels: TunnelRepository
val appState: AppStateRepository
}
@@ -1,10 +0,0 @@
package com.zaneschepke.wireguardautotunnel.domain.repository
import com.zaneschepke.wireguardautotunnel.domain.entity.AppSettings
import kotlinx.coroutines.flow.Flow
interface AppSettingRepository {
suspend fun save(appSettings: AppSettings)
val flow: Flow<AppSettings>
suspend fun get(): AppSettings
}
@@ -1,35 +0,0 @@
package com.zaneschepke.wireguardautotunnel.domain.repository
import com.zaneschepke.wireguardautotunnel.domain.entity.TunnelConf
import com.zaneschepke.wireguardautotunnel.util.extensions.Tunnels
import kotlinx.coroutines.flow.Flow
interface TunnelRepository {
val flow: Flow<List<TunnelConf>>
suspend fun getAll(): Tunnels
suspend fun save(tunnelConf: TunnelConf)
suspend fun updatePrimaryTunnel(tunnelConf: TunnelConf?)
suspend fun updateMobileDataTunnel(tunnelConf: TunnelConf?)
suspend fun updateEthernetTunnel(tunnelConf: TunnelConf?)
suspend fun delete(tunnelConf: TunnelConf)
suspend fun getById(id: Int): TunnelConf?
suspend fun getActive(): Tunnels
suspend fun count(): Int
suspend fun findByTunnelName(name: String): TunnelConf?
suspend fun findByTunnelNetworksName(name: String): Tunnels
suspend fun findByMobileDataTunnel(): Tunnels
suspend fun findPrimary(): Tunnels
}
@@ -1,9 +0,0 @@
package com.zaneschepke.wireguardautotunnel.domain.state
data class ConnectivityState(
val wifiAvailable: Boolean,
val ethernetAvailable: Boolean,
val cellularAvailable: Boolean,
) {
val allOffline = !wifiAvailable && !ethernetAvailable && !cellularAvailable
}
@@ -1,10 +0,0 @@
package com.zaneschepke.wireguardautotunnel.domain.state
import com.zaneschepke.wireguardautotunnel.domain.enums.BackendState
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelStatus
data class TunnelState(
val state: TunnelStatus = TunnelStatus.DOWN,
val backendState: BackendState = BackendState.INACTIVE,
val statistics: TunnelStatistics? = null,
)
@@ -0,0 +1,30 @@
package com.zaneschepke.wireguardautotunnel.module
import android.content.Context
import com.zaneschepke.logcatter.LogReader
import com.zaneschepke.logcatter.LogcatCollector
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): LogReader {
return LogcatCollector.init(context = context)
}
}
@@ -1,15 +1,7 @@
package com.zaneschepke.wireguardautotunnel.di
package com.zaneschepke.wireguardautotunnel.module
import javax.inject.Qualifier
@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class TunnelShell
@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class AppShell
@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class Kernel
@@ -17,3 +9,11 @@ annotation class Kernel
@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class Userspace
@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class TunnelShell
@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class AppShell
@@ -1,4 +1,4 @@
package com.zaneschepke.wireguardautotunnel.di
package com.zaneschepke.wireguardautotunnel.module
import javax.inject.Qualifier
@@ -1,4 +1,4 @@
package com.zaneschepke.wireguardautotunnel.di
package com.zaneschepke.wireguardautotunnel.module
import dagger.Module
import dagger.Provides
@@ -1,21 +1,21 @@
package com.zaneschepke.wireguardautotunnel.di
package com.zaneschepke.wireguardautotunnel.module
import android.content.Context
import androidx.room.Room
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.data.AppDatabase
import com.zaneschepke.wireguardautotunnel.data.DatabaseCallback
import com.zaneschepke.wireguardautotunnel.data.dao.SettingsDao
import com.zaneschepke.wireguardautotunnel.data.dao.TunnelConfigDao
import com.zaneschepke.wireguardautotunnel.data.DataStoreManager
import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository
import com.zaneschepke.wireguardautotunnel.data.SettingsDao
import com.zaneschepke.wireguardautotunnel.data.TunnelConfigDao
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.domain.repository.AppStateRepository
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.RoomTunnelRepository
import com.zaneschepke.wireguardautotunnel.domain.repository.AppSettingRepository
import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository
import com.zaneschepke.wireguardautotunnel.data.repository.RoomTunnelConfigRepository
import com.zaneschepke.wireguardautotunnel.data.repository.SettingsRepository
import com.zaneschepke.wireguardautotunnel.data.repository.TunnelConfigRepository
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
@@ -54,13 +54,13 @@ class RepositoryModule {
@Singleton
@Provides
fun provideTunnelConfigRepository(tunnelConfigDao: TunnelConfigDao, @IoDispatcher ioDispatcher: CoroutineDispatcher): TunnelRepository {
return RoomTunnelRepository(tunnelConfigDao, ioDispatcher)
fun provideTunnelConfigRepository(tunnelConfigDao: TunnelConfigDao, @IoDispatcher ioDispatcher: CoroutineDispatcher): TunnelConfigRepository {
return RoomTunnelConfigRepository(tunnelConfigDao, ioDispatcher)
}
@Singleton
@Provides
fun provideSettingsRepository(settingsDao: SettingsDao, @IoDispatcher ioDispatcher: CoroutineDispatcher): AppSettingRepository {
fun provideSettingsRepository(settingsDao: SettingsDao, @IoDispatcher ioDispatcher: CoroutineDispatcher): SettingsRepository {
return RoomSettingsRepository(settingsDao, ioDispatcher)
}
@@ -79,10 +79,10 @@ class RepositoryModule {
@Provides
@Singleton
fun provideAppDataRepository(
settingsRepository: AppSettingRepository,
tunnelRepository: TunnelRepository,
settingsRepository: SettingsRepository,
tunnelConfigRepository: TunnelConfigRepository,
appStateRepository: AppStateRepository,
): AppDataRepository {
return AppDataRoomRepository(settingsRepository, tunnelRepository, appStateRepository)
return AppDataRoomRepository(settingsRepository, tunnelConfigRepository, appStateRepository)
}
}
@@ -0,0 +1,33 @@
package com.zaneschepke.wireguardautotunnel.module
import com.zaneschepke.wireguardautotunnel.service.network.EthernetService
import com.zaneschepke.wireguardautotunnel.service.network.MobileDataService
import com.zaneschepke.wireguardautotunnel.service.network.NetworkService
import com.zaneschepke.wireguardautotunnel.service.network.WifiService
import com.zaneschepke.wireguardautotunnel.service.notification.NotificationService
import com.zaneschepke.wireguardautotunnel.service.notification.WireGuardNotification
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.android.components.ServiceComponent
import dagger.hilt.android.scopes.ServiceScoped
@Module
@InstallIn(ServiceComponent::class)
abstract class ServiceModule {
@Binds
@ServiceScoped
abstract fun provideNotificationService(wireGuardNotification: WireGuardNotification): NotificationService
@Binds
@ServiceScoped
abstract fun provideWifiService(wifiService: WifiService): NetworkService<WifiService>
@Binds
@ServiceScoped
abstract fun provideMobileDataService(mobileDataService: MobileDataService): NetworkService<MobileDataService>
@Binds
@ServiceScoped
abstract fun provideEthernetService(ethernetService: EthernetService): NetworkService<EthernetService>
}
@@ -0,0 +1,96 @@
package com.zaneschepke.wireguardautotunnel.module
import android.content.Context
import com.wireguard.android.backend.Backend
import com.wireguard.android.backend.GoBackend
import com.wireguard.android.backend.RootTunnelActionHandler
import com.wireguard.android.backend.WgQuickBackend
import com.wireguard.android.util.RootShell
import com.wireguard.android.util.ToolsInstaller
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
import com.zaneschepke.wireguardautotunnel.data.repository.TunnelConfigRepository
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelService
import com.zaneschepke.wireguardautotunnel.service.tunnel.WireGuardTunnel
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 javax.inject.Provider
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
class TunnelModule {
@Provides
@Singleton
@TunnelShell
fun provideTunnelRootShell(@ApplicationContext context: Context): RootShell {
return RootShell(context)
}
@Provides
@Singleton
@AppShell
fun provideAppRootShell(@ApplicationContext context: Context): RootShell {
return RootShell(context)
}
@Provides
@Singleton
fun provideRootShellAm(@ApplicationContext context: Context): org.amnezia.awg.util.RootShell {
return org.amnezia.awg.util.RootShell(context)
}
@Provides
@Singleton
@Userspace
fun provideUserspaceBackend(@ApplicationContext context: Context, @TunnelShell rootShell: RootShell): Backend {
return GoBackend(context, RootTunnelActionHandler(rootShell))
}
@Provides
@Singleton
@Kernel
fun provideKernelBackend(@ApplicationContext context: Context, @TunnelShell rootShell: RootShell): Backend {
return WgQuickBackend(context, rootShell, ToolsInstaller(context, rootShell), RootTunnelActionHandler(rootShell))
}
@Provides
@Singleton
fun provideAmneziaBackend(@ApplicationContext context: Context, rootShell: org.amnezia.awg.util.RootShell): org.amnezia.awg.backend.Backend {
return org.amnezia.awg.backend.GoBackend(context, org.amnezia.awg.backend.RootTunnelActionHandler(rootShell))
}
@Provides
@Singleton
fun provideVpnService(
amneziaBackend: Provider<org.amnezia.awg.backend.Backend>,
@Kernel kernelBackend: Provider<Backend>,
appDataRepository: AppDataRepository,
tunnelConfigRepository: TunnelConfigRepository,
@ApplicationScope applicationScope: CoroutineScope,
@IoDispatcher ioDispatcher: CoroutineDispatcher,
serviceManager: ServiceManager,
): TunnelService {
return WireGuardTunnel(
amneziaBackend,
tunnelConfigRepository,
kernelBackend,
appDataRepository,
applicationScope,
ioDispatcher,
serviceManager,
)
}
@Singleton
@Provides
fun provideServiceManager(@ApplicationContext context: Context): ServiceManager {
return ServiceManager.getInstance(context)
}
}
@@ -1,4 +1,4 @@
package com.zaneschepke.wireguardautotunnel.di
package com.zaneschepke.wireguardautotunnel.module
import android.content.Context
import com.zaneschepke.wireguardautotunnel.util.FileUtils
@@ -0,0 +1,47 @@
package com.zaneschepke.wireguardautotunnel.receiver
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
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.tunnel.TunnelService
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject
import javax.inject.Provider
@AndroidEntryPoint
class AppUpdateReceiver : BroadcastReceiver() {
@Inject
@ApplicationScope
lateinit var applicationScope: CoroutineScope
@Inject
lateinit var appDataRepository: AppDataRepository
@Inject
lateinit var tunnelService: Provider<TunnelService>
@Inject
lateinit var serviceManager: ServiceManager
override fun onReceive(context: Context, intent: Intent) {
if (intent.action != Intent.ACTION_MY_PACKAGE_REPLACED) return
applicationScope.launch {
val settings = appDataRepository.settings.getSettings()
if (settings.isAutoTunnelEnabled) {
Timber.i("Restarting services after upgrade")
serviceManager.startAutoTunnel(true)
}
if (!settings.isAutoTunnelEnabled) {
val tunnels = appDataRepository.tunnels.getAll().filter { it.isActive }
if (tunnels.isNotEmpty()) tunnelService.get().startTunnel(tunnels.first(), true)
}
}
}
}
@@ -0,0 +1,52 @@
package com.zaneschepke.wireguardautotunnel.receiver
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
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.tunnel.TunnelService
import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelState
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject
import javax.inject.Provider
@AndroidEntryPoint
class BootReceiver : BroadcastReceiver() {
@Inject
lateinit var appDataRepository: AppDataRepository
@Inject
lateinit var tunnelService: Provider<TunnelService>
@Inject
@ApplicationScope
lateinit var applicationScope: CoroutineScope
@Inject
lateinit var serviceManager: ServiceManager
override fun onReceive(context: Context, intent: Intent) {
if (Intent.ACTION_BOOT_COMPLETED != intent.action) return
applicationScope.launch {
with(appDataRepository.settings.getSettings()) {
if (isRestoreOnBootEnabled) {
val activeTunnels = appDataRepository.tunnels.getActive()
val tunState = tunnelService.get().vpnState.value.status
if (activeTunnels.isNotEmpty() && tunState != TunnelState.UP) {
Timber.i("Starting previously active tunnel")
tunnelService.get().startTunnel(activeTunnels.first(), true)
}
if (isAutoTunnelEnabled) {
Timber.i("Starting watcher service from boot")
serviceManager.startAutoTunnel(true)
}
}
}
}
}
}
@@ -0,0 +1,48 @@
package com.zaneschepke.wireguardautotunnel.receiver
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import com.zaneschepke.wireguardautotunnel.data.repository.TunnelConfigRepository
import com.zaneschepke.wireguardautotunnel.module.ApplicationScope
import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelService
import com.zaneschepke.wireguardautotunnel.util.extensions.requestTunnelTileServiceStateUpdate
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import javax.inject.Inject
import javax.inject.Provider
@AndroidEntryPoint
class KernelReceiver : BroadcastReceiver() {
@Inject
lateinit var tunnelService: Provider<TunnelService>
@Inject
@ApplicationScope
lateinit var applicationScope: CoroutineScope
@Inject
lateinit var tunnelConfigRepository: TunnelConfigRepository
override fun onReceive(context: Context, intent: Intent) {
val action = intent.action ?: return
applicationScope.launch {
if (action == REFRESH_TUNNELS_ACTION) {
tunnelService.get().runningTunnelNames().forEach { name ->
// TODO can optimize later
val tunnel = tunnelConfigRepository.findByTunnelName(name)
tunnel?.let {
tunnelConfigRepository.save(it.copy(isActive = true))
}
}
context.requestTunnelTileServiceStateUpdate()
}
}
}
companion object {
const val REFRESH_TUNNELS_ACTION = "com.wireguard.android.action.REFRESH_TUNNEL_STATES"
}
}
@@ -0,0 +1,86 @@
package com.zaneschepke.wireguardautotunnel.service.foreground
import android.app.Service
import android.content.Context
import android.content.Intent
import com.zaneschepke.wireguardautotunnel.service.foreground.autotunnel.AutoTunnelService
import com.zaneschepke.wireguardautotunnel.util.SingletonHolder
import com.zaneschepke.wireguardautotunnel.util.extensions.requestTunnelTileServiceStateUpdate
import jakarta.inject.Inject
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import timber.log.Timber
@OptIn(ExperimentalCoroutinesApi::class)
class ServiceManager
@Inject constructor(private val context: Context) {
private val _autoTunnelActive = MutableStateFlow(false)
val autoTunnelActive = _autoTunnelActive.asStateFlow()
var autoTunnelService = CompletableDeferred<AutoTunnelService>()
var backgroundService = CompletableDeferred<TunnelBackgroundService>()
companion object : SingletonHolder<ServiceManager, Context>(::ServiceManager)
private fun <T : Service> startService(cls: Class<T>, background: Boolean) {
runCatching {
val intent = Intent(context, cls)
if (background) {
context.startForegroundService(intent)
} else {
context.startService(intent)
}
}.onFailure { Timber.e(it) }
}
suspend fun startAutoTunnel(background: Boolean) {
if (autoTunnelService.isCompleted) return _autoTunnelActive.update { true }
kotlin.runCatching {
startService(AutoTunnelService::class.java, background)
autoTunnelService.await()
autoTunnelService.getCompleted().start()
_autoTunnelActive.update { true }
}.onFailure {
Timber.e(it)
}
}
suspend fun startBackgroundService() {
if (backgroundService.isCompleted) return
kotlin.runCatching {
startService(TunnelBackgroundService::class.java, true)
backgroundService.await()
backgroundService.getCompleted().start()
}.onFailure {
Timber.e(it)
}
}
fun stopBackgroundService() {
if (!backgroundService.isCompleted) return
runCatching {
backgroundService.getCompleted().stop()
}.onFailure {
Timber.e(it)
}
}
fun stopAutoTunnel() {
if (!autoTunnelService.isCompleted) return
runCatching {
autoTunnelService.getCompleted().stop()
_autoTunnelActive.update { false }
}.onFailure {
Timber.e(it)
}
}
fun requestTunnelTileUpdate() {
context.requestTunnelTileServiceStateUpdate()
}
}
@@ -1,4 +1,4 @@
package com.zaneschepke.wireguardautotunnel.core.service
package com.zaneschepke.wireguardautotunnel.service.foreground
import android.app.Notification
import android.content.Intent
@@ -6,31 +6,31 @@ import android.os.IBinder
import androidx.core.app.ServiceCompat
import androidx.lifecycle.LifecycleService
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.domain.enums.NotificationAction
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationManager
import com.zaneschepke.wireguardautotunnel.core.notification.WireGuardNotification
import com.zaneschepke.wireguardautotunnel.domain.entity.TunnelConf
import com.zaneschepke.wireguardautotunnel.service.notification.NotificationService
import com.zaneschepke.wireguardautotunnel.util.Constants
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.CompletableDeferred
import javax.inject.Inject
@AndroidEntryPoint
class TunnelForegroundService : LifecycleService() {
class TunnelBackgroundService : LifecycleService() {
@Inject
lateinit var notificationManager: NotificationManager
lateinit var notificationService: NotificationService
@Inject
lateinit var serviceManager: ServiceManager
private val foregroundId = 123
override fun onCreate() {
super.onCreate()
serviceManager.backgroundService.complete(this)
start()
}
override fun onBind(intent: Intent): IBinder? {
super.onBind(intent)
// We don't provide binding, so return null
return null
}
@@ -39,11 +39,11 @@ class TunnelForegroundService : LifecycleService() {
return super.onStartCommand(intent, flags, startId)
}
fun start(tunnelConf: TunnelConf) {
fun start() {
ServiceCompat.startForeground(
this@TunnelForegroundService,
NotificationManager.KERNEL_SERVICE_NOTIFICATION_ID,
createNotification(tunnelConf),
this,
foregroundId,
createNotification(),
Constants.SYSTEM_EXEMPT_SERVICE_TYPE_ID,
)
}
@@ -58,13 +58,12 @@ class TunnelForegroundService : LifecycleService() {
super.onDestroy()
}
private fun createNotification(tunnelConf: TunnelConf): Notification {
return notificationManager.createNotification(
WireGuardNotification.NotificationChannels.VPN,
title = "${getString(R.string.tunnel_running)} - ${tunnelConf.tunName}",
actions = listOf(
notificationManager.createNotificationAction(NotificationAction.TUNNEL_OFF, tunnelConf.id),
),
private fun createNotification(): Notification {
return notificationService.createNotification(
getString(R.string.vpn_channel_id),
getString(R.string.vpn_channel_name),
getString(R.string.tunnel_running),
description = "",
)
}
}
@@ -0,0 +1,307 @@
package com.zaneschepke.wireguardautotunnel.service.foreground.autotunnel
import android.content.Intent
import android.net.NetworkCapabilities
import android.os.IBinder
import android.os.PowerManager
import androidx.core.app.ServiceCompat
import androidx.lifecycle.LifecycleService
import androidx.lifecycle.lifecycleScope
import com.wireguard.android.util.RootShell
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.data.domain.Settings
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
import com.zaneschepke.wireguardautotunnel.module.AppShell
import com.zaneschepke.wireguardautotunnel.module.IoDispatcher
import com.zaneschepke.wireguardautotunnel.module.MainImmediateDispatcher
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
import com.zaneschepke.wireguardautotunnel.service.foreground.autotunnel.model.AutoTunnelEvent
import com.zaneschepke.wireguardautotunnel.service.foreground.autotunnel.model.AutoTunnelState
import com.zaneschepke.wireguardautotunnel.service.foreground.autotunnel.model.NetworkState
import com.zaneschepke.wireguardautotunnel.service.network.EthernetService
import com.zaneschepke.wireguardautotunnel.service.network.MobileDataService
import com.zaneschepke.wireguardautotunnel.service.network.NetworkService
import com.zaneschepke.wireguardautotunnel.service.network.NetworkStatus
import com.zaneschepke.wireguardautotunnel.service.network.WifiService
import com.zaneschepke.wireguardautotunnel.service.notification.NotificationService
import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelService
import com.zaneschepke.wireguardautotunnel.util.Constants
import com.zaneschepke.wireguardautotunnel.util.extensions.TunnelConfigs
import com.zaneschepke.wireguardautotunnel.util.extensions.cancelWithMessage
import com.zaneschepke.wireguardautotunnel.util.extensions.getCurrentWifiName
import com.zaneschepke.wireguardautotunnel.util.extensions.isReachable
import com.zaneschepke.wireguardautotunnel.util.extensions.onNotRunning
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.filterNot
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import timber.log.Timber
import java.net.InetAddress
import java.util.concurrent.atomic.AtomicBoolean
import javax.inject.Inject
import javax.inject.Provider
@AndroidEntryPoint
class AutoTunnelService : LifecycleService() {
private val foregroundId = 122
@Inject
@AppShell
lateinit var rootShell: Provider<RootShell>
@Inject
lateinit var wifiService: NetworkService<WifiService>
@Inject
lateinit var mobileDataService: NetworkService<MobileDataService>
@Inject
lateinit var ethernetService: NetworkService<EthernetService>
@Inject
lateinit var appDataRepository: Provider<AppDataRepository>
@Inject
lateinit var notificationService: NotificationService
@Inject
lateinit var tunnelService: Provider<TunnelService>
@Inject
@IoDispatcher
lateinit var ioDispatcher: CoroutineDispatcher
@Inject
lateinit var serviceManager: ServiceManager
@Inject
@MainImmediateDispatcher
lateinit var mainImmediateDispatcher: CoroutineDispatcher
private val autoTunnelStateFlow = MutableStateFlow(AutoTunnelState())
private var wakeLock: PowerManager.WakeLock? = null
private val pingTunnelRestartActive = AtomicBoolean(false)
private var pingJob: Job? = null
override fun onCreate() {
super.onCreate()
serviceManager.autoTunnelService.complete(this)
lifecycleScope.launch(mainImmediateDispatcher) {
kotlin.runCatching {
launchWatcherNotification()
}.onFailure {
Timber.e(it)
}
}
}
override fun onBind(intent: Intent): IBinder? {
super.onBind(intent)
return null
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
Timber.d("onStartCommand executed with startId: $startId")
serviceManager.autoTunnelService.complete(this)
return super.onStartCommand(intent, flags, startId)
}
fun start() {
kotlin.runCatching {
lifecycleScope.launch(mainImmediateDispatcher) {
launchWatcherNotification()
initWakeLock()
}
startAutoTunnelJob()
startAutoTunnelStateJob()
startPingStateJob()
}.onFailure {
Timber.e(it)
}
}
fun stop() {
wakeLock?.let { if (it.isHeld) it.release() }
stopSelf()
}
override fun onDestroy() {
cancelAndResetPingJob()
serviceManager.autoTunnelService = CompletableDeferred()
super.onDestroy()
}
private fun launchWatcherNotification(description: String = getString(R.string.monitoring_state_changes)) {
val notification =
notificationService.createNotification(
channelId = getString(R.string.watcher_channel_id),
channelName = getString(R.string.watcher_channel_name),
title = getString(R.string.auto_tunnel_title),
description = description,
)
ServiceCompat.startForeground(
this,
foregroundId,
notification,
Constants.SYSTEM_EXEMPT_SERVICE_TYPE_ID,
)
}
private fun initWakeLock() {
wakeLock = (getSystemService(POWER_SERVICE) as PowerManager).run {
val tag = this.javaClass.name
newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "$tag::lock").apply {
try {
Timber.i("Initiating wakelock with 10 min timeout")
acquire(Constants.BATTERY_SAVER_WATCHER_WAKE_LOCK_TIMEOUT)
} finally {
release()
}
}
}
}
private fun startPingJob() = lifecycleScope.launch {
watchForPingFailure()
}
private fun startPingStateJob() = lifecycleScope.launch {
autoTunnelStateFlow.collect {
if (it.isPingEnabled()) {
pingJob.onNotRunning { pingJob = startPingJob() }
} else {
if (!pingTunnelRestartActive.get()) cancelAndResetPingJob()
}
}
}
private suspend fun watchForPingFailure() {
withContext(ioDispatcher) {
Timber.i("Starting ping watcher")
runCatching {
do {
val vpnState = autoTunnelStateFlow.value.vpnState
if (vpnState.status.isUp() && !autoTunnelStateFlow.value.isNoConnectivity()) {
if (vpnState.tunnelConfig != null) {
val config = TunnelConfig.configFromWgQuick(vpnState.tunnelConfig.wgQuick)
val results = if (vpnState.tunnelConfig.pingIp != null) {
Timber.d("Pinging custom ip : ${vpnState.tunnelConfig.pingIp}")
listOf(InetAddress.getByName(vpnState.tunnelConfig.pingIp).isReachable(Constants.PING_TIMEOUT.toInt()))
} else {
Timber.d("Pinging all peers")
config.peers.map { peer ->
peer.isReachable()
}
}
Timber.i("Ping results reachable: $results")
if (results.contains(false)) {
Timber.i("Restarting VPN for ping failure")
val cooldown = vpnState.tunnelConfig.pingCooldown
pingTunnelRestartActive.set(true)
tunnelService.get().bounceTunnel()
pingTunnelRestartActive.set(false)
delay(cooldown ?: Constants.PING_COOLDOWN)
continue
}
}
}
delay(vpnState.tunnelConfig?.pingInterval ?: Constants.PING_INTERVAL)
} while (true)
}.onFailure {
Timber.e(it)
}
}
}
private fun startAutoTunnelStateJob() = lifecycleScope.launch(ioDispatcher) {
combine(
combineSettings(),
combineNetworkEventsJob(),
) { double, networkState ->
AutoTunnelState(tunnelService.get().vpnState.value, networkState, double.first, double.second)
}.collect { state ->
autoTunnelStateFlow.update {
it.copy(state.vpnState, state.networkState, state.settings, state.tunnels)
}
}
}
private fun cancelAndResetPingJob() {
pingJob?.cancelWithMessage("Ping job canceled")
pingJob = null
}
private fun combineNetworkEventsJob(): Flow<NetworkState> {
return combine(
wifiService.networkStatus,
mobileDataService.networkStatus,
ethernetService.networkStatus,
) { wifi, mobileData, ethernet ->
NetworkState(
wifi.isConnected,
mobileData.isConnected,
ethernet.isConnected,
when (wifi) {
is NetworkStatus.CapabilitiesChanged -> getWifiSSID(wifi.networkCapabilities)
is NetworkStatus.Available -> autoTunnelStateFlow.value.networkState.wifiName
is NetworkStatus.Unavailable -> null
},
)
}.distinctUntilChanged().filterNot { it.isWifiConnected && it.wifiName == null }
}
private fun combineSettings(): Flow<Pair<Settings, TunnelConfigs>> {
return combine(
appDataRepository.get().settings.getSettingsFlow(),
appDataRepository.get().tunnels.getTunnelConfigsFlow().distinctUntilChanged { old, new ->
old.map { it.isActive } != new.map { it.isActive }
},
) { settings, tunnels ->
Pair(settings, tunnels)
}.distinctUntilChanged()
}
private suspend fun getWifiSSID(networkCapabilities: NetworkCapabilities): String? {
return withContext(ioDispatcher) {
with(autoTunnelStateFlow.value.settings) {
if (isWifiNameByShellEnabled) return@withContext rootShell.get().getCurrentWifiName()
wifiService.getNetworkName(networkCapabilities)
}.also {
if (it?.contains(Constants.UNREADABLE_SSID) == true) {
Timber.w("SSID unreadable: missing permissions")
} else {
Timber.i("Detected valid SSID")
}
}
}
}
private fun startAutoTunnelJob() = lifecycleScope.launch(ioDispatcher) {
Timber.i("Starting auto-tunnel network event watcher")
autoTunnelStateFlow.collect { watcherState ->
Timber.d("New auto tunnel state emitted")
when (val event = watcherState.asAutoTunnelEvent()) {
is AutoTunnelEvent.Start -> tunnelService.get().startTunnel(
event.tunnelConfig
?: appDataRepository.get().getPrimaryOrFirstTunnel(),
)
is AutoTunnelEvent.Stop -> tunnelService.get().stopTunnel()
AutoTunnelEvent.DoNothing -> Timber.i("Auto-tunneling: no condition met")
}
}
}
}
@@ -0,0 +1,9 @@
package com.zaneschepke.wireguardautotunnel.service.foreground.autotunnel.model
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
sealed class AutoTunnelEvent {
data class Start(val tunnelConfig: TunnelConfig? = null) : AutoTunnelEvent()
data object Stop : AutoTunnelEvent()
data object DoNothing : AutoTunnelEvent()
}
@@ -1,16 +1,17 @@
package com.zaneschepke.wireguardautotunnel.domain.state
package com.zaneschepke.wireguardautotunnel.service.foreground.autotunnel.model
import com.zaneschepke.wireguardautotunnel.domain.events.KillSwitchEvent
import com.zaneschepke.wireguardautotunnel.domain.entity.AppSettings
import com.zaneschepke.wireguardautotunnel.domain.entity.TunnelConf
import com.zaneschepke.wireguardautotunnel.domain.events.AutoTunnelEvent
import com.zaneschepke.wireguardautotunnel.data.domain.Settings
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnState
import com.zaneschepke.wireguardautotunnel.util.extensions.TunnelConfigs
import com.zaneschepke.wireguardautotunnel.util.extensions.isMatchingToWildcardList
import timber.log.Timber
data class AutoTunnelState(
val activeTunnels: Map<Int, TunnelState> = emptyMap(),
val vpnState: VpnState = VpnState(),
val networkState: NetworkState = NetworkState(),
val settings: AppSettings = AppSettings(),
val tunnels: List<TunnelConf> = emptyList(),
val settings: Settings = Settings(),
val tunnels: TunnelConfigs = emptyList(),
) {
private fun isMobileDataActive(): Boolean {
@@ -20,23 +21,23 @@ data class AutoTunnelState(
private fun isMobileTunnelDataChangeNeeded(): Boolean {
val preferredTunnel = preferredMobileDataTunnel()
return preferredTunnel != null &&
activeTunnels.isNotEmpty() && !activeTunnels.any { it.key == preferredTunnel.id }
vpnState.status.isUp() && preferredTunnel.id != vpnState.tunnelConfig?.id
}
private fun isEthernetTunnelChangeNeeded(): Boolean {
val preferredTunnel = preferredEthernetTunnel()
return preferredTunnel != null && activeTunnels.isNotEmpty() && !activeTunnels.any { it.key == preferredTunnel.id }
return preferredTunnel != null && vpnState.status.isUp() && preferredTunnel.id != vpnState.tunnelConfig?.id
}
private fun preferredMobileDataTunnel(): TunnelConf? {
private fun preferredMobileDataTunnel(): TunnelConfig? {
return tunnels.firstOrNull { it.isMobileDataTunnel } ?: tunnels.firstOrNull { it.isPrimaryTunnel }
}
private fun preferredEthernetTunnel(): TunnelConf? {
private fun preferredEthernetTunnel(): TunnelConfig? {
return tunnels.firstOrNull { it.isEthernetTunnel } ?: tunnels.firstOrNull { it.isPrimaryTunnel }
}
private fun preferredWifiTunnel(): TunnelConf? {
private fun preferredWifiTunnel(): TunnelConfig? {
return getTunnelWithMatchingTunnelNetwork() ?: tunnels.firstOrNull { it.isPrimaryTunnel }
}
@@ -45,33 +46,23 @@ data class AutoTunnelState(
}
private fun startOnEthernet(): Boolean {
return networkState.isEthernetConnected && settings.isTunnelOnEthernetEnabled && activeTunnels.isEmpty()
return networkState.isEthernetConnected && settings.isTunnelOnEthernetEnabled && vpnState.status.isDown()
}
private fun stopOnEthernet(): Boolean {
return networkState.isEthernetConnected && !settings.isTunnelOnEthernetEnabled && activeTunnels.isNotEmpty()
return networkState.isEthernetConnected && !settings.isTunnelOnEthernetEnabled && vpnState.status.isUp()
}
// TODO test removed kill switch state check
private fun stopKillSwitchOnTrusted(): Boolean {
return networkState.isWifiConnected && settings.isVpnKillSwitchEnabled && settings.isDisableKillSwitchOnTrustedEnabled && isCurrentSSIDTrusted()
}
// TODO test, removed kill switch state check
private fun startKillSwitch(): Boolean {
return settings.isVpnKillSwitchEnabled && (!settings.isDisableKillSwitchOnTrustedEnabled || !isCurrentSSIDTrusted())
}
private fun isNoConnectivity(): Boolean {
fun isNoConnectivity(): Boolean {
return !networkState.isEthernetConnected && !networkState.isWifiConnected && !networkState.isMobileDataConnected
}
private fun stopOnMobileData(): Boolean {
return isMobileDataActive() && !settings.isTunnelOnMobileDataEnabled && activeTunnels.isNotEmpty()
return isMobileDataActive() && !settings.isTunnelOnMobileDataEnabled && vpnState.status.isUp()
}
private fun startOnMobileData(): Boolean {
return isMobileDataActive() && settings.isTunnelOnMobileDataEnabled && activeTunnels.isEmpty()
return isMobileDataActive() && settings.isTunnelOnMobileDataEnabled && vpnState.status.isDown()
}
private fun changeOnMobileData(): Boolean {
@@ -83,24 +74,30 @@ data class AutoTunnelState(
}
private fun stopOnWifi(): Boolean {
return isWifiActive() && !settings.isTunnelOnWifiEnabled && activeTunnels.isNotEmpty()
return isWifiActive() && !settings.isTunnelOnWifiEnabled && vpnState.status.isUp()
}
private fun stopOnTrustedWifi(): Boolean {
return isWifiActive() && settings.isTunnelOnWifiEnabled && activeTunnels.isNotEmpty() && isCurrentSSIDTrusted()
return isWifiActive() && settings.isTunnelOnWifiEnabled && vpnState.status.isUp() && isCurrentSSIDTrusted()
}
private fun startOnUntrustedWifi(): Boolean {
return isWifiActive() && settings.isTunnelOnWifiEnabled && activeTunnels.isEmpty() && !isCurrentSSIDTrusted()
Timber.d("Is tunnel on wifi enabled ${settings.isTunnelOnWifiEnabled}")
return isWifiActive() && settings.isTunnelOnWifiEnabled && vpnState.status.isDown() && !isCurrentSSIDTrusted()
}
private fun changeOnUntrustedWifi(): Boolean {
return isWifiActive() && settings.isTunnelOnWifiEnabled && activeTunnels.isNotEmpty() && !isCurrentSSIDTrusted() && !isWifiTunnelPreferred()
return isWifiActive() && settings.isTunnelOnWifiEnabled && vpnState.status.isUp() && !isCurrentSSIDTrusted() && !isWifiTunnelPreferred()
}
private fun isWifiTunnelPreferred(): Boolean {
val preferred = preferredWifiTunnel()
return activeTunnels.any { it.key == preferred?.id }
val vpnTunnel = vpnState.tunnelConfig
return if (preferred != null && vpnTunnel != null) {
preferred.id == vpnTunnel.id
} else {
true
}
}
fun asAutoTunnelEvent(): AutoTunnelEvent {
@@ -121,17 +118,6 @@ data class AutoTunnelState(
}
}
fun asKillSwitchEvent(): KillSwitchEvent {
return when {
stopKillSwitchOnTrusted() -> KillSwitchEvent.Stop
startKillSwitch() -> {
val allowedIps = if (settings.isLanOnKillSwitchEnabled) TunnelConf.LAN_BYPASS_ALLOWED_IPS else emptyList()
KillSwitchEvent.Start(allowedIps)
}
else -> KillSwitchEvent.DoNothing
}
}
private fun isCurrentSSIDTrusted(): Boolean {
return networkState.wifiName?.let {
hasTrustedWifiName(it)
@@ -146,11 +132,16 @@ data class AutoTunnelState(
}
}
private fun getTunnelWithMatchingTunnelNetwork(): TunnelConf? {
private fun getTunnelWithMatchingTunnelNetwork(): TunnelConfig? {
return networkState.wifiName?.let { wifiName ->
tunnels.firstOrNull {
hasTrustedWifiName(wifiName, it.tunnelNetworks)
}
}
}
fun isPingEnabled(): Boolean {
return settings.isPingEnabled ||
(vpnState.status.isUp() && vpnState.tunnelConfig != null && tunnels.first { it.id == vpnState.tunnelConfig.id }.isPingEnabled)
}
}
@@ -1,12 +1,8 @@
package com.zaneschepke.wireguardautotunnel.domain.state
package com.zaneschepke.wireguardautotunnel.service.foreground.autotunnel.model
data class NetworkState(
val isWifiConnected: Boolean = false,
val isMobileDataConnected: Boolean = false,
val isEthernetConnected: Boolean = false,
val wifiName: String? = null,
) {
fun hasNoCapabilities(): Boolean {
return !isWifiConnected && !isMobileDataConnected && !isEthernetConnected
}
}
)
@@ -0,0 +1,116 @@
package com.zaneschepke.wireguardautotunnel.service.network
import android.content.Context
import android.net.ConnectivityManager
import android.net.Network
import android.net.NetworkCapabilities
import android.net.NetworkRequest
import android.net.wifi.WifiManager
import android.os.Build
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.conflate
import kotlinx.coroutines.flow.map
import timber.log.Timber
abstract class BaseNetworkService<T : BaseNetworkService<T>>(
val context: Context,
networkCapability: Int,
) : NetworkService<T> {
private val connectivityManager =
context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
val wifiManager =
context.applicationContext.getSystemService(Context.WIFI_SERVICE) as WifiManager
fun checkHasCapability(networkCapability: Int): Boolean {
val network = connectivityManager.activeNetwork
val networkCapabilities = connectivityManager.getNetworkCapabilities(network)
return networkCapabilities?.hasTransport(networkCapability) == true
}
override val networkStatus =
callbackFlow {
if (!checkHasCapability(networkCapability)) {
trySend(NetworkStatus.Unavailable())
}
val networkStatusCallback =
when (Build.VERSION.SDK_INT) {
in Build.VERSION_CODES.S..Int.MAX_VALUE -> {
object :
ConnectivityManager.NetworkCallback(
FLAG_INCLUDE_LOCATION_INFO,
) {
override fun onAvailable(network: Network) {
trySend(NetworkStatus.Available(network))
}
override fun onLost(network: Network) {
trySend(NetworkStatus.Unavailable())
}
override fun onCapabilitiesChanged(network: Network, networkCapabilities: NetworkCapabilities) {
trySend(
NetworkStatus.CapabilitiesChanged(
network,
networkCapabilities,
),
)
}
}
}
else -> {
object : ConnectivityManager.NetworkCallback() {
override fun onAvailable(network: Network) {
trySend(NetworkStatus.Available(network))
}
override fun onLost(network: Network) {
trySend(NetworkStatus.Unavailable())
}
override fun onCapabilitiesChanged(network: Network, networkCapabilities: NetworkCapabilities) {
trySend(
NetworkStatus.CapabilitiesChanged(
network,
networkCapabilities,
),
)
}
}
}
}
val request =
NetworkRequest.Builder()
.addTransportType(networkCapability)
.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
.addCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED)
.build()
connectivityManager.registerNetworkCallback(request, networkStatusCallback)
awaitClose { connectivityManager.unregisterNetworkCallback(networkStatusCallback) }
}.catch {
Timber.e(it)
// conflate for backpressure
}.conflate()
}
inline fun <Result> Flow<NetworkStatus>.map(
crossinline onUnavailable: suspend () -> Result,
crossinline onAvailable: suspend (network: Network) -> Result,
crossinline onCapabilitiesChanged:
suspend (network: Network, networkCapabilities: NetworkCapabilities) -> Result,
): Flow<Result> = map { status ->
when (status) {
is NetworkStatus.Unavailable -> onUnavailable()
is NetworkStatus.Available -> onAvailable(status.network)
is NetworkStatus.CapabilitiesChanged ->
onCapabilitiesChanged(
status.network,
status.networkCapabilities,
)
}
}
@@ -0,0 +1,18 @@
package com.zaneschepke.wireguardautotunnel.service.network
import android.content.Context
import android.net.NetworkCapabilities
import dagger.hilt.android.qualifiers.ApplicationContext
import javax.inject.Inject
class EthernetService
@Inject
constructor(
@ApplicationContext context: Context,
) :
BaseNetworkService<EthernetService>(context, NetworkCapabilities.TRANSPORT_ETHERNET) {
override fun isNetworkSecure(): Boolean {
return true
}
}
@@ -0,0 +1,17 @@
package com.zaneschepke.wireguardautotunnel.service.network
import android.content.Context
import android.net.NetworkCapabilities
import dagger.hilt.android.qualifiers.ApplicationContext
import javax.inject.Inject
class MobileDataService
@Inject
constructor(
@ApplicationContext context: Context,
) :
BaseNetworkService<MobileDataService>(context, NetworkCapabilities.TRANSPORT_CELLULAR) {
override fun isNetworkSecure(): Boolean {
return false
}
}
@@ -1,4 +1,4 @@
package com.zaneschepke.wireguardautotunnel.core.network
package com.zaneschepke.wireguardautotunnel.service.network
import android.net.NetworkCapabilities
import android.net.wifi.WifiInfo
@@ -0,0 +1,14 @@
package com.zaneschepke.wireguardautotunnel.service.network
import android.net.NetworkCapabilities
import kotlinx.coroutines.flow.Flow
interface NetworkService<T> {
fun getNetworkName(networkCapabilities: NetworkCapabilities): String? {
return null
}
fun isNetworkSecure(): Boolean
val networkStatus: Flow<NetworkStatus>
}
@@ -0,0 +1,14 @@
package com.zaneschepke.wireguardautotunnel.service.network
import android.net.Network
import android.net.NetworkCapabilities
sealed class NetworkStatus {
abstract val isConnected: Boolean
class Available(val network: Network, override val isConnected: Boolean = true) : NetworkStatus()
class Unavailable(override val isConnected: Boolean = false) : NetworkStatus()
class CapabilitiesChanged(val network: Network, val networkCapabilities: NetworkCapabilities, override val isConnected: Boolean = true) :
NetworkStatus()
}
@@ -0,0 +1,32 @@
package com.zaneschepke.wireguardautotunnel.service.network
import android.content.Context
import android.net.NetworkCapabilities
import android.net.wifi.SupplicantState
import android.os.Build
import dagger.hilt.android.qualifiers.ApplicationContext
import javax.inject.Inject
class WifiService
@Inject
constructor(
@ApplicationContext context: Context,
) :
BaseNetworkService<WifiService>(context, NetworkCapabilities.TRANSPORT_WIFI) {
override fun getNetworkName(networkCapabilities: NetworkCapabilities): String? {
var ssid = networkCapabilities.getWifiName()
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.S) {
val info = wifiManager.connectionInfo
if (info.supplicantState === SupplicantState.COMPLETED) {
ssid = info.ssid
}
}
return ssid?.trim('"')
}
override fun isNetworkSecure(): Boolean {
// TODO
return false
}
}
@@ -0,0 +1,22 @@
package com.zaneschepke.wireguardautotunnel.service.notification
import android.app.Notification
import android.app.NotificationManager
import android.app.PendingIntent
interface NotificationService {
fun createNotification(
channelId: String,
channelName: String,
title: String = "",
action: PendingIntent? = null,
actionText: String? = null,
description: String,
showTimestamp: Boolean = false,
importance: Int = NotificationManager.IMPORTANCE_HIGH,
vibration: Boolean = false,
onGoing: Boolean = true,
lights: Boolean = true,
onlyAlertOnce: Boolean = true,
): Notification
}
@@ -0,0 +1,105 @@
package com.zaneschepke.wireguardautotunnel.service.notification
import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.graphics.Color
import androidx.core.app.NotificationCompat
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.ui.MainActivity
import dagger.hilt.android.qualifiers.ApplicationContext
import javax.inject.Inject
class WireGuardNotification
@Inject
constructor(
@ApplicationContext private val context: Context,
) :
NotificationService {
private val notificationManager =
context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
private val watcherBuilder: NotificationCompat.Builder =
NotificationCompat.Builder(
context,
context.getString(R.string.watcher_channel_id),
)
private val tunnelBuilder: NotificationCompat.Builder =
NotificationCompat.Builder(
context,
context.getString(R.string.vpn_channel_id),
)
override fun createNotification(
channelId: String,
channelName: String,
title: String,
action: PendingIntent?,
actionText: String?,
description: String,
showTimestamp: Boolean,
importance: Int,
vibration: Boolean,
onGoing: Boolean,
lights: Boolean,
onlyAlertOnce: Boolean,
): Notification {
val channel =
NotificationChannel(
channelId,
channelName,
importance,
)
.let {
it.description = title
it.enableLights(lights)
it.lightColor = Color.RED
it.enableVibration(vibration)
it.vibrationPattern = longArrayOf(100, 200, 300)
it
}
notificationManager.createNotificationChannel(channel)
val pendingIntent: PendingIntent =
Intent(context, MainActivity::class.java).let { notificationIntent ->
PendingIntent.getActivity(
context,
0,
notificationIntent,
PendingIntent.FLAG_IMMUTABLE,
)
}
val builder =
when (channelId) {
context.getString(R.string.watcher_channel_id) -> watcherBuilder
context.getString(R.string.vpn_channel_id) -> tunnelBuilder
else -> {
NotificationCompat.Builder(
context,
channelId,
)
}
}
return builder.let {
if (action != null && actionText != null) {
it.addAction(
NotificationCompat.Action.Builder(0, actionText, action).build(),
)
it.setAutoCancel(true)
}
it.setContentTitle(title)
.setContentText(description)
.setOnlyAlertOnce(onlyAlertOnce)
.setContentIntent(pendingIntent)
.setOngoing(onGoing)
.setPriority(NotificationCompat.PRIORITY_HIGH)
.setShowWhen(showTimestamp)
.setSmallIcon(R.drawable.ic_launcher)
.build()
}
}
}
@@ -1,23 +1,27 @@
package com.zaneschepke.wireguardautotunnel.core.shortcut
package com.zaneschepke.wireguardautotunnel.service.shortcut
import android.os.Bundle
import androidx.activity.ComponentActivity
import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository
import com.zaneschepke.wireguardautotunnel.di.ApplicationScope
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
import com.zaneschepke.wireguardautotunnel.core.service.autotunnel.AutoTunnelService
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelProvider
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.autotunnel.AutoTunnelService
import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelService
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject
import javax.inject.Provider
@AndroidEntryPoint
class ShortcutsActivity : ComponentActivity() {
@Inject
lateinit var appDataRepository: AppDataRepository
@Inject
lateinit var tunnelService: Provider<TunnelService>
@Inject
lateinit var serviceManager: ServiceManager
@@ -28,24 +32,24 @@ class ShortcutsActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
applicationScope.launch {
val settings = appDataRepository.settings.get()
val settings = appDataRepository.settings.getSettings()
if (settings.isShortcutsEnabled) {
when (intent.getStringExtra(CLASS_NAME_EXTRA_KEY)) {
LEGACY_TUNNEL_SERVICE_NAME, TunnelProvider::class.java.simpleName -> {
LEGACY_TUNNEL_SERVICE_NAME, TunnelService::class.java.simpleName -> {
val tunnelName = intent.getStringExtra(TUNNEL_NAME_EXTRA_KEY)
Timber.d("Tunnel name extra: $tunnelName")
val tunnelConfig = tunnelName?.let {
appDataRepository.tunnels.getAll()
.firstOrNull { it.tunName == tunnelName }
.firstOrNull { it.name == tunnelName }
} ?: appDataRepository.getStartTunnelConfig()
Timber.d("Shortcut action on name: ${tunnelConfig?.tunName}")
// tunnelConfig?.let {
// when (intent.action) {
// Action.START.name -> tunnelService.get().startTunnel(it)
// Action.STOP.name -> tunnelService.get().stopTunnel()
// else -> Unit
// }
// }
Timber.d("Shortcut action on name: ${tunnelConfig?.name}")
tunnelConfig?.let {
when (intent.action) {
Action.START.name -> tunnelService.get().startTunnel(it, true)
Action.STOP.name -> tunnelService.get().stopTunnel()
else -> Unit
}
}
}
AutoTunnelService::class.java.simpleName, LEGACY_AUTO_TUNNEL_SERVICE_NAME -> {
when (intent.action) {
@@ -1,21 +1,24 @@
package com.zaneschepke.wireguardautotunnel.core.service.tile
package com.zaneschepke.wireguardautotunnel.service.tile
import android.content.Intent
import android.os.IBinder
import android.service.quicksettings.Tile
import android.service.quicksettings.TileService
import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository
import com.zaneschepke.wireguardautotunnel.di.ApplicationScope
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.LifecycleRegistry
import androidx.lifecycle.lifecycleScope
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
import com.zaneschepke.wireguardautotunnel.module.ApplicationScope
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject
@AndroidEntryPoint
class AutoTunnelControlTile : TileService() {
class AutoTunnelControlTile : TileService(), LifecycleOwner {
@Inject
lateinit var appDataRepository: AppDataRepository
@@ -26,26 +29,32 @@ class AutoTunnelControlTile : TileService() {
@ApplicationScope
lateinit var applicationScope: CoroutineScope
private val lifecycleRegistry: LifecycleRegistry = LifecycleRegistry(this)
override fun onCreate() {
super.onCreate()
serviceManager.autoTunnelTile.complete(this)
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_CREATE)
}
override fun onStopListening() {
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_STOP)
}
override fun onDestroy() {
super.onDestroy()
serviceManager.autoTunnelTile = CompletableDeferred()
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_DESTROY)
}
override fun onStartListening() {
super.onStartListening()
serviceManager.autoTunnelTile.complete(this)
applicationScope.launch {
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_START)
lifecycleScope.launch {
if (appDataRepository.tunnels.getAll().isEmpty()) return@launch setUnavailable()
updateTileState()
}
}
fun updateTileState() {
private fun updateTileState() {
serviceManager.autoTunnelActive.value.let {
if (it) setActive() else setInactive()
}
@@ -54,7 +63,7 @@ class AutoTunnelControlTile : TileService() {
override fun onClick() {
super.onClick()
unlockAndRun {
applicationScope.launch {
lifecycleScope.launch {
if (serviceManager.autoTunnelActive.value) {
serviceManager.stopAutoTunnel()
setInactive()
@@ -67,19 +76,26 @@ class AutoTunnelControlTile : TileService() {
}
private fun setActive() {
runCatching {
kotlin.runCatching {
qsTile.state = Tile.STATE_ACTIVE
qsTile.updateTile()
}
}
private fun setInactive() {
runCatching {
kotlin.runCatching {
qsTile.state = Tile.STATE_INACTIVE
qsTile.updateTile()
}
}
private fun setUnavailable() {
kotlin.runCatching {
qsTile.state = Tile.STATE_UNAVAILABLE
qsTile.updateTile()
}
}
/* This works around an annoying unsolved frameworks bug some people are hitting. */
override fun onBind(intent: Intent): IBinder? {
var ret: IBinder? = null
@@ -91,10 +107,6 @@ class AutoTunnelControlTile : TileService() {
return ret
}
private fun setUnavailable() {
runCatching {
qsTile.state = Tile.STATE_UNAVAILABLE
qsTile.updateTile()
}
}
override val lifecycle: Lifecycle
get() = lifecycleRegistry
}
@@ -0,0 +1,142 @@
package com.zaneschepke.wireguardautotunnel.service.tile
import android.content.Intent
import android.os.Build
import android.os.IBinder
import android.service.quicksettings.Tile
import android.service.quicksettings.TileService
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.LifecycleRegistry
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.tunnel.TunnelService
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject
import javax.inject.Provider
@AndroidEntryPoint
class TunnelControlTile : TileService(), LifecycleOwner {
@Inject
lateinit var appDataRepository: AppDataRepository
@Inject
lateinit var tunnelService: Provider<TunnelService>
@Inject
@ApplicationScope
lateinit var applicationScope: CoroutineScope
private val lifecycleRegistry: LifecycleRegistry = LifecycleRegistry(this)
override fun onCreate() {
super.onCreate()
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_CREATE)
}
override fun onStopListening() {
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_STOP)
}
override fun onDestroy() {
super.onDestroy()
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_DESTROY)
}
override fun onStartListening() {
super.onStartListening()
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_START)
Timber.d("Updating tile!")
lifecycleScope.launch {
if (appDataRepository.tunnels.getAll().isEmpty()) return@launch setUnavailable()
updateTileState()
}
}
private suspend fun updateTileState() {
val lastActive = appDataRepository.getStartTunnelConfig()
lastActive?.let {
updateTile(it)
}
}
override fun onClick() {
super.onClick()
unlockAndRun {
lifecycleScope.launch {
val lastActive = appDataRepository.getStartTunnelConfig()
lastActive?.let { tunnel ->
if (tunnel.isActive) {
tunnelService.get().stopTunnel()
} else {
tunnelService.get().startTunnel(tunnel, true)
}
updateTileState()
}
}
}
}
private fun setActive() {
kotlin.runCatching {
qsTile.state = Tile.STATE_ACTIVE
qsTile.updateTile()
}
}
private fun setInactive() {
kotlin.runCatching {
qsTile.state = Tile.STATE_INACTIVE
qsTile.updateTile()
}
}
private fun setUnavailable() {
kotlin.runCatching {
qsTile.state = Tile.STATE_UNAVAILABLE
setTileDescription("")
qsTile.updateTile()
}
}
private fun setTileDescription(description: String) {
kotlin.runCatching {
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()
}
}
private fun updateTile(tunnelConfig: TunnelConfig?) {
kotlin.runCatching {
tunnelConfig?.let {
setTileDescription(it.name)
if (it.isActive) return setActive()
setInactive()
}
}
}
/* This works around an annoying unsolved frameworks bug some people are hitting. */
override fun onBind(intent: Intent): IBinder? {
var ret: IBinder? = null
try {
ret = super.onBind(intent)
} catch (_: Throwable) {
Timber.e("Failed to bind to TunnelControlTile")
}
return ret
}
override val lifecycle: Lifecycle
get() = lifecycleRegistry
}
@@ -0,0 +1,46 @@
package com.zaneschepke.wireguardautotunnel.service.tunnel
import android.content.Intent
import android.os.IBinder
import androidx.lifecycle.LifecycleService
import androidx.lifecycle.lifecycleScope
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject
import javax.inject.Provider
@AndroidEntryPoint
class AlwaysOnVpnService : LifecycleService() {
@Inject
lateinit var tunnelService: Provider<TunnelService>
@Inject
lateinit var appDataRepository: AppDataRepository
override fun onBind(intent: Intent): IBinder? {
super.onBind(intent)
// We don't provide binding, so return null
return null
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
if (intent == null || intent.component == null || intent.component!!.packageName != packageName) {
Timber.i("Always-on VPN requested started")
lifecycleScope.launch {
val settings = appDataRepository.settings.getSettings()
if (settings.isAlwaysOnVpnEnabled) {
val tunnel = appDataRepository.getPrimaryOrFirstTunnel()
tunnel?.let {
tunnelService.get().startTunnel(it)
}
} else {
Timber.w("Always-on VPN is not enabled in app settings")
}
}
}
return super.onStartCommand(intent, flags, startId)
}
}
@@ -1,4 +1,4 @@
package com.zaneschepke.wireguardautotunnel.domain.enums
package com.zaneschepke.wireguardautotunnel.service.tunnel
enum class BackendState {
KILL_SWITCH_ACTIVE,
@@ -1,4 +1,4 @@
package com.zaneschepke.wireguardautotunnel.domain.enums
package com.zaneschepke.wireguardautotunnel.service.tunnel
enum class HandshakeStatus {
HEALTHY,
@@ -0,0 +1,28 @@
package com.zaneschepke.wireguardautotunnel.service.tunnel
import com.wireguard.android.backend.Tunnel
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
import kotlinx.coroutines.flow.StateFlow
interface TunnelService : Tunnel, org.amnezia.awg.backend.Tunnel {
suspend fun startTunnel(tunnelConfig: TunnelConfig?, background: Boolean = false)
suspend fun stopTunnel()
suspend fun bounceTunnel()
suspend fun getBackendState(): BackendState
suspend fun setBackendState(backendState: BackendState, allowedIps: Collection<String>)
val vpnState: StateFlow<VpnState>
suspend fun runningTunnelNames(): Set<String>
suspend fun getState(): TunnelState
fun cancelActiveTunnelJobs()
fun startActiveTunnelJobs()
}
@@ -0,0 +1,52 @@
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
}
}
fun isDown(): Boolean {
return this == DOWN
}
fun isUp(): Boolean {
return this == UP
}
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
}
}
}
}
@@ -0,0 +1,10 @@
package com.zaneschepke.wireguardautotunnel.service.tunnel
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
import com.zaneschepke.wireguardautotunnel.service.tunnel.statistics.TunnelStatistics
data class VpnState(
val status: TunnelState = TunnelState.DOWN,
val tunnelConfig: TunnelConfig? = null,
val statistics: TunnelStatistics? = null,
)

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