mirror of
https://github.com/wgtunnel/android.git
synced 2026-07-03 14:07:49 +02:00
Compare commits
42 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ffa7a207fb | |||
| 515e91d191 | |||
| 16e65aec9f | |||
| ff87bee3b4 | |||
| 9739d35eda | |||
| 11ad494fbb | |||
| 90b006acc5 | |||
| eb7b39c379 | |||
| 0a17593310 | |||
| c0e58125dd | |||
| 3791261f91 | |||
| d1e61be3ae | |||
| afd4fb127f | |||
| e0cce8fba4 | |||
| b70ecbdfff | |||
| 513d08998b | |||
| 79583e0e61 | |||
| 75790ec6d5 | |||
| a1941b7229 | |||
| 37bae82700 | |||
| 77cd328a71 | |||
| 5a1430706b | |||
| 321730536d | |||
| 2912238f27 | |||
| bc7daacd13 | |||
| c8f2cfd758 | |||
| ca3f3fd439 | |||
| 235170508b | |||
| 11aea3f1c4 | |||
| 7fbc51af4c | |||
| 1714618f0c | |||
| 7cb798a111 | |||
| f8bc264f30 | |||
| 2174c3f48c | |||
| 413b9a37df | |||
| 6c30c6bae6 | |||
| 3c5aff31aa | |||
| 991d1224ab | |||
| 69b07eec6f | |||
| e81066f508 | |||
| 64bb9f3b82 | |||
| c1b560e822 |
@@ -0,0 +1,31 @@
|
|||||||
|
---
|
||||||
|
name: Bug report
|
||||||
|
about: Create a report to help us improve
|
||||||
|
title: "[BUG] - Problem with app"
|
||||||
|
labels: bug
|
||||||
|
assignees: zaneschepke
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Describe the bug**
|
||||||
|
A clear and concise description of what the bug is.
|
||||||
|
|
||||||
|
**Smartphone (please complete the following information):**
|
||||||
|
- Device: [e.g. iPhone6]
|
||||||
|
- Android Version: [e.g. iOS8.1]
|
||||||
|
- App Version [e.g. 22]
|
||||||
|
|
||||||
|
**To Reproduce**
|
||||||
|
Steps to reproduce the behavior:
|
||||||
|
1. Go to '...'
|
||||||
|
2. Click on '....'
|
||||||
|
3. Scroll down to '....'
|
||||||
|
4. See error
|
||||||
|
|
||||||
|
**Expected behavior**
|
||||||
|
A clear and concise description of what you expected to happen.
|
||||||
|
|
||||||
|
**Screenshots (Only if necessary)**
|
||||||
|
|
||||||
|
**Additional context**
|
||||||
|
Add any other context about the problem here.
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
---
|
||||||
|
name: Feature request
|
||||||
|
about: Suggest an idea for this project
|
||||||
|
title: "[FEATURE] - New feature request"
|
||||||
|
labels: enhancement
|
||||||
|
assignees: zaneschepke
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Is your feature request related to a problem? Please describe.**
|
||||||
|
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||||
|
|
||||||
|
**Describe the solution you'd like**
|
||||||
|
A clear and concise description of what you want to happen.
|
||||||
|
|
||||||
|
**Describe alternatives you've considered**
|
||||||
|
A clear and concise description of any alternative solutions or features you've considered.
|
||||||
|
|
||||||
|
**Additional context**
|
||||||
|
Add any other context or screenshots about the feature request here.
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
# Support
|
||||||
|
|
||||||
|
If you are experiencing issues with the app, the following resources are available to help you.
|
||||||
|
|
||||||
|
<ol>
|
||||||
|
<li>
|
||||||
|
See the app docs site <a href="https://zaneschepke.com/wgtunnel-docs/overview.html">here</a> (work in progress).
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
Chat with me and our community on Discord <a href="https://discord.gg/rbRRNh6H7V">here</a>, or open an issue on GitHub <a href="https://github.com/zaneschepke/wgtunnel/issues/new/choose">here</a>.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
Send me an email <a href="mailto:zanecschepke@gmail.com">here</a>.
|
||||||
|
</li>
|
||||||
|
</ol>
|
||||||
|
|
||||||
|
Thank you for using WG Tunnel.
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
# name of the workflow
|
||||||
|
name: Android CI Tag Deployment
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- '*.*.*'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
name: Build Signed APK
|
||||||
|
# change to macos because of hilt issues on ubuntu in gradle 8.3
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
env:
|
||||||
|
KEY_STORE_PATH: ${{ secrets.KEY_STORE_PATH }}
|
||||||
|
SIGNING_KEY_ALIAS: ${{ secrets.SIGNING_KEY_ALIAS }}
|
||||||
|
SIGNING_KEY_PASSWORD: ${{ secrets.SIGNING_KEY_PASSWORD }}
|
||||||
|
SIGNING_STORE_PASSWORD: ${{ secrets.SIGNING_STORE_PASSWORD }}
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
- name: Set up JDK 17
|
||||||
|
uses: actions/setup-java@v3
|
||||||
|
with:
|
||||||
|
distribution: 'temurin'
|
||||||
|
java-version: '17'
|
||||||
|
cache: gradle
|
||||||
|
- name: Grant execute permission for gradlew
|
||||||
|
run: chmod +x gradlew
|
||||||
|
|
||||||
|
# Here we need to decode keystore.jks from base64 string and place it
|
||||||
|
# in the folder specified in the release signing configuration
|
||||||
|
- name: Decode Keystore
|
||||||
|
id: decode_keystore
|
||||||
|
uses: timheuer/base64-to-file@v1.2
|
||||||
|
with:
|
||||||
|
fileName: 'android_keystore.jks'
|
||||||
|
fileDir: ${{ github.workspace }}/app/keystore/
|
||||||
|
encodedString: ${{ secrets.KEYSTORE }}
|
||||||
|
|
||||||
|
- name: Create service_account.json
|
||||||
|
id: createServiceAccount
|
||||||
|
run: echo '${{ secrets.SERVICE_ACCOUNT_JSON }}' > service_account.json
|
||||||
|
|
||||||
|
# Build and sign APK ("-x test" argument is used to skip tests)
|
||||||
|
# add fdroid flavor for apk upload
|
||||||
|
- name: Build Fdroid Release APK
|
||||||
|
run: ./gradlew :app:assembleFdroidRelease -x test
|
||||||
|
|
||||||
|
# get fdroid flavor release apk path
|
||||||
|
- name: Get apk path
|
||||||
|
id: apk-path
|
||||||
|
run: echo "path=$(find . -regex '^.*/build/outputs/apk/fdroid/release/.*\.apk$' -type f | head -1)" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
# Save the APK after the Build job is complete to publish it as a Github release in the next job
|
||||||
|
- name: Upload APK
|
||||||
|
uses: actions/upload-artifact@v3.1.2
|
||||||
|
with:
|
||||||
|
name: wgtunnel
|
||||||
|
path: ${{ steps.apk-path.outputs.path }}
|
||||||
|
- name: Download APK from build
|
||||||
|
uses: actions/download-artifact@v1
|
||||||
|
with:
|
||||||
|
name: wgtunnel
|
||||||
|
- name: Create Release with Fastlane changelog notes
|
||||||
|
id: create_release
|
||||||
|
uses: softprops/action-gh-release@v1
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
with:
|
||||||
|
# fix hardcode changelog file name
|
||||||
|
body_path: ${{ github.workspace }}/fastlane/metadata/android/en-US/changelogs/32400.txt
|
||||||
|
tag_name: ${{ github.ref_name }}
|
||||||
|
name: Release ${{ github.ref_name }}
|
||||||
|
draft: false
|
||||||
|
prerelease: false
|
||||||
|
files: ${{ github.workspace }}/${{ steps.apk-path.outputs.path }}
|
||||||
|
- name: Deploy with fastlane
|
||||||
|
uses: ruby/setup-ruby@v1
|
||||||
|
with:
|
||||||
|
ruby-version: '3.2' # Not needed with a .ruby-version file
|
||||||
|
bundler-cache: true
|
||||||
|
|
||||||
|
- name: Distribute app to Beta track 🚀
|
||||||
|
run: (cd ${{ github.workspace }} && bundle install && bundle exec fastlane beta)
|
||||||
|
|
||||||
@@ -69,3 +69,5 @@ lint/tmp/
|
|||||||
# App Specific cases
|
# App Specific cases
|
||||||
app/release/output.json
|
app/release/output.json
|
||||||
.idea/codeStyles/
|
.idea/codeStyles/
|
||||||
|
# where we keep our signing secrets locally
|
||||||
|
app/signing.properties
|
||||||
|
|||||||
@@ -2,60 +2,64 @@
|
|||||||
WG Tunnel
|
WG Tunnel
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
<span align="center">
|
<div align="center">
|
||||||
|
|
||||||
[](https://opensource.org/licenses/MIT)
|
[](https://opensource.org/licenses/MIT)
|
||||||
[](https://discord.gg/rbRRNh6H7V)
|
[](https://discord.gg/rbRRNh6H7V)
|
||||||
|
|
||||||
</span>
|
</div>
|
||||||
|
|
||||||
<span align="center">
|
<div align="center">
|
||||||
|
|
||||||
|
|
||||||
[](https://play.google.com/store/apps/details?id=com.zaneschepke.wireguardautotunnel)
|
[](https://play.google.com/store/apps/details?id=com.zaneschepke.wireguardautotunnel)
|
||||||
|
[](https://www.amazon.com/gp/product/B0CFGGL7WK)
|
||||||
|
[](https://f-droid.org/packages/com.zaneschepke.wireguardautotunnel/)
|
||||||
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<span align="center">
|
</div>
|
||||||
|
|
||||||
|
<div align="center">
|
||||||
|
|
||||||
[](https://ko-fi.com/N4N8NMJN2)
|
[](https://ko-fi.com/N4N8NMJN2)
|
||||||
|
|
||||||
</span>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<span align="left">
|
<div 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.
|
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.
|
||||||
|
|
||||||
</span>
|
</div>
|
||||||
|
|
||||||
<span align="center">
|
<div align="center">
|
||||||
|
|
||||||
## Screenshots
|
## Screenshots
|
||||||
|
|
||||||
<p float="center">
|
<p float="center">
|
||||||
<img label="Main" style="padding-right:25px" src="asset/main_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="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="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" />
|
<img label="Support" style="padding-left:25px" src="asset/support_screen.png" width="200" />
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<span align="left">
|
<div align="left">
|
||||||
|
|
||||||
## Inspiration
|
## Inspiration
|
||||||
|
|
||||||
The inspiration for this app came from the inconvenience of constantly having to turn VPN off and on while on different networks. With there being no free solution to this problem, this app was created to meet that need.
|
The original inspiration for this app came from the inconvenience of having to manually turn VPN off and on while on different networks. This app was created to offer a free solution to this problem.
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
* Add tunnels via .conf file
|
* Add tunnels via .conf file, zip, manual entry, or QR code
|
||||||
* Auto connect to VPN based on Wi-Fi SSID
|
* Auto connect to VPN based on Wi-Fi SSID
|
||||||
* Split tunneling by application with search
|
* Split tunneling by application with search
|
||||||
* Always-on VPN for Android support
|
* Always-on VPN for Android support
|
||||||
* Quick tile support for vpn toggling
|
* Export tunnels to zip
|
||||||
* Dynamic shortcuts support for automation integration
|
* Quick tile support for VPN toggling
|
||||||
* Configurable Trusted Network list
|
* Static shortcuts support for primary tunnel for automation integration
|
||||||
* Optional auto connect on mobile data
|
* Intent automation support for all tunnels
|
||||||
|
* Optional auto connect on mobile data, ethernet
|
||||||
* Automatic service restart after reboot
|
* Automatic service restart after reboot
|
||||||
* Service will stay running in background after app has been closed
|
* Service will stay running in background after app has been closed
|
||||||
|
|
||||||
@@ -65,7 +69,7 @@ The inspiration for this app came from the inconvenience of constantly having to
|
|||||||
```
|
```
|
||||||
$ git clone https://github.com/zaneschepke/wgtunnel
|
$ git clone https://github.com/zaneschepke/wgtunnel
|
||||||
$ cd wgtunnel
|
$ cd wgtunnel
|
||||||
$ ./gradlew assembleRelease
|
$ ./gradlew assembleDebug
|
||||||
```
|
```
|
||||||
|
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
+151
-66
@@ -1,31 +1,33 @@
|
|||||||
val rExtra = rootProject.extra
|
import java.util.Properties
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
id("com.android.application")
|
alias(libs.plugins.android.application)
|
||||||
id("org.jetbrains.kotlin.android")
|
alias(libs.plugins.kotlin.android)
|
||||||
kotlin("kapt")
|
alias(libs.plugins.hilt.android)
|
||||||
id("com.google.dagger.hilt.android")
|
|
||||||
id("com.google.gms.google-services")
|
|
||||||
id("com.google.firebase.crashlytics")
|
|
||||||
id("org.jetbrains.kotlin.plugin.serialization")
|
id("org.jetbrains.kotlin.plugin.serialization")
|
||||||
id("io.objectbox")
|
alias(libs.plugins.ksp)
|
||||||
}
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
namespace = "com.zaneschepke.wireguardautotunnel"
|
namespace = Constants.APP_ID
|
||||||
compileSdk = 34
|
compileSdk = Constants.TARGET_SDK
|
||||||
|
|
||||||
val versionMajor = 2
|
|
||||||
val versionMinor = 4
|
|
||||||
val versionPatch = 2
|
|
||||||
val versionBuild = 0
|
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId = "com.zaneschepke.wireguardautotunnel"
|
applicationId = Constants.APP_ID
|
||||||
minSdk = 28
|
minSdk = Constants.MIN_SDK
|
||||||
targetSdk = 34
|
targetSdk = Constants.TARGET_SDK
|
||||||
versionCode = versionMajor * 10000 + versionMinor * 1000 + versionPatch * 100 + versionBuild
|
versionCode = Constants.VERSION_CODE
|
||||||
versionName = "${versionMajor}.${versionMinor}.${versionPatch}"
|
versionName = Constants.VERSION_NAME
|
||||||
|
|
||||||
|
ksp {
|
||||||
|
arg("room.schemaLocation", "$projectDir/schemas")
|
||||||
|
}
|
||||||
|
|
||||||
|
sourceSets {
|
||||||
|
getByName("debug").assets.srcDirs(files("$projectDir/schemas")) // Room
|
||||||
|
}
|
||||||
|
|
||||||
|
resourceConfigurations.addAll(listOf("en"))
|
||||||
|
|
||||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||||
vectorDrawables {
|
vectorDrawables {
|
||||||
@@ -33,28 +35,98 @@ android {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
signingConfigs {
|
||||||
|
create(Constants.RELEASE) {
|
||||||
|
val properties =
|
||||||
|
Properties().apply {
|
||||||
|
// created local file for signing details
|
||||||
|
try {
|
||||||
|
load(file("signing.properties").reader())
|
||||||
|
} catch (_: Exception) {
|
||||||
|
load(file("signing_template.properties").reader())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// try to get secrets from env first for pipeline build, then properties file for local build
|
||||||
|
storeFile = file(
|
||||||
|
System.getenv().getOrDefault(
|
||||||
|
Constants.KEY_STORE_PATH_VAR,
|
||||||
|
properties.getProperty(Constants.KEY_STORE_PATH_VAR)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
storePassword = System.getenv().getOrDefault(
|
||||||
|
Constants.STORE_PASS_VAR,
|
||||||
|
properties.getProperty(Constants.STORE_PASS_VAR)
|
||||||
|
)
|
||||||
|
keyAlias = System.getenv().getOrDefault(
|
||||||
|
Constants.KEY_ALIAS_VAR,
|
||||||
|
properties.getProperty(Constants.KEY_ALIAS_VAR)
|
||||||
|
)
|
||||||
|
keyPassword = System.getenv().getOrDefault(
|
||||||
|
Constants.KEY_PASS_VAR,
|
||||||
|
properties.getProperty(Constants.KEY_PASS_VAR)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
buildTypes {
|
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 {
|
release {
|
||||||
isDebuggable = false
|
isDebuggable = false
|
||||||
isMinifyEnabled = false
|
isMinifyEnabled = true
|
||||||
|
isShrinkResources = true
|
||||||
proguardFiles(
|
proguardFiles(
|
||||||
getDefaultProguardFile("proguard-android-optimize.txt"),
|
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)) {
|
||||||
|
apply(plugin = "com.google.gms.google-services")
|
||||||
|
apply(plugin = "com.google.firebase.crashlytics")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
compileOptions {
|
compileOptions {
|
||||||
sourceCompatibility = JavaVersion.VERSION_17
|
sourceCompatibility = JavaVersion.VERSION_17
|
||||||
targetCompatibility = JavaVersion.VERSION_17
|
targetCompatibility = JavaVersion.VERSION_17
|
||||||
|
isCoreLibraryDesugaringEnabled = true
|
||||||
}
|
}
|
||||||
kotlinOptions {
|
kotlinOptions {
|
||||||
jvmTarget = "17"
|
jvmTarget = Constants.JVM_TARGET
|
||||||
}
|
}
|
||||||
buildFeatures {
|
buildFeatures {
|
||||||
compose = true
|
compose = true
|
||||||
|
buildConfig = true
|
||||||
}
|
}
|
||||||
composeOptions {
|
composeOptions {
|
||||||
kotlinCompilerExtensionVersion = "1.4.8"
|
kotlinCompilerExtensionVersion = libs.versions.composeCompiler.get()
|
||||||
}
|
}
|
||||||
packaging {
|
packaging {
|
||||||
resources {
|
resources {
|
||||||
@@ -63,68 +135,81 @@ android {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val generalImplementation by configurations
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation("androidx.core:core-ktx:1.10.1")
|
implementation(libs.androidx.core.ktx)
|
||||||
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.6.1")
|
implementation(libs.androidx.lifecycle.runtime.ktx)
|
||||||
implementation("androidx.activity:activity-compose:1.7.2")
|
// optional - helpers for implementing LifecycleOwner in a Service
|
||||||
implementation(platform("androidx.compose:compose-bom:2023.03.00"))
|
implementation(libs.androidx.lifecycle.service)
|
||||||
implementation("androidx.compose.ui:ui")
|
implementation(libs.androidx.activity.compose)
|
||||||
implementation("androidx.compose.ui:ui-graphics")
|
implementation(platform(libs.androidx.compose.bom))
|
||||||
implementation("androidx.compose.ui:ui-tooling-preview")
|
implementation(libs.androidx.compose.ui)
|
||||||
implementation("androidx.compose.material3:material3:1.1.1")
|
implementation(libs.androidx.compose.ui.graphics)
|
||||||
implementation("androidx.appcompat:appcompat:1.6.1")
|
implementation(libs.androidx.compose.ui.tooling.preview)
|
||||||
|
implementation(libs.androidx.material3)
|
||||||
|
implementation(libs.androidx.appcompat)
|
||||||
|
|
||||||
testImplementation("junit:junit:4.13.2")
|
// test
|
||||||
androidTestImplementation("androidx.test.ext:junit:1.1.5")
|
testImplementation(libs.junit)
|
||||||
androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")
|
testImplementation(libs.androidx.junit)
|
||||||
androidTestImplementation(platform("androidx.compose:compose-bom:2023.03.00"))
|
androidTestImplementation(libs.androidx.junit)
|
||||||
androidTestImplementation("androidx.compose.ui:ui-test-junit4")
|
androidTestImplementation(libs.androidx.espresso.core)
|
||||||
debugImplementation("androidx.compose.ui:ui-tooling")
|
androidTestImplementation(platform(libs.androidx.compose.bom))
|
||||||
debugImplementation("androidx.compose.ui:ui-test-manifest")
|
androidTestImplementation(libs.androidx.compose.ui.test)
|
||||||
|
androidTestImplementation(libs.androidx.room.testing)
|
||||||
|
debugImplementation(libs.androidx.compose.ui.tooling)
|
||||||
|
debugImplementation(libs.androidx.compose.manifest)
|
||||||
|
|
||||||
//wireguard tunnel
|
// wg
|
||||||
implementation("com.wireguard.android:tunnel:1.0.20230706")
|
implementation(libs.tunnel)
|
||||||
|
coreLibraryDesugaring(libs.desugar.jdk.libs)
|
||||||
|
|
||||||
// logging
|
// logging
|
||||||
implementation("com.jakewharton.timber:timber:5.0.1")
|
implementation(libs.timber)
|
||||||
|
|
||||||
// compose navigation
|
// compose navigation
|
||||||
implementation("androidx.navigation:navigation-compose:2.7.1")
|
implementation(libs.androidx.navigation.compose)
|
||||||
implementation("androidx.hilt:hilt-navigation-compose:1.0.0")
|
implementation(libs.androidx.hilt.navigation.compose)
|
||||||
|
|
||||||
// hilt
|
// hilt
|
||||||
implementation("com.google.dagger:hilt-android:${rExtra.get("hiltVersion")}")
|
implementation(libs.hilt.android)
|
||||||
kapt("com.google.dagger:hilt-android-compiler:${rExtra.get("hiltVersion")}")
|
ksp(libs.hilt.android.compiler)
|
||||||
|
|
||||||
// accompanist
|
// accompanist
|
||||||
implementation("com.google.accompanist:accompanist-systemuicontroller:${rExtra.get("accompanistVersion")}")
|
implementation(libs.accompanist.systemuicontroller)
|
||||||
implementation("com.google.accompanist:accompanist-permissions:${rExtra.get("accompanistVersion")}")
|
implementation(libs.accompanist.permissions)
|
||||||
implementation("com.google.accompanist:accompanist-flowlayout:${rExtra.get("accompanistVersion")}")
|
implementation(libs.accompanist.flowlayout)
|
||||||
implementation("com.google.accompanist:accompanist-navigation-animation:${rExtra.get("accompanistVersion")}")
|
implementation(libs.accompanist.drawablepainter)
|
||||||
implementation("com.google.accompanist:accompanist-drawablepainter:${rExtra.get("accompanistVersion")}")
|
|
||||||
|
|
||||||
//db
|
// storage
|
||||||
implementation("io.objectbox:objectbox-kotlin:${rExtra.get("objectBoxVersion")}")
|
implementation(libs.androidx.room.runtime)
|
||||||
|
ksp(libs.androidx.room.compiler)
|
||||||
|
implementation(libs.androidx.room.ktx)
|
||||||
|
implementation(libs.androidx.datastore.preferences)
|
||||||
|
|
||||||
// lifecycle
|
// lifecycle
|
||||||
implementation("androidx.lifecycle:lifecycle-runtime-compose:2.6.1")
|
implementation(libs.lifecycle.runtime.compose)
|
||||||
|
implementation(libs.androidx.lifecycle.runtime.ktx)
|
||||||
|
implementation(libs.androidx.lifecycle.process)
|
||||||
|
|
||||||
// icons
|
// icons
|
||||||
implementation("androidx.compose.material:material-icons-extended:1.5.0")
|
implementation(libs.material.icons.extended)
|
||||||
|
// serialization
|
||||||
|
implementation(libs.kotlinx.serialization.json)
|
||||||
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.1")
|
|
||||||
|
|
||||||
|
|
||||||
// firebase crashlytics
|
// firebase crashlytics
|
||||||
implementation(platform("com.google.firebase:firebase-bom:32.0.0"))
|
generalImplementation(platform(libs.firebase.bom))
|
||||||
implementation("com.google.firebase:firebase-crashlytics-ktx")
|
generalImplementation(libs.google.firebase.crashlytics.ktx)
|
||||||
implementation("com.google.firebase:firebase-analytics-ktx")
|
generalImplementation(libs.google.firebase.analytics.ktx)
|
||||||
|
|
||||||
// barcode scanning
|
// barcode scanning
|
||||||
implementation("com.google.android.gms:play-services-code-scanner:16.1.0")
|
implementation(libs.zxing.android.embedded)
|
||||||
}
|
implementation(libs.zxing.core)
|
||||||
|
|
||||||
kapt {
|
// bio
|
||||||
correctErrorTypes = true
|
implementation(libs.androidx.biometric.ktx)
|
||||||
|
|
||||||
|
// shortcuts
|
||||||
|
implementation(libs.androidx.core)
|
||||||
|
implementation(libs.androidx.core.google.shortcuts)
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
-dontwarn com.google.errorprone.annotations.**
|
||||||
|
|
||||||
|
-keepclassmembers class * extends androidx.datastore.preferences.protobuf.GeneratedMessageLite {
|
||||||
|
<fields>;
|
||||||
|
}
|
||||||
@@ -1,99 +0,0 @@
|
|||||||
{
|
|
||||||
"_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
|
|
||||||
}
|
|
||||||
@@ -1,94 +0,0 @@
|
|||||||
{
|
|
||||||
"_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
|
|
||||||
}
|
|
||||||
Vendored
+3
@@ -19,3 +19,6 @@
|
|||||||
# If you keep the line number information, uncomment this to
|
# If you keep the line number information, uncomment this to
|
||||||
# hide the original source file name.
|
# hide the original source file name.
|
||||||
#-renamesourcefileattribute SourceFile
|
#-renamesourcefileattribute SourceFile
|
||||||
|
-keepclassmembers class * extends androidx.datastore.preferences.protobuf.GeneratedMessageLite {
|
||||||
|
<fields>;
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,112 @@
|
|||||||
|
{
|
||||||
|
"formatVersion": 1,
|
||||||
|
"database": {
|
||||||
|
"version": 1,
|
||||||
|
"identityHash": "ba86153e6fb0b823197b987239b03e64",
|
||||||
|
"entities": [
|
||||||
|
{
|
||||||
|
"tableName": "Settings",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_tunnel_enabled` INTEGER NOT NULL, `is_tunnel_on_mobile_data_enabled` INTEGER NOT NULL, `trusted_network_ssids` TEXT NOT NULL, `default_tunnel` TEXT, `is_always_on_vpn_enabled` INTEGER NOT NULL, `is_tunnel_on_ethernet_enabled` INTEGER NOT NULL)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "id",
|
||||||
|
"columnName": "id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "isAutoTunnelEnabled",
|
||||||
|
"columnName": "is_tunnel_enabled",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "isTunnelOnMobileDataEnabled",
|
||||||
|
"columnName": "is_tunnel_on_mobile_data_enabled",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "trustedNetworkSSIDs",
|
||||||
|
"columnName": "trusted_network_ssids",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "defaultTunnel",
|
||||||
|
"columnName": "default_tunnel",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "isAlwaysOnVpnEnabled",
|
||||||
|
"columnName": "is_always_on_vpn_enabled",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "isTunnelOnEthernetEnabled",
|
||||||
|
"columnName": "is_tunnel_on_ethernet_enabled",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": true,
|
||||||
|
"columnNames": [
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "TunnelConfig",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `wg_quick` TEXT NOT NULL)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "id",
|
||||||
|
"columnName": "id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "name",
|
||||||
|
"columnName": "name",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "wgQuick",
|
||||||
|
"columnName": "wg_quick",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": true,
|
||||||
|
"columnNames": [
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_TunnelConfig_name",
|
||||||
|
"unique": true,
|
||||||
|
"columnNames": [
|
||||||
|
"name"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_TunnelConfig_name` ON `${TABLE_NAME}` (`name`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"views": [],
|
||||||
|
"setupQueries": [
|
||||||
|
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
||||||
|
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'ba86153e6fb0b823197b987239b03e64')"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,126 @@
|
|||||||
|
{
|
||||||
|
"formatVersion": 1,
|
||||||
|
"database": {
|
||||||
|
"version": 2,
|
||||||
|
"identityHash": "65b1c9efff61712231fa64d1f19f3915",
|
||||||
|
"entities": [
|
||||||
|
{
|
||||||
|
"tableName": "Settings",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_tunnel_enabled` INTEGER NOT NULL, `is_tunnel_on_mobile_data_enabled` INTEGER NOT NULL, `trusted_network_ssids` TEXT NOT NULL, `default_tunnel` TEXT, `is_always_on_vpn_enabled` INTEGER NOT NULL, `is_tunnel_on_ethernet_enabled` INTEGER NOT NULL, `is_shortcuts_enabled` INTEGER NOT NULL DEFAULT false, `is_battery_saver_enabled` INTEGER NOT NULL DEFAULT false)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "id",
|
||||||
|
"columnName": "id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "isAutoTunnelEnabled",
|
||||||
|
"columnName": "is_tunnel_enabled",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "isTunnelOnMobileDataEnabled",
|
||||||
|
"columnName": "is_tunnel_on_mobile_data_enabled",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "trustedNetworkSSIDs",
|
||||||
|
"columnName": "trusted_network_ssids",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "defaultTunnel",
|
||||||
|
"columnName": "default_tunnel",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "isAlwaysOnVpnEnabled",
|
||||||
|
"columnName": "is_always_on_vpn_enabled",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "isTunnelOnEthernetEnabled",
|
||||||
|
"columnName": "is_tunnel_on_ethernet_enabled",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "isShortcutsEnabled",
|
||||||
|
"columnName": "is_shortcuts_enabled",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true,
|
||||||
|
"defaultValue": "false"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "isBatterySaverEnabled",
|
||||||
|
"columnName": "is_battery_saver_enabled",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true,
|
||||||
|
"defaultValue": "false"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": true,
|
||||||
|
"columnNames": [
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "TunnelConfig",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `wg_quick` TEXT NOT NULL)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "id",
|
||||||
|
"columnName": "id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "name",
|
||||||
|
"columnName": "name",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "wgQuick",
|
||||||
|
"columnName": "wg_quick",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": true,
|
||||||
|
"columnNames": [
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_TunnelConfig_name",
|
||||||
|
"unique": true,
|
||||||
|
"columnNames": [
|
||||||
|
"name"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_TunnelConfig_name` ON `${TABLE_NAME}` (`name`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"views": [],
|
||||||
|
"setupQueries": [
|
||||||
|
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
||||||
|
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '65b1c9efff61712231fa64d1f19f3915')"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,133 @@
|
|||||||
|
{
|
||||||
|
"formatVersion": 1,
|
||||||
|
"database": {
|
||||||
|
"version": 3,
|
||||||
|
"identityHash": "6b30daba29bb95f8ddc4d26206329d4f",
|
||||||
|
"entities": [
|
||||||
|
{
|
||||||
|
"tableName": "Settings",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_tunnel_enabled` INTEGER NOT NULL, `is_tunnel_on_mobile_data_enabled` INTEGER NOT NULL, `trusted_network_ssids` TEXT NOT NULL, `default_tunnel` TEXT, `is_always_on_vpn_enabled` INTEGER NOT NULL, `is_tunnel_on_ethernet_enabled` INTEGER NOT NULL, `is_shortcuts_enabled` INTEGER NOT NULL DEFAULT false, `is_battery_saver_enabled` INTEGER NOT NULL DEFAULT false, `is_tunnel_on_wifi_enabled` INTEGER NOT NULL DEFAULT false)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "id",
|
||||||
|
"columnName": "id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "isAutoTunnelEnabled",
|
||||||
|
"columnName": "is_tunnel_enabled",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "isTunnelOnMobileDataEnabled",
|
||||||
|
"columnName": "is_tunnel_on_mobile_data_enabled",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "trustedNetworkSSIDs",
|
||||||
|
"columnName": "trusted_network_ssids",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "defaultTunnel",
|
||||||
|
"columnName": "default_tunnel",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "isAlwaysOnVpnEnabled",
|
||||||
|
"columnName": "is_always_on_vpn_enabled",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "isTunnelOnEthernetEnabled",
|
||||||
|
"columnName": "is_tunnel_on_ethernet_enabled",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "isShortcutsEnabled",
|
||||||
|
"columnName": "is_shortcuts_enabled",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true,
|
||||||
|
"defaultValue": "false"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "isBatterySaverEnabled",
|
||||||
|
"columnName": "is_battery_saver_enabled",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true,
|
||||||
|
"defaultValue": "false"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "isTunnelOnWifiEnabled",
|
||||||
|
"columnName": "is_tunnel_on_wifi_enabled",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true,
|
||||||
|
"defaultValue": "false"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": true,
|
||||||
|
"columnNames": [
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "TunnelConfig",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `wg_quick` TEXT NOT NULL)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "id",
|
||||||
|
"columnName": "id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "name",
|
||||||
|
"columnName": "name",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "wgQuick",
|
||||||
|
"columnName": "wg_quick",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": true,
|
||||||
|
"columnNames": [
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_TunnelConfig_name",
|
||||||
|
"unique": true,
|
||||||
|
"columnNames": [
|
||||||
|
"name"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_TunnelConfig_name` ON `${TABLE_NAME}` (`name`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"views": [],
|
||||||
|
"setupQueries": [
|
||||||
|
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
||||||
|
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '6b30daba29bb95f8ddc4d26206329d4f')"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,154 @@
|
|||||||
|
{
|
||||||
|
"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')"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
SIGNING_STORE_PASSWORD=
|
||||||
|
SIGNING_KEY_ALIAS=
|
||||||
|
SIGNING_KEY_PASSWORD=
|
||||||
|
KEY_STORE_PATH=/
|
||||||
+2
-4
@@ -1,13 +1,11 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel
|
package com.zaneschepke.wireguardautotunnel
|
||||||
|
|
||||||
import androidx.test.platform.app.InstrumentationRegistry
|
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
|
import androidx.test.platform.app.InstrumentationRegistry
|
||||||
|
import org.junit.Assert.assertEquals
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import org.junit.runner.RunWith
|
import org.junit.runner.RunWith
|
||||||
|
|
||||||
import org.junit.Assert.*
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Instrumented test, which will execute on an Android device.
|
* Instrumented test, which will execute on an Android device.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -0,0 +1,63 @@
|
|||||||
|
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.repository.AppDatabase
|
||||||
|
import java.io.IOException
|
||||||
|
import org.junit.Rule
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.runner.RunWith
|
||||||
|
|
||||||
|
@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 migrate2To3() {
|
||||||
|
helper.createDatabase(dbName, 3).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(
|
||||||
|
"INSERT INTO Settings (is_tunnel_enabled, " +
|
||||||
|
"is_tunnel_on_mobile_data_enabled," +
|
||||||
|
"trusted_network_ssids," +
|
||||||
|
"default_tunnel, " +
|
||||||
|
"is_always_on_vpn_enabled," +
|
||||||
|
"is_tunnel_on_ethernet_enabled," +
|
||||||
|
"is_shortcuts_enabled," +
|
||||||
|
"is_battery_saver_enabled," +
|
||||||
|
"is_tunnel_on_wifi_enabled)" +
|
||||||
|
" VALUES (" +
|
||||||
|
"false," +
|
||||||
|
"false," +
|
||||||
|
"'[trustedSSID1,trustedSSID2]'," +
|
||||||
|
"'defaultTunnel'," +
|
||||||
|
"false," +
|
||||||
|
"false," +
|
||||||
|
"false," +
|
||||||
|
"false," +
|
||||||
|
"false)"
|
||||||
|
)
|
||||||
|
execSQL(
|
||||||
|
"INSERT INTO TunnelConfig (name, wg_quick)" +
|
||||||
|
" VALUES ('hello', 'hello')"
|
||||||
|
)
|
||||||
|
// 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, 4, true)
|
||||||
|
// MigrationTestHelper automatically verifies the schema changes,
|
||||||
|
// but you need to validate that the data was migrated properly.
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,6 +4,10 @@
|
|||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<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" />
|
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"
|
android:maxSdkVersion="30"
|
||||||
tools:ignore="LeanbackUsesWifi" />
|
tools:ignore="LeanbackUsesWifi" />
|
||||||
@@ -13,6 +17,10 @@
|
|||||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
|
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
|
||||||
<uses-permission android:name="android.permission.CHANGE_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.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.FOREGROUND_SERVICE_LOCATION"/>
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_LOCATION"/>
|
||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_REMOTE_MESSAGING"/>
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_REMOTE_MESSAGING"/>
|
||||||
<!--foreground service permissions-->
|
<!--foreground service permissions-->
|
||||||
@@ -56,10 +64,21 @@
|
|||||||
<action android:name="android.intent.action.MAIN" />
|
<action android:name="android.intent.action.MAIN" />
|
||||||
<category android:name="android.intent.category.LAUNCHER" />
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
|
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
|
||||||
|
<action android:name="android.service.quicksettings.action.QS_TILE_PREFERENCES"/>
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
|
<meta-data android:name="android.app.shortcuts"
|
||||||
|
android:resource="@xml/shortcuts" />
|
||||||
</activity>
|
</activity>
|
||||||
|
<activity
|
||||||
|
android:name=".ui.CaptureActivityPortrait"
|
||||||
|
android:screenOrientation="fullSensor"
|
||||||
|
android:stateNotNeeded="true"
|
||||||
|
android:theme="@style/zxing_CaptureTheme"
|
||||||
|
android:windowSoftInputMode="stateAlwaysHidden" />
|
||||||
<activity
|
<activity
|
||||||
android:finishOnTaskLaunch="true"
|
android:finishOnTaskLaunch="true"
|
||||||
|
android:enabled="true"
|
||||||
|
android:exported="true"
|
||||||
android:theme="@android:style/Theme.NoDisplay"
|
android:theme="@android:style/Theme.NoDisplay"
|
||||||
android:name=".service.shortcut.ShortcutsActivity"/>
|
android:name=".service.shortcut.ShortcutsActivity"/>
|
||||||
<service
|
<service
|
||||||
@@ -87,7 +106,7 @@
|
|||||||
android:permission="android.permission.BIND_VPN_SERVICE"
|
android:permission="android.permission.BIND_VPN_SERVICE"
|
||||||
android:enabled="true"
|
android:enabled="true"
|
||||||
android:persistent="true"
|
android:persistent="true"
|
||||||
android:foregroundServiceType="remoteMessaging"
|
android:foregroundServiceType="systemExempted"
|
||||||
android:exported="false">
|
android:exported="false">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.net.VpnService"/>
|
<action android:name="android.net.VpnService"/>
|
||||||
@@ -100,8 +119,7 @@
|
|||||||
android:enabled="true"
|
android:enabled="true"
|
||||||
android:stopWithTask="false"
|
android:stopWithTask="false"
|
||||||
android:persistent="true"
|
android:persistent="true"
|
||||||
android:foregroundServiceType="location"
|
android:foregroundServiceType="systemExempted"
|
||||||
android:permission=""
|
|
||||||
android:exported="false">
|
android:exported="false">
|
||||||
</service>
|
</service>
|
||||||
<receiver android:enabled="true" android:name=".receiver.BootReceiver"
|
<receiver android:enabled="true" android:name=".receiver.BootReceiver"
|
||||||
@@ -111,11 +129,5 @@
|
|||||||
</intent-filter>
|
</intent-filter>
|
||||||
</receiver>
|
</receiver>
|
||||||
<receiver android:exported="false" android:name=".receiver.NotificationActionReceiver"/>
|
<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>
|
</application>
|
||||||
</manifest>
|
</manifest>
|
||||||
@@ -1,6 +1,21 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel
|
package com.zaneschepke.wireguardautotunnel
|
||||||
|
|
||||||
object Constants {
|
object Constants {
|
||||||
const val VPN_CONNECTIVITY_CHECK_INTERVAL = 3000L;
|
const val MANUAL_TUNNEL_CONFIG_ID = "0"
|
||||||
const val VPN_STATISTIC_CHECK_INTERVAL = 10000L;
|
const val WATCHER_SERVICE_WAKE_LOCK_TIMEOUT = 10 * 60 * 1000L // 10 minutes
|
||||||
|
const val VPN_CONNECTIVITY_CHECK_INTERVAL = 3000L
|
||||||
|
const val VPN_STATISTIC_CHECK_INTERVAL = 1000L
|
||||||
|
const val TOGGLE_TUNNEL_DELAY = 500L
|
||||||
|
const val FADE_IN_ANIMATION_DURATION = 1000
|
||||||
|
const val SLIDE_IN_ANIMATION_DURATION = 500
|
||||||
|
const val SLIDE_IN_TRANSITION_OFFSET = 1000
|
||||||
|
const val CONF_FILE_EXTENSION = ".conf"
|
||||||
|
const val ZIP_FILE_EXTENSION = ".zip"
|
||||||
|
const val URI_CONTENT_SCHEME = "content"
|
||||||
|
const val URI_PACKAGE_SCHEME = "package"
|
||||||
|
const val ALLOWED_FILE_TYPES = "*/*"
|
||||||
|
const val GOOGLE_TV_EXPLORER_STUB = "com.google.android.tv.frameworkpackagestubs"
|
||||||
|
const val ANDROID_TV_EXPLORER_STUB = "com.android.tv.frameworkpackagestubs"
|
||||||
|
const val EMAIL_MIME_TYPE = "message/rfc822"
|
||||||
|
const val SYSTEM_EXEMPT_SERVICE_TYPE_ID = 1024
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
package com.zaneschepke.wireguardautotunnel
|
||||||
|
|
||||||
|
import android.content.BroadcastReceiver
|
||||||
|
import java.math.BigDecimal
|
||||||
|
import java.text.DecimalFormat
|
||||||
|
import kotlin.coroutines.CoroutineContext
|
||||||
|
import kotlin.coroutines.EmptyCoroutineContext
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||||
|
import kotlinx.coroutines.GlobalScope
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
fun BroadcastReceiver.goAsync(
|
||||||
|
context: CoroutineContext = EmptyCoroutineContext,
|
||||||
|
block: suspend CoroutineScope.() -> Unit
|
||||||
|
) {
|
||||||
|
val pendingResult = goAsync()
|
||||||
|
@OptIn(DelicateCoroutinesApi::class) // Must run globally; there's no teardown callback.
|
||||||
|
GlobalScope.launch(context) {
|
||||||
|
try {
|
||||||
|
block()
|
||||||
|
} finally {
|
||||||
|
pendingResult.finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun BigDecimal.toThreeDecimalPlaceString(): String {
|
||||||
|
val df = DecimalFormat("#.###")
|
||||||
|
return df.format(this)
|
||||||
|
}
|
||||||
@@ -3,26 +3,49 @@ package com.zaneschepke.wireguardautotunnel
|
|||||||
import android.app.Application
|
import android.app.Application
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
import com.google.firebase.crashlytics.FirebaseCrashlytics
|
import androidx.lifecycle.ProcessLifecycleOwner
|
||||||
import com.zaneschepke.wireguardautotunnel.repository.Repository
|
import androidx.lifecycle.lifecycleScope
|
||||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.model.Settings
|
import com.zaneschepke.wireguardautotunnel.repository.SettingsDoa
|
||||||
|
import com.zaneschepke.wireguardautotunnel.repository.datastore.DataStoreManager
|
||||||
|
import com.zaneschepke.wireguardautotunnel.repository.model.Settings
|
||||||
import dagger.hilt.android.HiltAndroidApp
|
import dagger.hilt.android.HiltAndroidApp
|
||||||
import timber.log.Timber
|
import java.io.IOException
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import timber.log.Timber
|
||||||
|
|
||||||
@HiltAndroidApp
|
@HiltAndroidApp
|
||||||
class WireGuardAutoTunnel : Application() {
|
class WireGuardAutoTunnel : Application() {
|
||||||
|
@Inject
|
||||||
|
lateinit var settingsRepo: SettingsDoa
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var settingsRepo : Repository<Settings>
|
lateinit var dataStoreManager: DataStoreManager
|
||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
if(BuildConfig.DEBUG) {
|
if (BuildConfig.DEBUG) Timber.plant(Timber.DebugTree())
|
||||||
FirebaseCrashlytics.getInstance().setCrashlyticsCollectionEnabled(false);
|
initSettings()
|
||||||
Timber.plant(Timber.DebugTree())
|
with(ProcessLifecycleOwner.get()) {
|
||||||
|
lifecycleScope.launch {
|
||||||
|
try {
|
||||||
|
// load preferences into memory
|
||||||
|
dataStoreManager.init()
|
||||||
|
} catch (e: IOException) {
|
||||||
|
Timber.e("Failed to load preferences")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initSettings() {
|
||||||
|
with(ProcessLifecycleOwner.get()) {
|
||||||
|
lifecycleScope.launch {
|
||||||
|
if (settingsRepo.getAll().isEmpty()) {
|
||||||
|
settingsRepo.save(Settings())
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
settingsRepo.init()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|||||||
@@ -1,40 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
package com.zaneschepke.wireguardautotunnel.module
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.room.Room
|
||||||
|
import com.zaneschepke.wireguardautotunnel.R
|
||||||
|
import com.zaneschepke.wireguardautotunnel.repository.AppDatabase
|
||||||
|
import dagger.Module
|
||||||
|
import dagger.Provides
|
||||||
|
import dagger.hilt.InstallIn
|
||||||
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
|
import dagger.hilt.components.SingletonComponent
|
||||||
|
import javax.inject.Singleton
|
||||||
|
|
||||||
|
@Module
|
||||||
|
@InstallIn(SingletonComponent::class)
|
||||||
|
class DatabaseModule {
|
||||||
|
@Provides
|
||||||
|
@Singleton
|
||||||
|
fun provideDatabase(
|
||||||
|
@ApplicationContext context: Context
|
||||||
|
): AppDatabase {
|
||||||
|
return Room.databaseBuilder(
|
||||||
|
context,
|
||||||
|
AppDatabase::class.java,
|
||||||
|
context.getString(R.string.db_name)
|
||||||
|
)
|
||||||
|
.fallbackToDestructiveMigration()
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package com.zaneschepke.wireguardautotunnel.module
|
||||||
|
|
||||||
|
import javax.inject.Qualifier
|
||||||
|
|
||||||
|
@Qualifier
|
||||||
|
@Retention(AnnotationRetention.BINARY)
|
||||||
|
annotation class Kernel
|
||||||
@@ -1,25 +1,35 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.module
|
package com.zaneschepke.wireguardautotunnel.module
|
||||||
|
|
||||||
import com.zaneschepke.wireguardautotunnel.repository.Repository
|
import android.content.Context
|
||||||
import com.zaneschepke.wireguardautotunnel.repository.SettingsBox
|
import com.zaneschepke.wireguardautotunnel.repository.AppDatabase
|
||||||
import com.zaneschepke.wireguardautotunnel.repository.TunnelBox
|
import com.zaneschepke.wireguardautotunnel.repository.SettingsDoa
|
||||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.model.Settings
|
import com.zaneschepke.wireguardautotunnel.repository.TunnelConfigDao
|
||||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.model.TunnelConfig
|
import com.zaneschepke.wireguardautotunnel.repository.datastore.DataStoreManager
|
||||||
import dagger.Binds
|
|
||||||
import dagger.Module
|
import dagger.Module
|
||||||
|
import dagger.Provides
|
||||||
import dagger.hilt.InstallIn
|
import dagger.hilt.InstallIn
|
||||||
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
import dagger.hilt.components.SingletonComponent
|
import dagger.hilt.components.SingletonComponent
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
|
|
||||||
@Module
|
@Module
|
||||||
@InstallIn(SingletonComponent::class)
|
@InstallIn(SingletonComponent::class)
|
||||||
abstract class RepositoryModule {
|
class RepositoryModule {
|
||||||
|
|
||||||
@Binds
|
|
||||||
@Singleton
|
@Singleton
|
||||||
abstract fun provideSettingsRepository(settingsBox: SettingsBox) : Repository<Settings>
|
@Provides
|
||||||
|
fun provideSettingsRepository(appDatabase: AppDatabase): SettingsDoa {
|
||||||
@Binds
|
return appDatabase.settingDao()
|
||||||
@Singleton
|
}
|
||||||
abstract fun provideTunnelRepository(tunnelBox: TunnelBox) : Repository<TunnelConfig>
|
|
||||||
|
@Singleton
|
||||||
|
@Provides
|
||||||
|
fun provideTunnelConfigRepository(appDatabase: AppDatabase): TunnelConfigDao {
|
||||||
|
return appDatabase.tunnelConfigDoa()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Singleton
|
||||||
|
@Provides
|
||||||
|
fun providePreferencesDataStore(@ApplicationContext context: Context): DataStoreManager {
|
||||||
|
return DataStoreManager(context)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
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,5 +1,6 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.module
|
package com.zaneschepke.wireguardautotunnel.module
|
||||||
|
|
||||||
|
import com.zaneschepke.wireguardautotunnel.service.network.EthernetService
|
||||||
import com.zaneschepke.wireguardautotunnel.service.network.MobileDataService
|
import com.zaneschepke.wireguardautotunnel.service.network.MobileDataService
|
||||||
import com.zaneschepke.wireguardautotunnel.service.network.NetworkService
|
import com.zaneschepke.wireguardautotunnel.service.network.NetworkService
|
||||||
import com.zaneschepke.wireguardautotunnel.service.network.WifiService
|
import com.zaneschepke.wireguardautotunnel.service.network.WifiService
|
||||||
@@ -14,7 +15,6 @@ import dagger.hilt.android.scopes.ServiceScoped
|
|||||||
@Module
|
@Module
|
||||||
@InstallIn(ServiceComponent::class)
|
@InstallIn(ServiceComponent::class)
|
||||||
abstract class ServiceModule {
|
abstract class ServiceModule {
|
||||||
|
|
||||||
@Binds
|
@Binds
|
||||||
@ServiceScoped
|
@ServiceScoped
|
||||||
abstract fun provideNotificationService(wireGuardNotification: WireGuardNotification): NotificationService
|
abstract fun provideNotificationService(wireGuardNotification: WireGuardNotification): NotificationService
|
||||||
@@ -26,4 +26,8 @@ abstract class ServiceModule {
|
|||||||
@Binds
|
@Binds
|
||||||
@ServiceScoped
|
@ServiceScoped
|
||||||
abstract fun provideMobileDataService(mobileDataService: MobileDataService): NetworkService<MobileDataService>
|
abstract fun provideMobileDataService(mobileDataService: MobileDataService): NetworkService<MobileDataService>
|
||||||
|
|
||||||
|
@Binds
|
||||||
|
@ServiceScoped
|
||||||
|
abstract fun provideEthernetService(ethernetService: EthernetService): NetworkService<EthernetService>
|
||||||
}
|
}
|
||||||
@@ -3,6 +3,10 @@ package com.zaneschepke.wireguardautotunnel.module
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import com.wireguard.android.backend.Backend
|
import com.wireguard.android.backend.Backend
|
||||||
import com.wireguard.android.backend.GoBackend
|
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.repository.SettingsDoa
|
||||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnService
|
import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnService
|
||||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.WireGuardTunnel
|
import com.zaneschepke.wireguardautotunnel.service.tunnel.WireGuardTunnel
|
||||||
import dagger.Module
|
import dagger.Module
|
||||||
@@ -15,17 +19,40 @@ import javax.inject.Singleton
|
|||||||
@Module
|
@Module
|
||||||
@InstallIn(SingletonComponent::class)
|
@InstallIn(SingletonComponent::class)
|
||||||
class TunnelModule {
|
class TunnelModule {
|
||||||
|
@Provides
|
||||||
|
@Singleton
|
||||||
|
fun provideRootShell(
|
||||||
|
@ApplicationContext context: Context
|
||||||
|
): RootShell {
|
||||||
|
return RootShell(context)
|
||||||
|
}
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
@Singleton
|
||||||
fun provideBackend(@ApplicationContext context : Context) : Backend {
|
@Userspace
|
||||||
|
fun provideUserspaceBackend(
|
||||||
|
@ApplicationContext context: Context
|
||||||
|
): Backend {
|
||||||
return GoBackend(context)
|
return GoBackend(context)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
@Singleton
|
||||||
fun provideVpnService(backend: Backend) : VpnService {
|
@Kernel
|
||||||
return WireGuardTunnel(backend)
|
fun provideKernelBackend(
|
||||||
|
@ApplicationContext context: Context,
|
||||||
|
rootShell: RootShell
|
||||||
|
): Backend {
|
||||||
|
return WgQuickBackend(context, rootShell, ToolsInstaller(context, rootShell))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@Singleton
|
||||||
|
fun provideVpnService(
|
||||||
|
@Userspace userspaceBackend: Backend,
|
||||||
|
@Kernel kernelBackend: Backend,
|
||||||
|
settingsDoa: SettingsDoa
|
||||||
|
): VpnService {
|
||||||
|
return WireGuardTunnel(userspaceBackend, kernelBackend, settingsDoa)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package com.zaneschepke.wireguardautotunnel.module
|
||||||
|
|
||||||
|
import javax.inject.Qualifier
|
||||||
|
|
||||||
|
@Qualifier
|
||||||
|
@Retention(AnnotationRetention.BINARY)
|
||||||
|
annotation class Userspace
|
||||||
@@ -3,28 +3,26 @@ package com.zaneschepke.wireguardautotunnel.receiver
|
|||||||
import android.content.BroadcastReceiver
|
import android.content.BroadcastReceiver
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import com.zaneschepke.wireguardautotunnel.repository.Repository
|
import com.zaneschepke.wireguardautotunnel.goAsync
|
||||||
|
import com.zaneschepke.wireguardautotunnel.repository.SettingsDoa
|
||||||
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
|
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
|
||||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.model.Settings
|
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.cancel
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
import kotlinx.coroutines.cancel
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class BootReceiver : BroadcastReceiver() {
|
class BootReceiver : BroadcastReceiver() {
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var settingsRepo : Repository<Settings>
|
lateinit var settingsRepo: SettingsDoa
|
||||||
|
|
||||||
override fun onReceive(context: Context, intent: Intent) {
|
override fun onReceive(
|
||||||
|
context: Context,
|
||||||
|
intent: Intent
|
||||||
|
) = goAsync {
|
||||||
if (intent.action == Intent.ACTION_BOOT_COMPLETED) {
|
if (intent.action == Intent.ACTION_BOOT_COMPLETED) {
|
||||||
CoroutineScope(Dispatchers.IO).launch {
|
|
||||||
try {
|
try {
|
||||||
val settings = settingsRepo.getAll()
|
val settings = settingsRepo.getAll()
|
||||||
if (!settings.isNullOrEmpty()) {
|
if (settings.isNotEmpty()) {
|
||||||
val setting = settings.first()
|
val setting = settings.first()
|
||||||
if (setting.isAutoTunnelEnabled && setting.defaultTunnel != null) {
|
if (setting.isAutoTunnelEnabled && setting.defaultTunnel != null) {
|
||||||
ServiceManager.startWatcherService(context, setting.defaultTunnel!!)
|
ServiceManager.startWatcherService(context, setting.defaultTunnel!!)
|
||||||
@@ -36,4 +34,3 @@ class BootReceiver : BroadcastReceiver() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
+12
-13
@@ -3,31 +3,31 @@ package com.zaneschepke.wireguardautotunnel.receiver
|
|||||||
import android.content.BroadcastReceiver
|
import android.content.BroadcastReceiver
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import com.zaneschepke.wireguardautotunnel.repository.Repository
|
import com.zaneschepke.wireguardautotunnel.Constants
|
||||||
|
import com.zaneschepke.wireguardautotunnel.goAsync
|
||||||
|
import com.zaneschepke.wireguardautotunnel.repository.SettingsDoa
|
||||||
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
|
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
|
||||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.model.Settings
|
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import javax.inject.Inject
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.cancel
|
import kotlinx.coroutines.cancel
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class NotificationActionReceiver : BroadcastReceiver() {
|
class NotificationActionReceiver : BroadcastReceiver() {
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var settingsRepo : Repository<Settings>
|
lateinit var settingsRepo: SettingsDoa
|
||||||
override fun onReceive(context: Context, intent: Intent?) {
|
|
||||||
CoroutineScope(Dispatchers.IO).launch {
|
override fun onReceive(
|
||||||
|
context: Context,
|
||||||
|
intent: Intent?
|
||||||
|
) = goAsync {
|
||||||
try {
|
try {
|
||||||
val settings = settingsRepo.getAll()
|
val settings = settingsRepo.getAll()
|
||||||
if (!settings.isNullOrEmpty()) {
|
if (settings.isNotEmpty()) {
|
||||||
val setting = settings.first()
|
val setting = settings.first()
|
||||||
if (setting.defaultTunnel != null) {
|
if (setting.defaultTunnel != null) {
|
||||||
ServiceManager.stopVpnService(context)
|
ServiceManager.stopVpnService(context)
|
||||||
delay(1000)
|
delay(Constants.TOGGLE_TUNNEL_DELAY)
|
||||||
ServiceManager.startVpnService(context, setting.defaultTunnel.toString())
|
ServiceManager.startVpnService(context, setting.defaultTunnel.toString())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -36,4 +36,3 @@ class NotificationActionReceiver : BroadcastReceiver() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
package com.zaneschepke.wireguardautotunnel.repository
|
||||||
|
|
||||||
|
import androidx.room.AutoMigration
|
||||||
|
import androidx.room.Database
|
||||||
|
import androidx.room.RoomDatabase
|
||||||
|
import androidx.room.TypeConverters
|
||||||
|
import com.zaneschepke.wireguardautotunnel.repository.model.Settings
|
||||||
|
import com.zaneschepke.wireguardautotunnel.repository.model.TunnelConfig
|
||||||
|
|
||||||
|
@Database(
|
||||||
|
entities = [Settings::class, TunnelConfig::class],
|
||||||
|
version = 4,
|
||||||
|
autoMigrations = [
|
||||||
|
AutoMigration(from = 1, to = 2), AutoMigration(from = 2, to = 3), AutoMigration(
|
||||||
|
from = 3,
|
||||||
|
to = 4
|
||||||
|
)
|
||||||
|
],
|
||||||
|
exportSchema = true
|
||||||
|
)
|
||||||
|
@TypeConverters(DatabaseListConverters::class)
|
||||||
|
abstract class AppDatabase : RoomDatabase() {
|
||||||
|
abstract fun settingDao(): SettingsDoa
|
||||||
|
|
||||||
|
abstract fun tunnelConfigDoa(): TunnelConfigDao
|
||||||
|
}
|
||||||
+24
@@ -0,0 +1,24 @@
|
|||||||
|
package com.zaneschepke.wireguardautotunnel.repository
|
||||||
|
|
||||||
|
import androidx.room.TypeConverter
|
||||||
|
import kotlinx.serialization.encodeToString
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
|
||||||
|
class DatabaseListConverters {
|
||||||
|
@TypeConverter
|
||||||
|
fun listToString(value: MutableList<String>): String {
|
||||||
|
return Json.encodeToString(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
@TypeConverter
|
||||||
|
fun stringToList(value: String): MutableList<String> {
|
||||||
|
if (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,16 +0,0 @@
|
|||||||
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()
|
|
||||||
}
|
|
||||||
@@ -1,63 +0,0 @@
|
|||||||
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,33 @@
|
|||||||
|
package com.zaneschepke.wireguardautotunnel.repository
|
||||||
|
|
||||||
|
import androidx.room.Dao
|
||||||
|
import androidx.room.Delete
|
||||||
|
import androidx.room.Insert
|
||||||
|
import androidx.room.OnConflictStrategy
|
||||||
|
import androidx.room.Query
|
||||||
|
import com.zaneschepke.wireguardautotunnel.repository.model.Settings
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
||||||
|
@Dao
|
||||||
|
interface SettingsDoa {
|
||||||
|
@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")
|
||||||
|
fun getAllFlow(): Flow<MutableList<Settings>>
|
||||||
|
|
||||||
|
@Delete
|
||||||
|
suspend fun delete(t: Settings)
|
||||||
|
|
||||||
|
@Query("SELECT COUNT('id') FROM settings")
|
||||||
|
suspend fun count(): Long
|
||||||
|
}
|
||||||
@@ -1,57 +0,0 @@
|
|||||||
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,33 @@
|
|||||||
|
package com.zaneschepke.wireguardautotunnel.repository
|
||||||
|
|
||||||
|
import androidx.room.Dao
|
||||||
|
import androidx.room.Delete
|
||||||
|
import androidx.room.Insert
|
||||||
|
import androidx.room.OnConflictStrategy
|
||||||
|
import androidx.room.Query
|
||||||
|
import com.zaneschepke.wireguardautotunnel.repository.model.TunnelConfig
|
||||||
|
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: List<TunnelConfig>)
|
||||||
|
|
||||||
|
@Query("SELECT * FROM TunnelConfig WHERE id=:id")
|
||||||
|
suspend fun getById(id: Long): TunnelConfig?
|
||||||
|
|
||||||
|
@Query("SELECT * FROM TunnelConfig")
|
||||||
|
suspend fun getAll(): List<TunnelConfig>
|
||||||
|
|
||||||
|
@Delete
|
||||||
|
suspend fun delete(t: TunnelConfig)
|
||||||
|
|
||||||
|
@Query("SELECT COUNT('id') FROM TunnelConfig")
|
||||||
|
suspend fun count(): Long
|
||||||
|
|
||||||
|
@Query("SELECT * FROM tunnelconfig")
|
||||||
|
fun getAllFlow(): Flow<MutableList<TunnelConfig>>
|
||||||
|
}
|
||||||
+39
@@ -0,0 +1,39 @@
|
|||||||
|
package com.zaneschepke.wireguardautotunnel.repository.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.preferencesDataStore
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.first
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
|
|
||||||
|
class DataStoreManager(private val context: Context) {
|
||||||
|
companion object {
|
||||||
|
val LOCATION_DISCLOSURE_SHOWN = booleanPreferencesKey("LOCATION_DISCLOSURE_SHOWN")
|
||||||
|
}
|
||||||
|
|
||||||
|
// preferences
|
||||||
|
private val preferencesKey = "preferences"
|
||||||
|
private val Context.dataStore by preferencesDataStore(
|
||||||
|
name = preferencesKey
|
||||||
|
)
|
||||||
|
|
||||||
|
suspend fun init() {
|
||||||
|
context.dataStore.data.first()
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun <T> saveToDataStore(key: Preferences.Key<T>, value: T) =
|
||||||
|
context.dataStore.edit {
|
||||||
|
it[key] = value
|
||||||
|
}
|
||||||
|
|
||||||
|
fun <T> getFromStore(key: Preferences.Key<T>) =
|
||||||
|
context.dataStore.data.map {
|
||||||
|
it[key]
|
||||||
|
}
|
||||||
|
|
||||||
|
val locationDisclosureFlow: Flow<Boolean?> = context.dataStore.data.map {
|
||||||
|
it[LOCATION_DISCLOSURE_SHOWN]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
package com.zaneschepke.wireguardautotunnel.repository.model
|
||||||
|
|
||||||
|
import androidx.room.ColumnInfo
|
||||||
|
import androidx.room.Entity
|
||||||
|
import androidx.room.PrimaryKey
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
data class Settings(
|
||||||
|
@PrimaryKey(autoGenerate = true) val id: Int = 0,
|
||||||
|
@ColumnInfo(name = "is_tunnel_enabled") var isAutoTunnelEnabled: Boolean = false,
|
||||||
|
@ColumnInfo(name = "is_tunnel_on_mobile_data_enabled") var isTunnelOnMobileDataEnabled: Boolean = false,
|
||||||
|
@ColumnInfo(name = "trusted_network_ssids") var trustedNetworkSSIDs: MutableList<String> = mutableListOf(),
|
||||||
|
@ColumnInfo(name = "default_tunnel") var defaultTunnel: String? = null,
|
||||||
|
@ColumnInfo(name = "is_always_on_vpn_enabled") var isAlwaysOnVpnEnabled: Boolean = false,
|
||||||
|
@ColumnInfo(name = "is_tunnel_on_ethernet_enabled") var isTunnelOnEthernetEnabled: Boolean = false,
|
||||||
|
@ColumnInfo(
|
||||||
|
name = "is_shortcuts_enabled",
|
||||||
|
defaultValue = "false"
|
||||||
|
) var isShortcutsEnabled: Boolean = false,
|
||||||
|
@ColumnInfo(
|
||||||
|
name = "is_battery_saver_enabled",
|
||||||
|
defaultValue = "false"
|
||||||
|
) var isBatterySaverEnabled: Boolean = false,
|
||||||
|
@ColumnInfo(
|
||||||
|
name = "is_tunnel_on_wifi_enabled",
|
||||||
|
defaultValue = "false"
|
||||||
|
) var isTunnelOnWifiEnabled: Boolean = false,
|
||||||
|
@ColumnInfo(
|
||||||
|
name = "is_kernel_enabled",
|
||||||
|
defaultValue = "false"
|
||||||
|
) var isKernelEnabled: Boolean = false,
|
||||||
|
@ColumnInfo(
|
||||||
|
name = "is_restore_on_boot_enabled",
|
||||||
|
defaultValue = "false"
|
||||||
|
) var isRestoreOnBootEnabled: Boolean = false,
|
||||||
|
@ColumnInfo(
|
||||||
|
name = "is_multi_tunnel_enabled",
|
||||||
|
defaultValue = "false"
|
||||||
|
) var isMultiTunnelEnabled: Boolean = false
|
||||||
|
) {
|
||||||
|
fun isTunnelConfigDefault(tunnelConfig: TunnelConfig): Boolean {
|
||||||
|
return if (defaultTunnel != null) {
|
||||||
|
val defaultConfig = TunnelConfig.from(defaultTunnel!!)
|
||||||
|
(tunnelConfig.id == defaultConfig.id)
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+34
@@ -0,0 +1,34 @@
|
|||||||
|
package com.zaneschepke.wireguardautotunnel.repository.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
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
|
||||||
|
@Entity(indices = [Index(value = ["name"], unique = true)])
|
||||||
|
@Serializable
|
||||||
|
data class TunnelConfig(
|
||||||
|
@PrimaryKey(autoGenerate = true) val id: Int = 0,
|
||||||
|
@ColumnInfo(name = "name") var name: String,
|
||||||
|
@ColumnInfo(name = "wg_quick") var wgQuick: String
|
||||||
|
) {
|
||||||
|
override fun toString(): String {
|
||||||
|
return Json.encodeToString(serializer(), this)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun from(string: String): TunnelConfig {
|
||||||
|
return Json.decodeFromString<TunnelConfig>(string)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun configFromQuick(wgQuick: String): Config {
|
||||||
|
val inputStream: InputStream = wgQuick.byteInputStream()
|
||||||
|
val reader = inputStream.bufferedReader(Charsets.UTF_8)
|
||||||
|
return Config.parse(reader)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.service.barcode
|
|
||||||
|
|
||||||
import kotlinx.coroutines.flow.Flow
|
|
||||||
|
|
||||||
interface CodeScanner {
|
|
||||||
fun scan() : Flow<String?>
|
|
||||||
}
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
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,5 +2,6 @@ package com.zaneschepke.wireguardautotunnel.service.foreground
|
|||||||
|
|
||||||
enum class Action {
|
enum class Action {
|
||||||
START,
|
START,
|
||||||
|
START_FOREGROUND,
|
||||||
STOP
|
STOP
|
||||||
}
|
}
|
||||||
+10
-7
@@ -1,28 +1,32 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.service.foreground
|
package com.zaneschepke.wireguardautotunnel.service.foreground
|
||||||
|
|
||||||
import android.app.Service
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.IBinder
|
import android.os.IBinder
|
||||||
|
import androidx.lifecycle.LifecycleService
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
|
||||||
|
open class ForegroundService : LifecycleService() {
|
||||||
open class ForegroundService : Service() {
|
|
||||||
|
|
||||||
private var isServiceStarted = false
|
private var isServiceStarted = false
|
||||||
|
|
||||||
override fun onBind(intent: Intent): IBinder? {
|
override fun onBind(intent: Intent): IBinder? {
|
||||||
|
super.onBind(intent)
|
||||||
// We don't provide binding, so return null
|
// We don't provide binding, so return null
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
override fun onStartCommand(
|
||||||
|
intent: Intent?,
|
||||||
|
flags: Int,
|
||||||
|
startId: Int
|
||||||
|
): Int {
|
||||||
|
super.onStartCommand(intent, flags, startId)
|
||||||
Timber.d("onStartCommand executed with startId: $startId")
|
Timber.d("onStartCommand executed with startId: $startId")
|
||||||
if (intent != null) {
|
if (intent != null) {
|
||||||
val action = intent.action
|
val action = intent.action
|
||||||
Timber.d("using an intent with action $action")
|
Timber.d("using an intent with action $action")
|
||||||
when (action) {
|
when (action) {
|
||||||
Action.START.name -> startService(intent.extras)
|
Action.START.name, Action.START_FOREGROUND.name -> startService(intent.extras)
|
||||||
Action.STOP.name -> stopService(intent.extras)
|
Action.STOP.name -> stopService(intent.extras)
|
||||||
"android.net.VpnService" -> {
|
"android.net.VpnService" -> {
|
||||||
Timber.d("Always-on VPN starting service")
|
Timber.d("Always-on VPN starting service")
|
||||||
@@ -39,7 +43,6 @@ open class ForegroundService : Service() {
|
|||||||
return START_STICKY
|
return START_STICKY
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
Timber.d("The service has been destroyed")
|
Timber.d("The service has been destroyed")
|
||||||
|
|||||||
+81
-25
@@ -1,13 +1,10 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.service.foreground
|
package com.zaneschepke.wireguardautotunnel.service.foreground
|
||||||
|
|
||||||
import android.app.ActivityManager
|
import android.app.ActivityManager
|
||||||
import android.app.Application
|
|
||||||
import android.app.Service
|
import android.app.Service
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Context.ACTIVITY_SERVICE
|
import android.content.Context.ACTIVITY_SERVICE
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import com.google.firebase.crashlytics.ktx.crashlytics
|
|
||||||
import com.google.firebase.ktx.Firebase
|
|
||||||
import com.zaneschepke.wireguardautotunnel.R
|
import com.zaneschepke.wireguardautotunnel.R
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
|
||||||
@@ -19,15 +16,24 @@ object ServiceManager {
|
|||||||
.getRunningServices(Integer.MAX_VALUE)
|
.getRunningServices(Integer.MAX_VALUE)
|
||||||
.any { it.service.className == service.name }
|
.any { it.service.className == service.name }
|
||||||
|
|
||||||
fun <T : Service> getServiceState(context: Context, cls : Class<T>): ServiceState {
|
fun <T : Service> getServiceState(
|
||||||
|
context: Context,
|
||||||
|
cls: Class<T>
|
||||||
|
): ServiceState {
|
||||||
val isServiceRunning = context.isServiceRunning(cls)
|
val isServiceRunning = context.isServiceRunning(cls)
|
||||||
return if (isServiceRunning) ServiceState.STARTED else ServiceState.STOPPED
|
return if (isServiceRunning) ServiceState.STARTED else ServiceState.STOPPED
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun <T : Service> actionOnService(action: Action, context: Context, cls : Class<T>, extras : Map<String,String>? = null) {
|
private 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
|
if (getServiceState(context, cls) == ServiceState.STOPPED && action == Action.STOP) return
|
||||||
if (getServiceState(context, cls) == ServiceState.STARTED && action == Action.START) return
|
if (getServiceState(context, cls) == ServiceState.STARTED && action == Action.START) return
|
||||||
val intent = Intent(context, cls).also {
|
val intent =
|
||||||
|
Intent(context, cls).also {
|
||||||
it.action = action.name
|
it.action = action.name
|
||||||
extras?.forEach { (k, v) ->
|
extras?.forEach { (k, v) ->
|
||||||
it.putExtra(k, v)
|
it.putExtra(k, v)
|
||||||
@@ -36,28 +42,33 @@ object ServiceManager {
|
|||||||
intent.component?.javaClass
|
intent.component?.javaClass
|
||||||
try {
|
try {
|
||||||
when (action) {
|
when (action) {
|
||||||
Action.START -> {
|
Action.START_FOREGROUND -> {
|
||||||
try {
|
|
||||||
context.startForegroundService(intent)
|
context.startForegroundService(intent)
|
||||||
} catch (e : Exception) {
|
}
|
||||||
Timber.e("Unable to start service foreground ${e.message}")
|
|
||||||
|
Action.START -> {
|
||||||
context.startService(intent)
|
context.startService(intent)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
Action.STOP -> context.startService(intent)
|
Action.STOP -> context.startService(intent)
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
e.message?.let { Firebase.crashlytics.log(it) }
|
Timber.e(e.message)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun startVpnService(context : Context, tunnelConfig : String) {
|
fun startVpnService(
|
||||||
|
context: Context,
|
||||||
|
tunnelConfig: String
|
||||||
|
) {
|
||||||
actionOnService(
|
actionOnService(
|
||||||
Action.START,
|
Action.START,
|
||||||
context,
|
context,
|
||||||
WireGuardTunnelService::class.java,
|
WireGuardTunnelService::class.java,
|
||||||
mapOf(context.getString(R.string.tunnel_extras_key) to tunnelConfig))
|
mapOf(context.getString(R.string.tunnel_extras_key) to tunnelConfig)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun stopVpnService(context: Context) {
|
fun stopVpnService(context: Context) {
|
||||||
actionOnService(
|
actionOnService(
|
||||||
Action.STOP,
|
Action.STOP,
|
||||||
@@ -66,25 +77,70 @@ object ServiceManager {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun startWatcherService(context : Context, tunnelConfig : String) {
|
fun startVpnServiceForeground(
|
||||||
|
context: Context,
|
||||||
|
tunnelConfig: String
|
||||||
|
) {
|
||||||
actionOnService(
|
actionOnService(
|
||||||
Action.START, context,
|
Action.START_FOREGROUND,
|
||||||
WireGuardConnectivityWatcherService::class.java, mapOf(context.
|
context,
|
||||||
getString(R.string.tunnel_extras_key) to
|
WireGuardTunnelService::class.java,
|
||||||
tunnelConfig))
|
mapOf(context.getString(R.string.tunnel_extras_key) to tunnelConfig)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun startWatcherServiceForeground(
|
||||||
|
context: Context,
|
||||||
|
tunnelConfig: String
|
||||||
|
) {
|
||||||
|
actionOnService(
|
||||||
|
Action.START,
|
||||||
|
context,
|
||||||
|
WireGuardConnectivityWatcherService::class.java,
|
||||||
|
mapOf(
|
||||||
|
context
|
||||||
|
.getString(R.string.tunnel_extras_key) to
|
||||||
|
tunnelConfig
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun startWatcherService(
|
||||||
|
context: Context,
|
||||||
|
tunnelConfig: String
|
||||||
|
) {
|
||||||
|
actionOnService(
|
||||||
|
Action.START,
|
||||||
|
context,
|
||||||
|
WireGuardConnectivityWatcherService::class.java,
|
||||||
|
mapOf(
|
||||||
|
context
|
||||||
|
.getString(R.string.tunnel_extras_key) to
|
||||||
|
tunnelConfig
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun stopWatcherService(context: Context) {
|
fun stopWatcherService(context: Context) {
|
||||||
actionOnService(
|
actionOnService(
|
||||||
Action.STOP, context,
|
Action.STOP,
|
||||||
WireGuardConnectivityWatcherService::class.java)
|
context,
|
||||||
|
WireGuardConnectivityWatcherService::class.java
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun toggleWatcherService(context: Context, tunnelConfig : String) {
|
fun toggleWatcherServiceForeground(
|
||||||
when(getServiceState( context,
|
context: Context,
|
||||||
WireGuardConnectivityWatcherService::class.java,)) {
|
tunnelConfig: String
|
||||||
|
) {
|
||||||
|
when (
|
||||||
|
getServiceState(
|
||||||
|
context,
|
||||||
|
WireGuardConnectivityWatcherService::class.java
|
||||||
|
)
|
||||||
|
) {
|
||||||
ServiceState.STARTED -> stopWatcherService(context)
|
ServiceState.STARTED -> stopWatcherService(context)
|
||||||
ServiceState.STOPPED -> startWatcherService(context, tunnelConfig)
|
ServiceState.STOPPED -> startWatcherServiceForeground(context, tunnelConfig)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+167
-38
@@ -7,30 +7,32 @@ import android.content.Intent
|
|||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.PowerManager
|
import android.os.PowerManager
|
||||||
import android.os.SystemClock
|
import android.os.SystemClock
|
||||||
|
import androidx.core.app.ServiceCompat
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
import com.wireguard.android.backend.Tunnel
|
import com.wireguard.android.backend.Tunnel
|
||||||
import com.zaneschepke.wireguardautotunnel.Constants
|
import com.zaneschepke.wireguardautotunnel.Constants
|
||||||
import com.zaneschepke.wireguardautotunnel.R
|
import com.zaneschepke.wireguardautotunnel.R
|
||||||
import com.zaneschepke.wireguardautotunnel.repository.Repository
|
import com.zaneschepke.wireguardautotunnel.repository.SettingsDoa
|
||||||
|
import com.zaneschepke.wireguardautotunnel.repository.model.Settings
|
||||||
|
import com.zaneschepke.wireguardautotunnel.service.network.EthernetService
|
||||||
import com.zaneschepke.wireguardautotunnel.service.network.MobileDataService
|
import com.zaneschepke.wireguardautotunnel.service.network.MobileDataService
|
||||||
import com.zaneschepke.wireguardautotunnel.service.network.NetworkService
|
import com.zaneschepke.wireguardautotunnel.service.network.NetworkService
|
||||||
import com.zaneschepke.wireguardautotunnel.service.network.NetworkStatus
|
import com.zaneschepke.wireguardautotunnel.service.network.NetworkStatus
|
||||||
import com.zaneschepke.wireguardautotunnel.service.network.WifiService
|
import com.zaneschepke.wireguardautotunnel.service.network.WifiService
|
||||||
import com.zaneschepke.wireguardautotunnel.service.notification.NotificationService
|
import com.zaneschepke.wireguardautotunnel.service.notification.NotificationService
|
||||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnService
|
import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnService
|
||||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.model.Settings
|
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import javax.inject.Inject
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class WireGuardConnectivityWatcherService : ForegroundService() {
|
class WireGuardConnectivityWatcherService : ForegroundService() {
|
||||||
|
private val foregroundId = 122
|
||||||
private val foregroundId = 122;
|
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var wifiService: NetworkService<WifiService>
|
lateinit var wifiService: NetworkService<WifiService>
|
||||||
@@ -39,7 +41,10 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
|
|||||||
lateinit var mobileDataService: NetworkService<MobileDataService>
|
lateinit var mobileDataService: NetworkService<MobileDataService>
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var settingsRepo: Repository<Settings>
|
lateinit var ethernetService: NetworkService<EthernetService>
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var settingsRepo: SettingsDoa
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var notificationService: NotificationService
|
lateinit var notificationService: NotificationService
|
||||||
@@ -47,33 +52,50 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
|
|||||||
@Inject
|
@Inject
|
||||||
lateinit var vpnService: VpnService
|
lateinit var vpnService: VpnService
|
||||||
|
|
||||||
private var isWifiConnected = false;
|
private var isWifiConnected = false
|
||||||
private var isMobileDataConnected = false;
|
private var isEthernetConnected = false
|
||||||
private var currentNetworkSSID = "";
|
private var isMobileDataConnected = false
|
||||||
|
private var currentNetworkSSID = ""
|
||||||
|
|
||||||
private lateinit var watcherJob : Job;
|
private lateinit var watcherJob: Job
|
||||||
private lateinit var setting: Settings
|
private lateinit var setting: Settings
|
||||||
private lateinit var tunnelConfig: String
|
private lateinit var tunnelConfig: String
|
||||||
|
|
||||||
private var wakeLock: PowerManager.WakeLock? = null
|
private var wakeLock: PowerManager.WakeLock? = null
|
||||||
private val tag = this.javaClass.name;
|
private val tag = this.javaClass.name
|
||||||
|
|
||||||
|
override fun onCreate() {
|
||||||
|
super.onCreate()
|
||||||
|
lifecycleScope.launch(Dispatchers.Main) {
|
||||||
|
try {
|
||||||
|
launchWatcherNotification()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Timber.e("Failed to start watcher service, not enough permissions")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun startService(extras: Bundle?) {
|
override fun startService(extras: Bundle?) {
|
||||||
super.startService(extras)
|
super.startService(extras)
|
||||||
|
try {
|
||||||
|
launchWatcherNotification()
|
||||||
val tunnelId = extras?.getString(getString(R.string.tunnel_extras_key))
|
val tunnelId = extras?.getString(getString(R.string.tunnel_extras_key))
|
||||||
if (tunnelId != null) {
|
if (tunnelId != null) {
|
||||||
this.tunnelConfig = tunnelId
|
this.tunnelConfig = tunnelId
|
||||||
}
|
}
|
||||||
// we need this lock so our service gets not affected by Doze Mode
|
// we need this lock so our service gets not affected by Doze Mode
|
||||||
|
lifecycleScope.launch {
|
||||||
initWakeLock()
|
initWakeLock()
|
||||||
|
}
|
||||||
cancelWatcherJob()
|
cancelWatcherJob()
|
||||||
launchWatcherNotification()
|
|
||||||
if (this::tunnelConfig.isInitialized) {
|
if (this::tunnelConfig.isInitialized) {
|
||||||
startWatcherJob()
|
startWatcherJob()
|
||||||
} else {
|
} else {
|
||||||
stopService(extras)
|
stopService(extras)
|
||||||
}
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Timber.e("Failed to launch watcher service, no permissions")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun stopService(extras: Bundle?) {
|
override fun stopService(extras: Bundle?) {
|
||||||
@@ -88,32 +110,60 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun launchWatcherNotification() {
|
private fun launchWatcherNotification() {
|
||||||
val notification = notificationService.createNotification(
|
val notification =
|
||||||
|
notificationService.createNotification(
|
||||||
channelId = getString(R.string.watcher_channel_id),
|
channelId = getString(R.string.watcher_channel_id),
|
||||||
channelName = getString(R.string.watcher_channel_name),
|
channelName = getString(R.string.watcher_channel_name),
|
||||||
description = getString(R.string.watcher_notification_text))
|
description = getString(R.string.watcher_notification_text),
|
||||||
super.startForeground(foregroundId, notification)
|
vibration = false
|
||||||
|
)
|
||||||
|
ServiceCompat.startForeground(
|
||||||
|
this,
|
||||||
|
foregroundId,
|
||||||
|
notification,
|
||||||
|
Constants.SYSTEM_EXEMPT_SERVICE_TYPE_ID
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// try to start task again if killed
|
// try to start task again if killed
|
||||||
override fun onTaskRemoved(rootIntent: Intent) {
|
override fun onTaskRemoved(rootIntent: Intent) {
|
||||||
Timber.d("Task Removed called")
|
Timber.d("Task Removed called")
|
||||||
val restartServiceIntent = Intent(rootIntent)
|
val restartServiceIntent = Intent(rootIntent)
|
||||||
val restartServicePendingIntent: PendingIntent = PendingIntent.getService(this, 1, restartServiceIntent,
|
val restartServicePendingIntent: PendingIntent =
|
||||||
PendingIntent.FLAG_ONE_SHOT or PendingIntent.FLAG_IMMUTABLE);
|
PendingIntent.getService(
|
||||||
applicationContext.getSystemService(Context.ALARM_SERVICE);
|
this,
|
||||||
val alarmService: AlarmManager = applicationContext.getSystemService(Context.ALARM_SERVICE) as AlarmManager;
|
1,
|
||||||
alarmService.set(AlarmManager.ELAPSED_REALTIME, SystemClock.elapsedRealtime() + 1000, restartServicePendingIntent);
|
restartServiceIntent,
|
||||||
|
PendingIntent.FLAG_ONE_SHOT or PendingIntent.FLAG_IMMUTABLE
|
||||||
|
)
|
||||||
|
applicationContext.getSystemService(Context.ALARM_SERVICE)
|
||||||
|
val alarmService: AlarmManager =
|
||||||
|
applicationContext.getSystemService(Context.ALARM_SERVICE) as AlarmManager
|
||||||
|
alarmService.set(
|
||||||
|
AlarmManager.ELAPSED_REALTIME,
|
||||||
|
SystemClock.elapsedRealtime() + 1000,
|
||||||
|
restartServicePendingIntent
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun initWakeLock() {
|
private suspend fun initWakeLock() {
|
||||||
|
val isBatterySaverOn =
|
||||||
|
withContext(lifecycleScope.coroutineContext) {
|
||||||
|
settingsRepo.getAll().firstOrNull()?.isBatterySaverEnabled ?: false
|
||||||
|
}
|
||||||
wakeLock =
|
wakeLock =
|
||||||
(getSystemService(Context.POWER_SERVICE) as PowerManager).run {
|
(getSystemService(Context.POWER_SERVICE) as PowerManager).run {
|
||||||
newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "$tag::lock").apply {
|
newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "$tag::lock").apply {
|
||||||
|
if (isBatterySaverOn) {
|
||||||
|
Timber.d("Initiating wakelock with timeout")
|
||||||
|
acquire(Constants.WATCHER_SERVICE_WAKE_LOCK_TIMEOUT)
|
||||||
|
} else {
|
||||||
|
Timber.d("Initiating wakelock with zero timeout")
|
||||||
acquire()
|
acquire()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun cancelWatcherJob() {
|
private fun cancelWatcherJob() {
|
||||||
if (this::watcherJob.isInitialized) {
|
if (this::watcherJob.isInitialized) {
|
||||||
@@ -122,9 +172,10 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun startWatcherJob() {
|
private fun startWatcherJob() {
|
||||||
watcherJob = CoroutineScope(Dispatchers.IO).launch {
|
watcherJob =
|
||||||
val settings = settingsRepo.getAll();
|
lifecycleScope.launch(Dispatchers.IO) {
|
||||||
if(!settings.isNullOrEmpty()) {
|
val settings = settingsRepo.getAll()
|
||||||
|
if (settings.isNotEmpty()) {
|
||||||
setting = settings[0]
|
setting = settings[0]
|
||||||
}
|
}
|
||||||
launch {
|
launch {
|
||||||
@@ -135,6 +186,11 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
|
|||||||
watchForMobileDataConnectivityChanges()
|
watchForMobileDataConnectivityChanges()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (setting.isTunnelOnEthernetEnabled) {
|
||||||
|
launch {
|
||||||
|
watchForEthernetConnectivityChanges()
|
||||||
|
}
|
||||||
|
}
|
||||||
launch {
|
launch {
|
||||||
manageVpn()
|
manageVpn()
|
||||||
}
|
}
|
||||||
@@ -148,10 +204,12 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
|
|||||||
Timber.d("Gained Mobile data connection")
|
Timber.d("Gained Mobile data connection")
|
||||||
isMobileDataConnected = true
|
isMobileDataConnected = true
|
||||||
}
|
}
|
||||||
|
|
||||||
is NetworkStatus.CapabilitiesChanged -> {
|
is NetworkStatus.CapabilitiesChanged -> {
|
||||||
isMobileDataConnected = true
|
isMobileDataConnected = true
|
||||||
Timber.d("Mobile data capabilities changed")
|
Timber.d("Mobile data capabilities changed")
|
||||||
}
|
}
|
||||||
|
|
||||||
is NetworkStatus.Unavailable -> {
|
is NetworkStatus.Unavailable -> {
|
||||||
isMobileDataConnected = false
|
isMobileDataConnected = false
|
||||||
Timber.d("Lost mobile data connection")
|
Timber.d("Lost mobile data connection")
|
||||||
@@ -160,6 +218,27 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private suspend fun watchForEthernetConnectivityChanges() {
|
||||||
|
ethernetService.networkStatus.collect {
|
||||||
|
when (it) {
|
||||||
|
is NetworkStatus.Available -> {
|
||||||
|
Timber.d("Gained Ethernet connection")
|
||||||
|
isEthernetConnected = true
|
||||||
|
}
|
||||||
|
|
||||||
|
is NetworkStatus.CapabilitiesChanged -> {
|
||||||
|
Timber.d("Ethernet capabilities changed")
|
||||||
|
isEthernetConnected = true
|
||||||
|
}
|
||||||
|
|
||||||
|
is NetworkStatus.Unavailable -> {
|
||||||
|
isEthernetConnected = false
|
||||||
|
Timber.d("Lost Ethernet connection")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private suspend fun watchForWifiConnectivityChanges() {
|
private suspend fun watchForWifiConnectivityChanges() {
|
||||||
wifiService.networkStatus.collect {
|
wifiService.networkStatus.collect {
|
||||||
when (it) {
|
when (it) {
|
||||||
@@ -167,11 +246,15 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
|
|||||||
Timber.d("Gained Wi-Fi connection")
|
Timber.d("Gained Wi-Fi connection")
|
||||||
isWifiConnected = true
|
isWifiConnected = true
|
||||||
}
|
}
|
||||||
|
|
||||||
is NetworkStatus.CapabilitiesChanged -> {
|
is NetworkStatus.CapabilitiesChanged -> {
|
||||||
Timber.d("Wifi capabilities changed")
|
Timber.d("Wifi capabilities changed")
|
||||||
isWifiConnected = true
|
isWifiConnected = true
|
||||||
currentNetworkSSID = wifiService.getNetworkName(it.networkCapabilities) ?: "";
|
val ssid = wifiService.getNetworkName(it.networkCapabilities) ?: ""
|
||||||
|
Timber.d("Detected SSID: $ssid")
|
||||||
|
currentNetworkSSID = ssid
|
||||||
}
|
}
|
||||||
|
|
||||||
is NetworkStatus.Unavailable -> {
|
is NetworkStatus.Unavailable -> {
|
||||||
isWifiConnected = false
|
isWifiConnected = false
|
||||||
Timber.d("Lost Wi-Fi connection")
|
Timber.d("Lost Wi-Fi connection")
|
||||||
@@ -181,24 +264,70 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun manageVpn() {
|
private suspend fun manageVpn() {
|
||||||
while(watcherJob.isActive) {
|
while (true) {
|
||||||
if(setting.isTunnelOnMobileDataEnabled &&
|
when {
|
||||||
!isWifiConnected &&
|
(
|
||||||
isMobileDataConnected
|
(
|
||||||
&& vpnService.getState() == Tunnel.State.DOWN) {
|
isEthernetConnected &&
|
||||||
|
setting.isTunnelOnEthernetEnabled &&
|
||||||
|
vpnService.getState() == Tunnel.State.DOWN
|
||||||
|
)
|
||||||
|
) ->
|
||||||
ServiceManager.startVpnService(this, tunnelConfig)
|
ServiceManager.startVpnService(this, tunnelConfig)
|
||||||
} else if(!setting.isTunnelOnMobileDataEnabled &&
|
|
||||||
|
(
|
||||||
|
!isEthernetConnected &&
|
||||||
|
setting.isTunnelOnMobileDataEnabled &&
|
||||||
!isWifiConnected &&
|
!isWifiConnected &&
|
||||||
vpnService.getState() == Tunnel.State.UP) {
|
isMobileDataConnected &&
|
||||||
|
vpnService.getState() == Tunnel.State.DOWN
|
||||||
|
) ->
|
||||||
|
ServiceManager.startVpnService(this, tunnelConfig)
|
||||||
|
|
||||||
|
(
|
||||||
|
!isEthernetConnected &&
|
||||||
|
!setting.isTunnelOnMobileDataEnabled &&
|
||||||
|
!isWifiConnected &&
|
||||||
|
vpnService.getState() == Tunnel.State.UP
|
||||||
|
) ->
|
||||||
ServiceManager.stopVpnService(this)
|
ServiceManager.stopVpnService(this)
|
||||||
} else if(isWifiConnected &&
|
|
||||||
|
(
|
||||||
|
!isEthernetConnected && isWifiConnected &&
|
||||||
!setting.trustedNetworkSSIDs.contains(currentNetworkSSID) &&
|
!setting.trustedNetworkSSIDs.contains(currentNetworkSSID) &&
|
||||||
(vpnService.getState() != Tunnel.State.UP)) {
|
setting.isTunnelOnWifiEnabled &&
|
||||||
|
(vpnService.getState() != Tunnel.State.UP)
|
||||||
|
) ->
|
||||||
ServiceManager.startVpnService(this, tunnelConfig)
|
ServiceManager.startVpnService(this, tunnelConfig)
|
||||||
} else if((isWifiConnected &&
|
|
||||||
setting.trustedNetworkSSIDs.contains(currentNetworkSSID)) &&
|
(
|
||||||
(vpnService.getState() == Tunnel.State.UP)) {
|
!isEthernetConnected && (
|
||||||
|
isWifiConnected &&
|
||||||
|
setting.trustedNetworkSSIDs.contains(currentNetworkSSID)
|
||||||
|
) &&
|
||||||
|
(vpnService.getState() == Tunnel.State.UP)
|
||||||
|
) ->
|
||||||
ServiceManager.stopVpnService(this)
|
ServiceManager.stopVpnService(this)
|
||||||
|
|
||||||
|
(
|
||||||
|
!isEthernetConnected && (
|
||||||
|
isWifiConnected &&
|
||||||
|
!setting.isTunnelOnWifiEnabled &&
|
||||||
|
(vpnService.getState() == Tunnel.State.UP)
|
||||||
|
)
|
||||||
|
) ->
|
||||||
|
ServiceManager.stopVpnService(this)
|
||||||
|
|
||||||
|
(
|
||||||
|
!isEthernetConnected && !isWifiConnected &&
|
||||||
|
!isMobileDataConnected &&
|
||||||
|
(vpnService.getState() == Tunnel.State.UP)
|
||||||
|
) ->
|
||||||
|
ServiceManager.stopVpnService(this)
|
||||||
|
|
||||||
|
else -> {
|
||||||
|
// Do nothing
|
||||||
|
}
|
||||||
}
|
}
|
||||||
delay(Constants.VPN_CONNECTIVITY_CHECK_INTERVAL)
|
delay(Constants.VPN_CONNECTIVITY_CHECK_INTERVAL)
|
||||||
}
|
}
|
||||||
|
|||||||
+60
-24
@@ -3,32 +3,32 @@ package com.zaneschepke.wireguardautotunnel.service.foreground
|
|||||||
import android.app.PendingIntent
|
import android.app.PendingIntent
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import androidx.core.app.ServiceCompat
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import com.zaneschepke.wireguardautotunnel.Constants
|
||||||
import com.zaneschepke.wireguardautotunnel.R
|
import com.zaneschepke.wireguardautotunnel.R
|
||||||
import com.zaneschepke.wireguardautotunnel.receiver.NotificationActionReceiver
|
import com.zaneschepke.wireguardautotunnel.receiver.NotificationActionReceiver
|
||||||
import com.zaneschepke.wireguardautotunnel.repository.Repository
|
import com.zaneschepke.wireguardautotunnel.repository.SettingsDoa
|
||||||
|
import com.zaneschepke.wireguardautotunnel.repository.model.TunnelConfig
|
||||||
import com.zaneschepke.wireguardautotunnel.service.notification.NotificationService
|
import com.zaneschepke.wireguardautotunnel.service.notification.NotificationService
|
||||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.HandshakeStatus
|
import com.zaneschepke.wireguardautotunnel.service.tunnel.HandshakeStatus
|
||||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnService
|
import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnService
|
||||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.model.Settings
|
|
||||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.model.TunnelConfig
|
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import javax.inject.Inject
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class WireGuardTunnelService : ForegroundService() {
|
class WireGuardTunnelService : ForegroundService() {
|
||||||
|
private val foregroundId = 123
|
||||||
private val foregroundId = 123;
|
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var vpnService: VpnService
|
lateinit var vpnService: VpnService
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var settingsRepo: Repository<Settings>
|
lateinit var settingsRepo: SettingsDoa
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var notificationService: NotificationService
|
lateinit var notificationService: NotificationService
|
||||||
@@ -37,36 +37,44 @@ class WireGuardTunnelService : ForegroundService() {
|
|||||||
|
|
||||||
private var tunnelName: String = ""
|
private var tunnelName: String = ""
|
||||||
|
|
||||||
|
override fun onCreate() {
|
||||||
|
super.onCreate()
|
||||||
|
lifecycleScope.launch(Dispatchers.Main) {
|
||||||
|
launchVpnStartingNotification()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun startService(extras: Bundle?) {
|
override fun startService(extras: Bundle?) {
|
||||||
super.startService(extras)
|
super.startService(extras)
|
||||||
|
launchVpnStartingNotification()
|
||||||
val tunnelConfigString = extras?.getString(getString(R.string.tunnel_extras_key))
|
val tunnelConfigString = extras?.getString(getString(R.string.tunnel_extras_key))
|
||||||
cancelJob()
|
cancelJob()
|
||||||
job = CoroutineScope(Dispatchers.IO).launch {
|
job =
|
||||||
|
lifecycleScope.launch(Dispatchers.IO) {
|
||||||
|
launch {
|
||||||
if (tunnelConfigString != null) {
|
if (tunnelConfigString != null) {
|
||||||
try {
|
try {
|
||||||
val tunnelConfig = TunnelConfig.from(tunnelConfigString)
|
val tunnelConfig = TunnelConfig.from(tunnelConfigString)
|
||||||
tunnelName = tunnelConfig.name
|
tunnelName = tunnelConfig.name
|
||||||
vpnService.startTunnel(tunnelConfig)
|
vpnService.startTunnel(tunnelConfig)
|
||||||
launchVpnStartingNotification()
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Timber.e("Problem starting tunnel: ${e.message}")
|
Timber.e("Problem starting tunnel: ${e.message}")
|
||||||
stopService(extras)
|
stopService(extras)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Timber.d("Tunnel config null, starting default tunnel")
|
Timber.d("Tunnel config null, starting default tunnel")
|
||||||
val settings = settingsRepo.getAll();
|
val settings = settingsRepo.getAll()
|
||||||
if(!settings.isNullOrEmpty()) {
|
if (settings.isNotEmpty()) {
|
||||||
val setting = settings[0]
|
val setting = settings[0]
|
||||||
if (setting.defaultTunnel != null && setting.isAlwaysOnVpnEnabled) {
|
if (setting.defaultTunnel != null && setting.isAlwaysOnVpnEnabled) {
|
||||||
val tunnelConfig = TunnelConfig.from(setting.defaultTunnel!!)
|
val tunnelConfig = TunnelConfig.from(setting.defaultTunnel!!)
|
||||||
tunnelName = tunnelConfig.name
|
tunnelName = tunnelConfig.name
|
||||||
vpnService.startTunnel(tunnelConfig)
|
vpnService.startTunnel(tunnelConfig)
|
||||||
launchVpnStartingNotification()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
CoroutineScope(job).launch {
|
launch {
|
||||||
var didShowConnected = false
|
var didShowConnected = false
|
||||||
var didShowFailedHandshakeNotification = false
|
var didShowFailedHandshakeNotification = false
|
||||||
vpnService.handshakeStatus.collect {
|
vpnService.handshakeStatus.collect {
|
||||||
@@ -75,20 +83,26 @@ class WireGuardTunnelService : ForegroundService() {
|
|||||||
}
|
}
|
||||||
HandshakeStatus.NEVER_CONNECTED -> {
|
HandshakeStatus.NEVER_CONNECTED -> {
|
||||||
if (!didShowFailedHandshakeNotification) {
|
if (!didShowFailedHandshakeNotification) {
|
||||||
launchVpnConnectionFailedNotification(getString(R.string.initial_connection_failure_message))
|
launchVpnConnectionFailedNotification(
|
||||||
|
getString(R.string.initial_connection_failure_message)
|
||||||
|
)
|
||||||
didShowFailedHandshakeNotification = true
|
didShowFailedHandshakeNotification = true
|
||||||
didShowConnected = false
|
didShowConnected = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
HandshakeStatus.HEALTHY -> {
|
HandshakeStatus.HEALTHY -> {
|
||||||
if (!didShowConnected) {
|
if (!didShowConnected) {
|
||||||
launchVpnConnectedNotification()
|
launchVpnConnectedNotification()
|
||||||
didShowConnected = true
|
didShowConnected = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
HandshakeStatus.STALE -> {}
|
||||||
HandshakeStatus.UNHEALTHY -> {
|
HandshakeStatus.UNHEALTHY -> {
|
||||||
if (!didShowFailedHandshakeNotification) {
|
if (!didShowFailedHandshakeNotification) {
|
||||||
launchVpnConnectionFailedNotification(getString(R.string.lost_connection_failure_message))
|
launchVpnConnectionFailedNotification(
|
||||||
|
getString(R.string.lost_connection_failure_message)
|
||||||
|
)
|
||||||
didShowFailedHandshakeNotification = true
|
didShowFailedHandshakeNotification = true
|
||||||
didShowConnected = false
|
didShowConnected = false
|
||||||
}
|
}
|
||||||
@@ -97,10 +111,11 @@ class WireGuardTunnelService : ForegroundService() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun stopService(extras: Bundle?) {
|
override fun stopService(extras: Bundle?) {
|
||||||
super.stopService(extras)
|
super.stopService(extras)
|
||||||
CoroutineScope(Dispatchers.IO).launch() {
|
lifecycleScope.launch(Dispatchers.IO) {
|
||||||
vpnService.stopTunnel()
|
vpnService.stopTunnel()
|
||||||
}
|
}
|
||||||
cancelJob()
|
cancelJob()
|
||||||
@@ -108,44 +123,65 @@ class WireGuardTunnelService : ForegroundService() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun launchVpnConnectedNotification() {
|
private fun launchVpnConnectedNotification() {
|
||||||
val notification = notificationService.createNotification(
|
val notification =
|
||||||
|
notificationService.createNotification(
|
||||||
channelId = getString(R.string.vpn_channel_id),
|
channelId = getString(R.string.vpn_channel_id),
|
||||||
channelName = getString(R.string.vpn_channel_name),
|
channelName = getString(R.string.vpn_channel_name),
|
||||||
title = getString(R.string.tunnel_start_title),
|
title = getString(R.string.tunnel_start_title),
|
||||||
onGoing = false,
|
onGoing = false,
|
||||||
|
vibration = false,
|
||||||
showTimestamp = true,
|
showTimestamp = true,
|
||||||
description = "${getString(R.string.tunnel_start_text)} $tunnelName"
|
description = "${getString(R.string.tunnel_start_text)} $tunnelName"
|
||||||
)
|
)
|
||||||
super.startForeground(foregroundId, notification)
|
ServiceCompat.startForeground(
|
||||||
|
this,
|
||||||
|
foregroundId,
|
||||||
|
notification,
|
||||||
|
Constants.SYSTEM_EXEMPT_SERVICE_TYPE_ID
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun launchVpnStartingNotification() {
|
private fun launchVpnStartingNotification() {
|
||||||
val notification = notificationService.createNotification(
|
val notification =
|
||||||
|
notificationService.createNotification(
|
||||||
channelId = getString(R.string.vpn_channel_id),
|
channelId = getString(R.string.vpn_channel_id),
|
||||||
channelName = getString(R.string.vpn_channel_name),
|
channelName = getString(R.string.vpn_channel_name),
|
||||||
title = getString(R.string.vpn_starting),
|
title = getString(R.string.vpn_starting),
|
||||||
onGoing = false,
|
onGoing = false,
|
||||||
|
vibration = false,
|
||||||
showTimestamp = true,
|
showTimestamp = true,
|
||||||
description = getString(R.string.attempt_connection)
|
description = getString(R.string.attempt_connection)
|
||||||
)
|
)
|
||||||
super.startForeground(foregroundId, notification)
|
ServiceCompat.startForeground(
|
||||||
|
this,
|
||||||
|
foregroundId,
|
||||||
|
notification,
|
||||||
|
Constants.SYSTEM_EXEMPT_SERVICE_TYPE_ID
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun launchVpnConnectionFailedNotification(message: String) {
|
private fun launchVpnConnectionFailedNotification(message: String) {
|
||||||
val notification = notificationService.createNotification(
|
val notification =
|
||||||
|
notificationService.createNotification(
|
||||||
channelId = getString(R.string.vpn_channel_id),
|
channelId = getString(R.string.vpn_channel_id),
|
||||||
channelName = getString(R.string.vpn_channel_name),
|
channelName = getString(R.string.vpn_channel_name),
|
||||||
action = PendingIntent.getBroadcast(this,0,Intent(this, NotificationActionReceiver::class.java),PendingIntent.FLAG_IMMUTABLE),
|
action =
|
||||||
|
PendingIntent.getBroadcast(
|
||||||
|
this,
|
||||||
|
0,
|
||||||
|
Intent(this, NotificationActionReceiver::class.java),
|
||||||
|
PendingIntent.FLAG_IMMUTABLE
|
||||||
|
),
|
||||||
actionText = getString(R.string.restart),
|
actionText = getString(R.string.restart),
|
||||||
title = getString(R.string.vpn_connection_failed),
|
title = getString(R.string.vpn_connection_failed),
|
||||||
onGoing = false,
|
onGoing = false,
|
||||||
|
vibration = true,
|
||||||
showTimestamp = true,
|
showTimestamp = true,
|
||||||
description = message
|
description = message
|
||||||
)
|
)
|
||||||
super.startForeground(foregroundId, notification)
|
super.startForeground(foregroundId, notification)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private fun cancelJob() {
|
private fun cancelJob() {
|
||||||
if (this::job.isInitialized) {
|
if (this::job.isInitialized) {
|
||||||
job.cancel()
|
job.cancel()
|
||||||
|
|||||||
+32
-13
@@ -14,16 +14,20 @@ import kotlinx.coroutines.flow.Flow
|
|||||||
import kotlinx.coroutines.flow.callbackFlow
|
import kotlinx.coroutines.flow.callbackFlow
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
|
|
||||||
|
abstract class BaseNetworkService<T : BaseNetworkService<T>>(
|
||||||
abstract class BaseNetworkService<T : BaseNetworkService<T>>(val context: Context, networkCapability : Int) : NetworkService<T> {
|
val context: Context,
|
||||||
|
networkCapability: Int
|
||||||
|
) : NetworkService<T> {
|
||||||
private val connectivityManager =
|
private val connectivityManager =
|
||||||
context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
|
context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
|
||||||
|
|
||||||
private val wifiManager =
|
private val wifiManager =
|
||||||
context.applicationContext.getSystemService(Context.WIFI_SERVICE) as WifiManager
|
context.applicationContext.getSystemService(Context.WIFI_SERVICE) as WifiManager
|
||||||
|
|
||||||
override val networkStatus = callbackFlow {
|
override val networkStatus =
|
||||||
val networkStatusCallback = when (Build.VERSION.SDK_INT) {
|
callbackFlow {
|
||||||
|
val networkStatusCallback =
|
||||||
|
when (Build.VERSION.SDK_INT) {
|
||||||
in Build.VERSION_CODES.S..Int.MAX_VALUE -> {
|
in Build.VERSION_CODES.S..Int.MAX_VALUE -> {
|
||||||
object : ConnectivityManager.NetworkCallback(
|
object : ConnectivityManager.NetworkCallback(
|
||||||
FLAG_INCLUDE_LOCATION_INFO
|
FLAG_INCLUDE_LOCATION_INFO
|
||||||
@@ -40,14 +44,18 @@ abstract class BaseNetworkService<T : BaseNetworkService<T>>(val context: Contex
|
|||||||
network: Network,
|
network: Network,
|
||||||
networkCapabilities: NetworkCapabilities
|
networkCapabilities: NetworkCapabilities
|
||||||
) {
|
) {
|
||||||
trySend(NetworkStatus.CapabilitiesChanged(network, networkCapabilities))
|
trySend(
|
||||||
|
NetworkStatus.CapabilitiesChanged(
|
||||||
|
network,
|
||||||
|
networkCapabilities
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> {
|
else -> {
|
||||||
object : ConnectivityManager.NetworkCallback() {
|
object : ConnectivityManager.NetworkCallback() {
|
||||||
|
|
||||||
override fun onAvailable(network: Network) {
|
override fun onAvailable(network: Network) {
|
||||||
trySend(NetworkStatus.Available(network))
|
trySend(NetworkStatus.Available(network))
|
||||||
}
|
}
|
||||||
@@ -60,12 +68,18 @@ abstract class BaseNetworkService<T : BaseNetworkService<T>>(val context: Contex
|
|||||||
network: Network,
|
network: Network,
|
||||||
networkCapabilities: NetworkCapabilities
|
networkCapabilities: NetworkCapabilities
|
||||||
) {
|
) {
|
||||||
trySend(NetworkStatus.CapabilitiesChanged(network, networkCapabilities))
|
trySend(
|
||||||
|
NetworkStatus.CapabilitiesChanged(
|
||||||
|
network,
|
||||||
|
networkCapabilities
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val request = NetworkRequest.Builder()
|
val request =
|
||||||
|
NetworkRequest.Builder()
|
||||||
.addTransportType(networkCapability)
|
.addTransportType(networkCapability)
|
||||||
.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
|
.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
|
||||||
.addCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED)
|
.addCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED)
|
||||||
@@ -77,7 +91,6 @@ abstract class BaseNetworkService<T : BaseNetworkService<T>>(val context: Contex
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
override fun getNetworkName(networkCapabilities: NetworkCapabilities): String? {
|
override fun getNetworkName(networkCapabilities: NetworkCapabilities): String? {
|
||||||
var ssid: String? = getWifiNameFromCapabilities(networkCapabilities)
|
var ssid: String? = getWifiNameFromCapabilities(networkCapabilities)
|
||||||
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.R) {
|
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.R) {
|
||||||
@@ -89,7 +102,6 @@ abstract class BaseNetworkService<T : BaseNetworkService<T>>(val context: Contex
|
|||||||
return ssid?.trim('"')
|
return ssid?.trim('"')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private fun getWifiNameFromCapabilities(networkCapabilities: NetworkCapabilities): String? {
|
private fun getWifiNameFromCapabilities(networkCapabilities: NetworkCapabilities): String? {
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||||
@@ -107,11 +119,18 @@ abstract class BaseNetworkService<T : BaseNetworkService<T>>(val context: Contex
|
|||||||
inline fun <Result> Flow<NetworkStatus>.map(
|
inline fun <Result> Flow<NetworkStatus>.map(
|
||||||
crossinline onUnavailable: suspend (network: Network) -> Result,
|
crossinline onUnavailable: suspend (network: Network) -> Result,
|
||||||
crossinline onAvailable: suspend (network: Network) -> Result,
|
crossinline onAvailable: suspend (network: Network) -> Result,
|
||||||
crossinline onCapabilitiesChanged: suspend (network : Network, networkCapabilities : NetworkCapabilities) -> Result,
|
crossinline onCapabilitiesChanged: suspend (
|
||||||
): Flow<Result> = map { status ->
|
network: Network,
|
||||||
|
networkCapabilities: NetworkCapabilities
|
||||||
|
) -> Result
|
||||||
|
): Flow<Result> =
|
||||||
|
map { status ->
|
||||||
when (status) {
|
when (status) {
|
||||||
is NetworkStatus.Unavailable -> onUnavailable(status.network)
|
is NetworkStatus.Unavailable -> onUnavailable(status.network)
|
||||||
is NetworkStatus.Available -> onAvailable(status.network)
|
is NetworkStatus.Available -> onAvailable(status.network)
|
||||||
is NetworkStatus.CapabilitiesChanged -> onCapabilitiesChanged(status.network, status.networkCapabilities)
|
is NetworkStatus.CapabilitiesChanged -> onCapabilitiesChanged(
|
||||||
|
status.network,
|
||||||
|
status.networkCapabilities
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+13
@@ -0,0 +1,13 @@
|
|||||||
|
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
-3
@@ -5,6 +5,9 @@ import android.net.NetworkCapabilities
|
|||||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
class MobileDataService @Inject constructor(@ApplicationContext context: Context) :
|
class MobileDataService
|
||||||
BaseNetworkService<MobileDataService>(context, NetworkCapabilities.TRANSPORT_CELLULAR) {
|
@Inject
|
||||||
}
|
constructor(
|
||||||
|
@ApplicationContext context: Context
|
||||||
|
) :
|
||||||
|
BaseNetworkService<MobileDataService>(context, NetworkCapabilities.TRANSPORT_CELLULAR)
|
||||||
|
|||||||
+1
-1
@@ -5,6 +5,6 @@ import kotlinx.coroutines.flow.Flow
|
|||||||
|
|
||||||
interface NetworkService<T> {
|
interface NetworkService<T> {
|
||||||
fun getNetworkName(networkCapabilities: NetworkCapabilities): String?
|
fun getNetworkName(networkCapabilities: NetworkCapabilities): String?
|
||||||
val networkStatus : Flow<NetworkStatus>
|
|
||||||
|
|
||||||
|
val networkStatus: Flow<NetworkStatus>
|
||||||
}
|
}
|
||||||
+4
-1
@@ -5,6 +5,9 @@ import android.net.NetworkCapabilities
|
|||||||
|
|
||||||
sealed class NetworkStatus {
|
sealed class NetworkStatus {
|
||||||
class Available(val network: Network) : NetworkStatus()
|
class Available(val network: Network) : NetworkStatus()
|
||||||
|
|
||||||
class Unavailable(val network: Network) : NetworkStatus()
|
class Unavailable(val network: Network) : NetworkStatus()
|
||||||
class CapabilitiesChanged(val network : Network, val networkCapabilities : NetworkCapabilities) : NetworkStatus()
|
|
||||||
|
class CapabilitiesChanged(val network: Network, val networkCapabilities: NetworkCapabilities) :
|
||||||
|
NetworkStatus()
|
||||||
}
|
}
|
||||||
|
|||||||
+6
-3
@@ -5,6 +5,9 @@ import android.net.NetworkCapabilities
|
|||||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
class WifiService @Inject constructor(@ApplicationContext context: Context) :
|
class WifiService
|
||||||
BaseNetworkService<WifiService>(context, NetworkCapabilities.TRANSPORT_WIFI) {
|
@Inject
|
||||||
}
|
constructor(
|
||||||
|
@ApplicationContext context: Context
|
||||||
|
) :
|
||||||
|
BaseNetworkService<WifiService>(context, NetworkCapabilities.TRANSPORT_WIFI)
|
||||||
|
|||||||
+1
-1
@@ -14,7 +14,7 @@ interface NotificationService {
|
|||||||
description: String,
|
description: String,
|
||||||
showTimestamp: Boolean = false,
|
showTimestamp: Boolean = false,
|
||||||
importance: Int = NotificationManager.IMPORTANCE_HIGH,
|
importance: Int = NotificationManager.IMPORTANCE_HIGH,
|
||||||
vibration: Boolean = true,
|
vibration: Boolean = false,
|
||||||
onGoing: Boolean = true,
|
onGoing: Boolean = true,
|
||||||
lights: Boolean = true
|
lights: Boolean = true
|
||||||
): Notification
|
): Notification
|
||||||
|
|||||||
+11
-5
@@ -12,9 +12,13 @@ import com.zaneschepke.wireguardautotunnel.ui.MainActivity
|
|||||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
class WireGuardNotification @Inject constructor(@ApplicationContext private val context: Context) : NotificationService {
|
class WireGuardNotification
|
||||||
|
@Inject
|
||||||
private val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager;
|
constructor(
|
||||||
|
@ApplicationContext private val context: Context
|
||||||
|
) : NotificationService {
|
||||||
|
private val notificationManager =
|
||||||
|
context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||||
|
|
||||||
override fun createNotification(
|
override fun createNotification(
|
||||||
channelId: String,
|
channelId: String,
|
||||||
@@ -29,7 +33,8 @@ class WireGuardNotification @Inject constructor(@ApplicationContext private val
|
|||||||
onGoing: Boolean,
|
onGoing: Boolean,
|
||||||
lights: Boolean
|
lights: Boolean
|
||||||
): Notification {
|
): Notification {
|
||||||
val channel = NotificationChannel(
|
val channel =
|
||||||
|
NotificationChannel(
|
||||||
channelId,
|
channelId,
|
||||||
channelName,
|
channelName,
|
||||||
importance
|
importance
|
||||||
@@ -62,7 +67,8 @@ class WireGuardNotification @Inject constructor(@ApplicationContext private val
|
|||||||
// TODO find a not deprecated way to do this
|
// TODO find a not deprecated way to do this
|
||||||
it.addAction(
|
it.addAction(
|
||||||
Notification.Action.Builder(0, actionText, action)
|
Notification.Action.Builder(0, actionText, action)
|
||||||
.build())
|
.build()
|
||||||
|
)
|
||||||
it.setAutoCancel(true)
|
it.setAutoCancel(true)
|
||||||
}
|
}
|
||||||
it.setContentTitle(title)
|
it.setContentTitle(title)
|
||||||
|
|||||||
+72
-10
@@ -1,28 +1,90 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.service.shortcut
|
package com.zaneschepke.wireguardautotunnel.service.shortcut
|
||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.activity.ComponentActivity
|
||||||
import com.zaneschepke.wireguardautotunnel.R
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import com.zaneschepke.wireguardautotunnel.repository.SettingsDoa
|
||||||
|
import com.zaneschepke.wireguardautotunnel.repository.TunnelConfigDao
|
||||||
|
import com.zaneschepke.wireguardautotunnel.repository.model.Settings
|
||||||
|
import com.zaneschepke.wireguardautotunnel.repository.model.TunnelConfig
|
||||||
import com.zaneschepke.wireguardautotunnel.service.foreground.Action
|
import com.zaneschepke.wireguardautotunnel.service.foreground.Action
|
||||||
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
|
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
|
||||||
import com.zaneschepke.wireguardautotunnel.service.foreground.WireGuardTunnelService
|
import com.zaneschepke.wireguardautotunnel.service.foreground.WireGuardTunnelService
|
||||||
|
import com.zaneschepke.wireguardautotunnel.util.WgTunnelException
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
|
import javax.inject.Inject
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import timber.log.Timber
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class ShortcutsActivity : AppCompatActivity() {
|
class ShortcutsActivity : ComponentActivity() {
|
||||||
|
@Inject
|
||||||
|
lateinit var settingsRepo: SettingsDoa
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var tunnelConfigRepo: TunnelConfigDao
|
||||||
|
|
||||||
|
private fun attemptWatcherServiceToggle(tunnelConfig: String) {
|
||||||
|
lifecycleScope.launch(Dispatchers.Main) {
|
||||||
|
val settings = getSettings()
|
||||||
|
if (settings.isAutoTunnelEnabled) {
|
||||||
|
ServiceManager.toggleWatcherServiceForeground(this@ShortcutsActivity, tunnelConfig)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
if(intent.getStringExtra(ShortcutsManager.CLASS_NAME_EXTRA_KEY)
|
if (intent.getStringExtra(CLASS_NAME_EXTRA_KEY)
|
||||||
.equals(WireGuardTunnelService::class.java.name)) {
|
.equals(WireGuardTunnelService::class.java.simpleName)
|
||||||
intent.getStringExtra(getString(R.string.tunnel_extras_key))?.let {
|
) {
|
||||||
ServiceManager.toggleWatcherService(this, it)
|
lifecycleScope.launch(Dispatchers.Main) {
|
||||||
|
val settings = getSettings()
|
||||||
|
if (settings.isShortcutsEnabled) {
|
||||||
|
try {
|
||||||
|
val tunnelName = intent.getStringExtra(TUNNEL_NAME_EXTRA_KEY)
|
||||||
|
val tunnelConfig =
|
||||||
|
if (tunnelName != null) {
|
||||||
|
tunnelConfigRepo.getAll().firstOrNull { it.name == tunnelName }
|
||||||
|
} else {
|
||||||
|
if (settings.defaultTunnel == null) {
|
||||||
|
tunnelConfigRepo.getAll().first()
|
||||||
|
} else {
|
||||||
|
TunnelConfig.from(settings.defaultTunnel!!)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
tunnelConfig ?: return@launch
|
||||||
|
attemptWatcherServiceToggle(tunnelConfig.toString())
|
||||||
when (intent.action) {
|
when (intent.action) {
|
||||||
Action.STOP.name -> ServiceManager.stopVpnService(this)
|
Action.STOP.name -> ServiceManager.stopVpnService(
|
||||||
Action.START.name -> intent.getStringExtra(getString(R.string.tunnel_extras_key))
|
this@ShortcutsActivity
|
||||||
?.let { ServiceManager.startVpnService(this, it) }
|
)
|
||||||
|
Action.START.name -> ServiceManager.startVpnService(
|
||||||
|
this@ShortcutsActivity,
|
||||||
|
tunnelConfig.toString()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Timber.e(e.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
finish()
|
finish()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private suspend fun getSettings(): Settings {
|
||||||
|
val settings = settingsRepo.getAll()
|
||||||
|
return if (settings.isNotEmpty()) {
|
||||||
|
settings.first()
|
||||||
|
} else {
|
||||||
|
throw WgTunnelException("Settings empty")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val TUNNEL_NAME_EXTRA_KEY = "tunnelName"
|
||||||
|
const val CLASS_NAME_EXTRA_KEY = "className"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
-73
@@ -1,73 +0,0 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.service.shortcut
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
|
||||||
import androidx.core.content.pm.ShortcutInfoCompat
|
|
||||||
import androidx.core.content.pm.ShortcutManagerCompat
|
|
||||||
import androidx.core.graphics.drawable.IconCompat
|
|
||||||
import com.zaneschepke.wireguardautotunnel.R
|
|
||||||
import com.zaneschepke.wireguardautotunnel.service.foreground.Action
|
|
||||||
import com.zaneschepke.wireguardautotunnel.service.foreground.WireGuardTunnelService
|
|
||||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.model.TunnelConfig
|
|
||||||
|
|
||||||
object ShortcutsManager {
|
|
||||||
|
|
||||||
private const val SHORT_LABEL_MAX_SIZE = 10;
|
|
||||||
private const val LONG_LABEL_MAX_SIZE = 25;
|
|
||||||
private const val APPEND_ON = " On";
|
|
||||||
private const val APPEND_OFF = " Off"
|
|
||||||
const val CLASS_NAME_EXTRA_KEY = "className"
|
|
||||||
|
|
||||||
private fun createAndPushShortcut(context : Context, intent : Intent, id : String, shortLabel : String,
|
|
||||||
longLabel : String, drawable : Int ) {
|
|
||||||
val shortcut = ShortcutInfoCompat.Builder(context, id)
|
|
||||||
.setShortLabel(shortLabel)
|
|
||||||
.setLongLabel(longLabel)
|
|
||||||
.setIcon(IconCompat.createWithResource(context, drawable))
|
|
||||||
.setIntent(intent)
|
|
||||||
.build()
|
|
||||||
ShortcutManagerCompat.pushDynamicShortcut(context, shortcut)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun createTunnelShortcuts(context : Context, tunnelConfig : TunnelConfig) {
|
|
||||||
createAndPushShortcut(context,
|
|
||||||
createTunnelOnIntent(context, mapOf(context.getString(R.string.tunnel_extras_key) to tunnelConfig.toString())),
|
|
||||||
tunnelConfig.id.toString() + APPEND_ON,
|
|
||||||
tunnelConfig.name.take((SHORT_LABEL_MAX_SIZE - APPEND_ON.length)) + APPEND_ON,
|
|
||||||
tunnelConfig.name.take((LONG_LABEL_MAX_SIZE - APPEND_ON.length)) + APPEND_ON,
|
|
||||||
R.drawable.vpn_on
|
|
||||||
)
|
|
||||||
createAndPushShortcut(context,
|
|
||||||
createTunnelOffIntent(context, mapOf(context.getString(R.string.tunnel_extras_key) to tunnelConfig.toString())),
|
|
||||||
tunnelConfig.id.toString() + APPEND_OFF,
|
|
||||||
tunnelConfig.name.take((SHORT_LABEL_MAX_SIZE - APPEND_OFF.length)) + APPEND_OFF,
|
|
||||||
tunnelConfig.name.take((LONG_LABEL_MAX_SIZE - APPEND_OFF.length)) + APPEND_OFF,
|
|
||||||
R.drawable.vpn_off
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun removeTunnelShortcuts(context : Context, tunnelConfig : TunnelConfig) {
|
|
||||||
ShortcutManagerCompat.removeDynamicShortcuts(context, listOf(tunnelConfig.id.toString() + APPEND_ON,
|
|
||||||
tunnelConfig.id.toString() + APPEND_OFF ))
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun createTunnelOnIntent(context: Context, extras : Map<String,String>) : Intent {
|
|
||||||
return Intent(context, ShortcutsActivity::class.java).also {
|
|
||||||
it.action = Action.START.name
|
|
||||||
it.putExtra(CLASS_NAME_EXTRA_KEY, WireGuardTunnelService::class.java.name)
|
|
||||||
extras.forEach {(k, v) ->
|
|
||||||
it.putExtra(k, v)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun createTunnelOffIntent(context : Context, extras : Map<String,String>) : Intent {
|
|
||||||
return Intent(context, ShortcutsActivity::class.java).also {
|
|
||||||
it.action = Action.STOP.name
|
|
||||||
it.putExtra(CLASS_NAME_EXTRA_KEY, WireGuardTunnelService::class.java.name)
|
|
||||||
extras.forEach {(k, v) ->
|
|
||||||
it.putExtra(k, v)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+52
-35
@@ -5,68 +5,68 @@ import android.service.quicksettings.Tile
|
|||||||
import android.service.quicksettings.TileService
|
import android.service.quicksettings.TileService
|
||||||
import com.wireguard.android.backend.Tunnel
|
import com.wireguard.android.backend.Tunnel
|
||||||
import com.zaneschepke.wireguardautotunnel.R
|
import com.zaneschepke.wireguardautotunnel.R
|
||||||
import com.zaneschepke.wireguardautotunnel.repository.Repository
|
import com.zaneschepke.wireguardautotunnel.repository.SettingsDoa
|
||||||
|
import com.zaneschepke.wireguardautotunnel.repository.TunnelConfigDao
|
||||||
|
import com.zaneschepke.wireguardautotunnel.repository.model.TunnelConfig
|
||||||
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
|
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
|
||||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnService
|
import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnService
|
||||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.model.Settings
|
|
||||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.model.TunnelConfig
|
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
|
import javax.inject.Inject
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.cancel
|
import kotlinx.coroutines.cancel
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class TunnelControlTile : TileService() {
|
class TunnelControlTile : TileService() {
|
||||||
|
@Inject
|
||||||
|
lateinit var settingsRepo: SettingsDoa
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var settingsRepo : Repository<Settings>
|
lateinit var configRepo: TunnelConfigDao
|
||||||
|
|
||||||
@Inject
|
|
||||||
lateinit var configRepo : Repository<TunnelConfig>
|
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var vpnService: VpnService
|
lateinit var vpnService: VpnService
|
||||||
|
|
||||||
private val scope = CoroutineScope(Dispatchers.Main);
|
private val scope = CoroutineScope(Dispatchers.Main)
|
||||||
|
|
||||||
private lateinit var job: Job
|
private lateinit var job: Job
|
||||||
|
|
||||||
override fun onStartListening() {
|
override fun onStartListening() {
|
||||||
job = scope.launch {
|
job =
|
||||||
|
scope.launch {
|
||||||
updateTileState()
|
updateTileState()
|
||||||
}
|
}
|
||||||
super.onStartListening()
|
super.onStartListening()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onTileAdded() {
|
|
||||||
super.onTileAdded()
|
|
||||||
qsTile.contentDescription = this.resources.getString(R.string.toggle_vpn)
|
|
||||||
scope.launch {
|
|
||||||
updateTileState();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onTileRemoved() {
|
override fun onTileRemoved() {
|
||||||
super.onTileRemoved()
|
super.onTileRemoved()
|
||||||
cancelJob()
|
cancelJob()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onDestroy() {
|
||||||
|
super.onDestroy()
|
||||||
|
scope.cancel()
|
||||||
|
}
|
||||||
|
|
||||||
override fun onClick() {
|
override fun onClick() {
|
||||||
super.onClick()
|
super.onClick()
|
||||||
unlockAndRun {
|
unlockAndRun {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
try {
|
try {
|
||||||
val tunnel = determineTileTunnel();
|
val tunnel = determineTileTunnel()
|
||||||
if (tunnel != null) {
|
if (tunnel != null) {
|
||||||
attemptWatcherServiceToggle(tunnel.toString())
|
attemptWatcherServiceToggle(tunnel.toString())
|
||||||
if (vpnService.getState() == Tunnel.State.UP) {
|
if (vpnService.getState() == Tunnel.State.UP) {
|
||||||
ServiceManager.stopVpnService(this@TunnelControlTile)
|
ServiceManager.stopVpnService(this@TunnelControlTile)
|
||||||
} else {
|
} else {
|
||||||
ServiceManager.startVpnService(this@TunnelControlTile, tunnel.toString())
|
ServiceManager.startVpnServiceForeground(
|
||||||
|
this@TunnelControlTile,
|
||||||
|
tunnel.toString()
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
@@ -79,28 +79,37 @@ class TunnelControlTile : TileService() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun determineTileTunnel(): TunnelConfig? {
|
private suspend fun determineTileTunnel(): TunnelConfig? {
|
||||||
var tunnelConfig : TunnelConfig? = null;
|
var tunnelConfig: TunnelConfig? = null
|
||||||
val settings = settingsRepo.getAll()
|
val settings = settingsRepo.getAll()
|
||||||
if (!settings.isNullOrEmpty()) {
|
if (settings.isNotEmpty()) {
|
||||||
val setting = settings.first()
|
val setting = settings.first()
|
||||||
tunnelConfig = if (setting.defaultTunnel != null) {
|
tunnelConfig =
|
||||||
TunnelConfig.from(setting.defaultTunnel!!);
|
if (setting.defaultTunnel != null) {
|
||||||
|
TunnelConfig.from(setting.defaultTunnel!!)
|
||||||
} else {
|
} else {
|
||||||
val config = configRepo.getAll()?.first();
|
val configs = configRepo.getAll()
|
||||||
config;
|
val config =
|
||||||
|
if (configs.isNotEmpty()) {
|
||||||
|
configs.first()
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
config
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return tunnelConfig;
|
return tunnelConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private fun attemptWatcherServiceToggle(tunnelConfig: String) {
|
private fun attemptWatcherServiceToggle(tunnelConfig: String) {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
val settings = settingsRepo.getAll()
|
val settings = settingsRepo.getAll()
|
||||||
if (!settings.isNullOrEmpty()) {
|
if (settings.isNotEmpty()) {
|
||||||
val setting = settings.first()
|
val setting = settings.first()
|
||||||
if (setting.isAutoTunnelEnabled) {
|
if (setting.isAutoTunnelEnabled) {
|
||||||
ServiceManager.toggleWatcherService(this@TunnelControlTile, tunnelConfig)
|
ServiceManager.toggleWatcherServiceForeground(
|
||||||
|
this@TunnelControlTile,
|
||||||
|
tunnelConfig
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -108,20 +117,28 @@ class TunnelControlTile : TileService() {
|
|||||||
|
|
||||||
private suspend fun updateTileState() {
|
private suspend fun updateTileState() {
|
||||||
vpnService.state.collect {
|
vpnService.state.collect {
|
||||||
|
try {
|
||||||
when (it) {
|
when (it) {
|
||||||
Tunnel.State.UP -> {
|
Tunnel.State.UP -> {
|
||||||
qsTile.state = Tile.STATE_ACTIVE
|
qsTile.state = Tile.STATE_ACTIVE
|
||||||
}
|
}
|
||||||
|
|
||||||
Tunnel.State.DOWN -> {
|
Tunnel.State.DOWN -> {
|
||||||
qsTile.state = Tile.STATE_INACTIVE;
|
qsTile.state = Tile.STATE_INACTIVE
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> {
|
else -> {
|
||||||
qsTile.state = Tile.STATE_UNAVAILABLE
|
qsTile.state = Tile.STATE_UNAVAILABLE
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val config = determineTileTunnel();
|
val config = determineTileTunnel()
|
||||||
setTileDescription(config?.name ?: this.resources.getString(R.string.no_tunnel_available))
|
setTileDescription(
|
||||||
|
config?.name ?: this.resources.getString(R.string.no_tunnel_available)
|
||||||
|
)
|
||||||
qsTile.updateTile()
|
qsTile.updateTile()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Timber.e("Unable to update tile state")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -130,13 +147,13 @@ class TunnelControlTile : TileService() {
|
|||||||
qsTile.subtitle = description
|
qsTile.subtitle = description
|
||||||
}
|
}
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||||
qsTile.stateDescription = description;
|
qsTile.stateDescription = description
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun cancelJob() {
|
private fun cancelJob() {
|
||||||
if (this::job.isInitialized) {
|
if (this::job.isInitialized) {
|
||||||
job.cancel();
|
job.cancel()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+6
-3
@@ -2,13 +2,16 @@ package com.zaneschepke.wireguardautotunnel.service.tunnel
|
|||||||
|
|
||||||
enum class HandshakeStatus {
|
enum class HandshakeStatus {
|
||||||
HEALTHY,
|
HEALTHY,
|
||||||
|
STALE,
|
||||||
UNHEALTHY,
|
UNHEALTHY,
|
||||||
NEVER_CONNECTED,
|
NEVER_CONNECTED,
|
||||||
NOT_STARTED;
|
NOT_STARTED
|
||||||
|
;
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val WG_TYPICAL_HANDSHAKE_INTERVAL_WHEN_HEALTHY_SEC = 120
|
private const val WG_TYPICAL_HANDSHAKE_INTERVAL_WHEN_HEALTHY_SEC = 180
|
||||||
const val UNHEALTHY_TIME_LIMIT_SEC = WG_TYPICAL_HANDSHAKE_INTERVAL_WHEN_HEALTHY_SEC + 60
|
const val STATUS_CHANGE_TIME_BUFFER = 30
|
||||||
|
const val STALE_TIME_LIMIT_SEC = WG_TYPICAL_HANDSHAKE_INTERVAL_WHEN_HEALTHY_SEC + STATUS_CHANGE_TIME_BUFFER
|
||||||
const val NEVER_CONNECTED_TO_UNHEALTHY_TIME_LIMIT_SEC = 30
|
const val NEVER_CONNECTED_TO_UNHEALTHY_TIME_LIMIT_SEC = 30
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3,16 +3,19 @@ package com.zaneschepke.wireguardautotunnel.service.tunnel
|
|||||||
import com.wireguard.android.backend.Statistics
|
import com.wireguard.android.backend.Statistics
|
||||||
import com.wireguard.android.backend.Tunnel
|
import com.wireguard.android.backend.Tunnel
|
||||||
import com.wireguard.crypto.Key
|
import com.wireguard.crypto.Key
|
||||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.model.TunnelConfig
|
import com.zaneschepke.wireguardautotunnel.repository.model.TunnelConfig
|
||||||
import kotlinx.coroutines.flow.SharedFlow
|
import kotlinx.coroutines.flow.SharedFlow
|
||||||
|
|
||||||
interface VpnService : Tunnel {
|
interface VpnService : Tunnel {
|
||||||
suspend fun startTunnel(tunnelConfig: TunnelConfig): Tunnel.State
|
suspend fun startTunnel(tunnelConfig: TunnelConfig): Tunnel.State
|
||||||
|
|
||||||
suspend fun stopTunnel()
|
suspend fun stopTunnel()
|
||||||
|
|
||||||
val state: SharedFlow<Tunnel.State>
|
val state: SharedFlow<Tunnel.State>
|
||||||
val tunnelName: SharedFlow<String>
|
val tunnelName: SharedFlow<String>
|
||||||
val statistics: SharedFlow<Statistics>
|
val statistics: SharedFlow<Statistics>
|
||||||
val lastHandshake: SharedFlow<Map<Key, Long>>
|
val lastHandshake: SharedFlow<Map<Key, Long>>
|
||||||
val handshakeStatus: SharedFlow<HandshakeStatus>
|
val handshakeStatus: SharedFlow<HandshakeStatus>
|
||||||
|
|
||||||
fun getState(): Tunnel.State
|
fun getState(): Tunnel.State
|
||||||
}
|
}
|
||||||
+81
-25
@@ -4,10 +4,15 @@ import com.wireguard.android.backend.Backend
|
|||||||
import com.wireguard.android.backend.BackendException
|
import com.wireguard.android.backend.BackendException
|
||||||
import com.wireguard.android.backend.Statistics
|
import com.wireguard.android.backend.Statistics
|
||||||
import com.wireguard.android.backend.Tunnel
|
import com.wireguard.android.backend.Tunnel
|
||||||
|
import com.wireguard.config.Config
|
||||||
import com.wireguard.crypto.Key
|
import com.wireguard.crypto.Key
|
||||||
import com.zaneschepke.wireguardautotunnel.Constants
|
import com.zaneschepke.wireguardautotunnel.Constants
|
||||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.model.TunnelConfig
|
import com.zaneschepke.wireguardautotunnel.module.Kernel
|
||||||
|
import com.zaneschepke.wireguardautotunnel.module.Userspace
|
||||||
|
import com.zaneschepke.wireguardautotunnel.repository.SettingsDoa
|
||||||
|
import com.zaneschepke.wireguardautotunnel.repository.model.TunnelConfig
|
||||||
import com.zaneschepke.wireguardautotunnel.util.NumberUtils
|
import com.zaneschepke.wireguardautotunnel.util.NumberUtils
|
||||||
|
import javax.inject.Inject
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
@@ -20,21 +25,28 @@ import kotlinx.coroutines.flow.asSharedFlow
|
|||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
|
class WireGuardTunnel
|
||||||
class WireGuardTunnel @Inject constructor(private val backend : Backend,
|
@Inject
|
||||||
|
constructor(
|
||||||
|
@Userspace private val userspaceBackend: Backend,
|
||||||
|
@Kernel private val kernelBackend: Backend,
|
||||||
|
private val settingsRepo: SettingsDoa
|
||||||
) : VpnService {
|
) : VpnService {
|
||||||
|
|
||||||
private val _tunnelName = MutableStateFlow("")
|
private val _tunnelName = MutableStateFlow("")
|
||||||
override val tunnelName get() = _tunnelName.asStateFlow()
|
override val tunnelName get() = _tunnelName.asStateFlow()
|
||||||
|
|
||||||
private val _state = MutableSharedFlow<Tunnel.State>(
|
private val _state =
|
||||||
|
MutableSharedFlow<Tunnel.State>(
|
||||||
onBufferOverflow = BufferOverflow.DROP_OLDEST,
|
onBufferOverflow = BufferOverflow.DROP_OLDEST,
|
||||||
replay = 1)
|
replay = 1
|
||||||
|
)
|
||||||
|
|
||||||
private val _handshakeStatus = MutableSharedFlow<HandshakeStatus>(replay = 1,
|
private val _handshakeStatus =
|
||||||
onBufferOverflow = BufferOverflow.DROP_OLDEST)
|
MutableSharedFlow<HandshakeStatus>(
|
||||||
|
replay = 1,
|
||||||
|
onBufferOverflow = BufferOverflow.DROP_OLDEST
|
||||||
|
)
|
||||||
override val state get() = _state.asSharedFlow()
|
override val state get() = _state.asSharedFlow()
|
||||||
|
|
||||||
private val _statistics = MutableSharedFlow<Statistics>(replay = 1)
|
private val _statistics = MutableSharedFlow<Statistics>(replay = 1)
|
||||||
@@ -46,26 +58,62 @@ class WireGuardTunnel @Inject constructor(private val backend : Backend,
|
|||||||
override val handshakeStatus: SharedFlow<HandshakeStatus>
|
override val handshakeStatus: SharedFlow<HandshakeStatus>
|
||||||
get() = _handshakeStatus.asSharedFlow()
|
get() = _handshakeStatus.asSharedFlow()
|
||||||
|
|
||||||
|
private val scope = CoroutineScope(Dispatchers.IO)
|
||||||
|
|
||||||
private lateinit var statsJob: Job
|
private lateinit var statsJob: Job
|
||||||
|
|
||||||
|
private var config: Config? = null
|
||||||
|
|
||||||
|
private var backend: Backend = userspaceBackend
|
||||||
|
|
||||||
|
private var backendIsUserspace = true
|
||||||
|
|
||||||
|
init {
|
||||||
|
scope.launch {
|
||||||
|
settingsRepo.getAllFlow().collect {
|
||||||
|
val settings = it.first()
|
||||||
|
if (settings.isKernelEnabled && backendIsUserspace) {
|
||||||
|
Timber.d("Setting kernel backend")
|
||||||
|
backend = kernelBackend
|
||||||
|
backendIsUserspace = false
|
||||||
|
} else if (!settings.isKernelEnabled && !backendIsUserspace) {
|
||||||
|
Timber.d("Setting userspace backend")
|
||||||
|
backend = userspaceBackend
|
||||||
|
backendIsUserspace = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override suspend fun startTunnel(tunnelConfig: TunnelConfig): Tunnel.State {
|
override suspend fun startTunnel(tunnelConfig: TunnelConfig): Tunnel.State {
|
||||||
return try {
|
return try {
|
||||||
if(getState() == Tunnel.State.UP && _tunnelName.value != tunnelConfig.name) {
|
stopTunnelOnConfigChange(tunnelConfig)
|
||||||
stopTunnel()
|
emitTunnelName(tunnelConfig.name)
|
||||||
}
|
config = TunnelConfig.configFromQuick(tunnelConfig.wgQuick)
|
||||||
_tunnelName.emit(tunnelConfig.name)
|
val state =
|
||||||
val config = TunnelConfig.configFromQuick(tunnelConfig.wgQuick)
|
backend.setState(
|
||||||
val state = backend.setState(
|
this,
|
||||||
this, Tunnel.State.UP, config)
|
Tunnel.State.UP,
|
||||||
|
config
|
||||||
|
)
|
||||||
_state.emit(state)
|
_state.emit(state)
|
||||||
state;
|
state
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Timber.e("Failed to start tunnel with error: ${e.message}")
|
Timber.e("Failed to start tunnel with error: ${e.message}")
|
||||||
Tunnel.State.DOWN
|
Tunnel.State.DOWN
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private suspend fun emitTunnelName(name: String) {
|
||||||
|
_tunnelName.emit(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun stopTunnelOnConfigChange(tunnelConfig: TunnelConfig) {
|
||||||
|
if (getState() == Tunnel.State.UP && _tunnelName.value != tunnelConfig.name) {
|
||||||
|
stopTunnel()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun getName(): String {
|
override fun getName(): String {
|
||||||
return _tunnelName.value
|
return _tunnelName.value
|
||||||
}
|
}
|
||||||
@@ -86,18 +134,20 @@ class WireGuardTunnel @Inject constructor(private val backend : Backend,
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onStateChange(state: Tunnel.State) {
|
override fun onStateChange(state: Tunnel.State) {
|
||||||
val tunnel = this;
|
val tunnel = this
|
||||||
_state.tryEmit(state)
|
_state.tryEmit(state)
|
||||||
if (state == Tunnel.State.UP) {
|
if (state == Tunnel.State.UP) {
|
||||||
statsJob = CoroutineScope(Dispatchers.IO).launch {
|
statsJob =
|
||||||
|
scope.launch {
|
||||||
val handshakeMap = HashMap<Key, Long>()
|
val handshakeMap = HashMap<Key, Long>()
|
||||||
var neverHadHandshakeCounter = 0
|
var neverHadHandshakeCounter = 0
|
||||||
while (true) {
|
while (true) {
|
||||||
val statistics = backend.getStatistics(tunnel)
|
val statistics = backend.getStatistics(tunnel)
|
||||||
_statistics.emit(statistics)
|
_statistics.emit(statistics)
|
||||||
statistics.peers().forEach {
|
statistics.peers().forEach { key ->
|
||||||
val handshakeEpoch = statistics.peer(it)?.latestHandshakeEpochMillis ?: 0L
|
val handshakeEpoch =
|
||||||
handshakeMap[it] = handshakeEpoch
|
statistics.peer(key)?.latestHandshakeEpochMillis ?: 0L
|
||||||
|
handshakeMap[key] = handshakeEpoch
|
||||||
if (handshakeEpoch == 0L) {
|
if (handshakeEpoch == 0L) {
|
||||||
if (neverHadHandshakeCounter >= HandshakeStatus.NEVER_CONNECTED_TO_UNHEALTHY_TIME_LIMIT_SEC) {
|
if (neverHadHandshakeCounter >= HandshakeStatus.NEVER_CONNECTED_TO_UNHEALTHY_TIME_LIMIT_SEC) {
|
||||||
_handshakeStatus.emit(HandshakeStatus.NEVER_CONNECTED)
|
_handshakeStatus.emit(HandshakeStatus.NEVER_CONNECTED)
|
||||||
@@ -105,16 +155,22 @@ class WireGuardTunnel @Inject constructor(private val backend : Backend,
|
|||||||
_handshakeStatus.emit(HandshakeStatus.NOT_STARTED)
|
_handshakeStatus.emit(HandshakeStatus.NOT_STARTED)
|
||||||
}
|
}
|
||||||
if (neverHadHandshakeCounter <= HandshakeStatus.NEVER_CONNECTED_TO_UNHEALTHY_TIME_LIMIT_SEC) {
|
if (neverHadHandshakeCounter <= HandshakeStatus.NEVER_CONNECTED_TO_UNHEALTHY_TIME_LIMIT_SEC) {
|
||||||
neverHadHandshakeCounter += 10
|
neverHadHandshakeCounter += (1 * Constants.VPN_STATISTIC_CHECK_INTERVAL / 1000).toInt()
|
||||||
}
|
}
|
||||||
return@forEach
|
return@forEach
|
||||||
}
|
}
|
||||||
if(NumberUtils.getSecondsBetweenTimestampAndNow(handshakeEpoch) >= HandshakeStatus.UNHEALTHY_TIME_LIMIT_SEC) {
|
// TODO one day make each peer have their own dedicated status
|
||||||
_handshakeStatus.emit(HandshakeStatus.UNHEALTHY)
|
val lastHandshake = NumberUtils.getSecondsBetweenTimestampAndNow(
|
||||||
|
handshakeEpoch
|
||||||
|
)
|
||||||
|
if (lastHandshake != null) {
|
||||||
|
if (lastHandshake >= HandshakeStatus.STALE_TIME_LIMIT_SEC) {
|
||||||
|
_handshakeStatus.emit(HandshakeStatus.STALE)
|
||||||
} else {
|
} else {
|
||||||
_handshakeStatus.emit(HandshakeStatus.HEALTHY)
|
_handshakeStatus.emit(HandshakeStatus.HEALTHY)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
_lastHandshake.emit(handshakeMap)
|
_lastHandshake.emit(handshakeMap)
|
||||||
delay(Constants.VPN_STATISTIC_CHECK_INTERVAL)
|
delay(Constants.VPN_STATISTIC_CHECK_INTERVAL)
|
||||||
}
|
}
|
||||||
|
|||||||
-15
@@ -1,15 +0,0 @@
|
|||||||
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,
|
|
||||||
)
|
|
||||||
-89
@@ -1,89 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
package com.zaneschepke.wireguardautotunnel.ui
|
||||||
|
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
class ActivityViewModel @Inject constructor() : ViewModel() {
|
||||||
|
// TODO move shared logic to shared viewmodel
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
package com.zaneschepke.wireguardautotunnel.ui
|
||||||
|
|
||||||
|
import com.journeyapps.barcodescanner.CaptureActivity
|
||||||
|
|
||||||
|
class CaptureActivityPortrait : CaptureActivity()
|
||||||
@@ -11,13 +11,18 @@ import androidx.activity.compose.rememberLauncherForActivityResult
|
|||||||
import androidx.activity.compose.setContent
|
import androidx.activity.compose.setContent
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.compose.animation.ExperimentalAnimationApi
|
import androidx.compose.animation.ExitTransition
|
||||||
import androidx.compose.animation.core.tween
|
import androidx.compose.animation.core.tween
|
||||||
import androidx.compose.animation.fadeIn
|
import androidx.compose.animation.fadeIn
|
||||||
import androidx.compose.animation.slideInHorizontally
|
import androidx.compose.animation.slideInHorizontally
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Scaffold
|
import androidx.compose.material3.Scaffold
|
||||||
|
import androidx.compose.material3.SnackbarData
|
||||||
|
import androidx.compose.material3.SnackbarDuration
|
||||||
import androidx.compose.material3.SnackbarHost
|
import androidx.compose.material3.SnackbarHost
|
||||||
import androidx.compose.material3.SnackbarHostState
|
import androidx.compose.material3.SnackbarHostState
|
||||||
|
import androidx.compose.material3.SnackbarResult
|
||||||
|
import androidx.compose.material3.surfaceColorAtElevation
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
@@ -26,37 +31,42 @@ import androidx.compose.runtime.setValue
|
|||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.focus.FocusRequester
|
import androidx.compose.ui.focus.FocusRequester
|
||||||
import androidx.compose.ui.input.key.onKeyEvent
|
import androidx.compose.ui.input.key.onKeyEvent
|
||||||
import com.google.accompanist.navigation.animation.AnimatedNavHost
|
import androidx.compose.ui.unit.dp
|
||||||
import com.google.accompanist.navigation.animation.composable
|
import androidx.lifecycle.lifecycleScope
|
||||||
import com.google.accompanist.navigation.animation.rememberAnimatedNavController
|
import androidx.navigation.compose.NavHost
|
||||||
|
import androidx.navigation.compose.composable
|
||||||
|
import androidx.navigation.compose.rememberNavController
|
||||||
import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
||||||
import com.google.accompanist.permissions.isGranted
|
import com.google.accompanist.permissions.isGranted
|
||||||
import com.google.accompanist.permissions.rememberPermissionState
|
import com.google.accompanist.permissions.rememberPermissionState
|
||||||
import com.wireguard.android.backend.GoBackend
|
import com.wireguard.android.backend.GoBackend
|
||||||
|
import com.zaneschepke.wireguardautotunnel.Constants
|
||||||
import com.zaneschepke.wireguardautotunnel.R
|
import com.zaneschepke.wireguardautotunnel.R
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.common.PermissionRequestFailedScreen
|
import com.zaneschepke.wireguardautotunnel.ui.common.PermissionRequestFailedScreen
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.BottomNavBar
|
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.BottomNavBar
|
||||||
|
import com.zaneschepke.wireguardautotunnel.ui.common.prompt.CustomSnackBar
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.screens.config.ConfigScreen
|
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.main.MainScreen
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.SettingsScreen
|
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.SettingsScreen
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.screens.support.SupportScreen
|
import com.zaneschepke.wireguardautotunnel.ui.screens.support.SupportScreen
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.theme.TransparentSystemBars
|
import com.zaneschepke.wireguardautotunnel.ui.theme.TransparentSystemBars
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.theme.WireguardAutoTunnelTheme
|
import com.zaneschepke.wireguardautotunnel.ui.theme.WireguardAutoTunnelTheme
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import java.lang.IllegalStateException
|
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class MainActivity : AppCompatActivity() {
|
class MainActivity : AppCompatActivity() {
|
||||||
|
@OptIn(
|
||||||
@OptIn(ExperimentalAnimationApi::class,
|
|
||||||
ExperimentalPermissionsApi::class
|
ExperimentalPermissionsApi::class
|
||||||
)
|
)
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
setContent {
|
setContent {
|
||||||
val navController = rememberAnimatedNavController()
|
// TODO move shared logic to shared viewmodel
|
||||||
|
// val sharedViewModel = hiltViewModel<ActivityViewModel>()
|
||||||
|
val navController = rememberNavController()
|
||||||
val focusRequester = remember { FocusRequester() }
|
val focusRequester = remember { FocusRequester() }
|
||||||
|
|
||||||
WireguardAutoTunnelTheme {
|
WireguardAutoTunnelTheme {
|
||||||
@@ -74,46 +84,86 @@ class MainActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var vpnIntent by remember { mutableStateOf(GoBackend.VpnService.prepare(this)) }
|
var vpnIntent by remember { mutableStateOf(GoBackend.VpnService.prepare(this)) }
|
||||||
val vpnActivityResultState = rememberLauncherForActivityResult(
|
val vpnActivityResultState =
|
||||||
|
rememberLauncherForActivityResult(
|
||||||
ActivityResultContracts.StartActivityForResult(),
|
ActivityResultContracts.StartActivityForResult(),
|
||||||
onResult = {
|
onResult = {
|
||||||
val accepted = (it.resultCode == RESULT_OK)
|
val accepted = (it.resultCode == RESULT_OK)
|
||||||
if (accepted) {
|
if (accepted) {
|
||||||
vpnIntent = null
|
vpnIntent = null
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
|
)
|
||||||
LaunchedEffect(vpnIntent) {
|
LaunchedEffect(vpnIntent) {
|
||||||
if (vpnIntent != null) {
|
if (vpnIntent != null) {
|
||||||
vpnActivityResultState.launch(vpnIntent)
|
vpnActivityResultState.launch(vpnIntent)
|
||||||
} else requestNotificationPermission()
|
} else {
|
||||||
|
requestNotificationPermission()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Scaffold(snackbarHost = { SnackbarHost(snackbarHostState)},
|
fun showSnackBarMessage(message: String) {
|
||||||
modifier = Modifier.onKeyEvent {
|
lifecycleScope.launch(Dispatchers.Main) {
|
||||||
|
val result =
|
||||||
|
snackbarHostState.showSnackbar(
|
||||||
|
message = message,
|
||||||
|
actionLabel = applicationContext.getString(R.string.okay),
|
||||||
|
duration = SnackbarDuration.Short
|
||||||
|
)
|
||||||
|
when (result) {
|
||||||
|
SnackbarResult.ActionPerformed -> {
|
||||||
|
snackbarHostState.currentSnackbarData?.dismiss()
|
||||||
|
}
|
||||||
|
|
||||||
|
SnackbarResult.Dismissed -> {
|
||||||
|
snackbarHostState.currentSnackbarData?.dismiss()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Scaffold(
|
||||||
|
snackbarHost = {
|
||||||
|
SnackbarHost(snackbarHostState) { snackbarData: SnackbarData ->
|
||||||
|
CustomSnackBar(
|
||||||
|
snackbarData.visuals.message,
|
||||||
|
isRtl = false,
|
||||||
|
containerColor = MaterialTheme.colorScheme.surfaceColorAtElevation(
|
||||||
|
2.dp
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
modifier =
|
||||||
|
Modifier.onKeyEvent {
|
||||||
if (it.nativeKeyEvent.action == KeyEvent.ACTION_UP) {
|
if (it.nativeKeyEvent.action == KeyEvent.ACTION_UP) {
|
||||||
when (it.nativeKeyEvent.keyCode) {
|
when (it.nativeKeyEvent.keyCode) {
|
||||||
KeyEvent.KEYCODE_DPAD_UP -> {
|
KeyEvent.KEYCODE_DPAD_UP -> {
|
||||||
try {
|
try {
|
||||||
focusRequester.requestFocus()
|
focusRequester.requestFocus()
|
||||||
} catch (e: IllegalStateException) {
|
} catch (e: IllegalStateException) {
|
||||||
Timber.e("No D-Pad focus request modifier added to element on screen")
|
Timber.e(
|
||||||
|
"No D-Pad focus request modifier added to element on screen"
|
||||||
|
)
|
||||||
}
|
}
|
||||||
false
|
false
|
||||||
} else -> {
|
}
|
||||||
false;
|
|
||||||
|
else -> {
|
||||||
|
false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
bottomBar = if (vpnIntent == null && notificationPermissionState.status.isGranted) {
|
bottomBar =
|
||||||
|
if (vpnIntent == null && notificationPermissionState.status.isGranted) {
|
||||||
{ BottomNavBar(navController, Routes.navItems) }
|
{ BottomNavBar(navController, Routes.navItems) }
|
||||||
} else {
|
} else {
|
||||||
{}
|
{}
|
||||||
},
|
}
|
||||||
)
|
) { padding ->
|
||||||
{ padding ->
|
|
||||||
if (vpnIntent != null) {
|
if (vpnIntent != null) {
|
||||||
PermissionRequestFailedScreen(
|
PermissionRequestFailedScreen(
|
||||||
padding = padding,
|
padding = padding,
|
||||||
@@ -130,69 +180,107 @@ class MainActivity : AppCompatActivity() {
|
|||||||
val intentSettings =
|
val intentSettings =
|
||||||
Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
|
Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
|
||||||
intentSettings.data =
|
intentSettings.data =
|
||||||
Uri.fromParts("package", this.packageName, null)
|
Uri.fromParts(
|
||||||
startActivity(intentSettings);
|
Constants.URI_PACKAGE_SCHEME,
|
||||||
|
this.packageName,
|
||||||
|
null
|
||||||
|
)
|
||||||
|
startActivity(intentSettings)
|
||||||
},
|
},
|
||||||
message = getString(R.string.notification_permission_required),
|
message = getString(R.string.notification_permission_required),
|
||||||
getString(R.string.open_settings)
|
getString(R.string.open_settings)
|
||||||
)
|
)
|
||||||
return@Scaffold
|
return@Scaffold
|
||||||
}
|
}
|
||||||
AnimatedNavHost(navController, startDestination = Routes.Main.name) {
|
|
||||||
composable(Routes.Main.name, enterTransition = {
|
NavHost(navController, startDestination = Routes.Main.name) {
|
||||||
|
composable(
|
||||||
|
Routes.Main.name,
|
||||||
|
enterTransition = {
|
||||||
when (initialState.destination.route) {
|
when (initialState.destination.route) {
|
||||||
Routes.Settings.name, Routes.Support.name ->
|
Routes.Settings.name, Routes.Support.name ->
|
||||||
slideInHorizontally(
|
slideInHorizontally(
|
||||||
initialOffsetX = { -1000 },
|
initialOffsetX = {
|
||||||
animationSpec = tween(500)
|
-Constants.SLIDE_IN_TRANSITION_OFFSET
|
||||||
|
},
|
||||||
|
animationSpec = tween(
|
||||||
|
Constants.SLIDE_IN_ANIMATION_DURATION
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
else -> {
|
else -> {
|
||||||
fadeIn(animationSpec = tween(1000))
|
fadeIn(
|
||||||
|
animationSpec = tween(
|
||||||
|
Constants.FADE_IN_ANIMATION_DURATION
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}) {
|
},
|
||||||
MainScreen(padding = padding, snackbarHostState = snackbarHostState, navController = navController)
|
exitTransition = {
|
||||||
|
ExitTransition.None
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
MainScreen(padding = padding, showSnackbarMessage = { message ->
|
||||||
|
showSnackBarMessage(message)
|
||||||
|
}, navController = navController)
|
||||||
}
|
}
|
||||||
composable(Routes.Settings.name, enterTransition = {
|
composable(Routes.Settings.name, enterTransition = {
|
||||||
when (initialState.destination.route) {
|
when (initialState.destination.route) {
|
||||||
Routes.Main.name ->
|
Routes.Main.name ->
|
||||||
slideInHorizontally(
|
slideInHorizontally(
|
||||||
initialOffsetX = { 1000 },
|
initialOffsetX = { Constants.SLIDE_IN_TRANSITION_OFFSET },
|
||||||
animationSpec = tween(500)
|
animationSpec = tween(Constants.SLIDE_IN_ANIMATION_DURATION)
|
||||||
)
|
)
|
||||||
|
|
||||||
Routes.Support.name -> {
|
Routes.Support.name -> {
|
||||||
slideInHorizontally(
|
slideInHorizontally(
|
||||||
initialOffsetX = { -1000 },
|
initialOffsetX = { -Constants.SLIDE_IN_TRANSITION_OFFSET },
|
||||||
animationSpec = tween(500)
|
animationSpec = tween(Constants.SLIDE_IN_ANIMATION_DURATION)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> {
|
else -> {
|
||||||
fadeIn(animationSpec = tween(1000))
|
fadeIn(
|
||||||
|
animationSpec = tween(Constants.FADE_IN_ANIMATION_DURATION)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}) { SettingsScreen(padding = padding, snackbarHostState = snackbarHostState, navController = navController, focusRequester = focusRequester) }
|
}) {
|
||||||
|
SettingsScreen(padding = padding, showSnackbarMessage = { message ->
|
||||||
|
showSnackBarMessage(message)
|
||||||
|
}, focusRequester = focusRequester)
|
||||||
|
}
|
||||||
composable(Routes.Support.name, enterTransition = {
|
composable(Routes.Support.name, enterTransition = {
|
||||||
when (initialState.destination.route) {
|
when (initialState.destination.route) {
|
||||||
Routes.Settings.name, Routes.Main.name ->
|
Routes.Settings.name, Routes.Main.name ->
|
||||||
slideInHorizontally(
|
slideInHorizontally(
|
||||||
initialOffsetX = { 1000 },
|
initialOffsetX = { Constants.SLIDE_IN_ANIMATION_DURATION },
|
||||||
animationSpec = tween(500)
|
animationSpec = tween(Constants.SLIDE_IN_ANIMATION_DURATION)
|
||||||
)
|
)
|
||||||
|
|
||||||
else -> {
|
else -> {
|
||||||
fadeIn(animationSpec = tween(1000))
|
fadeIn(
|
||||||
|
animationSpec = tween(Constants.FADE_IN_ANIMATION_DURATION)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}) { SupportScreen(padding = padding, focusRequester) }
|
}) { SupportScreen(padding = padding, focusRequester = focusRequester) }
|
||||||
composable("${Routes.Config.name}/{id}", enterTransition = {
|
composable("${Routes.Config.name}/{id}", enterTransition = {
|
||||||
fadeIn(animationSpec = tween(1000))
|
fadeIn(animationSpec = tween(Constants.FADE_IN_ANIMATION_DURATION))
|
||||||
}) { ConfigScreen(padding = padding, navController = navController, id = it.arguments?.getString("id"), focusRequester = focusRequester)}
|
}) {
|
||||||
composable("${Routes.Detail.name}/{id}", enterTransition = {
|
val id = it.arguments?.getString("id")
|
||||||
fadeIn(animationSpec = tween(1000))
|
if (!id.isNullOrBlank()) {
|
||||||
}) { DetailScreen(padding = padding, id = it.arguments?.getString("id")) }
|
ConfigScreen(
|
||||||
|
navController = navController,
|
||||||
|
id = id,
|
||||||
|
showSnackbarMessage = { message ->
|
||||||
|
showSnackBarMessage(message)
|
||||||
|
},
|
||||||
|
focusRequester = focusRequester
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,26 +10,26 @@ enum class Routes {
|
|||||||
Main,
|
Main,
|
||||||
Settings,
|
Settings,
|
||||||
Support,
|
Support,
|
||||||
Config,
|
Config
|
||||||
Detail;
|
;
|
||||||
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
val navItems = listOf(
|
val navItems =
|
||||||
|
listOf(
|
||||||
BottomNavItem(
|
BottomNavItem(
|
||||||
name = "Tunnels",
|
name = "Tunnels",
|
||||||
route = Main.name,
|
route = Main.name,
|
||||||
icon = Icons.Rounded.Home,
|
icon = Icons.Rounded.Home
|
||||||
),
|
),
|
||||||
BottomNavItem(
|
BottomNavItem(
|
||||||
name = "Settings",
|
name = "Settings",
|
||||||
route = Settings.name,
|
route = Settings.name,
|
||||||
icon = Icons.Rounded.Settings,
|
icon = Icons.Rounded.Settings
|
||||||
),
|
),
|
||||||
BottomNavItem(
|
BottomNavItem(
|
||||||
name = "Support",
|
name = "Support",
|
||||||
route = Support.name,
|
route = Support.name,
|
||||||
icon = Icons.Rounded.QuestionMark,
|
icon = Icons.Rounded.QuestionMark
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +0,0 @@
|
|||||||
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
|
|
||||||
)
|
|
||||||
+18
-6
@@ -1,27 +1,39 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.ui.common
|
package com.zaneschepke.wireguardautotunnel.ui.common
|
||||||
|
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.IntrinsicSize
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.material3.Button
|
import androidx.compose.foundation.layout.width
|
||||||
import androidx.compose.material3.ButtonDefaults
|
import androidx.compose.material3.ButtonDefaults
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TextButton
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.vector.ImageVector
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
import com.zaneschepke.wireguardautotunnel.R
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ClickableIconButton(onIconClick : () -> Unit, text : String, icon : ImageVector, enabled : Boolean) {
|
fun ClickableIconButton(
|
||||||
Button(onClick = {},
|
onIconClick: () -> Unit,
|
||||||
|
text: String,
|
||||||
|
icon: ImageVector,
|
||||||
|
enabled: Boolean
|
||||||
|
) {
|
||||||
|
TextButton(
|
||||||
|
onClick = {},
|
||||||
enabled = enabled
|
enabled = enabled
|
||||||
) {
|
) {
|
||||||
Text(text)
|
Text(text, Modifier.weight(1f, false))
|
||||||
Spacer(modifier = Modifier.size(ButtonDefaults.IconSpacing))
|
Spacer(modifier = Modifier.size(ButtonDefaults.IconSpacing))
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = icon,
|
imageVector = icon,
|
||||||
contentDescription = "Delete",
|
contentDescription = stringResource(R.string.delete),
|
||||||
modifier = Modifier.size(ButtonDefaults.IconSize).clickable {
|
modifier =
|
||||||
|
Modifier.size(ButtonDefaults.IconSize).weight(1f, false).clickable {
|
||||||
if (enabled) {
|
if (enabled) {
|
||||||
onIconClick()
|
onIconClick()
|
||||||
}
|
}
|
||||||
|
|||||||
+12
-4
@@ -16,13 +16,21 @@ import androidx.compose.ui.unit.dp
|
|||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun PermissionRequestFailedScreen(padding : PaddingValues, onRequestAgain : () -> Unit, message : String, buttonText : String ) {
|
fun PermissionRequestFailedScreen(
|
||||||
|
padding: PaddingValues,
|
||||||
|
onRequestAgain: () -> Unit,
|
||||||
|
message: String,
|
||||||
|
buttonText: String
|
||||||
|
) {
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
Column(horizontalAlignment = Alignment.CenterHorizontally,
|
Column(
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
verticalArrangement = Arrangement.Center,
|
verticalArrangement = Arrangement.Center,
|
||||||
modifier = Modifier
|
modifier =
|
||||||
|
Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.padding(padding)) {
|
.padding(padding)
|
||||||
|
) {
|
||||||
Text(message, textAlign = TextAlign.Center, modifier = Modifier.padding(15.dp))
|
Text(message, textAlign = TextAlign.Center, modifier = Modifier.padding(15.dp))
|
||||||
Button(onClick = {
|
Button(onClick = {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
|
|||||||
@@ -1,27 +1,42 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.ui.common
|
package com.zaneschepke.wireguardautotunnel.ui.common
|
||||||
|
|
||||||
|
import androidx.compose.animation.animateContentSize
|
||||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||||
import androidx.compose.foundation.combinedClickable
|
import androidx.compose.foundation.combinedClickable
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material3.Icon
|
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.graphics.vector.ImageVector
|
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import com.wireguard.android.backend.Statistics
|
||||||
|
import com.zaneschepke.wireguardautotunnel.toThreeDecimalPlaceString
|
||||||
|
import com.zaneschepke.wireguardautotunnel.util.NumberUtils
|
||||||
|
|
||||||
@OptIn(ExperimentalFoundationApi::class)
|
@OptIn(ExperimentalFoundationApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun RowListItem(leadingIcon : ImageVector? = null, leadingIconColor : Color = Color.Gray, text : String, onHold : () -> Unit, onClick: () -> Unit, rowButton : @Composable() () -> Unit ) {
|
fun RowListItem(
|
||||||
|
icon: @Composable () -> Unit,
|
||||||
|
text: String,
|
||||||
|
onHold: () -> Unit,
|
||||||
|
onClick: () -> Unit,
|
||||||
|
rowButton: @Composable () -> Unit,
|
||||||
|
expanded: Boolean,
|
||||||
|
statistics: Statistics?
|
||||||
|
) {
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.animateContentSize()
|
||||||
|
.clip(RoundedCornerShape(30.dp))
|
||||||
.combinedClickable(
|
.combinedClickable(
|
||||||
onClick = {
|
onClick = {
|
||||||
onClick()
|
onClick()
|
||||||
@@ -31,25 +46,52 @@ fun RowListItem(leadingIcon : ImageVector? = null, leadingIconColor : Color = Co
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
|
Column {
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier =
|
||||||
|
Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(14.dp),
|
.padding(horizontal = 14.dp, vertical = 5.dp),
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
horizontalArrangement = Arrangement.SpaceBetween
|
horizontalArrangement = Arrangement.SpaceBetween
|
||||||
) {
|
) {
|
||||||
Row(verticalAlignment = Alignment.CenterVertically,) {
|
Row(
|
||||||
if(leadingIcon != null) {
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
Icon(
|
modifier = Modifier.fillMaxWidth(.60f)
|
||||||
leadingIcon, "status",
|
) {
|
||||||
tint = leadingIconColor,
|
icon()
|
||||||
modifier = Modifier.padding(end = 10.dp).size(15.dp)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
Text(text)
|
Text(text)
|
||||||
}
|
}
|
||||||
|
|
||||||
rowButton()
|
rowButton()
|
||||||
}
|
}
|
||||||
|
if (expanded) {
|
||||||
|
statistics?.peers()?.forEach {
|
||||||
|
Row(
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(end = 10.dp, bottom = 10.dp, start = 10.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.SpaceEvenly
|
||||||
|
) {
|
||||||
|
val handshakeEpoch = statistics.peer(it)!!.latestHandshakeEpochMillis
|
||||||
|
val peerTx = statistics.peer(it)!!.txBytes
|
||||||
|
val peerRx = statistics.peer(it)!!.rxBytes
|
||||||
|
val peerId = it.toBase64().subSequence(0, 3).toString() + "***"
|
||||||
|
val handshakeSec =
|
||||||
|
NumberUtils.getSecondsBetweenTimestampAndNow(handshakeEpoch)
|
||||||
|
val handshake =
|
||||||
|
if (handshakeSec == null) "never" else "$handshakeSec secs ago"
|
||||||
|
val peerTxMB = NumberUtils.bytesToMB(peerTx).toThreeDecimalPlaceString()
|
||||||
|
val peerRxMB = NumberUtils.bytesToMB(peerRx).toThreeDecimalPlaceString()
|
||||||
|
val fontSize = 9.sp
|
||||||
|
Text("peer: $peerId", fontSize = fontSize)
|
||||||
|
Text("handshake: $handshake", fontSize = fontSize)
|
||||||
|
Text("tx: $peerTxMB MB", fontSize = fontSize)
|
||||||
|
Text("rx: $peerRxMB MB", fontSize = fontSize)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -25,9 +25,7 @@ import androidx.compose.ui.text.input.KeyboardType
|
|||||||
import com.zaneschepke.wireguardautotunnel.R
|
import com.zaneschepke.wireguardautotunnel.R
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun SearchBar(
|
fun SearchBar(onQuery: (queryString: String) -> Unit) {
|
||||||
onQuery : (queryString : String) -> Unit
|
|
||||||
) {
|
|
||||||
// Immediately update and keep track of query from text field changes.
|
// Immediately update and keep track of query from text field changes.
|
||||||
var query: String by rememberSaveable { mutableStateOf("") }
|
var query: String by rememberSaveable { mutableStateOf("") }
|
||||||
var showClearIcon by rememberSaveable { mutableStateOf(false) }
|
var showClearIcon by rememberSaveable { mutableStateOf(false) }
|
||||||
@@ -64,16 +62,18 @@ fun SearchBar(
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
colors = TextFieldDefaults.colors(
|
colors =
|
||||||
|
TextFieldDefaults.colors(
|
||||||
focusedContainerColor = Color.Transparent,
|
focusedContainerColor = Color.Transparent,
|
||||||
unfocusedContainerColor = Color.Transparent,
|
unfocusedContainerColor = Color.Transparent,
|
||||||
disabledContainerColor = Color.Transparent,
|
disabledContainerColor = Color.Transparent
|
||||||
),
|
),
|
||||||
placeholder = { Text(text = stringResource(R.string.hint_search_packages)) },
|
placeholder = { Text(text = stringResource(R.string.hint_search_packages)) },
|
||||||
textStyle = MaterialTheme.typography.bodySmall,
|
textStyle = MaterialTheme.typography.bodySmall,
|
||||||
singleLine = true,
|
singleLine = true,
|
||||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Text),
|
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Text),
|
||||||
modifier = Modifier
|
modifier =
|
||||||
|
Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.background(color = MaterialTheme.colorScheme.background, shape = RectangleShape)
|
.background(color = MaterialTheme.colorScheme.background, shape = RectangleShape)
|
||||||
)
|
)
|
||||||
|
|||||||
+40
@@ -0,0 +1,40 @@
|
|||||||
|
package com.zaneschepke.wireguardautotunnel.ui.common.config
|
||||||
|
|
||||||
|
import androidx.compose.foundation.text.KeyboardActions
|
||||||
|
import androidx.compose.foundation.text.KeyboardOptions
|
||||||
|
import androidx.compose.material3.OutlinedTextField
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.text.input.ImeAction
|
||||||
|
import androidx.compose.ui.text.input.KeyboardCapitalization
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ConfigurationTextBox(
|
||||||
|
value: String,
|
||||||
|
hint: String,
|
||||||
|
onValueChange: (String) -> Unit,
|
||||||
|
keyboardActions: KeyboardActions,
|
||||||
|
label: String,
|
||||||
|
modifier: Modifier
|
||||||
|
) {
|
||||||
|
OutlinedTextField(
|
||||||
|
modifier = modifier,
|
||||||
|
value = value,
|
||||||
|
singleLine = true,
|
||||||
|
onValueChange = {
|
||||||
|
onValueChange(it)
|
||||||
|
},
|
||||||
|
label = { Text(label) },
|
||||||
|
maxLines = 1,
|
||||||
|
placeholder = {
|
||||||
|
Text(hint)
|
||||||
|
},
|
||||||
|
keyboardOptions =
|
||||||
|
KeyboardOptions(
|
||||||
|
capitalization = KeyboardCapitalization.None,
|
||||||
|
imeAction = ImeAction.Done
|
||||||
|
),
|
||||||
|
keyboardActions = keyboardActions
|
||||||
|
)
|
||||||
|
}
|
||||||
+41
@@ -0,0 +1,41 @@
|
|||||||
|
package com.zaneschepke.wireguardautotunnel.ui.common.config
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.material3.Switch
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.unit.Dp
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ConfigurationToggle(
|
||||||
|
label: String,
|
||||||
|
enabled: Boolean,
|
||||||
|
checked: Boolean,
|
||||||
|
padding: Dp,
|
||||||
|
onCheckChanged: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(padding),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween
|
||||||
|
) {
|
||||||
|
Text(label)
|
||||||
|
Switch(
|
||||||
|
modifier = modifier,
|
||||||
|
enabled = enabled,
|
||||||
|
checked = checked,
|
||||||
|
onCheckedChange = {
|
||||||
|
onCheckChanged()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
+7
-5
@@ -11,12 +11,14 @@ import androidx.navigation.NavController
|
|||||||
import androidx.navigation.compose.currentBackStackEntryAsState
|
import androidx.navigation.compose.currentBackStackEntryAsState
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun BottomNavBar(navController : NavController, bottomNavItems : List<BottomNavItem>) {
|
fun BottomNavBar(
|
||||||
|
navController: NavController,
|
||||||
|
bottomNavItems: List<BottomNavItem>
|
||||||
|
) {
|
||||||
val backStackEntry = navController.currentBackStackEntryAsState()
|
val backStackEntry = navController.currentBackStackEntryAsState()
|
||||||
|
|
||||||
NavigationBar(
|
NavigationBar(
|
||||||
containerColor = MaterialTheme.colorScheme.background,
|
containerColor = MaterialTheme.colorScheme.background
|
||||||
) {
|
) {
|
||||||
bottomNavItems.forEach { item ->
|
bottomNavItems.forEach { item ->
|
||||||
val selected = item.route == backStackEntry.value?.destination?.route
|
val selected = item.route == backStackEntry.value?.destination?.route
|
||||||
@@ -27,13 +29,13 @@ fun BottomNavBar(navController : NavController, bottomNavItems : List<BottomNavI
|
|||||||
label = {
|
label = {
|
||||||
Text(
|
Text(
|
||||||
text = item.name,
|
text = item.name,
|
||||||
fontWeight = FontWeight.SemiBold,
|
fontWeight = FontWeight.SemiBold
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
icon = {
|
icon = {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = item.icon,
|
imageVector = item.icon,
|
||||||
contentDescription = "${item.name} Icon",
|
contentDescription = "${item.name} Icon"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|||||||
+97
@@ -0,0 +1,97 @@
|
|||||||
|
package com.zaneschepke.wireguardautotunnel.ui.common.prompt
|
||||||
|
|
||||||
|
import androidx.biometric.BiometricManager
|
||||||
|
import androidx.biometric.BiometricManager.Authenticators.BIOMETRIC_WEAK
|
||||||
|
import androidx.biometric.BiometricManager.Authenticators.DEVICE_CREDENTIAL
|
||||||
|
import androidx.biometric.BiometricPrompt
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
|
import androidx.fragment.app.FragmentActivity
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun AuthorizationPrompt(
|
||||||
|
onSuccess: () -> Unit,
|
||||||
|
onFailure: () -> Unit,
|
||||||
|
onError: (String) -> Unit
|
||||||
|
) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
val biometricManager = BiometricManager.from(context)
|
||||||
|
val bio = biometricManager.canAuthenticate(BIOMETRIC_WEAK or DEVICE_CREDENTIAL)
|
||||||
|
val isBiometricAvailable =
|
||||||
|
remember {
|
||||||
|
when (bio) {
|
||||||
|
BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE -> {
|
||||||
|
onError("Biometrics not available")
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED -> {
|
||||||
|
onError("Biometrics not created")
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE -> {
|
||||||
|
onError("Biometric hardware not found")
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
BiometricManager.BIOMETRIC_ERROR_SECURITY_UPDATE_REQUIRED -> {
|
||||||
|
onError("Biometric security update required")
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
BiometricManager.BIOMETRIC_ERROR_UNSUPPORTED -> {
|
||||||
|
onError("Biometrics not supported")
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
BiometricManager.BIOMETRIC_STATUS_UNKNOWN -> {
|
||||||
|
onError("Biometrics status unknown")
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
BiometricManager.BIOMETRIC_SUCCESS -> true
|
||||||
|
else -> false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (isBiometricAvailable) {
|
||||||
|
val executor = remember { ContextCompat.getMainExecutor(context) }
|
||||||
|
|
||||||
|
val promptInfo =
|
||||||
|
BiometricPrompt.PromptInfo.Builder()
|
||||||
|
.setAllowedAuthenticators(BIOMETRIC_WEAK or DEVICE_CREDENTIAL)
|
||||||
|
.setTitle("Biometric Authentication")
|
||||||
|
.setSubtitle("Log in using your biometric credential")
|
||||||
|
.build()
|
||||||
|
|
||||||
|
val biometricPrompt =
|
||||||
|
BiometricPrompt(
|
||||||
|
context as FragmentActivity,
|
||||||
|
executor,
|
||||||
|
object : BiometricPrompt.AuthenticationCallback() {
|
||||||
|
override fun onAuthenticationError(
|
||||||
|
errorCode: Int,
|
||||||
|
errString: CharSequence
|
||||||
|
) {
|
||||||
|
super.onAuthenticationError(errorCode, errString)
|
||||||
|
onFailure()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onAuthenticationSucceeded(
|
||||||
|
result: BiometricPrompt.AuthenticationResult
|
||||||
|
) {
|
||||||
|
super.onAuthenticationSucceeded(result)
|
||||||
|
onSuccess()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onAuthenticationFailed() {
|
||||||
|
super.onAuthenticationFailed()
|
||||||
|
onFailure()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
biometricPrompt.authenticate(promptInfo)
|
||||||
|
}
|
||||||
|
}
|
||||||
+64
@@ -0,0 +1,64 @@
|
|||||||
|
package com.zaneschepke.wireguardautotunnel.ui.common.prompt
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.IntrinsicSize
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.rounded.Info
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Snackbar
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.CompositionLocalProvider
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.platform.LocalLayoutDirection
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.unit.LayoutDirection
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.zaneschepke.wireguardautotunnel.R
|
||||||
|
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun CustomSnackBar(
|
||||||
|
message: String,
|
||||||
|
isRtl: Boolean = true,
|
||||||
|
containerColor: Color = MaterialTheme.colorScheme.surface
|
||||||
|
) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
Snackbar(
|
||||||
|
containerColor = containerColor,
|
||||||
|
modifier =
|
||||||
|
Modifier.fillMaxWidth(
|
||||||
|
if (WireGuardAutoTunnel.isRunningOnAndroidTv(context)) 1 / 3f else 2 / 3f
|
||||||
|
).padding(bottom = 100.dp),
|
||||||
|
shape = RoundedCornerShape(16.dp)
|
||||||
|
) {
|
||||||
|
CompositionLocalProvider(
|
||||||
|
LocalLayoutDirection provides
|
||||||
|
if (isRtl) LayoutDirection.Rtl else LayoutDirection.Ltr
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.width(IntrinsicSize.Max).height(IntrinsicSize.Min),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.Start
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
Icons.Rounded.Info,
|
||||||
|
contentDescription = stringResource(R.string.info),
|
||||||
|
tint = Color.White,
|
||||||
|
modifier = Modifier.padding(end = 10.dp)
|
||||||
|
)
|
||||||
|
Text(message, color = Color.White, modifier = Modifier.padding(end = 5.dp))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
package com.zaneschepke.wireguardautotunnel.ui.common.text
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.text.TextStyle
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
|
import androidx.compose.ui.unit.Dp
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun SectionTitle(
|
||||||
|
title: String,
|
||||||
|
padding: Dp
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
title,
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
style = TextStyle(fontSize = 18.sp, fontWeight = FontWeight.ExtraBold),
|
||||||
|
modifier = Modifier.padding(padding, bottom = 5.dp, top = 5.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
package com.zaneschepke.wireguardautotunnel.ui.models
|
||||||
|
|
||||||
|
import com.wireguard.config.Interface
|
||||||
|
|
||||||
|
data class InterfaceProxy(
|
||||||
|
var privateKey: String = "",
|
||||||
|
var publicKey: String = "",
|
||||||
|
var addresses: String = "",
|
||||||
|
var dnsServers: String = "",
|
||||||
|
var listenPort: String = "",
|
||||||
|
var mtu: String = ""
|
||||||
|
) {
|
||||||
|
companion object {
|
||||||
|
fun from(i: Interface): InterfaceProxy {
|
||||||
|
return InterfaceProxy(
|
||||||
|
publicKey = i.keyPair.publicKey.toBase64().trim(),
|
||||||
|
privateKey = i.keyPair.privateKey.toBase64().trim(),
|
||||||
|
addresses = i.addresses.joinToString(", ").trim(),
|
||||||
|
dnsServers = i.dnsServers.joinToString(", ").replace("/", "").trim(),
|
||||||
|
listenPort = if (i.listenPort.isPresent) {
|
||||||
|
i.listenPort.get().toString()
|
||||||
|
.trim()
|
||||||
|
} else {
|
||||||
|
""
|
||||||
|
},
|
||||||
|
mtu = if (i.mtu.isPresent) i.mtu.get().toString().trim() else ""
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
package com.zaneschepke.wireguardautotunnel.ui.models
|
||||||
|
|
||||||
|
import com.wireguard.config.Peer
|
||||||
|
|
||||||
|
data class PeerProxy(
|
||||||
|
var publicKey: String = "",
|
||||||
|
var preSharedKey: String = "",
|
||||||
|
var persistentKeepalive: String = "",
|
||||||
|
var endpoint: String = "",
|
||||||
|
var allowedIps: String = IPV4_WILDCARD.joinToString(", ").trim()
|
||||||
|
) {
|
||||||
|
companion object {
|
||||||
|
fun from(peer: Peer): PeerProxy {
|
||||||
|
return PeerProxy(
|
||||||
|
publicKey = peer.publicKey.toBase64(),
|
||||||
|
preSharedKey = if (peer.preSharedKey.isPresent) {
|
||||||
|
peer.preSharedKey.get().toBase64()
|
||||||
|
.trim()
|
||||||
|
} else {
|
||||||
|
""
|
||||||
|
},
|
||||||
|
persistentKeepalive = if (peer.persistentKeepalive.isPresent) {
|
||||||
|
peer.persistentKeepalive.get()
|
||||||
|
.toString().trim()
|
||||||
|
} else {
|
||||||
|
""
|
||||||
|
},
|
||||||
|
endpoint = if (peer.endpoint.isPresent) {
|
||||||
|
peer.endpoint.get().toString()
|
||||||
|
.trim()
|
||||||
|
} else {
|
||||||
|
""
|
||||||
|
},
|
||||||
|
allowedIps = peer.allowedIps.joinToString(", ").trim()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val IPV4_PUBLIC_NETWORKS =
|
||||||
|
setOf(
|
||||||
|
"0.0.0.0/5", "8.0.0.0/7", "11.0.0.0/8", "12.0.0.0/6", "16.0.0.0/4", "32.0.0.0/3",
|
||||||
|
"64.0.0.0/2", "128.0.0.0/3", "160.0.0.0/5", "168.0.0.0/6", "172.0.0.0/12",
|
||||||
|
"172.32.0.0/11", "172.64.0.0/10", "172.128.0.0/9", "173.0.0.0/8", "174.0.0.0/7",
|
||||||
|
"176.0.0.0/4", "192.0.0.0/9", "192.128.0.0/11", "192.160.0.0/13", "192.169.0.0/16",
|
||||||
|
"192.170.0.0/15", "192.172.0.0/14", "192.176.0.0/12", "192.192.0.0/10",
|
||||||
|
"193.0.0.0/8", "194.0.0.0/7", "196.0.0.0/6", "200.0.0.0/5", "208.0.0.0/4"
|
||||||
|
)
|
||||||
|
val IPV4_WILDCARD = setOf("0.0.0.0/0")
|
||||||
|
}
|
||||||
|
}
|
||||||
+583
-90
@@ -1,120 +1,205 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.ui.screens.config
|
package com.zaneschepke.wireguardautotunnel.ui.screens.config
|
||||||
|
|
||||||
import android.widget.Toast
|
import android.annotation.SuppressLint
|
||||||
|
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||||
import androidx.compose.foundation.Image
|
import androidx.compose.foundation.Image
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.focusGroup
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.IntrinsicSize
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxHeight
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.items
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.foundation.text.KeyboardActions
|
import androidx.compose.foundation.text.KeyboardActions
|
||||||
import androidx.compose.foundation.text.KeyboardOptions
|
import androidx.compose.foundation.text.KeyboardOptions
|
||||||
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.rounded.Android
|
import androidx.compose.material.icons.rounded.Android
|
||||||
import androidx.compose.material3.Button
|
import androidx.compose.material.icons.rounded.ContentCopy
|
||||||
|
import androidx.compose.material.icons.rounded.Delete
|
||||||
|
import androidx.compose.material.icons.rounded.Refresh
|
||||||
|
import androidx.compose.material.icons.rounded.Save
|
||||||
|
import androidx.compose.material3.AlertDialog
|
||||||
import androidx.compose.material3.Checkbox
|
import androidx.compose.material3.Checkbox
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.FabPosition
|
||||||
|
import androidx.compose.material3.FloatingActionButton
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.IconButton
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.OutlinedTextField
|
import androidx.compose.material3.OutlinedTextField
|
||||||
|
import androidx.compose.material3.Scaffold
|
||||||
|
import androidx.compose.material3.Surface
|
||||||
import androidx.compose.material3.Switch
|
import androidx.compose.material3.Switch
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TextButton
|
||||||
|
import androidx.compose.material3.surfaceColorAtElevation
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.ExperimentalComposeUiApi
|
import androidx.compose.ui.ExperimentalComposeUiApi
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.focus.FocusRequester
|
import androidx.compose.ui.focus.FocusRequester
|
||||||
import androidx.compose.ui.focus.focusRequester
|
import androidx.compose.ui.focus.focusRequester
|
||||||
|
import androidx.compose.ui.focus.onFocusChanged
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.platform.ClipboardManager
|
||||||
|
import androidx.compose.ui.platform.LocalClipboardManager
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.platform.LocalFocusManager
|
|
||||||
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.AnnotatedString
|
||||||
import androidx.compose.ui.text.input.ImeAction
|
import androidx.compose.ui.text.input.ImeAction
|
||||||
import androidx.compose.ui.text.input.KeyboardCapitalization
|
import androidx.compose.ui.text.input.KeyboardCapitalization
|
||||||
|
import androidx.compose.ui.text.input.PasswordVisualTransformation
|
||||||
|
import androidx.compose.ui.text.input.VisualTransformation
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.hilt.navigation.compose.hiltViewModel
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import androidx.navigation.NavController
|
import androidx.navigation.NavController
|
||||||
import com.google.accompanist.drawablepainter.DrawablePainter
|
import com.google.accompanist.drawablepainter.DrawablePainter
|
||||||
|
import com.zaneschepke.wireguardautotunnel.Constants
|
||||||
import com.zaneschepke.wireguardautotunnel.R
|
import com.zaneschepke.wireguardautotunnel.R
|
||||||
|
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.Routes
|
import com.zaneschepke.wireguardautotunnel.ui.Routes
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.common.SearchBar
|
import com.zaneschepke.wireguardautotunnel.ui.common.SearchBar
|
||||||
|
import com.zaneschepke.wireguardautotunnel.ui.common.config.ConfigurationTextBox
|
||||||
|
import com.zaneschepke.wireguardautotunnel.ui.common.prompt.AuthorizationPrompt
|
||||||
|
import com.zaneschepke.wireguardautotunnel.ui.common.text.SectionTitle
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import timber.log.Timber
|
||||||
|
|
||||||
@OptIn(ExperimentalComposeUiApi::class)
|
@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter")
|
||||||
|
@OptIn(
|
||||||
|
ExperimentalComposeUiApi::class,
|
||||||
|
ExperimentalMaterial3Api::class,
|
||||||
|
ExperimentalFoundationApi::class
|
||||||
|
)
|
||||||
@Composable
|
@Composable
|
||||||
fun ConfigScreen(
|
fun ConfigScreen(
|
||||||
viewModel: ConfigViewModel = hiltViewModel(),
|
viewModel: ConfigViewModel = hiltViewModel(),
|
||||||
padding: PaddingValues,
|
|
||||||
focusRequester: FocusRequester,
|
focusRequester: FocusRequester,
|
||||||
navController: NavController,
|
navController: NavController,
|
||||||
id : String?
|
showSnackbarMessage: (String) -> Unit,
|
||||||
|
id: String
|
||||||
) {
|
) {
|
||||||
|
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val focusManager = LocalFocusManager.current
|
|
||||||
|
|
||||||
val keyboardController = LocalSoftwareKeyboardController.current
|
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
|
val clipboardManager: ClipboardManager = LocalClipboardManager.current
|
||||||
|
val keyboardController = LocalSoftwareKeyboardController.current
|
||||||
|
|
||||||
val tunnel by viewModel.tunnel.collectAsStateWithLifecycle(null)
|
val tunnel by viewModel.tunnel.collectAsStateWithLifecycle(null)
|
||||||
val tunnelName = viewModel.tunnelName.collectAsStateWithLifecycle()
|
val tunnelName = viewModel.tunnelName.collectAsStateWithLifecycle()
|
||||||
val packages by viewModel.packages.collectAsStateWithLifecycle()
|
val packages by viewModel.packages.collectAsStateWithLifecycle()
|
||||||
val checkedPackages by viewModel.checkedPackages.collectAsStateWithLifecycle()
|
val checkedPackages by viewModel.checkedPackages.collectAsStateWithLifecycle()
|
||||||
val include by viewModel.include.collectAsStateWithLifecycle()
|
val include by viewModel.include.collectAsStateWithLifecycle()
|
||||||
val allApplications by viewModel.allApplications.collectAsStateWithLifecycle()
|
val isAllApplicationsEnabled by viewModel.isAllApplicationsEnabled.collectAsStateWithLifecycle()
|
||||||
|
val proxyPeers by viewModel.proxyPeers.collectAsStateWithLifecycle()
|
||||||
LaunchedEffect(Unit) {
|
val proxyInterface by viewModel.interfaceProxy.collectAsStateWithLifecycle()
|
||||||
viewModel.getTunnelById(id)
|
var showApplicationsDialog by remember { mutableStateOf(false) }
|
||||||
viewModel.emitQueriedPackages("")
|
var showAuthPrompt by remember { mutableStateOf(false) }
|
||||||
viewModel.emitCurrentPackageConfigurations(id)
|
var isAuthenticated by remember { mutableStateOf(false) }
|
||||||
|
val baseTextBoxModifier =
|
||||||
|
Modifier.onFocusChanged {
|
||||||
|
if (WireGuardAutoTunnel.isRunningOnAndroidTv(context)) {
|
||||||
|
keyboardController?.hide()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if(tunnel != null) {
|
val keyboardActions =
|
||||||
LazyColumn(
|
KeyboardActions(
|
||||||
horizontalAlignment = Alignment.Start,
|
onDone = {
|
||||||
verticalArrangement = Arrangement.Top,
|
keyboardController?.hide()
|
||||||
modifier = Modifier
|
}
|
||||||
.fillMaxSize()
|
)
|
||||||
.padding(padding)
|
|
||||||
) {
|
val keyboardOptions =
|
||||||
item {
|
KeyboardOptions(
|
||||||
Row(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(horizontal = 20.dp, vertical = 7.dp),
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
horizontalArrangement = Arrangement.SpaceBetween
|
|
||||||
) {
|
|
||||||
OutlinedTextField(
|
|
||||||
modifier = Modifier.focusRequester(focusRequester),
|
|
||||||
value = tunnelName.value,
|
|
||||||
onValueChange = {
|
|
||||||
viewModel.onTunnelNameChange(it)
|
|
||||||
},
|
|
||||||
label = { Text(stringResource(id = R.string.tunnel_name)) },
|
|
||||||
maxLines = 1,
|
|
||||||
keyboardOptions = KeyboardOptions(
|
|
||||||
capitalization = KeyboardCapitalization.None,
|
capitalization = KeyboardCapitalization.None,
|
||||||
imeAction = ImeAction.Done
|
imeAction = ImeAction.Done
|
||||||
),
|
)
|
||||||
keyboardActions = KeyboardActions(
|
|
||||||
onDone = {
|
val fillMaxHeight = .85f
|
||||||
focusManager.clearFocus()
|
val fillMaxWidth = .85f
|
||||||
keyboardController?.hide()
|
val screenPadding = 5.dp
|
||||||
viewModel.onTunnelNameChange(tunnelName.value)
|
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
scope.launch(Dispatchers.IO) {
|
||||||
|
try {
|
||||||
|
viewModel.onScreenLoad(id)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
showSnackbarMessage(e.message!!)
|
||||||
|
navController.navigate(Routes.Main.name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val applicationButtonText = {
|
||||||
|
"Tunneling apps: " +
|
||||||
|
if (isAllApplicationsEnabled) {
|
||||||
|
"all"
|
||||||
|
} else {
|
||||||
|
"${checkedPackages.size} " + (if (include) "included" else "excluded")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (showAuthPrompt) {
|
||||||
|
AuthorizationPrompt(
|
||||||
|
onSuccess = {
|
||||||
|
showAuthPrompt = false
|
||||||
|
isAuthenticated = true
|
||||||
|
},
|
||||||
|
onError = { error ->
|
||||||
|
showSnackbarMessage(error)
|
||||||
|
showAuthPrompt = false
|
||||||
|
},
|
||||||
|
onFailure = {
|
||||||
|
showAuthPrompt = false
|
||||||
|
showSnackbarMessage(context.getString(R.string.authentication_failed))
|
||||||
}
|
}
|
||||||
),
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (showApplicationsDialog) {
|
||||||
|
val sortedPackages =
|
||||||
|
remember(packages) {
|
||||||
|
packages.sortedBy { viewModel.getPackageLabel(it) }
|
||||||
}
|
}
|
||||||
item {
|
AlertDialog(onDismissRequest = {
|
||||||
|
showApplicationsDialog = false
|
||||||
|
}) {
|
||||||
|
Surface(
|
||||||
|
tonalElevation = 2.dp,
|
||||||
|
shadowElevation = 2.dp,
|
||||||
|
shape = RoundedCornerShape(12.dp),
|
||||||
|
color = MaterialTheme.colorScheme.surface,
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.fillMaxHeight(if (isAllApplicationsEnabled) 1 / 5f else 4 / 5f)
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
) {
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier =
|
||||||
|
Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(horizontal = 20.dp, vertical = 7.dp),
|
.padding(horizontal = 20.dp, vertical = 7.dp),
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
@@ -122,19 +207,21 @@ fun ConfigScreen(
|
|||||||
) {
|
) {
|
||||||
Text(stringResource(id = R.string.tunnel_all))
|
Text(stringResource(id = R.string.tunnel_all))
|
||||||
Switch(
|
Switch(
|
||||||
checked = allApplications,
|
checked = isAllApplicationsEnabled,
|
||||||
onCheckedChange = {
|
onCheckedChange = {
|
||||||
viewModel.onAllApplicationsChange(!allApplications)
|
viewModel.onAllApplicationsChange(it)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
if (!isAllApplicationsEnabled) {
|
||||||
if (!allApplications) {
|
|
||||||
item {
|
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier =
|
||||||
|
Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(horizontal = 20.dp, vertical = 7.dp),
|
.padding(
|
||||||
|
horizontal = 20.dp,
|
||||||
|
vertical = 7.dp
|
||||||
|
),
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
horizontalArrangement = Arrangement.SpaceBetween
|
horizontalArrangement = Arrangement.SpaceBetween
|
||||||
) {
|
) {
|
||||||
@@ -163,77 +250,483 @@ fun ConfigScreen(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
item {
|
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier =
|
||||||
|
Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(horizontal = 20.dp, vertical = 7.dp),
|
.padding(
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
horizontal = 20.dp,
|
||||||
horizontalArrangement = Arrangement.SpaceBetween) {
|
vertical = 7.dp
|
||||||
SearchBar(viewModel::emitQueriedPackages);
|
),
|
||||||
}
|
|
||||||
}
|
|
||||||
items(packages) { pack ->
|
|
||||||
Row(
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
horizontalArrangement = Arrangement.SpaceBetween
|
horizontalArrangement = Arrangement.SpaceBetween
|
||||||
) {
|
) {
|
||||||
|
SearchBar(viewModel::emitQueriedPackages)
|
||||||
|
}
|
||||||
|
Spacer(Modifier.padding(5.dp))
|
||||||
|
LazyColumn(
|
||||||
|
horizontalAlignment = Alignment.Start,
|
||||||
|
verticalArrangement = Arrangement.Top,
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.fillMaxHeight(4 / 5f)
|
||||||
|
) {
|
||||||
|
items(
|
||||||
|
sortedPackages,
|
||||||
|
key = { it.packageName }
|
||||||
|
) { pack ->
|
||||||
Row(
|
Row(
|
||||||
horizontalArrangement = Arrangement.Center,
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
modifier = Modifier.padding(5.dp)
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(5.dp)
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier =
|
||||||
|
Modifier.fillMaxWidth(
|
||||||
|
fillMaxWidth
|
||||||
|
)
|
||||||
) {
|
) {
|
||||||
val drawable =
|
val drawable =
|
||||||
pack.applicationInfo?.loadIcon(context.packageManager)
|
pack.applicationInfo?.loadIcon(
|
||||||
|
context.packageManager
|
||||||
|
)
|
||||||
if (drawable != null) {
|
if (drawable != null) {
|
||||||
Image(
|
Image(
|
||||||
painter = DrawablePainter(drawable),
|
painter =
|
||||||
|
DrawablePainter(
|
||||||
|
drawable
|
||||||
|
),
|
||||||
stringResource(id = R.string.icon),
|
stringResource(id = R.string.icon),
|
||||||
modifier = Modifier.size(50.dp, 50.dp)
|
modifier =
|
||||||
|
Modifier.size(
|
||||||
|
50.dp,
|
||||||
|
50.dp
|
||||||
|
)
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
Icon(
|
Icon(
|
||||||
Icons.Rounded.Android,
|
Icons.Rounded.Android,
|
||||||
stringResource(id = R.string.edit),
|
stringResource(id = R.string.edit),
|
||||||
modifier = Modifier.size(50.dp, 50.dp)
|
modifier =
|
||||||
|
Modifier.size(
|
||||||
|
50.dp,
|
||||||
|
50.dp
|
||||||
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
Text(
|
Text(
|
||||||
pack.applicationInfo.loadLabel(context.packageManager)
|
viewModel.getPackageLabel(pack),
|
||||||
.toString(), modifier = Modifier.padding(5.dp)
|
modifier = Modifier.padding(5.dp)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
Checkbox(
|
Checkbox(
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
checked = (checkedPackages.contains(pack.packageName)),
|
checked = (checkedPackages.contains(pack.packageName)),
|
||||||
onCheckedChange = {
|
onCheckedChange = {
|
||||||
if (it) viewModel.onAddCheckedPackage(pack.packageName) else viewModel.onRemoveCheckedPackage(
|
if (it) {
|
||||||
|
viewModel.onAddCheckedPackage(
|
||||||
|
pack.packageName
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
viewModel.onRemoveCheckedPackage(
|
||||||
pack.packageName
|
pack.packageName
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
item {
|
}
|
||||||
Row(
|
Row(
|
||||||
horizontalArrangement = Arrangement.Center,
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
modifier = Modifier.fillMaxWidth()
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(top = 5.dp),
|
||||||
|
horizontalArrangement = Arrangement.Center
|
||||||
) {
|
) {
|
||||||
Button(onClick = {
|
TextButton(
|
||||||
|
onClick = {
|
||||||
|
showApplicationsDialog = false
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Text(stringResource(R.string.done))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tunnel != null) {
|
||||||
|
Scaffold(
|
||||||
|
floatingActionButtonPosition = FabPosition.End,
|
||||||
|
floatingActionButton = {
|
||||||
|
val secondaryColor = MaterialTheme.colorScheme.secondary
|
||||||
|
val hoverColor = MaterialTheme.colorScheme.surfaceColorAtElevation(2.dp)
|
||||||
|
var fobColor by remember { mutableStateOf(secondaryColor) }
|
||||||
|
FloatingActionButton(
|
||||||
|
modifier =
|
||||||
|
Modifier.padding(bottom = 90.dp).onFocusChanged {
|
||||||
|
if (WireGuardAutoTunnel.isRunningOnAndroidTv(context)) {
|
||||||
|
fobColor = if (it.isFocused) hoverColor else secondaryColor
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onClick = {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
|
try {
|
||||||
viewModel.onSaveAllChanges()
|
viewModel.onSaveAllChanges()
|
||||||
Toast.makeText(
|
|
||||||
context,
|
|
||||||
context.resources.getString(R.string.config_changes_saved),
|
|
||||||
Toast.LENGTH_LONG
|
|
||||||
).show()
|
|
||||||
navController.navigate(Routes.Main.name)
|
navController.navigate(Routes.Main.name)
|
||||||
|
showSnackbarMessage(
|
||||||
|
context.resources.getString(R.string.config_changes_saved)
|
||||||
|
)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Timber.e(e.message)
|
||||||
|
showSnackbarMessage(e.message!!)
|
||||||
}
|
}
|
||||||
}, Modifier.padding(25.dp)) {
|
}
|
||||||
Text(stringResource(id = R.string.save_changes))
|
},
|
||||||
}
|
containerColor = fobColor,
|
||||||
|
shape = RoundedCornerShape(16.dp)
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Rounded.Save,
|
||||||
|
contentDescription = stringResource(id = R.string.save_changes),
|
||||||
|
tint = Color.DarkGray
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Column {
|
||||||
|
Column(
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
verticalArrangement = Arrangement.Top,
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.verticalScroll(rememberScrollState())
|
||||||
|
.weight(1f, true)
|
||||||
|
.fillMaxSize()
|
||||||
|
) {
|
||||||
|
Surface(
|
||||||
|
tonalElevation = 2.dp,
|
||||||
|
shadowElevation = 2.dp,
|
||||||
|
shape = RoundedCornerShape(12.dp),
|
||||||
|
color = MaterialTheme.colorScheme.surface,
|
||||||
|
modifier =
|
||||||
|
(
|
||||||
|
if (WireGuardAutoTunnel.isRunningOnAndroidTv(context)) {
|
||||||
|
Modifier
|
||||||
|
.fillMaxHeight(fillMaxHeight)
|
||||||
|
.fillMaxWidth(fillMaxWidth)
|
||||||
|
} else {
|
||||||
|
Modifier.fillMaxWidth(fillMaxWidth)
|
||||||
|
}
|
||||||
|
).padding(
|
||||||
|
top = 50.dp,
|
||||||
|
bottom = 10.dp
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
horizontalAlignment = Alignment.Start,
|
||||||
|
verticalArrangement = Arrangement.Top,
|
||||||
|
modifier = Modifier.padding(15.dp).focusGroup()
|
||||||
|
) {
|
||||||
|
SectionTitle(
|
||||||
|
stringResource(R.string.interface_),
|
||||||
|
padding = screenPadding
|
||||||
|
)
|
||||||
|
ConfigurationTextBox(
|
||||||
|
value = tunnelName.value,
|
||||||
|
onValueChange = { value ->
|
||||||
|
viewModel.onTunnelNameChange(value)
|
||||||
|
},
|
||||||
|
keyboardActions = keyboardActions,
|
||||||
|
label = stringResource(R.string.name),
|
||||||
|
hint = stringResource(R.string.tunnel_name).lowercase(),
|
||||||
|
modifier = baseTextBoxModifier.fillMaxWidth().focusRequester(
|
||||||
|
focusRequester
|
||||||
|
)
|
||||||
|
)
|
||||||
|
OutlinedTextField(
|
||||||
|
modifier =
|
||||||
|
baseTextBoxModifier.fillMaxWidth().clickable {
|
||||||
|
showAuthPrompt = true
|
||||||
|
},
|
||||||
|
value = proxyInterface.privateKey,
|
||||||
|
visualTransformation = if ((id == Constants.MANUAL_TUNNEL_CONFIG_ID) || isAuthenticated) VisualTransformation.None else PasswordVisualTransformation(),
|
||||||
|
enabled = (id == Constants.MANUAL_TUNNEL_CONFIG_ID) || isAuthenticated,
|
||||||
|
onValueChange = { value ->
|
||||||
|
viewModel.onPrivateKeyChange(value)
|
||||||
|
},
|
||||||
|
trailingIcon = {
|
||||||
|
IconButton(
|
||||||
|
modifier = Modifier.focusRequester(FocusRequester.Default),
|
||||||
|
onClick = {
|
||||||
|
viewModel.generateKeyPair()
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
Icons.Rounded.Refresh,
|
||||||
|
stringResource(R.string.rotate_keys),
|
||||||
|
tint = Color.White
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
label = { Text(stringResource(R.string.private_key)) },
|
||||||
|
singleLine = true,
|
||||||
|
placeholder = { Text(stringResource(R.string.base64_key)) },
|
||||||
|
keyboardOptions = keyboardOptions,
|
||||||
|
keyboardActions = keyboardActions
|
||||||
|
)
|
||||||
|
OutlinedTextField(
|
||||||
|
modifier = baseTextBoxModifier.fillMaxWidth().focusRequester(
|
||||||
|
FocusRequester.Default
|
||||||
|
),
|
||||||
|
value = proxyInterface.publicKey,
|
||||||
|
enabled = false,
|
||||||
|
onValueChange = {},
|
||||||
|
trailingIcon = {
|
||||||
|
IconButton(
|
||||||
|
modifier = Modifier.focusRequester(FocusRequester.Default),
|
||||||
|
onClick = {
|
||||||
|
clipboardManager.setText(
|
||||||
|
AnnotatedString(proxyInterface.publicKey)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
Icons.Rounded.ContentCopy,
|
||||||
|
stringResource(R.string.copy_public_key),
|
||||||
|
tint = Color.White
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
label = { Text(stringResource(R.string.public_key)) },
|
||||||
|
singleLine = true,
|
||||||
|
placeholder = { Text(stringResource(R.string.base64_key)) },
|
||||||
|
keyboardOptions = keyboardOptions,
|
||||||
|
keyboardActions = keyboardActions
|
||||||
|
)
|
||||||
|
Row(modifier = Modifier.fillMaxWidth()) {
|
||||||
|
ConfigurationTextBox(
|
||||||
|
value = proxyInterface.addresses,
|
||||||
|
onValueChange = { value ->
|
||||||
|
viewModel.onAddressesChanged(value)
|
||||||
|
},
|
||||||
|
keyboardActions = keyboardActions,
|
||||||
|
label = stringResource(R.string.addresses),
|
||||||
|
hint = stringResource(R.string.comma_separated_list),
|
||||||
|
modifier =
|
||||||
|
baseTextBoxModifier
|
||||||
|
.fillMaxWidth(3 / 5f)
|
||||||
|
.padding(end = 5.dp)
|
||||||
|
)
|
||||||
|
ConfigurationTextBox(
|
||||||
|
value = proxyInterface.listenPort,
|
||||||
|
onValueChange = { value -> viewModel.onListenPortChanged(value) },
|
||||||
|
keyboardActions = keyboardActions,
|
||||||
|
label = stringResource(R.string.listen_port),
|
||||||
|
hint = stringResource(R.string.random),
|
||||||
|
modifier = baseTextBoxModifier.width(IntrinsicSize.Min)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Row(modifier = Modifier.fillMaxWidth()) {
|
||||||
|
ConfigurationTextBox(
|
||||||
|
value = proxyInterface.dnsServers,
|
||||||
|
onValueChange = { value -> viewModel.onDnsServersChanged(value) },
|
||||||
|
keyboardActions = keyboardActions,
|
||||||
|
label = stringResource(R.string.dns_servers),
|
||||||
|
hint = stringResource(R.string.comma_separated_list),
|
||||||
|
modifier =
|
||||||
|
baseTextBoxModifier
|
||||||
|
.fillMaxWidth(3 / 5f)
|
||||||
|
.padding(end = 5.dp)
|
||||||
|
)
|
||||||
|
ConfigurationTextBox(
|
||||||
|
value = proxyInterface.mtu,
|
||||||
|
onValueChange = { value -> viewModel.onMtuChanged(value) },
|
||||||
|
keyboardActions = keyboardActions,
|
||||||
|
label = stringResource(R.string.mtu),
|
||||||
|
hint = stringResource(R.string.auto),
|
||||||
|
modifier = baseTextBoxModifier.width(IntrinsicSize.Min)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(top = 5.dp),
|
||||||
|
horizontalArrangement = Arrangement.Center
|
||||||
|
) {
|
||||||
|
TextButton(
|
||||||
|
onClick = {
|
||||||
|
showApplicationsDialog = true
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Text(applicationButtonText())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
proxyPeers.forEachIndexed { index, peer ->
|
||||||
|
Surface(
|
||||||
|
tonalElevation = 2.dp,
|
||||||
|
shadowElevation = 2.dp,
|
||||||
|
shape = RoundedCornerShape(12.dp),
|
||||||
|
color = MaterialTheme.colorScheme.surface,
|
||||||
|
modifier =
|
||||||
|
(
|
||||||
|
if (WireGuardAutoTunnel.isRunningOnAndroidTv(context)) {
|
||||||
|
Modifier
|
||||||
|
.fillMaxHeight(fillMaxHeight)
|
||||||
|
.fillMaxWidth(fillMaxWidth)
|
||||||
|
} else {
|
||||||
|
Modifier.fillMaxWidth(fillMaxWidth)
|
||||||
|
}
|
||||||
|
).padding(
|
||||||
|
top = 10.dp,
|
||||||
|
bottom = 10.dp
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
horizontalAlignment = Alignment.Start,
|
||||||
|
verticalArrangement = Arrangement.Top,
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.padding(horizontal = 15.dp)
|
||||||
|
.padding(bottom = 10.dp)
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 5.dp)
|
||||||
|
) {
|
||||||
|
SectionTitle(
|
||||||
|
stringResource(R.string.peer),
|
||||||
|
padding = screenPadding
|
||||||
|
)
|
||||||
|
IconButton(
|
||||||
|
onClick = {
|
||||||
|
viewModel.onDeletePeer(index)
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Icon(Icons.Rounded.Delete, stringResource(R.string.delete))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ConfigurationTextBox(
|
||||||
|
value = peer.publicKey,
|
||||||
|
onValueChange = { value ->
|
||||||
|
viewModel.onPeerPublicKeyChange(
|
||||||
|
index,
|
||||||
|
value
|
||||||
|
)
|
||||||
|
},
|
||||||
|
keyboardActions = keyboardActions,
|
||||||
|
label = stringResource(R.string.public_key),
|
||||||
|
hint = stringResource(R.string.base64_key),
|
||||||
|
modifier = baseTextBoxModifier.fillMaxWidth()
|
||||||
|
)
|
||||||
|
ConfigurationTextBox(
|
||||||
|
value = peer.preSharedKey,
|
||||||
|
onValueChange = { value ->
|
||||||
|
viewModel.onPreSharedKeyChange(
|
||||||
|
index,
|
||||||
|
value
|
||||||
|
)
|
||||||
|
},
|
||||||
|
keyboardActions = keyboardActions,
|
||||||
|
label = stringResource(R.string.preshared_key),
|
||||||
|
hint = stringResource(R.string.optional),
|
||||||
|
modifier = baseTextBoxModifier.fillMaxWidth()
|
||||||
|
)
|
||||||
|
OutlinedTextField(
|
||||||
|
modifier = baseTextBoxModifier.fillMaxWidth(),
|
||||||
|
value = peer.persistentKeepalive,
|
||||||
|
enabled = true,
|
||||||
|
onValueChange = { value ->
|
||||||
|
viewModel.onPersistentKeepaliveChanged(index, value)
|
||||||
|
},
|
||||||
|
trailingIcon = {
|
||||||
|
Text(
|
||||||
|
stringResource(R.string.seconds),
|
||||||
|
modifier = Modifier.padding(end = 10.dp)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
label = { Text(stringResource(R.string.persistent_keepalive)) },
|
||||||
|
singleLine = true,
|
||||||
|
placeholder = {
|
||||||
|
Text(stringResource(R.string.optional_no_recommend))
|
||||||
|
},
|
||||||
|
keyboardOptions = keyboardOptions,
|
||||||
|
keyboardActions = keyboardActions
|
||||||
|
)
|
||||||
|
ConfigurationTextBox(
|
||||||
|
value = peer.endpoint,
|
||||||
|
onValueChange = { value ->
|
||||||
|
viewModel.onEndpointChange(
|
||||||
|
index,
|
||||||
|
value
|
||||||
|
)
|
||||||
|
},
|
||||||
|
keyboardActions = keyboardActions,
|
||||||
|
label = stringResource(R.string.endpoint),
|
||||||
|
hint = stringResource(R.string.endpoint).lowercase(),
|
||||||
|
modifier = baseTextBoxModifier.fillMaxWidth()
|
||||||
|
)
|
||||||
|
OutlinedTextField(
|
||||||
|
modifier = baseTextBoxModifier.fillMaxWidth(),
|
||||||
|
value = peer.allowedIps,
|
||||||
|
enabled = true,
|
||||||
|
onValueChange = { value ->
|
||||||
|
viewModel.onAllowedIpsChange(
|
||||||
|
index,
|
||||||
|
value
|
||||||
|
)
|
||||||
|
},
|
||||||
|
label = { Text(stringResource(R.string.allowed_ips)) },
|
||||||
|
singleLine = true,
|
||||||
|
placeholder = {
|
||||||
|
Text(stringResource(R.string.comma_separated_list))
|
||||||
|
},
|
||||||
|
keyboardOptions = keyboardOptions,
|
||||||
|
keyboardActions = keyboardActions
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Row(
|
||||||
|
horizontalArrangement = Arrangement.SpaceEvenly,
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(bottom = 140.dp)
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.Center
|
||||||
|
) {
|
||||||
|
TextButton(
|
||||||
|
onClick = {
|
||||||
|
viewModel.addEmptyPeer()
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Text(stringResource(R.string.add_peer))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (WireGuardAutoTunnel.isRunningOnAndroidTv(context)) {
|
||||||
|
Spacer(modifier = Modifier.weight(.17f))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+352
-65
@@ -9,25 +9,45 @@ import androidx.compose.runtime.mutableStateListOf
|
|||||||
import androidx.compose.runtime.toMutableStateList
|
import androidx.compose.runtime.toMutableStateList
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import com.zaneschepke.wireguardautotunnel.repository.Repository
|
import com.wireguard.config.Config
|
||||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.model.Settings
|
import com.wireguard.config.Interface
|
||||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.model.TunnelConfig
|
import com.wireguard.config.Peer
|
||||||
|
import com.wireguard.crypto.Key
|
||||||
|
import com.wireguard.crypto.KeyPair
|
||||||
|
import com.zaneschepke.wireguardautotunnel.Constants
|
||||||
|
import com.zaneschepke.wireguardautotunnel.repository.SettingsDoa
|
||||||
|
import com.zaneschepke.wireguardautotunnel.repository.TunnelConfigDao
|
||||||
|
import com.zaneschepke.wireguardautotunnel.repository.model.TunnelConfig
|
||||||
|
import com.zaneschepke.wireguardautotunnel.ui.models.InterfaceProxy
|
||||||
|
import com.zaneschepke.wireguardautotunnel.ui.models.PeerProxy
|
||||||
|
import com.zaneschepke.wireguardautotunnel.util.NumberUtils
|
||||||
|
import com.zaneschepke.wireguardautotunnel.util.WgTunnelException
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
|
import javax.inject.Inject
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import timber.log.Timber
|
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
class ConfigViewModel @Inject constructor(private val application : Application,
|
class ConfigViewModel
|
||||||
private val tunnelRepo : Repository<TunnelConfig>,
|
@Inject
|
||||||
private val settingsRepo : Repository<Settings>) : ViewModel() {
|
constructor(
|
||||||
|
private val application: Application,
|
||||||
|
private val tunnelRepo: TunnelConfigDao,
|
||||||
|
private val settingsRepo: SettingsDoa
|
||||||
|
) : ViewModel() {
|
||||||
private val _tunnel = MutableStateFlow<TunnelConfig?>(null)
|
private val _tunnel = MutableStateFlow<TunnelConfig?>(null)
|
||||||
private val _tunnelName = MutableStateFlow("")
|
private val _tunnelName = MutableStateFlow("")
|
||||||
val tunnelName get() = _tunnelName.asStateFlow()
|
val tunnelName get() = _tunnelName.asStateFlow()
|
||||||
val tunnel get() = _tunnel.asStateFlow()
|
val tunnel get() = _tunnel.asStateFlow()
|
||||||
|
|
||||||
|
private var _proxyPeers = MutableStateFlow(mutableStateListOf<PeerProxy>())
|
||||||
|
val proxyPeers get() = _proxyPeers.asStateFlow()
|
||||||
|
|
||||||
|
private var _interface = MutableStateFlow(InterfaceProxy())
|
||||||
|
val interfaceProxy = _interface.asStateFlow()
|
||||||
|
|
||||||
private val _packages = MutableStateFlow(emptyList<PackageInfo>())
|
private val _packages = MutableStateFlow(emptyList<PackageInfo>())
|
||||||
val packages get() = _packages.asStateFlow()
|
val packages get() = _packages.asStateFlow()
|
||||||
private val packageManager = application.packageManager
|
private val packageManager = application.packageManager
|
||||||
@@ -37,27 +57,89 @@ class ConfigViewModel @Inject constructor(private val application : Application,
|
|||||||
private val _include = MutableStateFlow(true)
|
private val _include = MutableStateFlow(true)
|
||||||
val include get() = _include.asStateFlow()
|
val include get() = _include.asStateFlow()
|
||||||
|
|
||||||
private val _allApplications = MutableStateFlow(true)
|
private val _isAllApplicationsEnabled = MutableStateFlow(false)
|
||||||
val allApplications get() = _allApplications.asStateFlow()
|
val isAllApplicationsEnabled get() = _isAllApplicationsEnabled.asStateFlow()
|
||||||
|
private val _isDefaultTunnel = MutableStateFlow(false)
|
||||||
|
|
||||||
suspend fun getTunnelById(id : String?) : TunnelConfig? {
|
private lateinit var tunnelConfig: TunnelConfig
|
||||||
|
|
||||||
|
suspend fun onScreenLoad(id: String) {
|
||||||
|
if (id != Constants.MANUAL_TUNNEL_CONFIG_ID) {
|
||||||
|
tunnelConfig = getTunnelConfigById(id) ?: throw WgTunnelException(
|
||||||
|
"Config not found"
|
||||||
|
)
|
||||||
|
emitScreenData()
|
||||||
|
} else {
|
||||||
|
emitEmptyScreenData()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun emitEmptyScreenData() {
|
||||||
|
tunnelConfig = TunnelConfig(name = NumberUtils.generateRandomTunnelName(), wgQuick = "")
|
||||||
|
viewModelScope.launch {
|
||||||
|
emitTunnelConfig()
|
||||||
|
emitPeerProxy(PeerProxy())
|
||||||
|
emitInterfaceProxy(InterfaceProxy())
|
||||||
|
emitTunnelConfigName()
|
||||||
|
emitDefaultTunnelStatus()
|
||||||
|
emitQueriedPackages("")
|
||||||
|
emitTunnelAllApplicationsEnabled()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun emitScreenData() {
|
||||||
|
emitTunnelConfig()
|
||||||
|
emitPeersFromConfig()
|
||||||
|
emitInterfaceFromConfig()
|
||||||
|
emitTunnelConfigName()
|
||||||
|
emitDefaultTunnelStatus()
|
||||||
|
emitQueriedPackages("")
|
||||||
|
emitCurrentPackageConfigurations()
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun emitDefaultTunnelStatus() {
|
||||||
|
val settings = settingsRepo.getAll()
|
||||||
|
if (settings.isNotEmpty()) {
|
||||||
|
_isDefaultTunnel.value = settings.first().isTunnelConfigDefault(tunnelConfig)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun emitInterfaceFromConfig() {
|
||||||
|
val config = TunnelConfig.configFromQuick(tunnelConfig.wgQuick)
|
||||||
|
_interface.value = InterfaceProxy.from(config.`interface`)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun emitPeersFromConfig() {
|
||||||
|
val config = TunnelConfig.configFromQuick(tunnelConfig.wgQuick)
|
||||||
|
config.peers.forEach {
|
||||||
|
_proxyPeers.value.add(PeerProxy.from(it))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun emitPeerProxy(peerProxy: PeerProxy) {
|
||||||
|
_proxyPeers.value.add(peerProxy)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun emitInterfaceProxy(interfaceProxy: InterfaceProxy) {
|
||||||
|
_interface.value = interfaceProxy
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun getTunnelConfigById(id: String): TunnelConfig? {
|
||||||
return try {
|
return try {
|
||||||
if(id != null) {
|
tunnelRepo.getById(id.toLong())
|
||||||
val config = tunnelRepo.getById(id.toLong())
|
} catch (_: Exception) {
|
||||||
if (config != null) {
|
|
||||||
_tunnel.emit(config)
|
|
||||||
_tunnelName.emit(config.name)
|
|
||||||
|
|
||||||
}
|
|
||||||
return config
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
} catch (e : Exception) {
|
|
||||||
Timber.e(e.message)
|
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private suspend fun emitTunnelConfig() {
|
||||||
|
_tunnel.emit(tunnelConfig)
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun emitTunnelConfigName() {
|
||||||
|
_tunnelName.emit(tunnelConfig.name)
|
||||||
|
}
|
||||||
|
|
||||||
fun onTunnelNameChange(name: String) {
|
fun onTunnelNameChange(name: String) {
|
||||||
_tunnelName.value = name
|
_tunnelName.value = name
|
||||||
}
|
}
|
||||||
@@ -65,45 +147,82 @@ class ConfigViewModel @Inject constructor(private val application : Application,
|
|||||||
fun onIncludeChange(include: Boolean) {
|
fun onIncludeChange(include: Boolean) {
|
||||||
_include.value = include
|
_include.value = include
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onAddCheckedPackage(packageName: String) {
|
fun onAddCheckedPackage(packageName: String) {
|
||||||
_checkedPackages.value.add(packageName)
|
_checkedPackages.value.add(packageName)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onAllApplicationsChange(allApplications : Boolean) {
|
fun onAllApplicationsChange(isAllApplicationsEnabled: Boolean) {
|
||||||
_allApplications.value = allApplications
|
_isAllApplicationsEnabled.value = isAllApplicationsEnabled
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onRemoveCheckedPackage(packageName: String) {
|
fun onRemoveCheckedPackage(packageName: String) {
|
||||||
_checkedPackages.value.remove(packageName)
|
_checkedPackages.value.remove(packageName)
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun emitCurrentPackageConfigurations(id : String?) {
|
private suspend fun emitSplitTunnelConfiguration(config: Config) {
|
||||||
val tunnelConfig = getTunnelById(id)
|
|
||||||
if(tunnelConfig != null) {
|
|
||||||
val config = TunnelConfig.configFromQuick(tunnelConfig.wgQuick)
|
|
||||||
val excludedApps = config.`interface`.excludedApplications
|
val excludedApps = config.`interface`.excludedApplications
|
||||||
val includedApps = config.`interface`.includedApplications
|
val includedApps = config.`interface`.includedApplications
|
||||||
if(excludedApps.isNullOrEmpty() && includedApps.isNullOrEmpty()) {
|
if (excludedApps.isNotEmpty() || includedApps.isNotEmpty()) {
|
||||||
_allApplications.emit(true)
|
emitTunnelAllApplicationsDisabled()
|
||||||
return
|
determineAppInclusionState(excludedApps, includedApps)
|
||||||
}
|
|
||||||
if(excludedApps.isEmpty()) {
|
|
||||||
_include.emit(true)
|
|
||||||
_checkedPackages.emit(includedApps.toMutableStateList())
|
|
||||||
} else {
|
} else {
|
||||||
_include.emit(false)
|
emitTunnelAllApplicationsEnabled()
|
||||||
_checkedPackages.emit(excludedApps.toMutableStateList())
|
|
||||||
}
|
}
|
||||||
_allApplications.emit(false)
|
}
|
||||||
|
|
||||||
|
private suspend fun determineAppInclusionState(
|
||||||
|
excludedApps: Set<String>,
|
||||||
|
includedApps: Set<String>
|
||||||
|
) {
|
||||||
|
if (excludedApps.isEmpty()) {
|
||||||
|
emitIncludedAppsExist()
|
||||||
|
emitCheckedApps(includedApps)
|
||||||
|
} else {
|
||||||
|
emitExcludedAppsExist()
|
||||||
|
emitCheckedApps(excludedApps)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun emitIncludedAppsExist() {
|
||||||
|
_include.emit(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun emitExcludedAppsExist() {
|
||||||
|
_include.emit(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun emitCheckedApps(apps: Set<String>) {
|
||||||
|
_checkedPackages.emit(apps.toMutableStateList())
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun emitTunnelAllApplicationsEnabled() {
|
||||||
|
_isAllApplicationsEnabled.emit(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun emitTunnelAllApplicationsDisabled() {
|
||||||
|
_isAllApplicationsEnabled.emit(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun emitCurrentPackageConfigurations() {
|
||||||
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
|
val config = TunnelConfig.configFromQuick(tunnelConfig.wgQuick)
|
||||||
|
emitSplitTunnelConfiguration(config)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun emitQueriedPackages(query: String) {
|
fun emitQueriedPackages(query: String) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
_packages.emit(getAllInternetCapablePackages().filter {
|
val packages =
|
||||||
it.packageName.contains(query)
|
getAllInternetCapablePackages().filter {
|
||||||
})
|
getPackageLabel(it).lowercase().contains(query.lowercase())
|
||||||
}
|
}
|
||||||
|
_packages.emit(packages)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getPackageLabel(packageInfo: PackageInfo): String {
|
||||||
|
return packageInfo.applicationInfo.loadLabel(application.packageManager).toString()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getAllInternetCapablePackages(): List<PackageInfo> {
|
private fun getAllInternetCapablePackages(): List<PackageInfo> {
|
||||||
@@ -112,41 +231,209 @@ class ConfigViewModel @Inject constructor(private val application : Application,
|
|||||||
|
|
||||||
private fun getPackagesHoldingPermissions(permissions: Array<String>): List<PackageInfo> {
|
private fun getPackagesHoldingPermissions(permissions: Array<String>): List<PackageInfo> {
|
||||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||||
packageManager.getPackagesHoldingPermissions(permissions, PackageManager.PackageInfoFlags.of(0L))
|
packageManager.getPackagesHoldingPermissions(
|
||||||
|
permissions,
|
||||||
|
PackageManager.PackageInfoFlags.of(0L)
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
@Suppress("DEPRECATION")
|
|
||||||
packageManager.getPackagesHoldingPermissions(permissions, 0)
|
packageManager.getPackagesHoldingPermissions(permissions, 0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun onSaveAllChanges() {
|
private fun isAllApplicationsEnabled(): Boolean {
|
||||||
var wgQuick = _tunnel.value?.wgQuick
|
return _isAllApplicationsEnabled.value
|
||||||
if(wgQuick != null) {
|
|
||||||
wgQuick = if(_include.value) {
|
|
||||||
TunnelConfig.setIncludedApplicationsOnQuick(_checkedPackages.value, wgQuick)
|
|
||||||
} else {
|
|
||||||
TunnelConfig.setExcludedApplicationsOnQuick(_checkedPackages.value, wgQuick)
|
|
||||||
}
|
}
|
||||||
if(_allApplications.value) {
|
|
||||||
wgQuick = TunnelConfig.clearAllApplicationsFromConfig(wgQuick)
|
private suspend fun saveConfig(tunnelConfig: TunnelConfig) {
|
||||||
|
tunnelRepo.save(tunnelConfig)
|
||||||
}
|
}
|
||||||
_tunnel.value?.copy(
|
|
||||||
name = _tunnelName.value,
|
private suspend fun updateTunnelConfig(tunnelConfig: TunnelConfig?) {
|
||||||
wgQuick = wgQuick
|
if (tunnelConfig != null) {
|
||||||
)?.let {
|
saveConfig(tunnelConfig)
|
||||||
tunnelRepo.save(it)
|
updateSettingsDefaultTunnel(tunnelConfig)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun updateSettingsDefaultTunnel(tunnelConfig: TunnelConfig) {
|
||||||
val settings = settingsRepo.getAll()
|
val settings = settingsRepo.getAll()
|
||||||
if(settings != null) {
|
if (settings.isNotEmpty()) {
|
||||||
val setting = settings[0]
|
val setting = settings[0]
|
||||||
if (setting.defaultTunnel != null) {
|
if (setting.defaultTunnel != null) {
|
||||||
if(it.id == TunnelConfig.from(setting.defaultTunnel!!).id) {
|
if (tunnelConfig.id == TunnelConfig.from(setting.defaultTunnel!!).id) {
|
||||||
settingsRepo.save(setting.copy(
|
settingsRepo.save(
|
||||||
defaultTunnel = it.toString()
|
setting.copy(
|
||||||
))
|
defaultTunnel = tunnelConfig.toString()
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun buildPeerListFromProxyPeers(): List<Peer> {
|
||||||
|
return _proxyPeers.value.map {
|
||||||
|
val builder = Peer.Builder()
|
||||||
|
if (it.allowedIps.isNotEmpty()) builder.parseAllowedIPs(it.allowedIps.trim())
|
||||||
|
if (it.publicKey.isNotEmpty()) builder.parsePublicKey(it.publicKey.trim())
|
||||||
|
if (it.preSharedKey.isNotEmpty()) builder.parsePreSharedKey(it.preSharedKey.trim())
|
||||||
|
if (it.endpoint.isNotEmpty()) builder.parseEndpoint(it.endpoint.trim())
|
||||||
|
if (it.persistentKeepalive.isNotEmpty()) {
|
||||||
|
builder.parsePersistentKeepalive(
|
||||||
|
it.persistentKeepalive.trim()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
builder.build()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun buildInterfaceListFromProxyInterface(): Interface {
|
||||||
|
val builder = Interface.Builder()
|
||||||
|
builder.parsePrivateKey(_interface.value.privateKey.trim())
|
||||||
|
builder.parseAddresses(_interface.value.addresses.trim())
|
||||||
|
builder.parseDnsServers(_interface.value.dnsServers.trim())
|
||||||
|
if (_interface.value.mtu.isNotEmpty()) builder.parseMtu(_interface.value.mtu.trim())
|
||||||
|
if (_interface.value.listenPort.isNotEmpty()) {
|
||||||
|
builder.parseListenPort(
|
||||||
|
_interface.value.listenPort.trim()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (isAllApplicationsEnabled()) _checkedPackages.value.clear()
|
||||||
|
if (_include.value) builder.includeApplications(_checkedPackages.value)
|
||||||
|
if (!_include.value) builder.excludeApplications(_checkedPackages.value)
|
||||||
|
return builder.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun onSaveAllChanges() {
|
||||||
|
try {
|
||||||
|
val peerList = buildPeerListFromProxyPeers()
|
||||||
|
val wgInterface = buildInterfaceListFromProxyInterface()
|
||||||
|
val config = Config.Builder().addPeers(peerList).setInterface(wgInterface).build()
|
||||||
|
val tunnelConfig =
|
||||||
|
_tunnel.value?.copy(
|
||||||
|
name = _tunnelName.value,
|
||||||
|
wgQuick = config.toWgQuickString()
|
||||||
|
)
|
||||||
|
updateTunnelConfig(tunnelConfig)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
throw WgTunnelException(
|
||||||
|
"Error: ${e.cause?.message?.lowercase() ?: "unknown error occurred"}"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onPeerPublicKeyChange(
|
||||||
|
index: Int,
|
||||||
|
publicKey: String
|
||||||
|
) {
|
||||||
|
_proxyPeers.value[index] =
|
||||||
|
_proxyPeers.value[index].copy(
|
||||||
|
publicKey = publicKey
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onPreSharedKeyChange(
|
||||||
|
index: Int,
|
||||||
|
value: String
|
||||||
|
) {
|
||||||
|
_proxyPeers.value[index] =
|
||||||
|
_proxyPeers.value[index].copy(
|
||||||
|
preSharedKey = value
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onEndpointChange(
|
||||||
|
index: Int,
|
||||||
|
value: String
|
||||||
|
) {
|
||||||
|
_proxyPeers.value[index] =
|
||||||
|
_proxyPeers.value[index].copy(
|
||||||
|
endpoint = value
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onAllowedIpsChange(
|
||||||
|
index: Int,
|
||||||
|
value: String
|
||||||
|
) {
|
||||||
|
_proxyPeers.value[index] =
|
||||||
|
_proxyPeers.value[index].copy(
|
||||||
|
allowedIps = value
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onPersistentKeepaliveChanged(
|
||||||
|
index: Int,
|
||||||
|
value: String
|
||||||
|
) {
|
||||||
|
_proxyPeers.value[index] =
|
||||||
|
_proxyPeers.value[index].copy(
|
||||||
|
persistentKeepalive = value
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onDeletePeer(index: Int) {
|
||||||
|
proxyPeers.value.removeAt(index)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun addEmptyPeer() {
|
||||||
|
_proxyPeers.value.add(PeerProxy())
|
||||||
|
}
|
||||||
|
|
||||||
|
fun generateKeyPair() {
|
||||||
|
val keyPair = KeyPair()
|
||||||
|
_interface.value =
|
||||||
|
_interface.value.copy(
|
||||||
|
privateKey = keyPair.privateKey.toBase64(),
|
||||||
|
publicKey = keyPair.publicKey.toBase64()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onAddressesChanged(value: String) {
|
||||||
|
_interface.value =
|
||||||
|
_interface.value.copy(
|
||||||
|
addresses = value
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onListenPortChanged(value: String) {
|
||||||
|
_interface.value =
|
||||||
|
_interface.value.copy(
|
||||||
|
listenPort = value
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onDnsServersChanged(value: String) {
|
||||||
|
_interface.value =
|
||||||
|
_interface.value.copy(
|
||||||
|
dnsServers = value
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onMtuChanged(value: String) {
|
||||||
|
_interface.value =
|
||||||
|
_interface.value.copy(
|
||||||
|
mtu = value
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onInterfacePublicKeyChange(value: String) {
|
||||||
|
_interface.value =
|
||||||
|
_interface.value.copy(
|
||||||
|
publicKey = value
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onPrivateKeyChange(value: String) {
|
||||||
|
_interface.value =
|
||||||
|
_interface.value.copy(
|
||||||
|
privateKey = value
|
||||||
|
)
|
||||||
|
if (NumberUtils.isValidKey(value)) {
|
||||||
|
val pair = KeyPair(Key.fromBase64(value))
|
||||||
|
onInterfacePublicKeyChange(pair.publicKey.toBase64())
|
||||||
|
} else {
|
||||||
|
onInterfacePublicKeyChange("")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
-139
@@ -1,139 +0,0 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.ui.screens.detail
|
|
||||||
|
|
||||||
import androidx.compose.foundation.clickable
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
|
||||||
import androidx.compose.foundation.layout.Box
|
|
||||||
import androidx.compose.foundation.layout.Column
|
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
|
||||||
import androidx.compose.foundation.layout.Row
|
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.foundation.rememberScrollState
|
|
||||||
import androidx.compose.foundation.verticalScroll
|
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.platform.ClipboardManager
|
|
||||||
import androidx.compose.ui.platform.LocalClipboardManager
|
|
||||||
import androidx.compose.ui.res.stringResource
|
|
||||||
import androidx.compose.ui.text.AnnotatedString
|
|
||||||
import androidx.compose.ui.text.font.FontStyle
|
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import androidx.compose.ui.unit.sp
|
|
||||||
import androidx.hilt.navigation.compose.hiltViewModel
|
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
|
||||||
import com.zaneschepke.wireguardautotunnel.R
|
|
||||||
import com.zaneschepke.wireguardautotunnel.util.NumberUtils
|
|
||||||
import java.time.Duration
|
|
||||||
import java.time.Instant
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun DetailScreen(
|
|
||||||
viewModel: DetailViewModel = hiltViewModel(),
|
|
||||||
padding: PaddingValues,
|
|
||||||
id : String?
|
|
||||||
) {
|
|
||||||
|
|
||||||
val clipboardManager: ClipboardManager = LocalClipboardManager.current
|
|
||||||
val tunnelStats by viewModel.tunnelStats.collectAsStateWithLifecycle(null)
|
|
||||||
val tunnel by viewModel.tunnel.collectAsStateWithLifecycle(null)
|
|
||||||
val tunnelName by viewModel.tunnelName.collectAsStateWithLifecycle()
|
|
||||||
val lastHandshake by viewModel.lastHandshake.collectAsStateWithLifecycle(emptyMap())
|
|
||||||
|
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
|
||||||
viewModel.getTunnelById(id)
|
|
||||||
}
|
|
||||||
|
|
||||||
if(tunnel != null) {
|
|
||||||
val interfaceKey = tunnel?.`interface`?.keyPair?.publicKey?.toBase64().toString()
|
|
||||||
val addresses = tunnel?.`interface`?.addresses!!.joinToString()
|
|
||||||
val dnsServers = tunnel?.`interface`?.dnsServers!!.joinToString()
|
|
||||||
val optionalMtu = tunnel?.`interface`?.mtu
|
|
||||||
val mtu = if(optionalMtu?.isPresent == true) optionalMtu.get().toString() else "None"
|
|
||||||
Column(
|
|
||||||
horizontalAlignment = Alignment.Start,
|
|
||||||
verticalArrangement = Arrangement.Top,
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxSize()
|
|
||||||
.verticalScroll(rememberScrollState())
|
|
||||||
.padding(padding)
|
|
||||||
) {
|
|
||||||
Row(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(horizontal = 20.dp, vertical = 7.dp),
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
horizontalArrangement = Arrangement.SpaceBetween
|
|
||||||
) {
|
|
||||||
Column {
|
|
||||||
Text(stringResource(R.string.config_interface), fontWeight = FontWeight.Bold, fontSize = 20.sp)
|
|
||||||
Text(stringResource(R.string.name), fontStyle = FontStyle.Italic)
|
|
||||||
Text(text = tunnelName, modifier = Modifier.clickable {
|
|
||||||
clipboardManager.setText(AnnotatedString(tunnelName))
|
|
||||||
})
|
|
||||||
Text(stringResource(R.string.public_key), fontStyle = FontStyle.Italic)
|
|
||||||
Text(text = interfaceKey, modifier = Modifier.clickable {
|
|
||||||
clipboardManager.setText(AnnotatedString(interfaceKey))
|
|
||||||
})
|
|
||||||
Text(stringResource(R.string.addresses), fontStyle = FontStyle.Italic)
|
|
||||||
Text(text = addresses, modifier = Modifier.clickable {
|
|
||||||
clipboardManager.setText(AnnotatedString(addresses))
|
|
||||||
})
|
|
||||||
Text(stringResource(R.string.dns_servers), fontStyle = FontStyle.Italic)
|
|
||||||
Text(text = dnsServers, modifier = Modifier.clickable {
|
|
||||||
clipboardManager.setText(AnnotatedString(dnsServers))
|
|
||||||
})
|
|
||||||
Text(stringResource(R.string.mtu), fontStyle = FontStyle.Italic)
|
|
||||||
Text(text = mtu, modifier = Modifier.clickable {
|
|
||||||
clipboardManager.setText(AnnotatedString(mtu))
|
|
||||||
})
|
|
||||||
Box(modifier = Modifier.padding(10.dp))
|
|
||||||
tunnel?.peers?.forEach{
|
|
||||||
val peerKey = it.publicKey.toBase64().toString()
|
|
||||||
val allowedIps = it.allowedIps.joinToString()
|
|
||||||
val endpoint = if(it.endpoint.isPresent) it.endpoint.get().toString() else "None"
|
|
||||||
Text(stringResource(R.string.peer), fontWeight = FontWeight.Bold, fontSize = 20.sp)
|
|
||||||
Text(stringResource(R.string.public_key), fontStyle = FontStyle.Italic)
|
|
||||||
Text(text = peerKey, modifier = Modifier.clickable {
|
|
||||||
clipboardManager.setText(AnnotatedString(peerKey))
|
|
||||||
})
|
|
||||||
Text(stringResource(id = R.string.allowed_ips), fontStyle = FontStyle.Italic)
|
|
||||||
Text(text = allowedIps, modifier = Modifier.clickable {
|
|
||||||
clipboardManager.setText(AnnotatedString(allowedIps))
|
|
||||||
})
|
|
||||||
Text(stringResource(R.string.endpoint), fontStyle = FontStyle.Italic)
|
|
||||||
Text(text = endpoint, modifier = Modifier.clickable {
|
|
||||||
clipboardManager.setText(AnnotatedString(endpoint))
|
|
||||||
})
|
|
||||||
if (tunnelStats != null) {
|
|
||||||
val totalRx = tunnelStats?.totalRx() ?: 0
|
|
||||||
val totalTx = tunnelStats?.totalTx() ?: 0
|
|
||||||
if((totalRx + totalTx != 0L)) {
|
|
||||||
val rxKB = NumberUtils.bytesToKB(tunnelStats!!.totalRx())
|
|
||||||
val txKB = NumberUtils.bytesToKB(tunnelStats!!.totalTx())
|
|
||||||
Text(stringResource(R.string.transfer), fontStyle = FontStyle.Italic)
|
|
||||||
Text("rx: ${NumberUtils.formatDecimalTwoPlaces(rxKB)} KB tx: ${NumberUtils.formatDecimalTwoPlaces(txKB)} KB")
|
|
||||||
Text(stringResource(R.string.last_handshake), fontStyle = FontStyle.Italic)
|
|
||||||
val handshakeEpoch = lastHandshake[it.publicKey]
|
|
||||||
if(handshakeEpoch != null) {
|
|
||||||
if(handshakeEpoch == 0L) {
|
|
||||||
Text("Never")
|
|
||||||
} else {
|
|
||||||
val time = Instant.ofEpochMilli(handshakeEpoch)
|
|
||||||
Text("${Duration.between(time, Instant.now()).seconds} seconds ago")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-45
@@ -1,45 +0,0 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.ui.screens.detail
|
|
||||||
|
|
||||||
import androidx.lifecycle.ViewModel
|
|
||||||
import com.wireguard.config.Config
|
|
||||||
import com.zaneschepke.wireguardautotunnel.repository.Repository
|
|
||||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnService
|
|
||||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.model.TunnelConfig
|
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
|
||||||
import timber.log.Timber
|
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
@HiltViewModel
|
|
||||||
class DetailViewModel @Inject constructor(private val tunnelRepo : Repository<TunnelConfig>, private val vpnService : VpnService
|
|
||||||
|
|
||||||
) : ViewModel() {
|
|
||||||
|
|
||||||
private val _tunnel = MutableStateFlow<Config?>(null)
|
|
||||||
val tunnel get() = _tunnel.asStateFlow()
|
|
||||||
|
|
||||||
private val _tunnelName = MutableStateFlow<String>("")
|
|
||||||
val tunnelName = _tunnelName.asStateFlow()
|
|
||||||
val tunnelStats get() = vpnService.statistics
|
|
||||||
val lastHandshake get() = vpnService.lastHandshake
|
|
||||||
|
|
||||||
private var config : TunnelConfig? = null
|
|
||||||
|
|
||||||
suspend fun getTunnelById(id : String?) : TunnelConfig? {
|
|
||||||
return try {
|
|
||||||
if(id != null) {
|
|
||||||
config = tunnelRepo.getById(id.toLong())
|
|
||||||
if (config != null) {
|
|
||||||
_tunnel.emit(TunnelConfig.configFromQuick(config!!.wgQuick))
|
|
||||||
_tunnelName.emit(config!!.name)
|
|
||||||
}
|
|
||||||
return config
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
} catch (e : Exception) {
|
|
||||||
Timber.e(e.message)
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+328
-81
@@ -1,6 +1,10 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.ui.screens.main
|
package com.zaneschepke.wireguardautotunnel.ui.screens.main
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.pm.PackageManager
|
||||||
|
import android.os.Build
|
||||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.compose.animation.AnimatedVisibility
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
@@ -16,11 +20,12 @@ import androidx.compose.foundation.layout.Row
|
|||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.items
|
||||||
import androidx.compose.foundation.lazy.itemsIndexed
|
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.Create
|
||||||
import androidx.compose.material.icons.filled.FileOpen
|
import androidx.compose.material.icons.filled.FileOpen
|
||||||
import androidx.compose.material.icons.filled.QrCode
|
import androidx.compose.material.icons.filled.QrCode
|
||||||
import androidx.compose.material.icons.rounded.Add
|
import androidx.compose.material.icons.rounded.Add
|
||||||
@@ -28,6 +33,8 @@ import androidx.compose.material.icons.rounded.Circle
|
|||||||
import androidx.compose.material.icons.rounded.Delete
|
import androidx.compose.material.icons.rounded.Delete
|
||||||
import androidx.compose.material.icons.rounded.Edit
|
import androidx.compose.material.icons.rounded.Edit
|
||||||
import androidx.compose.material.icons.rounded.Info
|
import androidx.compose.material.icons.rounded.Info
|
||||||
|
import androidx.compose.material.icons.rounded.Star
|
||||||
|
import androidx.compose.material3.AlertDialog
|
||||||
import androidx.compose.material3.Divider
|
import androidx.compose.material3.Divider
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.FabPosition
|
import androidx.compose.material3.FabPosition
|
||||||
@@ -37,14 +44,12 @@ import androidx.compose.material3.IconButton
|
|||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.ModalBottomSheet
|
import androidx.compose.material3.ModalBottomSheet
|
||||||
import androidx.compose.material3.Scaffold
|
import androidx.compose.material3.Scaffold
|
||||||
import androidx.compose.material3.SnackbarDuration
|
|
||||||
import androidx.compose.material3.SnackbarHostState
|
|
||||||
import androidx.compose.material3.SnackbarResult
|
|
||||||
import androidx.compose.material3.Switch
|
import androidx.compose.material3.Switch
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TextButton
|
||||||
import androidx.compose.material3.rememberModalBottomSheetState
|
import androidx.compose.material3.rememberModalBottomSheetState
|
||||||
|
import androidx.compose.material3.surfaceColorAtElevation
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
@@ -55,6 +60,7 @@ import androidx.compose.ui.Alignment
|
|||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.focus.FocusRequester
|
import androidx.compose.ui.focus.FocusRequester
|
||||||
import androidx.compose.ui.focus.focusRequester
|
import androidx.compose.ui.focus.focusRequester
|
||||||
|
import androidx.compose.ui.focus.onFocusChanged
|
||||||
import androidx.compose.ui.geometry.Offset
|
import androidx.compose.ui.geometry.Offset
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
|
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
|
||||||
@@ -70,43 +76,59 @@ import androidx.compose.ui.unit.dp
|
|||||||
import androidx.hilt.navigation.compose.hiltViewModel
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import androidx.navigation.NavController
|
import androidx.navigation.NavController
|
||||||
|
import com.journeyapps.barcodescanner.ScanContract
|
||||||
|
import com.journeyapps.barcodescanner.ScanOptions
|
||||||
import com.wireguard.android.backend.Tunnel
|
import com.wireguard.android.backend.Tunnel
|
||||||
|
import com.zaneschepke.wireguardautotunnel.Constants
|
||||||
import com.zaneschepke.wireguardautotunnel.R
|
import com.zaneschepke.wireguardautotunnel.R
|
||||||
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
|
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
|
||||||
|
import com.zaneschepke.wireguardautotunnel.repository.model.TunnelConfig
|
||||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.HandshakeStatus
|
import com.zaneschepke.wireguardautotunnel.service.tunnel.HandshakeStatus
|
||||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.model.TunnelConfig
|
import com.zaneschepke.wireguardautotunnel.ui.CaptureActivityPortrait
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.Routes
|
import com.zaneschepke.wireguardautotunnel.ui.Routes
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.common.RowListItem
|
import com.zaneschepke.wireguardautotunnel.ui.common.RowListItem
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.theme.brickRed
|
import com.zaneschepke.wireguardautotunnel.ui.theme.brickRed
|
||||||
|
import com.zaneschepke.wireguardautotunnel.ui.theme.corn
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.theme.mint
|
import com.zaneschepke.wireguardautotunnel.ui.theme.mint
|
||||||
|
import com.zaneschepke.wireguardautotunnel.util.WgTunnelException
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter")
|
@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter")
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun MainScreen(
|
fun MainScreen(
|
||||||
viewModel: MainViewModel = hiltViewModel(), padding: PaddingValues,
|
viewModel: MainViewModel = hiltViewModel(),
|
||||||
snackbarHostState: SnackbarHostState, navController: NavController
|
padding: PaddingValues,
|
||||||
|
showSnackbarMessage: (String) -> Unit,
|
||||||
|
navController: NavController
|
||||||
) {
|
) {
|
||||||
|
|
||||||
val haptic = LocalHapticFeedback.current
|
val haptic = LocalHapticFeedback.current
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val isVisible = rememberSaveable { mutableStateOf(true) }
|
val isVisible = rememberSaveable { mutableStateOf(true) }
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope { Dispatchers.IO }
|
||||||
|
|
||||||
val sheetState = rememberModalBottomSheetState()
|
val sheetState = rememberModalBottomSheetState()
|
||||||
var showBottomSheet by remember { mutableStateOf(false) }
|
var showBottomSheet by remember { mutableStateOf(false) }
|
||||||
|
var showPrimaryChangeAlertDialog by remember { mutableStateOf(false) }
|
||||||
val tunnels by viewModel.tunnels.collectAsStateWithLifecycle(mutableListOf())
|
val tunnels by viewModel.tunnels.collectAsStateWithLifecycle(mutableListOf())
|
||||||
val handshakeStatus by viewModel.handshakeStatus.collectAsStateWithLifecycle(HandshakeStatus.NOT_STARTED)
|
val handshakeStatus by viewModel.handshakeStatus.collectAsStateWithLifecycle(
|
||||||
val viewState = viewModel.viewState.collectAsStateWithLifecycle()
|
HandshakeStatus.NOT_STARTED
|
||||||
|
)
|
||||||
var selectedTunnel by remember { mutableStateOf<TunnelConfig?>(null) }
|
var selectedTunnel by remember { mutableStateOf<TunnelConfig?>(null) }
|
||||||
val state by viewModel.state.collectAsStateWithLifecycle(Tunnel.State.DOWN)
|
val state by viewModel.state.collectAsStateWithLifecycle(Tunnel.State.DOWN)
|
||||||
val tunnelName by viewModel.tunnelName.collectAsStateWithLifecycle("")
|
val tunnelName by viewModel.tunnelName.collectAsStateWithLifecycle("")
|
||||||
|
val settings by viewModel.settings.collectAsStateWithLifecycle()
|
||||||
|
val statistics by viewModel.statistics.collectAsStateWithLifecycle(null)
|
||||||
|
|
||||||
// Nested scroll for control FAB
|
// Nested scroll for control FAB
|
||||||
val nestedScrollConnection = remember {
|
val nestedScrollConnection =
|
||||||
|
remember {
|
||||||
object : NestedScrollConnection {
|
object : NestedScrollConnection {
|
||||||
override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
|
override fun onPreScroll(
|
||||||
|
available: Offset,
|
||||||
|
source: NestedScrollSource
|
||||||
|
): Offset {
|
||||||
// Hide FAB
|
// Hide FAB
|
||||||
if (available.y < -1) {
|
if (available.y < -1) {
|
||||||
isVisible.value = false
|
isVisible.value = false
|
||||||
@@ -120,30 +142,114 @@ fun MainScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
LaunchedEffect(viewState.value) {
|
val tunnelFileImportResultLauncher =
|
||||||
if (viewState.value.showSnackbarMessage) {
|
rememberLauncherForActivityResult(
|
||||||
val result = snackbarHostState.showSnackbar(
|
object : ActivityResultContracts.GetContent() {
|
||||||
message = viewState.value.snackbarMessage,
|
override fun createIntent(
|
||||||
actionLabel = viewState.value.snackbarActionText,
|
context: Context,
|
||||||
duration = SnackbarDuration.Long,
|
input: String
|
||||||
|
): Intent {
|
||||||
|
val intent = super.createIntent(context, input)
|
||||||
|
|
||||||
|
/* AndroidTV now comes with stubs that do nothing but display a Toast less helpful than
|
||||||
|
* what we can do, so detect this and throw an exception that we can catch later. */
|
||||||
|
val activitiesToResolveIntent =
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||||
|
context.packageManager.queryIntentActivities(
|
||||||
|
intent,
|
||||||
|
PackageManager.ResolveInfoFlags.of(
|
||||||
|
PackageManager.MATCH_DEFAULT_ONLY.toLong()
|
||||||
)
|
)
|
||||||
when (result) {
|
)
|
||||||
SnackbarResult.ActionPerformed -> viewState.value.onSnackbarActionClick
|
} else {
|
||||||
SnackbarResult.Dismissed -> viewState.value.onSnackbarActionClick
|
context.packageManager.queryIntentActivities(
|
||||||
|
intent,
|
||||||
|
PackageManager.MATCH_DEFAULT_ONLY
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (activitiesToResolveIntent.all {
|
||||||
|
val name = it.activityInfo.packageName
|
||||||
|
name.startsWith(Constants.GOOGLE_TV_EXPLORER_STUB) || name.startsWith(
|
||||||
|
Constants.ANDROID_TV_EXPLORER_STUB
|
||||||
|
)
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
throw WgTunnelException(context.getString(R.string.no_file_explorer))
|
||||||
|
}
|
||||||
|
return intent
|
||||||
|
}
|
||||||
|
}
|
||||||
|
) { data ->
|
||||||
|
if (data == null) return@rememberLauncherForActivityResult
|
||||||
|
scope.launch(Dispatchers.IO) {
|
||||||
|
try {
|
||||||
|
viewModel.onTunnelFileSelected(data)
|
||||||
|
} catch (e: WgTunnelException) {
|
||||||
|
showSnackbarMessage(e.message)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val pickFileLauncher = rememberLauncherForActivityResult(
|
val scanLauncher =
|
||||||
ActivityResultContracts.GetContent()
|
rememberLauncherForActivityResult(
|
||||||
) { file ->
|
contract = ScanContract(),
|
||||||
if (file != null) {
|
onResult = {
|
||||||
viewModel.onTunnelFileSelected(file)
|
scope.launch {
|
||||||
|
try {
|
||||||
|
viewModel.onTunnelQrResult(it.contents)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
when (e) {
|
||||||
|
is WgTunnelException -> {
|
||||||
|
showSnackbarMessage(e.message)
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> {
|
||||||
|
showSnackbarMessage("No QR code scanned")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
if (showPrimaryChangeAlertDialog) {
|
||||||
|
AlertDialog(
|
||||||
|
onDismissRequest = {
|
||||||
|
showPrimaryChangeAlertDialog = false
|
||||||
|
},
|
||||||
|
confirmButton = {
|
||||||
|
TextButton(onClick = {
|
||||||
|
scope.launch {
|
||||||
|
viewModel.onDefaultTunnelChange(selectedTunnel)
|
||||||
|
showPrimaryChangeAlertDialog = false
|
||||||
|
selectedTunnel = null
|
||||||
|
}
|
||||||
|
}) { Text(text = stringResource(R.string.okay)) }
|
||||||
|
},
|
||||||
|
dismissButton = {
|
||||||
|
TextButton(onClick = {
|
||||||
|
showPrimaryChangeAlertDialog = false
|
||||||
|
}) { Text(text = stringResource(R.string.cancel)) }
|
||||||
|
},
|
||||||
|
title = { Text(text = stringResource(R.string.primary_tunnel_change)) },
|
||||||
|
text = { Text(text = stringResource(R.string.primary_tunnel_change_question)) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onTunnelToggle(
|
||||||
|
checked: Boolean,
|
||||||
|
tunnel: TunnelConfig
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
if (checked) viewModel.onTunnelStart(tunnel) else viewModel.onTunnelStop()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
showSnackbarMessage(e.message!!)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
modifier = Modifier.pointerInput(Unit) {
|
modifier =
|
||||||
|
Modifier.pointerInput(Unit) {
|
||||||
detectTapGestures(onTap = {
|
detectTapGestures(onTap = {
|
||||||
selectedTunnel = null
|
selectedTunnel = null
|
||||||
})
|
})
|
||||||
@@ -153,20 +259,30 @@ fun MainScreen(
|
|||||||
AnimatedVisibility(
|
AnimatedVisibility(
|
||||||
visible = isVisible.value,
|
visible = isVisible.value,
|
||||||
enter = slideInVertically(initialOffsetY = { it * 2 }),
|
enter = slideInVertically(initialOffsetY = { it * 2 }),
|
||||||
exit = slideOutVertically(targetOffsetY = { it * 2 }),
|
exit = slideOutVertically(targetOffsetY = { it * 2 })
|
||||||
) {
|
) {
|
||||||
|
val secondaryColor = MaterialTheme.colorScheme.secondary
|
||||||
|
val hoverColor = MaterialTheme.colorScheme.surfaceColorAtElevation(2.dp)
|
||||||
|
var fobColor by remember { mutableStateOf(secondaryColor) }
|
||||||
FloatingActionButton(
|
FloatingActionButton(
|
||||||
modifier = Modifier.padding(bottom = 90.dp),
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.padding(bottom = 90.dp)
|
||||||
|
.onFocusChanged {
|
||||||
|
if (WireGuardAutoTunnel.isRunningOnAndroidTv(context)) {
|
||||||
|
fobColor = if (it.isFocused) hoverColor else secondaryColor
|
||||||
|
}
|
||||||
|
},
|
||||||
onClick = {
|
onClick = {
|
||||||
showBottomSheet = true
|
showBottomSheet = true
|
||||||
},
|
},
|
||||||
containerColor = MaterialTheme.colorScheme.secondary,
|
containerColor = fobColor,
|
||||||
shape = RoundedCornerShape(16.dp),
|
shape = RoundedCornerShape(16.dp)
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Rounded.Add,
|
imageVector = Icons.Rounded.Add,
|
||||||
contentDescription = stringResource(id = R.string.add_tunnel),
|
contentDescription = stringResource(id = R.string.add_tunnel),
|
||||||
tint = Color.DarkGray,
|
tint = Color.DarkGray
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -176,7 +292,8 @@ fun MainScreen(
|
|||||||
Column(
|
Column(
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
verticalArrangement = Arrangement.Center,
|
verticalArrangement = Arrangement.Center,
|
||||||
modifier = Modifier
|
modifier =
|
||||||
|
Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.padding(padding)
|
.padding(padding)
|
||||||
) {
|
) {
|
||||||
@@ -192,11 +309,16 @@ fun MainScreen(
|
|||||||
) {
|
) {
|
||||||
// Sheet content
|
// Sheet content
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier =
|
||||||
|
Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.clickable {
|
.clickable {
|
||||||
showBottomSheet = false
|
showBottomSheet = false
|
||||||
pickFileLauncher.launch("*/*")
|
try {
|
||||||
|
tunnelFileImportResultLauncher.launch(Constants.ALLOWED_FILE_TYPES)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
showSnackbarMessage(e.message!!)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.padding(10.dp)
|
.padding(10.dp)
|
||||||
) {
|
) {
|
||||||
@@ -206,17 +328,27 @@ fun MainScreen(
|
|||||||
modifier = Modifier.padding(10.dp)
|
modifier = Modifier.padding(10.dp)
|
||||||
)
|
)
|
||||||
Text(
|
Text(
|
||||||
stringResource(id = R.string.add_from_file),
|
stringResource(id = R.string.add_tunnels_text),
|
||||||
modifier = Modifier.padding(10.dp)
|
modifier = Modifier.padding(10.dp)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
if (!WireGuardAutoTunnel.isRunningOnAndroidTv(context)) {
|
||||||
Divider()
|
Divider()
|
||||||
Row(modifier = Modifier
|
Row(
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.clickable {
|
.clickable {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
showBottomSheet = false
|
showBottomSheet = false
|
||||||
viewModel.onTunnelQRSelected()
|
val scanOptions = ScanOptions()
|
||||||
|
scanOptions.setDesiredBarcodeFormats(ScanOptions.QR_CODE)
|
||||||
|
scanOptions.setOrientationLocked(true)
|
||||||
|
scanOptions.setPrompt(context.getString(R.string.scanning_qr))
|
||||||
|
scanOptions.setBeepEnabled(false)
|
||||||
|
scanOptions.captureActivity =
|
||||||
|
CaptureActivityPortrait::class.java
|
||||||
|
scanLauncher.launch(scanOptions)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.padding(10.dp)
|
.padding(10.dp)
|
||||||
@@ -232,57 +364,147 @@ fun MainScreen(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Divider()
|
||||||
|
Row(
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.clickable {
|
||||||
|
showBottomSheet = false
|
||||||
|
navController.navigate(
|
||||||
|
"${Routes.Config.name}/${Constants.MANUAL_TUNNEL_CONFIG_ID}"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.padding(10.dp)
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
Icons.Filled.Create,
|
||||||
|
contentDescription = stringResource(id = R.string.create_import),
|
||||||
|
modifier = Modifier.padding(10.dp)
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
stringResource(id = R.string.create_import),
|
||||||
|
modifier = Modifier.padding(10.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Column(
|
Column(
|
||||||
horizontalAlignment = Alignment.Start,
|
horizontalAlignment = Alignment.Start,
|
||||||
verticalArrangement = Arrangement.Top,
|
verticalArrangement = Arrangement.Top,
|
||||||
modifier = Modifier
|
modifier =
|
||||||
|
Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.padding(padding)
|
.padding(padding)
|
||||||
) {
|
) {
|
||||||
|
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
modifier = Modifier.fillMaxSize()
|
modifier =
|
||||||
.nestedScroll(nestedScrollConnection),
|
Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(top = 10.dp)
|
||||||
|
.nestedScroll(nestedScrollConnection)
|
||||||
) {
|
) {
|
||||||
itemsIndexed(tunnels.toList()) { index, tunnel ->
|
items(tunnels, key = { tunnel -> tunnel.id }) { tunnel ->
|
||||||
val focusRequester = FocusRequester();
|
val leadingIconColor = (
|
||||||
RowListItem(leadingIcon = Icons.Rounded.Circle,
|
if (tunnelName == tunnel.name) {
|
||||||
leadingIconColor = if (tunnelName == tunnel.name) when (handshakeStatus) {
|
when (handshakeStatus) {
|
||||||
HandshakeStatus.HEALTHY -> mint
|
HandshakeStatus.HEALTHY -> mint
|
||||||
HandshakeStatus.UNHEALTHY -> brickRed
|
HandshakeStatus.UNHEALTHY -> brickRed
|
||||||
|
HandshakeStatus.STALE -> corn
|
||||||
HandshakeStatus.NOT_STARTED -> Color.Gray
|
HandshakeStatus.NOT_STARTED -> Color.Gray
|
||||||
HandshakeStatus.NEVER_CONNECTED -> brickRed
|
HandshakeStatus.NEVER_CONNECTED -> brickRed
|
||||||
} else Color.Gray,
|
}
|
||||||
|
} else {
|
||||||
|
Color.Gray
|
||||||
|
}
|
||||||
|
)
|
||||||
|
val focusRequester = remember { FocusRequester() }
|
||||||
|
val expanded =
|
||||||
|
remember {
|
||||||
|
mutableStateOf(false)
|
||||||
|
}
|
||||||
|
RowListItem(
|
||||||
|
icon = {
|
||||||
|
if (settings.isTunnelConfigDefault(tunnel)) {
|
||||||
|
Icon(
|
||||||
|
Icons.Rounded.Star,
|
||||||
|
stringResource(R.string.status),
|
||||||
|
tint = leadingIconColor,
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.padding(end = 10.dp)
|
||||||
|
.size(20.dp)
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
Icon(
|
||||||
|
Icons.Rounded.Circle,
|
||||||
|
stringResource(R.string.status),
|
||||||
|
tint = leadingIconColor,
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.padding(end = 15.dp)
|
||||||
|
.size(15.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
text = tunnel.name,
|
text = tunnel.name,
|
||||||
onHold = {
|
onHold = {
|
||||||
if (state == Tunnel.State.UP && tunnel.name == tunnelName) {
|
if ((state == Tunnel.State.UP) && (tunnel.name == tunnelName)) {
|
||||||
scope.launch {
|
showSnackbarMessage(
|
||||||
viewModel.showSnackBarMessage(context.resources.getString(R.string.turn_off_tunnel))
|
context.resources.getString(R.string.turn_off_tunnel)
|
||||||
}
|
)
|
||||||
return@RowListItem
|
return@RowListItem
|
||||||
}
|
}
|
||||||
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
|
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
|
||||||
selectedTunnel = tunnel;
|
selectedTunnel = tunnel
|
||||||
},
|
},
|
||||||
onClick = {
|
onClick = {
|
||||||
if (!WireGuardAutoTunnel.isRunningOnAndroidTv(context)) {
|
if (!WireGuardAutoTunnel.isRunningOnAndroidTv(context)) {
|
||||||
navController.navigate("${Routes.Detail.name}/${tunnel.id}")
|
if (state == Tunnel.State.UP && (tunnelName == tunnel.name)) {
|
||||||
|
expanded.value = !expanded.value
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
|
selectedTunnel = tunnel
|
||||||
focusRequester.requestFocus()
|
focusRequester.requestFocus()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
statistics = statistics,
|
||||||
|
expanded = expanded.value,
|
||||||
rowButton = {
|
rowButton = {
|
||||||
if (tunnel.id == selectedTunnel?.id) {
|
if (tunnel.id == selectedTunnel?.id && !WireGuardAutoTunnel.isRunningOnAndroidTv(
|
||||||
|
context
|
||||||
|
)
|
||||||
|
) {
|
||||||
Row {
|
Row {
|
||||||
|
if (!settings.isTunnelConfigDefault(tunnel)) {
|
||||||
IconButton(onClick = {
|
IconButton(onClick = {
|
||||||
navController.navigate("${Routes.Config.name}/${selectedTunnel?.id}")
|
if (settings.isAutoTunnelEnabled) {
|
||||||
|
showSnackbarMessage(
|
||||||
|
context.resources.getString(
|
||||||
|
R.string.turn_off_auto
|
||||||
|
)
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
showPrimaryChangeAlertDialog = true
|
||||||
|
}
|
||||||
|
}) {
|
||||||
|
Icon(
|
||||||
|
Icons.Rounded.Star,
|
||||||
|
stringResource(id = R.string.set_primary)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
IconButton(onClick = {
|
||||||
|
navController.navigate(
|
||||||
|
"${Routes.Config.name}/${selectedTunnel?.id}"
|
||||||
|
)
|
||||||
}) {
|
}) {
|
||||||
Icon(Icons.Rounded.Edit, stringResource(id = R.string.edit))
|
Icon(Icons.Rounded.Edit, stringResource(id = R.string.edit))
|
||||||
}
|
}
|
||||||
IconButton(
|
IconButton(
|
||||||
modifier = Modifier.focusable(),
|
modifier = Modifier.focusable(),
|
||||||
onClick = { viewModel.onDelete(tunnel) }) {
|
onClick = { viewModel.onDelete(tunnel) }
|
||||||
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
Icons.Rounded.Delete,
|
Icons.Rounded.Delete,
|
||||||
stringResource(id = R.string.delete)
|
stringResource(id = R.string.delete)
|
||||||
@@ -290,25 +512,60 @@ fun MainScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
val checked = state == Tunnel.State.UP && tunnel.name == tunnelName
|
||||||
|
if (!checked) expanded.value = false
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun TunnelSwitch() =
|
||||||
|
Switch(
|
||||||
|
modifier = Modifier.focusRequester(focusRequester),
|
||||||
|
checked = checked,
|
||||||
|
onCheckedChange = { checked ->
|
||||||
|
if (!checked) expanded.value = false
|
||||||
|
onTunnelToggle(checked, tunnel)
|
||||||
|
}
|
||||||
|
)
|
||||||
if (WireGuardAutoTunnel.isRunningOnAndroidTv(context)) {
|
if (WireGuardAutoTunnel.isRunningOnAndroidTv(context)) {
|
||||||
Row {
|
Row {
|
||||||
|
if (!settings.isTunnelConfigDefault(tunnel)) {
|
||||||
|
IconButton(onClick = {
|
||||||
|
if (settings.isAutoTunnelEnabled) {
|
||||||
|
showSnackbarMessage(
|
||||||
|
context.resources.getString(
|
||||||
|
R.string.turn_off_auto
|
||||||
|
)
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
showPrimaryChangeAlertDialog = true
|
||||||
|
}
|
||||||
|
}) {
|
||||||
|
Icon(
|
||||||
|
Icons.Rounded.Star,
|
||||||
|
stringResource(id = R.string.set_primary)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
IconButton(
|
IconButton(
|
||||||
modifier = Modifier.focusRequester(focusRequester),
|
modifier = Modifier.focusRequester(focusRequester),
|
||||||
onClick = {
|
onClick = {
|
||||||
navController.navigate("${Routes.Detail.name}/${tunnel.id}")
|
if (state == Tunnel.State.UP && (tunnelName == tunnel.name)) {
|
||||||
}) {
|
expanded.value = !expanded.value
|
||||||
Icon(Icons.Rounded.Info, "Info")
|
}
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Icon(Icons.Rounded.Info, stringResource(R.string.info))
|
||||||
}
|
}
|
||||||
IconButton(onClick = {
|
IconButton(onClick = {
|
||||||
if (state == Tunnel.State.UP && tunnel.name == tunnelName)
|
if (state == Tunnel.State.UP && tunnel.name == tunnelName) {
|
||||||
scope.launch {
|
showSnackbarMessage(
|
||||||
viewModel.showSnackBarMessage(
|
|
||||||
context.resources.getString(
|
context.resources.getString(
|
||||||
R.string.turn_off_tunnel
|
R.string.turn_off_tunnel
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
navController.navigate("${Routes.Config.name}/${tunnel.id}")
|
navController.navigate(
|
||||||
|
"${Routes.Config.name}/${tunnel.id}"
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}) {
|
}) {
|
||||||
Icon(
|
Icon(
|
||||||
@@ -317,9 +574,8 @@ fun MainScreen(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
IconButton(onClick = {
|
IconButton(onClick = {
|
||||||
if (state == Tunnel.State.UP && tunnel.name == tunnelName)
|
if (state == Tunnel.State.UP && tunnel.name == tunnelName) {
|
||||||
scope.launch {
|
showSnackbarMessage(
|
||||||
viewModel.showSnackBarMessage(
|
|
||||||
context.resources.getString(
|
context.resources.getString(
|
||||||
R.string.turn_off_tunnel
|
R.string.turn_off_tunnel
|
||||||
)
|
)
|
||||||
@@ -333,25 +589,16 @@ fun MainScreen(
|
|||||||
stringResource(id = R.string.delete)
|
stringResource(id = R.string.delete)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
Switch(
|
TunnelSwitch()
|
||||||
checked = (state == Tunnel.State.UP && tunnel.name == tunnelName),
|
|
||||||
onCheckedChange = { checked ->
|
|
||||||
if (checked) viewModel.onTunnelStart(tunnel) else viewModel.onTunnelStop()
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Switch(
|
TunnelSwitch()
|
||||||
checked = (state == Tunnel.State.UP && tunnel.name == tunnelName),
|
}
|
||||||
onCheckedChange = { checked ->
|
}
|
||||||
if (checked) viewModel.onTunnelStart(tunnel) else viewModel.onTunnelStop()
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+175
-95
@@ -1,60 +1,59 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.ui.screens.main
|
package com.zaneschepke.wireguardautotunnel.ui.screens.main
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.database.Cursor
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.provider.OpenableColumns
|
import android.provider.OpenableColumns
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import com.wireguard.config.BadConfigException
|
|
||||||
import com.wireguard.config.Config
|
import com.wireguard.config.Config
|
||||||
|
import com.zaneschepke.wireguardautotunnel.Constants
|
||||||
import com.zaneschepke.wireguardautotunnel.R
|
import com.zaneschepke.wireguardautotunnel.R
|
||||||
import com.zaneschepke.wireguardautotunnel.repository.Repository
|
import com.zaneschepke.wireguardautotunnel.repository.SettingsDoa
|
||||||
import com.zaneschepke.wireguardautotunnel.service.barcode.CodeScanner
|
import com.zaneschepke.wireguardautotunnel.repository.TunnelConfigDao
|
||||||
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceState
|
import com.zaneschepke.wireguardautotunnel.repository.model.Settings
|
||||||
|
import com.zaneschepke.wireguardautotunnel.repository.model.TunnelConfig
|
||||||
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
|
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
|
||||||
|
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceState
|
||||||
import com.zaneschepke.wireguardautotunnel.service.foreground.WireGuardConnectivityWatcherService
|
import com.zaneschepke.wireguardautotunnel.service.foreground.WireGuardConnectivityWatcherService
|
||||||
import com.zaneschepke.wireguardautotunnel.service.shortcut.ShortcutsManager
|
import com.zaneschepke.wireguardautotunnel.service.foreground.WireGuardTunnelService
|
||||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnService
|
import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnService
|
||||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.model.Settings
|
import com.zaneschepke.wireguardautotunnel.util.NumberUtils
|
||||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.model.TunnelConfig
|
import com.zaneschepke.wireguardautotunnel.util.WgTunnelException
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.ViewState
|
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
|
import java.io.InputStream
|
||||||
|
import java.util.zip.ZipInputStream
|
||||||
|
import javax.inject.Inject
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
import kotlinx.coroutines.flow.filter
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import timber.log.Timber
|
import kotlinx.coroutines.withContext
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
|
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
class MainViewModel @Inject constructor(private val application : Application,
|
class MainViewModel
|
||||||
private val tunnelRepo : Repository<TunnelConfig>,
|
@Inject
|
||||||
private val settingsRepo : Repository<Settings>,
|
constructor(
|
||||||
private val vpnService: VpnService,
|
private val application: Application,
|
||||||
private val codeScanner: CodeScanner
|
private val tunnelRepo: TunnelConfigDao,
|
||||||
|
private val settingsRepo: SettingsDoa,
|
||||||
|
private val vpnService: VpnService
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
|
val tunnels get() = tunnelRepo.getAllFlow()
|
||||||
private val _viewState = MutableStateFlow(ViewState())
|
|
||||||
val viewState get() = _viewState.asStateFlow()
|
|
||||||
val tunnels get() = tunnelRepo.itemFlow
|
|
||||||
val state get() = vpnService.state
|
val state get() = vpnService.state
|
||||||
|
|
||||||
val handshakeStatus get() = vpnService.handshakeStatus
|
val handshakeStatus get() = vpnService.handshakeStatus
|
||||||
val tunnelName get() = vpnService.tunnelName
|
val tunnelName get() = vpnService.tunnelName
|
||||||
private val _settings = MutableStateFlow(Settings())
|
private val _settings = MutableStateFlow(Settings())
|
||||||
val settings get() = _settings.asStateFlow()
|
val settings get() = _settings.asStateFlow()
|
||||||
|
val statistics get() = vpnService.statistics
|
||||||
private val defaultConfigName = {
|
|
||||||
"tunnel${(Math.random() * 100000).toInt()}"
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
init {
|
init {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
settingsRepo.itemFlow.collect {
|
settingsRepo.getAllFlow().filter { it.isNotEmpty() }.collect {
|
||||||
val settings = it.first()
|
val settings = it.first()
|
||||||
validateWatcherServiceState(settings)
|
validateWatcherServiceState(settings)
|
||||||
_settings.emit(settings)
|
_settings.emit(settings)
|
||||||
@@ -63,19 +62,25 @@ class MainViewModel @Inject constructor(private val application : Application,
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun validateWatcherServiceState(settings: Settings) {
|
private fun validateWatcherServiceState(settings: Settings) {
|
||||||
val watcherState = ServiceManager.getServiceState(application.applicationContext, WireGuardConnectivityWatcherService::class.java)
|
val watcherState =
|
||||||
|
ServiceManager.getServiceState(
|
||||||
|
application.applicationContext,
|
||||||
|
WireGuardConnectivityWatcherService::class.java
|
||||||
|
)
|
||||||
if (settings.isAutoTunnelEnabled && watcherState == ServiceState.STOPPED && settings.defaultTunnel != null) {
|
if (settings.isAutoTunnelEnabled && watcherState == ServiceState.STOPPED && settings.defaultTunnel != null) {
|
||||||
ServiceManager.startWatcherService(application.applicationContext, settings.defaultTunnel!!)
|
ServiceManager.startWatcherService(
|
||||||
|
application.applicationContext,
|
||||||
|
settings.defaultTunnel!!
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
fun onDelete(tunnel: TunnelConfig) {
|
fun onDelete(tunnel: TunnelConfig) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
if (tunnelRepo.count() == 1L) {
|
if (tunnelRepo.count() == 1L) {
|
||||||
ServiceManager.stopWatcherService(application.applicationContext)
|
ServiceManager.stopWatcherService(application.applicationContext)
|
||||||
val settings = settingsRepo.getAll()
|
val settings = settingsRepo.getAll()
|
||||||
if(!settings.isNullOrEmpty()) {
|
if (settings.isNotEmpty()) {
|
||||||
val setting = settings[0]
|
val setting = settings[0]
|
||||||
setting.defaultTunnel = null
|
setting.defaultTunnel = null
|
||||||
setting.isAutoTunnelEnabled = false
|
setting.isAutoTunnelEnabled = false
|
||||||
@@ -84,100 +89,164 @@ class MainViewModel @Inject constructor(private val application : Application,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
tunnelRepo.delete(tunnel)
|
tunnelRepo.delete(tunnel)
|
||||||
ShortcutsManager.removeTunnelShortcuts(application.applicationContext, tunnel)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onTunnelStart(tunnelConfig : TunnelConfig) = viewModelScope.launch {
|
fun onTunnelStart(tunnelConfig: TunnelConfig) {
|
||||||
|
viewModelScope.launch {
|
||||||
|
stopActiveTunnel()
|
||||||
|
startTunnel(tunnelConfig)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun startTunnel(tunnelConfig: TunnelConfig) {
|
||||||
ServiceManager.startVpnService(application.applicationContext, tunnelConfig.toString())
|
ServiceManager.startVpnService(application.applicationContext, tunnelConfig.toString())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private suspend fun stopActiveTunnel() {
|
||||||
|
if (ServiceManager.getServiceState(
|
||||||
|
application.applicationContext,
|
||||||
|
WireGuardTunnelService::class.java
|
||||||
|
) == ServiceState.STARTED
|
||||||
|
) {
|
||||||
|
onTunnelStop()
|
||||||
|
delay(Constants.TOGGLE_TUNNEL_DELAY)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun onTunnelStop() {
|
fun onTunnelStop() {
|
||||||
ServiceManager.stopVpnService(application.applicationContext)
|
ServiceManager.stopVpnService(application.applicationContext)
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun onTunnelQRSelected() {
|
private fun validateConfigString(config: String) {
|
||||||
codeScanner.scan().collect {
|
TunnelConfig.configFromQuick(config)
|
||||||
if(!it.isNullOrEmpty() && it.contains(application.resources.getString(R.string.config_validation))) {
|
|
||||||
val tunnelConfig = TunnelConfig(name = defaultConfigName(), wgQuick = it)
|
|
||||||
saveTunnel(tunnelConfig)
|
|
||||||
} else if(!it.isNullOrEmpty() && it.contains(application.resources.getString(R.string.barcode_downloading))) {
|
|
||||||
showSnackBarMessage(application.resources.getString(R.string.barcode_downloading_message))
|
|
||||||
} else {
|
|
||||||
showSnackBarMessage(application.resources.getString(R.string.barcode_error))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun onTunnelQrResult(result: String) {
|
||||||
|
try {
|
||||||
|
validateConfigString(result)
|
||||||
|
val tunnelConfig =
|
||||||
|
TunnelConfig(name = NumberUtils.generateRandomTunnelName(), wgQuick = result)
|
||||||
|
addTunnel(tunnelConfig)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
throw WgTunnelException(e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onTunnelFileSelected(uri : Uri) {
|
private suspend fun saveTunnelConfigFromStream(
|
||||||
try {
|
stream: InputStream,
|
||||||
val fileName = getFileName(application.applicationContext, uri)
|
fileName: String
|
||||||
val extension = getFileExtensionFromFileName(fileName)
|
) {
|
||||||
if(extension != ".conf") {
|
|
||||||
viewModelScope.launch {
|
|
||||||
showSnackBarMessage(application.resources.getString(R.string.file_extension_message))
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
val stream = application.applicationContext.contentResolver.openInputStream(uri)
|
|
||||||
stream ?: return
|
|
||||||
val bufferReader = stream.bufferedReader(charset = Charsets.UTF_8)
|
val bufferReader = stream.bufferedReader(charset = Charsets.UTF_8)
|
||||||
val config = Config.parse(bufferReader)
|
val config = Config.parse(bufferReader)
|
||||||
val tunnelName = getNameFromFileName(fileName)
|
val tunnelName = getNameFromFileName(fileName)
|
||||||
saveTunnel(TunnelConfig(name = tunnelName, wgQuick = config.toWgQuickString()))
|
addTunnel(TunnelConfig(name = tunnelName, wgQuick = config.toWgQuickString()))
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
stream.close()
|
stream.close()
|
||||||
} catch(_: BadConfigException) {
|
|
||||||
viewModelScope.launch {
|
|
||||||
showSnackBarMessage(application.applicationContext.getString(R.string.bad_config))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun saveTunnel(tunnelConfig : TunnelConfig) {
|
|
||||||
viewModelScope.launch {
|
|
||||||
tunnelRepo.save(tunnelConfig)
|
|
||||||
ShortcutsManager.createTunnelShortcuts(application.applicationContext, tunnelConfig)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("Range")
|
private fun getInputStreamFromUri(uri: Uri): InputStream {
|
||||||
private fun getFileName(context: Context, uri: Uri): String {
|
return application.applicationContext.contentResolver.openInputStream(uri)
|
||||||
if (uri.scheme == "content") {
|
?: throw WgTunnelException(application.getString(R.string.stream_failed))
|
||||||
val cursor = try {
|
}
|
||||||
context.contentResolver.query(uri, null, null, null, null)
|
|
||||||
|
suspend fun onTunnelFileSelected(uri: Uri) {
|
||||||
|
try {
|
||||||
|
val fileName = getFileName(application.applicationContext, uri)
|
||||||
|
val fileExtension = getFileExtensionFromFileName(fileName)
|
||||||
|
when (fileExtension) {
|
||||||
|
Constants.CONF_FILE_EXTENSION -> saveTunnelFromConfUri(fileName, uri)
|
||||||
|
Constants.ZIP_FILE_EXTENSION -> saveTunnelsFromZipUri(uri)
|
||||||
|
else -> throw WgTunnelException(
|
||||||
|
application.getString(R.string.file_extension_message)
|
||||||
|
)
|
||||||
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Timber.d("Exception getting config name")
|
throw WgTunnelException(e)
|
||||||
null
|
|
||||||
}
|
}
|
||||||
cursor ?: return defaultConfigName()
|
}
|
||||||
|
|
||||||
|
private suspend fun saveTunnelsFromZipUri(uri: Uri) {
|
||||||
|
ZipInputStream(getInputStreamFromUri(uri)).use { zip ->
|
||||||
|
generateSequence { zip.nextEntry }
|
||||||
|
.filterNot {
|
||||||
|
it.isDirectory ||
|
||||||
|
getFileExtensionFromFileName(it.name) != Constants.CONF_FILE_EXTENSION
|
||||||
|
}
|
||||||
|
.forEach {
|
||||||
|
val name = getNameFromFileName(it.name)
|
||||||
|
val config = Config.parse(zip)
|
||||||
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
|
addTunnel(TunnelConfig(name = name, wgQuick = config.toWgQuickString()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun saveTunnelFromConfUri(
|
||||||
|
name: String,
|
||||||
|
uri: Uri
|
||||||
|
) {
|
||||||
|
val stream = getInputStreamFromUri(uri)
|
||||||
|
saveTunnelConfigFromStream(stream, name)
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun addTunnel(tunnelConfig: TunnelConfig) {
|
||||||
|
saveTunnel(tunnelConfig)
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun saveTunnel(tunnelConfig: TunnelConfig) {
|
||||||
|
tunnelRepo.save(tunnelConfig)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getFileNameByCursor(
|
||||||
|
context: Context,
|
||||||
|
uri: Uri
|
||||||
|
): String {
|
||||||
|
val cursor = context.contentResolver.query(uri, null, null, null, null)
|
||||||
|
if (cursor != null) {
|
||||||
cursor.use {
|
cursor.use {
|
||||||
|
return getDisplayNameByCursor(it)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw WgTunnelException("Failed to initialize cursor")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getDisplayNameColumnIndex(cursor: Cursor): Int {
|
||||||
|
val columnIndex = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME)
|
||||||
|
if (columnIndex == -1) {
|
||||||
|
throw WgTunnelException("Cursor out of bounds")
|
||||||
|
}
|
||||||
|
return columnIndex
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getDisplayNameByCursor(cursor: Cursor): String {
|
||||||
if (cursor.moveToFirst()) {
|
if (cursor.moveToFirst()) {
|
||||||
return cursor.getString(cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME))
|
val index = getDisplayNameColumnIndex(cursor)
|
||||||
|
return cursor.getString(index)
|
||||||
|
} else {
|
||||||
|
throw WgTunnelException("Cursor failed to move to first")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
return defaultConfigName()
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun showSnackBarMessage(message : String) {
|
private fun validateUriContentScheme(uri: Uri) {
|
||||||
_viewState.emit(_viewState.value.copy(
|
if (uri.scheme != Constants.URI_CONTENT_SCHEME) {
|
||||||
showSnackbarMessage = true,
|
throw WgTunnelException(application.getString(R.string.file_extension_message))
|
||||||
snackbarMessage = message,
|
|
||||||
snackbarActionText = "Okay",
|
|
||||||
onSnackbarActionClick = {
|
|
||||||
viewModelScope.launch {
|
|
||||||
dismissSnackBar()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
))
|
|
||||||
delay(3000)
|
|
||||||
dismissSnackBar()
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun dismissSnackBar() {
|
private fun getFileName(
|
||||||
_viewState.emit(_viewState.value.copy(
|
context: Context,
|
||||||
showSnackbarMessage = false
|
uri: Uri
|
||||||
))
|
): String {
|
||||||
|
validateUriContentScheme(uri)
|
||||||
|
return try {
|
||||||
|
getFileNameByCursor(context, uri)
|
||||||
|
} catch (_: Exception) {
|
||||||
|
NumberUtils.generateRandomTunnelName()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getNameFromFileName(fileName: String): String {
|
private fun getNameFromFileName(fileName: String): String {
|
||||||
@@ -191,4 +260,15 @@ class MainViewModel @Inject constructor(private val application : Application,
|
|||||||
""
|
""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun onDefaultTunnelChange(selectedTunnel: TunnelConfig?) {
|
||||||
|
if (selectedTunnel != null) {
|
||||||
|
_settings.emit(
|
||||||
|
_settings.value.copy(
|
||||||
|
defaultTunnel = selectedTunnel.toString()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
settingsRepo.save(_settings.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
+397
-216
@@ -5,20 +5,24 @@ import android.content.Intent
|
|||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.provider.Settings
|
import android.provider.Settings
|
||||||
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
||||||
import androidx.compose.foundation.layout.FlowRow
|
import androidx.compose.foundation.layout.FlowRow
|
||||||
|
import androidx.compose.foundation.layout.IntrinsicSize
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.fillMaxHeight
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.foundation.text.KeyboardActions
|
import androidx.compose.foundation.text.KeyboardActions
|
||||||
import androidx.compose.foundation.text.KeyboardOptions
|
import androidx.compose.foundation.text.KeyboardOptions
|
||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
@@ -26,34 +30,30 @@ import androidx.compose.material.icons.Icons
|
|||||||
import androidx.compose.material.icons.filled.Close
|
import androidx.compose.material.icons.filled.Close
|
||||||
import androidx.compose.material.icons.outlined.Add
|
import androidx.compose.material.icons.outlined.Add
|
||||||
import androidx.compose.material.icons.rounded.LocationOff
|
import androidx.compose.material.icons.rounded.LocationOff
|
||||||
import androidx.compose.material3.Button
|
|
||||||
import androidx.compose.material3.DropdownMenuItem
|
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
|
||||||
import androidx.compose.material3.ExposedDropdownMenuBox
|
|
||||||
import androidx.compose.material3.ExposedDropdownMenuDefaults
|
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.IconButton
|
import androidx.compose.material3.IconButton
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.OutlinedTextField
|
import androidx.compose.material3.OutlinedTextField
|
||||||
import androidx.compose.material3.SnackbarDuration
|
import androidx.compose.material3.Surface
|
||||||
import androidx.compose.material3.SnackbarHostState
|
|
||||||
import androidx.compose.material3.SnackbarResult
|
|
||||||
import androidx.compose.material3.Switch
|
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TextField
|
import androidx.compose.material3.TextButton
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.SideEffect
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.ExperimentalComposeUiApi
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.focus.FocusRequester
|
import androidx.compose.ui.focus.FocusRequester
|
||||||
import androidx.compose.ui.focus.focusRequester
|
import androidx.compose.ui.focus.focusRequester
|
||||||
|
import androidx.compose.ui.focus.onFocusChanged
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.platform.LocalFocusManager
|
import androidx.compose.ui.platform.LocalFocusManager
|
||||||
|
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.font.FontStyle
|
import androidx.compose.ui.text.font.FontStyle
|
||||||
import androidx.compose.ui.text.input.ImeAction
|
import androidx.compose.ui.text.input.ImeAction
|
||||||
@@ -63,68 +63,101 @@ import androidx.compose.ui.unit.dp
|
|||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import androidx.hilt.navigation.compose.hiltViewModel
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import androidx.navigation.NavController
|
|
||||||
import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
||||||
import com.google.accompanist.permissions.isGranted
|
import com.google.accompanist.permissions.isGranted
|
||||||
import com.google.accompanist.permissions.rememberPermissionState
|
import com.google.accompanist.permissions.rememberPermissionState
|
||||||
|
import com.wireguard.android.backend.Tunnel
|
||||||
|
import com.wireguard.android.backend.WgQuickBackend
|
||||||
import com.zaneschepke.wireguardautotunnel.R
|
import com.zaneschepke.wireguardautotunnel.R
|
||||||
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
|
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
|
||||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.model.TunnelConfig
|
import com.zaneschepke.wireguardautotunnel.repository.datastore.DataStoreManager
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.Routes
|
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.common.ClickableIconButton
|
import com.zaneschepke.wireguardautotunnel.ui.common.ClickableIconButton
|
||||||
|
import com.zaneschepke.wireguardautotunnel.ui.common.config.ConfigurationToggle
|
||||||
|
import com.zaneschepke.wireguardautotunnel.ui.common.prompt.AuthorizationPrompt
|
||||||
|
import com.zaneschepke.wireguardautotunnel.ui.common.text.SectionTitle
|
||||||
|
import com.zaneschepke.wireguardautotunnel.util.FileUtils
|
||||||
|
import com.zaneschepke.wireguardautotunnel.util.WgTunnelException
|
||||||
|
import java.io.File
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
@OptIn(
|
||||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalPermissionsApi::class,
|
ExperimentalPermissionsApi::class,
|
||||||
ExperimentalLayoutApi::class
|
ExperimentalLayoutApi::class,
|
||||||
|
ExperimentalComposeUiApi::class
|
||||||
)
|
)
|
||||||
@Composable
|
@Composable
|
||||||
fun SettingsScreen(
|
fun SettingsScreen(
|
||||||
viewModel: SettingsViewModel = hiltViewModel(),
|
viewModel: SettingsViewModel = hiltViewModel(),
|
||||||
padding: PaddingValues,
|
padding: PaddingValues,
|
||||||
navController: NavController,
|
showSnackbarMessage: (String) -> Unit,
|
||||||
focusRequester: FocusRequester,
|
focusRequester: FocusRequester
|
||||||
snackbarHostState: SnackbarHostState = remember { SnackbarHostState() }
|
|
||||||
) {
|
) {
|
||||||
|
val scope = rememberCoroutineScope { Dispatchers.IO }
|
||||||
val scope = rememberCoroutineScope()
|
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val focusManager = LocalFocusManager.current
|
val focusManager = LocalFocusManager.current
|
||||||
|
val keyboardController = LocalSoftwareKeyboardController.current
|
||||||
val interactionSource = remember { MutableInteractionSource() }
|
val interactionSource = remember { MutableInteractionSource() }
|
||||||
|
|
||||||
var expanded by remember { mutableStateOf(false) }
|
|
||||||
val viewState by viewModel.viewState.collectAsStateWithLifecycle()
|
|
||||||
val settings by viewModel.settings.collectAsStateWithLifecycle()
|
val settings by viewModel.settings.collectAsStateWithLifecycle()
|
||||||
val trustedSSIDs by viewModel.trustedSSIDs.collectAsStateWithLifecycle()
|
val trustedSSIDs by viewModel.trustedSSIDs.collectAsStateWithLifecycle()
|
||||||
val tunnels by viewModel.tunnels.collectAsStateWithLifecycle(mutableListOf())
|
val tunnels by viewModel.tunnels.collectAsStateWithLifecycle(mutableListOf())
|
||||||
val backgroundLocationState =
|
|
||||||
rememberPermissionState(Manifest.permission.ACCESS_BACKGROUND_LOCATION)
|
|
||||||
val fineLocationState = rememberPermissionState(Manifest.permission.ACCESS_FINE_LOCATION)
|
val fineLocationState = rememberPermissionState(Manifest.permission.ACCESS_FINE_LOCATION)
|
||||||
var currentText by remember { mutableStateOf("") }
|
var currentText by remember { mutableStateOf("") }
|
||||||
val scrollState = rememberScrollState()
|
val scrollState = rememberScrollState()
|
||||||
var isLocationServicesEnabled by remember { mutableStateOf(viewModel.checkLocationServicesEnabled())}
|
var isBackgroundLocationGranted by remember { mutableStateOf(true) }
|
||||||
|
var showAuthPrompt by remember { mutableStateOf(false) }
|
||||||
LaunchedEffect(viewState) {
|
var didExportFiles by remember { mutableStateOf(false) }
|
||||||
if (viewState.showSnackbarMessage) {
|
val isLocationDisclosureShown by viewModel.disclosureShown.collectAsStateWithLifecycle(
|
||||||
val result = snackbarHostState.showSnackbar(
|
null
|
||||||
message = viewState.snackbarMessage,
|
)
|
||||||
actionLabel = viewState.snackbarActionText,
|
val vpnState = viewModel.vpnState.collectAsStateWithLifecycle(initialValue = Tunnel.State.DOWN)
|
||||||
duration = SnackbarDuration.Long,
|
|
||||||
|
val screenPadding = 5.dp
|
||||||
|
val fillMaxWidth = .85f
|
||||||
|
|
||||||
|
fun setLocationDisclosureShown() = scope.launch {
|
||||||
|
viewModel.dataStoreManager.saveToDataStore(
|
||||||
|
DataStoreManager.LOCATION_DISCLOSURE_SHOWN,
|
||||||
|
true
|
||||||
)
|
)
|
||||||
when (result) {
|
|
||||||
SnackbarResult.ActionPerformed -> viewState.onSnackbarActionClick
|
|
||||||
SnackbarResult.Dismissed -> viewState.onSnackbarActionClick
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun exportAllConfigs() {
|
||||||
|
try {
|
||||||
|
val files = tunnels.map { File(context.cacheDir, "${it.name}.conf") }
|
||||||
|
files.forEachIndexed { index, file ->
|
||||||
|
file.outputStream().use {
|
||||||
|
it.write(tunnels[index].wgQuick.toByteArray())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
FileUtils.saveFilesToZip(context, files)
|
||||||
|
didExportFiles = true
|
||||||
|
showSnackbarMessage(context.getString(R.string.exported_configs_message))
|
||||||
|
} catch (e: Exception) {
|
||||||
|
showSnackbarMessage(e.message!!)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun saveTrustedSSID() {
|
fun saveTrustedSSID() {
|
||||||
if (currentText.isNotEmpty()) {
|
if (currentText.isNotEmpty()) {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
|
try {
|
||||||
viewModel.onSaveTrustedSSID(currentText)
|
viewModel.onSaveTrustedSSID(currentText)
|
||||||
currentText = ""
|
currentText = ""
|
||||||
|
} catch (e: Exception) {
|
||||||
|
showSnackbarMessage(e.message ?: context.getString(R.string.unknown_error))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun isAllAutoTunnelPermissionsEnabled(): Boolean {
|
||||||
|
return (
|
||||||
|
isBackgroundLocationGranted &&
|
||||||
|
fineLocationState.status.isGranted &&
|
||||||
|
!viewModel.isLocationServicesNeeded()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
fun openSettings() {
|
fun openSettings() {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
@@ -136,34 +169,82 @@ fun SettingsScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if(!backgroundLocationState.status.isGranted && Build.VERSION.SDK_INT > Build.VERSION_CODES.P) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||||
Column(horizontalAlignment = Alignment.CenterHorizontally,
|
val backgroundLocationState =
|
||||||
|
rememberPermissionState(Manifest.permission.ACCESS_BACKGROUND_LOCATION)
|
||||||
|
isBackgroundLocationGranted = if (!backgroundLocationState.status.isGranted) {
|
||||||
|
false
|
||||||
|
} else {
|
||||||
|
SideEffect {
|
||||||
|
setLocationDisclosureShown()
|
||||||
|
}
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.P) {
|
||||||
|
if (!fineLocationState.status.isGranted) {
|
||||||
|
isBackgroundLocationGranted = false
|
||||||
|
} else {
|
||||||
|
SideEffect {
|
||||||
|
setLocationDisclosureShown()
|
||||||
|
}
|
||||||
|
isBackgroundLocationGranted = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isLocationDisclosureShown != true) {
|
||||||
|
Column(
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
verticalArrangement = Arrangement.Top,
|
verticalArrangement = Arrangement.Top,
|
||||||
modifier = Modifier
|
modifier =
|
||||||
|
Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.verticalScroll(scrollState)
|
.verticalScroll(scrollState)
|
||||||
.padding(padding)) {
|
.padding(padding)
|
||||||
Icon(Icons.Rounded.LocationOff, contentDescription = stringResource(id = R.string.map), modifier = Modifier
|
) {
|
||||||
|
Icon(
|
||||||
|
Icons.Rounded.LocationOff,
|
||||||
|
contentDescription = stringResource(id = R.string.map),
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
.padding(30.dp)
|
.padding(30.dp)
|
||||||
.size(128.dp))
|
.size(128.dp)
|
||||||
Text(stringResource(R.string.prominent_background_location_title), textAlign = TextAlign.Center, modifier = Modifier.padding(30.dp), fontSize = 20.sp)
|
)
|
||||||
Text(stringResource(R.string.prominent_background_location_message), textAlign = TextAlign.Center, modifier = Modifier.padding(30.dp), fontSize = 15.sp)
|
Text(
|
||||||
|
stringResource(R.string.prominent_background_location_title),
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
modifier = Modifier.padding(30.dp),
|
||||||
|
fontSize = 20.sp
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
stringResource(R.string.prominent_background_location_message),
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
modifier = Modifier.padding(30.dp),
|
||||||
|
fontSize = 15.sp
|
||||||
|
)
|
||||||
Row(
|
Row(
|
||||||
modifier = if(WireGuardAutoTunnel.isRunningOnAndroidTv(context)) Modifier
|
modifier =
|
||||||
|
if (WireGuardAutoTunnel.isRunningOnAndroidTv(context)) {
|
||||||
|
Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(10.dp) else Modifier
|
.padding(10.dp)
|
||||||
|
} else {
|
||||||
|
Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(30.dp),
|
.padding(30.dp)
|
||||||
|
},
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
horizontalArrangement = Arrangement.SpaceEvenly
|
horizontalArrangement = Arrangement.SpaceEvenly
|
||||||
) {
|
) {
|
||||||
Button(onClick = {
|
TextButton(onClick = {
|
||||||
navController.navigate(Routes.Main.name)
|
setLocationDisclosureShown()
|
||||||
}) {
|
}) {
|
||||||
Text(stringResource(id = R.string.no_thanks))
|
Text(stringResource(id = R.string.no_thanks))
|
||||||
}
|
}
|
||||||
Button(modifier = Modifier.focusRequester(focusRequester), onClick = {
|
TextButton(modifier = Modifier.focusRequester(focusRequester), onClick = {
|
||||||
openSettings()
|
openSettings()
|
||||||
|
setLocationDisclosureShown()
|
||||||
}) {
|
}) {
|
||||||
Text(stringResource(id = R.string.turn_on))
|
Text(stringResource(id = R.string.turn_on))
|
||||||
}
|
}
|
||||||
@@ -172,35 +253,29 @@ fun SettingsScreen(
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if(!fineLocationState.status.isGranted) {
|
if (showAuthPrompt) {
|
||||||
Column(
|
AuthorizationPrompt(
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
onSuccess = {
|
||||||
verticalArrangement = Arrangement.Center,
|
showAuthPrompt = false
|
||||||
modifier = Modifier
|
exportAllConfigs()
|
||||||
.fillMaxSize()
|
},
|
||||||
.padding(padding)
|
onError = { error ->
|
||||||
) {
|
showSnackbarMessage(error)
|
||||||
Text(
|
showAuthPrompt = false
|
||||||
stringResource(id = R.string.precise_location_message),
|
},
|
||||||
textAlign = TextAlign.Center,
|
onFailure = {
|
||||||
modifier = Modifier.padding(15.dp),
|
showAuthPrompt = false
|
||||||
fontStyle = FontStyle.Italic
|
showSnackbarMessage(context.getString(R.string.authentication_failed))
|
||||||
|
}
|
||||||
)
|
)
|
||||||
Button(modifier = Modifier.focusRequester(focusRequester),onClick = {
|
|
||||||
fineLocationState.launchPermissionRequest()
|
|
||||||
}) {
|
|
||||||
Text(stringResource(id = R.string.request))
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (tunnels.isEmpty()) {
|
if (tunnels.isEmpty()) {
|
||||||
Column(
|
Column(
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
verticalArrangement = Arrangement.Center,
|
verticalArrangement = Arrangement.Center,
|
||||||
modifier = Modifier
|
modifier =
|
||||||
|
Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.padding(padding)
|
.padding(padding)
|
||||||
) {
|
) {
|
||||||
@@ -213,136 +288,84 @@ fun SettingsScreen(
|
|||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if(!isLocationServicesEnabled && Build.VERSION.SDK_INT > Build.VERSION_CODES.P) {
|
|
||||||
Column(
|
Column(
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
verticalArrangement = Arrangement.Center,
|
verticalArrangement = Arrangement.Top,
|
||||||
modifier = Modifier
|
modifier =
|
||||||
|
Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.padding(padding)
|
.verticalScroll(scrollState)
|
||||||
|
.clickable(indication = null, interactionSource = interactionSource) {
|
||||||
|
focusManager.clearFocus()
|
||||||
|
}
|
||||||
) {
|
) {
|
||||||
Text(
|
Surface(
|
||||||
stringResource(id = R.string.location_services_not_detected),
|
tonalElevation = 2.dp,
|
||||||
textAlign = TextAlign.Center,
|
shadowElevation = 2.dp,
|
||||||
modifier = Modifier.padding(15.dp),
|
shape = RoundedCornerShape(12.dp),
|
||||||
fontStyle = FontStyle.Italic
|
color = MaterialTheme.colorScheme.surface,
|
||||||
)
|
modifier =
|
||||||
Button(modifier = Modifier.focusRequester(focusRequester), onClick = {
|
(
|
||||||
val locationServicesEnabled = viewModel.checkLocationServicesEnabled()
|
if (WireGuardAutoTunnel.isRunningOnAndroidTv(context)) {
|
||||||
isLocationServicesEnabled = locationServicesEnabled
|
Modifier
|
||||||
if(!locationServicesEnabled) {
|
.height(IntrinsicSize.Min)
|
||||||
scope.launch {
|
.fillMaxWidth(fillMaxWidth)
|
||||||
viewModel.showSnackBarMessage(context.getString(R.string.detecting_location_services_disabled))
|
.padding(top = 10.dp)
|
||||||
|
} else {
|
||||||
|
Modifier
|
||||||
|
.fillMaxWidth(fillMaxWidth)
|
||||||
|
.padding(top = 60.dp)
|
||||||
}
|
}
|
||||||
}
|
).padding(bottom = 10.dp)
|
||||||
}) {
|
) {
|
||||||
Text(stringResource(id = R.string.check_again))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
val screenPadding = if(WireGuardAutoTunnel.isRunningOnAndroidTv(context)) 5.dp else 15.dp
|
|
||||||
Column(
|
Column(
|
||||||
horizontalAlignment = Alignment.Start,
|
horizontalAlignment = Alignment.Start,
|
||||||
verticalArrangement = Arrangement.Top,
|
verticalArrangement = Arrangement.Top,
|
||||||
modifier = if(WireGuardAutoTunnel.isRunningOnAndroidTv(context)) Modifier
|
modifier = Modifier.padding(15.dp)
|
||||||
.fillMaxHeight(.85f)
|
|
||||||
.fillMaxWidth()
|
|
||||||
.verticalScroll(scrollState)
|
|
||||||
.clickable(indication = null, interactionSource = interactionSource) {
|
|
||||||
focusManager.clearFocus()
|
|
||||||
}
|
|
||||||
.padding(padding) else Modifier
|
|
||||||
.fillMaxSize()
|
|
||||||
.verticalScroll(scrollState)
|
|
||||||
.clickable(indication = null, interactionSource = interactionSource) {
|
|
||||||
focusManager.clearFocus()
|
|
||||||
}
|
|
||||||
.padding(padding)
|
|
||||||
) {
|
) {
|
||||||
Row(
|
SectionTitle(
|
||||||
modifier = Modifier
|
title = stringResource(id = R.string.auto_tunneling),
|
||||||
.fillMaxWidth()
|
padding = screenPadding
|
||||||
.padding(screenPadding),
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
horizontalArrangement = Arrangement.SpaceBetween
|
|
||||||
) {
|
|
||||||
Text(stringResource(R.string.enable_auto_tunnel))
|
|
||||||
Switch(
|
|
||||||
modifier = Modifier.focusRequester(focusRequester),
|
|
||||||
enabled = !settings.isAlwaysOnVpnEnabled,
|
|
||||||
checked = settings.isAutoTunnelEnabled,
|
|
||||||
onCheckedChange = {
|
|
||||||
scope.launch {
|
|
||||||
viewModel.toggleAutoTunnel()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
}
|
ConfigurationToggle(
|
||||||
Text(
|
stringResource(id = R.string.tunnel_on_wifi),
|
||||||
stringResource(id = R.string.select_tunnel),
|
|
||||||
textAlign = TextAlign.Center,
|
|
||||||
modifier = Modifier.padding(screenPadding, bottom = 5.dp, top = 5.dp)
|
|
||||||
)
|
|
||||||
ExposedDropdownMenuBox(
|
|
||||||
expanded = expanded,
|
|
||||||
onExpandedChange = {
|
|
||||||
if(!(settings.isAutoTunnelEnabled || settings.isAlwaysOnVpnEnabled)) {
|
|
||||||
expanded = !expanded }},
|
|
||||||
modifier = Modifier.padding(start = 15.dp, top = 5.dp, bottom = 10.dp).clickable {
|
|
||||||
expanded = !expanded
|
|
||||||
},
|
|
||||||
) {
|
|
||||||
TextField(
|
|
||||||
enabled = !(settings.isAutoTunnelEnabled || settings.isAlwaysOnVpnEnabled),
|
enabled = !(settings.isAutoTunnelEnabled || settings.isAlwaysOnVpnEnabled),
|
||||||
value = settings.defaultTunnel?.let {
|
checked = settings.isTunnelOnWifiEnabled,
|
||||||
TunnelConfig.from(it).name }
|
padding = screenPadding,
|
||||||
?: "",
|
onCheckChanged = {
|
||||||
readOnly = true,
|
|
||||||
modifier = Modifier.menuAnchor(),
|
|
||||||
label = { Text(stringResource(R.string.tunnels)) },
|
|
||||||
onValueChange = { },
|
|
||||||
trailingIcon = {
|
|
||||||
ExposedDropdownMenuDefaults.TrailingIcon(
|
|
||||||
expanded = expanded
|
|
||||||
)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
ExposedDropdownMenu(
|
|
||||||
expanded = expanded,
|
|
||||||
onDismissRequest = {
|
|
||||||
expanded = false
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
tunnels.forEach() { tunnel ->
|
|
||||||
DropdownMenuItem(
|
|
||||||
onClick = {
|
|
||||||
scope.launch {
|
scope.launch {
|
||||||
viewModel.onDefaultTunnelSelected(tunnel)
|
viewModel.onToggleTunnelOnWifi()
|
||||||
}
|
}
|
||||||
expanded = false
|
|
||||||
},
|
},
|
||||||
text = { Text(text = tunnel.name) }
|
modifier = Modifier.focusRequester(focusRequester)
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Text(
|
|
||||||
stringResource(R.string.trusted_ssid),
|
|
||||||
textAlign = TextAlign.Center,
|
|
||||||
modifier = Modifier.padding(screenPadding, bottom = 5.dp, top = 5.dp)
|
|
||||||
)
|
)
|
||||||
|
AnimatedVisibility(visible = settings.isTunnelOnWifiEnabled) {
|
||||||
|
Column {
|
||||||
FlowRow(
|
FlowRow(
|
||||||
modifier = Modifier.padding(screenPadding),
|
modifier = Modifier
|
||||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
.padding(screenPadding)
|
||||||
verticalArrangement = Arrangement.SpaceEvenly
|
.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(5.dp)
|
||||||
) {
|
) {
|
||||||
trustedSSIDs.forEach { ssid ->
|
trustedSSIDs.forEach { ssid ->
|
||||||
ClickableIconButton(onIconClick = {
|
ClickableIconButton(
|
||||||
|
onIconClick = {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
viewModel.onDeleteTrustedSSID(ssid)
|
viewModel.onDeleteTrustedSSID(ssid)
|
||||||
}
|
}
|
||||||
}, text = ssid, icon = Icons.Filled.Close, enabled = !(settings.isAutoTunnelEnabled || settings.isAlwaysOnVpnEnabled))
|
},
|
||||||
|
text = ssid,
|
||||||
|
icon = Icons.Filled.Close,
|
||||||
|
enabled = !(settings.isAutoTunnelEnabled || settings.isAlwaysOnVpnEnabled)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (trustedSSIDs.isEmpty()) {
|
||||||
|
Text(
|
||||||
|
stringResource(R.string.none),
|
||||||
|
fontStyle = FontStyle.Italic,
|
||||||
|
color = Color.Gray
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
OutlinedTextField(
|
OutlinedTextField(
|
||||||
@@ -350,66 +373,224 @@ fun SettingsScreen(
|
|||||||
value = currentText,
|
value = currentText,
|
||||||
onValueChange = { currentText = it },
|
onValueChange = { currentText = it },
|
||||||
label = { Text(stringResource(R.string.add_trusted_ssid)) },
|
label = { Text(stringResource(R.string.add_trusted_ssid)) },
|
||||||
modifier = Modifier.padding(start = screenPadding, top = 5.dp),
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.padding(start = screenPadding, top = 5.dp, bottom = 10.dp)
|
||||||
|
.onFocusChanged {
|
||||||
|
if (WireGuardAutoTunnel.isRunningOnAndroidTv(context)) {
|
||||||
|
keyboardController?.hide()
|
||||||
|
}
|
||||||
|
},
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
keyboardOptions = KeyboardOptions(
|
keyboardOptions =
|
||||||
|
KeyboardOptions(
|
||||||
capitalization = KeyboardCapitalization.None,
|
capitalization = KeyboardCapitalization.None,
|
||||||
imeAction = ImeAction.Done
|
imeAction = ImeAction.Done
|
||||||
),
|
),
|
||||||
keyboardActions = KeyboardActions(
|
keyboardActions =
|
||||||
|
KeyboardActions(
|
||||||
onDone = {
|
onDone = {
|
||||||
saveTrustedSSID()
|
saveTrustedSSID()
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
trailingIcon = {
|
trailingIcon = {
|
||||||
|
if (currentText != "") {
|
||||||
IconButton(onClick = { saveTrustedSSID() }) {
|
IconButton(onClick = { saveTrustedSSID() }) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Outlined.Add,
|
imageVector = Icons.Outlined.Add,
|
||||||
contentDescription = if (currentText == "") stringResource(id = R.string.trusted_ssid_empty_description) else stringResource(
|
contentDescription =
|
||||||
|
if (currentText == "") {
|
||||||
|
stringResource(
|
||||||
|
id = R.string.trusted_ssid_empty_description
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
stringResource(
|
||||||
id = R.string.trusted_ssid_value_description
|
id = R.string.trusted_ssid_value_description
|
||||||
),
|
)
|
||||||
tint = if(currentText == "") Color.Transparent else Color.Green
|
},
|
||||||
|
tint = MaterialTheme.colorScheme.primary
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
|
}
|
||||||
)
|
)
|
||||||
Row(
|
}
|
||||||
modifier = Modifier
|
}
|
||||||
.fillMaxWidth()
|
ConfigurationToggle(
|
||||||
.padding(screenPadding),
|
stringResource(R.string.tunnel_mobile_data),
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
horizontalArrangement = Arrangement.SpaceBetween
|
|
||||||
) {
|
|
||||||
Text(stringResource(R.string.tunnel_mobile_data))
|
|
||||||
Switch(
|
|
||||||
enabled = !(settings.isAutoTunnelEnabled || settings.isAlwaysOnVpnEnabled),
|
enabled = !(settings.isAutoTunnelEnabled || settings.isAlwaysOnVpnEnabled),
|
||||||
checked = settings.isTunnelOnMobileDataEnabled,
|
checked = settings.isTunnelOnMobileDataEnabled,
|
||||||
onCheckedChange = {
|
padding = screenPadding,
|
||||||
|
onCheckChanged = {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
viewModel.onToggleTunnelOnMobileData()
|
viewModel.onToggleTunnelOnMobileData()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
ConfigurationToggle(
|
||||||
Row(
|
stringResource(id = R.string.tunnel_on_ethernet),
|
||||||
modifier = Modifier
|
enabled = !(settings.isAutoTunnelEnabled || settings.isAlwaysOnVpnEnabled),
|
||||||
.fillMaxWidth()
|
checked = settings.isTunnelOnEthernetEnabled,
|
||||||
.padding(screenPadding),
|
padding = screenPadding,
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
onCheckChanged = {
|
||||||
horizontalArrangement = Arrangement.SpaceBetween
|
|
||||||
) {
|
|
||||||
Text(stringResource(R.string.always_on_vpn_support))
|
|
||||||
Switch(
|
|
||||||
enabled = !settings.isAutoTunnelEnabled,
|
|
||||||
checked = settings.isAlwaysOnVpnEnabled,
|
|
||||||
onCheckedChange = {
|
|
||||||
scope.launch {
|
scope.launch {
|
||||||
viewModel.onToggleAlwaysOnVPN()
|
viewModel.onToggleTunnelOnEthernet()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
ConfigurationToggle(
|
||||||
|
stringResource(R.string.battery_saver),
|
||||||
|
enabled = !(settings.isAutoTunnelEnabled || settings.isAlwaysOnVpnEnabled),
|
||||||
|
checked = settings.isBatterySaverEnabled,
|
||||||
|
padding = screenPadding,
|
||||||
|
onCheckChanged = {
|
||||||
|
scope.launch {
|
||||||
|
viewModel.onToggleBatterySaver()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(top = 5.dp),
|
||||||
|
horizontalArrangement = Arrangement.Center
|
||||||
|
) {
|
||||||
|
TextButton(
|
||||||
|
enabled = !settings.isAlwaysOnVpnEnabled,
|
||||||
|
onClick = {
|
||||||
|
if (!isAllAutoTunnelPermissionsEnabled() && settings.isTunnelOnWifiEnabled) {
|
||||||
|
val message =
|
||||||
|
if (!isBackgroundLocationGranted) {
|
||||||
|
context.getString(R.string.background_location_required)
|
||||||
|
} else if (viewModel.isLocationServicesNeeded()) {
|
||||||
|
context.getString(R.string.location_services_required)
|
||||||
|
} else {
|
||||||
|
context.getString(R.string.precise_location_required)
|
||||||
|
}
|
||||||
|
showSnackbarMessage(message)
|
||||||
|
} else {
|
||||||
|
scope.launch {
|
||||||
|
viewModel.toggleAutoTunnel()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
val autoTunnelButtonText =
|
||||||
|
if (settings.isAutoTunnelEnabled) {
|
||||||
|
stringResource(R.string.disable_auto_tunnel)
|
||||||
|
} else {
|
||||||
|
stringResource(id = R.string.enable_auto_tunnel)
|
||||||
|
}
|
||||||
|
Text(autoTunnelButtonText)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (WgQuickBackend.hasKernelSupport()) {
|
||||||
|
Surface(
|
||||||
|
tonalElevation = 2.dp,
|
||||||
|
shadowElevation = 2.dp,
|
||||||
|
shape = RoundedCornerShape(12.dp),
|
||||||
|
color = MaterialTheme.colorScheme.surface,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth(fillMaxWidth)
|
||||||
|
.padding(vertical = 10.dp)
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
horizontalAlignment = Alignment.Start,
|
||||||
|
verticalArrangement = Arrangement.Top,
|
||||||
|
modifier = Modifier.padding(15.dp)
|
||||||
|
) {
|
||||||
|
SectionTitle(
|
||||||
|
title = stringResource(id = R.string.kernel),
|
||||||
|
padding = screenPadding
|
||||||
|
)
|
||||||
|
ConfigurationToggle(
|
||||||
|
stringResource(R.string.use_kernel),
|
||||||
|
enabled = !(
|
||||||
|
settings.isAutoTunnelEnabled || settings.isAlwaysOnVpnEnabled ||
|
||||||
|
(vpnState.value == Tunnel.State.UP)
|
||||||
|
),
|
||||||
|
checked = settings.isKernelEnabled,
|
||||||
|
padding = screenPadding,
|
||||||
|
onCheckChanged = {
|
||||||
|
scope.launch {
|
||||||
|
try {
|
||||||
|
viewModel.onToggleKernelMode()
|
||||||
|
} catch (e: WgTunnelException) {
|
||||||
|
showSnackbarMessage(e.message)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (!WireGuardAutoTunnel.isRunningOnAndroidTv(context)) {
|
||||||
|
Surface(
|
||||||
|
tonalElevation = 2.dp,
|
||||||
|
shadowElevation = 2.dp,
|
||||||
|
shape = RoundedCornerShape(12.dp),
|
||||||
|
color = MaterialTheme.colorScheme.surface,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth(fillMaxWidth).padding(vertical = 10.dp)
|
||||||
|
.padding(bottom = 140.dp)
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
horizontalAlignment = Alignment.Start,
|
||||||
|
verticalArrangement = Arrangement.Top,
|
||||||
|
modifier = Modifier.padding(15.dp)
|
||||||
|
) {
|
||||||
|
SectionTitle(
|
||||||
|
title = stringResource(id = R.string.other),
|
||||||
|
padding = screenPadding
|
||||||
|
)
|
||||||
|
ConfigurationToggle(
|
||||||
|
stringResource(R.string.always_on_vpn_support),
|
||||||
|
enabled = !settings.isAutoTunnelEnabled,
|
||||||
|
checked = settings.isAlwaysOnVpnEnabled,
|
||||||
|
padding = screenPadding,
|
||||||
|
onCheckChanged = {
|
||||||
|
scope.launch {
|
||||||
|
viewModel.onToggleAlwaysOnVPN()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
ConfigurationToggle(
|
||||||
|
stringResource(R.string.enabled_app_shortcuts),
|
||||||
|
enabled = true,
|
||||||
|
checked = settings.isShortcutsEnabled,
|
||||||
|
padding = screenPadding,
|
||||||
|
onCheckChanged = {
|
||||||
|
scope.launch {
|
||||||
|
viewModel.onToggleShortcutsEnabled()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(top = 5.dp),
|
||||||
|
horizontalArrangement = Arrangement.Center
|
||||||
|
) {
|
||||||
|
TextButton(
|
||||||
|
enabled = !didExportFiles,
|
||||||
|
onClick = {
|
||||||
|
showAuthPrompt = true
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Text(stringResource(R.string.export_configs))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (WireGuardAutoTunnel.isRunningOnAndroidTv(context)) {
|
||||||
|
Spacer(modifier = Modifier.weight(.17f))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+129
-56
@@ -3,65 +3,74 @@ package com.zaneschepke.wireguardautotunnel.ui.screens.settings
|
|||||||
import android.app.Application
|
import android.app.Application
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.location.LocationManager
|
import android.location.LocationManager
|
||||||
|
import android.os.Build
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import com.zaneschepke.wireguardautotunnel.R
|
import com.wireguard.android.util.RootShell
|
||||||
import com.zaneschepke.wireguardautotunnel.repository.Repository
|
import com.zaneschepke.wireguardautotunnel.repository.SettingsDoa
|
||||||
|
import com.zaneschepke.wireguardautotunnel.repository.TunnelConfigDao
|
||||||
|
import com.zaneschepke.wireguardautotunnel.repository.datastore.DataStoreManager
|
||||||
|
import com.zaneschepke.wireguardautotunnel.repository.model.Settings
|
||||||
|
import com.zaneschepke.wireguardautotunnel.repository.model.TunnelConfig
|
||||||
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
|
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
|
||||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.model.Settings
|
import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnService
|
||||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.model.TunnelConfig
|
import com.zaneschepke.wireguardautotunnel.util.WgTunnelException
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.ViewState
|
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
|
import javax.inject.Inject
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.async
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
import kotlinx.coroutines.flow.filter
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import javax.inject.Inject
|
import timber.log.Timber
|
||||||
|
|
||||||
|
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
class SettingsViewModel @Inject constructor(private val application : Application,
|
class SettingsViewModel
|
||||||
private val tunnelRepo : Repository<TunnelConfig>, private val settingsRepo : Repository<Settings>
|
@Inject
|
||||||
|
constructor(
|
||||||
|
private val application: Application,
|
||||||
|
private val tunnelRepo: TunnelConfigDao,
|
||||||
|
private val settingsRepo: SettingsDoa,
|
||||||
|
val dataStoreManager: DataStoreManager,
|
||||||
|
private val rootShell: RootShell,
|
||||||
|
private val vpnService: VpnService
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
|
|
||||||
private val _trustedSSIDs = MutableStateFlow(emptyList<String>())
|
private val _trustedSSIDs = MutableStateFlow(emptyList<String>())
|
||||||
val trustedSSIDs = _trustedSSIDs.asStateFlow()
|
val trustedSSIDs = _trustedSSIDs.asStateFlow()
|
||||||
private val _settings = MutableStateFlow(Settings())
|
private val _settings = MutableStateFlow(Settings())
|
||||||
val settings get() = _settings.asStateFlow()
|
val settings get() = _settings.asStateFlow()
|
||||||
val tunnels get() = tunnelRepo.itemFlow
|
val vpnState get() = vpnService.state
|
||||||
private val _viewState = MutableStateFlow(ViewState())
|
val tunnels get() = tunnelRepo.getAllFlow()
|
||||||
val viewState get() = _viewState.asStateFlow()
|
val disclosureShown = dataStoreManager.locationDisclosureFlow
|
||||||
|
|
||||||
init {
|
init {
|
||||||
checkLocationServicesEnabled()
|
isLocationServicesEnabled()
|
||||||
viewModelScope.launch {
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
settingsRepo.itemFlow.collect {
|
settingsRepo.getAllFlow().filter { it.isNotEmpty() }.collect {
|
||||||
val settings = it.first()
|
val settings = it.first()
|
||||||
_settings.emit(settings)
|
_settings.emit(settings)
|
||||||
_trustedSSIDs.emit(settings.trustedNetworkSSIDs.toList())
|
_trustedSSIDs.emit(settings.trustedNetworkSSIDs.toList())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun onSaveTrustedSSID(ssid: String) {
|
suspend fun onSaveTrustedSSID(ssid: String) {
|
||||||
val trimmed = ssid.trim()
|
val trimmed = ssid.trim()
|
||||||
if (!_settings.value.trustedNetworkSSIDs.contains(trimmed)) {
|
if (!_settings.value.trustedNetworkSSIDs.contains(trimmed)) {
|
||||||
_settings.value.trustedNetworkSSIDs.add(trimmed)
|
_settings.value.trustedNetworkSSIDs.add(trimmed)
|
||||||
settingsRepo.save(_settings.value)
|
settingsRepo.save(_settings.value)
|
||||||
} else {
|
} else {
|
||||||
showSnackBarMessage("SSID already exists.")
|
throw WgTunnelException("SSID already exists.")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun onDefaultTunnelSelected(tunnelConfig: TunnelConfig) {
|
|
||||||
settingsRepo.save(_settings.value.copy(
|
|
||||||
defaultTunnel = tunnelConfig.toString()
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun onToggleTunnelOnMobileData() {
|
suspend fun onToggleTunnelOnMobileData() {
|
||||||
settingsRepo.save(_settings.value.copy(
|
settingsRepo.save(
|
||||||
|
_settings.value.copy(
|
||||||
isTunnelOnMobileDataEnabled = !_settings.value.isTunnelOnMobileDataEnabled
|
isTunnelOnMobileDataEnabled = !_settings.value.isTunnelOnMobileDataEnabled
|
||||||
))
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun onDeleteTrustedSSID(ssid: String) {
|
suspend fun onDeleteTrustedSSID(ssid: String) {
|
||||||
@@ -69,56 +78,120 @@ class SettingsViewModel @Inject constructor(private val application : Applicatio
|
|||||||
settingsRepo.save(_settings.value)
|
settingsRepo.save(_settings.value)
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun toggleAutoTunnel() {
|
private fun emitFirstTunnelAsDefault() =
|
||||||
if(_settings.value.defaultTunnel.isNullOrEmpty() && !_settings.value.isAutoTunnelEnabled) {
|
viewModelScope.async {
|
||||||
showSnackBarMessage(application.getString(R.string.select_tunnel_message))
|
_settings.emit(_settings.value.copy(defaultTunnel = getFirstTunnelConfig().toString()))
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun toggleAutoTunnel() {
|
||||||
if (_settings.value.isAutoTunnelEnabled) {
|
if (_settings.value.isAutoTunnelEnabled) {
|
||||||
ServiceManager.stopWatcherService(application)
|
ServiceManager.stopWatcherService(application)
|
||||||
} else {
|
} else {
|
||||||
if(_settings.value.defaultTunnel != null) {
|
if (_settings.value.defaultTunnel == null) {
|
||||||
|
emitFirstTunnelAsDefault().await()
|
||||||
|
}
|
||||||
val defaultTunnel = _settings.value.defaultTunnel
|
val defaultTunnel = _settings.value.defaultTunnel
|
||||||
ServiceManager.startWatcherService(application, defaultTunnel!!)
|
ServiceManager.startWatcherService(application, defaultTunnel!!)
|
||||||
}
|
}
|
||||||
}
|
settingsRepo.save(
|
||||||
settingsRepo.save(_settings.value.copy(
|
_settings.value.copy(
|
||||||
isAutoTunnelEnabled = !_settings.value.isAutoTunnelEnabled
|
isAutoTunnelEnabled = !_settings.value.isAutoTunnelEnabled
|
||||||
))
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun showSnackBarMessage(message : String) {
|
private suspend fun getFirstTunnelConfig(): TunnelConfig {
|
||||||
_viewState.emit(_viewState.value.copy(
|
return tunnelRepo.getAll().first()
|
||||||
showSnackbarMessage = true,
|
|
||||||
snackbarMessage = message,
|
|
||||||
snackbarActionText = "Okay",
|
|
||||||
onSnackbarActionClick = {
|
|
||||||
viewModelScope.launch {
|
|
||||||
dismissSnackBar()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun dismissSnackBar() {
|
|
||||||
_viewState.emit(_viewState.value.copy(
|
|
||||||
showSnackbarMessage = false
|
|
||||||
))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun onToggleAlwaysOnVPN() {
|
suspend fun onToggleAlwaysOnVPN() {
|
||||||
if(_settings.value.defaultTunnel != null) {
|
if (_settings.value.defaultTunnel == null) {
|
||||||
|
emitFirstTunnelAsDefault().await()
|
||||||
|
}
|
||||||
|
val updatedSettings =
|
||||||
|
_settings.value.copy(
|
||||||
|
isAlwaysOnVpnEnabled = !_settings.value.isAlwaysOnVpnEnabled
|
||||||
|
)
|
||||||
|
emitSettings(updatedSettings)
|
||||||
|
saveSettings(updatedSettings)
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun emitSettings(settings: Settings) {
|
||||||
_settings.emit(
|
_settings.emit(
|
||||||
_settings.value.copy(isAlwaysOnVpnEnabled = !_settings.value.isAlwaysOnVpnEnabled)
|
settings
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun saveSettings(settings: Settings) {
|
||||||
|
settingsRepo.save(settings)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun onToggleTunnelOnEthernet() {
|
||||||
|
if (_settings.value.defaultTunnel == null) {
|
||||||
|
emitFirstTunnelAsDefault().await()
|
||||||
|
}
|
||||||
|
_settings.emit(
|
||||||
|
_settings.value.copy(
|
||||||
|
isTunnelOnEthernetEnabled = !_settings.value.isTunnelOnEthernetEnabled
|
||||||
|
)
|
||||||
)
|
)
|
||||||
settingsRepo.save(_settings.value)
|
settingsRepo.save(_settings.value)
|
||||||
} else {
|
|
||||||
showSnackBarMessage(application.getString(R.string.select_tunnel_message))
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
fun checkLocationServicesEnabled() : Boolean {
|
private fun isLocationServicesEnabled(): Boolean {
|
||||||
val locationManager =
|
val locationManager =
|
||||||
application.getSystemService(Context.LOCATION_SERVICE) as LocationManager
|
application.getSystemService(Context.LOCATION_SERVICE) as LocationManager
|
||||||
return locationManager.isProviderEnabled(LocationManager.NETWORK_PROVIDER)
|
return locationManager.isProviderEnabled(LocationManager.NETWORK_PROVIDER)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun isLocationServicesNeeded(): Boolean {
|
||||||
|
return (!isLocationServicesEnabled() && Build.VERSION.SDK_INT > Build.VERSION_CODES.P)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun onToggleShortcutsEnabled() {
|
||||||
|
settingsRepo.save(
|
||||||
|
_settings.value.copy(
|
||||||
|
isShortcutsEnabled = !_settings.value.isShortcutsEnabled
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun onToggleBatterySaver() {
|
||||||
|
settingsRepo.save(
|
||||||
|
_settings.value.copy(
|
||||||
|
isBatterySaverEnabled = !_settings.value.isBatterySaverEnabled
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun saveKernelMode(on: Boolean) {
|
||||||
|
settingsRepo.save(
|
||||||
|
_settings.value.copy(
|
||||||
|
isKernelEnabled = on
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun onToggleKernelMode() {
|
||||||
|
if (!_settings.value.isKernelEnabled) {
|
||||||
|
try {
|
||||||
|
rootShell.start()
|
||||||
|
Timber.d("Root shell accepted!")
|
||||||
|
saveKernelMode(on = true)
|
||||||
|
} catch (e: RootShell.RootShellException) {
|
||||||
|
saveKernelMode(on = false)
|
||||||
|
throw WgTunnelException("Root shell denied!")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
saveKernelMode(on = false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun onToggleTunnelOnWifi() {
|
||||||
|
settingsRepo.save(
|
||||||
|
_settings.value.copy(
|
||||||
|
isTunnelOnWifiEnabled = !_settings.value.isTunnelOnWifiEnabled
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
+189
-28
@@ -1,26 +1,36 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.ui.screens.support
|
package com.zaneschepke.wireguardautotunnel.ui.screens.support
|
||||||
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.pm.PackageManager
|
import android.content.Intent.createChooser
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.focusable
|
import androidx.compose.foundation.focusable
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.IntrinsicSize
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.rounded.ArrowForward
|
||||||
|
import androidx.compose.material.icons.rounded.Book
|
||||||
|
import androidx.compose.material.icons.rounded.Mail
|
||||||
|
import androidx.compose.material3.Divider
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.IconButton
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Surface
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TextButton
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.focus.FocusRequester
|
import androidx.compose.ui.focus.FocusRequester
|
||||||
@@ -34,12 +44,25 @@ import androidx.compose.ui.text.style.TextAlign
|
|||||||
import androidx.compose.ui.text.style.TextDecoration
|
import androidx.compose.ui.text.style.TextDecoration
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
|
import androidx.core.content.ContextCompat.startActivity
|
||||||
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
|
import com.zaneschepke.wireguardautotunnel.BuildConfig
|
||||||
|
import com.zaneschepke.wireguardautotunnel.Constants
|
||||||
import com.zaneschepke.wireguardautotunnel.R
|
import com.zaneschepke.wireguardautotunnel.R
|
||||||
|
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
|
||||||
|
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.SettingsViewModel
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun SupportScreen(padding : PaddingValues, focusRequester: FocusRequester) {
|
fun SupportScreen(
|
||||||
|
viewModel: SettingsViewModel = hiltViewModel(),
|
||||||
|
padding: PaddingValues,
|
||||||
|
focusRequester: FocusRequester
|
||||||
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
|
val fillMaxWidth = .85f
|
||||||
|
|
||||||
|
val settings by viewModel.settings.collectAsStateWithLifecycle()
|
||||||
|
|
||||||
fun openWebPage(url: String) {
|
fun openWebPage(url: String) {
|
||||||
val webpage: Uri = Uri.parse(url)
|
val webpage: Uri = Uri.parse(url)
|
||||||
@@ -47,37 +70,175 @@ fun SupportScreen(padding : PaddingValues, focusRequester: FocusRequester) {
|
|||||||
context.startActivity(intent)
|
context.startActivity(intent)
|
||||||
}
|
}
|
||||||
|
|
||||||
Column(horizontalAlignment = Alignment.CenterHorizontally,
|
fun launchEmail() {
|
||||||
|
val intent =
|
||||||
|
Intent(Intent.ACTION_SEND).apply {
|
||||||
|
type = Constants.EMAIL_MIME_TYPE
|
||||||
|
putExtra(Intent.EXTRA_EMAIL, context.getString(R.string.my_email))
|
||||||
|
putExtra(Intent.EXTRA_SUBJECT, context.getString(R.string.email_subject))
|
||||||
|
}
|
||||||
|
startActivity(
|
||||||
|
context,
|
||||||
|
createChooser(intent, context.getString(R.string.email_chooser)),
|
||||||
|
null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Column(
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
verticalArrangement = Arrangement.Top,
|
verticalArrangement = Arrangement.Top,
|
||||||
modifier = Modifier
|
modifier =
|
||||||
|
Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.verticalScroll(rememberScrollState())
|
.verticalScroll(rememberScrollState())
|
||||||
.focusable()
|
.focusable()
|
||||||
.padding(padding)) {
|
.padding(padding)
|
||||||
Text(stringResource(R.string.support_text), textAlign = TextAlign.Center, modifier = Modifier.padding(30.dp), fontSize = 15.sp)
|
|
||||||
Row(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(14.dp),
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
horizontalArrangement = Arrangement.SpaceEvenly
|
|
||||||
) {
|
) {
|
||||||
IconButton(onClick = {
|
Surface(
|
||||||
openWebPage(context.resources.getString(R.string.discord_url))
|
tonalElevation = 2.dp,
|
||||||
}) {
|
shadowElevation = 2.dp,
|
||||||
Icon(imageVector = ImageVector.vectorResource(R.drawable.discord), "Discord")
|
shape = RoundedCornerShape(12.dp),
|
||||||
|
color = MaterialTheme.colorScheme.surface,
|
||||||
|
modifier =
|
||||||
|
(
|
||||||
|
if (WireGuardAutoTunnel.isRunningOnAndroidTv(context)) {
|
||||||
|
Modifier
|
||||||
|
.height(IntrinsicSize.Min)
|
||||||
|
.fillMaxWidth(fillMaxWidth)
|
||||||
|
.padding(top = 10.dp)
|
||||||
|
} else {
|
||||||
|
Modifier
|
||||||
|
.fillMaxWidth(fillMaxWidth)
|
||||||
|
.padding(top = 20.dp)
|
||||||
|
}
|
||||||
|
).padding(bottom = 25.dp)
|
||||||
|
) {
|
||||||
|
Column(modifier = Modifier.padding(20.dp)) {
|
||||||
|
Text(
|
||||||
|
stringResource(R.string.thank_you),
|
||||||
|
textAlign = TextAlign.Start,
|
||||||
|
modifier = Modifier.padding(bottom = 20.dp),
|
||||||
|
fontSize = 16.sp
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
stringResource(id = R.string.support_help_text),
|
||||||
|
textAlign = TextAlign.Start,
|
||||||
|
fontSize = 16.sp,
|
||||||
|
modifier = Modifier.padding(bottom = 20.dp)
|
||||||
|
)
|
||||||
|
TextButton(onClick = {
|
||||||
|
openWebPage(context.resources.getString(R.string.docs_url))
|
||||||
|
}, modifier = Modifier.padding(vertical = 5.dp).focusRequester(focusRequester)) {
|
||||||
|
Row(
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
) {
|
||||||
|
Row {
|
||||||
|
Icon(Icons.Rounded.Book, stringResource(id = R.string.docs))
|
||||||
|
Text(
|
||||||
|
stringResource(id = R.string.docs_description),
|
||||||
|
textAlign = TextAlign.Justify,
|
||||||
|
modifier = Modifier.padding(start = 10.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Icon(Icons.Rounded.ArrowForward, stringResource(id = R.string.go))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Divider(color = MaterialTheme.colorScheme.onBackground, thickness = 0.5.dp)
|
||||||
|
TextButton(
|
||||||
|
onClick = { openWebPage(context.resources.getString(R.string.discord_url)) },
|
||||||
|
modifier = Modifier.padding(vertical = 5.dp)
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
) {
|
||||||
|
Row {
|
||||||
|
Icon(
|
||||||
|
imageVector = ImageVector.vectorResource(R.drawable.discord),
|
||||||
|
stringResource(
|
||||||
|
id = R.string.discord
|
||||||
|
),
|
||||||
|
Modifier.size(25.dp)
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
stringResource(id = R.string.discord_description),
|
||||||
|
textAlign = TextAlign.Justify,
|
||||||
|
modifier = Modifier.padding(start = 10.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Icon(Icons.Rounded.ArrowForward, stringResource(id = R.string.go))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Divider(color = MaterialTheme.colorScheme.onBackground, thickness = 0.5.dp)
|
||||||
|
TextButton(
|
||||||
|
onClick = { openWebPage(context.resources.getString(R.string.github_url)) },
|
||||||
|
modifier = Modifier.padding(vertical = 5.dp)
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
) {
|
||||||
|
Row {
|
||||||
|
Icon(
|
||||||
|
imageVector = ImageVector.vectorResource(R.drawable.github),
|
||||||
|
stringResource(
|
||||||
|
id = R.string.github
|
||||||
|
),
|
||||||
|
Modifier.size(25.dp)
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
"Open an issue",
|
||||||
|
textAlign = TextAlign.Justify,
|
||||||
|
modifier = Modifier.padding(start = 10.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Icon(Icons.Rounded.ArrowForward, stringResource(id = R.string.go))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Divider(color = MaterialTheme.colorScheme.onBackground, thickness = 0.5.dp)
|
||||||
|
TextButton(
|
||||||
|
onClick = { launchEmail() },
|
||||||
|
modifier = Modifier.padding(vertical = 5.dp)
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
) {
|
||||||
|
Row {
|
||||||
|
Icon(Icons.Rounded.Mail, stringResource(id = R.string.email))
|
||||||
|
Text(
|
||||||
|
stringResource(id = R.string.email_description),
|
||||||
|
textAlign = TextAlign.Justify,
|
||||||
|
modifier = Modifier.padding(start = 10.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Icon(Icons.Rounded.ArrowForward, stringResource(id = R.string.go))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
IconButton(modifier = Modifier.focusRequester(focusRequester),onClick = {
|
|
||||||
openWebPage(context.resources.getString(R.string.github_url))
|
|
||||||
}) {
|
|
||||||
Icon(imageVector = ImageVector.vectorResource(R.drawable.github), "Github")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Spacer(modifier = Modifier.weight(1f))
|
Spacer(modifier = Modifier.weight(1f))
|
||||||
Text(stringResource(id = R.string.privacy_policy), style = TextStyle(textDecoration = TextDecoration.Underline),
|
Text(
|
||||||
modifier = Modifier.clickable {
|
stringResource(id = R.string.privacy_policy),
|
||||||
|
style = TextStyle(textDecoration = TextDecoration.Underline),
|
||||||
|
fontSize = 16.sp,
|
||||||
|
modifier =
|
||||||
|
Modifier.clickable {
|
||||||
openWebPage(context.resources.getString(R.string.privacy_policy_url))
|
openWebPage(context.resources.getString(R.string.privacy_policy_url))
|
||||||
})
|
}
|
||||||
Text("App version: ${com.zaneschepke.wireguardautotunnel.BuildConfig.VERSION_NAME}", Modifier.padding(25.dp))
|
)
|
||||||
|
Row(
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(25.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
modifier = Modifier.padding(25.dp)
|
||||||
|
) {
|
||||||
|
Text("Version: ${BuildConfig.VERSION_NAME}")
|
||||||
|
Text("Mode: ${if (settings.isKernelEnabled) "Kernel" else "Userspace" }")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+25
@@ -0,0 +1,25 @@
|
|||||||
|
package com.zaneschepke.wireguardautotunnel.ui.screens.support
|
||||||
|
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import com.zaneschepke.wireguardautotunnel.repository.SettingsDoa
|
||||||
|
import com.zaneschepke.wireguardautotunnel.repository.model.Settings
|
||||||
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
|
import javax.inject.Inject
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
@HiltViewModel
|
||||||
|
class SupportViewModel @Inject constructor(
|
||||||
|
private val settingsRepo: SettingsDoa
|
||||||
|
) : ViewModel() {
|
||||||
|
private val _settings = MutableStateFlow(Settings())
|
||||||
|
val settings get() = _settings.asStateFlow()
|
||||||
|
init {
|
||||||
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
|
_settings.value = settingsRepo.getAll().first()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -13,5 +13,6 @@ val Pink40 = Color(0xFFFFFFFF)
|
|||||||
|
|
||||||
// status colors
|
// status colors
|
||||||
val brickRed = Color(0xFFCE4257)
|
val brickRed = Color(0xFFCE4257)
|
||||||
|
val corn = Color(0xFFFBEC5D)
|
||||||
val pinkRed = Color(0xFFEF476F)
|
val pinkRed = Color(0xFFEF476F)
|
||||||
val mint = Color(0xFF52B788)
|
val mint = Color(0xFF52B788)
|
||||||
|
|||||||
@@ -15,7 +15,8 @@ import androidx.compose.ui.platform.LocalContext
|
|||||||
import androidx.compose.ui.platform.LocalView
|
import androidx.compose.ui.platform.LocalView
|
||||||
import androidx.core.view.WindowCompat
|
import androidx.core.view.WindowCompat
|
||||||
|
|
||||||
private val DarkColorScheme = darkColorScheme(
|
private val DarkColorScheme =
|
||||||
|
darkColorScheme(
|
||||||
// primary = Purple80,
|
// primary = Purple80,
|
||||||
primary = virdigris,
|
primary = virdigris,
|
||||||
secondary = virdigris,
|
secondary = virdigris,
|
||||||
@@ -24,11 +25,11 @@ private val DarkColorScheme = darkColorScheme(
|
|||||||
// tertiary = Pink80
|
// tertiary = Pink80
|
||||||
)
|
)
|
||||||
|
|
||||||
private val LightColorScheme = lightColorScheme(
|
private val LightColorScheme =
|
||||||
|
lightColorScheme(
|
||||||
primary = Purple40,
|
primary = Purple40,
|
||||||
secondary = PurpleGrey40,
|
secondary = PurpleGrey40,
|
||||||
tertiary = Pink40
|
tertiary = Pink40
|
||||||
|
|
||||||
/* Other default colors to override
|
/* Other default colors to override
|
||||||
background = Color(0xFFFFFBFE),
|
background = Color(0xFFFFFBFE),
|
||||||
surface = Color(0xFFFFFBFE),
|
surface = Color(0xFFFFFBFE),
|
||||||
@@ -50,8 +51,8 @@ fun WireguardAutoTunnelTheme(
|
|||||||
dynamicColor: Boolean = false,
|
dynamicColor: Boolean = false,
|
||||||
content: @Composable () -> Unit
|
content: @Composable () -> Unit
|
||||||
) {
|
) {
|
||||||
|
val colorScheme =
|
||||||
val colorScheme = when {
|
when {
|
||||||
dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
|
dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
|
if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
|
||||||
|
|||||||
@@ -7,8 +7,10 @@ import androidx.compose.ui.text.font.FontWeight
|
|||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
|
|
||||||
// Set of Material typography styles to start with
|
// Set of Material typography styles to start with
|
||||||
val Typography = Typography(
|
val Typography =
|
||||||
bodyLarge = TextStyle(
|
Typography(
|
||||||
|
bodyLarge =
|
||||||
|
TextStyle(
|
||||||
fontFamily = FontFamily.Default,
|
fontFamily = FontFamily.Default,
|
||||||
fontWeight = FontWeight.Normal,
|
fontWeight = FontWeight.Normal,
|
||||||
fontSize = 16.sp,
|
fontSize = 16.sp,
|
||||||
|
|||||||
@@ -0,0 +1,66 @@
|
|||||||
|
package com.zaneschepke.wireguardautotunnel.util
|
||||||
|
|
||||||
|
import android.content.ContentValues
|
||||||
|
import android.content.Context
|
||||||
|
import android.os.Build
|
||||||
|
import android.os.Environment
|
||||||
|
import android.provider.MediaStore
|
||||||
|
import android.provider.MediaStore.MediaColumns
|
||||||
|
import com.zaneschepke.wireguardautotunnel.Constants
|
||||||
|
import java.io.File
|
||||||
|
import java.io.OutputStream
|
||||||
|
import java.time.Instant
|
||||||
|
import java.util.zip.ZipEntry
|
||||||
|
import java.util.zip.ZipOutputStream
|
||||||
|
|
||||||
|
object FileUtils {
|
||||||
|
private const val ZIP_FILE_MIME_TYPE = "application/zip"
|
||||||
|
|
||||||
|
private fun createDownloadsFileOutputStream(
|
||||||
|
context: Context,
|
||||||
|
fileName: String,
|
||||||
|
mimeType: String = Constants.ALLOWED_FILE_TYPES
|
||||||
|
): OutputStream? {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||||
|
val resolver = context.contentResolver
|
||||||
|
val contentValues =
|
||||||
|
ContentValues().apply {
|
||||||
|
put(MediaColumns.DISPLAY_NAME, fileName)
|
||||||
|
put(MediaColumns.MIME_TYPE, mimeType)
|
||||||
|
put(MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_DOWNLOADS)
|
||||||
|
}
|
||||||
|
val uri = resolver.insert(MediaStore.Downloads.EXTERNAL_CONTENT_URI, contentValues)
|
||||||
|
if (uri != null) {
|
||||||
|
return resolver.openOutputStream(uri)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
val target =
|
||||||
|
File(
|
||||||
|
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS),
|
||||||
|
fileName
|
||||||
|
)
|
||||||
|
return target.outputStream()
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
fun saveFilesToZip(
|
||||||
|
context: Context,
|
||||||
|
files: List<File>
|
||||||
|
) {
|
||||||
|
val zipOutputStream = createDownloadsFileOutputStream(
|
||||||
|
context,
|
||||||
|
"wg-export_${Instant.now().epochSecond}.zip",
|
||||||
|
ZIP_FILE_MIME_TYPE
|
||||||
|
)
|
||||||
|
ZipOutputStream(zipOutputStream).use { zos ->
|
||||||
|
files.forEach { file ->
|
||||||
|
val entry = ZipEntry(file.name)
|
||||||
|
zos.putNextEntry(entry)
|
||||||
|
if (file.isFile) {
|
||||||
|
file.inputStream().use { fis -> fis.copyTo(zos) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,25 +1,33 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.util
|
package com.zaneschepke.wireguardautotunnel.util
|
||||||
|
|
||||||
import java.math.BigDecimal
|
import java.math.BigDecimal
|
||||||
import java.text.DecimalFormat
|
|
||||||
import java.time.Duration
|
import java.time.Duration
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
|
import kotlin.math.pow
|
||||||
|
|
||||||
object NumberUtils {
|
object NumberUtils {
|
||||||
|
private const val BYTES_IN_KB = 1024.0
|
||||||
|
private val BYTES_IN_MB = BYTES_IN_KB.pow(2.0)
|
||||||
|
private val keyValidationRegex = """^[A-Za-z0-9+/]{42}[AEIMQUYcgkosw480]=${'$'}""".toRegex()
|
||||||
|
|
||||||
private const val BYTES_IN_KB = 1024L
|
fun bytesToMB(bytes: Long): BigDecimal {
|
||||||
|
return bytes.toBigDecimal().divide(BYTES_IN_MB.toBigDecimal())
|
||||||
fun bytesToKB(bytes : Long) : BigDecimal {
|
|
||||||
return bytes.toBigDecimal().divide(BYTES_IN_KB.toBigDecimal())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun formatDecimalTwoPlaces(bigDecimal: BigDecimal) : String {
|
fun isValidKey(key: String): Boolean {
|
||||||
val df = DecimalFormat("#.##")
|
return key.matches(keyValidationRegex)
|
||||||
return df.format(bigDecimal)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getSecondsBetweenTimestampAndNow(epoch : Long) : Long {
|
fun generateRandomTunnelName(): String {
|
||||||
|
return "tunnel${(Math.random() * 100000).toInt()}"
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getSecondsBetweenTimestampAndNow(epoch: Long): Long? {
|
||||||
|
return if (epoch != 0L) {
|
||||||
val time = Instant.ofEpochMilli(epoch)
|
val time = Instant.ofEpochMilli(epoch)
|
||||||
return Duration.between(time, Instant.now()).seconds
|
return Duration.between(time, Instant.now()).seconds
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
package com.zaneschepke.wireguardautotunnel.util
|
||||||
|
|
||||||
|
import com.wireguard.config.BadConfigException
|
||||||
|
|
||||||
|
class WgTunnelException(e: Exception) : Exception() {
|
||||||
|
constructor(message: String) : this(Exception(message))
|
||||||
|
|
||||||
|
override val message: String = generateExceptionMessage(e)
|
||||||
|
|
||||||
|
private fun generateExceptionMessage(e: Exception): String {
|
||||||
|
return when (e) {
|
||||||
|
is BadConfigException -> "${e.section.name} ${e.location.name} ${e.reason.name}"
|
||||||
|
else -> e.message ?: "Unknown error occurred"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,9 +6,10 @@
|
|||||||
<string name="watcher_channel_id">Watcher Channel</string>
|
<string name="watcher_channel_id">Watcher Channel</string>
|
||||||
<string name="watcher_channel_name">Watcher Notification Channel</string>
|
<string name="watcher_channel_name">Watcher Notification Channel</string>
|
||||||
<string name="foreground_file">FOREGROUND_FILE</string>
|
<string name="foreground_file">FOREGROUND_FILE</string>
|
||||||
<string name="github_url">https://github.com/zaneschepke/wgtunnel</string>
|
<string name="github_url">https://github.com/zaneschepke/wgtunnel/issues</string>
|
||||||
<string name="privacy_policy_url">https://zaneschepke.github.io/wgtunnel/</string>
|
<string name="docs_url">https://zaneschepke.com/wgtunnel-docs/overview.html</string>
|
||||||
<string name="file_extension_message">File is not a .conf file</string>
|
<string name="privacy_policy_url">https://zaneschepke.com/wgtunnel-docs/privacypolicy.html</string>
|
||||||
|
<string name="file_extension_message">File is not a .conf or .zip</string>
|
||||||
<string name="turn_off_tunnel">Turn off tunnel before editing</string>
|
<string name="turn_off_tunnel">Turn off tunnel before editing</string>
|
||||||
<string name="no_tunnels">No tunnels added yet!</string>
|
<string name="no_tunnels">No tunnels added yet!</string>
|
||||||
<string name="tunnel_exists">Tunnel name already exists</string>
|
<string name="tunnel_exists">Tunnel name already exists</string>
|
||||||
@@ -20,11 +21,10 @@
|
|||||||
<string name="vpn_permission_required">VPN permission is required for the app to work properly. If this permission is not launching, please disable \"Always-on VPN\" in your phone settings for the official WireGuard mobile app and try again.</string>
|
<string name="vpn_permission_required">VPN permission is required for the app to work properly. If this permission is not launching, please disable \"Always-on VPN\" in your phone settings for the official WireGuard mobile app and try again.</string>
|
||||||
<string name="notification_permission_required">Notifications permission is required for the app to work properly.</string>
|
<string name="notification_permission_required">Notifications permission is required for the app to work properly.</string>
|
||||||
<string name="open_settings">Open Settings</string>
|
<string name="open_settings">Open Settings</string>
|
||||||
<string name="add_trusted_ssid">Add Trusted SSID</string>
|
<string name="add_trusted_ssid">Add trusted wifi name</string>
|
||||||
<string name="trusted_ssid">Trusted SSID</string>
|
|
||||||
<string name="tunnels">Tunnels</string>
|
<string name="tunnels">Tunnels</string>
|
||||||
<string name="select_tunnel">Select Tunnel</string>
|
<string name="enable_auto_tunnel">Start auto-tunneling</string>
|
||||||
<string name="enable_auto_tunnel">Enable auto tunneling</string>
|
<string name="disable_auto_tunnel">Stop auto-tunneling</string>
|
||||||
<string name="tunnel_mobile_data">Tunnel on mobile data</string>
|
<string name="tunnel_mobile_data">Tunnel on mobile data</string>
|
||||||
<string name="background_location_reason">\"Allow all the time\" location permission is required for retrieving Wi-Fi SSID in the background. Permission is needed for this feature.</string>
|
<string name="background_location_reason">\"Allow all the time\" location permission is required for retrieving Wi-Fi SSID in the background. Permission is needed for this feature.</string>
|
||||||
<string name="location_permission_reason">Location permission is required for this feature to work properly.</string>
|
<string name="location_permission_reason">Location permission is required for this feature to work properly.</string>
|
||||||
@@ -32,16 +32,16 @@
|
|||||||
<string name="retry">"Retry"</string>
|
<string name="retry">"Retry"</string>
|
||||||
<string name="privacy_policy">View Privacy Policy</string>
|
<string name="privacy_policy">View Privacy Policy</string>
|
||||||
<string name="okay">Okay</string>
|
<string name="okay">Okay</string>
|
||||||
|
<string name="tunnel_on_ethernet">Tunnel on ethernet</string>
|
||||||
<string name="prominent_background_location_message">This feature requires background location permission to enable Wi-Fi SSID monitoring even while the application is closed. For more details, please see the Privacy Policy linked on the Support screen.</string>
|
<string name="prominent_background_location_message">This feature requires background location permission to enable Wi-Fi SSID monitoring even while the application is closed. For more details, please see the Privacy Policy linked on the Support screen.</string>
|
||||||
<string name="prominent_background_location_title">Background Location Disclosure</string>
|
<string name="prominent_background_location_title">Background Location Disclosure</string>
|
||||||
<string name="support_text">Thank you for using WG Tunnel! If you are experiencing issues with the app, please reach out on Discord or create an issue on Github. I will try to address the issue as quickly as possible. Thank you!</string>
|
<string name="thank_you">Thank you for using WG Tunnel!</string>
|
||||||
<string name="trusted_ssid_empty_description">Enter SSID</string>
|
<string name="trusted_ssid_empty_description">Enter SSID</string>
|
||||||
<string name="trusted_ssid_value_description">Submit SSID</string>
|
<string name="trusted_ssid_value_description">Submit SSID</string>
|
||||||
<string name="config_validation">[Interface]</string>
|
<string name="config_validation">[Interface]</string>
|
||||||
<string name="invalid_qr">Invalid QR code.</string>
|
<string name="add_tunnels_text">Add from file or zip</string>
|
||||||
<string name="add_from_file">Add tunnel from files</string>
|
|
||||||
<string name="open_file">File Open</string>
|
<string name="open_file">File Open</string>
|
||||||
<string name="add_from_qr">Add tunnel from QR code</string>
|
<string name="add_from_qr">Add from QR code</string>
|
||||||
<string name="qr_scan">QR Scan</string>
|
<string name="qr_scan">QR Scan</string>
|
||||||
<string name="tunnel_edit">Tunnel Edit</string>
|
<string name="tunnel_edit">Tunnel Edit</string>
|
||||||
<string name="tunnel_name">Tunnel Name</string>
|
<string name="tunnel_name">Tunnel Name</string>
|
||||||
@@ -52,7 +52,7 @@
|
|||||||
<string name="include">Include</string>
|
<string name="include">Include</string>
|
||||||
<string name="tunnel_all">Tunnel all applications</string>
|
<string name="tunnel_all">Tunnel all applications</string>
|
||||||
<string name="config_changes_saved">Configuration changes saved.</string>
|
<string name="config_changes_saved">Configuration changes saved.</string>
|
||||||
<string name="save_changes">Save changes</string>
|
<string name="save_changes">Save</string>
|
||||||
<string name="icon">Icon</string>
|
<string name="icon">Icon</string>
|
||||||
<string name="no_thanks">No thanks</string>
|
<string name="no_thanks">No thanks</string>
|
||||||
<string name="turn_on">Turn on</string>
|
<string name="turn_on">Turn on</string>
|
||||||
@@ -62,7 +62,6 @@
|
|||||||
<string name="public_key">Public key</string>
|
<string name="public_key">Public key</string>
|
||||||
<string name="barcode_downloading">Waiting for the Barcode UI module to be downloaded.</string>
|
<string name="barcode_downloading">Waiting for the Barcode UI module to be downloaded.</string>
|
||||||
<string name="barcode_downloading_message">Barcode module downloading. Try again.</string>
|
<string name="barcode_downloading_message">Barcode module downloading. Try again.</string>
|
||||||
<string name="barcode_error">Invalid QR code. Try again.</string>
|
|
||||||
<string name="addresses">Addresses</string>
|
<string name="addresses">Addresses</string>
|
||||||
<string name="dns_servers">DNS servers</string>
|
<string name="dns_servers">DNS servers</string>
|
||||||
<string name="mtu">MTU</string>
|
<string name="mtu">MTU</string>
|
||||||
@@ -77,7 +76,7 @@
|
|||||||
<string name="failed_connection_to">Failed connection to -</string>
|
<string name="failed_connection_to">Failed connection to -</string>
|
||||||
<string name="initial_connection_failure_message">Attempting to connect to server after 30 seconds of no response.</string>
|
<string name="initial_connection_failure_message">Attempting to connect to server after 30 seconds of no response.</string>
|
||||||
<string name="lost_connection_failure_message">Attempting to reconnect to server after more than one minute of no response.</string>
|
<string name="lost_connection_failure_message">Attempting to reconnect to server after more than one minute of no response.</string>
|
||||||
<string name="always_on_vpn_support">Enable Always-On VPN support</string>
|
<string name="always_on_vpn_support">Allow Always-On VPN </string>
|
||||||
<string name="select_tunnel_message">Please select a tunnel first</string>
|
<string name="select_tunnel_message">Please select a tunnel first</string>
|
||||||
<string name="location_services_not_detected">Unable to detect Location Services which are required for this feature. Please enable Location Services.</string>
|
<string name="location_services_not_detected">Unable to detect Location Services which are required for this feature. Please enable Location Services.</string>
|
||||||
<string name="check_again">Check again</string>
|
<string name="check_again">Check again</string>
|
||||||
@@ -91,4 +90,68 @@
|
|||||||
<string name="search_icon">Search Icon</string>
|
<string name="search_icon">Search Icon</string>
|
||||||
<string name="attempt_connection">Attempting connection..</string>
|
<string name="attempt_connection">Attempting connection..</string>
|
||||||
<string name="vpn_starting">VPN Starting</string>
|
<string name="vpn_starting">VPN Starting</string>
|
||||||
|
<string name="db_name">wg-tunnel-db</string>
|
||||||
|
<string name="scanning_qr">Scanning for QR</string>
|
||||||
|
<string name="qr_result_failed">QR scan failed</string>
|
||||||
|
<string name="none">No trusted wifi names</string>
|
||||||
|
<string name="never">Never</string>
|
||||||
|
<string name="stream_failed">Failed to open file stream.</string>
|
||||||
|
<string name="unknown_error_message">An unknown error occurred.</string>
|
||||||
|
<string name="no_file_app">No file app installed.</string>
|
||||||
|
<string name="other">Other</string>
|
||||||
|
<string name="auto_tunneling">Auto-tunneling</string>
|
||||||
|
<string name="select_tunnel">Select tunnel to use</string>
|
||||||
|
<string name="vpn_on">VPN on</string>
|
||||||
|
<string name="vpn_off">VPN off</string>
|
||||||
|
<string name="default_vpn_on">Primary VPN on</string>
|
||||||
|
<string name="default_vpn_off">Primary VPN off</string>
|
||||||
|
<string name="create_import">Create from scratch</string>
|
||||||
|
<string name="set_primary">Set primary</string>
|
||||||
|
<string name="turn_off_auto">Action requires auto-tunnel disabled</string>
|
||||||
|
<string name="add_peer">Add peer</string>
|
||||||
|
<string name="info">Info</string>
|
||||||
|
<string name="done">Done</string>
|
||||||
|
<string name="interface_">Interface</string>
|
||||||
|
<string name="rotate_keys">Rotate keys</string>
|
||||||
|
<string name="private_key">Private key</string>
|
||||||
|
<string name="copy_public_key">Copy public key</string>
|
||||||
|
<string name="base64_key">base64 key</string>
|
||||||
|
<string name="comma_separated_list">comma separated list</string>
|
||||||
|
<string name="listen_port">Listen port</string>
|
||||||
|
<string name="random">(random)</string>
|
||||||
|
<string name="auto">(auto)</string>
|
||||||
|
<string name="optional">(optional)</string>
|
||||||
|
<string name="optional_no_recommend">(optional, not recommended)</string>
|
||||||
|
<string name="preshared_key">Pre-shared key</string>
|
||||||
|
<string name="seconds">seconds</string>
|
||||||
|
<string name="persistent_keepalive">Persistent keepalive</string>
|
||||||
|
<string name="cancel">Cancel</string>
|
||||||
|
<string name="primary_tunnel_change">Primary tunnel change</string>
|
||||||
|
<string name="primary_tunnel_change_question">Would you like to make this your primary tunnel?</string>
|
||||||
|
<string name="authentication_failed">Authentication failed</string>
|
||||||
|
<string name="enabled_app_shortcuts">Enable app shortcuts</string>
|
||||||
|
<string name="export_configs">Export configs</string>
|
||||||
|
<string name="battery_saver">Battery saver (beta)</string>
|
||||||
|
<string name="location_services_required">Location services required</string>
|
||||||
|
<string name="background_location_required">Background location required</string>
|
||||||
|
<string name="precise_location_required">Precise location required</string>
|
||||||
|
<string name="unknown_error">Unknown error occurred</string>
|
||||||
|
<string name="exported_configs_message">Exported configs to downloads</string>
|
||||||
|
<string name="no_file_explorer">No file explorer installed</string>
|
||||||
|
<string name="status">status</string>
|
||||||
|
<string name="tunnel_on_wifi">Tunnel on untrusted wifi</string>
|
||||||
|
<string name="my_email">zanecschepke@gmail.com</string>
|
||||||
|
<string name="email_subject">WG Tunnel Support</string>
|
||||||
|
<string name="email_chooser">Send an email…</string>
|
||||||
|
<string name="go">go</string>
|
||||||
|
<string name="docs_description">Read the docs (WIP)</string>
|
||||||
|
<string name="discord_description">Join the community</string>
|
||||||
|
<string name="discord">Discord</string>
|
||||||
|
<string name="docs">Docs</string>
|
||||||
|
<string name="github">GitHub</string>
|
||||||
|
<string name="email">Email</string>
|
||||||
|
<string name="email_description">Send me an email</string>
|
||||||
|
<string name="support_help_text">If you are experiencing issues, have improvement ideas, or just want to engage, the following resources are available:</string>
|
||||||
|
<string name="kernel">Kernel</string>
|
||||||
|
<string name="use_kernel">Use kernel module</string>
|
||||||
</resources>
|
</resources>
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user