Compare commits

..

3 Commits

Author SHA1 Message Date
Zane Schepke b3cb7a7988 docs: update README support screenshot 2023-07-23 00:39:30 -04:00
Zane Schepke 6415f49377 chore: add feature graphic and banner to assets 2023-07-23 00:34:17 -04:00
Zane Schepke baed8ff2e7 feat: support for Always-On VPN and Android TV
Added support for Android TV

Added support for Always-On VPN in settings

Fixes bug where handshake notification is not dismissed after successful handshake

Closes #2,  Closes #5
2023-07-23 00:16:16 -04:00
255 changed files with 3135 additions and 9870 deletions
-85
View File
@@ -1,85 +0,0 @@
[{*.kt,*.kts}]
indent_style = space
insert_final_newline = true
max_line_length = 100
indent_size = 4
ij_continuation_indent_size = 4
ij_java_names_count_to_use_import_on_demand = 9999
ij_kotlin_align_in_columns_case_branch = false
ij_kotlin_align_multiline_binary_operation = false
ij_kotlin_align_multiline_extends_list = false
ij_kotlin_align_multiline_method_parentheses = false
ij_kotlin_align_multiline_parameters = true
ij_kotlin_align_multiline_parameters_in_calls = false
ij_kotlin_allow_trailing_comma = true
ij_kotlin_allow_trailing_comma_on_call_site = true
ij_kotlin_assignment_wrap = normal
ij_kotlin_blank_lines_after_class_header = 0
ij_kotlin_blank_lines_around_block_when_branches = 0
ij_kotlin_blank_lines_before_declaration_with_comment_or_annotation_on_separate_line = 1
ij_kotlin_block_comment_at_first_column = true
ij_kotlin_call_parameters_new_line_after_left_paren = true
ij_kotlin_call_parameters_right_paren_on_new_line = false
ij_kotlin_call_parameters_wrap = on_every_item
ij_kotlin_catch_on_new_line = false
ij_kotlin_class_annotation_wrap = split_into_lines
ij_kotlin_code_style_defaults = KOTLIN_OFFICIAL
ij_kotlin_continuation_indent_for_chained_calls = true
ij_kotlin_continuation_indent_for_expression_bodies = true
ij_kotlin_continuation_indent_in_argument_lists = true
ij_kotlin_continuation_indent_in_elvis = false
ij_kotlin_continuation_indent_in_if_conditions = false
ij_kotlin_continuation_indent_in_parameter_lists = false
ij_kotlin_continuation_indent_in_supertype_lists = false
ij_kotlin_else_on_new_line = false
ij_kotlin_enum_constants_wrap = off
ij_kotlin_extends_list_wrap = normal
ij_kotlin_field_annotation_wrap = split_into_lines
ij_kotlin_finally_on_new_line = false
ij_kotlin_if_rparen_on_new_line = false
ij_kotlin_import_nested_classes = false
ij_kotlin_insert_whitespaces_in_simple_one_line_method = true
ij_kotlin_keep_blank_lines_before_right_brace = 2
ij_kotlin_keep_blank_lines_in_code = 2
ij_kotlin_keep_blank_lines_in_declarations = 2
ij_kotlin_keep_first_column_comment = true
ij_kotlin_keep_indents_on_empty_lines = false
ij_kotlin_keep_line_breaks = true
ij_kotlin_lbrace_on_next_line = false
ij_kotlin_line_comment_add_space = false
ij_kotlin_line_comment_at_first_column = true
ij_kotlin_method_annotation_wrap = split_into_lines
ij_kotlin_method_call_chain_wrap = normal
ij_kotlin_method_parameters_new_line_after_left_paren = true
ij_kotlin_method_parameters_right_paren_on_new_line = true
ij_kotlin_method_parameters_wrap = on_every_item
ij_kotlin_name_count_to_use_star_import = 9999
ij_kotlin_name_count_to_use_star_import_for_members = 9999
ij_kotlin_parameter_annotation_wrap = off
ij_kotlin_space_after_comma = true
ij_kotlin_space_after_extend_colon = true
ij_kotlin_space_after_type_colon = true
ij_kotlin_space_before_catch_parentheses = true
ij_kotlin_space_before_comma = false
ij_kotlin_space_before_extend_colon = true
ij_kotlin_space_before_for_parentheses = true
ij_kotlin_space_before_if_parentheses = true
ij_kotlin_space_before_lambda_arrow = true
ij_kotlin_space_before_type_colon = false
ij_kotlin_space_before_when_parentheses = true
ij_kotlin_space_before_while_parentheses = true
ij_kotlin_spaces_around_additive_operators = true
ij_kotlin_spaces_around_assignment_operators = true
ij_kotlin_spaces_around_equality_operators = true
ij_kotlin_spaces_around_function_type_arrow = true
ij_kotlin_spaces_around_logical_operators = true
ij_kotlin_spaces_around_multiplicative_operators = true
ij_kotlin_spaces_around_range = false
ij_kotlin_spaces_around_relational_operators = true
ij_kotlin_spaces_around_unary_operator = false
ij_kotlin_spaces_around_when_arrow = true
ij_kotlin_variable_annotation_wrap = off
ij_kotlin_while_on_new_line = false
ij_kotlin_wrap_elvis_expressions = 1
ij_kotlin_wrap_expression_body_functions = 1
ij_kotlin_wrap_first_method_in_call_chain = false
-2
View File
@@ -1,2 +0,0 @@
ko_fi: zaneschepke
liberapay: zaneschepke
-33
View File
@@ -1,33 +0,0 @@
---
name: Bug report
about: Create a report to help us improve
title: "[BUG] - Problem with app"
labels: bug
assignees: zaneschepke
---
**Describe the bug**
A clear and concise description of what the bug is.
**Smartphone (please complete the following information):**
- Device: [e.g. Pixel 4a]
- Android Version: [e.g. Android 13]
- App Version [e.g. 3.3.3]
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots (Only if necessary)**
**Additional context**
Add any other context about the problem here.
-20
View File
@@ -1,20 +0,0 @@
---
name: Feature request
about: Suggest an idea for this project
title: "[FEATURE] - New feature request"
labels: enhancement
assignees: zaneschepke
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.
-17
View File
@@ -1,17 +0,0 @@
# Support
If you are experiencing issues with the app, the following resources are available to help you.
<ol>
<li>
See the app docs site <a href="https://zaneschepke.com/wgtunnel-docs/overview.html">here</a> (work in progress).
</li>
<li>
Chat with me and our community on Discord <a href="https://discord.gg/rbRRNh6H7V">here</a>, or open an issue on GitHub <a href="https://github.com/zaneschepke/wgtunnel/issues/new/choose">here</a>.
</li>
<li>
Send me an email <a href="mailto:zanecschepke@gmail.com">here</a>.
</li>
</ol>
Thank you for using WG Tunnel.
-10
View File
@@ -1,10 +0,0 @@
version: 2
updates:
- package-ecosystem: github-actions
directory: /
schedule:
interval: daily
- package-ecosystem: gradle
directory: /
schedule:
interval: daily
-124
View File
@@ -1,124 +0,0 @@
# name of the workflow
name: Android CI Tag Deployment (Pre-release)
on:
workflow_dispatch:
push:
tags:
- '*.*.*-**'
jobs:
build:
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_TOKEN: ${{ secrets.GH_TOKEN }}
steps:
- uses: actions/checkout@v4
- name: Set up JDK 17
uses: actions/setup-java@v4
with:
distribution: 'temurin'
java-version: '17'
cache: gradle
- name: Grant execute permission for gradlew
run: chmod +x gradlew
# Here we need to decode keystore.jks from base64 string and place it
# in the folder specified in the release signing configuration
- name: Decode Keystore
id: decode_keystore
uses: timheuer/base64-to-file@v1.2
with:
fileName: ${{ env.KEY_STORE_FILE }}
fileDir: ${{ env.KEY_STORE_LOCATION }}
encodedString: ${{ secrets.KEYSTORE }}
# create keystore path for gradle to read
- name: Create keystore path env var
run: |
store_path=${{ env.KEY_STORE_LOCATION }}${{ env.KEY_STORE_FILE }}
echo "KEY_STORE_PATH=$store_path" >> $GITHUB_ENV
- name: Create service_account.json
id: createServiceAccount
run: echo '${{ secrets.SERVICE_ACCOUNT_JSON }}' > service_account.json
# Build and sign APK ("-x test" argument is used to skip tests)
# add fdroid flavor for apk upload
- name: Build Fdroid Release APK
run: ./gradlew :app:assembleFdroidRelease -x test
# get fdroid flavor release apk path
- name: Get apk path
id: apk-path
run: echo "path=$(find . -regex '^.*/build/outputs/apk/fdroid/release/.*\.apk$' -type f | head -1)" >> $GITHUB_OUTPUT
- name: Get version code
run: |
version_code=$(grep "VERSION_CODE" buildSrc/src/main/kotlin/Constants.kt | awk '{print $5}' | tr -d '\n')
echo "VERSION_CODE=$version_code" >> $GITHUB_ENV
# Save the APK after the Build job is complete to publish it as a Github release in the next job
- name: Upload APK
uses: actions/upload-artifact@v4.3.1
with:
name: wgtunnel
path: ${{ steps.apk-path.outputs.path }}
- name: Download APK from build
uses: actions/download-artifact@v4
with:
name: wgtunnel
- name: Create Release with Fastlane changelog notes
id: create_release
uses: softprops/action-gh-release@v2
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
# fix hardcode changelog file name
body_path: ${{ github.workspace }}/fastlane/metadata/android/en-US/changelogs/${{ env.VERSION_CODE }}.txt
tag_name: ${{ github.ref_name }}
name: ${{ github.ref_name }}
draft: false
prerelease: true
files: ${{ github.workspace }}/${{ steps.apk-path.outputs.path }}
- name: Install apksigner
run: |
sudo apt-get update
sudo apt-get install -y apksigner
- name: Get checksum
id: checksum
run: echo "checksum=$(apksigner verify -print-certs ${{ steps.apk-path.outputs.path }} | grep -Po "(?<=SHA-256 digest:) .*" | tr -d "[:blank:]")" >> $GITHUB_OUTPUT
- name: Append checksum
id: append_checksum
uses: softprops/action-gh-release@v2
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
body: |
SHA256 fingerprint:
```${{ steps.checksum.outputs.checksum }}```
tag_name: ${{ github.ref_name }}
name: ${{ github.ref_name }}
draft: false
prerelease: true
append_body: true
- name: Deploy with fastlane
uses: ruby/setup-ruby@v1
with:
ruby-version: '3.2' # Not needed with a .ruby-version file
bundler-cache: true
- name: Distribute app to Beta track 🚀
run: (cd ${{ github.workspace }} && bundle install && bundle exec fastlane beta)
-130
View File
@@ -1,130 +0,0 @@
# name of the workflow
name: Android CI Tag Deployment (Release)
on:
workflow_dispatch:
push:
tags:
- '*.*.*'
- '!*.*.*-**'
jobs:
build:
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_TOKEN: ${{ secrets.GH_TOKEN }}
steps:
- uses: actions/checkout@v4
- name: Set up JDK 17
uses: actions/setup-java@v4
with:
distribution: 'temurin'
java-version: '17'
cache: gradle
- name: Grant execute permission for gradlew
run: chmod +x gradlew
# Here we need to decode keystore.jks from base64 string and place it
# in the folder specified in the release signing configuration
- name: Decode Keystore
id: decode_keystore
uses: timheuer/base64-to-file@v1.2
with:
fileName: ${{ env.KEY_STORE_FILE }}
fileDir: ${{ env.KEY_STORE_LOCATION }}
encodedString: ${{ secrets.KEYSTORE }}
# create keystore path for gradle to read
- name: Create keystore path env var
run: |
store_path=${{ env.KEY_STORE_LOCATION }}${{ env.KEY_STORE_FILE }}
echo "KEY_STORE_PATH=$store_path" >> $GITHUB_ENV
- name: Create service_account.json
id: createServiceAccount
run: echo '${{ secrets.SERVICE_ACCOUNT_JSON }}' > service_account.json
# Build and sign APK ("-x test" argument is used to skip tests)
# add fdroid flavor for apk upload
- name: Build Fdroid Release APK
run: ./gradlew :app:assembleFdroidRelease -x test
# get fdroid flavor release apk path
- name: Get apk path
id: apk-path
run: echo "path=$(find . -regex '^.*/build/outputs/apk/fdroid/release/.*\.apk$' -type f | head -1)" >> $GITHUB_OUTPUT
- name: Get version code
run: |
version_code=$(grep "VERSION_CODE" buildSrc/src/main/kotlin/Constants.kt | awk '{print $5}' | tr -d '\n')
echo "VERSION_CODE=$version_code" >> $GITHUB_ENV
# Save the APK after the Build job is complete to publish it as a Github release in the next job
- name: Upload APK
uses: actions/upload-artifact@v4.3.1
with:
name: wgtunnel
path: ${{ steps.apk-path.outputs.path }}
- name: Download APK from build
uses: actions/download-artifact@v4
with:
name: wgtunnel
- name: Repository Dispatch for my F-Droid repo
uses: peter-evans/repository-dispatch@v3
with:
token: ${{ secrets.PAT }}
repository: zaneschepke/fdroid
event-type: fdroid-update
- name: Create Release with Fastlane changelog notes
id: create_release
uses: softprops/action-gh-release@v2
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
body_path: ${{ github.workspace }}/fastlane/metadata/android/en-US/changelogs/${{ env.VERSION_CODE }}.txt
tag_name: ${{ github.ref_name }}
name: ${{ github.ref_name }}
draft: false
prerelease: false
files: ${{ github.workspace }}/${{ steps.apk-path.outputs.path }}
- name: Install apksigner
run: |
sudo apt-get update
sudo apt-get install -y apksigner
- name: Get checksum
id: checksum
run: echo "checksum=$(apksigner verify -print-certs ${{ steps.apk-path.outputs.path }} | grep -Po "(?<=SHA-256 digest:) .*" | tr -d "[:blank:]")" >> $GITHUB_OUTPUT
- name: Append checksum
id: append_checksum
uses: softprops/action-gh-release@v2
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
body: |
SHA256 fingerprint:
```${{ steps.checksum.outputs.checksum }}```
tag_name: ${{ github.ref_name }}
name: ${{ github.ref_name }}
draft: false
prerelease: false
append_body: true
- name: Deploy with fastlane
uses: ruby/setup-ruby@v1
with:
ruby-version: '3.2' # Not needed with a .ruby-version file
bundler-cache: true
- name: Distribute app to Prod track 🚀
run: (cd ${{ github.workspace }} && bundle install && bundle exec fastlane production)
-2
View File
@@ -69,5 +69,3 @@ lint/tmp/
# App Specific cases
app/release/output.json
.idea/codeStyles/
# where we keep our signing secrets locally
app/signing.properties
-3
View File
@@ -1,3 +0,0 @@
source "https://rubygems.org"
gem "fastlane"
+24 -47
View File
@@ -2,84 +2,61 @@
WG Tunnel
</h1>
<div align="center">
<span align="center">
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
[![Discord Chat](https://img.shields.io/discord/1108285024631001111.svg)](https://discord.gg/rbRRNh6H7V)
</div>
</span>
<div align="center">
<span 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/)
</span>
</div>
<span align="left">
This is an alternative Android Application for [WireGuard](https://www.wireguard.com/) 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.
<div align="left">
</span>
This is an alternative Android Application for [WireGuard](https://www.wireguard.com/) 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.
</div>
<div align="center">
<span align="center">
## Screenshots
<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" />
<img label="Main" style="padding-right:25px" src="asset/main_screen.png" width="200" />
<img label="Config" style="padding-left:25px" src="./asset/config_screen.png" width="200" />
<img label="Settings" style="padding-left:25px" src="asset/settings_screen.png" width="200" />
<img label="Support" style="padding-left:25px" src="asset/support_screen.png" width="200" />
</p>
<div align="left">
<span 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.
The inspiration for this app came from the inconvenience of constantly having to turn VPN off and on while on different networks. With there being no free solution to this problem, this app was created to meet that need.
## Features
* 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
* WireGuard support for kernel and userspace modes
* Always-On VPN support
* Export tunnels to zip
* Quick tile support for tunnel toggling, auto-tunneling
* Static shortcuts support for tunnel toggling, auto-tunneling
* Intent automation support for all tunnels
* Automatic auto-tunneling service restart after reboot
* Automatic tunnel restart after reboot
* Battery preservation measures
* Restart tunnel on ping failure (beta)
* Add tunnels via .conf file
* Auto connect to VPN based on Wi-Fi SSID
* Split tunneling by application
* Always-on VPN for Android support
* Configurable Trusted Network list
* Optional auto connect on mobile data
* Automatic service restart after reboot
* Service will stay running in background after app has been closed
## Docs (WIP)
Basic documentation of the feature and behaviors of this app can be
found [here](https://zaneschepke.com/wgtunnel-docs/overview.html).
The repository for these docs can be found [here](https://github.com/zaneschepke/wgtunnel-docs).
## Building
```
$ git clone https://github.com/zaneschepke/wgtunnel
$ cd wgtunnel
```
And then build the app:
```
$ ./gradlew assembleDebug
$ ./gradlew assembleRelease
```
</span>
-5
View File
@@ -1,5 +0,0 @@
# Security Policy
## Reporting a Vulnerability
Please report security issues to `support@zaneschepke.com`
+87 -165
View File
@@ -1,211 +1,133 @@
import java.util.Properties
val rExtra = rootProject.extra
plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.android)
alias(libs.plugins.hilt.android)
alias(libs.plugins.kotlinxSerialization)
alias(libs.plugins.ksp)
id("com.android.application")
id("org.jetbrains.kotlin.android")
kotlin("kapt")
id("com.google.dagger.hilt.android")
id("com.google.gms.google-services")
id("com.google.firebase.crashlytics")
id("org.jetbrains.kotlin.plugin.serialization")
id("io.objectbox")
}
android {
namespace = Constants.APP_ID
compileSdk = Constants.TARGET_SDK
namespace = "com.zaneschepke.wireguardautotunnel"
compileSdk = 34
androidResources {
generateLocaleConfig = true
}
val versionMajor = 2
val versionMinor = 2
val versionPatch = 0
val versionBuild = 0
defaultConfig {
applicationId = Constants.APP_ID
minSdk = Constants.MIN_SDK
targetSdk = Constants.TARGET_SDK
versionCode = Constants.VERSION_CODE
versionName = Constants.VERSION_NAME
ksp { arg("room.schemaLocation", "$projectDir/schemas") }
sourceSets {
getByName("debug").assets.srcDirs(files("$projectDir/schemas")) // Room
}
applicationId = "com.zaneschepke.wireguardautotunnel"
minSdk = 29
targetSdk = 34
versionCode = versionMajor * 10000 + versionMinor * 1000 + versionPatch * 100 + versionBuild
versionName = "${versionMajor}.${versionMinor}.${versionPatch}"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables { useSupportLibrary = true }
}
signingConfigs {
create(Constants.RELEASE) {
val properties =
Properties().apply {
// created local file for signing details
try {
load(file("signing.properties").reader())
} catch (_: Exception) {
load(file("signing_template.properties").reader())
}
}
// try to get secrets from env first for pipeline build, then properties file for local
// build
storeFile =
file(
System.getenv()
.getOrDefault(
Constants.KEY_STORE_PATH_VAR,
properties.getProperty(Constants.KEY_STORE_PATH_VAR),
),
)
storePassword =
System.getenv()
.getOrDefault(
Constants.STORE_PASS_VAR,
properties.getProperty(Constants.STORE_PASS_VAR),
)
keyAlias =
System.getenv()
.getOrDefault(
Constants.KEY_ALIAS_VAR,
properties.getProperty(Constants.KEY_ALIAS_VAR),
)
keyPassword =
System.getenv()
.getOrDefault(
Constants.KEY_PASS_VAR,
properties.getProperty(Constants.KEY_PASS_VAR),
)
vectorDrawables {
useSupportLibrary = true
}
}
buildTypes {
// don't strip
packaging.jniLibs.keepDebugSymbols.addAll(
listOf("libwg-go.so", "libwg-quick.so", "libwg.so"),
)
applicationVariants.all {
val variant = this
variant.outputs
.map { it as com.android.build.gradle.internal.api.BaseVariantOutputImpl }
.forEach { output ->
val outputFileName =
"${Constants.APP_NAME}-${variant.flavorName}-${variant.buildType.name}-${variant.versionName}.apk"
output.outputFileName = outputFileName
}
}
release {
isDebuggable = false
isMinifyEnabled = true
isShrinkResources = true
isMinifyEnabled = false
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro",
"proguard-rules.pro"
)
signingConfig = signingConfigs.getByName(Constants.RELEASE)
}
debug { isDebuggable = true }
}
flavorDimensions.add(Constants.TYPE)
productFlavors {
create("fdroid") {
dimension = Constants.TYPE
proguardFile("fdroid-rules.pro")
}
create("general") {
dimension = Constants.TYPE
if (BuildHelper.isReleaseBuild(gradle) && BuildHelper.isGeneralFlavor(gradle)) {
//any plugins general specific
}
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
isCoreLibraryDesugaringEnabled = true
}
kotlinOptions { jvmTarget = Constants.JVM_TARGET }
kotlinOptions {
jvmTarget = "17"
}
buildFeatures {
compose = true
buildConfig = true
}
composeOptions { kotlinCompilerExtensionVersion = Constants.COMPOSE_COMPILER_EXTENSION_VERSION }
packaging { resources { excludes += "/META-INF/{AL2.0,LGPL2.1}" } }
composeOptions {
kotlinCompilerExtensionVersion = "1.4.8"
}
packaging {
resources {
excludes += "/META-INF/{AL2.0,LGPL2.1}"
}
}
}
val generalImplementation by configurations
dependencies {
implementation("androidx.core:core-ktx:1.10.1")
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.6.1")
implementation("androidx.activity:activity-compose:1.7.2")
implementation(platform("androidx.compose:compose-bom:2023.03.00"))
implementation("androidx.compose.ui:ui")
implementation("androidx.compose.ui:ui-graphics")
implementation("androidx.compose.ui:ui-tooling-preview")
implementation("androidx.compose.material3:material3:1.1.1")
implementation("androidx.appcompat:appcompat:1.6.1")
implementation(project(":logcatter"))
testImplementation("junit:junit:4.13.2")
androidTestImplementation("androidx.test.ext:junit:1.1.5")
androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")
androidTestImplementation(platform("androidx.compose:compose-bom:2023.03.00"))
androidTestImplementation("androidx.compose.ui:ui-test-junit4")
debugImplementation("androidx.compose.ui:ui-tooling")
debugImplementation("androidx.compose.ui:ui-test-manifest")
implementation(libs.androidx.core.ktx)
implementation(libs.androidx.lifecycle.runtime.ktx)
// helpers for implementing LifecycleOwner in a Service
implementation(libs.androidx.lifecycle.service)
implementation(libs.androidx.activity.compose)
implementation(platform(libs.androidx.compose.bom))
implementation(libs.androidx.compose.ui)
implementation(libs.androidx.compose.ui.graphics)
implementation(libs.androidx.compose.ui.tooling.preview)
implementation(libs.androidx.material3)
implementation(libs.androidx.appcompat)
// test
testImplementation(libs.junit)
testImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.espresso.core)
androidTestImplementation(platform(libs.androidx.compose.bom))
androidTestImplementation(libs.androidx.compose.ui.test)
androidTestImplementation(libs.androidx.room.testing)
debugImplementation(libs.androidx.compose.ui.tooling)
debugImplementation(libs.androidx.compose.manifest)
// get tunnel lib from github packages or mavenLocal
implementation(libs.tunnel)
coreLibraryDesugaring(libs.desugar.jdk.libs)
// logging
implementation(libs.timber)
//wireguard tunnel
implementation("com.wireguard.android:tunnel:1.0.20230427")
//logging
implementation("com.jakewharton.timber:timber:5.0.1")
// compose navigation
implementation(libs.androidx.navigation.compose)
implementation(libs.androidx.hilt.navigation.compose)
implementation("androidx.navigation:navigation-compose:2.6.0")
implementation("androidx.hilt:hilt-navigation-compose:1.0.0")
// hilt
implementation(libs.hilt.android)
ksp(libs.hilt.android.compiler)
implementation("com.google.dagger:hilt-android:${rExtra.get("hiltVersion")}")
kapt("com.google.dagger:hilt-android-compiler:${rExtra.get("hiltVersion")}")
// accompanist
implementation(libs.accompanist.permissions)
implementation(libs.accompanist.flowlayout)
implementation(libs.accompanist.drawablepainter)
//accompanist
implementation("com.google.accompanist:accompanist-systemuicontroller:${rExtra.get("accompanistVersion")}")
implementation("com.google.accompanist:accompanist-permissions:${rExtra.get("accompanistVersion")}")
implementation("com.google.accompanist:accompanist-flowlayout:${rExtra.get("accompanistVersion")}")
implementation("com.google.accompanist:accompanist-navigation-animation:${rExtra.get("accompanistVersion")}")
implementation("com.google.accompanist:accompanist-drawablepainter:${rExtra.get("accompanistVersion")}")
// storage
implementation(libs.androidx.room.runtime)
ksp(libs.androidx.room.compiler)
implementation(libs.androidx.room.ktx)
implementation(libs.androidx.datastore.preferences)
//db
implementation("io.objectbox:objectbox-kotlin:${rExtra.get("objectBoxVersion")}")
// lifecycle
implementation(libs.lifecycle.runtime.compose)
implementation(libs.androidx.lifecycle.runtime.ktx)
implementation(libs.androidx.lifecycle.process)
//lifecycle
implementation("androidx.lifecycle:lifecycle-runtime-compose:2.6.1")
// icons
implementation(libs.material.icons.extended)
// serialization
implementation(libs.kotlinx.serialization.json)
//icons
implementation("androidx.compose.material:material-icons-extended:1.4.3")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.1")
//firebase crashlytics
implementation(platform("com.google.firebase:firebase-bom:32.0.0"))
implementation("com.google.firebase:firebase-crashlytics-ktx")
implementation("com.google.firebase:firebase-analytics-ktx")
//barcode scanning
implementation("com.google.android.gms:play-services-code-scanner:16.0.0")
// barcode scanning
implementation(libs.zxing.android.embedded)
implementation(libs.zxing.core)
// bio
implementation(libs.androidx.biometric.ktx)
implementation(libs.pin.lock.compose)
// shortcuts
implementation(libs.androidx.core)
implementation(libs.androidx.core.google.shortcuts)
}
kapt {
correctErrorTypes = true
}
-5
View File
@@ -1,5 +0,0 @@
-dontwarn com.google.errorprone.annotations.**
-keepclassmembers class * extends androidx.datastore.preferences.protobuf.GeneratedMessageLite {
<fields>;
}
+39
View File
@@ -0,0 +1,39 @@
{
"project_info": {
"project_number": "328300975830",
"project_id": "wireguard-auto-tunnel",
"storage_bucket": "wireguard-auto-tunnel.appspot.com"
},
"client": [
{
"client_info": {
"mobilesdk_app_id": "1:328300975830:android:82cd774598ccb7234b1b77",
"android_client_info": {
"package_name": "com.zaneschepke.wireguardautotunnel"
}
},
"oauth_client": [
{
"client_id": "328300975830-m72lc3hr69ddhdqh9ngr27rvc8o0jb2d.apps.googleusercontent.com",
"client_type": 3
}
],
"api_key": [
{
"current_key": "AIzaSyBsSMY0LlckizXDnuYBy7nXWGSdl8zZedI"
}
],
"services": {
"appinvite_service": {
"other_platform_oauth_client": [
{
"client_id": "328300975830-m72lc3hr69ddhdqh9ngr27rvc8o0jb2d.apps.googleusercontent.com",
"client_type": 3
}
]
}
}
}
],
"configuration_version": "1"
}
+99
View File
@@ -0,0 +1,99 @@
{
"_note1": "KEEP THIS FILE! Check it into a version control system (VCS) like git.",
"_note2": "ObjectBox manages crucial IDs for your object model. See docs for details.",
"_note3": "If you have VCS merge conflicts, you must resolve them according to ObjectBox docs.",
"entities": [
{
"id": "1:2692736974585027589",
"lastPropertyId": "15:5057486545428188436",
"name": "TunnelConfig",
"properties": [
{
"id": "1:1985347930017457084",
"name": "id",
"type": 6,
"flags": 1
},
{
"id": "12:2409068226744965585",
"name": "name",
"indexId": "1:4811206443952699137",
"type": 9,
"flags": 34848
},
{
"id": "13:8987443291286312275",
"name": "wgQuick",
"type": 9
}
],
"relations": []
},
{
"id": "2:8887605597748372702",
"lastPropertyId": "9:4468844863383145378",
"name": "Settings",
"properties": [
{
"id": "1:7485739868216068651",
"name": "id",
"type": 6,
"flags": 1
},
{
"id": "2:5814013113141456749",
"name": "isAutoTunnelEnabled",
"type": 1
},
{
"id": "4:5645665441196906014",
"name": "trustedNetworkSSIDs",
"type": 30
},
{
"id": "5:4989886999117763881",
"name": "isTunnelOnMobileDataEnabled",
"type": 1
},
{
"id": "6:3370284381040192129",
"name": "defaultTunnel",
"type": 9
},
{
"id": "9:4468844863383145378",
"name": "isAlwaysOnVpnEnabled",
"type": 1
}
],
"relations": []
}
],
"lastEntityId": "2:8887605597748372702",
"lastIndexId": "1:4811206443952699137",
"lastRelationId": "0:0",
"lastSequenceId": "0:0",
"modelVersion": 5,
"modelVersionParserMinimum": 5,
"retiredEntityUids": [],
"retiredIndexUids": [],
"retiredPropertyUids": [
1763475292291320186,
6483820955437198310,
8323071516033820771,
5904440563612311217,
1408037976996390989,
7737847485212546994,
8215616901775229364,
8021610768066328637,
6174306582797008721,
2175939938544485767,
7555225587864607050,
969146862000617878,
5057486545428188436,
2814640993034665120,
4981008812459251156
],
"retiredRelationUids": [],
"version": 1
}
+94
View File
@@ -0,0 +1,94 @@
{
"_note1": "KEEP THIS FILE! Check it into a version control system (VCS) like git.",
"_note2": "ObjectBox manages crucial IDs for your object model. See docs for details.",
"_note3": "If you have VCS merge conflicts, you must resolve them according to ObjectBox docs.",
"entities": [
{
"id": "1:2692736974585027589",
"lastPropertyId": "15:5057486545428188436",
"name": "TunnelConfig",
"properties": [
{
"id": "1:1985347930017457084",
"name": "id",
"type": 6,
"flags": 1
},
{
"id": "12:2409068226744965585",
"name": "name",
"indexId": "1:4811206443952699137",
"type": 9,
"flags": 34848
},
{
"id": "13:8987443291286312275",
"name": "wgQuick",
"type": 9
}
],
"relations": []
},
{
"id": "2:8887605597748372702",
"lastPropertyId": "8:4981008812459251156",
"name": "Settings",
"properties": [
{
"id": "1:7485739868216068651",
"name": "id",
"type": 6,
"flags": 1
},
{
"id": "2:5814013113141456749",
"name": "isAutoTunnelEnabled",
"type": 1
},
{
"id": "4:5645665441196906014",
"name": "trustedNetworkSSIDs",
"type": 30
},
{
"id": "5:4989886999117763881",
"name": "isTunnelOnMobileDataEnabled",
"type": 1
},
{
"id": "6:3370284381040192129",
"name": "defaultTunnel",
"type": 9
}
],
"relations": []
}
],
"lastEntityId": "2:8887605597748372702",
"lastIndexId": "1:4811206443952699137",
"lastRelationId": "0:0",
"lastSequenceId": "0:0",
"modelVersion": 5,
"modelVersionParserMinimum": 5,
"retiredEntityUids": [],
"retiredIndexUids": [],
"retiredPropertyUids": [
1763475292291320186,
6483820955437198310,
8323071516033820771,
5904440563612311217,
1408037976996390989,
7737847485212546994,
8215616901775229364,
8021610768066328637,
6174306582797008721,
2175939938544485767,
7555225587864607050,
969146862000617878,
5057486545428188436,
2814640993034665120,
4981008812459251156
],
"retiredRelationUids": [],
"version": 1
}
+1 -6
View File
@@ -18,9 +18,4 @@
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
-keepclassmembers class * extends androidx.datastore.preferences.protobuf.GeneratedMessageLite {
<fields>;
}
#-renamesourcefileattribute SourceFile
@@ -1,112 +0,0 @@
{
"formatVersion": 1,
"database": {
"version": 1,
"identityHash": "ba86153e6fb0b823197b987239b03e64",
"entities": [
{
"tableName": "Settings",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_tunnel_enabled` INTEGER NOT NULL, `is_tunnel_on_mobile_data_enabled` INTEGER NOT NULL, `trusted_network_ssids` TEXT NOT NULL, `default_tunnel` TEXT, `is_always_on_vpn_enabled` INTEGER NOT NULL, `is_tunnel_on_ethernet_enabled` INTEGER NOT NULL)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isAutoTunnelEnabled",
"columnName": "is_tunnel_enabled",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isTunnelOnMobileDataEnabled",
"columnName": "is_tunnel_on_mobile_data_enabled",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "trustedNetworkSSIDs",
"columnName": "trusted_network_ssids",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "defaultTunnel",
"columnName": "default_tunnel",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "isAlwaysOnVpnEnabled",
"columnName": "is_always_on_vpn_enabled",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isTunnelOnEthernetEnabled",
"columnName": "is_tunnel_on_ethernet_enabled",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "TunnelConfig",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `wg_quick` TEXT NOT NULL)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "wgQuick",
"columnName": "wg_quick",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
},
"indices": [
{
"name": "index_TunnelConfig_name",
"unique": true,
"columnNames": [
"name"
],
"orders": [],
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_TunnelConfig_name` ON `${TABLE_NAME}` (`name`)"
}
],
"foreignKeys": []
}
],
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'ba86153e6fb0b823197b987239b03e64')"
]
}
}
@@ -1,126 +0,0 @@
{
"formatVersion": 1,
"database": {
"version": 2,
"identityHash": "65b1c9efff61712231fa64d1f19f3915",
"entities": [
{
"tableName": "Settings",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_tunnel_enabled` INTEGER NOT NULL, `is_tunnel_on_mobile_data_enabled` INTEGER NOT NULL, `trusted_network_ssids` TEXT NOT NULL, `default_tunnel` TEXT, `is_always_on_vpn_enabled` INTEGER NOT NULL, `is_tunnel_on_ethernet_enabled` INTEGER NOT NULL, `is_shortcuts_enabled` INTEGER NOT NULL DEFAULT false, `is_battery_saver_enabled` INTEGER NOT NULL DEFAULT false)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isAutoTunnelEnabled",
"columnName": "is_tunnel_enabled",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isTunnelOnMobileDataEnabled",
"columnName": "is_tunnel_on_mobile_data_enabled",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "trustedNetworkSSIDs",
"columnName": "trusted_network_ssids",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "defaultTunnel",
"columnName": "default_tunnel",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "isAlwaysOnVpnEnabled",
"columnName": "is_always_on_vpn_enabled",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isTunnelOnEthernetEnabled",
"columnName": "is_tunnel_on_ethernet_enabled",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isShortcutsEnabled",
"columnName": "is_shortcuts_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isBatterySaverEnabled",
"columnName": "is_battery_saver_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "TunnelConfig",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `wg_quick` TEXT NOT NULL)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "wgQuick",
"columnName": "wg_quick",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
},
"indices": [
{
"name": "index_TunnelConfig_name",
"unique": true,
"columnNames": [
"name"
],
"orders": [],
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_TunnelConfig_name` ON `${TABLE_NAME}` (`name`)"
}
],
"foreignKeys": []
}
],
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '65b1c9efff61712231fa64d1f19f3915')"
]
}
}
@@ -1,133 +0,0 @@
{
"formatVersion": 1,
"database": {
"version": 3,
"identityHash": "6b30daba29bb95f8ddc4d26206329d4f",
"entities": [
{
"tableName": "Settings",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_tunnel_enabled` INTEGER NOT NULL, `is_tunnel_on_mobile_data_enabled` INTEGER NOT NULL, `trusted_network_ssids` TEXT NOT NULL, `default_tunnel` TEXT, `is_always_on_vpn_enabled` INTEGER NOT NULL, `is_tunnel_on_ethernet_enabled` INTEGER NOT NULL, `is_shortcuts_enabled` INTEGER NOT NULL DEFAULT false, `is_battery_saver_enabled` INTEGER NOT NULL DEFAULT false, `is_tunnel_on_wifi_enabled` INTEGER NOT NULL DEFAULT false)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isAutoTunnelEnabled",
"columnName": "is_tunnel_enabled",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isTunnelOnMobileDataEnabled",
"columnName": "is_tunnel_on_mobile_data_enabled",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "trustedNetworkSSIDs",
"columnName": "trusted_network_ssids",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "defaultTunnel",
"columnName": "default_tunnel",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "isAlwaysOnVpnEnabled",
"columnName": "is_always_on_vpn_enabled",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isTunnelOnEthernetEnabled",
"columnName": "is_tunnel_on_ethernet_enabled",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isShortcutsEnabled",
"columnName": "is_shortcuts_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isBatterySaverEnabled",
"columnName": "is_battery_saver_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isTunnelOnWifiEnabled",
"columnName": "is_tunnel_on_wifi_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "TunnelConfig",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `wg_quick` TEXT NOT NULL)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "wgQuick",
"columnName": "wg_quick",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
},
"indices": [
{
"name": "index_TunnelConfig_name",
"unique": true,
"columnNames": [
"name"
],
"orders": [],
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_TunnelConfig_name` ON `${TABLE_NAME}` (`name`)"
}
],
"foreignKeys": []
}
],
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '6b30daba29bb95f8ddc4d26206329d4f')"
]
}
}
@@ -1,154 +0,0 @@
{
"formatVersion": 1,
"database": {
"version": 4,
"identityHash": "aee55639422df8dadfe74c3bad204477",
"entities": [
{
"tableName": "Settings",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_tunnel_enabled` INTEGER NOT NULL, `is_tunnel_on_mobile_data_enabled` INTEGER NOT NULL, `trusted_network_ssids` TEXT NOT NULL, `default_tunnel` TEXT, `is_always_on_vpn_enabled` INTEGER NOT NULL, `is_tunnel_on_ethernet_enabled` INTEGER NOT NULL, `is_shortcuts_enabled` INTEGER NOT NULL DEFAULT false, `is_battery_saver_enabled` INTEGER NOT NULL DEFAULT false, `is_tunnel_on_wifi_enabled` INTEGER NOT NULL DEFAULT false, `is_kernel_enabled` INTEGER NOT NULL DEFAULT false, `is_restore_on_boot_enabled` INTEGER NOT NULL DEFAULT false, `is_multi_tunnel_enabled` INTEGER NOT NULL DEFAULT false)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isAutoTunnelEnabled",
"columnName": "is_tunnel_enabled",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isTunnelOnMobileDataEnabled",
"columnName": "is_tunnel_on_mobile_data_enabled",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "trustedNetworkSSIDs",
"columnName": "trusted_network_ssids",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "defaultTunnel",
"columnName": "default_tunnel",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "isAlwaysOnVpnEnabled",
"columnName": "is_always_on_vpn_enabled",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isTunnelOnEthernetEnabled",
"columnName": "is_tunnel_on_ethernet_enabled",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isShortcutsEnabled",
"columnName": "is_shortcuts_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isBatterySaverEnabled",
"columnName": "is_battery_saver_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isTunnelOnWifiEnabled",
"columnName": "is_tunnel_on_wifi_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isKernelEnabled",
"columnName": "is_kernel_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isRestoreOnBootEnabled",
"columnName": "is_restore_on_boot_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isMultiTunnelEnabled",
"columnName": "is_multi_tunnel_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "TunnelConfig",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `wg_quick` TEXT NOT NULL)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "wgQuick",
"columnName": "wg_quick",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
},
"indices": [
{
"name": "index_TunnelConfig_name",
"unique": true,
"columnNames": [
"name"
],
"orders": [],
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_TunnelConfig_name` ON `${TABLE_NAME}` (`name`)"
}
],
"foreignKeys": []
}
],
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'aee55639422df8dadfe74c3bad204477')"
]
}
}
@@ -1,161 +0,0 @@
{
"formatVersion": 1,
"database": {
"version": 5,
"identityHash": "bc15003a44746e18b9c260ec49737089",
"entities": [
{
"tableName": "Settings",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_tunnel_enabled` INTEGER NOT NULL, `is_tunnel_on_mobile_data_enabled` INTEGER NOT NULL, `trusted_network_ssids` TEXT NOT NULL, `default_tunnel` TEXT, `is_always_on_vpn_enabled` INTEGER NOT NULL, `is_tunnel_on_ethernet_enabled` INTEGER NOT NULL, `is_shortcuts_enabled` INTEGER NOT NULL DEFAULT false, `is_battery_saver_enabled` INTEGER NOT NULL DEFAULT false, `is_tunnel_on_wifi_enabled` INTEGER NOT NULL DEFAULT false, `is_kernel_enabled` INTEGER NOT NULL DEFAULT false, `is_restore_on_boot_enabled` INTEGER NOT NULL DEFAULT false, `is_multi_tunnel_enabled` INTEGER NOT NULL DEFAULT false, `is_auto_tunnel_paused` INTEGER NOT NULL DEFAULT false)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isAutoTunnelEnabled",
"columnName": "is_tunnel_enabled",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isTunnelOnMobileDataEnabled",
"columnName": "is_tunnel_on_mobile_data_enabled",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "trustedNetworkSSIDs",
"columnName": "trusted_network_ssids",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "defaultTunnel",
"columnName": "default_tunnel",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "isAlwaysOnVpnEnabled",
"columnName": "is_always_on_vpn_enabled",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isTunnelOnEthernetEnabled",
"columnName": "is_tunnel_on_ethernet_enabled",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isShortcutsEnabled",
"columnName": "is_shortcuts_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isBatterySaverEnabled",
"columnName": "is_battery_saver_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isTunnelOnWifiEnabled",
"columnName": "is_tunnel_on_wifi_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isKernelEnabled",
"columnName": "is_kernel_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isRestoreOnBootEnabled",
"columnName": "is_restore_on_boot_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isMultiTunnelEnabled",
"columnName": "is_multi_tunnel_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isAutoTunnelPaused",
"columnName": "is_auto_tunnel_paused",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "TunnelConfig",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `wg_quick` TEXT NOT NULL)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "wgQuick",
"columnName": "wg_quick",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
},
"indices": [
{
"name": "index_TunnelConfig_name",
"unique": true,
"columnNames": [
"name"
],
"orders": [],
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_TunnelConfig_name` ON `${TABLE_NAME}` (`name`)"
}
],
"foreignKeys": []
}
],
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'bc15003a44746e18b9c260ec49737089')"
]
}
}
@@ -1,168 +0,0 @@
{
"formatVersion": 1,
"database": {
"version": 6,
"identityHash": "625820076477aca948536f7bccccc7ca",
"entities": [
{
"tableName": "Settings",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_tunnel_enabled` INTEGER NOT NULL, `is_tunnel_on_mobile_data_enabled` INTEGER NOT NULL, `trusted_network_ssids` TEXT NOT NULL, `default_tunnel` TEXT, `is_always_on_vpn_enabled` INTEGER NOT NULL, `is_tunnel_on_ethernet_enabled` INTEGER NOT NULL, `is_shortcuts_enabled` INTEGER NOT NULL DEFAULT false, `is_battery_saver_enabled` INTEGER NOT NULL DEFAULT false, `is_tunnel_on_wifi_enabled` INTEGER NOT NULL DEFAULT false, `is_kernel_enabled` INTEGER NOT NULL DEFAULT false, `is_restore_on_boot_enabled` INTEGER NOT NULL DEFAULT false, `is_multi_tunnel_enabled` INTEGER NOT NULL DEFAULT false, `is_auto_tunnel_paused` INTEGER NOT NULL DEFAULT false, `is_ping_enabled` INTEGER NOT NULL DEFAULT false)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isAutoTunnelEnabled",
"columnName": "is_tunnel_enabled",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isTunnelOnMobileDataEnabled",
"columnName": "is_tunnel_on_mobile_data_enabled",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "trustedNetworkSSIDs",
"columnName": "trusted_network_ssids",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "defaultTunnel",
"columnName": "default_tunnel",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "isAlwaysOnVpnEnabled",
"columnName": "is_always_on_vpn_enabled",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isTunnelOnEthernetEnabled",
"columnName": "is_tunnel_on_ethernet_enabled",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isShortcutsEnabled",
"columnName": "is_shortcuts_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isBatterySaverEnabled",
"columnName": "is_battery_saver_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isTunnelOnWifiEnabled",
"columnName": "is_tunnel_on_wifi_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isKernelEnabled",
"columnName": "is_kernel_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isRestoreOnBootEnabled",
"columnName": "is_restore_on_boot_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isMultiTunnelEnabled",
"columnName": "is_multi_tunnel_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isAutoTunnelPaused",
"columnName": "is_auto_tunnel_paused",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isPingEnabled",
"columnName": "is_ping_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "TunnelConfig",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `wg_quick` TEXT NOT NULL)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "wgQuick",
"columnName": "wg_quick",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
},
"indices": [
{
"name": "index_TunnelConfig_name",
"unique": true,
"columnNames": [
"name"
],
"orders": [],
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_TunnelConfig_name` ON `${TABLE_NAME}` (`name`)"
}
],
"foreignKeys": []
}
],
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '625820076477aca948536f7bccccc7ca')"
]
}
}
@@ -1,176 +0,0 @@
{
"formatVersion": 1,
"database": {
"version": 7,
"identityHash": "e65e4e7cf01f50fb03196d47b54288b1",
"entities": [
{
"tableName": "Settings",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_tunnel_enabled` INTEGER NOT NULL, `is_tunnel_on_mobile_data_enabled` INTEGER NOT NULL, `trusted_network_ssids` TEXT NOT NULL, `is_always_on_vpn_enabled` INTEGER NOT NULL, `is_tunnel_on_ethernet_enabled` INTEGER NOT NULL, `is_shortcuts_enabled` INTEGER NOT NULL DEFAULT false, `is_tunnel_on_wifi_enabled` INTEGER NOT NULL DEFAULT false, `is_kernel_enabled` INTEGER NOT NULL DEFAULT false, `is_restore_on_boot_enabled` INTEGER NOT NULL DEFAULT false, `is_multi_tunnel_enabled` INTEGER NOT NULL DEFAULT false, `is_auto_tunnel_paused` INTEGER NOT NULL DEFAULT false, `is_ping_enabled` INTEGER NOT NULL DEFAULT false)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isAutoTunnelEnabled",
"columnName": "is_tunnel_enabled",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isTunnelOnMobileDataEnabled",
"columnName": "is_tunnel_on_mobile_data_enabled",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "trustedNetworkSSIDs",
"columnName": "trusted_network_ssids",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "isAlwaysOnVpnEnabled",
"columnName": "is_always_on_vpn_enabled",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isTunnelOnEthernetEnabled",
"columnName": "is_tunnel_on_ethernet_enabled",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isShortcutsEnabled",
"columnName": "is_shortcuts_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isTunnelOnWifiEnabled",
"columnName": "is_tunnel_on_wifi_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isKernelEnabled",
"columnName": "is_kernel_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isRestoreOnBootEnabled",
"columnName": "is_restore_on_boot_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isMultiTunnelEnabled",
"columnName": "is_multi_tunnel_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isAutoTunnelPaused",
"columnName": "is_auto_tunnel_paused",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isPingEnabled",
"columnName": "is_ping_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "TunnelConfig",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `wg_quick` TEXT NOT NULL, `tunnel_networks` TEXT NOT NULL DEFAULT '', `is_mobile_data_tunnel` INTEGER NOT NULL DEFAULT false, `is_primary_tunnel` INTEGER NOT NULL DEFAULT false)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "wgQuick",
"columnName": "wg_quick",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "tunnelNetworks",
"columnName": "tunnel_networks",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "''"
},
{
"fieldPath": "isMobileDataTunnel",
"columnName": "is_mobile_data_tunnel",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isPrimaryTunnel",
"columnName": "is_primary_tunnel",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
},
"indices": [
{
"name": "index_TunnelConfig_name",
"unique": true,
"columnNames": [
"name"
],
"orders": [],
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_TunnelConfig_name` ON `${TABLE_NAME}` (`name`)"
}
],
"foreignKeys": []
}
],
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'e65e4e7cf01f50fb03196d47b54288b1')"
]
}
}
-4
View File
@@ -1,4 +0,0 @@
SIGNING_STORE_PASSWORD=
SIGNING_KEY_ALIAS=
SIGNING_KEY_PASSWORD=
KEY_STORE_PATH=/
@@ -1,11 +1,13 @@
package com.zaneschepke.wireguardautotunnel
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import org.junit.Assert.assertEquals
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.Assert.*
/**
* Instrumented test, which will execute on an Android device.
*
@@ -19,4 +21,4 @@ class ExampleInstrumentedTest {
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
assertEquals("com.zaneschepke.wireguardautotunnel", appContext.packageName)
}
}
}
@@ -1,44 +0,0 @@
package com.zaneschepke.wireguardautotunnel
import androidx.room.testing.MigrationTestHelper
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import com.zaneschepke.wireguardautotunnel.data.AppDatabase
import com.zaneschepke.wireguardautotunnel.data.Queries
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import java.io.IOException
@RunWith(AndroidJUnit4::class)
class MigrationTest {
private val dbName = "migration-test"
@get:Rule
val helper: MigrationTestHelper =
MigrationTestHelper(
InstrumentationRegistry.getInstrumentation(),
AppDatabase::class.java,
)
@Test
@Throws(IOException::class)
fun migrate6To7() {
helper.createDatabase(dbName, 6).apply {
// Database has schema version 1. Insert some data using SQL queries.
// You can't use DAO classes because they expect the latest schema.
execSQL(Queries.createDefaultSettings())
execSQL(
Queries.createTunnelConfig(),
)
// Prepare for the next version.
close()
}
// Re-open the database with version 2 and provide
// MIGRATION_1_2 as the migration process.
helper.runMigrationsAndValidate(dbName, 7, true)
// MigrationTestHelper automatically verifies the schema changes,
// but you need to validate that the data was migrated properly.
}
}
+36 -114
View File
@@ -1,176 +1,98 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission
android:name="android.permission.READ_EXTERNAL_STORAGE"
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"
android:maxSdkVersion="32" />
<uses-permission
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="32"
tools:ignore="ScopedStorage" />
<uses-permission
android:name="android.permission.ACCESS_WIFI_STATE"
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE"
android:maxSdkVersion="30"
tools:ignore="LeanbackUsesWifi" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
<!--foreground service exempt android 14-->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SYSTEM_EXEMPTED" />
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE"/>
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_LOCATION"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_REMOTE_MESSAGING"/>
<!--foreground service permissions-->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
<!--start service on boot permission-->
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
<!--android tv support-->
<uses-feature
android:name="android.software.leanback"
<uses-feature android:name="android.software.leanback"
android:required="false" />
<uses-feature
android:name="android.hardware.touchscreen"
<uses-feature android:name="android.hardware.touchscreen"
android:required="false" />
<uses-feature
android:name="android.hardware.location.gps"
android:required="false" />
<uses-feature
android:name="android.hardware.screen.portrait"
android:required="false" />
<queries>
<intent>
<action android:name="android.intent.action.MAIN" />
</intent>
</queries>
<application
android:name=".WireGuardAutoTunnel"
android:allowBackup="true"
android:banner="@drawable/ic_banner"
android:name=".WireGuardAutoTunnel"
android:dataExtractionRules="@xml/data_extraction_rules"
android:enableOnBackInvokedCallback="true"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:banner="@mipmap/ic_launcher_foreground"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.WireguardAutoTunnel"
tools:targetApi="tiramisu">
tools:targetApi="31">
<activity
android:name=".ui.MainActivity"
android:exported="true"
android:theme="@style/Theme.WireguardAutoTunnel">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<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=".ui.CaptureActivityPortrait"
android:screenOrientation="fullSensor"
android:stateNotNeeded="true"
android:theme="@style/zxing_CaptureTheme"
android:windowSoftInputMode="stateAlwaysHidden"
tools:ignore="DiscouragedApi" />
<activity
android:name=".service.shortcut.ShortcutsActivity"
android:enabled="true"
android:exported="true"
android:finishOnTaskLaunch="true"
android:theme="@android:style/Theme.NoDisplay" />
<service
android:name=".service.foreground.ForegroundService"
android:enabled="true"
android:exported="false"
android:foregroundServiceType="systemExempted"
tools:node="merge" />
<service
android:name=".service.tile.TunnelControlTile"
android:exported="true"
android:icon="@drawable/ic_launcher"
android:label="Tunnel control"
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE">
<meta-data
android:name="android.service.quicksettings.ACTIVE_TILE"
android:value="true" />
<meta-data
android:name="android.service.quicksettings.TOGGLEABLE_TILE"
android:value="true" />
<intent-filter>
<action android:name="android.service.quicksettings.action.QS_TILE" />
</intent-filter>
</service>
<service
android:name=".service.tile.AutoTunnelControlTile"
android:exported="true"
android:icon="@drawable/ic_launcher"
android:label="Auto-tunnel"
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE">
<meta-data
android:name="android.service.quicksettings.ACTIVE_TILE"
android:value="true" />
<meta-data
android:name="android.service.quicksettings.TOGGLEABLE_TILE"
android:value="true" />
<intent-filter>
<action android:name="android.service.quicksettings.action.QS_TILE" />
</intent-filter>
android:foregroundServiceType="remoteMessaging"
android:exported="false">
</service>
<service
android:name=".service.foreground.WireGuardTunnelService"
android:enabled="true"
android:exported="false"
android:foregroundServiceType="systemExempted"
android:permission="android.permission.BIND_VPN_SERVICE"
android:persistent="true"
tools:node="merge">
android:enabled="true"
android:foregroundServiceType="remoteMessaging"
android:exported="false">
<intent-filter>
<action android:name="android.net.VpnService" />
<action android:name="android.net.VpnService"/>
</intent-filter>
<meta-data
android:name="android.net.VpnService.SUPPORTS_ALWAYS_ON"
android:value="true" />
<meta-data android:name="android.net.VpnService.SUPPORTS_ALWAYS_ON"
android:value="true"/>
</service>
<service
android:name=".service.foreground.WireGuardConnectivityWatcherService"
android:enabled="true"
android:exported="false"
android:foregroundServiceType="systemExempted"
android:persistent="true"
android:stopWithTask="false"
tools:node="merge" />
<receiver
android:name=".receiver.BootReceiver"
android:enabled="true"
android:foregroundServiceType="location"
android:permission=""
android:exported="false">
</service>
<receiver android:enabled="true" android:name=".receiver.BootReceiver"
android:exported="true">
<intent-filter>
<category android:name="android.intent.category.DEFAULT" />
<action android:name="android.intent.action.BOOT_COMPLETED" />
<action android:name="android.intent.action.ACTION_BOOT_COMPLETED" />
<action android:name="android.intent.action.QUICKBOOT_POWERON" />
<action android:name="com.htc.intent.action.QUICKBOOT_POWERON" />
<action android:name="android.intent.action.BOOT_COMPLETED"/>
</intent-filter>
</receiver>
<receiver
android:name=".receiver.NotificationActionReceiver"
android:exported="false" />
<receiver android:exported="false" android:name=".receiver.NotificationActionReceiver"/>
<meta-data
android:name="com.google.mlkit.vision.DEPENDENCIES"
android:value="barcode_ui"/>
<meta-data
android:name="firebase_crashlytics_collection_enabled"
android:value="true" />
</application>
</manifest>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 38 KiB

@@ -1,46 +1,25 @@
package com.zaneschepke.wireguardautotunnel
import android.app.Application
import android.content.ComponentName
import android.content.Context
import android.content.pm.PackageManager
import android.service.quicksettings.TileService
import com.zaneschepke.wireguardautotunnel.service.tile.AutoTunnelControlTile
import com.zaneschepke.wireguardautotunnel.service.tile.TunnelControlTile
import com.zaneschepke.wireguardautotunnel.util.ReleaseTree
import com.google.firebase.crashlytics.FirebaseCrashlytics
import com.zaneschepke.wireguardautotunnel.repository.Repository
import com.zaneschepke.wireguardautotunnel.service.tunnel.model.Settings
import dagger.hilt.android.HiltAndroidApp
import timber.log.Timber
import xyz.teamgravity.pin_lock_compose.PinManager
import javax.inject.Inject
@HiltAndroidApp
class WireGuardAutoTunnel : Application() {
@Inject
lateinit var settingsRepo : Repository<Settings>
override fun onCreate() {
super.onCreate()
instance = this
if (BuildConfig.DEBUG) Timber.plant(Timber.DebugTree()) else Timber.plant(ReleaseTree())
PinManager.initialize(this)
if(BuildConfig.DEBUG) {
FirebaseCrashlytics.getInstance().setCrashlyticsCollectionEnabled(false);
Timber.plant(Timber.DebugTree())
}
settingsRepo.init()
}
companion object {
lateinit var instance: WireGuardAutoTunnel
private set
fun isRunningOnAndroidTv(): Boolean {
return instance.packageManager.hasSystemFeature(PackageManager.FEATURE_LEANBACK)
}
fun requestTunnelTileServiceStateUpdate(context: Context) {
TileService.requestListeningState(
context,
ComponentName(instance, TunnelControlTile::class.java),
)
}
fun requestAutoTunnelTileServiceUpdate(context: Context) {
TileService.requestListeningState(
context,
ComponentName(instance, AutoTunnelControlTile::class.java),
)
}
}
}
}
@@ -1,54 +0,0 @@
package com.zaneschepke.wireguardautotunnel.data
import androidx.room.AutoMigration
import androidx.room.Database
import androidx.room.DeleteColumn
import androidx.room.RoomDatabase
import androidx.room.TypeConverters
import androidx.room.migration.AutoMigrationSpec
import com.zaneschepke.wireguardautotunnel.data.model.Settings
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
@Database(
entities = [Settings::class, TunnelConfig::class],
version = 7,
autoMigrations =
[
AutoMigration(from = 1, to = 2),
AutoMigration(from = 2, to = 3),
AutoMigration(
from = 3,
to = 4,
),
AutoMigration(
from = 4,
to = 5,
),
AutoMigration(
from = 5,
to = 6,
),
AutoMigration(
from = 6,
to = 7,
spec = RemoveLegacySettingColumnsMigration::class,
),
],
exportSchema = true,
)
@TypeConverters(DatabaseListConverters::class)
abstract class AppDatabase : RoomDatabase() {
abstract fun settingDao(): SettingsDao
abstract fun tunnelConfigDoa(): TunnelConfigDao
}
@DeleteColumn(
tableName = "Settings",
columnName = "default_tunnel",
)
@DeleteColumn(
tableName = "Settings",
columnName = "is_battery_saver_enabled",
)
class RemoveLegacySettingColumnsMigration : AutoMigrationSpec
@@ -1,21 +0,0 @@
package com.zaneschepke.wireguardautotunnel.data
import androidx.room.RoomDatabase
import androidx.sqlite.db.SupportSQLiteDatabase
import timber.log.Timber
class DatabaseCallback : RoomDatabase.Callback() {
override fun onCreate(db: SupportSQLiteDatabase) = db.run {
// Notice non-ui thread is here
beginTransaction()
try {
execSQL(Queries.createDefaultSettings())
Timber.i("Bootstrapping settings data")
setTransactionSuccessful()
} catch (e: Exception) {
Timber.e(e)
} finally {
endTransaction()
}
}
}
@@ -1,24 +0,0 @@
package com.zaneschepke.wireguardautotunnel.data
import androidx.room.TypeConverter
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
class DatabaseListConverters {
@TypeConverter
fun listToString(value: MutableList<String>): String {
return Json.encodeToString(value)
}
@TypeConverter
fun stringToList(value: String): MutableList<String> {
if (value.isBlank() || value.isEmpty()) return mutableListOf()
return try {
Json.decodeFromString<MutableList<String>>(value)
} catch (e: Exception) {
val list = value.split(",").toMutableList()
val json = listToString(list)
Json.decodeFromString<MutableList<String>>(json)
}
}
}
@@ -1,35 +0,0 @@
package com.zaneschepke.wireguardautotunnel.data
object Queries {
fun createDefaultSettings(): String {
return """
INSERT INTO Settings (is_tunnel_enabled,
is_tunnel_on_mobile_data_enabled,
trusted_network_ssids,
is_always_on_vpn_enabled,
is_tunnel_on_ethernet_enabled,
is_shortcuts_enabled,
is_tunnel_on_wifi_enabled,
is_kernel_enabled,
is_restore_on_boot_enabled,
is_multi_tunnel_enabled)
VALUES
('false',
'false',
'sampleSSID1,sampleSSID2',
'false',
'false',
'false',
'false',
'false',
'false',
'false')
""".trimIndent()
}
fun createTunnelConfig(): String {
return """
INSERT INTO TunnelConfig (name, wg_quick) VALUES ('test', 'test')
""".trimIndent()
}
}
@@ -1,36 +0,0 @@
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 kotlinx.coroutines.flow.Flow
@Dao
interface SettingsDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun save(t: Settings)
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun saveAll(t: List<Settings>)
@Query("SELECT * FROM settings WHERE id=:id")
suspend fun getById(id: Long): Settings?
@Query("SELECT * FROM settings")
suspend fun getAll(): List<Settings>
@Query("SELECT * FROM settings LIMIT 1")
fun getSettingsFlow(): Flow<Settings>
@Query("SELECT * FROM settings")
fun getAllFlow(): Flow<MutableList<Settings>>
@Delete
suspend fun delete(t: Settings)
@Query("SELECT COUNT('id') FROM settings")
suspend fun count(): Long
}
@@ -1,49 +0,0 @@
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.util.TunnelConfigs
import kotlinx.coroutines.flow.Flow
@Dao
interface TunnelConfigDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun save(t: TunnelConfig)
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun saveAll(t: TunnelConfigs)
@Query("SELECT * FROM TunnelConfig WHERE id=:id")
suspend fun getById(id: Long): TunnelConfig?
@Query("SELECT * FROM TunnelConfig")
suspend fun getAll(): TunnelConfigs
@Delete
suspend fun delete(t: TunnelConfig)
@Query("SELECT COUNT('id') FROM TunnelConfig")
suspend fun count(): Long
@Query("SELECT * FROM TunnelConfig WHERE tunnel_networks LIKE '%' || :name || '%'")
suspend fun findByTunnelNetworkName(name: String): TunnelConfigs
@Query("UPDATE TunnelConfig SET is_primary_tunnel = 0 WHERE is_primary_tunnel =1")
fun resetPrimaryTunnel()
@Query("UPDATE TunnelConfig SET is_mobile_data_tunnel = 0 WHERE is_mobile_data_tunnel =1")
fun resetMobileDataTunnel()
@Query("SELECT * FROM TUNNELCONFIG WHERE is_primary_tunnel=1")
suspend fun findByPrimary(): TunnelConfigs
@Query("SELECT * FROM TUNNELCONFIG WHERE is_mobile_data_tunnel=1")
suspend fun findByMobileDataTunnel(): TunnelConfigs
@Query("SELECT * FROM tunnelconfig")
fun getAllFlow(): Flow<MutableList<TunnelConfig>>
}
@@ -1,70 +0,0 @@
package com.zaneschepke.wireguardautotunnel.data.datastore
import android.content.Context
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.booleanPreferencesKey
import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.intPreferencesKey
import androidx.datastore.preferences.core.stringPreferencesKey
import androidx.datastore.preferences.preferencesDataStore
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.runBlocking
import timber.log.Timber
import java.io.IOException
class DataStoreManager(private val context: Context) {
companion object {
val LOCATION_DISCLOSURE_SHOWN = booleanPreferencesKey("LOCATION_DISCLOSURE_SHOWN")
val BATTERY_OPTIMIZE_DISABLE_SHOWN = booleanPreferencesKey("BATTERY_OPTIMIZE_DISABLE_SHOWN")
val TUNNEL_RUNNING_FROM_MANUAL_START =
booleanPreferencesKey("TUNNEL_RUNNING_FROM_MANUAL_START")
val ACTIVE_TUNNEL = intPreferencesKey("ACTIVE_TUNNEL")
val CURRENT_SSID = stringPreferencesKey("CURRENT_SSID")
}
// preferences
private val preferencesKey = "preferences"
private val Context.dataStore by
preferencesDataStore(
name = preferencesKey,
)
suspend fun init() {
try {
context.dataStore.data.first()
} catch (e: IOException) {
Timber.e(e)
}
}
suspend fun <T> saveToDataStore(key: Preferences.Key<T>, value: T) {
try {
context.dataStore.edit { it[key] = value }
} catch (e: IOException) {
Timber.e(e)
} catch (e: Exception) {
Timber.e(e)
}
}
fun <T> getFromStoreFlow(key: Preferences.Key<T>) = context.dataStore.data.map { it[key] }
suspend fun <T> getFromStore(key: Preferences.Key<T>): T? {
return try {
context.dataStore.data.map { it[key] }.first()
} catch (e: IOException) {
Timber.e(e)
null
}
}
fun <T> getFromStoreBlocking(key: Preferences.Key<T>) = runBlocking {
context.dataStore.data.map { it[key] }.first()
}
val preferencesFlow: Flow<Preferences?> = context.dataStore.data
}
@@ -1,14 +0,0 @@
package com.zaneschepke.wireguardautotunnel.data.model
data class GeneralState(
val locationDisclosureShown: Boolean = LOCATION_DISCLOSURE_SHOWN_DEFAULT,
val batteryOptimizationDisableShown: Boolean = BATTERY_OPTIMIZATION_DISABLE_SHOWN_DEFAULT,
val tunnelRunningFromManualStart: Boolean = TUNNELING_RUNNING_FROM_MANUAL_START_DEFAULT,
val activeTunnelId: Int? = null
) {
companion object {
const val LOCATION_DISCLOSURE_SHOWN_DEFAULT = false
const val BATTERY_OPTIMIZATION_DISABLE_SHOWN_DEFAULT = false
const val TUNNELING_RUNNING_FROM_MANUAL_START_DEFAULT = false
}
}
@@ -1,53 +0,0 @@
package com.zaneschepke.wireguardautotunnel.data.model
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
@Entity
data class Settings(
@PrimaryKey(autoGenerate = true) val id: Int = 0,
@ColumnInfo(name = "is_tunnel_enabled") val isAutoTunnelEnabled: Boolean = false,
@ColumnInfo(name = "is_tunnel_on_mobile_data_enabled")
val isTunnelOnMobileDataEnabled: Boolean = false,
@ColumnInfo(name = "trusted_network_ssids")
val trustedNetworkSSIDs: MutableList<String> = mutableListOf(),
@ColumnInfo(name = "is_always_on_vpn_enabled") val isAlwaysOnVpnEnabled: Boolean = false,
@ColumnInfo(name = "is_tunnel_on_ethernet_enabled")
val isTunnelOnEthernetEnabled: Boolean = false,
@ColumnInfo(
name = "is_shortcuts_enabled",
defaultValue = "false",
)
val isShortcutsEnabled: Boolean = false,
@ColumnInfo(
name = "is_tunnel_on_wifi_enabled",
defaultValue = "false",
)
val isTunnelOnWifiEnabled: Boolean = false,
@ColumnInfo(
name = "is_kernel_enabled",
defaultValue = "false",
)
val isKernelEnabled: Boolean = false,
@ColumnInfo(
name = "is_restore_on_boot_enabled",
defaultValue = "false",
)
val isRestoreOnBootEnabled: Boolean = false,
@ColumnInfo(
name = "is_multi_tunnel_enabled",
defaultValue = "false",
)
val isMultiTunnelEnabled: Boolean = false,
@ColumnInfo(
name = "is_auto_tunnel_paused",
defaultValue = "false",
)
val isAutoTunnelPaused: Boolean = false,
@ColumnInfo(
name = "is_ping_enabled",
defaultValue = "false",
)
val isPingEnabled: Boolean = false,
)
@@ -1,38 +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.wireguard.config.Config
import java.io.InputStream
@Entity(indices = [Index(value = ["name"], unique = true)])
data class TunnelConfig(
@PrimaryKey(autoGenerate = true) val id: Int = 0,
@ColumnInfo(name = "name") val name: String,
@ColumnInfo(name = "wg_quick") val wgQuick: String,
@ColumnInfo(
name = "tunnel_networks",
defaultValue = "",
)
val tunnelNetworks: MutableList<String> = mutableListOf(),
@ColumnInfo(
name = "is_mobile_data_tunnel",
defaultValue = "false",
)
val isMobileDataTunnel: Boolean = false,
@ColumnInfo(
name = "is_primary_tunnel",
defaultValue = "false",
)
val isPrimaryTunnel: Boolean = false,
) {
companion object {
fun configFromQuick(wgQuick: String): Config {
val inputStream: InputStream = wgQuick.byteInputStream()
val reader = inputStream.bufferedReader(Charsets.UTF_8)
return Config.parse(reader)
}
}
}
@@ -1,14 +0,0 @@
package com.zaneschepke.wireguardautotunnel.data.repository
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
interface AppDataRepository {
suspend fun getPrimaryOrFirstTunnel(): TunnelConfig?
suspend fun getStartTunnelConfig(): TunnelConfig?
suspend fun toggleWatcherServicePause()
val settings: SettingsRepository
val tunnels: TunnelConfigRepository
val appState: AppStateRepository
}
@@ -1,34 +0,0 @@
package com.zaneschepke.wireguardautotunnel.data.repository
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
import javax.inject.Inject
class AppDataRoomRepository @Inject constructor(
override val settings: SettingsRepository,
override val tunnels: TunnelConfigRepository,
override val appState: AppStateRepository
) : AppDataRepository {
override suspend fun getPrimaryOrFirstTunnel(): TunnelConfig? {
return tunnels.findPrimary().firstOrNull() ?: tunnels.getAll().firstOrNull()
}
override suspend fun getStartTunnelConfig(): TunnelConfig? {
return if (appState.isTunnelRunningFromManualStart()) {
appState.getActiveTunnelId()?.let {
tunnels.getById(it)
}
} else null
}
override suspend fun toggleWatcherServicePause() {
val settings = settings.getSettings()
if (settings.isAutoTunnelEnabled) {
val pauseAutoTunnel = !settings.isAutoTunnelPaused
this.settings.save(
settings.copy(
isAutoTunnelPaused = pauseAutoTunnel,
),
)
}
}
}
@@ -1,26 +0,0 @@
package com.zaneschepke.wireguardautotunnel.data.repository
import com.zaneschepke.wireguardautotunnel.data.model.GeneralState
import kotlinx.coroutines.flow.Flow
interface AppStateRepository {
suspend fun isLocationDisclosureShown(): Boolean
suspend fun setLocationDisclosureShown(shown: Boolean)
suspend fun isBatteryOptimizationDisableShown(): Boolean
suspend fun setBatteryOptimizationDisableShown(shown: Boolean)
suspend fun isTunnelRunningFromManualStart(): Boolean
suspend fun setTunnelRunningFromManualStart(id: Int)
suspend fun setManualStop()
suspend fun getActiveTunnelId(): Int?
suspend fun getCurrentSsid(): String?
suspend fun setCurrentSsid(ssid: String)
val generalStateFlow: Flow<GeneralState>
}
@@ -1,81 +0,0 @@
package com.zaneschepke.wireguardautotunnel.data.repository
import com.zaneschepke.wireguardautotunnel.data.datastore.DataStoreManager
import com.zaneschepke.wireguardautotunnel.data.model.GeneralState
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import timber.log.Timber
class DataStoreAppStateRepository(private val dataStoreManager: DataStoreManager) :
AppStateRepository {
override suspend fun isLocationDisclosureShown(): Boolean {
return dataStoreManager.getFromStore(DataStoreManager.LOCATION_DISCLOSURE_SHOWN)
?: GeneralState.LOCATION_DISCLOSURE_SHOWN_DEFAULT
}
override suspend fun setLocationDisclosureShown(shown: Boolean) {
dataStoreManager.saveToDataStore(DataStoreManager.LOCATION_DISCLOSURE_SHOWN, shown)
}
override suspend fun isBatteryOptimizationDisableShown(): Boolean {
return dataStoreManager.getFromStore(DataStoreManager.BATTERY_OPTIMIZE_DISABLE_SHOWN)
?: GeneralState.BATTERY_OPTIMIZATION_DISABLE_SHOWN_DEFAULT
}
override suspend fun setBatteryOptimizationDisableShown(shown: Boolean) {
dataStoreManager.saveToDataStore(DataStoreManager.BATTERY_OPTIMIZE_DISABLE_SHOWN, shown)
}
override suspend fun isTunnelRunningFromManualStart(): Boolean {
return dataStoreManager.getFromStore(DataStoreManager.TUNNEL_RUNNING_FROM_MANUAL_START)
?: GeneralState.TUNNELING_RUNNING_FROM_MANUAL_START_DEFAULT
}
override suspend fun setTunnelRunningFromManualStart(id: Int) {
setTunnelRunningFromManualStart(true)
setActiveTunnelId(id)
}
override suspend fun setManualStop() {
setTunnelRunningFromManualStart(false)
}
private suspend fun setTunnelRunningFromManualStart(running: Boolean) {
dataStoreManager.saveToDataStore(DataStoreManager.TUNNEL_RUNNING_FROM_MANUAL_START, running)
}
override suspend fun getActiveTunnelId(): Int? {
return dataStoreManager.getFromStore(DataStoreManager.ACTIVE_TUNNEL)
}
private suspend fun setActiveTunnelId(id: Int) {
dataStoreManager.saveToDataStore(DataStoreManager.ACTIVE_TUNNEL, id)
}
override suspend fun getCurrentSsid(): String? {
return dataStoreManager.getFromStore(DataStoreManager.CURRENT_SSID)
}
override suspend fun setCurrentSsid(ssid: String) {
dataStoreManager.saveToDataStore(DataStoreManager.CURRENT_SSID, ssid)
}
override val generalStateFlow: Flow<GeneralState> =
dataStoreManager.preferencesFlow.map { prefs ->
prefs?.let { pref ->
try {
GeneralState(
locationDisclosureShown = pref[DataStoreManager.LOCATION_DISCLOSURE_SHOWN]
?: GeneralState.LOCATION_DISCLOSURE_SHOWN_DEFAULT,
batteryOptimizationDisableShown = pref[DataStoreManager.BATTERY_OPTIMIZE_DISABLE_SHOWN]
?: GeneralState.BATTERY_OPTIMIZATION_DISABLE_SHOWN_DEFAULT,
tunnelRunningFromManualStart = pref[DataStoreManager.TUNNEL_RUNNING_FROM_MANUAL_START]
?: GeneralState.TUNNELING_RUNNING_FROM_MANUAL_START_DEFAULT,
)
} catch (e: IllegalArgumentException) {
Timber.e(e)
GeneralState()
}
} ?: GeneralState()
}
}
@@ -1,24 +0,0 @@
package com.zaneschepke.wireguardautotunnel.data.repository
import com.zaneschepke.wireguardautotunnel.data.SettingsDao
import com.zaneschepke.wireguardautotunnel.data.model.Settings
import kotlinx.coroutines.flow.Flow
class RoomSettingsRepository(private val settingsDoa: SettingsDao) : SettingsRepository {
override suspend fun save(settings: Settings) {
settingsDoa.save(settings)
}
override fun getSettingsFlow(): Flow<Settings> {
return settingsDoa.getSettingsFlow()
}
override suspend fun getSettings(): Settings {
return settingsDoa.getAll().firstOrNull() ?: Settings()
}
override suspend fun getAll(): List<Settings> {
return settingsDoa.getAll()
}
}
@@ -1,68 +0,0 @@
package com.zaneschepke.wireguardautotunnel.data.repository
import com.zaneschepke.wireguardautotunnel.data.TunnelConfigDao
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
import com.zaneschepke.wireguardautotunnel.util.TunnelConfigs
import kotlinx.coroutines.flow.Flow
class RoomTunnelConfigRepository(private val tunnelConfigDao: TunnelConfigDao) :
TunnelConfigRepository {
override fun getTunnelConfigsFlow(): Flow<TunnelConfigs> {
return tunnelConfigDao.getAllFlow()
}
override suspend fun getAll(): TunnelConfigs {
return tunnelConfigDao.getAll()
}
override suspend fun save(tunnelConfig: TunnelConfig) {
tunnelConfigDao.save(tunnelConfig)
}
override suspend fun updatePrimaryTunnel(tunnelConfig: TunnelConfig?) {
tunnelConfigDao.resetPrimaryTunnel()
tunnelConfig?.let {
save(
it.copy(
isPrimaryTunnel = true,
),
)
}
}
override suspend fun updateMobileDataTunnel(tunnelConfig: TunnelConfig?) {
tunnelConfigDao.resetMobileDataTunnel()
tunnelConfig?.let {
save(
it.copy(
isMobileDataTunnel = true,
),
)
}
}
override suspend fun delete(tunnelConfig: TunnelConfig) {
tunnelConfigDao.delete(tunnelConfig)
}
override suspend fun getById(id: Int): TunnelConfig? {
return tunnelConfigDao.getById(id.toLong())
}
override suspend fun count(): Int {
return tunnelConfigDao.count().toInt()
}
override suspend fun findByTunnelNetworksName(name: String): TunnelConfigs {
return tunnelConfigDao.findByTunnelNetworkName(name)
}
override suspend fun findByMobileDataTunnel(): TunnelConfigs {
return tunnelConfigDao.findByMobileDataTunnel()
}
override suspend fun findPrimary(): TunnelConfigs {
return tunnelConfigDao.findByPrimary()
}
}
@@ -1,14 +0,0 @@
package com.zaneschepke.wireguardautotunnel.data.repository
import com.zaneschepke.wireguardautotunnel.data.model.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>
}
@@ -1,30 +0,0 @@
package com.zaneschepke.wireguardautotunnel.data.repository
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
import com.zaneschepke.wireguardautotunnel.util.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 delete(tunnelConfig: TunnelConfig)
suspend fun getById(id: Int): TunnelConfig?
suspend fun count(): Int
suspend fun findByTunnelNetworksName(name: String): TunnelConfigs
suspend fun findByMobileDataTunnel(): TunnelConfigs
suspend fun findPrimary(): TunnelConfigs
}
@@ -0,0 +1,40 @@
package com.zaneschepke.wireguardautotunnel.module
import android.content.Context
import com.zaneschepke.wireguardautotunnel.service.tunnel.model.MyObjectBox
import com.zaneschepke.wireguardautotunnel.service.tunnel.model.Settings
import com.zaneschepke.wireguardautotunnel.service.tunnel.model.TunnelConfig
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import io.objectbox.Box
import io.objectbox.BoxStore
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
class BoxModule {
@Provides
@Singleton
fun provideBoxStore(@ApplicationContext context : Context) : BoxStore {
return MyObjectBox.builder()
.androidContext(context.applicationContext)
.build()
}
@Provides
@Singleton
fun provideBoxForSettings(store : BoxStore) : Box<Settings> {
return store.boxFor(Settings::class.java)
}
@Provides
@Singleton
fun provideBoxForTunnels(store : BoxStore) : Box<TunnelConfig> {
return store.boxFor(TunnelConfig::class.java)
}
}
@@ -1,30 +0,0 @@
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 dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
class DatabaseModule {
@Provides
@Singleton
fun provideDatabase(@ApplicationContext context: Context): AppDatabase {
return Room.databaseBuilder(
context,
AppDatabase::class.java,
context.getString(R.string.db_name),
)
.fallbackToDestructiveMigration()
.addCallback(DatabaseCallback())
.build()
}
}
@@ -1,7 +0,0 @@
package com.zaneschepke.wireguardautotunnel.module
import javax.inject.Qualifier
@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class Kernel
@@ -1,73 +1,25 @@
package com.zaneschepke.wireguardautotunnel.module
import android.content.Context
import com.zaneschepke.wireguardautotunnel.data.AppDatabase
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.data.repository.AppStateRepository
import com.zaneschepke.wireguardautotunnel.data.repository.DataStoreAppStateRepository
import com.zaneschepke.wireguardautotunnel.data.repository.RoomSettingsRepository
import com.zaneschepke.wireguardautotunnel.data.repository.RoomTunnelConfigRepository
import com.zaneschepke.wireguardautotunnel.data.repository.SettingsRepository
import com.zaneschepke.wireguardautotunnel.data.repository.TunnelConfigRepository
import com.zaneschepke.wireguardautotunnel.repository.Repository
import com.zaneschepke.wireguardautotunnel.repository.SettingsBox
import com.zaneschepke.wireguardautotunnel.repository.TunnelBox
import com.zaneschepke.wireguardautotunnel.service.tunnel.model.Settings
import com.zaneschepke.wireguardautotunnel.service.tunnel.model.TunnelConfig
import dagger.Binds
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
class RepositoryModule {
abstract class RepositoryModule {
@Binds
@Singleton
@Provides
fun provideSettingsDoa(appDatabase: AppDatabase): SettingsDao {
return appDatabase.settingDao()
}
abstract fun provideSettingsRepository(settingsBox: SettingsBox) : Repository<Settings>
@Binds
@Singleton
@Provides
fun provideTunnelConfigDoa(appDatabase: AppDatabase): TunnelConfigDao {
return appDatabase.tunnelConfigDoa()
}
@Singleton
@Provides
fun provideTunnelConfigRepository(tunnelConfigDao: TunnelConfigDao): TunnelConfigRepository {
return RoomTunnelConfigRepository(tunnelConfigDao)
}
@Singleton
@Provides
fun provideSettingsRepository(settingsDao: SettingsDao): SettingsRepository {
return RoomSettingsRepository(settingsDao)
}
@Singleton
@Provides
fun providePreferencesDataStore(@ApplicationContext context: Context): DataStoreManager {
return DataStoreManager(context)
}
@Provides
@Singleton
fun provideGeneralStateRepository(dataStoreManager: DataStoreManager): AppStateRepository {
return DataStoreAppStateRepository(dataStoreManager)
}
@Provides
@Singleton
fun provideAppDataRepository(
settingsRepository: SettingsRepository,
tunnelConfigRepository: TunnelConfigRepository,
appStateRepository: AppStateRepository
): AppDataRepository {
return AppDataRoomRepository(settingsRepository, tunnelConfigRepository, appStateRepository)
}
}
abstract fun provideTunnelRepository(tunnelBox: TunnelBox) : Repository<TunnelConfig>
}
@@ -0,0 +1,40 @@
package com.zaneschepke.wireguardautotunnel.module
import android.content.Context
import com.google.mlkit.vision.barcode.common.Barcode
import com.google.mlkit.vision.codescanner.GmsBarcodeScanner
import com.google.mlkit.vision.codescanner.GmsBarcodeScannerOptions
import com.google.mlkit.vision.codescanner.GmsBarcodeScanning
import com.zaneschepke.wireguardautotunnel.service.barcode.CodeScanner
import com.zaneschepke.wireguardautotunnel.service.barcode.QRScanner
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.components.ViewModelComponent
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.android.scopes.ViewModelScoped
@Module
@InstallIn(ViewModelComponent::class)
class ScannerModule {
@ViewModelScoped
@Provides
fun provideBarCodeOptions() : GmsBarcodeScannerOptions {
return GmsBarcodeScannerOptions.Builder()
.setBarcodeFormats(Barcode.FORMAT_QR_CODE)
.build()
}
@ViewModelScoped
@Provides
fun provideBarCodeScanner(@ApplicationContext context: Context, options: GmsBarcodeScannerOptions) : GmsBarcodeScanner {
return GmsBarcodeScanning.getClient(context, options)
}
@ViewModelScoped
@Provides
fun provideQRScanner(gmsBarcodeScanner: GmsBarcodeScanner) : CodeScanner {
return QRScanner(gmsBarcodeScanner)
}
}
@@ -1,6 +1,7 @@
package com.zaneschepke.wireguardautotunnel.module
import com.zaneschepke.wireguardautotunnel.service.network.EthernetService
import com.zaneschepke.wireguardautotunnel.service.barcode.CodeScanner
import com.zaneschepke.wireguardautotunnel.service.barcode.QRScanner
import com.zaneschepke.wireguardautotunnel.service.network.MobileDataService
import com.zaneschepke.wireguardautotunnel.service.network.NetworkService
import com.zaneschepke.wireguardautotunnel.service.network.WifiService
@@ -11,29 +12,21 @@ import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.android.components.ServiceComponent
import dagger.hilt.android.scopes.ServiceScoped
import dagger.hilt.android.scopes.ViewModelScoped
@Module
@InstallIn(ServiceComponent::class)
abstract class ServiceModule {
@Binds
@ServiceScoped
abstract fun provideNotificationService(
wireGuardNotification: WireGuardNotification
): NotificationService
@Binds
@ServiceScoped
abstract fun provideWifiService(wifiService: WifiService): NetworkService<WifiService>
abstract fun provideNotificationService(wireGuardNotification: WireGuardNotification) : NotificationService
@Binds
@ServiceScoped
abstract fun provideMobileDataService(
mobileDataService: MobileDataService
): NetworkService<MobileDataService>
abstract fun provideWifiService(wifiService: WifiService) : NetworkService<WifiService>
@Binds
@ServiceScoped
abstract fun provideEthernetService(
ethernetService: EthernetService
): NetworkService<EthernetService>
}
abstract fun provideMobileDataService(mobileDataService : MobileDataService) : NetworkService<MobileDataService>
}
@@ -3,11 +3,6 @@ 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.WgQuickBackend
import com.wireguard.android.util.RootShell
import com.wireguard.android.util.ToolsInstaller
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnService
import com.zaneschepke.wireguardautotunnel.service.tunnel.WireGuardTunnel
import dagger.Module
@@ -20,39 +15,17 @@ import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
class TunnelModule {
@Provides
@Singleton
fun provideRootShell(@ApplicationContext context: Context): RootShell {
return RootShell(context)
}
@Provides
@Singleton
@Userspace
fun provideUserspaceBackend(@ApplicationContext context: Context): Backend {
fun provideBackend(@ApplicationContext context : Context) : Backend {
return GoBackend(context)
}
@Provides
@Singleton
@Kernel
fun provideKernelBackend(@ApplicationContext context: Context, rootShell: RootShell): Backend {
return WgQuickBackend(context, rootShell, ToolsInstaller(context, rootShell))
fun provideVpnService(backend: Backend) : VpnService {
return WireGuardTunnel(backend)
}
@Provides
@Singleton
fun provideVpnService(
@Userspace userspaceBackend: Backend,
@Kernel kernelBackend: Backend,
appDataRepository: AppDataRepository
): VpnService {
return WireGuardTunnel(userspaceBackend, kernelBackend, appDataRepository)
}
@Provides
@Singleton
fun provideServiceManager(appDataRepository: AppDataRepository): ServiceManager {
return ServiceManager(appDataRepository)
}
}
}
@@ -1,7 +0,0 @@
package com.zaneschepke.wireguardautotunnel.module
import javax.inject.Qualifier
@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class Userspace
@@ -3,43 +3,46 @@ 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.service.foreground.ServiceManager
import com.zaneschepke.wireguardautotunnel.util.goAsync
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.repository.Repository
import com.zaneschepke.wireguardautotunnel.service.foreground.Action
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceTracker
import com.zaneschepke.wireguardautotunnel.service.foreground.WireGuardConnectivityWatcherService
import com.zaneschepke.wireguardautotunnel.service.tunnel.model.Settings
import com.zaneschepke.wireguardautotunnel.service.tunnel.model.TunnelConfig
import dagger.hilt.android.AndroidEntryPoint
import timber.log.Timber
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch
import javax.inject.Inject
@AndroidEntryPoint
class BootReceiver : BroadcastReceiver() {
@Inject
lateinit var appDataRepository: AppDataRepository
lateinit var settingsRepo : Repository<Settings>
@Inject
lateinit var serviceManager: ServiceManager
override fun onReceive(context: Context?, intent: Intent?) = goAsync {
if (Intent.ACTION_BOOT_COMPLETED != intent?.action) return@goAsync
context?.run {
val settings = appDataRepository.settings.getSettings()
if (settings.isAutoTunnelEnabled) {
Timber.i("Starting watcher service from boot")
serviceManager.startWatcherServiceForeground(context)
}
if (appDataRepository.appState.isTunnelRunningFromManualStart()) {
appDataRepository.appState.getActiveTunnelId()?.let {
Timber.i("Starting tunnel that was active before reboot")
serviceManager.startVpnServiceForeground(
context,
appDataRepository.tunnels.getById(it)?.id,
)
override fun onReceive(context: Context, intent: Intent) {
if (intent.action == Intent.ACTION_BOOT_COMPLETED) {
CoroutineScope(SupervisorJob()).launch {
try {
val settings = settingsRepo.getAll()
if (!settings.isNullOrEmpty()) {
val setting = settings.first()
if (setting.isAutoTunnelEnabled && setting.defaultTunnel != null) {
ServiceTracker.actionOnService(
Action.START, context,
WireGuardConnectivityWatcherService::class.java,
mapOf(context.resources.getString(R.string.tunnel_extras_key) to
setting.defaultTunnel!!)
)
}
}
} finally {
cancel()
}
}
if (settings.isAlwaysOnVpnEnabled) {
Timber.i("Starting vpn service from boot AOVPN")
serviceManager.startVpnServiceForeground(context)
}
}
}
}
}
@@ -3,34 +3,54 @@ package com.zaneschepke.wireguardautotunnel.receiver
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import com.zaneschepke.wireguardautotunnel.data.repository.SettingsRepository
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
import com.zaneschepke.wireguardautotunnel.util.Constants
import com.zaneschepke.wireguardautotunnel.util.goAsync
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.repository.Repository
import com.zaneschepke.wireguardautotunnel.service.foreground.Action
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceTracker
import com.zaneschepke.wireguardautotunnel.service.foreground.WireGuardTunnelService
import com.zaneschepke.wireguardautotunnel.service.tunnel.model.Settings
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
import kotlinx.coroutines.delay
import timber.log.Timber
import kotlinx.coroutines.launch
import javax.inject.Inject
@AndroidEntryPoint
class NotificationActionReceiver : BroadcastReceiver() {
@Inject
lateinit var settingsRepository: SettingsRepository
@Inject
lateinit var serviceManager: ServiceManager
override fun onReceive(context: Context, intent: Intent?) = goAsync {
try {
//TODO fix for manual start changes when enabled
serviceManager.stopVpnServiceForeground(context)
delay(Constants.TOGGLE_TUNNEL_DELAY)
serviceManager.startVpnServiceForeground(context)
} catch (e: Exception) {
Timber.e(e)
} finally {
cancel()
lateinit var settingsRepo : Repository<Settings>
override fun onReceive(context: Context, intent: Intent?) {
CoroutineScope(SupervisorJob()).launch {
try {
val settings = settingsRepo.getAll()
if (!settings.isNullOrEmpty()) {
val setting = settings.first()
if (setting.defaultTunnel != null) {
ServiceTracker.actionOnService(
Action.STOP, context,
WireGuardTunnelService::class.java,
mapOf(
context.resources.getString(R.string.tunnel_extras_key) to
setting.defaultTunnel!!
)
)
delay(1000)
ServiceTracker.actionOnService(
Action.START, context,
WireGuardTunnelService::class.java,
mapOf(
context.resources.getString(R.string.tunnel_extras_key) to
setting.defaultTunnel!!
)
)
}
}
} finally {
cancel()
}
}
}
}
}
@@ -0,0 +1,16 @@
package com.zaneschepke.wireguardautotunnel.repository
import kotlinx.coroutines.flow.Flow
interface Repository<T> {
suspend fun save(t : T)
suspend fun saveAll(t : List<T>)
suspend fun getById(id : Long) : T?
suspend fun getAll() : List<T>?
suspend fun delete(t : T) : Boolean?
suspend fun count() : Long?
val itemFlow : Flow<MutableList<T>>
fun init()
}
@@ -0,0 +1,63 @@
package com.zaneschepke.wireguardautotunnel.repository
import com.zaneschepke.wireguardautotunnel.service.tunnel.model.Settings
import io.objectbox.Box
import io.objectbox.BoxStore
import io.objectbox.kotlin.awaitCallInTx
import io.objectbox.kotlin.toFlow
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.launch
import javax.inject.Inject
class SettingsBox @Inject constructor(private val box : Box<Settings>, private val boxStore : BoxStore) : Repository<Settings> {
@OptIn(ExperimentalCoroutinesApi::class)
override val itemFlow = box.query().build().subscribe().toFlow()
override fun init() {
CoroutineScope(Dispatchers.IO).launch {
if(getAll().isNullOrEmpty()) {
save(Settings())
}
}
}
override suspend fun save(t : Settings) {
boxStore.awaitCallInTx {
box.put(t)
}
}
override suspend fun saveAll(t : List<Settings>) {
boxStore.awaitCallInTx {
box.put(t)
}
}
override suspend fun getById(id: Long): Settings? {
return boxStore.awaitCallInTx {
box[id]
}
}
override suspend fun getAll(): List<Settings>? {
return boxStore.awaitCallInTx {
box.all
}
}
override suspend fun delete(t : Settings): Boolean? {
return boxStore.awaitCallInTx {
box.remove(t)
}
}
override suspend fun count() : Long? {
return boxStore.awaitCallInTx {
box.count()
}
}
}
@@ -0,0 +1,57 @@
package com.zaneschepke.wireguardautotunnel.repository
import com.zaneschepke.wireguardautotunnel.service.tunnel.model.TunnelConfig
import io.objectbox.Box
import io.objectbox.BoxStore
import io.objectbox.kotlin.awaitCallInTx
import io.objectbox.kotlin.toFlow
import kotlinx.coroutines.ExperimentalCoroutinesApi
import timber.log.Timber
import javax.inject.Inject
class TunnelBox @Inject constructor(private val box : Box<TunnelConfig>,private val boxStore : BoxStore) : Repository<TunnelConfig> {
@OptIn(ExperimentalCoroutinesApi::class)
override val itemFlow = box.query().build().subscribe().toFlow()
override fun init() {
}
override suspend fun save(t : TunnelConfig) {
Timber.d("Saving tunnel config")
boxStore.awaitCallInTx {
box.put(t)
}
}
override suspend fun saveAll(t : List<TunnelConfig>) {
boxStore.awaitCallInTx {
box.put(t)
}
}
override suspend fun getById(id: Long): TunnelConfig? {
return boxStore.awaitCallInTx {
box[id]
}
}
override suspend fun getAll(): List<TunnelConfig>? {
return boxStore.awaitCallInTx {
box.all
}
}
override suspend fun delete(t : TunnelConfig): Boolean? {
return boxStore.awaitCallInTx {
box.remove(t)
}
}
override suspend fun count() : Long? {
return boxStore.awaitCallInTx {
box.count()
}
}
}
@@ -0,0 +1,7 @@
package com.zaneschepke.wireguardautotunnel.service.barcode
import kotlinx.coroutines.flow.Flow
interface CodeScanner {
fun scan() : Flow<String?>
}
@@ -0,0 +1,23 @@
package com.zaneschepke.wireguardautotunnel.service.barcode
import com.google.mlkit.vision.codescanner.GmsBarcodeScanner
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.callbackFlow
import timber.log.Timber
import javax.inject.Inject
class QRScanner @Inject constructor(private val gmsBarcodeScanner: GmsBarcodeScanner) : CodeScanner {
override fun scan(): Flow<String?> {
return callbackFlow {
gmsBarcodeScanner.startScan().addOnSuccessListener {
trySend(it.rawValue)
}.addOnFailureListener {
trySend(it.message)
Timber.e(it.message)
}
awaitClose {
}
}
}
}
@@ -2,7 +2,5 @@ package com.zaneschepke.wireguardautotunnel.service.foreground
enum class Action {
START,
START_FOREGROUND,
STOP,
STOP_FOREGROUND
}
STOP
}
@@ -1,55 +1,67 @@
package com.zaneschepke.wireguardautotunnel.service.foreground
import android.app.Service
import android.content.Intent
import android.os.Bundle
import android.os.IBinder
import androidx.lifecycle.LifecycleService
import com.zaneschepke.wireguardautotunnel.util.Constants
import com.zaneschepke.wireguardautotunnel.repository.Repository
import com.zaneschepke.wireguardautotunnel.service.tunnel.model.Settings
import timber.log.Timber
import javax.inject.Inject
open class ForegroundService : Service() {
open class ForegroundService : LifecycleService() {
private var isServiceStarted = false
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 {
super.onStartCommand(intent, flags, startId)
Timber.d("onStartCommand executed with startId: $startId")
if (intent != null) {
val action = intent.action
Timber.d("using an intent with action $action")
when (action) {
Action.START.name,
Action.START_FOREGROUND.name -> startService(intent.extras)
Action.STOP.name, Action.STOP_FOREGROUND.name -> stopService()
Constants.ALWAYS_ON_VPN_ACTION -> {
Timber.i("Always-on VPN starting service")
Action.START.name -> startService(intent.extras)
Action.STOP.name -> stopService(intent.extras)
"android.net.VpnService" -> {
Timber.d("Always-on VPN starting service")
startService(intent.extras)
}
else -> Timber.d("This should never happen. No action in the received intent")
}
} else {
Timber.d(
"with a null intent. It has been probably restarted by the system.",
"with a null intent. It has been probably restarted by the system."
)
}
// by returning this we make sure the service is restarted if the system kills the service
return START_STICKY
}
protected open fun startService(extras: Bundle?) {
override fun onDestroy() {
super.onDestroy()
Timber.d("The service has been destroyed")
}
protected open fun startService(extras : Bundle?) {
if (isServiceStarted) return
Timber.d("Starting ${this.javaClass.simpleName}")
isServiceStarted = true
}
protected open fun stopService() {
protected open fun stopService(extras : Bundle?) {
Timber.d("Stopping ${this.javaClass.simpleName}")
stopForeground(STOP_FOREGROUND_REMOVE)
stopSelf()
try {
stopForeground(STOP_FOREGROUND_REMOVE)
stopSelf()
} catch (e: Exception) {
Timber.d("Service stopped without being started: ${e.message}")
}
isServiceStarted = false
}
}
}
@@ -1,117 +0,0 @@
package com.zaneschepke.wireguardautotunnel.service.foreground
import android.app.Service
import android.content.Context
import android.content.Intent
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
import com.zaneschepke.wireguardautotunnel.util.Constants
import timber.log.Timber
class ServiceManager(private val appDataRepository: AppDataRepository) {
private fun <T : Service> actionOnService(
action: Action,
context: Context,
cls: Class<T>,
extras: Map<String, Int>? = null
) {
val intent =
Intent(context, cls).also {
it.action = action.name
extras?.forEach { (k, v) -> it.putExtra(k, v) }
}
intent.component?.javaClass
try {
when (action) {
Action.START_FOREGROUND, Action.STOP_FOREGROUND -> context.startForegroundService(intent)
Action.START, Action.STOP -> context.startService(intent)
}
} catch (e: Exception) {
Timber.e(e.message)
}
}
suspend fun startVpnService(
context: Context,
tunnelId: Int? = null,
isManualStart: Boolean = false
) {
if (isManualStart) onManualStart(tunnelId)
actionOnService(
Action.START,
context,
WireGuardTunnelService::class.java,
tunnelId?.let { mapOf(Constants.TUNNEL_EXTRA_KEY to it) },
)
}
suspend fun stopVpnServiceForeground(context: Context, isManualStop: Boolean = false) {
if (isManualStop) onManualStop()
Timber.i("Stopping vpn service")
actionOnService(
Action.STOP_FOREGROUND,
context,
WireGuardTunnelService::class.java,
)
}
suspend fun stopVpnService(context: Context, isManualStop: Boolean = false) {
if (isManualStop) onManualStop()
Timber.i("Stopping vpn service")
actionOnService(
Action.STOP,
context,
WireGuardTunnelService::class.java,
)
}
private suspend fun onManualStop() {
appDataRepository.appState.setManualStop()
}
private suspend fun onManualStart(tunnelId: Int?) {
tunnelId?.let {
appDataRepository.appState.setTunnelRunningFromManualStart(it)
}
}
suspend fun startVpnServiceForeground(
context: Context,
tunnelId: Int? = null,
isManualStart: Boolean = false
) {
if (isManualStart) onManualStart(tunnelId)
actionOnService(
Action.START_FOREGROUND,
context,
WireGuardTunnelService::class.java,
tunnelId?.let { mapOf(Constants.TUNNEL_EXTRA_KEY to it) },
)
}
fun startWatcherServiceForeground(
context: Context,
) {
actionOnService(
Action.START_FOREGROUND,
context,
WireGuardConnectivityWatcherService::class.java,
)
}
fun startWatcherService(context: Context) {
actionOnService(
Action.START,
context,
WireGuardConnectivityWatcherService::class.java,
)
}
fun stopWatcherService(context: Context) {
actionOnService(
Action.STOP,
context,
WireGuardConnectivityWatcherService::class.java,
)
}
}
@@ -0,0 +1,6 @@
package com.zaneschepke.wireguardautotunnel.service.foreground
enum class ServiceState {
STARTED,
STOPPED,
}
@@ -0,0 +1,56 @@
package com.zaneschepke.wireguardautotunnel.service.foreground
import android.app.ActivityManager
import android.app.Application
import android.app.Service
import android.content.Context
import android.content.Context.ACTIVITY_SERVICE
import android.content.Intent
import com.google.firebase.crashlytics.ktx.crashlytics
import com.google.firebase.ktx.Firebase
object ServiceTracker {
@Suppress("DEPRECATION")
private // Deprecated for third party Services.
fun <T> Context.isServiceRunning(service: Class<T>) =
(getSystemService(ACTIVITY_SERVICE) as ActivityManager)
.getRunningServices(Integer.MAX_VALUE)
.any { it.service.className == service.name }
fun <T : Service> getServiceState(context: Context, cls : Class<T>): ServiceState {
val isServiceRunning = context.isServiceRunning(cls)
return if(isServiceRunning) ServiceState.STARTED else ServiceState.STOPPED
}
fun <T : Service> actionOnService(action: Action, application: Application, cls : Class<T>, extras : Map<String,String>? = null) {
if (getServiceState(application, cls) == ServiceState.STOPPED && action == Action.STOP) return
val intent = Intent(application, cls).also {
it.action = action.name
extras?.forEach {(k, v) ->
it.putExtra(k, v)
}
}
intent.component?.javaClass
try {
application.startService(intent)
} catch (e : Exception) {
e.message?.let { Firebase.crashlytics.log(it) }
}
}
fun <T : Service> actionOnService(action: Action, context: Context, cls : Class<T>, extras : Map<String,String>? = null) {
if (getServiceState(context, cls) == ServiceState.STOPPED && action == Action.STOP) return
val intent = Intent(context, cls).also {
it.action = action.name
extras?.forEach {(k, v) ->
it.putExtra(k, v)
}
}
intent.component?.javaClass
try {
context.startService(intent)
} catch (e : Exception) {
e.message?.let { Firebase.crashlytics.log(it) }
}
}
}
@@ -1,84 +0,0 @@
package com.zaneschepke.wireguardautotunnel.service.foreground
import com.wireguard.android.backend.Tunnel
import com.zaneschepke.wireguardautotunnel.data.model.Settings
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
data class WatcherState(
val isWifiConnected: Boolean = false,
val config: TunnelConfig? = null,
val vpnStatus: Tunnel.State = Tunnel.State.DOWN,
val isEthernetConnected: Boolean = false,
val isMobileDataConnected: Boolean = false,
val currentNetworkSSID: String = "",
val settings: Settings = Settings()
) {
private fun isVpnConnected() = vpnStatus == Tunnel.State.UP
fun isEthernetConditionMet(): Boolean {
return (isEthernetConnected &&
settings.isTunnelOnEthernetEnabled &&
!isVpnConnected())
}
fun isMobileDataConditionMet(): Boolean {
return (!isEthernetConnected &&
settings.isTunnelOnMobileDataEnabled &&
!isWifiConnected &&
isMobileDataConnected &&
!isVpnConnected())
}
fun isTunnelNotMobileDataPreferredConditionMet(): Boolean {
return (!isEthernetConnected &&
settings.isTunnelOnMobileDataEnabled &&
!isWifiConnected &&
isMobileDataConnected &&
config?.isMobileDataTunnel == false && isVpnConnected())
}
fun isTunnelOffOnMobileDataConditionMet(): Boolean {
return (!isEthernetConnected &&
!settings.isTunnelOnMobileDataEnabled &&
isMobileDataConnected &&
!isWifiConnected &&
isVpnConnected())
}
fun isUntrustedWifiConditionMet(): Boolean {
return (!isEthernetConnected &&
isWifiConnected &&
!settings.trustedNetworkSSIDs.contains(currentNetworkSSID) &&
settings.isTunnelOnWifiEnabled
&& !isVpnConnected())
}
fun isTunnelNotWifiNamePreferredMet(ssid: String): Boolean {
return (!isEthernetConnected &&
isWifiConnected &&
!settings.trustedNetworkSSIDs.contains(currentNetworkSSID) &&
settings.isTunnelOnWifiEnabled && config?.tunnelNetworks?.contains(ssid) == false && isVpnConnected())
}
fun isTrustedWifiConditionMet(): Boolean {
return (!isEthernetConnected &&
(isWifiConnected &&
settings.trustedNetworkSSIDs.contains(currentNetworkSSID)) &&
(isVpnConnected()))
}
fun isTunnelOffOnWifiConditionMet(): Boolean {
return (!isEthernetConnected &&
(isWifiConnected &&
!settings.isTunnelOnWifiEnabled &&
(isVpnConnected())))
}
fun isTunnelOffOnNoConnectivityMet(): Boolean {
return (!isEthernetConnected &&
!isWifiConnected &&
!isMobileDataConnected &&
(isVpnConnected()))
}
}
@@ -1,303 +1,166 @@
package com.zaneschepke.wireguardautotunnel.service.foreground
import android.app.AlarmManager
import android.app.Application
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.os.PowerManager
import androidx.core.app.ServiceCompat
import androidx.lifecycle.lifecycleScope
import android.os.SystemClock
import com.wireguard.android.backend.Tunnel
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
import com.zaneschepke.wireguardautotunnel.service.network.EthernetService
import com.zaneschepke.wireguardautotunnel.repository.Repository
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.VpnService
import com.zaneschepke.wireguardautotunnel.util.Constants
import com.zaneschepke.wireguardautotunnel.service.tunnel.model.Settings
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch
import timber.log.Timber
import java.net.InetAddress
import javax.inject.Inject
@AndroidEntryPoint
class WireGuardConnectivityWatcherService : ForegroundService() {
private val foregroundId = 122
private val foregroundId = 122;
@Inject
lateinit var wifiService: NetworkService<WifiService>
lateinit var wifiService : NetworkService<WifiService>
@Inject
lateinit var mobileDataService: NetworkService<MobileDataService>
lateinit var mobileDataService : NetworkService<MobileDataService>
@Inject
lateinit var ethernetService: NetworkService<EthernetService>
lateinit var settingsRepo: Repository<Settings>
@Inject
lateinit var appDataRepository: AppDataRepository
lateinit var notificationService : NotificationService
@Inject
lateinit var notificationService: NotificationService
lateinit var vpnService : VpnService
@Inject
lateinit var vpnService: VpnService
private lateinit var watcherJob : Job;
private lateinit var setting : Settings
private lateinit var tunnelId: String
@Inject
lateinit var serviceManager: ServiceManager
private val networkEventsFlow = MutableStateFlow(WatcherState())
private var watcherJob: Job? = null
private var connecting = false
private var disconnecting = false
private var isWifiConnected = false
private var isMobileDataConnected = false
private var wakeLock: PowerManager.WakeLock? = null
private val tag = this.javaClass.name
private val tag = this.javaClass.name;
override fun onCreate() {
super.onCreate()
lifecycleScope.launch(Dispatchers.Main) {
try {
if (appDataRepository.settings.getSettings().isAutoTunnelPaused) {
launchWatcherPausedNotification()
} else launchWatcherNotification()
} catch (e: Exception) {
Timber.e("Failed to start watcher service, not enough permissions")
}
}
}
override fun startService(extras: Bundle?) {
super.startService(extras)
try {
// we need this lock so our service gets not affected by Doze Mode
lifecycleScope.launch { initWakeLock() }
cancelWatcherJob()
val tunnelId = extras?.getString(getString(R.string.tunnel_extras_key))
if (tunnelId != null) {
this.tunnelId = tunnelId
}
// we need this lock so our service gets not affected by Doze Mode
initWakeLock()
cancelWatcherJob()
launchWatcherNotification()
if(this::tunnelId.isInitialized) {
startWatcherJob()
} catch (e: Exception) {
Timber.e("Failed to launch watcher service, no permissions")
} else {
stopService(extras)
}
}
override fun stopService() {
super.stopService()
override fun stopService(extras: Bundle?) {
super.stopService(extras)
wakeLock?.let {
if (it.isHeld) {
it.release()
}
}
cancelWatcherJob()
stopVPN()
stopSelf()
}
private fun launchWatcherNotification(
description: String = getString(R.string.watcher_notification_text_active)
) {
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 launchWatcherNotification() {
val notification = notificationService.createNotification(
channelId = getString(R.string.watcher_channel_id),
channelName = getString(R.string.watcher_channel_name),
description = getString(R.string.watcher_notification_text))
super.startForeground(foregroundId, notification)
}
private fun launchWatcherPausedNotification() {
launchWatcherNotification(getString(R.string.watcher_notification_text_paused))
//try to start task again if killed
override fun onTaskRemoved(rootIntent: Intent) {
Timber.d("Task Removed called")
val restartServiceIntent = Intent(rootIntent)
val restartServicePendingIntent: PendingIntent = PendingIntent.getService(this, 1, restartServiceIntent,
PendingIntent.FLAG_ONE_SHOT or PendingIntent.FLAG_IMMUTABLE);
applicationContext.getSystemService(Context.ALARM_SERVICE);
val alarmService: AlarmManager = applicationContext.getSystemService(Context.ALARM_SERVICE) as AlarmManager;
alarmService.set(AlarmManager.ELAPSED_REALTIME, SystemClock.elapsedRealtime() + 1000, restartServicePendingIntent);
}
private fun initWakeLock() {
wakeLock =
(getSystemService(Context.POWER_SERVICE) as PowerManager).run {
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()
}
acquire()
}
}
}
private fun cancelWatcherJob() {
try {
watcherJob?.cancel()
} catch (e : CancellationException) {
Timber.i("Watcher job cancelled")
if(this::watcherJob.isInitialized) {
watcherJob.cancel()
}
}
private fun startWatcherJob() {
watcherJob =
lifecycleScope.launch(Dispatchers.IO) {
val setting = appDataRepository.settings.getSettings()
launch {
Timber.i("Starting wifi watcher")
watchForWifiConnectivityChanges()
}
if (setting.isTunnelOnMobileDataEnabled) {
launch {
Timber.i("Starting mobile data watcher")
watchForMobileDataConnectivityChanges()
}
}
if (setting.isTunnelOnEthernetEnabled) {
launch {
Timber.i("Starting ethernet data watcher")
watchForEthernetConnectivityChanges()
}
}
launch {
Timber.i("Starting vpn state watcher")
watchForVpnConnectivityChanges()
}
launch {
Timber.i("Starting settings watcher")
watchForSettingsChanges()
}
if (setting.isPingEnabled) {
launch {
Timber.i("Starting ping watcher")
watchForPingFailure()
}
}
launch {
Timber.i("Starting management watcher")
manageVpn()
}
watcherJob = CoroutineScope(SupervisorJob()).launch {
val settings = settingsRepo.getAll();
if(!settings.isNullOrEmpty()) {
setting = settings[0]
}
CoroutineScope(watcherJob).launch {
watchForWifiConnectivityChanges()
}
if(setting.isTunnelOnMobileDataEnabled) {
CoroutineScope(watcherJob).launch {
watchForMobileDataConnectivityChanges()
}
}
}
}
private suspend fun watchForMobileDataConnectivityChanges() {
mobileDataService.networkStatus.collect {
when (it) {
when(it) {
is NetworkStatus.Available -> {
Timber.i("Gained Mobile data connection")
networkEventsFlow.value =
networkEventsFlow.value.copy(
isMobileDataConnected = true,
)
Timber.d("Gained Mobile data connection")
isMobileDataConnected = true
}
is NetworkStatus.CapabilitiesChanged -> {
networkEventsFlow.value =
networkEventsFlow.value.copy(
isMobileDataConnected = true,
)
Timber.i("Mobile data capabilities changed")
}
is NetworkStatus.Unavailable -> {
networkEventsFlow.value =
networkEventsFlow.value.copy(
isMobileDataConnected = false,
)
Timber.i("Lost mobile data connection")
}
}
}
}
private suspend fun watchForPingFailure() {
try {
do {
if (vpnService.vpnState.value.status == Tunnel.State.UP) {
val tunnelConfig = vpnService.vpnState.value.tunnelConfig
tunnelConfig?.let {
val config = TunnelConfig.configFromQuick(it.wgQuick)
val results = config.peers.map { peer ->
val host = if (peer.endpoint.isPresent &&
peer.endpoint.get().resolved.isPresent)
peer.endpoint.get().resolved.get().host
else Constants.BACKUP_PING_HOST
Timber.i("Checking reachability of: $host")
val reachable = InetAddress.getByName(host)
.isReachable(Constants.PING_TIMEOUT.toInt())
Timber.i("Result: reachable - $reachable")
reachable
}
if (results.contains(false)) {
Timber.i("Restarting VPN for ping failure")
serviceManager.stopVpnServiceForeground(this)
delay(Constants.VPN_RESTART_DELAY)
serviceManager.startVpnServiceForeground(this)
delay(Constants.PING_COOLDOWN)
}
isMobileDataConnected = true
Timber.d("Mobile data capabilities changed")
if(!disconnecting && !connecting) {
if(!isWifiConnected && setting.isTunnelOnMobileDataEnabled
&& vpnService.getState() == Tunnel.State.DOWN)
startVPN()
}
}
delay(Constants.PING_INTERVAL)
} while (true)
} catch (e: Exception) {
Timber.e(e)
}
}
private suspend fun watchForSettingsChanges() {
appDataRepository.settings.getSettingsFlow().collect {
if (networkEventsFlow.value.settings.isAutoTunnelPaused != it.isAutoTunnelPaused) {
when (it.isAutoTunnelPaused) {
true -> launchWatcherPausedNotification()
false -> launchWatcherNotification()
}
}
networkEventsFlow.value =
networkEventsFlow.value.copy(
settings = it,
)
}
}
private suspend fun watchForVpnConnectivityChanges() {
vpnService.vpnState.collect {
networkEventsFlow.value =
networkEventsFlow.value.copy(
vpnStatus = it.status,
config = it.tunnelConfig,
)
}
}
private suspend fun watchForEthernetConnectivityChanges() {
ethernetService.networkStatus.collect {
when (it) {
is NetworkStatus.Available -> {
Timber.i("Gained Ethernet connection")
networkEventsFlow.value =
networkEventsFlow.value.copy(
isEthernetConnected = true,
)
}
is NetworkStatus.CapabilitiesChanged -> {
Timber.i("Ethernet capabilities changed")
networkEventsFlow.value =
networkEventsFlow.value.copy(
isEthernetConnected = true,
)
}
is NetworkStatus.Unavailable -> {
networkEventsFlow.value =
networkEventsFlow.value.copy(
isEthernetConnected = false,
)
Timber.i("Lost Ethernet connection")
isMobileDataConnected = false
if(!disconnecting && !connecting) {
if(!isWifiConnected && vpnService.getState() == Tunnel.State.UP) stopVPN()
}
Timber.d("Lost mobile data connection")
}
}
}
@@ -305,126 +168,69 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
private suspend fun watchForWifiConnectivityChanges() {
wifiService.networkStatus.collect {
when (it) {
is NetworkStatus.Available -> {
Timber.i("Gained Wi-Fi connection")
networkEventsFlow.value =
networkEventsFlow.value.copy(
isWifiConnected = true,
)
}
is NetworkStatus.CapabilitiesChanged -> {
Timber.i("Wifi capabilities changed")
networkEventsFlow.value =
networkEventsFlow.value.copy(
isWifiConnected = true,
)
val ssid = wifiService.getNetworkName(it.networkCapabilities)
ssid?.let {
if(it.contains(Constants.UNREADABLE_SSID)) {
Timber.w("SSID unreadable: missing permissions")
} else Timber.i("Detected valid SSID")
appDataRepository.appState.setCurrentSsid(ssid)
networkEventsFlow.value =
networkEventsFlow.value.copy(
currentNetworkSSID = ssid,
)
} ?: Timber.w("Failed to read ssid")
}
is NetworkStatus.Unavailable -> {
networkEventsFlow.value =
networkEventsFlow.value.copy(
isWifiConnected = false,
)
Timber.i("Lost Wi-Fi connection")
}
}
}
}
private suspend fun getMobileDataTunnel(): TunnelConfig? {
return appDataRepository.tunnels.findByMobileDataTunnel().firstOrNull()
}
private suspend fun getSsidTunnel(ssid: String): TunnelConfig? {
return appDataRepository.tunnels.findByTunnelNetworksName(ssid).firstOrNull()
}
private suspend fun manageVpn() {
networkEventsFlow.collectLatest { watcherState ->
val autoTunnel = "Auto-tunnel watcher"
if (!watcherState.settings.isAutoTunnelPaused) {
//delay for rapid network state changes and then collect latest
delay(Constants.WATCHER_COLLECTION_DELAY)
when {
watcherState.isEthernetConditionMet() -> {
Timber.i("$autoTunnel - tunnel on on ethernet condition met")
serviceManager.startVpnServiceForeground(this)
when (it) {
is NetworkStatus.Available -> {
Timber.d("Gained Wi-Fi connection")
isWifiConnected = true
}
watcherState.isMobileDataConditionMet() -> {
Timber.i("$autoTunnel - tunnel on on mobile data condition met")
serviceManager.startVpnServiceForeground(this, getMobileDataTunnel()?.id)
}
watcherState.isTunnelNotMobileDataPreferredConditionMet() -> {
getMobileDataTunnel()?.let {
Timber.i("$autoTunnel - tunnel connected on mobile data is not preferred condition met, switching to preferred")
serviceManager.startVpnServiceForeground(
this,
getMobileDataTunnel()?.id,
)
is NetworkStatus.CapabilitiesChanged -> {
Timber.d("Wifi capabilities changed")
isWifiConnected = true
if (!connecting && !disconnecting) {
Timber.d("Not connect and not disconnecting")
val ssid = wifiService.getNetworkName(it.networkCapabilities);
Timber.d("SSID: $ssid")
if (!setting.trustedNetworkSSIDs.contains(ssid) && vpnService.getState() == Tunnel.State.DOWN) {
Timber.d("Starting VPN Tunnel for untrusted network: $ssid")
startVPN()
} else if (!disconnecting && vpnService.getState() == Tunnel.State.UP && setting.trustedNetworkSSIDs.contains(
ssid
)
) {
Timber.d("Stopping VPN Tunnel for trusted network with ssid: $ssid")
stopVPN()
}
}
}
watcherState.isTunnelOffOnMobileDataConditionMet() -> {
Timber.i("$autoTunnel - tunnel off on mobile data met, turning vpn off")
serviceManager.stopVpnServiceForeground(this)
}
watcherState.isTunnelNotWifiNamePreferredMet(watcherState.currentNetworkSSID) -> {
Timber.i("$autoTunnel - tunnel on ssid not associated with current tunnel condition met")
getSsidTunnel(watcherState.currentNetworkSSID)?.let {
Timber.i("Found tunnel associated with this SSID, bringing tunnel up")
serviceManager.startVpnServiceForeground(this, it.id)
} ?: suspend {
Timber.i("No tunnel associated with this SSID, using defaults")
if (appDataRepository.getPrimaryOrFirstTunnel()?.name != vpnService.name) {
serviceManager.startVpnServiceForeground(this)
is NetworkStatus.Unavailable -> {
isWifiConnected = false
Timber.d("Lost Wi-Fi connection")
if(!connecting || !disconnecting) {
if(setting.isTunnelOnMobileDataEnabled && vpnService.getState() == Tunnel.State.DOWN
&& isMobileDataConnected){
Timber.d("Wifi not available so starting vpn for mobile data")
startVPN()
}
}.invoke()
}
if(!setting.isTunnelOnMobileDataEnabled && vpnService.getState() == Tunnel.State.UP) {
Timber.d("Lost WiFi connection, disabling vpn")
stopVPN()
}
}
watcherState.isUntrustedWifiConditionMet() -> {
Timber.i("$autoTunnel - tunnel on untrusted wifi condition met")
serviceManager.startVpnServiceForeground(
this,
getSsidTunnel(watcherState.currentNetworkSSID)?.id,
)
}
watcherState.isTrustedWifiConditionMet() -> {
Timber.i("$autoTunnel - tunnel off on trusted wifi condition met, turning vpn off")
serviceManager.stopVpnServiceForeground(this)
}
watcherState.isTunnelOffOnWifiConditionMet() -> {
Timber.i("$autoTunnel - tunnel off on wifi condition met, turning vpn off")
serviceManager.stopVpnServiceForeground(this)
}
watcherState.isTunnelOffOnNoConnectivityMet() -> {
Timber.i("$autoTunnel - tunnel off on no connectivity met, turning vpn off")
serviceManager.stopVpnServiceForeground(this)
}
else -> {
Timber.i("$autoTunnel - no condition met")
}
}
}
}
private fun startVPN() {
if(!connecting) {
connecting = true
ServiceTracker.actionOnService(
Action.START,
this.applicationContext as Application,
WireGuardTunnelService::class.java,
mapOf(getString(R.string.tunnel_extras_key) to tunnelId))
connecting = false
}
}
}
private fun stopVPN() {
if(!disconnecting) {
disconnecting = true
ServiceTracker.actionOnService(
Action.STOP,
this.applicationContext as Application,
WireGuardTunnelService::class.java
)
disconnecting = false
}
}
}
@@ -3,186 +3,139 @@ package com.zaneschepke.wireguardautotunnel.service.foreground
import android.app.PendingIntent
import android.content.Intent
import android.os.Bundle
import androidx.core.app.ServiceCompat
import androidx.lifecycle.lifecycleScope
import com.wireguard.android.backend.Tunnel
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
import com.zaneschepke.wireguardautotunnel.receiver.NotificationActionReceiver
import com.zaneschepke.wireguardautotunnel.repository.Repository
import com.zaneschepke.wireguardautotunnel.service.notification.NotificationService
import com.zaneschepke.wireguardautotunnel.service.tunnel.HandshakeStatus
import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnService
import com.zaneschepke.wireguardautotunnel.util.Constants
import com.zaneschepke.wireguardautotunnel.util.handshakeStatus
import com.zaneschepke.wireguardautotunnel.util.mapPeerStats
import com.zaneschepke.wireguardautotunnel.service.tunnel.model.Settings
import com.zaneschepke.wireguardautotunnel.service.tunnel.model.TunnelConfig
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject
@AndroidEntryPoint
class WireGuardTunnelService : ForegroundService() {
private val foregroundId = 123
private val foregroundId = 123;
@Inject
lateinit var vpnService: VpnService
lateinit var vpnService : VpnService
@Inject
lateinit var appDataRepository: AppDataRepository
lateinit var settingsRepo: Repository<Settings>
@Inject
lateinit var notificationService: NotificationService
lateinit var notificationService : NotificationService
private var job: Job? = null
private lateinit var job : Job
private var didShowConnected = false
private var tunnelName : String = ""
override fun onCreate() {
super.onCreate()
lifecycleScope.launch(Dispatchers.Main) {
//TODO fix this to not launch if AOVPN
if (appDataRepository.tunnels.count() != 0) {
launchVpnNotification()
}
}
}
override fun startService(extras: Bundle?) {
override fun startService(extras : Bundle?) {
super.startService(extras)
val tunnelConfigString = extras?.getString(getString(R.string.tunnel_extras_key))
cancelJob()
job =
lifecycleScope.launch(Dispatchers.IO) {
launch {
val tunnelId = extras?.getInt(Constants.TUNNEL_EXTRA_KEY)
if (vpnService.getState() == Tunnel.State.UP) {
vpnService.stopTunnel()
}
vpnService.startTunnel(
tunnelId?.let {
appDataRepository.tunnels.getById(it)
},
)
job = CoroutineScope(SupervisorJob()).launch {
if(tunnelConfigString != null) {
try {
val tunnelConfig = TunnelConfig.from(tunnelConfigString)
tunnelName = tunnelConfig.name
vpnService.startTunnel(tunnelConfig)
} catch (e : Exception) {
Timber.e("Problem starting tunnel: ${e.message}")
stopService(extras)
}
launch {
handshakeNotifications()
} else {
Timber.d("Tunnel config null, starting default tunnel")
val settings = settingsRepo.getAll();
if(!settings.isNullOrEmpty()) {
val setting = settings[0]
if(setting.defaultTunnel != null && setting.isAlwaysOnVpnEnabled) {
val tunnelConfig = TunnelConfig.from(setting.defaultTunnel!!)
tunnelName = tunnelConfig.name
vpnService.startTunnel(tunnelConfig)
}
}
}
}
//TODO improve tunnel notifications
private suspend fun handshakeNotifications() {
var tunnelName: String? = null
vpnService.vpnState.collect { state ->
state.statistics
?.mapPeerStats()
?.map { it.value?.handshakeStatus() }
.let { statuses ->
when {
statuses?.all { it == HandshakeStatus.HEALTHY } == true -> {
if (!didShowConnected) {
delay(Constants.VPN_CONNECTED_NOTIFICATION_DELAY)
tunnelName = state.tunnelConfig?.name
launchVpnNotification(
getString(R.string.tunnel_start_title),
"${getString(R.string.tunnel_start_text)} - $tunnelName",
)
didShowConnected = true
}
}
CoroutineScope(job).launch {
var didShowConnected = false
var didShowFailedHandshakeNotification = false
vpnService.handshakeStatus.collect {
when(it) {
HandshakeStatus.NOT_STARTED -> {
}
HandshakeStatus.NEVER_CONNECTED -> {
if(!didShowFailedHandshakeNotification) {
launchVpnConnectionFailedNotification(getString(R.string.initial_connection_failure_message))
didShowFailedHandshakeNotification = true
didShowConnected = false
}
statuses?.any { it == HandshakeStatus.STALE } == true -> {}
statuses?.all { it == HandshakeStatus.NOT_STARTED } ==
true -> {
}
HandshakeStatus.HEALTHY -> {
if(!didShowConnected) {
launchVpnConnectedNotification()
didShowConnected = true
}
}
HandshakeStatus.UNHEALTHY -> {
if(!didShowFailedHandshakeNotification) {
launchVpnConnectionFailedNotification(getString(R.string.lost_connection_failure_message))
didShowFailedHandshakeNotification = true
didShowConnected = false
}
else -> {}
}
}
if (state.status == Tunnel.State.UP && state.tunnelConfig?.name != tunnelName) {
tunnelName = state.tunnelConfig?.name
launchVpnNotification(
getString(R.string.tunnel_start_title),
"${getString(R.string.tunnel_start_text)} - $tunnelName",
)
}
}
}
private fun launchAlwaysOnDisabledNotification() {
launchVpnNotification(
title = this.getString(R.string.vpn_connection_failed),
description = this.getString(R.string.always_on_disabled),
)
}
override fun stopService() {
super.stopService()
lifecycleScope.launch(Dispatchers.IO) {
override fun stopService(extras : Bundle?) {
super.stopService(extras)
CoroutineScope(Dispatchers.IO).launch() {
vpnService.stopTunnel()
didShowConnected = false
}
cancelJob()
stopSelf()
}
private fun launchVpnNotification(
title: String = getString(R.string.vpn_starting),
description: String = getString(R.string.attempt_connection)
) {
val notification =
notificationService.createNotification(
channelId = getString(R.string.vpn_channel_id),
channelName = getString(R.string.vpn_channel_name),
title = title,
onGoing = false,
vibration = false,
showTimestamp = true,
description = description,
)
ServiceCompat.startForeground(
this,
foregroundId,
notification,
Constants.SYSTEM_EXEMPT_SERVICE_TYPE_ID,
private fun launchVpnConnectedNotification() {
val notification = notificationService.createNotification(
channelId = getString(R.string.vpn_channel_id),
channelName = getString(R.string.vpn_channel_name),
title = getString(R.string.tunnel_start_title),
onGoing = false,
showTimestamp = true,
description = "${getString(R.string.tunnel_start_text)} $tunnelName"
)
super.startForeground(foregroundId, notification)
}
private fun launchVpnConnectionFailedNotification(message: String) {
val notification =
notificationService.createNotification(
channelId = getString(R.string.vpn_channel_id),
channelName = getString(R.string.vpn_channel_name),
action =
PendingIntent.getBroadcast(
this,
0,
Intent(this, NotificationActionReceiver::class.java),
PendingIntent.FLAG_IMMUTABLE,
),
actionText = getString(R.string.restart),
title = getString(R.string.vpn_connection_failed),
onGoing = false,
vibration = true,
showTimestamp = true,
description = message,
)
ServiceCompat.startForeground(
this,
foregroundId,
notification,
Constants.SYSTEM_EXEMPT_SERVICE_TYPE_ID,
private fun launchVpnConnectionFailedNotification(message : String) {
val notification = notificationService.createNotification(
channelId = getString(R.string.vpn_channel_id),
channelName = getString(R.string.vpn_channel_name),
action = PendingIntent.getBroadcast(this,0,Intent(this, NotificationActionReceiver::class.java),PendingIntent.FLAG_IMMUTABLE),
actionText = getString(R.string.restart),
title = getString(R.string.vpn_connection_failed),
onGoing = false,
showTimestamp = true,
description = message
)
super.startForeground(foregroundId, notification)
}
private fun cancelJob() {
try {
job?.cancel()
} catch (e : CancellationException) {
Timber.i("Tunnel job cancelled")
if(this::job.isInitialized) {
job.cancel()
}
}
}
}
@@ -14,10 +14,8 @@ import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.map
abstract class BaseNetworkService<T : BaseNetworkService<T>>(
val context: Context,
networkCapability: Int
) : NetworkService<T> {
abstract class BaseNetworkService<T : BaseNetworkService<T>>(val context: Context, networkCapability : Int) : NetworkService<T> {
private val connectivityManager =
context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
@@ -25,73 +23,62 @@ abstract class BaseNetworkService<T : BaseNetworkService<T>>(
context.applicationContext.getSystemService(Context.WIFI_SERVICE) as WifiManager
override val networkStatus = callbackFlow {
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))
}
val networkStatusCallback = when (Build.VERSION.SDK_INT) {
in Build.VERSION_CODES.S..Int.MAX_VALUE -> {
object : ConnectivityManager.NetworkCallback(
FLAG_INCLUDE_LOCATION_INFO
) {
override fun onLost(network: Network) {
trySend(NetworkStatus.Unavailable(network))
}
override fun onCapabilitiesChanged(
network: Network,
networkCapabilities: NetworkCapabilities
) {
trySend(
NetworkStatus.CapabilitiesChanged(
network,
networkCapabilities,
),
)
}
override fun onAvailable(network: Network) {
trySend(NetworkStatus.Available(network))
}
}
else -> {
object : ConnectivityManager.NetworkCallback() {
override fun onAvailable(network: Network) {
trySend(NetworkStatus.Available(network))
}
override fun onLost(network: Network) {
trySend(NetworkStatus.Unavailable(network))
}
override fun onLost(network: Network) {
trySend(NetworkStatus.Unavailable(network))
}
override fun onCapabilitiesChanged(
network: Network,
networkCapabilities: NetworkCapabilities
) {
trySend(
NetworkStatus.CapabilitiesChanged(
network,
networkCapabilities,
),
)
}
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()
else -> {
object : ConnectivityManager.NetworkCallback() {
override fun onAvailable(network: Network) {
trySend(NetworkStatus.Available(network))
}
override fun onLost(network: Network) {
trySend(NetworkStatus.Unavailable(network))
}
override fun onCapabilitiesChanged(
network: Network,
networkCapabilities: NetworkCapabilities
) {
trySend(NetworkStatus.CapabilitiesChanged(network, networkCapabilities))
}
}
}
}
val request = NetworkRequest.Builder()
.addTransportType(networkCapability)
.build()
connectivityManager.registerNetworkCallback(request, networkStatusCallback)
awaitClose { connectivityManager.unregisterNetworkCallback(networkStatusCallback) }
awaitClose {
connectivityManager.unregisterNetworkCallback(networkStatusCallback)
}
}
override fun getNetworkName(networkCapabilities: NetworkCapabilities): String? {
var ssid: String? = getWifiNameFromCapabilities(networkCapabilities)
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.R) {
var ssid : String? = getWifiNameFromCapabilities(networkCapabilities)
if((Build.VERSION.SDK_INT == Build.VERSION_CODES.Q) || (Build.VERSION.SDK_INT == Build.VERSION_CODES.R)) {
val info = wifiManager.connectionInfo
if (info.supplicantState === SupplicantState.COMPLETED) {
ssid = info.ssid
@@ -100,33 +87,28 @@ abstract class BaseNetworkService<T : BaseNetworkService<T>>(
return ssid?.trim('"')
}
companion object {
private fun getWifiNameFromCapabilities(networkCapabilities: NetworkCapabilities): String? {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
val info: WifiInfo
if (networkCapabilities.transportInfo is WifiInfo) {
info = networkCapabilities.transportInfo as WifiInfo
return info.ssid
}
private fun getWifiNameFromCapabilities(networkCapabilities: NetworkCapabilities) : String? {
val info : WifiInfo
if(networkCapabilities.transportInfo is WifiInfo) {
info = networkCapabilities.transportInfo as WifiInfo
} else {
return null
}
return null
return info.ssid
}
}
}
inline fun <Result> Flow<NetworkStatus>.map(
crossinline onUnavailable: suspend (network: Network) -> Result,
crossinline onAvailable: suspend (network: Network) -> Result,
crossinline onCapabilitiesChanged:
suspend (network: Network, networkCapabilities: NetworkCapabilities) -> Result
crossinline onUnavailable: suspend (network : Network) -> 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(status.network)
is NetworkStatus.Available -> onAvailable(status.network)
is NetworkStatus.CapabilitiesChanged ->
onCapabilitiesChanged(
status.network,
status.networkCapabilities,
)
is NetworkStatus.CapabilitiesChanged -> onCapabilitiesChanged(status.network, status.networkCapabilities)
}
}
}
@@ -1,9 +0,0 @@
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)
@@ -6,4 +6,5 @@ import dagger.hilt.android.qualifiers.ApplicationContext
import javax.inject.Inject
class MobileDataService @Inject constructor(@ApplicationContext context: Context) :
BaseNetworkService<MobileDataService>(context, NetworkCapabilities.TRANSPORT_CELLULAR)
BaseNetworkService<MobileDataService>(context, NetworkCapabilities.TRANSPORT_CELLULAR) {
}
@@ -4,7 +4,7 @@ import android.net.NetworkCapabilities
import kotlinx.coroutines.flow.Flow
interface NetworkService<T> {
fun getNetworkName(networkCapabilities: NetworkCapabilities): String?
fun getNetworkName(networkCapabilities: NetworkCapabilities) : String?
val networkStatus : Flow<NetworkStatus>
val networkStatus: Flow<NetworkStatus>
}
}
@@ -4,10 +4,7 @@ import android.net.Network
import android.net.NetworkCapabilities
sealed class NetworkStatus {
class Available(val network: Network) : NetworkStatus()
class Unavailable(val network: Network) : NetworkStatus()
class CapabilitiesChanged(val network: Network, val networkCapabilities: NetworkCapabilities) :
NetworkStatus()
class Available(val network : Network) : NetworkStatus()
class Unavailable(val network : Network) : NetworkStatus()
class CapabilitiesChanged(val network : Network, val networkCapabilities : NetworkCapabilities) : NetworkStatus()
}
@@ -6,4 +6,5 @@ import dagger.hilt.android.qualifiers.ApplicationContext
import javax.inject.Inject
class WifiService @Inject constructor(@ApplicationContext context: Context) :
BaseNetworkService<WifiService>(context, NetworkCapabilities.TRANSPORT_WIFI)
BaseNetworkService<WifiService>(context, NetworkCapabilities.TRANSPORT_WIFI) {
}
@@ -12,11 +12,10 @@ interface NotificationService {
action: PendingIntent? = null,
actionText: String? = null,
description: String,
showTimestamp: Boolean = false,
showTimestamp : Boolean = false,
importance: Int = NotificationManager.IMPORTANCE_HIGH,
vibration: Boolean = false,
vibration: Boolean = true,
onGoing: Boolean = true,
lights: Boolean = true,
onlyAlertOnce: Boolean = true,
lights: Boolean = true
): Notification
}
}
@@ -7,27 +7,14 @@ 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
class WireGuardNotification @Inject constructor(@ApplicationContext private val context: Context) : NotificationService {
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),
)
private val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager;
override fun createNotification(
channelId: String,
@@ -40,23 +27,20 @@ class WireGuardNotification @Inject constructor(@ApplicationContext private val
importance: Int,
vibration: Boolean,
onGoing: Boolean,
lights: Boolean,
onlyAlertOnce: Boolean,
lights: 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
}
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, 400, 500, 400, 300, 200, 400)
it
}
notificationManager.createNotificationChannel(channel)
val pendingIntent: PendingIntent =
Intent(context, MainActivity::class.java).let { notificationIntent ->
@@ -64,38 +48,30 @@ class WireGuardNotification @Inject constructor(@ApplicationContext private val
context,
0,
notificationIntent,
PendingIntent.FLAG_IMMUTABLE,
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,
)
}
}
val builder: Notification.Builder =
Notification.Builder(
context,
channelId
)
return builder.let {
if (action != null && actionText != null) {
if(action != null && actionText != null) {
//TODO find a not deprecated way to do this
it.addAction(
NotificationCompat.Action.Builder(0, actionText, action).build(),
)
it.setAutoCancel(true)
Notification.Action.Builder(0, actionText, action)
.build())
it.setAutoCancel(true)
}
it.setContentTitle(title)
it.setContentTitle(title)
.setContentText(description)
.setOnlyAlertOnce(onlyAlertOnce)
.setContentIntent(pendingIntent)
.setOngoing(onGoing)
.setPriority(NotificationCompat.PRIORITY_HIGH)
.setShowWhen(showTimestamp)
.setSmallIcon(R.drawable.ic_launcher)
.setSmallIcon(R.mipmap.ic_launcher_foreground)
.build()
}
}
}
}
@@ -1,75 +0,0 @@
package com.zaneschepke.wireguardautotunnel.service.shortcut
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.lifecycle.lifecycleScope
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
import com.zaneschepke.wireguardautotunnel.service.foreground.Action
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
import com.zaneschepke.wireguardautotunnel.service.foreground.WireGuardConnectivityWatcherService
import com.zaneschepke.wireguardautotunnel.service.foreground.WireGuardTunnelService
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import javax.inject.Inject
@AndroidEntryPoint
class ShortcutsActivity : ComponentActivity() {
@Inject
lateinit var appDataRepository: AppDataRepository
@Inject
lateinit var serviceManager: ServiceManager
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
lifecycleScope.launch(Dispatchers.Main) {
val settings = appDataRepository.settings.getSettings()
if (settings.isShortcutsEnabled) {
when (intent.getStringExtra(CLASS_NAME_EXTRA_KEY)) {
WireGuardTunnelService::class.java.simpleName -> {
val tunnelName = intent.getStringExtra(TUNNEL_NAME_EXTRA_KEY)
val tunnelConfig = tunnelName?.let {
appDataRepository.tunnels.getAll().firstOrNull {
it.name == tunnelName
}
}
when (intent.action) {
Action.START.name -> serviceManager.startVpnServiceForeground(
this@ShortcutsActivity, tunnelConfig?.id, isManualStart = true,
)
Action.STOP.name -> serviceManager.stopVpnServiceForeground(
this@ShortcutsActivity,
isManualStop = true,
)
}
}
WireGuardConnectivityWatcherService::class.java.simpleName -> {
when (intent.action) {
Action.START.name -> appDataRepository.settings.save(
settings.copy(
isAutoTunnelPaused = false,
),
)
Action.STOP.name -> appDataRepository.settings.save(
settings.copy(
isAutoTunnelPaused = true,
),
)
}
}
}
}
}
finish()
}
companion object {
const val TUNNEL_NAME_EXTRA_KEY = "tunnelName"
const val CLASS_NAME_EXTRA_KEY = "className"
}
}
@@ -1,111 +0,0 @@
package com.zaneschepke.wireguardautotunnel.service.tile
import android.os.Build
import android.service.quicksettings.Tile
import android.service.quicksettings.TileService
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject
@AndroidEntryPoint
class AutoTunnelControlTile : TileService() {
@Inject
lateinit var appDataRepository: AppDataRepository
@Inject
lateinit var serviceManager: ServiceManager
private val scope = CoroutineScope(Dispatchers.IO)
private var manualStartConfig: TunnelConfig? = null
override fun onStartListening() {
super.onStartListening()
scope.launch {
appDataRepository.settings.getSettingsFlow().collectLatest {
when (it.isAutoTunnelEnabled) {
true -> {
if (it.isAutoTunnelPaused) {
setInactive()
setTileDescription(this@AutoTunnelControlTile.getString(R.string.paused))
} else {
setActive()
setTileDescription(this@AutoTunnelControlTile.getString(R.string.active))
}
}
false -> {
setTileDescription(this@AutoTunnelControlTile.getString(R.string.disabled))
setUnavailable()
}
}
}
}
}
override fun onTileAdded() {
super.onTileAdded()
onStartListening()
}
override fun onDestroy() {
super.onDestroy()
scope.cancel()
}
override fun onTileRemoved() {
super.onTileRemoved()
scope.cancel()
}
override fun onClick() {
super.onClick()
unlockAndRun {
scope.launch {
try {
appDataRepository.toggleWatcherServicePause()
} catch (e: Exception) {
Timber.e(e.message)
} finally {
cancel()
}
}
}
}
private fun setActive() {
qsTile.state = Tile.STATE_ACTIVE
qsTile.updateTile()
}
private fun setInactive() {
qsTile.state = Tile.STATE_INACTIVE
qsTile.updateTile()
}
private fun setUnavailable() {
manualStartConfig = null
qsTile.state = Tile.STATE_UNAVAILABLE
qsTile.updateTile()
}
private fun setTileDescription(description: String) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
qsTile.subtitle = description
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
qsTile.stateDescription = description
}
qsTile.updateTile()
}
}
@@ -1,126 +0,0 @@
package com.zaneschepke.wireguardautotunnel.service.tile
import android.os.Build
import android.service.quicksettings.Tile
import android.service.quicksettings.TileService
import com.wireguard.android.backend.Tunnel
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnService
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject
@AndroidEntryPoint
class TunnelControlTile : TileService() {
@Inject
lateinit var appDataRepository: AppDataRepository
@Inject
lateinit var vpnService: VpnService
@Inject
lateinit var serviceManager: ServiceManager
private val scope = CoroutineScope(Dispatchers.IO)
private var manualStartConfig: TunnelConfig? = null
override fun onStartListening() {
super.onStartListening()
Timber.d("On start listening called")
scope.launch {
vpnService.vpnState.collect { it ->
when (it.status) {
Tunnel.State.UP -> {
setActive()
it.tunnelConfig?.name?.let { name -> setTileDescription(name) }
}
Tunnel.State.DOWN -> {
setInactive()
val config = appDataRepository.getStartTunnelConfig()?.also { config ->
manualStartConfig = config
} ?: appDataRepository.getPrimaryOrFirstTunnel()
config?.let {
setTileDescription(it.name)
} ?: setUnavailable()
}
else -> setInactive()
}
}
}
}
override fun onDestroy() {
super.onDestroy()
scope.cancel()
}
override fun onTileRemoved() {
super.onTileRemoved()
scope.cancel()
}
override fun onTileAdded() {
super.onTileAdded()
onStartListening()
}
override fun onClick() {
super.onClick()
unlockAndRun {
scope.launch {
try {
if (vpnService.getState() == Tunnel.State.UP) {
serviceManager.stopVpnServiceForeground(
this@TunnelControlTile,
isManualStop = true,
)
} else {
serviceManager.startVpnServiceForeground(
this@TunnelControlTile, manualStartConfig?.id, isManualStart = true,
)
}
} catch (e: Exception) {
Timber.e(e.message)
} finally {
cancel()
}
}
}
}
private fun setActive() {
qsTile.state = Tile.STATE_ACTIVE
qsTile.updateTile()
}
private fun setInactive() {
qsTile.state = Tile.STATE_INACTIVE
qsTile.updateTile()
}
private fun setUnavailable() {
manualStartConfig = null
qsTile.state = Tile.STATE_UNAVAILABLE
qsTile.updateTile()
}
private fun setTileDescription(description: String) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
qsTile.subtitle = description
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
qsTile.stateDescription = description
}
qsTile.updateTile()
}
}
@@ -2,15 +2,13 @@ package com.zaneschepke.wireguardautotunnel.service.tunnel
enum class HandshakeStatus {
HEALTHY,
STALE,
UNKNOWN,
UNHEALTHY,
NEVER_CONNECTED,
NOT_STARTED;
companion object {
private const val WG_TYPICAL_HANDSHAKE_INTERVAL_WHEN_HEALTHY_SEC = 180
const val STATUS_CHANGE_TIME_BUFFER = 30
const val STALE_TIME_LIMIT_SEC =
WG_TYPICAL_HANDSHAKE_INTERVAL_WHEN_HEALTHY_SEC + STATUS_CHANGE_TIME_BUFFER
private const val WG_TYPICAL_HANDSHAKE_INTERVAL_WHEN_HEALTHY_SEC = 120
const val UNHEALTHY_TIME_LIMIT_SEC = WG_TYPICAL_HANDSHAKE_INTERVAL_WHEN_HEALTHY_SEC + 60
const val NEVER_CONNECTED_TO_UNHEALTHY_TIME_LIMIT_SEC = 30
}
}
}
@@ -1,15 +1,18 @@
package com.zaneschepke.wireguardautotunnel.service.tunnel
import com.wireguard.android.backend.Statistics
import com.wireguard.android.backend.Tunnel
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
import kotlinx.coroutines.flow.StateFlow
import com.wireguard.crypto.Key
import com.zaneschepke.wireguardautotunnel.service.tunnel.model.TunnelConfig
import kotlinx.coroutines.flow.SharedFlow
interface VpnService : Tunnel {
suspend fun startTunnel(tunnelConfig: TunnelConfig? = null): Tunnel.State
suspend fun startTunnel(tunnelConfig : TunnelConfig) : Tunnel.State
suspend fun stopTunnel()
val vpnState: StateFlow<VpnState>
fun getState(): Tunnel.State
}
val state : SharedFlow<Tunnel.State>
val tunnelName : SharedFlow<String>
val statistics : SharedFlow<Statistics>
val lastHandshake : SharedFlow<Map<Key,Long>>
val handshakeStatus : SharedFlow<HandshakeStatus>
fun getState() : Tunnel.State
}
@@ -1,11 +0,0 @@
package com.zaneschepke.wireguardautotunnel.service.tunnel
import com.wireguard.android.backend.Statistics
import com.wireguard.android.backend.Tunnel
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
data class VpnState(
val status: Tunnel.State = Tunnel.State.DOWN,
val tunnelConfig: TunnelConfig? = null,
val statistics: Statistics? = null
)
@@ -3,143 +3,127 @@ package com.zaneschepke.wireguardautotunnel.service.tunnel
import com.wireguard.android.backend.Backend
import com.wireguard.android.backend.BackendException
import com.wireguard.android.backend.Statistics
import com.wireguard.android.backend.Tunnel.State
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
import com.zaneschepke.wireguardautotunnel.module.Kernel
import com.zaneschepke.wireguardautotunnel.module.Userspace
import com.zaneschepke.wireguardautotunnel.util.Constants
import kotlinx.coroutines.CancellationException
import com.wireguard.android.backend.Tunnel
import com.wireguard.crypto.Key
import com.zaneschepke.wireguardautotunnel.service.tunnel.model.TunnelConfig
import com.zaneschepke.wireguardautotunnel.util.NumberUtils
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject
class WireGuardTunnel
@Inject
constructor(
@Userspace private val userspaceBackend: Backend,
@Kernel private val kernelBackend: Backend,
private val appDataRepository: AppDataRepository,
class WireGuardTunnel @Inject constructor(private val backend : Backend,
) : VpnService {
private val _vpnState = MutableStateFlow(VpnState())
override val vpnState: StateFlow<VpnState> = _vpnState.asStateFlow()
private val scope = CoroutineScope(Dispatchers.IO)
private val _tunnelName = MutableStateFlow("")
override val tunnelName get() = _tunnelName.asStateFlow()
private var statsJob: Job? = null
private val _state = MutableSharedFlow<Tunnel.State>(
replay = 1)
private var backend: Backend = userspaceBackend
private val _handshakeStatus = MutableSharedFlow<HandshakeStatus>(replay = 1,
onBufferOverflow = BufferOverflow.DROP_OLDEST)
override val state get() = _state.asSharedFlow()
private var backendIsUserspace = true
private val _statistics = MutableSharedFlow<Statistics>(replay = 1)
override val statistics get() = _statistics.asSharedFlow()
init {
scope.launch {
appDataRepository.settings.getSettingsFlow().collect {
if (it.isKernelEnabled && backendIsUserspace) {
Timber.d("Setting kernel backend")
backend = kernelBackend
backendIsUserspace = false
} else if (!it.isKernelEnabled && !backendIsUserspace) {
Timber.d("Setting userspace backend")
backend = userspaceBackend
backendIsUserspace = true
}
}
}
}
private val _lastHandshake = MutableSharedFlow<Map<Key, Long>>(replay = 1)
override val lastHandshake get() = _lastHandshake.asSharedFlow()
override suspend fun startTunnel(tunnelConfig: TunnelConfig?): State {
override val handshakeStatus: SharedFlow<HandshakeStatus>
get() = _handshakeStatus.asSharedFlow()
private lateinit var statsJob : Job
override suspend fun startTunnel(tunnelConfig: TunnelConfig) : Tunnel.State{
return try {
//TODO we need better error handling here
val config = tunnelConfig ?: appDataRepository.getPrimaryOrFirstTunnel()
if (config != null) {
emitTunnelConfig(config)
val wgConfig = TunnelConfig.configFromQuick(config.wgQuick)
val state =
backend.setState(
this,
State.UP,
wgConfig,
)
state
} else throw Exception("No tunnels")
} catch (e: BackendException) {
if(getState() == Tunnel.State.UP && _tunnelName.value != tunnelConfig.name) {
stopTunnel()
}
_tunnelName.emit(tunnelConfig.name)
val config = TunnelConfig.configFromQuick(tunnelConfig.wgQuick)
val state = backend.setState(
this, Tunnel.State.UP, config)
_state.emit(state)
state;
} catch (e : Exception) {
Timber.e("Failed to start tunnel with error: ${e.message}")
State.DOWN
Tunnel.State.DOWN
}
}
private fun emitTunnelState(state: State) {
_vpnState.tryEmit(
_vpnState.value.copy(
status = state,
),
)
override fun getName(): String {
return _tunnelName.value
}
private fun emitBackendStatistics(statistics: Statistics) {
_vpnState.tryEmit(
_vpnState.value.copy(
statistics = statistics,
),
)
}
private suspend fun emitTunnelConfig(tunnelConfig: TunnelConfig?) {
_vpnState.emit(
_vpnState.value.copy(
tunnelConfig = tunnelConfig,
),
)
}
override suspend fun stopTunnel() {
override suspend fun stopTunnel() {
try {
if (getState() == State.UP) {
val state = backend.setState(this, State.DOWN, null)
emitTunnelState(state)
if(getState() == Tunnel.State.UP) {
val state = backend.setState(this, Tunnel.State.DOWN, null)
_state.emit(state)
}
} catch (e: BackendException) {
} catch (e : BackendException) {
Timber.e("Failed to stop tunnel with error: ${e.message}")
}
}
override fun getState(): State {
override fun getState(): Tunnel.State {
return backend.getState(this)
}
override fun getName(): String {
return _vpnState.value.tunnelConfig?.name ?: ""
}
override fun onStateChange(state: State) {
val tunnel = this
emitTunnelState(state)
WireGuardAutoTunnel.requestTunnelTileServiceStateUpdate(WireGuardAutoTunnel.instance)
if (state == State.UP) {
statsJob =
scope.launch {
while (true) {
val statistics = backend.getStatistics(tunnel)
emitBackendStatistics(statistics)
delay(Constants.VPN_STATISTIC_CHECK_INTERVAL)
override fun onStateChange(state : Tunnel.State) {
val tunnel = this;
_state.tryEmit(state)
if(state == Tunnel.State.UP) {
statsJob = CoroutineScope(Dispatchers.IO).launch {
val handshakeMap = HashMap<Key, Long>()
var neverHadHandshakeCounter = 0
while (true) {
val statistics = backend.getStatistics(tunnel)
_statistics.emit(statistics)
statistics.peers().forEach {
val handshakeEpoch = statistics.peer(it)?.latestHandshakeEpochMillis ?: 0L
handshakeMap[it] = handshakeEpoch
if(handshakeEpoch == 0L) {
if(neverHadHandshakeCounter >= HandshakeStatus.NEVER_CONNECTED_TO_UNHEALTHY_TIME_LIMIT_SEC) {
_handshakeStatus.emit(HandshakeStatus.NEVER_CONNECTED)
} else {
_handshakeStatus.emit(HandshakeStatus.NOT_STARTED)
}
if(neverHadHandshakeCounter <= HandshakeStatus.NEVER_CONNECTED_TO_UNHEALTHY_TIME_LIMIT_SEC) {
neverHadHandshakeCounter++
}
return@forEach
}
if(NumberUtils.getSecondsBetweenTimestampAndNow(handshakeEpoch) >= HandshakeStatus.UNHEALTHY_TIME_LIMIT_SEC) {
_handshakeStatus.emit(HandshakeStatus.UNHEALTHY)
} else {
_handshakeStatus.emit(HandshakeStatus.HEALTHY)
}
}
_lastHandshake.emit(handshakeMap)
delay(1000)
}
}
if (state == State.DOWN) {
try {
statsJob?.cancel()
} catch (e : CancellationException) {
Timber.i("Stats job cancelled")
}
}
if(state == Tunnel.State.DOWN) {
if(this::statsJob.isInitialized) {
statsJob.cancel()
}
_handshakeStatus.tryEmit(HandshakeStatus.NOT_STARTED)
_lastHandshake.tryEmit(emptyMap())
}
}
}
}
@@ -0,0 +1,15 @@
package com.zaneschepke.wireguardautotunnel.service.tunnel.model
import io.objectbox.annotation.Entity
import io.objectbox.annotation.Id
@Entity
data class Settings(
@Id
var id : Long = 0,
var isAutoTunnelEnabled : Boolean = false,
var isTunnelOnMobileDataEnabled : Boolean = false,
var trustedNetworkSSIDs : MutableList<String> = mutableListOf(),
var defaultTunnel : String? = null,
var isAlwaysOnVpnEnabled : Boolean = false
)
@@ -0,0 +1,89 @@
package com.zaneschepke.wireguardautotunnel.service.tunnel.model
import com.wireguard.config.Config
import io.objectbox.annotation.ConflictStrategy
import io.objectbox.annotation.Entity
import io.objectbox.annotation.Id
import io.objectbox.annotation.Unique
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import java.io.InputStream
@Entity
@Serializable
data class TunnelConfig(
@Id
var id : Long = 0,
@Unique(onConflict = ConflictStrategy.REPLACE)
var name : String,
var wgQuick : String
) {
override fun toString(): String {
return Json.encodeToString(serializer(), this)
}
companion object {
private const val INCLUDED_APPLICATIONS = "IncludedApplications = "
private const val EXCLUDED_APPLICATIONS = "ExcludedApplications = "
private const val INTERFACE = "[Interface]"
private const val NEWLINE_CHAR = "\n"
private const val APP_CONFIG_SEPARATOR = ", "
private fun addApplicationsToConfig(appConfig : String, wgQuick : String) : String {
val configList = wgQuick.split(NEWLINE_CHAR).toMutableList()
val interfaceIndex = configList.indexOf(INTERFACE)
configList.add(interfaceIndex + 1, appConfig)
return configList.joinToString(NEWLINE_CHAR)
}
fun clearAllApplicationsFromConfig(wgQuick : String) : String {
val configList = wgQuick.split(NEWLINE_CHAR).toMutableList()
val itr = configList.iterator()
while (itr.hasNext()) {
val next = itr.next()
if(next.contains(INCLUDED_APPLICATIONS) || next.contains(EXCLUDED_APPLICATIONS)) {
itr.remove()
}
}
return configList.joinToString(NEWLINE_CHAR)
}
fun setExcludedApplicationsOnQuick(packages : List<String>, wgQuick: String) : String {
if(packages.isEmpty()) {
return wgQuick
}
val clearedWgQuick = clearAllApplicationsFromConfig(wgQuick)
val excludeConfig = buildExcludedApplicationsString(packages)
return addApplicationsToConfig(excludeConfig, clearedWgQuick)
}
fun setIncludedApplicationsOnQuick(packages : List<String>, wgQuick: String) : String {
if(packages.isEmpty()) {
return wgQuick
}
val clearedWgQuick = clearAllApplicationsFromConfig(wgQuick)
val includeConfig = buildIncludedApplicationsString(packages)
return addApplicationsToConfig(includeConfig, clearedWgQuick)
}
private fun buildExcludedApplicationsString(packages : List<String>) : String {
return EXCLUDED_APPLICATIONS + packages.joinToString(APP_CONFIG_SEPARATOR)
}
private fun buildIncludedApplicationsString(packages : List<String>) : String {
return INCLUDED_APPLICATIONS + packages.joinToString(APP_CONFIG_SEPARATOR)
}
fun from(string : String) : TunnelConfig {
return Json.decodeFromString<TunnelConfig>(string)
}
fun configFromQuick(wgQuick: String): Config {
val inputStream: InputStream = wgQuick.byteInputStream()
val reader = inputStream.bufferedReader(Charsets.UTF_8)
return Config.parse(reader)
}
}
}
@@ -1,9 +0,0 @@
package com.zaneschepke.wireguardautotunnel.ui
data class AppUiState(
val snackbarMessage: String = "",
val snackbarMessageConsumed: Boolean = true,
val vpnPermissionAccepted: Boolean = false,
val notificationPermissionAccepted: Boolean = false,
val requestPermissions: Boolean = false
)
@@ -1,148 +0,0 @@
package com.zaneschepke.wireguardautotunnel.ui
import android.app.Application
import android.content.ActivityNotFoundException
import android.content.Intent
import android.net.Uri
import android.widget.Toast
import androidx.compose.runtime.mutableStateListOf
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.wireguard.android.backend.GoBackend
import com.zaneschepke.logcatter.Logcatter
import com.zaneschepke.logcatter.model.LogMessage
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
import com.zaneschepke.wireguardautotunnel.util.Constants
import com.zaneschepke.wireguardautotunnel.util.FileUtils
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
import timber.log.Timber
import java.time.Instant
import javax.inject.Inject
@HiltViewModel
class AppViewModel
@Inject
constructor(
private val application: Application,
) : ViewModel() {
val vpnIntent: Intent? = GoBackend.VpnService.prepare(WireGuardAutoTunnel.instance)
private val _appUiState = MutableStateFlow(
AppUiState(
vpnPermissionAccepted = vpnIntent == null,
),
)
val appUiState = _appUiState.asStateFlow()
fun isRequiredPermissionGranted(): Boolean {
val allAccepted =
(_appUiState.value.vpnPermissionAccepted && _appUiState.value.vpnPermissionAccepted)
if (!allAccepted) requestPermissions()
return allAccepted
}
private fun requestPermissions() {
_appUiState.value = _appUiState.value.copy(
requestPermissions = true,
)
}
fun permissionsRequested() {
_appUiState.value = _appUiState.value.copy(
requestPermissions = false,
)
}
fun openWebPage(url: String) {
try {
val webpage: Uri = Uri.parse(url)
val intent = Intent(Intent.ACTION_VIEW, webpage).apply {
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
}
application.startActivity(intent)
} catch (e: ActivityNotFoundException) {
Timber.e(e)
showSnackbarMessage(application.getString(R.string.no_browser_detected))
}
}
fun onVpnPermissionAccepted() {
_appUiState.value = _appUiState.value.copy(
vpnPermissionAccepted = true,
)
}
fun launchEmail() {
try {
val intent =
Intent(Intent.ACTION_SENDTO).apply {
type = Constants.EMAIL_MIME_TYPE
putExtra(Intent.EXTRA_EMAIL, arrayOf(application.getString(R.string.my_email)))
putExtra(Intent.EXTRA_SUBJECT, application.getString(R.string.email_subject))
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
}
application.startActivity(
Intent.createChooser(intent, application.getString(R.string.email_chooser)).apply {
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
},
)
} catch (e: ActivityNotFoundException) {
Timber.e(e)
showSnackbarMessage(application.getString(R.string.no_email_detected))
}
}
fun showSnackbarMessage(message: String) {
_appUiState.value = _appUiState.value.copy(
snackbarMessage = message,
snackbarMessageConsumed = false,
)
}
fun snackbarMessageConsumed() {
_appUiState.value = _appUiState.value.copy(
snackbarMessage = "",
snackbarMessageConsumed = true,
)
}
val logs = mutableStateListOf<LogMessage>()
fun readLogCatOutput() =
viewModelScope.launch(viewModelScope.coroutineContext + Dispatchers.IO) {
launch {
Logcatter.logs(callback = {
logs.add(it)
if (logs.size > Constants.LOG_BUFFER_SIZE) {
logs.removeRange(0, (logs.size - Constants.LOG_BUFFER_SIZE).toInt())
}
})
}
}
fun clearLogs() {
logs.clear()
Logcatter.clear()
}
fun saveLogsToFile() {
val fileName = "${Constants.BASE_LOG_FILE_NAME}-${Instant.now().epochSecond}.txt"
val content = logs.joinToString(separator = "\n")
FileUtils.saveFileToDownloads(application.applicationContext, content, fileName)
Toast.makeText(application, application.getString(R.string.logs_saved), Toast.LENGTH_SHORT)
.show()
}
fun setNotificationPermissionAccepted(accepted: Boolean) {
_appUiState.value = _appUiState.value.copy(
notificationPermissionAccepted = accepted,
)
}
}
@@ -1,5 +0,0 @@
package com.zaneschepke.wireguardautotunnel.ui
import com.journeyapps.barcodescanner.CaptureActivity
class CaptureActivityPortrait : CaptureActivity()
@@ -1,265 +1,172 @@
package com.zaneschepke.wireguardautotunnel.ui
import android.Manifest
import android.content.Intent
import android.net.Uri
import android.os.Build
import android.os.Bundle
import androidx.activity.SystemBarStyle
import android.provider.Settings
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity
import androidx.compose.foundation.focusable
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme
import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn
import androidx.compose.animation.slideInHorizontally
import androidx.compose.material3.Scaffold
import androidx.compose.material3.SnackbarData
import androidx.compose.material3.SnackbarDuration
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.SnackbarResult
import androidx.compose.material3.surfaceColorAtElevation
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusProperties
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.lifecycleScope
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.currentBackStackEntryAsState
import androidx.navigation.compose.rememberNavController
import androidx.compose.runtime.setValue
import com.google.accompanist.navigation.animation.AnimatedNavHost
import com.google.accompanist.navigation.animation.composable
import com.google.accompanist.navigation.animation.rememberAnimatedNavController
import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.google.accompanist.permissions.isGranted
import com.google.accompanist.permissions.rememberPermissionState
import com.wireguard.android.backend.GoBackend
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
import com.zaneschepke.wireguardautotunnel.data.datastore.DataStoreManager
import com.zaneschepke.wireguardautotunnel.data.repository.SettingsRepository
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
import com.zaneschepke.wireguardautotunnel.ui.common.PermissionRequestFailedScreen
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.BottomNavBar
import com.zaneschepke.wireguardautotunnel.ui.common.prompt.CustomSnackBar
import com.zaneschepke.wireguardautotunnel.ui.screens.config.ConfigScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.detail.DetailScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.main.MainScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.options.OptionsScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.pinlock.PinLockScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.SettingsScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.support.SupportScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.support.logs.LogsScreen
import com.zaneschepke.wireguardautotunnel.ui.theme.TransparentSystemBars
import com.zaneschepke.wireguardautotunnel.ui.theme.WireguardAutoTunnelTheme
import com.zaneschepke.wireguardautotunnel.util.StringValue
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import xyz.teamgravity.pin_lock_compose.PinManager
import javax.inject.Inject
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
@Inject
lateinit var dataStoreManager: DataStoreManager
@Inject
lateinit var settingsRepository: SettingsRepository
@Inject
lateinit var serviceManager: ServiceManager
@OptIn(
ExperimentalPermissionsApi::class,
@OptIn(ExperimentalAnimationApi::class,
ExperimentalPermissionsApi::class
)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge(navigationBarStyle = SystemBarStyle.dark(Color.Transparent.toArgb()))
// load preferences into memory and init data
lifecycleScope.launch {
dataStoreManager.init()
WireGuardAutoTunnel.requestTunnelTileServiceStateUpdate(this@MainActivity)
val settings = settingsRepository.getSettings()
if (settings.isAutoTunnelEnabled) {
serviceManager.startWatcherService(application.applicationContext)
}
}
setContent {
val appViewModel = hiltViewModel<AppViewModel>()
val appUiState by appViewModel.appUiState.collectAsStateWithLifecycle()
val navController = rememberNavController()
val navBackStackEntry by navController.currentBackStackEntryAsState()
val navController = rememberAnimatedNavController()
WireguardAutoTunnelTheme {
TransparentSystemBars()
val notificationPermissionState =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU)
rememberPermissionState(Manifest.permission.POST_NOTIFICATIONS) else null
val snackbarHostState = remember { SnackbarHostState() }
val snackbarHostState = remember { SnackbarHostState() }
val notificationPermissionState =
rememberPermissionState(Manifest.permission.POST_NOTIFICATIONS)
val vpnActivityResultState =
rememberLauncherForActivityResult(
fun requestNotificationPermission() {
if (!notificationPermissionState.status.isGranted && Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
notificationPermissionState.launchPermissionRequest()
}
}
var vpnIntent by remember { mutableStateOf(GoBackend.VpnService.prepare(this)) }
val vpnActivityResultState = rememberLauncherForActivityResult(
ActivityResultContracts.StartActivityForResult(),
onResult = {
val accepted = (it.resultCode == RESULT_OK)
if (accepted) {
appViewModel.onVpnPermissionAccepted()
vpnIntent = null
}
})
LaunchedEffect(vpnIntent) {
if (vpnIntent != null) {
vpnActivityResultState.launch(vpnIntent)
} else requestNotificationPermission()
}
Scaffold(snackbarHost = { SnackbarHost(snackbarHostState) },
bottomBar = if (vpnIntent == null && notificationPermissionState.status.isGranted) {
{ BottomNavBar(navController, Routes.navItems) }
} else {
{}
},
)
fun showSnackBarMessage(message: StringValue) {
lifecycleScope.launch(Dispatchers.Main) {
val result =
snackbarHostState.showSnackbar(
message = message.asString(this@MainActivity),
duration = SnackbarDuration.Short,
{ padding ->
if (vpnIntent != null) {
PermissionRequestFailedScreen(
padding = padding,
onRequestAgain = { vpnActivityResultState.launch(vpnIntent) },
message = getString(R.string.vpn_permission_required),
getString(R.string.retry)
)
when (result) {
SnackbarResult.ActionPerformed,
SnackbarResult.Dismissed -> {
snackbarHostState.currentSnackbarData?.dismiss()
}
return@Scaffold
}
}
}
LaunchedEffect(appUiState.requestPermissions) {
if (appUiState.requestPermissions) {
appViewModel.permissionsRequested()
if (notificationPermissionState != null && !notificationPermissionState.status.isGranted
) {
showSnackBarMessage(StringValue.StringResource(R.string.notification_permission_required))
return@LaunchedEffect notificationPermissionState.launchPermissionRequest()
}
if (!appUiState.vpnPermissionAccepted) {
return@LaunchedEffect vpnActivityResultState.launch(appViewModel.vpnIntent)
}
}
}
WireguardAutoTunnelTheme {
LaunchedEffect(Unit) {
appViewModel.setNotificationPermissionAccepted(
notificationPermissionState?.status?.isGranted ?: true,
)
if (!WireGuardAutoTunnel.isRunningOnAndroidTv()) appViewModel.readLogCatOutput()
}
LaunchedEffect(appUiState.snackbarMessageConsumed) {
if (!appUiState.snackbarMessageConsumed) {
showSnackBarMessage(StringValue.DynamicString(appUiState.snackbarMessage))
appViewModel.snackbarMessageConsumed()
}
}
val focusRequester = remember { FocusRequester() }
Scaffold(
snackbarHost = {
SnackbarHost(snackbarHostState) { snackbarData: SnackbarData ->
CustomSnackBar(
snackbarData.visuals.message,
isRtl = false,
containerColor =
MaterialTheme.colorScheme.surfaceColorAtElevation(
2.dp,
),
)
}
},
//TODO refactor
modifier = Modifier
.focusable()
.focusProperties {
when (navBackStackEntry?.destination?.route) {
Screen.Lock.route -> Unit
else -> up = focusRequester
}
},
bottomBar = {
BottomNavBar(
navController,
listOf(
Screen.Main.navItem,
Screen.Settings.navItem,
Screen.Support.navItem,
),
if (!notificationPermissionState.status.isGranted) {
PermissionRequestFailedScreen(
padding = padding,
onRequestAgain = {
val intentSettings =
Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
intentSettings.data =
Uri.fromParts("package", this.packageName, null)
startActivity(intentSettings);
},
message = getString(R.string.notification_permission_required),
getString(R.string.open_settings)
)
},
) { padding ->
NavHost(
navController,
startDestination =
(if (PinManager.pinExists()) Screen.Lock.route else Screen.Main.route),
modifier =
Modifier
.padding(padding)
.fillMaxSize(),
) {
composable(
Screen.Main.route,
) {
MainScreen(
focusRequester = focusRequester,
appViewModel = appViewModel,
navController = navController,
)
}
composable(
Screen.Settings.route,
) {
SettingsScreen(
appViewModel = appViewModel,
navController = navController,
focusRequester = focusRequester,
)
}
composable(
Screen.Support.route,
) {
SupportScreen(
focusRequester = focusRequester,
appViewModel = appViewModel,
navController = navController,
)
}
composable(Screen.Support.Logs.route) {
LogsScreen(appViewModel)
}
composable("${Screen.Config.route}/{id}") {
val id = it.arguments?.getString("id")
if (!id.isNullOrBlank()) {
ConfigScreen(
navController = navController,
tunnelId = id,
appViewModel = appViewModel,
focusRequester = focusRequester,
)
return@Scaffold
}
AnimatedNavHost(navController, startDestination = Routes.Main.name) {
composable(Routes.Main.name, enterTransition = {
when (initialState.destination.route) {
Routes.Settings.name, Routes.Support.name ->
slideInHorizontally(
initialOffsetX = { -1000 },
animationSpec = tween(500)
)
else -> {
fadeIn(animationSpec = tween(1000))
}
}
}) {
MainScreen(padding = padding, snackbarHostState = snackbarHostState, navController = navController)
}
composable("${Screen.Option.route}/{id}") {
val id = it.arguments?.getString("id")
if (!id.isNullOrBlank()) {
OptionsScreen(
navController = navController,
tunnelId = id,
appViewModel = appViewModel,
focusRequester = focusRequester,
)
composable(Routes.Settings.name, enterTransition = {
when (initialState.destination.route) {
Routes.Main.name ->
slideInHorizontally(
initialOffsetX = { 1000 },
animationSpec = tween(500)
)
Routes.Support.name -> {
slideInHorizontally(
initialOffsetX = { -1000 },
animationSpec = tween(500)
)
}
else -> {
fadeIn(animationSpec = tween(1000))
}
}
}
composable(Screen.Lock.route) {
PinLockScreen(
navController = navController,
appViewModel = appViewModel,
)
}
}) { SettingsScreen(padding = padding, snackbarHostState = snackbarHostState, navController = navController) }
composable(Routes.Support.name, enterTransition = {
when (initialState.destination.route) {
Routes.Settings.name, Routes.Main.name ->
slideInHorizontally(
initialOffsetX = { 1000 },
animationSpec = tween(500)
)
else -> {
fadeIn(animationSpec = tween(1000))
}
}
}) { SupportScreen(padding = padding) }
composable("${Routes.Config.name}/{id}", enterTransition = {
fadeIn(animationSpec = tween(1000))
}) { ConfigScreen(padding = padding, navController = navController, id = it.arguments?.getString("id"))}
composable("${Routes.Detail.name}/{id}", enterTransition = {
fadeIn(animationSpec = tween(1000))
}) { DetailScreen(padding = padding, id = it.arguments?.getString("id")) }
}
}
}
@@ -0,0 +1,36 @@
package com.zaneschepke.wireguardautotunnel.ui
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.Home
import androidx.compose.material.icons.rounded.QuestionMark
import androidx.compose.material.icons.rounded.Settings
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.BottomNavItem
enum class Routes {
Main,
Settings,
Support,
Config,
Detail;
companion object {
val navItems = listOf(
BottomNavItem(
name = "Tunnels",
route = Main.name,
icon = Icons.Rounded.Home,
),
BottomNavItem(
name = "Settings",
route = Settings.name,
icon = Icons.Rounded.Settings,
),
BottomNavItem(
name = "Support",
route = Support.name,
icon = Icons.Rounded.QuestionMark,
)
)
}
}
@@ -1,47 +0,0 @@
package com.zaneschepke.wireguardautotunnel.ui
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.Home
import androidx.compose.material.icons.rounded.QuestionMark
import androidx.compose.material.icons.rounded.Settings
import androidx.compose.ui.res.stringResource
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.BottomNavItem
import com.zaneschepke.wireguardautotunnel.util.StringValue
sealed class Screen(val route: String) {
data object Main : Screen("main") {
val navItem =
BottomNavItem(
name = WireGuardAutoTunnel.instance.getString(R.string.tunnels),
route = route,
icon = Icons.Rounded.Home,
)
}
data object Settings : Screen("settings") {
val navItem =
BottomNavItem(
name = WireGuardAutoTunnel.instance.getString(R.string.settings),
route = route,
icon = Icons.Rounded.Settings,
)
}
data object Support : Screen("support") {
val navItem =
BottomNavItem(
name = WireGuardAutoTunnel.instance.getString(R.string.support),
route = route,
icon = Icons.Rounded.QuestionMark,
)
data object Logs : Screen("support/logs")
}
data object Config : Screen("config")
data object Lock : Screen("lock")
data object Option : Screen("option")
}
@@ -0,0 +1,9 @@
package com.zaneschepke.wireguardautotunnel.ui
data class ViewState(
val showSnackbarMessage : Boolean = false,
val snackbarMessage : String = "",
val snackbarActionText : String = "",
val onSnackbarActionClick : () -> Unit = {},
val isLoading : Boolean = false
)
@@ -3,40 +3,29 @@ package com.zaneschepke.wireguardautotunnel.ui.common
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.size
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
@Composable
fun ClickableIconButton(
onClick: () -> Unit,
onIconClick: () -> Unit,
text: String,
icon: ImageVector,
enabled: Boolean
) {
TextButton(
onClick = onClick,
enabled = enabled,
fun ClickableIconButton(onIconClick : () -> Unit, text : String, icon : ImageVector, enabled : Boolean) {
Button(onClick = {},
enabled = enabled
) {
Text(text, Modifier.weight(1f, false))
Text(text)
Spacer(modifier = Modifier.size(ButtonDefaults.IconSpacing))
Icon(
imageVector = icon,
contentDescription = icon.name,
modifier =
Modifier
.size(ButtonDefaults.IconSize)
.weight(1f, false)
.clickable {
if (enabled) {
onIconClick()
}
},
contentDescription = "Delete",
modifier = Modifier.size(ButtonDefaults.IconSize).clickable {
if(enabled) {
onIconClick()
}
}
)
}
}
}
@@ -0,0 +1,35 @@
package com.zaneschepke.wireguardautotunnel.ui.common
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Button
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.launch
@Composable
fun PermissionRequestFailedScreen(padding : PaddingValues, onRequestAgain : () -> Unit, message : String, buttonText : String ) {
val scope = rememberCoroutineScope()
Column(horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center,
modifier = Modifier
.fillMaxSize()
.padding(padding)) {
Text(message, textAlign = TextAlign.Center, modifier = Modifier.padding(15.dp))
Button(onClick = {
scope.launch {
onRequestAgain()
}
}) {
Text(buttonText)
}
}
}
@@ -1,92 +1,55 @@
package com.zaneschepke.wireguardautotunnel.ui.common
import androidx.compose.animation.animateContentSize
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.layout.size
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.wireguard.android.backend.Statistics
import com.zaneschepke.wireguardautotunnel.util.NumberUtils
import com.zaneschepke.wireguardautotunnel.util.toThreeDecimalPlaceString
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun RowListItem(
icon: @Composable () -> Unit,
text: String,
onHold: () -> Unit,
onClick: () -> Unit,
rowButton: @Composable () -> Unit,
expanded: Boolean,
statistics: Statistics?
) {
fun RowListItem(leadingIcon : ImageVector? = null, leadingIconColor : Color = Color.Gray, text : String, onHold : () -> Unit, onClick: () -> Unit, rowButton : @Composable() () -> Unit ) {
Box(
modifier =
Modifier
.animateContentSize()
.clip(RoundedCornerShape(30.dp))
modifier = Modifier
.combinedClickable(
onClick = { onClick() },
onLongClick = { onHold() },
),
onClick = {
onClick()
},
onLongClick = {
onHold()
}
)
) {
Column {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 15.dp, vertical = 5.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween,
) {
Row(
verticalAlignment = Alignment.CenterVertically,
) {
icon()
Text(text)
}
rowButton()
}
if (expanded) {
statistics?.peers()?.forEach {
Row(
modifier =
Modifier
.fillMaxWidth()
.padding(end = 10.dp, bottom = 10.dp, start = 10.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceEvenly,
) {
//TODO change these to string resources
val handshakeEpoch = statistics.peer(it)!!.latestHandshakeEpochMillis
val peerTx = statistics.peer(it)!!.txBytes
val peerRx = statistics.peer(it)!!.rxBytes
val peerId = it.toBase64().subSequence(0, 3).toString() + "***"
val handshakeSec =
NumberUtils.getSecondsBetweenTimestampAndNow(handshakeEpoch)
val handshake =
if (handshakeSec == null) "never" else "$handshakeSec secs ago"
val peerTxMB = NumberUtils.bytesToMB(peerTx).toThreeDecimalPlaceString()
val peerRxMB = NumberUtils.bytesToMB(peerRx).toThreeDecimalPlaceString()
val fontSize = 9.sp
Text("peer: $peerId", fontSize = fontSize)
Text("handshake: $handshake", fontSize = fontSize)
Text("tx: $peerTxMB MB", fontSize = fontSize)
Text("rx: $peerRxMB MB", fontSize = fontSize)
}
Row(
modifier = Modifier
.fillMaxWidth()
.padding(14.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween
) {
Row(verticalAlignment = Alignment.CenterVertically,) {
if(leadingIcon != null) {
Icon(
leadingIcon, "status",
tint = leadingIconColor,
modifier = Modifier.padding(end = 10.dp).size(15.dp)
)
}
Text(text)
}
rowButton()
}
}
}
}
@@ -1,80 +0,0 @@
package com.zaneschepke.wireguardautotunnel.ui.common
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.Clear
import androidx.compose.material.icons.rounded.Search
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TextField
import androidx.compose.material3.TextFieldDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.RectangleShape
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.KeyboardType
import com.zaneschepke.wireguardautotunnel.R
@Composable
fun SearchBar(onQuery: (queryString: String) -> Unit) {
// Immediately update and keep track of query from text field changes.
var query: String by rememberSaveable { mutableStateOf("") }
var showClearIcon by rememberSaveable { mutableStateOf(false) }
if (query.isEmpty()) {
showClearIcon = false
} else if (query.isNotEmpty()) {
showClearIcon = true
}
TextField(
value = query,
onValueChange = { onQueryChanged ->
// If user makes changes to text, immediately updated it.
query = onQueryChanged
onQuery(onQueryChanged)
},
leadingIcon = {
Icon(
imageVector = Icons.Rounded.Search,
tint = MaterialTheme.colorScheme.onBackground,
contentDescription = stringResource(id = R.string.search_icon),
)
},
trailingIcon = {
if (showClearIcon) {
IconButton(onClick = { query = "" }) {
Icon(
imageVector = Icons.Rounded.Clear,
tint = MaterialTheme.colorScheme.onBackground,
contentDescription = stringResource(id = R.string.clear_icon),
)
}
}
},
maxLines = 1,
colors =
TextFieldDefaults.colors(
focusedContainerColor = Color.Transparent,
unfocusedContainerColor = Color.Transparent,
disabledContainerColor = Color.Transparent,
),
placeholder = { Text(text = stringResource(R.string.hint_search_packages)) },
textStyle = MaterialTheme.typography.bodySmall,
singleLine = true,
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Text),
modifier =
Modifier
.fillMaxWidth()
.background(color = MaterialTheme.colorScheme.background, shape = RectangleShape),
)
}

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