mirror of
https://github.com/wgtunnel/android.git
synced 2026-07-03 14:07:49 +02:00
Compare commits
135 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d43a219dce | |||
| 7e98691181 | |||
| b9becd433c | |||
| 2120810f5f | |||
| d8c2c24023 | |||
| 043865dcdc | |||
| 94d0227c42 | |||
| c56d314f32 | |||
| 795d18a44a | |||
| 1b987a8c80 | |||
| 4c647bd5c7 | |||
| 511b96dbc7 | |||
| acba25cbb2 | |||
| 87bb514da5 | |||
| d169385b5c | |||
| bcb010e3fd | |||
| 66101ed515 | |||
| 9e81a4d283 | |||
| d9ff97f83e | |||
| cf975a71d8 | |||
| ca555805b3 | |||
| 9c10e7c7b2 | |||
| f3fb4ab77c | |||
| 7a648fd930 | |||
| 2266cfc771 | |||
| 7166752d50 | |||
| f75e91fdc0 | |||
| 2b7d2d3da6 | |||
| 277068f6fb | |||
| 097b86db9b | |||
| a8ec89f59e | |||
| db993759e4 | |||
| 00a9f5ba8f | |||
| b3284339c1 | |||
| d67d524e7b | |||
| 6f1a1bdc48 | |||
| e5790f1fa0 | |||
| 525a12b2f2 | |||
| f083441370 | |||
| 2945fcbf75 | |||
| d379bf8960 | |||
| 4421d31f69 | |||
| 294081cec4 | |||
| 47617012c2 | |||
| 8cad5c6a14 | |||
| 8ac54c7a97 | |||
| 642f98fdaf | |||
| 91cb569585 | |||
| 4368bfdc62 | |||
| 9ac510eeff | |||
| ce08a5c308 | |||
| 5db281ba3e | |||
| 48917ad0b8 | |||
| d5c3983a5e | |||
| e0a75d8dad | |||
| eaf52b1476 | |||
| b1b906d863 | |||
| 351f8ba573 | |||
| 38168814b4 | |||
| 4214aeea37 | |||
| 85a0ca695a | |||
| 0a281bb257 | |||
| d71f307052 | |||
| de8f85ae9e | |||
| 402e258fcf | |||
| 271f1e5df3 | |||
| 0842364f74 | |||
| 427baab115 | |||
| ef636fdafa | |||
| e5e4bca5fa | |||
| cb3eeee9f8 | |||
| 47a84985f5 | |||
| 57a86709ec | |||
| d37c1cc139 | |||
| fcdcc07668 | |||
| 9bfda12147 | |||
| 84366f9642 | |||
| a213dba276 | |||
| 831723dfc2 | |||
| 0ddfb278df | |||
| c475b5c702 | |||
| 4b669ce8ec | |||
| 00f63ff914 | |||
| 2341d7a229 | |||
| 02bb01de76 | |||
| a0e42fd77e | |||
| 356fba697c | |||
| 1fad725e87 | |||
| 4ade7b2e64 | |||
| 8f60cf90ea | |||
| 3bb16c307f | |||
| 0d4c5a2731 | |||
| 3a968cf23c | |||
| d18798ab56 | |||
| fe32e32783 | |||
| d7b01f8b36 | |||
| 9702210303 | |||
| 403425dab7 | |||
| f5e71157f4 | |||
| 999a269fcb | |||
| eaf6421e82 | |||
| 219c036e8b | |||
| bd3268dcbc | |||
| c22deeee96 | |||
| 0966b724d6 | |||
| f9b73591e4 | |||
| 60dc43ebd5 | |||
| 69b7fd98de | |||
| 3b7c0880fc | |||
| ac69ff85d4 | |||
| 031bdba9dd | |||
| 51b3330154 | |||
| 71e66380b4 | |||
| aaad893d95 | |||
| dd40b166bc | |||
| 3e0cdb0b11 | |||
| 6a8fbdda5c | |||
| c6e023e217 | |||
| 154355c071 | |||
| 4eaa9fb481 | |||
| 255f1b3c23 | |||
| eaa32eb8cb | |||
| 0a2b57782f | |||
| 32d7305297 | |||
| 38d1ee76ce | |||
| c08f92f798 | |||
| d35adeb2dd | |||
| 97935d6ff9 | |||
| fb0bb144f9 | |||
| d264eee841 | |||
| 6cef71bf37 | |||
| 1a26f6f68c | |||
| 0795a20e9c | |||
| 076bd7f6df | |||
| 2f2864dade |
@@ -70,16 +70,16 @@ jobs:
|
|||||||
outputs:
|
outputs:
|
||||||
UPLOAD_DIR_ANDROID: ${{ env.UPLOAD_DIR_ANDROID }}
|
UPLOAD_DIR_ANDROID: ${{ env.UPLOAD_DIR_ANDROID }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v7
|
- uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
submodules: recursive
|
submodules: recursive
|
||||||
|
|
||||||
- name: Set up JDK 21
|
- name: Set up JDK 17
|
||||||
uses: actions/setup-java@v5
|
uses: actions/setup-java@v5
|
||||||
with:
|
with:
|
||||||
distribution: 'temurin'
|
distribution: 'temurin'
|
||||||
java-version: '21'
|
java-version: '17'
|
||||||
cache: gradle
|
cache: gradle
|
||||||
|
|
||||||
- name: Grant execute permission for gradlew
|
- name: Grant execute permission for gradlew
|
||||||
|
|||||||
@@ -72,15 +72,15 @@ jobs:
|
|||||||
outputs:
|
outputs:
|
||||||
UPLOAD_DIR_ANDROID: ${{ env.UPLOAD_DIR_ANDROID }}
|
UPLOAD_DIR_ANDROID: ${{ env.UPLOAD_DIR_ANDROID }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v7
|
- uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
submodules: recursive
|
submodules: recursive
|
||||||
- name: Set up JDK 21
|
- name: Set up JDK 17
|
||||||
uses: actions/setup-java@v5
|
uses: actions/setup-java@v5
|
||||||
with:
|
with:
|
||||||
distribution: 'temurin'
|
distribution: 'temurin'
|
||||||
java-version: '21'
|
java-version: '17'
|
||||||
cache: gradle
|
cache: gradle
|
||||||
- name: Grant execute permission for gradlew
|
- name: Grant execute permission for gradlew
|
||||||
run: chmod +x gradlew
|
run: chmod +x gradlew
|
||||||
@@ -112,9 +112,6 @@ jobs:
|
|||||||
./gradlew :app:assemble${flavor^}Debug --stacktrace
|
./gradlew :app:assemble${flavor^}Debug --stacktrace
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
env:
|
|
||||||
GITHUB_SHA: ${{ github.sha }}
|
|
||||||
GITHUB_RUN_NUMBER: ${{ github.run_number }}
|
|
||||||
- name: Get release apk path
|
- name: Get release apk path
|
||||||
id: apk-path
|
id: apk-path
|
||||||
run: echo "path=$(find . -regex '^.*/build/outputs/apk/${{ inputs.flavor }}/${{ inputs.build_type }}/.*\.apk$' -type f | head -1 | tail -c+2)" >> $GITHUB_OUTPUT
|
run: echo "path=$(find . -regex '^.*/build/outputs/apk/${{ inputs.flavor }}/${{ inputs.build_type }}/.*\.apk$' -type f | head -1 | tail -c+2)" >> $GITHUB_OUTPUT
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ jobs:
|
|||||||
has_new_commits: ${{ steps.check.outputs.new_commits }}
|
has_new_commits: ${{ steps.check.outputs.new_commits }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout Repository
|
- name: Checkout Repository
|
||||||
uses: actions/checkout@v7
|
uses: actions/checkout@v6
|
||||||
- name: Check for new commits
|
- name: Check for new commits
|
||||||
id: check
|
id: check
|
||||||
env:
|
env:
|
||||||
@@ -43,7 +43,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
|
|
||||||
- uses: actions/checkout@v7
|
- uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
submodules: recursive
|
submodules: recursive
|
||||||
|
|
||||||
|
|||||||
@@ -1,26 +1,21 @@
|
|||||||
name: on-pr
|
name: on-pr
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
|
|
||||||
on:
|
on:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
pull_request:
|
pull_request:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
format_check:
|
format_check:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v7
|
- uses: actions/checkout@v6
|
||||||
|
- name: Set up JDK 17
|
||||||
- name: Verify Gradle Wrapper
|
|
||||||
uses: gradle/actions/wrapper-validation@v6
|
|
||||||
|
|
||||||
- name: Set up JDK 21
|
|
||||||
uses: actions/setup-java@v5
|
uses: actions/setup-java@v5
|
||||||
with:
|
with:
|
||||||
distribution: 'temurin'
|
distribution: 'temurin'
|
||||||
java-version: '21'
|
java-version: '17'
|
||||||
cache: gradle
|
cache: gradle
|
||||||
|
|
||||||
- name: Grant execute permission for gradlew
|
- name: Grant execute permission for gradlew
|
||||||
|
|||||||
@@ -78,7 +78,7 @@ jobs:
|
|||||||
name: publish-github
|
name: publish-github
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v7
|
- uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
ref: ${{ github.event_name == 'push' && github.ref || 'master' }}
|
ref: ${{ github.event_name == 'push' && github.ref || 'master' }}
|
||||||
- name: Install system dependencies
|
- name: Install system dependencies
|
||||||
@@ -187,61 +187,57 @@ jobs:
|
|||||||
repository: wgtunnel/fdroid
|
repository: wgtunnel/fdroid
|
||||||
event-type: fdroid-update
|
event-type: fdroid-update
|
||||||
|
|
||||||
build-google-aab:
|
|
||||||
if: >-
|
|
||||||
${{
|
|
||||||
github.event_name == 'push' ||
|
|
||||||
inputs.track != 'none'
|
|
||||||
}}
|
|
||||||
uses: ./.github/workflows/build-aab.yml
|
|
||||||
secrets: inherit
|
|
||||||
with:
|
|
||||||
build_type: release
|
|
||||||
flavor: google
|
|
||||||
|
|
||||||
publish-play:
|
publish-play:
|
||||||
if: ${{ github.event_name == 'push' || inputs.track != 'none' }}
|
if: ${{ github.event_name == 'push' || inputs.track != 'none' }}
|
||||||
name: Publish to Google Play
|
name: Publish to Google Play
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs: build-google-aab
|
|
||||||
|
env:
|
||||||
|
SIGNING_KEY_ALIAS: ${{ secrets.SIGNING_KEY_ALIAS }}
|
||||||
|
SIGNING_KEY_PASSWORD: ${{ secrets.SIGNING_KEY_PASSWORD }}
|
||||||
|
SIGNING_STORE_PASSWORD: ${{ secrets.SIGNING_STORE_PASSWORD }}
|
||||||
|
KEY_STORE_FILE: 'android_keystore.jks'
|
||||||
|
KEY_STORE_LOCATION: ${{ github.workspace }}/app/keystore/
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v7
|
- uses: actions/checkout@v6
|
||||||
|
- name: Set up JDK 17
|
||||||
|
uses: actions/setup-java@v5
|
||||||
with:
|
with:
|
||||||
ref: ${{ github.event_name == 'push' && github.ref || 'master' }}
|
distribution: 'temurin'
|
||||||
|
java-version: '17'
|
||||||
|
cache: gradle
|
||||||
|
|
||||||
- name: Download AAB artifact
|
- name: Grant execute permission for gradlew
|
||||||
uses: actions/download-artifact@v8
|
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@v2.0
|
||||||
with:
|
with:
|
||||||
name: google-play-aab
|
fileName: ${{ env.KEY_STORE_FILE }}
|
||||||
path: ${{ github.workspace }}/aab
|
fileDir: ${{ env.KEY_STORE_LOCATION }}
|
||||||
|
encodedString: ${{ secrets.KEYSTORE }}
|
||||||
|
|
||||||
- name: Find exact AAB file path
|
# create keystore path for gradle to read
|
||||||
id: find-aab
|
- name: Create keystore path env var
|
||||||
run: |
|
run: |
|
||||||
AAB_PATH=$(find "${{ github.workspace }}/aab" -name "*.aab" -type f | head -1)
|
store_path=${{ env.KEY_STORE_LOCATION }}${{ env.KEY_STORE_FILE }}
|
||||||
if [ -z "$AAB_PATH" ]; then
|
echo "KEY_STORE_PATH=$store_path" >> $GITHUB_ENV
|
||||||
echo "ERROR: No .aab file found after download!"
|
|
||||||
find "${{ github.workspace }}/aab" -type f
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
echo "Found AAB: $AAB_PATH"
|
|
||||||
echo "aab_path=$AAB_PATH" >> $GITHUB_OUTPUT
|
|
||||||
|
|
||||||
- name: Create service_account.json
|
- name: Create service_account.json
|
||||||
|
id: createServiceAccount
|
||||||
run: echo '${{ secrets.SERVICE_ACCOUNT_JSON }}' > service_account.json
|
run: echo '${{ secrets.SERVICE_ACCOUNT_JSON }}' > service_account.json
|
||||||
|
|
||||||
- name: Set up Ruby
|
- name: Deploy with fastlane
|
||||||
uses: ruby/setup-ruby@v1
|
uses: ruby/setup-ruby@v1
|
||||||
with:
|
with:
|
||||||
ruby-version: '3.4'
|
ruby-version: '3.2' # Not needed with a .ruby-version file
|
||||||
bundler-cache: true
|
bundler-cache: true
|
||||||
|
|
||||||
- name: Upload to Google Play
|
- name: Distribute app to Prod track 🚀
|
||||||
run: |
|
run: |
|
||||||
track=${{ github.event_name == 'push' && 'production' || inputs.track }}
|
track=${{ github.event_name == 'push' && 'production' || inputs.track }}
|
||||||
bundle exec fastlane run upload_to_play_store \
|
(cd ${{ github.workspace }} && bundle install && bundle exec fastlane $track --verbose)
|
||||||
track:"$track" \
|
|
||||||
aab:"${{ steps.find-aab.outputs.aab_path }}" \
|
|
||||||
json_key:"service_account.json" \
|
|
||||||
package_name:"com.zaneschepke.wireguardautotunnel" \
|
|
||||||
skip_upload_apk:true
|
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
source "https://rubygems.org"
|
source "https://rubygems.org"
|
||||||
|
|
||||||
gem "fastlane"
|
gem "fastlane"
|
||||||
gem "multi_json"
|
|
||||||
@@ -49,7 +49,7 @@ and [AmneziaWG](https://docs.amnezia.org/documentation/amnezia-wg/)
|
|||||||
|
|
||||||
## About
|
## About
|
||||||
|
|
||||||
WG Tunnel is an alternative Android client for WireGuard and AmneziaWG, inspired by the official WireGuard Android app. It fills gaps in the official client by adding advanced features like auto-tunneling, AmneziaWG support, different app modes like **Lockdown** (a custom kill switch for leak prevention), and **Local Proxy** (expose a tunnel over a local SOCKS5/HTTP proxy server) for enhanced privacy, censorship resistance, and flexibility.
|
WG Tunnel is an alternative Android client for WireGuard and AmneziaWG, inspired by the official WireGuard Android app. It fills gaps in the official client by adding advanced features like auto-tunneling (on-demand VPN activation), while seamlessly supporting both protocols across app modes—including Kernel (for direct WireGuard kernel integration; AmneziaWG not supported), VPN (standard system-level tunneling), Lockdown (a custom kill switch for leak prevention), and Proxy (built-in HTTP/SOCKS5 forwarding)—for enhanced privacy, censorship resistance, and flexibility.
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -67,18 +67,21 @@ WG Tunnel is an alternative Android client for WireGuard and AmneziaWG, inspired
|
|||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
- **Auto-Tunneling:** Automatically activate tunnels based on your device's active network details.
|
- **Tunnel Import Methods**: Easily add tunnels using .conf files, ZIP archives, manual entry, or QR code scanning.
|
||||||
- **Deferred Endpoint Bootstrapping:** Safely resolves endpoints and updates peers after the tunnel is up for better reliability and leak protection on startup.
|
- **Auto-Tunneling**: Automatically activate tunnels based on Wi-Fi SSID, Ethernet connections, or mobile data networks.
|
||||||
- **Handshake Monitoring:** Real-time handshake monitoring for instant tunnel health feedback.
|
- **Split Tunneling**: Flexible support for routing specific apps or traffic through the VPN.
|
||||||
- **AmneziaWG Support:** Full support for AmneziaWG 2.0, providing robust censorship protection.
|
- **WireGuard Modes**: Full compatibility with WireGuard in both kernel and userspace implementations.
|
||||||
- **Split Tunneling:** Flexible support for routing specific apps or traffic through the VPN.
|
- **AmneziaWG Integration**: Userspace mode for AmneziaWG, providing robust censorship evasion.
|
||||||
- **Local Proxy Mode:** Expose WireGuard tunnels over a local SOCKS5 or HTTP proxy to browsers or firewall apps (like AdGuard).
|
- **Always-On VPN**: Ensures continuous protection with Android's Always-On VPN feature.
|
||||||
- **Lockdown Mode:** Advanced in-app kill switch that blocks all traffic while the tunnel is down.
|
- **Quick Controls**: Quick Settings tile and home screen shortcuts for easy VPN toggling.
|
||||||
- **Quick Controls:** Quick Settings tile and home screen shortcuts for easy toggling.
|
- **Automation Support**: Intent-based automation for controlling tunnels.
|
||||||
- **Remote Control Support:** Intent-based automation for controlling tunnels and auto-tunneling from automation apps (like Tasker).
|
- **Auto-Restore**: Seamlessly restores auto-tunneling and active tunnels after device restarts or app updates.
|
||||||
- **Dynamic DNS Handling:** Automatically detect and update endpoints on server IP changes without requiring a restart.
|
- **Proxying Options**: Built-in HTTP and SOCKS5 proxy support within tunnels.
|
||||||
- **IPv6 Endpoints:** Automatically upgrade to IPv6 endpoints or fall back to IPv4 based on network conditions without requiring a restart.
|
- **Lockdown Mode**: Custom kill switch for maximum leak prevention and security.
|
||||||
- **Android TV Support:** Full support for nearly all features on Android TV.
|
- **Dynamic DNS Handling**: Detects and updates DNS changes without tunnel restarts.
|
||||||
|
- **Monitoring Tools**: Advanced tunnel monitoring features for tunnel performance monitoring.
|
||||||
|
- **Android TV Support**: Android TV support for secure streaming and browsing.
|
||||||
|
- **Advanced DNS**: DNS over HTTPS support for tunnel endpoint resolutions.
|
||||||
|
|
||||||
## Building
|
## Building
|
||||||
|
|
||||||
|
|||||||
+32
-38
@@ -1,5 +1,6 @@
|
|||||||
import com.android.build.api.dsl.ApplicationExtension
|
import com.android.build.api.dsl.ApplicationExtension
|
||||||
import com.android.build.api.variant.FilterConfiguration
|
import com.android.build.api.variant.FilterConfiguration
|
||||||
|
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
alias(libs.plugins.android.application)
|
alias(libs.plugins.android.application)
|
||||||
@@ -22,6 +23,13 @@ licensee {
|
|||||||
ignoreDependencies("com.github.topjohnwu.libsu")
|
ignoreDependencies("com.github.topjohnwu.libsu")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
kotlin {
|
||||||
|
compilerOptions {
|
||||||
|
jvmTarget = JvmTarget.JVM_17
|
||||||
|
freeCompilerArgs = listOf("-XXLanguage:+PropertyParamAnnotationDefaultTargetMode")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
configure<ApplicationExtension> {
|
configure<ApplicationExtension> {
|
||||||
namespace = Constants.APP_ID
|
namespace = Constants.APP_ID
|
||||||
compileSdk = Constants.TARGET_SDK
|
compileSdk = Constants.TARGET_SDK
|
||||||
@@ -38,11 +46,10 @@ configure<ApplicationExtension> {
|
|||||||
|
|
||||||
splits {
|
splits {
|
||||||
abi {
|
abi {
|
||||||
val noSplits = providers.gradleProperty("noSplits").isPresent
|
isEnable = !project.hasProperty("noSplits")
|
||||||
isEnable = !noSplits
|
|
||||||
reset()
|
reset()
|
||||||
include("armeabi-v7a", "arm64-v8a")
|
include("armeabi-v7a", "arm64-v8a")
|
||||||
isUniversalApk = !noSplits
|
isUniversalApk = !project.hasProperty("noSplits")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -50,17 +57,14 @@ configure<ApplicationExtension> {
|
|||||||
applicationId = Constants.APP_ID
|
applicationId = Constants.APP_ID
|
||||||
minSdk = Constants.MIN_SDK
|
minSdk = Constants.MIN_SDK
|
||||||
targetSdk = Constants.TARGET_SDK
|
targetSdk = Constants.TARGET_SDK
|
||||||
versionCode = Constants.VERSION_CODE
|
versionCode = computeVersionCode()
|
||||||
versionName = Constants.VERSION_NAME
|
versionName = computeVersionName()
|
||||||
|
|
||||||
experimentalProperties["android.experimental.disableGitVersion"] = true
|
|
||||||
|
|
||||||
sourceSets {
|
sourceSets {
|
||||||
getByName("debug").assets.directories += "$projectDir/schemas"
|
getByName("debug").assets.directories += "$projectDir/schemas"
|
||||||
}
|
}
|
||||||
|
|
||||||
val languagesProvider = project.languageListProvider()
|
val languagesArray = buildLanguagesArray(languageList())
|
||||||
val languagesArray = buildLanguagesArray(languagesProvider.get())
|
|
||||||
buildConfigField("String[]", "LANGUAGES", "new String[]{ $languagesArray }")
|
buildConfigField("String[]", "LANGUAGES", "new String[]{ $languagesArray }")
|
||||||
|
|
||||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||||
@@ -130,6 +134,8 @@ configure<ApplicationExtension> {
|
|||||||
|
|
||||||
compileOptions {
|
compileOptions {
|
||||||
isCoreLibraryDesugaringEnabled = true
|
isCoreLibraryDesugaringEnabled = true
|
||||||
|
sourceCompatibility = JavaVersion.VERSION_17
|
||||||
|
targetCompatibility = JavaVersion.VERSION_17
|
||||||
}
|
}
|
||||||
|
|
||||||
buildFeatures {
|
buildFeatures {
|
||||||
@@ -142,42 +148,31 @@ configure<ApplicationExtension> {
|
|||||||
|
|
||||||
androidComponents {
|
androidComponents {
|
||||||
onVariants { variant ->
|
onVariants { variant ->
|
||||||
val isNightly = project.isNightlyBuild()
|
|
||||||
|
|
||||||
if (isNightly) {
|
val abiNameMap =
|
||||||
variant.outputs.forEach { output ->
|
mapOf(
|
||||||
|
"armeabi-v7a" to "armv7",
|
||||||
output.versionCode.set(
|
"arm64-v8a" to "arm64",
|
||||||
output.versionCode.get() + project.getVersionCodeIncrement()
|
"x86" to "x86",
|
||||||
)
|
"x86_64" to "x64",
|
||||||
|
)
|
||||||
val currentVersion = output.versionName.get()
|
|
||||||
val nextVersion = bumpToNextPatchVersion(currentVersion)
|
|
||||||
val gitHash = project.getGitCommitHash()
|
|
||||||
|
|
||||||
output.versionName.set("$nextVersion-nightly+git.$gitHash")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val abiNameMap = mapOf(
|
|
||||||
"armeabi-v7a" to "armv7",
|
|
||||||
"arm64-v8a" to "arm64",
|
|
||||||
"x86" to "x86",
|
|
||||||
"x86_64" to "x64",
|
|
||||||
)
|
|
||||||
|
|
||||||
variant.outputs.forEach { output ->
|
variant.outputs.forEach { output ->
|
||||||
val abi = output.filters.find { it.filterType == FilterConfiguration.FilterType.ABI }?.identifier
|
val abi = output.filters.find { it.filterType == FilterConfiguration.FilterType.ABI }?.identifier
|
||||||
|
|
||||||
val flavorName = variant.productFlavors.joinToString("-") { it.second }
|
val flavorName = variant.productFlavors.joinToString("-") { it.second }
|
||||||
|
|
||||||
val versionName = output.versionName.get()
|
val versionName = output.versionName.get()
|
||||||
|
|
||||||
val baseFileName = "${Constants.APP_NAME}-${flavorName}-v${versionName}"
|
val baseFileName = "${Constants.APP_NAME}-${flavorName}-v${versionName}"
|
||||||
|
|
||||||
val outputFileName = if (!abi.isNullOrEmpty()) {
|
val outputFileName =
|
||||||
val shortAbiName = abiNameMap.getOrDefault(abi, abi)
|
if (!abi.isNullOrEmpty()) {
|
||||||
"${baseFileName}-${shortAbiName}.apk"
|
val shortAbiName = abiNameMap.getOrDefault(abi, abi)
|
||||||
} else {
|
"${baseFileName}-${shortAbiName}.apk"
|
||||||
"${baseFileName}.apk"
|
} else {
|
||||||
}
|
"${baseFileName}.apk"
|
||||||
|
}
|
||||||
|
|
||||||
output.outputFileName.set(outputFileName)
|
output.outputFileName.set(outputFileName)
|
||||||
}
|
}
|
||||||
@@ -230,7 +225,6 @@ dependencies {
|
|||||||
// UI utilities
|
// UI utilities
|
||||||
implementation(libs.bundles.ui.utilities)
|
implementation(libs.bundles.ui.utilities)
|
||||||
implementation(libs.lottie.compose)
|
implementation(libs.lottie.compose)
|
||||||
implementation(libs.sonner)
|
|
||||||
|
|
||||||
// Misc utilities
|
// Misc utilities
|
||||||
implementation(libs.bundles.misc.utilities)
|
implementation(libs.bundles.misc.utilities)
|
||||||
|
|||||||
@@ -1,513 +0,0 @@
|
|||||||
{
|
|
||||||
"formatVersion": 1,
|
|
||||||
"database": {
|
|
||||||
"version": 31,
|
|
||||||
"identityHash": "1dee3799f1c6526c48723fd2fee58d11",
|
|
||||||
"entities": [
|
|
||||||
{
|
|
||||||
"tableName": "tunnel_config",
|
|
||||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `tunnel_networks` TEXT NOT NULL DEFAULT '', `is_mobile_data_tunnel` INTEGER NOT NULL DEFAULT false, `is_primary_tunnel` INTEGER NOT NULL DEFAULT false, `quick_config` TEXT NOT NULL DEFAULT '', `dynamic_dns` INTEGER NOT NULL DEFAULT false, `is_ethernet_tunnel` INTEGER NOT NULL DEFAULT false, `prefer_ipv6` INTEGER NOT NULL DEFAULT false, `position` INTEGER NOT NULL DEFAULT 0, `auto_tunnel_apps` TEXT NOT NULL DEFAULT '[]', `is_metered` INTEGER NOT NULL DEFAULT false, `ipv4_fallback` INTEGER NOT NULL DEFAULT false, `ipv6_restore` INTEGER NOT NULL DEFAULT false)",
|
|
||||||
"fields": [
|
|
||||||
{
|
|
||||||
"fieldPath": "id",
|
|
||||||
"columnName": "id",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "name",
|
|
||||||
"columnName": "name",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "tunnelNetworks",
|
|
||||||
"columnName": "tunnel_networks",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "''"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "isMobileDataTunnel",
|
|
||||||
"columnName": "is_mobile_data_tunnel",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "false"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "isPrimaryTunnel",
|
|
||||||
"columnName": "is_primary_tunnel",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "false"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "quickConfig",
|
|
||||||
"columnName": "quick_config",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "''"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "dynamicDnsEnabled",
|
|
||||||
"columnName": "dynamic_dns",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "false"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "isEthernetTunnel",
|
|
||||||
"columnName": "is_ethernet_tunnel",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "false"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "isIpv6Preferred",
|
|
||||||
"columnName": "prefer_ipv6",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "false"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "position",
|
|
||||||
"columnName": "position",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "0"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "autoTunnelApps",
|
|
||||||
"columnName": "auto_tunnel_apps",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "'[]'"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "isMetered",
|
|
||||||
"columnName": "is_metered",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "false"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "ipv4FallbackEnabled",
|
|
||||||
"columnName": "ipv4_fallback",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "false"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "ipv6RestoreEnabled",
|
|
||||||
"columnName": "ipv6_restore",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "false"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"primaryKey": {
|
|
||||||
"autoGenerate": true,
|
|
||||||
"columnNames": [
|
|
||||||
"id"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"indices": [
|
|
||||||
{
|
|
||||||
"name": "index_tunnel_config_name",
|
|
||||||
"unique": true,
|
|
||||||
"columnNames": [
|
|
||||||
"name"
|
|
||||||
],
|
|
||||||
"orders": [],
|
|
||||||
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_tunnel_config_name` ON `${TABLE_NAME}` (`name`)"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"tableName": "proxy_settings",
|
|
||||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `socks5_proxy_enabled` INTEGER NOT NULL DEFAULT 0, `socks5_proxy_bind_address` TEXT, `http_proxy_enable` INTEGER NOT NULL DEFAULT 0, `http_proxy_bind_address` TEXT, `proxy_username` TEXT, `proxy_password` TEXT)",
|
|
||||||
"fields": [
|
|
||||||
{
|
|
||||||
"fieldPath": "id",
|
|
||||||
"columnName": "id",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "socks5ProxyEnabled",
|
|
||||||
"columnName": "socks5_proxy_enabled",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "0"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "socks5ProxyBindAddress",
|
|
||||||
"columnName": "socks5_proxy_bind_address",
|
|
||||||
"affinity": "TEXT"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "httpProxyEnabled",
|
|
||||||
"columnName": "http_proxy_enable",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "0"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "httpProxyBindAddress",
|
|
||||||
"columnName": "http_proxy_bind_address",
|
|
||||||
"affinity": "TEXT"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "proxyUsername",
|
|
||||||
"columnName": "proxy_username",
|
|
||||||
"affinity": "TEXT"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "proxyPassword",
|
|
||||||
"columnName": "proxy_password",
|
|
||||||
"affinity": "TEXT"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"primaryKey": {
|
|
||||||
"autoGenerate": true,
|
|
||||||
"columnNames": [
|
|
||||||
"id"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"tableName": "general_settings",
|
|
||||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_shortcuts_enabled` INTEGER NOT NULL DEFAULT 0, `is_restore_on_boot_enabled` INTEGER NOT NULL DEFAULT 0, `is_multi_tunnel_enabled` INTEGER NOT NULL DEFAULT 0, `global_split_tunnel_enabled` INTEGER NOT NULL DEFAULT 0, `app_mode` INTEGER NOT NULL DEFAULT 0, `theme` TEXT NOT NULL DEFAULT 'AUTOMATIC', `locale` TEXT, `remote_key` TEXT, `is_remote_control_enabled` INTEGER NOT NULL DEFAULT 0, `is_pin_lock_enabled` INTEGER NOT NULL DEFAULT 0, `is_always_on_vpn_enabled` INTEGER NOT NULL DEFAULT 0, `already_donated` INTEGER NOT NULL DEFAULT 0, `screen_recording_security` INTEGER NOT NULL DEFAULT 1, `global_amnezia_enabled` INTEGER NOT NULL DEFAULT 0, `tunnel_scripting_enabled` INTEGER NOT NULL DEFAULT 0)",
|
|
||||||
"fields": [
|
|
||||||
{
|
|
||||||
"fieldPath": "id",
|
|
||||||
"columnName": "id",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "isShortcutsEnabled",
|
|
||||||
"columnName": "is_shortcuts_enabled",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "0"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "isRestoreOnBootEnabled",
|
|
||||||
"columnName": "is_restore_on_boot_enabled",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "0"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "isMultiTunnelEnabled",
|
|
||||||
"columnName": "is_multi_tunnel_enabled",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "0"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "isGlobalSplitTunnelEnabled",
|
|
||||||
"columnName": "global_split_tunnel_enabled",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "0"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "tunnelMode",
|
|
||||||
"columnName": "app_mode",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "0"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "theme",
|
|
||||||
"columnName": "theme",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "'AUTOMATIC'"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "locale",
|
|
||||||
"columnName": "locale",
|
|
||||||
"affinity": "TEXT"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "remoteKey",
|
|
||||||
"columnName": "remote_key",
|
|
||||||
"affinity": "TEXT"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "isRemoteControlEnabled",
|
|
||||||
"columnName": "is_remote_control_enabled",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "0"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "isPinLockEnabled",
|
|
||||||
"columnName": "is_pin_lock_enabled",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "0"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "isAlwaysOnVpnEnabled",
|
|
||||||
"columnName": "is_always_on_vpn_enabled",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "0"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "alreadyDonated",
|
|
||||||
"columnName": "already_donated",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "0"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "screenRecordingSecurityEnabled",
|
|
||||||
"columnName": "screen_recording_security",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "1"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "isGlobalAmneziaEnabled",
|
|
||||||
"columnName": "global_amnezia_enabled",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "0"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "tunnelScriptingEnabled",
|
|
||||||
"columnName": "tunnel_scripting_enabled",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "0"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"primaryKey": {
|
|
||||||
"autoGenerate": true,
|
|
||||||
"columnNames": [
|
|
||||||
"id"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"tableName": "auto_tunnel_settings",
|
|
||||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_tunnel_enabled` INTEGER NOT NULL DEFAULT 0, `is_tunnel_on_mobile_data_enabled` INTEGER NOT NULL DEFAULT 0, `trusted_network_ssids` TEXT NOT NULL DEFAULT '', `is_tunnel_on_ethernet_enabled` INTEGER NOT NULL DEFAULT 0, `is_tunnel_on_wifi_enabled` INTEGER NOT NULL DEFAULT 0, `is_wildcards_enabled` INTEGER NOT NULL DEFAULT 0, `is_stop_on_no_internet_enabled` INTEGER NOT NULL DEFAULT 0, `is_tunnel_on_unsecure_enabled` INTEGER NOT NULL DEFAULT 0, `wifi_detection_method` INTEGER NOT NULL DEFAULT 0, `start_on_boot` INTEGER NOT NULL DEFAULT 0, `disable_on_captive_portal` INTEGER NOT NULL DEFAULT 1)",
|
|
||||||
"fields": [
|
|
||||||
{
|
|
||||||
"fieldPath": "id",
|
|
||||||
"columnName": "id",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "isAutoTunnelEnabled",
|
|
||||||
"columnName": "is_tunnel_enabled",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "0"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "isTunnelOnMobileDataEnabled",
|
|
||||||
"columnName": "is_tunnel_on_mobile_data_enabled",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "0"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "trustedNetworkSSIDs",
|
|
||||||
"columnName": "trusted_network_ssids",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "''"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "isTunnelOnEthernetEnabled",
|
|
||||||
"columnName": "is_tunnel_on_ethernet_enabled",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "0"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "isTunnelOnWifiEnabled",
|
|
||||||
"columnName": "is_tunnel_on_wifi_enabled",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "0"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "isWildcardsEnabled",
|
|
||||||
"columnName": "is_wildcards_enabled",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "0"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "isStopOnNoInternetEnabled",
|
|
||||||
"columnName": "is_stop_on_no_internet_enabled",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "0"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "isTunnelOnUnsecureEnabled",
|
|
||||||
"columnName": "is_tunnel_on_unsecure_enabled",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "0"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "wifiDetectionMethod",
|
|
||||||
"columnName": "wifi_detection_method",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "0"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "startOnBoot",
|
|
||||||
"columnName": "start_on_boot",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "0"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "disableTunnelOnCaptivePortal",
|
|
||||||
"columnName": "disable_on_captive_portal",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "1"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"primaryKey": {
|
|
||||||
"autoGenerate": true,
|
|
||||||
"columnNames": [
|
|
||||||
"id"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"tableName": "monitoring_settings",
|
|
||||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_local_logs_enabled` INTEGER NOT NULL DEFAULT 0, `tunnel_statistics_enabled` INTEGER NOT NULL DEFAULT 1, `tunnel_statistics_poll_interval` INTEGER NOT NULL DEFAULT 3)",
|
|
||||||
"fields": [
|
|
||||||
{
|
|
||||||
"fieldPath": "id",
|
|
||||||
"columnName": "id",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "isLocalLogsEnabled",
|
|
||||||
"columnName": "is_local_logs_enabled",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "0"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "tunnelStatisticsEnabled",
|
|
||||||
"columnName": "tunnel_statistics_enabled",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "1"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "tunnelStatisticsPollInterval",
|
|
||||||
"columnName": "tunnel_statistics_poll_interval",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "3"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"primaryKey": {
|
|
||||||
"autoGenerate": true,
|
|
||||||
"columnNames": [
|
|
||||||
"id"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"tableName": "dns_settings",
|
|
||||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `dns_protocol` INTEGER NOT NULL DEFAULT 0, `dns_endpoint` TEXT, `global_tunnel_dns_enabled` INTEGER NOT NULL DEFAULT 0)",
|
|
||||||
"fields": [
|
|
||||||
{
|
|
||||||
"fieldPath": "id",
|
|
||||||
"columnName": "id",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "dnsProtocol",
|
|
||||||
"columnName": "dns_protocol",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "0"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "dnsEndpoint",
|
|
||||||
"columnName": "dns_endpoint",
|
|
||||||
"affinity": "TEXT"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "isGlobalTunnelDnsEnabled",
|
|
||||||
"columnName": "global_tunnel_dns_enabled",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "0"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"primaryKey": {
|
|
||||||
"autoGenerate": true,
|
|
||||||
"columnNames": [
|
|
||||||
"id"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"tableName": "lockdown_settings",
|
|
||||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `bypass_lan` INTEGER NOT NULL DEFAULT 0, `metered` INTEGER NOT NULL DEFAULT 0, `dual_stack` INTEGER NOT NULL DEFAULT 0)",
|
|
||||||
"fields": [
|
|
||||||
{
|
|
||||||
"fieldPath": "id",
|
|
||||||
"columnName": "id",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "bypassLan",
|
|
||||||
"columnName": "bypass_lan",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "0"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "metered",
|
|
||||||
"columnName": "metered",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "0"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "dualStack",
|
|
||||||
"columnName": "dual_stack",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "0"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"primaryKey": {
|
|
||||||
"autoGenerate": true,
|
|
||||||
"columnNames": [
|
|
||||||
"id"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"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, '1dee3799f1c6526c48723fd2fee58d11')"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -53,6 +53,7 @@
|
|||||||
android:fullBackupContent="@xml/backup_rules"
|
android:fullBackupContent="@xml/backup_rules"
|
||||||
android:icon="@mipmap/ic_launcher"
|
android:icon="@mipmap/ic_launcher"
|
||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
|
android:roundIcon="@mipmap/ic_launcher_round"
|
||||||
android:supportsRtl="true"
|
android:supportsRtl="true"
|
||||||
android:theme="@style/Theme.App.Start"
|
android:theme="@style/Theme.App.Start"
|
||||||
tools:targetApi="tiramisu">
|
tools:targetApi="tiramisu">
|
||||||
@@ -73,13 +74,6 @@
|
|||||||
<category android:name="android.intent.category.DEFAULT" />
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
|
|
||||||
<intent-filter>
|
|
||||||
<action android:name="android.intent.action.VIEW" />
|
|
||||||
<category android:name="android.intent.category.DEFAULT" />
|
|
||||||
<category android:name="android.intent.category.BROWSABLE" />
|
|
||||||
<data android:scheme="wg" />
|
|
||||||
</intent-filter>
|
|
||||||
|
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN" />
|
<action android:name="android.intent.action.MAIN" />
|
||||||
<action android:name="android.intent.action.SHOW_APP_INFO" />
|
<action android:name="android.intent.action.SHOW_APP_INFO" />
|
||||||
@@ -175,9 +169,9 @@
|
|||||||
tools:node="remove" />
|
tools:node="remove" />
|
||||||
</provider>
|
</provider>
|
||||||
<service
|
<service
|
||||||
android:name=".service.tile.TunnelControlTile"
|
android:name=".core.service.tile.TunnelControlTile"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
android:icon="@drawable/ic_qs_logo"
|
android:icon="@drawable/ic_notification"
|
||||||
android:label="@string/tunnel_control"
|
android:label="@string/tunnel_control"
|
||||||
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE">
|
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE">
|
||||||
<meta-data
|
<meta-data
|
||||||
@@ -192,9 +186,9 @@
|
|||||||
</intent-filter>
|
</intent-filter>
|
||||||
</service>
|
</service>
|
||||||
<service
|
<service
|
||||||
android:name=".service.tile.AutoTunnelControlTile"
|
android:name=".core.service.tile.AutoTunnelControlTile"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
android:icon="@drawable/ic_qs_logo"
|
android:icon="@drawable/ic_notification"
|
||||||
android:label="@string/auto_tunnel"
|
android:label="@string/auto_tunnel"
|
||||||
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE">
|
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE">
|
||||||
<meta-data
|
<meta-data
|
||||||
@@ -209,7 +203,7 @@
|
|||||||
</intent-filter>
|
</intent-filter>
|
||||||
</service>
|
</service>
|
||||||
<service
|
<service
|
||||||
android:name=".service.autotunnel.AutoTunnelService"
|
android:name=".core.service.autotunnel.AutoTunnelService"
|
||||||
android:enabled="true"
|
android:enabled="true"
|
||||||
android:exported="false"
|
android:exported="false"
|
||||||
android:foregroundServiceType="specialUse"
|
android:foregroundServiceType="specialUse"
|
||||||
|
|||||||
@@ -1,14 +1,12 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel
|
package com.zaneschepke.wireguardautotunnel
|
||||||
|
|
||||||
import ProxySettingsScreen
|
import ProxySettingsScreen
|
||||||
import android.Manifest
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.graphics.Color
|
import android.graphics.Color
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.net.VpnService
|
import android.net.VpnService
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.provider.Settings
|
|
||||||
import android.view.WindowManager
|
import android.view.WindowManager
|
||||||
import androidx.activity.SystemBarStyle
|
import androidx.activity.SystemBarStyle
|
||||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
@@ -21,29 +19,17 @@ import androidx.compose.animation.fadeOut
|
|||||||
import androidx.compose.animation.slideInHorizontally
|
import androidx.compose.animation.slideInHorizontally
|
||||||
import androidx.compose.animation.slideOutHorizontally
|
import androidx.compose.animation.slideOutHorizontally
|
||||||
import androidx.compose.animation.togetherWith
|
import androidx.compose.animation.togetherWith
|
||||||
import androidx.compose.foundation.BorderStroke
|
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
|
||||||
import androidx.compose.foundation.layout.consumeWindowInsets
|
import androidx.compose.foundation.layout.consumeWindowInsets
|
||||||
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.shape.RoundedCornerShape
|
import androidx.compose.foundation.layout.wrapContentHeight
|
||||||
import androidx.compose.material.icons.Icons
|
|
||||||
import androidx.compose.material.icons.outlined.CheckCircleOutline
|
|
||||||
import androidx.compose.material.icons.outlined.ErrorOutline
|
|
||||||
import androidx.compose.material.icons.outlined.FavoriteBorder
|
|
||||||
import androidx.compose.material.icons.outlined.Info
|
|
||||||
import androidx.compose.material.icons.outlined.WarningAmber
|
|
||||||
import androidx.compose.material3.ButtonDefaults
|
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.Icon
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Scaffold
|
import androidx.compose.material3.Scaffold
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.material3.TextButton
|
|
||||||
import androidx.compose.material3.surfaceColorAtElevation
|
import androidx.compose.material3.surfaceColorAtElevation
|
||||||
import androidx.compose.runtime.CompositionLocalProvider
|
import androidx.compose.runtime.CompositionLocalProvider
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
@@ -52,19 +38,20 @@ 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.saveable.rememberSaveable
|
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
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.Brush
|
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.LinkAnnotation
|
||||||
|
import androidx.compose.ui.text.SpanStyle
|
||||||
|
import androidx.compose.ui.text.TextLinkStyles
|
||||||
|
import androidx.compose.ui.text.buildAnnotatedString
|
||||||
import androidx.compose.ui.text.intl.Locale
|
import androidx.compose.ui.text.intl.Locale
|
||||||
import androidx.compose.ui.unit.IntOffset
|
import androidx.compose.ui.text.style.TextDecoration
|
||||||
|
import androidx.compose.ui.text.withLink
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.zIndex
|
import androidx.compose.ui.zIndex
|
||||||
import androidx.core.app.ActivityCompat
|
|
||||||
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
|
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.lifecycle.viewmodel.navigation3.rememberViewModelStoreNavEntryDecorator
|
import androidx.lifecycle.viewmodel.navigation3.rememberViewModelStoreNavEntryDecorator
|
||||||
@@ -72,10 +59,6 @@ import androidx.navigation3.runtime.entryProvider
|
|||||||
import androidx.navigation3.runtime.rememberNavBackStack
|
import androidx.navigation3.runtime.rememberNavBackStack
|
||||||
import androidx.navigation3.runtime.rememberSaveableStateHolderNavEntryDecorator
|
import androidx.navigation3.runtime.rememberSaveableStateHolderNavEntryDecorator
|
||||||
import androidx.navigation3.ui.NavDisplay
|
import androidx.navigation3.ui.NavDisplay
|
||||||
import com.dokar.sonner.TextToastAction
|
|
||||||
import com.dokar.sonner.ToastType
|
|
||||||
import com.dokar.sonner.Toaster
|
|
||||||
import com.dokar.sonner.rememberToasterState
|
|
||||||
import com.zaneschepke.networkmonitor.NetworkMonitor
|
import com.zaneschepke.networkmonitor.NetworkMonitor
|
||||||
import com.zaneschepke.wireguardautotunnel.data.AppDatabase
|
import com.zaneschepke.wireguardautotunnel.data.AppDatabase
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelMode
|
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelMode
|
||||||
@@ -86,9 +69,11 @@ import com.zaneschepke.wireguardautotunnel.domain.sideeffect.GlobalSideEffect
|
|||||||
import com.zaneschepke.wireguardautotunnel.ui.LocalIsAndroidTV
|
import com.zaneschepke.wireguardautotunnel.ui.LocalIsAndroidTV
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.LocalNavController
|
import com.zaneschepke.wireguardautotunnel.ui.LocalNavController
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.common.banner.AppAlertBanner
|
import com.zaneschepke.wireguardautotunnel.ui.common.banner.AppAlertBanner
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.common.dialog.InfoDialog
|
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.common.dialog.LocalNetworkPermissionDialog
|
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.common.dialog.VpnDeniedDialog
|
import com.zaneschepke.wireguardautotunnel.ui.common.dialog.VpnDeniedDialog
|
||||||
|
import com.zaneschepke.wireguardautotunnel.ui.common.snackbar.CustomSnackBar
|
||||||
|
import com.zaneschepke.wireguardautotunnel.ui.common.snackbar.SnackbarInfo
|
||||||
|
import com.zaneschepke.wireguardautotunnel.ui.common.snackbar.SnackbarType
|
||||||
|
import com.zaneschepke.wireguardautotunnel.ui.common.snackbar.rememberCustomSnackbarState
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.navigation.Route
|
import com.zaneschepke.wireguardautotunnel.ui.navigation.Route
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.navigation.SecureRoute
|
import com.zaneschepke.wireguardautotunnel.ui.navigation.SecureRoute
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.navigation.Tab
|
import com.zaneschepke.wireguardautotunnel.ui.navigation.Tab
|
||||||
@@ -125,29 +110,19 @@ import com.zaneschepke.wireguardautotunnel.ui.screens.tunnels.settings.ipv6.IPv6
|
|||||||
import com.zaneschepke.wireguardautotunnel.ui.screens.tunnels.sort.SortScreen
|
import com.zaneschepke.wireguardautotunnel.ui.screens.tunnels.sort.SortScreen
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.screens.tunnels.splittunnel.SplitTunnelScreen
|
import com.zaneschepke.wireguardautotunnel.ui.screens.tunnels.splittunnel.SplitTunnelScreen
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.theme.AlertRed
|
import com.zaneschepke.wireguardautotunnel.ui.theme.AlertRed
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.theme.Heart
|
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.theme.OffWhite
|
import com.zaneschepke.wireguardautotunnel.ui.theme.OffWhite
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.theme.SilverTree
|
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.theme.Straw
|
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.theme.WireguardAutoTunnelTheme
|
import com.zaneschepke.wireguardautotunnel.ui.theme.WireguardAutoTunnelTheme
|
||||||
import com.zaneschepke.wireguardautotunnel.util.FileUtils
|
|
||||||
import com.zaneschepke.wireguardautotunnel.util.LocaleUtil
|
import com.zaneschepke.wireguardautotunnel.util.LocaleUtil
|
||||||
import com.zaneschepke.wireguardautotunnel.util.StringValue
|
|
||||||
import com.zaneschepke.wireguardautotunnel.util.extensions.installApk
|
import com.zaneschepke.wireguardautotunnel.util.extensions.installApk
|
||||||
import com.zaneschepke.wireguardautotunnel.util.extensions.isRunningOnTv
|
import com.zaneschepke.wireguardautotunnel.util.extensions.isRunningOnTv
|
||||||
import com.zaneschepke.wireguardautotunnel.util.extensions.openWebUrl
|
import com.zaneschepke.wireguardautotunnel.util.extensions.openWebUrl
|
||||||
import com.zaneschepke.wireguardautotunnel.util.extensions.restartApp
|
import com.zaneschepke.wireguardautotunnel.util.extensions.restartApp
|
||||||
import com.zaneschepke.wireguardautotunnel.util.permission.LocalNetworkPermissionHelper
|
import com.zaneschepke.wireguardautotunnel.util.extensions.showToast
|
||||||
import com.zaneschepke.wireguardautotunnel.viewmodel.ConfigEditViewModel
|
import com.zaneschepke.wireguardautotunnel.viewmodel.ConfigEditViewModel
|
||||||
import com.zaneschepke.wireguardautotunnel.viewmodel.SharedAppViewModel
|
import com.zaneschepke.wireguardautotunnel.viewmodel.SharedAppViewModel
|
||||||
import com.zaneschepke.wireguardautotunnel.viewmodel.SplitTunnelViewModel
|
import com.zaneschepke.wireguardautotunnel.viewmodel.SplitTunnelViewModel
|
||||||
import com.zaneschepke.wireguardautotunnel.viewmodel.TunnelViewModel
|
import com.zaneschepke.wireguardautotunnel.viewmodel.TunnelViewModel
|
||||||
import de.raphaelebner.roomdatabasebackup.core.OnCompleteListener.Companion.EXIT_CODE_ERROR
|
|
||||||
import de.raphaelebner.roomdatabasebackup.core.OnCompleteListener.Companion.EXIT_CODE_ERROR_DECRYPTION_ERROR
|
|
||||||
import de.raphaelebner.roomdatabasebackup.core.OnCompleteListener.Companion.EXIT_CODE_ERROR_RESTORE_BACKUP_IS_ENCRYPTED
|
|
||||||
import de.raphaelebner.roomdatabasebackup.core.OnCompleteListener.Companion.EXIT_CODE_ERROR_WRONG_DECRYPTION_PASSWORD
|
|
||||||
import de.raphaelebner.roomdatabasebackup.core.RoomBackup
|
import de.raphaelebner.roomdatabasebackup.core.RoomBackup
|
||||||
import kotlin.time.Duration.Companion.milliseconds
|
|
||||||
import kotlinx.coroutines.awaitCancellation
|
import kotlinx.coroutines.awaitCancellation
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.flow.collectLatest
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
@@ -157,7 +132,6 @@ import org.koin.androidx.compose.koinViewModel
|
|||||||
import org.koin.androidx.viewmodel.ext.android.viewModel
|
import org.koin.androidx.viewmodel.ext.android.viewModel
|
||||||
import org.koin.core.parameter.parametersOf
|
import org.koin.core.parameter.parametersOf
|
||||||
import org.orbitmvi.orbit.compose.collectAsState
|
import org.orbitmvi.orbit.compose.collectAsState
|
||||||
import timber.log.Timber
|
|
||||||
import xyz.teamgravity.pin_lock_compose.PinManager
|
import xyz.teamgravity.pin_lock_compose.PinManager
|
||||||
|
|
||||||
class MainActivity : AppCompatActivity() {
|
class MainActivity : AppCompatActivity() {
|
||||||
@@ -181,10 +155,9 @@ class MainActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
roomBackup = RoomBackup(this).database(appDatabase).enableLogDebug(true).maxFileCount(5)
|
handleIncomingIntent(intent)
|
||||||
|
|
||||||
handleConfigFileIntent(intent)
|
roomBackup = RoomBackup(this)
|
||||||
handleWgDeepLinkIntent(intent)
|
|
||||||
|
|
||||||
installSplashScreen().apply {
|
installSplashScreen().apply {
|
||||||
setKeepOnScreenCondition { !viewModel.container.stateFlow.value.isAppLoaded }
|
setKeepOnScreenCondition { !viewModel.container.stateFlow.value.isAppLoaded }
|
||||||
@@ -202,54 +175,12 @@ class MainActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val toaster = rememberToasterState()
|
val snackbarState = rememberCustomSnackbarState()
|
||||||
var showVpnPermissionDialog by remember { mutableStateOf(false) }
|
var showVpnPermissionDialog by remember { mutableStateOf(false) }
|
||||||
var vpnPermissionDenied by remember { mutableStateOf(false) }
|
var vpnPermissionDenied by remember { mutableStateOf(false) }
|
||||||
var requestingTunnelMode by remember {
|
var requestingTunnelMode by remember {
|
||||||
mutableStateOf<Pair<TunnelMode?, TunnelConfig?>>(Pair(null, null))
|
mutableStateOf<Pair<TunnelMode?, TunnelConfig?>>(Pair(null, null))
|
||||||
}
|
}
|
||||||
var showLocalNetworkRationale by remember { mutableStateOf(false) }
|
|
||||||
var hasPromptedLocalNetwork by rememberSaveable { mutableStateOf(false) }
|
|
||||||
|
|
||||||
val localNetworkPermissionLauncher =
|
|
||||||
rememberLauncherForActivityResult(
|
|
||||||
contract = ActivityResultContracts.RequestPermission()
|
|
||||||
) { isGranted ->
|
|
||||||
if (!isGranted) {
|
|
||||||
val canAskAgain =
|
|
||||||
ActivityCompat.shouldShowRequestPermissionRationale(
|
|
||||||
this,
|
|
||||||
Manifest.permission.ACCESS_LOCAL_NETWORK,
|
|
||||||
)
|
|
||||||
|
|
||||||
if (!canAskAgain) {
|
|
||||||
val intent =
|
|
||||||
Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply {
|
|
||||||
data = Uri.fromParts("package", packageName, null)
|
|
||||||
}
|
|
||||||
startActivity(intent)
|
|
||||||
} else {
|
|
||||||
toaster.show(
|
|
||||||
message =
|
|
||||||
context.getString(R.string.local_network_permission_denied),
|
|
||||||
type = ToastType.Warning,
|
|
||||||
duration = 6000.milliseconds,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
LaunchedEffect(uiState.isAppLoaded) {
|
|
||||||
if (
|
|
||||||
uiState.isAppLoaded &&
|
|
||||||
!hasPromptedLocalNetwork &&
|
|
||||||
LocalNetworkPermissionHelper.shouldRequestPermission() &&
|
|
||||||
!LocalNetworkPermissionHelper.isPermissionGranted(context)
|
|
||||||
) {
|
|
||||||
hasPromptedLocalNetwork = true
|
|
||||||
showLocalNetworkRationale = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val startingStack = buildList {
|
val startingStack = buildList {
|
||||||
add(Route.Tunnels)
|
add(Route.Tunnels)
|
||||||
@@ -301,19 +232,23 @@ class MainActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
is GlobalSideEffect.Snackbar -> {
|
is GlobalSideEffect.Snackbar -> {
|
||||||
when (sideEffect.type) {
|
scope.launch {
|
||||||
ToastType.Warning,
|
snackbarState.showSnackbar(
|
||||||
ToastType.Error -> toaster.dismissAll()
|
SnackbarInfo(
|
||||||
else -> Unit
|
message =
|
||||||
|
buildAnnotatedString {
|
||||||
|
append(sideEffect.message.asString(context))
|
||||||
|
},
|
||||||
|
type = sideEffect.type ?: SnackbarType.INFO,
|
||||||
|
durationMs = sideEffect.durationMs ?: 4000L,
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
toaster.show(
|
|
||||||
message = sideEffect.message.asString(context),
|
|
||||||
type = sideEffect.type,
|
|
||||||
duration = (sideEffect.durationMs ?: 4000L).milliseconds,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
is GlobalSideEffect.Toast ->
|
||||||
|
scope.launch { context.showToast(sideEffect.message.asString(context)) }
|
||||||
|
|
||||||
is GlobalSideEffect.LaunchUrl -> context.openWebUrl(sideEffect.url)
|
is GlobalSideEffect.LaunchUrl -> context.openWebUrl(sideEffect.url)
|
||||||
is GlobalSideEffect.InstallApk -> context.installApk(sideEffect.apk)
|
is GlobalSideEffect.InstallApk -> context.installApk(sideEffect.apk)
|
||||||
}
|
}
|
||||||
@@ -340,58 +275,49 @@ class MainActivity : AppCompatActivity() {
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
if (showLocalNetworkRationale) {
|
val annotatedMessage = buildAnnotatedString {
|
||||||
LocalNetworkPermissionDialog(
|
append(context.getString(R.string.donation_prompt_prefix))
|
||||||
onDismiss = {
|
append(" ")
|
||||||
showLocalNetworkRationale = false
|
withLink(
|
||||||
toaster.show(
|
LinkAnnotation.Clickable(
|
||||||
message =
|
tag = context.getString(R.string.support),
|
||||||
context.getString(R.string.local_network_permission_denied),
|
styles =
|
||||||
type = ToastType.Warning,
|
TextLinkStyles(
|
||||||
duration = 6000.milliseconds,
|
style =
|
||||||
)
|
SpanStyle(
|
||||||
},
|
textDecoration = TextDecoration.Underline,
|
||||||
onAttest = {
|
color = MaterialTheme.colorScheme.primary,
|
||||||
showLocalNetworkRationale = false
|
),
|
||||||
|
focusedStyle =
|
||||||
localNetworkPermissionLauncher.launch(
|
SpanStyle(
|
||||||
Manifest.permission.ACCESS_LOCAL_NETWORK
|
textDecoration = TextDecoration.Underline,
|
||||||
)
|
color = MaterialTheme.colorScheme.primary,
|
||||||
},
|
background =
|
||||||
)
|
MaterialTheme.colorScheme.primary.copy(
|
||||||
}
|
alpha = 0.2f
|
||||||
|
),
|
||||||
uiState.pendingWgImportUrl?.let { url ->
|
),
|
||||||
val host = Uri.parse(url).host ?: url
|
),
|
||||||
InfoDialog(
|
) {
|
||||||
onDismiss = { viewModel.dismissWgImport() },
|
snackbarState.dismissCurrent()
|
||||||
onAttest = { viewModel.importFromUrl(url) },
|
navController.push(Route.Donate)
|
||||||
title = stringResource(R.string.add_from_url),
|
}
|
||||||
body = { Text(stringResource(R.string.wg_url_confirm_message, host)) },
|
) {
|
||||||
confirmText = stringResource(R.string.okay),
|
append(context.getString(R.string.donation_prompt_link))
|
||||||
)
|
}
|
||||||
|
append(" ")
|
||||||
|
append(context.getString(R.string.donation_prompt_suffix))
|
||||||
}
|
}
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
if (uiState.shouldShowDonationSnackbar && !uiState.alreadyDonated) {
|
if (uiState.shouldShowDonationSnackbar && !uiState.alreadyDonated) {
|
||||||
viewModel.setShouldShowDonationSnackbar(false)
|
viewModel.setShouldShowDonationSnackbar(false)
|
||||||
toaster.show(
|
snackbarState.showSnackbar(
|
||||||
message =
|
SnackbarInfo(
|
||||||
context.getString(R.string.donation_prompt_prefix) +
|
message = annotatedMessage,
|
||||||
" " +
|
type = SnackbarType.THANK_YOU,
|
||||||
context.getString(R.string.donation_prompt_link) +
|
durationMs = 30_000L,
|
||||||
" " +
|
)
|
||||||
context.getString(R.string.donation_prompt_suffix),
|
|
||||||
type = ToastType.Normal,
|
|
||||||
duration = 30_000L.milliseconds,
|
|
||||||
action =
|
|
||||||
TextToastAction(
|
|
||||||
text = context.getString(R.string.donate_title),
|
|
||||||
onClick = { toastId ->
|
|
||||||
toaster.dismiss(toastId)
|
|
||||||
navController.push(Route.Donate)
|
|
||||||
},
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -452,6 +378,25 @@ class MainActivity : AppCompatActivity() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
Scaffold(
|
Scaffold(
|
||||||
|
snackbarHost = {
|
||||||
|
snackbarState.SnackbarHost(
|
||||||
|
modifier =
|
||||||
|
Modifier.align(Alignment.BottomCenter)
|
||||||
|
.padding(bottom = 80.dp)
|
||||||
|
) { info ->
|
||||||
|
CustomSnackBar(
|
||||||
|
message = info.message,
|
||||||
|
type = info.type,
|
||||||
|
onDismiss = { snackbarState.dismissCurrent() },
|
||||||
|
containerColor =
|
||||||
|
MaterialTheme.colorScheme.surfaceColorAtElevation(
|
||||||
|
2.dp
|
||||||
|
),
|
||||||
|
modifier =
|
||||||
|
Modifier.wrapContentHeight(align = Alignment.Top),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
topBar = { DynamicTopAppBar(navState) },
|
topBar = { DynamicTopAppBar(navState) },
|
||||||
bottomBar = {
|
bottomBar = {
|
||||||
if (navState.showBottomItems) {
|
if (navState.showBottomItems) {
|
||||||
@@ -603,70 +548,6 @@ class MainActivity : AppCompatActivity() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Toaster(
|
|
||||||
state = toaster,
|
|
||||||
alignment = Alignment.BottomCenter,
|
|
||||||
offset = IntOffset(0, -220),
|
|
||||||
richColors = true,
|
|
||||||
background = {
|
|
||||||
Brush.linearGradient(
|
|
||||||
listOf(
|
|
||||||
MaterialTheme.colorScheme.surfaceColorAtElevation(2.dp),
|
|
||||||
MaterialTheme.colorScheme.surfaceColorAtElevation(2.dp),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
},
|
|
||||||
elevation = 1.dp,
|
|
||||||
shadowAmbientColor =
|
|
||||||
MaterialTheme.colorScheme.onSurface.copy(alpha = 0.08f),
|
|
||||||
shadowSpotColor =
|
|
||||||
MaterialTheme.colorScheme.onSurface.copy(alpha = 0.12f),
|
|
||||||
border = {
|
|
||||||
BorderStroke(
|
|
||||||
0.dp,
|
|
||||||
androidx.compose.ui.graphics.Color.Transparent,
|
|
||||||
)
|
|
||||||
},
|
|
||||||
actionSlot = { toast ->
|
|
||||||
(toast.action as? TextToastAction)?.let { action ->
|
|
||||||
TextButton(
|
|
||||||
onClick = { action.onClick(toast) },
|
|
||||||
colors =
|
|
||||||
ButtonDefaults.textButtonColors(
|
|
||||||
contentColor = MaterialTheme.colorScheme.primary
|
|
||||||
),
|
|
||||||
contentPadding = PaddingValues(horizontal = 12.dp),
|
|
||||||
) {
|
|
||||||
Text(text = action.text, fontWeight = FontWeight.Medium)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
iconSlot = { toast ->
|
|
||||||
val (icon, color) =
|
|
||||||
when (toast.type) {
|
|
||||||
ToastType.Success ->
|
|
||||||
Icons.Outlined.CheckCircleOutline to SilverTree
|
|
||||||
ToastType.Error ->
|
|
||||||
Icons.Outlined.ErrorOutline to AlertRed
|
|
||||||
ToastType.Warning ->
|
|
||||||
Icons.Outlined.WarningAmber to Straw
|
|
||||||
ToastType.Info ->
|
|
||||||
Icons.Outlined.Info to
|
|
||||||
MaterialTheme.colorScheme.onSurface
|
|
||||||
ToastType.Normal ->
|
|
||||||
Icons.Outlined.FavoriteBorder to Heart
|
|
||||||
}
|
|
||||||
Icon(
|
|
||||||
imageVector = icon,
|
|
||||||
contentDescription = null,
|
|
||||||
tint = color,
|
|
||||||
modifier = Modifier.padding(end = 12.dp),
|
|
||||||
)
|
|
||||||
},
|
|
||||||
contentColor = { MaterialTheme.colorScheme.onSurface },
|
|
||||||
shape = { RoundedCornerShape(16.dp) },
|
|
||||||
showCloseButton = true,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -674,14 +555,55 @@ class MainActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleWgDeepLinkIntent(intent: Intent) {
|
fun performBackup() = lifecycleScope.launch {
|
||||||
if (intent.action == Intent.ACTION_VIEW) {
|
roomBackup
|
||||||
val uri = intent.data ?: return
|
.database(appDatabase)
|
||||||
if (uri.scheme == "wg") {
|
.backupLocation(RoomBackup.BACKUP_FILE_LOCATION_CUSTOM_DIALOG)
|
||||||
val httpsUrl = uri.toString().replaceFirst("wg://", "https://")
|
.enableLogDebug(true)
|
||||||
viewModel.promptWgImport(httpsUrl)
|
.maxFileCount(5)
|
||||||
|
.apply {
|
||||||
|
onCompleteListener { success, _, _ ->
|
||||||
|
lifecycleScope.launch {
|
||||||
|
if (success) {
|
||||||
|
showToast(
|
||||||
|
getString(
|
||||||
|
R.string.backup_success,
|
||||||
|
getString(R.string.restarting_app),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
restartApp()
|
||||||
|
} else {
|
||||||
|
showToast(R.string.backup_failed)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
.backup()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun performRestore() = lifecycleScope.launch {
|
||||||
|
roomBackup
|
||||||
|
.database(appDatabase)
|
||||||
|
.enableLogDebug(true)
|
||||||
|
.backupLocation(RoomBackup.BACKUP_FILE_LOCATION_CUSTOM_DIALOG)
|
||||||
|
.apply {
|
||||||
|
onCompleteListener { success, _, _ ->
|
||||||
|
lifecycleScope.launch {
|
||||||
|
if (success) {
|
||||||
|
showToast(
|
||||||
|
getString(
|
||||||
|
R.string.restore_success,
|
||||||
|
getString(R.string.restarting_app),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
restartApp()
|
||||||
|
} else {
|
||||||
|
showToast(R.string.restore_failed)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.restore()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
@@ -689,105 +611,21 @@ class MainActivity : AppCompatActivity() {
|
|||||||
networkMonitor.checkPermissionsAndUpdateState()
|
networkMonitor.checkPermissionsAndUpdateState()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun performBackup(encrypt: Boolean = false, password: String? = null) {
|
|
||||||
roomBackup
|
|
||||||
.backupLocation(RoomBackup.BACKUP_FILE_LOCATION_CUSTOM_DIALOG)
|
|
||||||
.apply {
|
|
||||||
if (encrypt && !password.isNullOrBlank()) {
|
|
||||||
backupIsEncrypted(true)
|
|
||||||
customEncryptPassword(password)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.onCompleteListener { success, _, _ ->
|
|
||||||
lifecycleScope.launch {
|
|
||||||
val sideEffect =
|
|
||||||
if (success) {
|
|
||||||
GlobalSideEffect.Snackbar(
|
|
||||||
StringValue.StringResource(R.string.backup_success),
|
|
||||||
ToastType.Success,
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
GlobalSideEffect.Snackbar(
|
|
||||||
StringValue.StringResource(R.string.backup_failed),
|
|
||||||
ToastType.Error,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
viewModel.postSideEffect(sideEffect)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.backup()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun performRestore(encrypt: Boolean = false, password: String? = null) {
|
|
||||||
roomBackup
|
|
||||||
.backupLocation(RoomBackup.BACKUP_FILE_LOCATION_CUSTOM_DIALOG)
|
|
||||||
.apply {
|
|
||||||
if (encrypt && !password.isNullOrBlank()) {
|
|
||||||
backupIsEncrypted(true)
|
|
||||||
customEncryptPassword(password)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.onCompleteListener { success, message, exitCode ->
|
|
||||||
lifecycleScope.launch {
|
|
||||||
if (success) {
|
|
||||||
viewModel.postSideEffect(
|
|
||||||
GlobalSideEffect.Snackbar(
|
|
||||||
StringValue.StringResource(R.string.restore_success),
|
|
||||||
ToastType.Success,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
roomBackup.restartApp(Intent(this@MainActivity, MainActivity::class.java))
|
|
||||||
} else {
|
|
||||||
Timber.w("Restore failed, exitCode=$exitCode, message=$message")
|
|
||||||
|
|
||||||
val errorMessage =
|
|
||||||
when (exitCode) {
|
|
||||||
EXIT_CODE_ERROR_WRONG_DECRYPTION_PASSWORD ->
|
|
||||||
getString(R.string.restore_failed_wrong_password)
|
|
||||||
|
|
||||||
EXIT_CODE_ERROR,
|
|
||||||
EXIT_CODE_ERROR_DECRYPTION_ERROR,
|
|
||||||
EXIT_CODE_ERROR_RESTORE_BACKUP_IS_ENCRYPTED ->
|
|
||||||
getString(R.string.restore_failed_invalid_file)
|
|
||||||
|
|
||||||
else -> getString(R.string.restore_failed)
|
|
||||||
}
|
|
||||||
|
|
||||||
viewModel.postSideEffect(
|
|
||||||
GlobalSideEffect.Snackbar(
|
|
||||||
StringValue.DynamicString(errorMessage),
|
|
||||||
ToastType.Error,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.restore()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onNewIntent(intent: Intent) {
|
override fun onNewIntent(intent: Intent) {
|
||||||
super.onNewIntent(intent)
|
super.onNewIntent(intent)
|
||||||
setIntent(intent)
|
setIntent(intent)
|
||||||
handleConfigFileIntent(intent)
|
handleIncomingIntent(intent)
|
||||||
handleWgDeepLinkIntent(intent)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleConfigFileIntent(intent: Intent?) {
|
private fun handleIncomingIntent(intent: Intent?) {
|
||||||
intent ?: return
|
intent ?: return
|
||||||
|
|
||||||
when (intent.action) {
|
when (intent.action) {
|
||||||
Intent.ACTION_VIEW,
|
Intent.ACTION_VIEW,
|
||||||
Intent.ACTION_EDIT,
|
Intent.ACTION_EDIT,
|
||||||
Intent.ACTION_SEND -> {
|
Intent.ACTION_SEND -> {
|
||||||
val uri: Uri? = intent.data ?: return
|
val uri: Uri? = intent.data
|
||||||
val name = uri?.lastPathSegment?.lowercase() ?: return
|
uri?.let { viewModel.importFromUri(it) }
|
||||||
if (
|
|
||||||
!name.endsWith(FileUtils.CONF_FILE_EXTENSION) &&
|
|
||||||
!name.endsWith(FileUtils.ZIP_FILE_EXTENSION)
|
|
||||||
) {
|
|
||||||
Timber.d("Ignoring non-config URI in handleIncomingIntent: $uri")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
viewModel.importFromUri(uri)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,8 +6,11 @@ import com.zaneschepke.tunnel.backend.Backend
|
|||||||
import com.zaneschepke.tunnel.di.tunnelModule
|
import com.zaneschepke.tunnel.di.tunnelModule
|
||||||
import com.zaneschepke.tunnel.service.VpnService
|
import com.zaneschepke.tunnel.service.VpnService
|
||||||
import com.zaneschepke.wireguardautotunnel.core.event.TunnelEventDispatcher
|
import com.zaneschepke.wireguardautotunnel.core.event.TunnelEventDispatcher
|
||||||
|
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationService
|
||||||
import com.zaneschepke.wireguardautotunnel.core.orchestration.AppBoostrapCoordinator
|
import com.zaneschepke.wireguardautotunnel.core.orchestration.AppBoostrapCoordinator
|
||||||
import com.zaneschepke.wireguardautotunnel.core.orchestration.TunnelCoordinator
|
import com.zaneschepke.wireguardautotunnel.core.orchestration.TunnelCoordinator
|
||||||
|
import com.zaneschepke.wireguardautotunnel.core.service.tile.AutoTunnelTileRefresher
|
||||||
|
import com.zaneschepke.wireguardautotunnel.core.service.tile.TunnelTileRefresher
|
||||||
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelProvider
|
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelProvider
|
||||||
import com.zaneschepke.wireguardautotunnel.di.Dispatcher
|
import com.zaneschepke.wireguardautotunnel.di.Dispatcher
|
||||||
import com.zaneschepke.wireguardautotunnel.di.Scope
|
import com.zaneschepke.wireguardautotunnel.di.Scope
|
||||||
@@ -18,9 +21,6 @@ import com.zaneschepke.wireguardautotunnel.di.dispatchersModule
|
|||||||
import com.zaneschepke.wireguardautotunnel.di.networkModule
|
import com.zaneschepke.wireguardautotunnel.di.networkModule
|
||||||
import com.zaneschepke.wireguardautotunnel.di.tunnelBackendProviderModule
|
import com.zaneschepke.wireguardautotunnel.di.tunnelBackendProviderModule
|
||||||
import com.zaneschepke.wireguardautotunnel.di.workerModule
|
import com.zaneschepke.wireguardautotunnel.di.workerModule
|
||||||
import com.zaneschepke.wireguardautotunnel.notification.NotificationService
|
|
||||||
import com.zaneschepke.wireguardautotunnel.service.tile.AutoTunnelTileRefresher
|
|
||||||
import com.zaneschepke.wireguardautotunnel.service.tile.TunnelTileRefresher
|
|
||||||
import com.zaneschepke.wireguardautotunnel.util.ReleaseTree
|
import com.zaneschepke.wireguardautotunnel.util.ReleaseTree
|
||||||
import kotlinx.coroutines.CoroutineDispatcher
|
import kotlinx.coroutines.CoroutineDispatcher
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
@@ -51,13 +51,6 @@ class WireGuardAutoTunnel : Application(), KoinComponent {
|
|||||||
|
|
||||||
private val backend: Backend by inject()
|
private val backend: Backend by inject()
|
||||||
|
|
||||||
private val alwaysOnCallback =
|
|
||||||
object : VpnService.AlwaysOnCallback {
|
|
||||||
override fun alwaysOnTriggered() {
|
|
||||||
applicationScope.launch { tunnelCoordinator.startDefault() }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@OptIn(KoinViewModelScopeApi::class)
|
@OptIn(KoinViewModelScopeApi::class)
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
@@ -78,10 +71,11 @@ class WireGuardAutoTunnel : Application(), KoinComponent {
|
|||||||
lazyModules(networkModule)
|
lazyModules(networkModule)
|
||||||
}
|
}
|
||||||
instance = this
|
instance = this
|
||||||
|
|
||||||
notificationService.createAllChannels()
|
notificationService.createAllChannels()
|
||||||
|
|
||||||
syncTiles()
|
// Sync tiles
|
||||||
|
AutoTunnelTileRefresher.refresh(this)
|
||||||
|
TunnelTileRefresher.refresh(this)
|
||||||
|
|
||||||
if (BuildConfig.DEBUG) {
|
if (BuildConfig.DEBUG) {
|
||||||
Timber.plant(Timber.DebugTree())
|
Timber.plant(Timber.DebugTree())
|
||||||
@@ -93,7 +87,13 @@ class WireGuardAutoTunnel : Application(), KoinComponent {
|
|||||||
Timber.plant(ReleaseTree())
|
Timber.plant(ReleaseTree())
|
||||||
}
|
}
|
||||||
|
|
||||||
backend.setAlwaysOnCallback(alwaysOnCallback)
|
backend.setAlwaysOnCallback(
|
||||||
|
object : VpnService.AlwaysOnCallback {
|
||||||
|
override fun alwaysOnTriggered() {
|
||||||
|
applicationScope.launch { tunnelCoordinator.startDefault() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
val dispatcher = get<TunnelEventDispatcher>()
|
val dispatcher = get<TunnelEventDispatcher>()
|
||||||
val coordinator = get<TunnelCoordinator>()
|
val coordinator = get<TunnelCoordinator>()
|
||||||
@@ -111,11 +111,6 @@ class WireGuardAutoTunnel : Application(), KoinComponent {
|
|||||||
applicationScope.launch(ioDispatcher) { boostrapCoordinator.bootstrap() }
|
applicationScope.launch(ioDispatcher) { boostrapCoordinator.bootstrap() }
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun syncTiles() {
|
|
||||||
AutoTunnelTileRefresher.refresh(this)
|
|
||||||
TunnelTileRefresher.refresh(this)
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
lateinit var instance: WireGuardAutoTunnel
|
lateinit var instance: WireGuardAutoTunnel
|
||||||
private set
|
private set
|
||||||
|
|||||||
+1
-1
@@ -3,11 +3,11 @@ package com.zaneschepke.wireguardautotunnel.core.broadcast
|
|||||||
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.core.notification.NotificationService
|
||||||
import com.zaneschepke.wireguardautotunnel.core.orchestration.AutoTunnelCoordinator
|
import com.zaneschepke.wireguardautotunnel.core.orchestration.AutoTunnelCoordinator
|
||||||
import com.zaneschepke.wireguardautotunnel.core.orchestration.TunnelCoordinator
|
import com.zaneschepke.wireguardautotunnel.core.orchestration.TunnelCoordinator
|
||||||
import com.zaneschepke.wireguardautotunnel.di.Scope
|
import com.zaneschepke.wireguardautotunnel.di.Scope
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.enums.NotificationAction
|
import com.zaneschepke.wireguardautotunnel.domain.enums.NotificationAction
|
||||||
import com.zaneschepke.wireguardautotunnel.notification.NotificationService
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.koin.core.component.KoinComponent
|
import org.koin.core.component.KoinComponent
|
||||||
|
|||||||
+14
-160
@@ -1,41 +1,29 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.core.event
|
package com.zaneschepke.wireguardautotunnel.core.event
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import com.dokar.sonner.ToastType
|
|
||||||
import com.zaneschepke.tunnel.event.TunnelEvent
|
import com.zaneschepke.tunnel.event.TunnelEvent
|
||||||
import com.zaneschepke.tunnel.model.BackendMode
|
import com.zaneschepke.tunnel.model.BackendMode
|
||||||
import com.zaneschepke.tunnel.state.BackendStatus
|
import com.zaneschepke.tunnel.state.BackendStatus
|
||||||
import com.zaneschepke.wireguardautotunnel.R
|
import com.zaneschepke.wireguardautotunnel.R
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.repository.GlobalEffectRepository
|
import com.zaneschepke.wireguardautotunnel.core.notification.TunnelNotificationLine
|
||||||
|
import com.zaneschepke.wireguardautotunnel.core.notification.TunnelNotificationService
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository
|
import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.sideeffect.GlobalSideEffect
|
|
||||||
import com.zaneschepke.wireguardautotunnel.lifecyle.AppVisibilityObserver
|
|
||||||
import com.zaneschepke.wireguardautotunnel.notification.TunnelNotificationLine
|
|
||||||
import com.zaneschepke.wireguardautotunnel.notification.TunnelNotificationService
|
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.state.DisplayTunnelState
|
import com.zaneschepke.wireguardautotunnel.ui.state.DisplayTunnelState
|
||||||
import com.zaneschepke.wireguardautotunnel.util.StringValue
|
|
||||||
import kotlin.time.Duration.Companion.milliseconds
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.FlowPreview
|
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.combine
|
import kotlinx.coroutines.flow.combine
|
||||||
import kotlinx.coroutines.flow.debounce
|
|
||||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
import kotlinx.coroutines.flow.launchIn
|
import kotlinx.coroutines.flow.launchIn
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import kotlinx.coroutines.flow.onEach
|
import kotlinx.coroutines.flow.onEach
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
|
|
||||||
class TunnelEventDispatcher(
|
class TunnelEventDispatcher(
|
||||||
private val notificationManager: TunnelNotificationService,
|
private val notificationManager: TunnelNotificationService,
|
||||||
private val tunnelRepository: TunnelRepository,
|
private val tunnelRepository: TunnelRepository,
|
||||||
private val context: Context,
|
private val context: Context,
|
||||||
private val appVisibilityObserver: AppVisibilityObserver,
|
|
||||||
private val globalEffectRepository: GlobalEffectRepository,
|
|
||||||
) {
|
) {
|
||||||
|
|
||||||
@OptIn(FlowPreview::class)
|
|
||||||
fun bind(
|
fun bind(
|
||||||
scope: CoroutineScope,
|
scope: CoroutineScope,
|
||||||
providerEvents: Flow<TunnelEvent>,
|
providerEvents: Flow<TunnelEvent>,
|
||||||
@@ -44,174 +32,54 @@ class TunnelEventDispatcher(
|
|||||||
tunnelDisplayStates: StateFlow<Map<Int, DisplayTunnelState>>,
|
tunnelDisplayStates: StateFlow<Map<Int, DisplayTunnelState>>,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
// Informational events from tunnel backend
|
// informational events
|
||||||
providerEvents
|
providerEvents
|
||||||
|
.distinctUntilChanged()
|
||||||
.onEach { event ->
|
.onEach { event ->
|
||||||
when (event) {
|
when (event) {
|
||||||
is TunnelEvent.FallbackToIpv4 -> {
|
is TunnelEvent.FallbackToIpv4 -> {
|
||||||
val name = getTunnelName(event.tunnelId)
|
val name = getTunnelName(event.tunnelId)
|
||||||
showOrNotify(
|
notificationManager.showIpv4Fallback(name)
|
||||||
scope = scope,
|
|
||||||
foregroundAction = {
|
|
||||||
globalEffectRepository.post(
|
|
||||||
GlobalSideEffect.Snackbar(
|
|
||||||
message =
|
|
||||||
StringValue.DynamicString(
|
|
||||||
context.getString(
|
|
||||||
R.string.notification_ipv4_fallback_message,
|
|
||||||
name,
|
|
||||||
)
|
|
||||||
),
|
|
||||||
type = ToastType.Info,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
},
|
|
||||||
backgroundAction = { notificationManager.showIpv4Fallback(name) },
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
is TunnelEvent.RecoveredToIpv6 -> {
|
is TunnelEvent.RecoveredToIpv6 -> {
|
||||||
val name = getTunnelName(event.tunnelId)
|
val name = getTunnelName(event.tunnelId)
|
||||||
showOrNotify(
|
notificationManager.showIpv6Recovery(name)
|
||||||
scope = scope,
|
|
||||||
foregroundAction = {
|
|
||||||
globalEffectRepository.post(
|
|
||||||
GlobalSideEffect.Snackbar(
|
|
||||||
message =
|
|
||||||
StringValue.DynamicString(
|
|
||||||
context.getString(
|
|
||||||
R.string.notification_ipv6_recovery_message,
|
|
||||||
name,
|
|
||||||
)
|
|
||||||
),
|
|
||||||
type = ToastType.Success,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
},
|
|
||||||
backgroundAction = { notificationManager.showIpv6Recovery(name) },
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
is TunnelEvent.DynamicDnsUpdate -> {
|
is TunnelEvent.DynamicDnsUpdate -> {
|
||||||
val name = getTunnelName(event.tunnelId)
|
val name = getTunnelName(event.tunnelId)
|
||||||
showOrNotify(
|
notificationManager.showDynamicDnsUpdate(name)
|
||||||
scope = scope,
|
|
||||||
foregroundAction = {
|
|
||||||
globalEffectRepository.post(
|
|
||||||
GlobalSideEffect.Snackbar(
|
|
||||||
message =
|
|
||||||
StringValue.DynamicString(
|
|
||||||
context.getString(
|
|
||||||
R.string.notification_dynamic_dns_message,
|
|
||||||
name,
|
|
||||||
)
|
|
||||||
),
|
|
||||||
type = ToastType.Info,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
},
|
|
||||||
backgroundAction = { notificationManager.showDynamicDnsUpdate(name) },
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
is TunnelEvent.NoRootShellAccess -> {
|
is TunnelEvent.NoRootShellAccess -> {
|
||||||
showOrNotify(
|
notificationManager.showRootShellAccess()
|
||||||
scope = scope,
|
|
||||||
foregroundAction = {
|
|
||||||
globalEffectRepository.post(
|
|
||||||
GlobalSideEffect.Snackbar(
|
|
||||||
message =
|
|
||||||
StringValue.DynamicString(
|
|
||||||
context.getString(R.string.error_root_denied)
|
|
||||||
),
|
|
||||||
type = ToastType.Error,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
},
|
|
||||||
backgroundAction = { notificationManager.showRootShellAccess() },
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.launchIn(scope)
|
.launchIn(scope)
|
||||||
|
|
||||||
// Errors from our tunnel coordinator
|
// errors from the coordinator
|
||||||
coordinatorErrors
|
coordinatorErrors
|
||||||
|
.distinctUntilChanged()
|
||||||
.onEach { error ->
|
.onEach { error ->
|
||||||
when (error) {
|
when (error) {
|
||||||
is TunnelErrorEvent.VpnPermissionDenied -> {
|
is TunnelErrorEvent.VpnPermissionDenied -> {
|
||||||
showOrNotify(
|
notificationManager.showVpnRequired()
|
||||||
scope = scope,
|
|
||||||
foregroundAction = {
|
|
||||||
globalEffectRepository.post(
|
|
||||||
GlobalSideEffect.Snackbar(
|
|
||||||
message =
|
|
||||||
StringValue.DynamicString(
|
|
||||||
context.getString(R.string.vpn_permission_required)
|
|
||||||
),
|
|
||||||
type = ToastType.Error,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
},
|
|
||||||
backgroundAction = { notificationManager.showVpnRequired() },
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
is TunnelErrorEvent.InternalFailure -> {
|
is TunnelErrorEvent.InternalFailure -> {
|
||||||
showOrNotify(
|
notificationManager.showError(error.message)
|
||||||
scope = scope,
|
|
||||||
foregroundAction = {
|
|
||||||
globalEffectRepository.post(
|
|
||||||
GlobalSideEffect.Snackbar(
|
|
||||||
message = StringValue.DynamicString(error.message),
|
|
||||||
type = ToastType.Error,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
},
|
|
||||||
backgroundAction = { notificationManager.showError(error.message) },
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
is TunnelErrorEvent.Socks5PortUnavailable -> {
|
is TunnelErrorEvent.Socks5PortUnavailable -> {
|
||||||
val name = getTunnelName(error.tunnelId)
|
val name = getTunnelName(error.tunnelId)
|
||||||
val message =
|
notificationManager.showSocks5PortUnavailable(error.port, name)
|
||||||
context.getString(R.string.error_socks5_port_unavailable, error.port)
|
|
||||||
|
|
||||||
showOrNotify(
|
|
||||||
scope = scope,
|
|
||||||
foregroundAction = {
|
|
||||||
globalEffectRepository.post(
|
|
||||||
GlobalSideEffect.Snackbar(
|
|
||||||
message = StringValue.DynamicString(message),
|
|
||||||
type = ToastType.Error,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
},
|
|
||||||
backgroundAction = {
|
|
||||||
notificationManager.showSocks5PortUnavailable(error.port, name)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
is TunnelErrorEvent.HttpPortUnavailable -> {
|
is TunnelErrorEvent.HttpPortUnavailable -> {
|
||||||
val name = getTunnelName(error.tunnelId)
|
val name = getTunnelName(error.tunnelId)
|
||||||
val message =
|
notificationManager.showHttpPortUnavailable(error.port, name)
|
||||||
context.getString(R.string.error_http_port_unavailable, error.port)
|
|
||||||
|
|
||||||
showOrNotify(
|
|
||||||
scope = scope,
|
|
||||||
foregroundAction = {
|
|
||||||
globalEffectRepository.post(
|
|
||||||
GlobalSideEffect.Snackbar(
|
|
||||||
message = StringValue.DynamicString(message),
|
|
||||||
type = ToastType.Error,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
},
|
|
||||||
backgroundAction = {
|
|
||||||
notificationManager.showHttpPortUnavailable(error.port, name)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -245,7 +113,6 @@ class TunnelEventDispatcher(
|
|||||||
.associateBy { it.id }
|
.associateBy { it.id }
|
||||||
}
|
}
|
||||||
.distinctUntilChanged()
|
.distinctUntilChanged()
|
||||||
.debounce(500.milliseconds) // give the service notification time to display
|
|
||||||
.onEach { vpnLines -> notificationManager.updateVpnPersistentNotification(vpnLines) }
|
.onEach { vpnLines -> notificationManager.updateVpnPersistentNotification(vpnLines) }
|
||||||
.launchIn(scope)
|
.launchIn(scope)
|
||||||
|
|
||||||
@@ -273,25 +140,12 @@ class TunnelEventDispatcher(
|
|||||||
.associateBy { it.id }
|
.associateBy { it.id }
|
||||||
}
|
}
|
||||||
.distinctUntilChanged()
|
.distinctUntilChanged()
|
||||||
.debounce(500.milliseconds) // give the service notification time to display
|
|
||||||
.onEach { proxyLines ->
|
.onEach { proxyLines ->
|
||||||
notificationManager.updateProxyPersistentNotification(proxyLines)
|
notificationManager.updateProxyPersistentNotification(proxyLines)
|
||||||
}
|
}
|
||||||
.launchIn(scope)
|
.launchIn(scope)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun showOrNotify(
|
|
||||||
scope: CoroutineScope,
|
|
||||||
foregroundAction: suspend () -> Unit,
|
|
||||||
backgroundAction: () -> Unit,
|
|
||||||
) {
|
|
||||||
if (appVisibilityObserver.isForeground.value) {
|
|
||||||
scope.launch { foregroundAction() }
|
|
||||||
} else {
|
|
||||||
backgroundAction()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun getTunnelName(tunnelId: Int): String {
|
private suspend fun getTunnelName(tunnelId: Int): String {
|
||||||
return tunnelRepository.getById(tunnelId)?.name ?: context.getString(R.string.unknown)
|
return tunnelRepository.getById(tunnelId)?.name ?: context.getString(R.string.unknown)
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-2
@@ -1,4 +1,4 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.notification
|
package com.zaneschepke.wireguardautotunnel.core.notification
|
||||||
|
|
||||||
import android.Manifest
|
import android.Manifest
|
||||||
import android.app.Notification
|
import android.app.Notification
|
||||||
@@ -17,8 +17,8 @@ import androidx.core.app.NotificationManagerCompat.IMPORTANCE_HIGH
|
|||||||
import com.zaneschepke.wireguardautotunnel.MainActivity
|
import com.zaneschepke.wireguardautotunnel.MainActivity
|
||||||
import com.zaneschepke.wireguardautotunnel.R
|
import com.zaneschepke.wireguardautotunnel.R
|
||||||
import com.zaneschepke.wireguardautotunnel.core.broadcast.NotificationActionReceiver
|
import com.zaneschepke.wireguardautotunnel.core.broadcast.NotificationActionReceiver
|
||||||
|
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationService.Companion.EXTRA_ID
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.enums.NotificationAction
|
import com.zaneschepke.wireguardautotunnel.domain.enums.NotificationAction
|
||||||
import com.zaneschepke.wireguardautotunnel.notification.NotificationService.Companion.EXTRA_ID
|
|
||||||
import com.zaneschepke.wireguardautotunnel.util.StringValue
|
import com.zaneschepke.wireguardautotunnel.util.StringValue
|
||||||
|
|
||||||
class AndroidNotificationService(override val context: Context) : NotificationService {
|
class AndroidNotificationService(override val context: Context) : NotificationService {
|
||||||
+25
-130
@@ -1,116 +1,21 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.notification
|
package com.zaneschepke.wireguardautotunnel.core.notification
|
||||||
|
|
||||||
import androidx.core.app.NotificationCompat
|
import androidx.core.app.NotificationCompat
|
||||||
import com.zaneschepke.wireguardautotunnel.R
|
import com.zaneschepke.wireguardautotunnel.R
|
||||||
|
import com.zaneschepke.wireguardautotunnel.core.notification.AndroidNotificationService.NotificationChannels
|
||||||
|
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationService.Companion.PROXY_GROUP_KEY
|
||||||
|
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationService.Companion.PROXY_NOTIFICATION_ID
|
||||||
|
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationService.Companion.TUNNEL_ERROR_NOTIFICATION_ID
|
||||||
|
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationService.Companion.TUNNEL_MESSAGES_NOTIFICATION_ID
|
||||||
|
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationService.Companion.VPN_GROUP_KEY
|
||||||
|
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationService.Companion.VPN_NOTIFICATION_ID
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.enums.NotificationAction
|
import com.zaneschepke.wireguardautotunnel.domain.enums.NotificationAction
|
||||||
import com.zaneschepke.wireguardautotunnel.notification.AndroidNotificationService.NotificationChannels
|
|
||||||
import com.zaneschepke.wireguardautotunnel.notification.NotificationService.Companion.PROXY_GROUP_KEY
|
|
||||||
import com.zaneschepke.wireguardautotunnel.notification.NotificationService.Companion.PROXY_NOTIFICATION_ID
|
|
||||||
import com.zaneschepke.wireguardautotunnel.notification.NotificationService.Companion.TUNNEL_ERROR_NOTIFICATION_ID
|
|
||||||
import com.zaneschepke.wireguardautotunnel.notification.NotificationService.Companion.TUNNEL_MESSAGES_NOTIFICATION_ID
|
|
||||||
import com.zaneschepke.wireguardautotunnel.notification.NotificationService.Companion.VPN_GROUP_KEY
|
|
||||||
import com.zaneschepke.wireguardautotunnel.notification.NotificationService.Companion.VPN_NOTIFICATION_ID
|
|
||||||
|
|
||||||
class AndroidTunnelNotificationService(private val notificationService: NotificationService) :
|
class AndroidTunnelNotificationService(private val notificationService: NotificationService) :
|
||||||
TunnelNotificationService {
|
TunnelNotificationService {
|
||||||
|
|
||||||
private val context = notificationService.context
|
private val context = notificationService.context
|
||||||
|
|
||||||
private fun createGroupNotification(
|
|
||||||
tunnelNotificationLines: Map<Int, TunnelNotificationLine>,
|
|
||||||
channel: NotificationChannels.Tunnel,
|
|
||||||
groupKey: String,
|
|
||||||
): android.app.Notification {
|
|
||||||
val title =
|
|
||||||
if (tunnelNotificationLines.size == 1) {
|
|
||||||
val name = tunnelNotificationLines.values.first().name
|
|
||||||
when (channel) {
|
|
||||||
is NotificationChannels.Tunnel.VPN ->
|
|
||||||
"${context.getString(R.string.vpn)} • $name"
|
|
||||||
is NotificationChannels.Tunnel.Proxy ->
|
|
||||||
"${context.getString(R.string.proxy)} • $name"
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
when (channel) {
|
|
||||||
is NotificationChannels.Tunnel.VPN -> context.getString(R.string.vpn)
|
|
||||||
is NotificationChannels.Tunnel.Proxy -> context.getString(R.string.proxy)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val formattedLines =
|
|
||||||
tunnelNotificationLines.values.map { line ->
|
|
||||||
val status = line.displayState.asLocalizedString(context)
|
|
||||||
|
|
||||||
if (tunnelNotificationLines.size == 1) {
|
|
||||||
status
|
|
||||||
} else {
|
|
||||||
context.getString(R.string.notification_tunnel_status_format, line.name, status)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
val description = formattedLines.joinToString("\n")
|
|
||||||
|
|
||||||
val actions =
|
|
||||||
if (tunnelNotificationLines.size == 1) {
|
|
||||||
val tunnelId = tunnelNotificationLines.keys.first()
|
|
||||||
listOf(
|
|
||||||
notificationService.createNotificationAction(
|
|
||||||
notificationAction = NotificationAction.TUNNEL_OFF,
|
|
||||||
extraId = tunnelId,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
listOf(
|
|
||||||
notificationService.createNotificationAction(
|
|
||||||
notificationAction = NotificationAction.STOP_ALL,
|
|
||||||
extraId = null,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
val style =
|
|
||||||
if (tunnelNotificationLines.size > 1) {
|
|
||||||
NotificationCompat.InboxStyle()
|
|
||||||
.setBigContentTitle(title)
|
|
||||||
.setSummaryText(
|
|
||||||
"${tunnelNotificationLines.size} ${context.getString(R.string.tunnels).lowercase()}"
|
|
||||||
)
|
|
||||||
.also { inbox -> formattedLines.forEach { inbox.addLine(it) } }
|
|
||||||
} else {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
|
|
||||||
return notificationService.createNotification(
|
|
||||||
channel = channel,
|
|
||||||
title = title,
|
|
||||||
description = description,
|
|
||||||
actions = actions,
|
|
||||||
onGoing = true,
|
|
||||||
onlyAlertOnce = true,
|
|
||||||
groupKey = groupKey,
|
|
||||||
style = style,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun buildVpnPersistentNotification(
|
|
||||||
tunnelNotificationLines: Map<Int, TunnelNotificationLine>
|
|
||||||
): android.app.Notification {
|
|
||||||
return createGroupNotification(
|
|
||||||
tunnelNotificationLines,
|
|
||||||
NotificationChannels.Tunnel.VPN,
|
|
||||||
VPN_GROUP_KEY,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun buildProxyPersistentNotification(
|
|
||||||
tunnelNotificationLines: Map<Int, TunnelNotificationLine>
|
|
||||||
): android.app.Notification {
|
|
||||||
return createGroupNotification(
|
|
||||||
tunnelNotificationLines,
|
|
||||||
NotificationChannels.Tunnel.Proxy,
|
|
||||||
PROXY_GROUP_KEY,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun updateGroupNotification(
|
private fun updateGroupNotification(
|
||||||
tunnelNotificationLines: Map<Int, TunnelNotificationLine>,
|
tunnelNotificationLines: Map<Int, TunnelNotificationLine>,
|
||||||
notificationId: Int,
|
notificationId: Int,
|
||||||
@@ -183,36 +88,26 @@ class AndroidTunnelNotificationService(private val notificationService: Notifica
|
|||||||
notificationService.show(notificationId, notification)
|
notificationService.show(notificationId, notification)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun updateVpnPersistentNotification(
|
|
||||||
tunnelNotificationLines: Map<Int, TunnelNotificationLine>
|
|
||||||
) {
|
|
||||||
if (tunnelNotificationLines.isEmpty()) {
|
|
||||||
notificationService.remove(VPN_NOTIFICATION_ID)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
val notification =
|
|
||||||
createGroupNotification(
|
|
||||||
tunnelNotificationLines,
|
|
||||||
NotificationChannels.Tunnel.VPN,
|
|
||||||
VPN_GROUP_KEY,
|
|
||||||
)
|
|
||||||
notificationService.show(VPN_NOTIFICATION_ID, notification)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun updateProxyPersistentNotification(
|
override fun updateProxyPersistentNotification(
|
||||||
tunnelNotificationLines: Map<Int, TunnelNotificationLine>
|
tunnelNotificationLines: Map<Int, TunnelNotificationLine>
|
||||||
) {
|
) {
|
||||||
if (tunnelNotificationLines.isEmpty()) {
|
updateGroupNotification(
|
||||||
notificationService.remove(PROXY_NOTIFICATION_ID)
|
tunnelNotificationLines = tunnelNotificationLines,
|
||||||
return
|
notificationId = PROXY_NOTIFICATION_ID,
|
||||||
}
|
channel = NotificationChannels.Tunnel.Proxy,
|
||||||
val notification =
|
groupKey = PROXY_GROUP_KEY,
|
||||||
createGroupNotification(
|
)
|
||||||
tunnelNotificationLines,
|
}
|
||||||
NotificationChannels.Tunnel.Proxy,
|
|
||||||
PROXY_GROUP_KEY,
|
override fun updateVpnPersistentNotification(
|
||||||
)
|
tunnelNotificationLines: Map<Int, TunnelNotificationLine>
|
||||||
notificationService.show(PROXY_NOTIFICATION_ID, notification)
|
) {
|
||||||
|
updateGroupNotification(
|
||||||
|
tunnelNotificationLines = tunnelNotificationLines,
|
||||||
|
notificationId = VPN_NOTIFICATION_ID,
|
||||||
|
channel = NotificationChannels.Tunnel.VPN,
|
||||||
|
groupKey = VPN_GROUP_KEY,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun showIpv4Fallback(tunnelName: String) {
|
override fun showIpv4Fallback(tunnelName: String) {
|
||||||
+2
-2
@@ -1,10 +1,10 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.notification
|
package com.zaneschepke.wireguardautotunnel.core.notification
|
||||||
|
|
||||||
import android.app.Notification
|
import android.app.Notification
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import androidx.core.app.NotificationCompat
|
import androidx.core.app.NotificationCompat
|
||||||
|
import com.zaneschepke.wireguardautotunnel.core.notification.AndroidNotificationService.NotificationChannels
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.enums.NotificationAction
|
import com.zaneschepke.wireguardautotunnel.domain.enums.NotificationAction
|
||||||
import com.zaneschepke.wireguardautotunnel.notification.AndroidNotificationService.NotificationChannels
|
|
||||||
import com.zaneschepke.wireguardautotunnel.util.StringValue
|
import com.zaneschepke.wireguardautotunnel.util.StringValue
|
||||||
|
|
||||||
interface NotificationService {
|
interface NotificationService {
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.notification
|
package com.zaneschepke.wireguardautotunnel.core.notification
|
||||||
|
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.state.DisplayTunnelState
|
import com.zaneschepke.wireguardautotunnel.ui.state.DisplayTunnelState
|
||||||
|
|
||||||
+1
-11
@@ -1,6 +1,4 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.notification
|
package com.zaneschepke.wireguardautotunnel.core.notification
|
||||||
|
|
||||||
import android.app.Notification
|
|
||||||
|
|
||||||
interface TunnelNotificationService {
|
interface TunnelNotificationService {
|
||||||
|
|
||||||
@@ -8,14 +6,6 @@ interface TunnelNotificationService {
|
|||||||
|
|
||||||
fun updateVpnPersistentNotification(tunnelNotificationLines: Map<Int, TunnelNotificationLine>)
|
fun updateVpnPersistentNotification(tunnelNotificationLines: Map<Int, TunnelNotificationLine>)
|
||||||
|
|
||||||
fun buildVpnPersistentNotification(
|
|
||||||
tunnelNotificationLines: Map<Int, TunnelNotificationLine>
|
|
||||||
): Notification
|
|
||||||
|
|
||||||
fun buildProxyPersistentNotification(
|
|
||||||
tunnelNotificationLines: Map<Int, TunnelNotificationLine>
|
|
||||||
): Notification
|
|
||||||
|
|
||||||
fun showIpv4Fallback(tunnelName: String)
|
fun showIpv4Fallback(tunnelName: String)
|
||||||
|
|
||||||
fun showIpv6Recovery(tunnelName: String)
|
fun showIpv6Recovery(tunnelName: String)
|
||||||
+2
-2
@@ -1,8 +1,8 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.core.orchestration
|
package com.zaneschepke.wireguardautotunnel.core.orchestration
|
||||||
|
|
||||||
|
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
|
||||||
|
import com.zaneschepke.wireguardautotunnel.core.service.autotunnel.AutoTunnelStateHolder
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.repository.AutoTunnelSettingsRepository
|
import com.zaneschepke.wireguardautotunnel.domain.repository.AutoTunnelSettingsRepository
|
||||||
import com.zaneschepke.wireguardautotunnel.service.ServiceManager
|
|
||||||
import com.zaneschepke.wireguardautotunnel.service.autotunnel.AutoTunnelStateHolder
|
|
||||||
|
|
||||||
class AutoTunnelCoordinator(
|
class AutoTunnelCoordinator(
|
||||||
private val repository: AutoTunnelSettingsRepository,
|
private val repository: AutoTunnelSettingsRepository,
|
||||||
|
|||||||
+10
-50
@@ -2,6 +2,7 @@ package com.zaneschepke.wireguardautotunnel.core.orchestration
|
|||||||
|
|
||||||
import com.zaneschepke.tunnel.model.BackendMode
|
import com.zaneschepke.tunnel.model.BackendMode
|
||||||
import com.zaneschepke.wireguardautotunnel.core.event.TunnelErrorEvent
|
import com.zaneschepke.wireguardautotunnel.core.event.TunnelErrorEvent
|
||||||
|
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
|
||||||
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelProvider
|
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelProvider
|
||||||
import com.zaneschepke.wireguardautotunnel.data.repository.RoomDnsSettingsRepository
|
import com.zaneschepke.wireguardautotunnel.data.repository.RoomDnsSettingsRepository
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelActionSource
|
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelActionSource
|
||||||
@@ -9,16 +10,13 @@ import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelMode
|
|||||||
import com.zaneschepke.wireguardautotunnel.domain.events.TunnelActionEvent
|
import com.zaneschepke.wireguardautotunnel.domain.events.TunnelActionEvent
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.model.DnsSettings
|
import com.zaneschepke.wireguardautotunnel.domain.model.DnsSettings
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.model.GeneralSettings
|
import com.zaneschepke.wireguardautotunnel.domain.model.GeneralSettings
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.model.LockdownSettings
|
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.model.MonitoringSettings
|
import com.zaneschepke.wireguardautotunnel.domain.model.MonitoringSettings
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.model.ProxySettings
|
import com.zaneschepke.wireguardautotunnel.domain.model.ProxySettings
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConfig
|
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConfig
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.repository.GeneralSettingRepository
|
import com.zaneschepke.wireguardautotunnel.domain.repository.GeneralSettingRepository
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.repository.LockdownSettingsRepository
|
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.repository.MonitoringSettingsRepository
|
import com.zaneschepke.wireguardautotunnel.domain.repository.MonitoringSettingsRepository
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.repository.ProxySettingsRepository
|
import com.zaneschepke.wireguardautotunnel.domain.repository.ProxySettingsRepository
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository
|
import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository
|
||||||
import com.zaneschepke.wireguardautotunnel.service.ServiceManager
|
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.state.DisplayTunnelState
|
import com.zaneschepke.wireguardautotunnel.ui.state.DisplayTunnelState
|
||||||
import kotlin.time.Duration.Companion.milliseconds
|
import kotlin.time.Duration.Companion.milliseconds
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
@@ -46,13 +44,9 @@ class TunnelCoordinator(
|
|||||||
dnsSettingsRepository: RoomDnsSettingsRepository,
|
dnsSettingsRepository: RoomDnsSettingsRepository,
|
||||||
monitoringSettingsRepository: MonitoringSettingsRepository,
|
monitoringSettingsRepository: MonitoringSettingsRepository,
|
||||||
proxyRepository: ProxySettingsRepository,
|
proxyRepository: ProxySettingsRepository,
|
||||||
lockdownModeRepository: LockdownSettingsRepository,
|
|
||||||
scope: CoroutineScope,
|
scope: CoroutineScope,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
private val _userOverrideFlow = MutableSharedFlow<Unit>(extraBufferCapacity = 1)
|
|
||||||
val userOverrideFlow = _userOverrideFlow.asSharedFlow()
|
|
||||||
|
|
||||||
@OptIn(FlowPreview::class)
|
@OptIn(FlowPreview::class)
|
||||||
val tunnelDisplayStates: StateFlow<Map<Int, DisplayTunnelState>> =
|
val tunnelDisplayStates: StateFlow<Map<Int, DisplayTunnelState>> =
|
||||||
tunnelProvider.backendStatus
|
tunnelProvider.backendStatus
|
||||||
@@ -69,7 +63,6 @@ class TunnelCoordinator(
|
|||||||
val dns: DnsSettings,
|
val dns: DnsSettings,
|
||||||
val monitoring: MonitoringSettings,
|
val monitoring: MonitoringSettings,
|
||||||
val proxy: ProxySettings,
|
val proxy: ProxySettings,
|
||||||
val lockdown: LockdownSettings,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
private val runtimeSettingsSnapshot =
|
private val runtimeSettingsSnapshot =
|
||||||
@@ -78,14 +71,12 @@ class TunnelCoordinator(
|
|||||||
dnsSettingsRepository.flow,
|
dnsSettingsRepository.flow,
|
||||||
monitoringSettingsRepository.flow,
|
monitoringSettingsRepository.flow,
|
||||||
proxyRepository.flow,
|
proxyRepository.flow,
|
||||||
lockdownModeRepository.flow,
|
) { general, dns, monitoring, proxy ->
|
||||||
) { general, dns, monitoring, proxy, lockdown ->
|
|
||||||
RuntimeSettingsSnapshot(
|
RuntimeSettingsSnapshot(
|
||||||
general = general,
|
general = general,
|
||||||
dns = dns,
|
dns = dns,
|
||||||
monitoring = monitoring,
|
monitoring = monitoring,
|
||||||
proxy = proxy,
|
proxy = proxy,
|
||||||
lockdown = lockdown,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -116,34 +107,15 @@ class TunnelCoordinator(
|
|||||||
) = tunnelMutex.withLock {
|
) = tunnelMutex.withLock {
|
||||||
// wait for app to be bootstrapped
|
// wait for app to be bootstrapped
|
||||||
bootstrapCoordinator.isReady.first { it }
|
bootstrapCoordinator.isReady.first { it }
|
||||||
|
|
||||||
if (source == TunnelActionSource.USER) {
|
|
||||||
_userOverrideFlow.tryEmit(Unit)
|
|
||||||
}
|
|
||||||
|
|
||||||
// enforce single tunnel, for now
|
|
||||||
if (backendStatus.value.activeTunnels.isNotEmpty()) {
|
|
||||||
stopActiveTunnelsInternal(source)
|
|
||||||
}
|
|
||||||
|
|
||||||
startTunnelInternal(config, source)
|
startTunnelInternal(config, source)
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun stopTunnel(id: Int, source: TunnelActionSource = TunnelActionSource.USER) =
|
suspend fun stopTunnel(id: Int, source: TunnelActionSource = TunnelActionSource.USER) =
|
||||||
tunnelMutex.withLock {
|
tunnelMutex.withLock {
|
||||||
if (source == TunnelActionSource.USER) {
|
|
||||||
_userOverrideFlow.tryEmit(Unit)
|
|
||||||
}
|
|
||||||
stopTunnelInternal(id, source)
|
stopTunnelInternal(id, source)
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun stopActiveTunnels(source: TunnelActionSource = TunnelActionSource.USER) =
|
suspend fun stopActiveTunnels() = tunnelMutex.withLock { stopActiveTunnelsInternal() }
|
||||||
tunnelMutex.withLock {
|
|
||||||
if (source == TunnelActionSource.USER) {
|
|
||||||
_userOverrideFlow.tryEmit(Unit)
|
|
||||||
}
|
|
||||||
stopActiveTunnelsInternal(source)
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun startTunnelInternal(
|
private suspend fun startTunnelInternal(
|
||||||
tunnelConfig: TunnelConfig,
|
tunnelConfig: TunnelConfig,
|
||||||
@@ -155,7 +127,6 @@ class TunnelCoordinator(
|
|||||||
val dnsSettings = snapshot.dns
|
val dnsSettings = snapshot.dns
|
||||||
val proxySettings = snapshot.proxy
|
val proxySettings = snapshot.proxy
|
||||||
val monitoringSettings = snapshot.monitoring
|
val monitoringSettings = snapshot.monitoring
|
||||||
val lockdownSettings = snapshot.lockdown
|
|
||||||
|
|
||||||
val config = tunnelConfig.getConfig()
|
val config = tunnelConfig.getConfig()
|
||||||
val policy =
|
val policy =
|
||||||
@@ -191,13 +162,14 @@ class TunnelCoordinator(
|
|||||||
}
|
}
|
||||||
|
|
||||||
TunnelMode.LOCK_DOWN -> {
|
TunnelMode.LOCK_DOWN -> {
|
||||||
BackendMode.Proxy.KillSwitchPrimary(
|
|
||||||
runConfig,
|
BackendMode.Proxy.KillSwitchPrimary(runConfig)
|
||||||
lockdownSettings.toKillSwitchConfig(),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO for now, enforce single tunnel until multi-tunneling is implement
|
||||||
|
stopActiveTunnelsInternal()
|
||||||
|
|
||||||
tunnelProvider
|
tunnelProvider
|
||||||
.startTunnel(
|
.startTunnel(
|
||||||
tunnel =
|
tunnel =
|
||||||
@@ -221,10 +193,6 @@ class TunnelCoordinator(
|
|||||||
|
|
||||||
suspend fun toggleTunnels(source: TunnelActionSource = TunnelActionSource.USER) =
|
suspend fun toggleTunnels(source: TunnelActionSource = TunnelActionSource.USER) =
|
||||||
tunnelMutex.withLock {
|
tunnelMutex.withLock {
|
||||||
if (source == TunnelActionSource.USER) {
|
|
||||||
_userOverrideFlow.tryEmit(Unit)
|
|
||||||
}
|
|
||||||
|
|
||||||
val active = tunnelProvider.backendStatus.value.activeTunnels
|
val active = tunnelProvider.backendStatus.value.activeTunnels
|
||||||
if (active.isNotEmpty()) {
|
if (active.isNotEmpty()) {
|
||||||
lastActiveTunnels = active.keys.toList()
|
lastActiveTunnels = active.keys.toList()
|
||||||
@@ -233,7 +201,7 @@ class TunnelCoordinator(
|
|||||||
_actions.emit(TunnelActionEvent.Stopped(tunnelId = id, source = source))
|
_actions.emit(TunnelActionEvent.Stopped(tunnelId = id, source = source))
|
||||||
}
|
}
|
||||||
|
|
||||||
stopActiveTunnelsInternal(source)
|
stopActiveTunnelsInternal()
|
||||||
return@withLock
|
return@withLock
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -258,15 +226,7 @@ class TunnelCoordinator(
|
|||||||
.onFailure { _errors.emit(TunnelErrorEvent.from(it, id)) }
|
.onFailure { _errors.emit(TunnelErrorEvent.from(it, id)) }
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun stopActiveTunnelsInternal(
|
private suspend fun stopActiveTunnelsInternal() {
|
||||||
source: TunnelActionSource = TunnelActionSource.USER
|
|
||||||
) {
|
|
||||||
val active = tunnelProvider.backendStatus.value.activeTunnels
|
|
||||||
|
|
||||||
active.keys.forEach { id ->
|
|
||||||
_actions.emit(TunnelActionEvent.Stopped(tunnelId = id, source = source))
|
|
||||||
}
|
|
||||||
|
|
||||||
tunnelProvider.stopActiveTunnels()
|
tunnelProvider.stopActiveTunnels()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-2
@@ -1,9 +1,9 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.service
|
package com.zaneschepke.wireguardautotunnel.core.service
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.net.VpnService
|
import android.net.VpnService
|
||||||
import com.zaneschepke.wireguardautotunnel.service.autotunnel.AutoTunnelService
|
import com.zaneschepke.wireguardautotunnel.core.service.autotunnel.AutoTunnelService
|
||||||
|
|
||||||
class ServiceManager(private val context: Context) {
|
class ServiceManager(private val context: Context) {
|
||||||
|
|
||||||
+6
-25
@@ -1,4 +1,4 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.service.autotunnel
|
package com.zaneschepke.wireguardautotunnel.core.service.autotunnel
|
||||||
|
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.events.AutoTunnelEvent
|
import com.zaneschepke.wireguardautotunnel.domain.events.AutoTunnelEvent
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConfig
|
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConfig
|
||||||
@@ -17,7 +17,6 @@ class AutoTunnelEngine {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
Decision.None -> AutoTunnelEvent.DoNothing
|
Decision.None -> AutoTunnelEvent.DoNothing
|
||||||
is Decision.StopDueToNoInternet -> AutoTunnelEvent.StopAllDueToNoInternet
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -28,29 +27,13 @@ class AutoTunnelEngine {
|
|||||||
|
|
||||||
val activeTunnelIds = backend.activeTunnels.keys.toSet()
|
val activeTunnelIds = backend.activeTunnels.keys.toSet()
|
||||||
|
|
||||||
val isOnCaptivePortalWifi =
|
|
||||||
network.activeNetwork is ActiveNetwork.Wifi &&
|
|
||||||
network.activeNetwork.requiresCaptivePortalLogin
|
|
||||||
|
|
||||||
if (isOnCaptivePortalWifi && settings.disableTunnelOnCaptivePortal) {
|
|
||||||
return if (activeTunnelIds.isNotEmpty()) {
|
|
||||||
Decision.Sync(start = emptySet(), stop = activeTunnelIds)
|
|
||||||
} else {
|
|
||||||
Decision.None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!network.hasUsableNetwork) {
|
|
||||||
return if (settings.isStopOnNoInternetEnabled) {
|
|
||||||
Decision.StopDueToNoInternet
|
|
||||||
} else {
|
|
||||||
// keep tunnel state neutral on no internet otherwise
|
|
||||||
Decision.None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val desiredTunnels = resolveDesiredTunnels(state).map { it.id }.toSet()
|
val desiredTunnels = resolveDesiredTunnels(state).map { it.id }.toSet()
|
||||||
|
|
||||||
|
// stop condition overrides everything
|
||||||
|
if (!network.hasInternet() && settings.isStopOnNoInternetEnabled) {
|
||||||
|
return Decision.Sync(start = emptySet(), stop = activeTunnelIds)
|
||||||
|
}
|
||||||
|
|
||||||
val toStart = desiredTunnels - activeTunnelIds
|
val toStart = desiredTunnels - activeTunnelIds
|
||||||
val toStop = activeTunnelIds - desiredTunnels
|
val toStop = activeTunnelIds - desiredTunnels
|
||||||
|
|
||||||
@@ -113,7 +96,5 @@ class AutoTunnelEngine {
|
|||||||
data class Sync(val start: Set<TunnelConfig>, val stop: Set<Int>) : Decision
|
data class Sync(val start: Set<TunnelConfig>, val stop: Set<Int>) : Decision
|
||||||
|
|
||||||
data object None : Decision
|
data object None : Decision
|
||||||
|
|
||||||
data object StopDueToNoInternet : Decision
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+75
-89
@@ -1,4 +1,4 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.service.autotunnel
|
package com.zaneschepke.wireguardautotunnel.core.service.autotunnel
|
||||||
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import androidx.core.app.ServiceCompat
|
import androidx.core.app.ServiceCompat
|
||||||
@@ -7,12 +7,16 @@ import androidx.lifecycle.lifecycleScope
|
|||||||
import com.zaneschepke.networkmonitor.AndroidNetworkMonitor
|
import com.zaneschepke.networkmonitor.AndroidNetworkMonitor
|
||||||
import com.zaneschepke.networkmonitor.StableNetworkEngine
|
import com.zaneschepke.networkmonitor.StableNetworkEngine
|
||||||
import com.zaneschepke.wireguardautotunnel.R
|
import com.zaneschepke.wireguardautotunnel.R
|
||||||
|
import com.zaneschepke.wireguardautotunnel.core.notification.AndroidNotificationService
|
||||||
|
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationService
|
||||||
import com.zaneschepke.wireguardautotunnel.core.orchestration.TunnelCoordinator
|
import com.zaneschepke.wireguardautotunnel.core.orchestration.TunnelCoordinator
|
||||||
|
import com.zaneschepke.wireguardautotunnel.core.service.tile.AutoTunnelTileRefresher
|
||||||
import com.zaneschepke.wireguardautotunnel.di.Dispatcher
|
import com.zaneschepke.wireguardautotunnel.di.Dispatcher
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.enums.NotificationAction
|
import com.zaneschepke.wireguardautotunnel.domain.enums.NotificationAction
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelActionSource
|
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelActionSource
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelMode
|
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelMode
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.events.AutoTunnelEvent
|
import com.zaneschepke.wireguardautotunnel.domain.events.AutoTunnelEvent
|
||||||
|
import com.zaneschepke.wireguardautotunnel.domain.events.TunnelActionEvent
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.model.AutoTunnelSettings
|
import com.zaneschepke.wireguardautotunnel.domain.model.AutoTunnelSettings
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConfig
|
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConfig
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.repository.AutoTunnelSettingsRepository
|
import com.zaneschepke.wireguardautotunnel.domain.repository.AutoTunnelSettingsRepository
|
||||||
@@ -20,22 +24,15 @@ import com.zaneschepke.wireguardautotunnel.domain.repository.GeneralSettingRepos
|
|||||||
import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository
|
import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.state.AutoTunnelState
|
import com.zaneschepke.wireguardautotunnel.domain.state.AutoTunnelState
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.state.toDomain
|
import com.zaneschepke.wireguardautotunnel.domain.state.toDomain
|
||||||
import com.zaneschepke.wireguardautotunnel.notification.AndroidNotificationService
|
|
||||||
import com.zaneschepke.wireguardautotunnel.notification.NotificationService
|
|
||||||
import com.zaneschepke.wireguardautotunnel.service.tile.AutoTunnelTileRefresher
|
|
||||||
import com.zaneschepke.wireguardautotunnel.util.Constants
|
import com.zaneschepke.wireguardautotunnel.util.Constants
|
||||||
import com.zaneschepke.wireguardautotunnel.util.extensions.to
|
import com.zaneschepke.wireguardautotunnel.util.extensions.to
|
||||||
import kotlin.time.Duration.Companion.milliseconds
|
|
||||||
import kotlinx.coroutines.CoroutineDispatcher
|
import kotlinx.coroutines.CoroutineDispatcher
|
||||||
import kotlinx.coroutines.FlowPreview
|
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.delay
|
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.collectLatest
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
import kotlinx.coroutines.flow.combine
|
import kotlinx.coroutines.flow.combine
|
||||||
import kotlinx.coroutines.flow.debounce
|
|
||||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
import kotlinx.coroutines.flow.firstOrNull
|
import kotlinx.coroutines.flow.distinctUntilChangedBy
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import kotlinx.coroutines.flow.mapNotNull
|
import kotlinx.coroutines.flow.mapNotNull
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
@@ -66,7 +63,8 @@ class AutoTunnelService : LifecycleService() {
|
|||||||
private var autoTunnelJob: Job? = null
|
private var autoTunnelJob: Job? = null
|
||||||
private var permissionsJob: Job? = null
|
private var permissionsJob: Job? = null
|
||||||
private var overridesJob: Job? = null
|
private var overridesJob: Job? = null
|
||||||
private var noInternetStopJob: Job? = null
|
|
||||||
|
@Volatile private var manualOverrideState = ManualOverrideState()
|
||||||
|
|
||||||
private data class PermissionWarningState(
|
private data class PermissionWarningState(
|
||||||
val detectionMethod: AndroidNetworkMonitor.WifiDetectionMethod,
|
val detectionMethod: AndroidNetworkMonitor.WifiDetectionMethod,
|
||||||
@@ -75,19 +73,19 @@ class AutoTunnelService : LifecycleService() {
|
|||||||
val ssidReadRequired: Boolean,
|
val ssidReadRequired: Boolean,
|
||||||
)
|
)
|
||||||
|
|
||||||
@Volatile private var hasUserOverride = false
|
private data class ManualOverrideState(
|
||||||
private var lastNetworkFingerprint: AutoTunnelState.NetworkFingerprint? = null
|
val fingerprint: AutoTunnelState.NetworkFingerprint? = null,
|
||||||
|
val stoppedTunnelIds: Set<Int> = emptySet(),
|
||||||
|
val startedTunnelIds: Set<Int> = emptySet(),
|
||||||
|
)
|
||||||
|
|
||||||
@OptIn(FlowPreview::class)
|
|
||||||
private val autoTunnelStateFlow: Flow<AutoTunnelState> by lazy {
|
private val autoTunnelStateFlow: Flow<AutoTunnelState> by lazy {
|
||||||
val networkFlow = networkEngine.stableState.mapNotNull { it?.state?.toDomain() }
|
val networkFlow = networkEngine.stableState.mapNotNull { it?.state?.toDomain() }
|
||||||
|
|
||||||
val settingsFlow = combineSettings()
|
val settingsFlow = combineSettings()
|
||||||
|
|
||||||
val backendFlow =
|
val backendFlow =
|
||||||
tunnelCoordinator.backendStatus
|
tunnelCoordinator.backendStatus.distinctUntilChangedBy { it.activeTunnels.keys.toSet() }
|
||||||
.distinctUntilChanged { old, new -> old.activeTunnels == new.activeTunnels }
|
|
||||||
.debounce(300L.milliseconds)
|
|
||||||
|
|
||||||
combine(networkFlow, settingsFlow, backendFlow) { network, settings, backend ->
|
combine(networkFlow, settingsFlow, backendFlow) { network, settings, backend ->
|
||||||
AutoTunnelState(
|
AutoTunnelState(
|
||||||
@@ -123,7 +121,7 @@ class AutoTunnelService : LifecycleService() {
|
|||||||
permissionsJob?.cancel()
|
permissionsJob?.cancel()
|
||||||
permissionsJob = startLocationPermissionsNotificationJob()
|
permissionsJob = startLocationPermissionsNotificationJob()
|
||||||
overridesJob?.cancel()
|
overridesJob?.cancel()
|
||||||
overridesJob = startUserOverrideJob()
|
overridesJob = startOverridesJob()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun stop() {
|
fun stop() {
|
||||||
@@ -132,23 +130,48 @@ class AutoTunnelService : LifecycleService() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
cancelNoInternetStopJob()
|
|
||||||
ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_REMOVE)
|
ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_REMOVE)
|
||||||
stateHolder.setActive(false)
|
stateHolder.setActive(false)
|
||||||
AutoTunnelTileRefresher.refresh(this)
|
AutoTunnelTileRefresher.refresh(this)
|
||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun startUserOverrideJob(): Job =
|
private fun startOverridesJob(): Job =
|
||||||
lifecycleScope.launch(ioDispatcher) {
|
lifecycleScope.launch(ioDispatcher) {
|
||||||
tunnelCoordinator.userOverrideFlow.collect {
|
tunnelCoordinator.actions.collect { action ->
|
||||||
reconciliationMutex.withLock {
|
reconciliationMutex.withLock {
|
||||||
if (!hasUserOverride) {
|
manualOverrideState =
|
||||||
Timber.d(
|
when (action) {
|
||||||
"User manually overrode Auto Tunnel on current network. Pausing auto decisions."
|
is TunnelActionEvent.Started -> {
|
||||||
)
|
|
||||||
}
|
if (action.source != TunnelActionSource.USER) {
|
||||||
hasUserOverride = true
|
return@withLock
|
||||||
|
}
|
||||||
|
|
||||||
|
manualOverrideState.copy(
|
||||||
|
startedTunnelIds =
|
||||||
|
manualOverrideState.startedTunnelIds + action.tunnelId,
|
||||||
|
stoppedTunnelIds =
|
||||||
|
manualOverrideState.stoppedTunnelIds - action.tunnelId,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
is TunnelActionEvent.Stopped -> {
|
||||||
|
|
||||||
|
if (action.source != TunnelActionSource.USER) {
|
||||||
|
return@withLock
|
||||||
|
}
|
||||||
|
|
||||||
|
manualOverrideState.copy(
|
||||||
|
stoppedTunnelIds =
|
||||||
|
manualOverrideState.stoppedTunnelIds + action.tunnelId,
|
||||||
|
startedTunnelIds =
|
||||||
|
manualOverrideState.startedTunnelIds - action.tunnelId,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Timber.d("Updated manual overrides: $manualOverrideState")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -179,83 +202,50 @@ class AutoTunnelService : LifecycleService() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Instead of stopping tunnel right away on no internet, we kick off this job to add short delay
|
|
||||||
// and re-evaluation to prevent unwanted stops
|
|
||||||
// on flaky networks and network transitions
|
|
||||||
private fun scheduleNoInternetStop() {
|
|
||||||
noInternetStopJob?.cancel()
|
|
||||||
|
|
||||||
noInternetStopJob =
|
|
||||||
lifecycleScope.launch(ioDispatcher) {
|
|
||||||
delay(NO_INTERNET_GRACE_PERIOD_MS.milliseconds)
|
|
||||||
|
|
||||||
reconciliationMutex.withLock {
|
|
||||||
val currentNetworkState = networkEngine.stableState.value?.state?.toDomain()
|
|
||||||
|
|
||||||
val stillNoUsableNetwork = currentNetworkState?.hasUsableNetwork == false
|
|
||||||
val stopOnNoInternetEnabled =
|
|
||||||
autoTunnelRepository.flow.firstOrNull()?.isStopOnNoInternetEnabled == true
|
|
||||||
|
|
||||||
if (stillNoUsableNetwork && stopOnNoInternetEnabled) {
|
|
||||||
val currentActiveIds =
|
|
||||||
tunnelCoordinator.backendStatus.value.activeTunnels.keys
|
|
||||||
|
|
||||||
if (currentActiveIds.isNotEmpty()) {
|
|
||||||
Timber.w(
|
|
||||||
"No internet grace period expired and still no internet. Stopping tunnels: $currentActiveIds"
|
|
||||||
)
|
|
||||||
currentActiveIds.forEach { tunnelId ->
|
|
||||||
tunnelCoordinator.stopTunnel(
|
|
||||||
tunnelId,
|
|
||||||
TunnelActionSource.AUTO_TUNNEL,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Timber.d(
|
|
||||||
"No internet grace period expired, but internet is back or setting disabled. Doing nothing."
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun cancelNoInternetStopJob() {
|
|
||||||
noInternetStopJob?.cancel()
|
|
||||||
noInternetStopJob = null
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun startAutoTunnelStateJob(): Job =
|
private fun startAutoTunnelStateJob(): Job =
|
||||||
lifecycleScope.launch(ioDispatcher) {
|
lifecycleScope.launch(ioDispatcher) {
|
||||||
autoTunnelStateFlow.collectLatest { state ->
|
autoTunnelStateFlow.collectLatest { state ->
|
||||||
reconciliationMutex.withLock {
|
reconciliationMutex.withLock {
|
||||||
updateFingerprintIfNeeded(state)
|
updateFingerprintIfNeeded(state)
|
||||||
|
|
||||||
val rawEvent = engine.evaluate(state)
|
val rawEvent = engine.evaluate(state)
|
||||||
|
|
||||||
val event = applyOverrides(rawEvent)
|
val event = applyOverrides(rawEvent)
|
||||||
|
|
||||||
Timber.d("AutoTunnel reconciliation event: $event")
|
Timber.d("AutoTunnel reconciliation event: $event")
|
||||||
|
|
||||||
handleAutoTunnelEvent(event)
|
handleAutoTunnelEvent(event)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateFingerprintIfNeeded(state: AutoTunnelState) {
|
private fun updateFingerprintIfNeeded(state: AutoTunnelState) {
|
||||||
val currentFingerprint = state.networkFingerPrint
|
val fingerprint = state.networkFingerPrint
|
||||||
|
|
||||||
if (lastNetworkFingerprint != currentFingerprint) {
|
if (manualOverrideState.fingerprint != fingerprint) {
|
||||||
if (hasUserOverride) {
|
Timber.d("Network changed, clearing overrides")
|
||||||
Timber.d("Network fingerprint changed, clearing user override")
|
|
||||||
}
|
manualOverrideState = ManualOverrideState(fingerprint = fingerprint)
|
||||||
hasUserOverride = false
|
|
||||||
lastNetworkFingerprint = currentFingerprint
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun applyOverrides(event: AutoTunnelEvent): AutoTunnelEvent {
|
private fun applyOverrides(event: AutoTunnelEvent): AutoTunnelEvent {
|
||||||
return if (hasUserOverride) {
|
|
||||||
AutoTunnelEvent.DoNothing
|
if (event !is AutoTunnelEvent.Sync) {
|
||||||
} else {
|
return event
|
||||||
event
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val filteredStart =
|
||||||
|
event.start.filterNot { it.id in manualOverrideState.stoppedTunnelIds }.toSet()
|
||||||
|
|
||||||
|
val filteredStop =
|
||||||
|
event.stop.filterNot { it in manualOverrideState.startedTunnelIds }.toSet()
|
||||||
|
|
||||||
|
if (filteredStart.isEmpty() && filteredStop.isEmpty()) {
|
||||||
|
return AutoTunnelEvent.DoNothing
|
||||||
|
}
|
||||||
|
|
||||||
|
return event.copy(start = filteredStart, stop = filteredStop)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun combineSettings():
|
private fun combineSettings():
|
||||||
@@ -344,7 +334,7 @@ class AutoTunnelService : LifecycleService() {
|
|||||||
private suspend fun handleAutoTunnelEvent(event: AutoTunnelEvent) {
|
private suspend fun handleAutoTunnelEvent(event: AutoTunnelEvent) {
|
||||||
when (event) {
|
when (event) {
|
||||||
is AutoTunnelEvent.Sync -> {
|
is AutoTunnelEvent.Sync -> {
|
||||||
cancelNoInternetStopJob()
|
|
||||||
event.stop.forEach { tunnelId ->
|
event.stop.forEach { tunnelId ->
|
||||||
Timber.d("Stopping tunnel: $tunnelId")
|
Timber.d("Stopping tunnel: $tunnelId")
|
||||||
tunnelCoordinator.stopTunnel(tunnelId, TunnelActionSource.AUTO_TUNNEL)
|
tunnelCoordinator.stopTunnel(tunnelId, TunnelActionSource.AUTO_TUNNEL)
|
||||||
@@ -355,12 +345,8 @@ class AutoTunnelService : LifecycleService() {
|
|||||||
tunnelCoordinator.startTunnel(config, TunnelActionSource.AUTO_TUNNEL)
|
tunnelCoordinator.startTunnel(config, TunnelActionSource.AUTO_TUNNEL)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
AutoTunnelEvent.StopAllDueToNoInternet -> scheduleNoInternetStop()
|
|
||||||
AutoTunnelEvent.DoNothing -> Unit
|
AutoTunnelEvent.DoNothing -> Unit
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
|
||||||
private const val NO_INTERNET_GRACE_PERIOD_MS = 10_000L
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.service.autotunnel
|
package com.zaneschepke.wireguardautotunnel.core.service.autotunnel
|
||||||
|
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
+2
-2
@@ -1,9 +1,9 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.service.tile
|
package com.zaneschepke.wireguardautotunnel.core.service.tile
|
||||||
|
|
||||||
import android.service.quicksettings.Tile
|
import android.service.quicksettings.Tile
|
||||||
import android.service.quicksettings.TileService
|
import android.service.quicksettings.TileService
|
||||||
import com.zaneschepke.wireguardautotunnel.core.orchestration.AutoTunnelCoordinator
|
import com.zaneschepke.wireguardautotunnel.core.orchestration.AutoTunnelCoordinator
|
||||||
import com.zaneschepke.wireguardautotunnel.service.autotunnel.AutoTunnelStateHolder
|
import com.zaneschepke.wireguardautotunnel.core.service.autotunnel.AutoTunnelStateHolder
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.service.tile
|
package com.zaneschepke.wireguardautotunnel.core.service.tile
|
||||||
|
|
||||||
import android.content.ComponentName
|
import android.content.ComponentName
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.service.tile
|
package com.zaneschepke.wireguardautotunnel.core.service.tile
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.service.tile
|
package com.zaneschepke.wireguardautotunnel.core.service.tile
|
||||||
|
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.service.quicksettings.Tile
|
import android.service.quicksettings.Tile
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.service.tile
|
package com.zaneschepke.wireguardautotunnel.core.service.tile
|
||||||
|
|
||||||
import android.content.ComponentName
|
import android.content.ComponentName
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
+4
-3
@@ -19,8 +19,9 @@ class ShortcutsActivity : ComponentActivity() {
|
|||||||
|
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
finish()
|
applicationScope.launch {
|
||||||
|
shortcutCoordinator.handle(intent)
|
||||||
applicationScope.launch { shortcutCoordinator.handle(intent) }
|
finish()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
-116
@@ -1,116 +0,0 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.core.tunnel
|
|
||||||
|
|
||||||
import android.app.Notification
|
|
||||||
import android.app.PendingIntent
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
|
||||||
import com.zaneschepke.tunnel.ApplicationProvider
|
|
||||||
import com.zaneschepke.tunnel.model.BackendMode
|
|
||||||
import com.zaneschepke.tunnel.state.BackendStatus
|
|
||||||
import com.zaneschepke.wireguardautotunnel.MainActivity
|
|
||||||
import com.zaneschepke.wireguardautotunnel.R
|
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository
|
|
||||||
import com.zaneschepke.wireguardautotunnel.notification.AndroidNotificationService
|
|
||||||
import com.zaneschepke.wireguardautotunnel.notification.NotificationService
|
|
||||||
import com.zaneschepke.wireguardautotunnel.notification.NotificationService.Companion.PROXY_GROUP_KEY
|
|
||||||
import com.zaneschepke.wireguardautotunnel.notification.NotificationService.Companion.VPN_GROUP_KEY
|
|
||||||
import com.zaneschepke.wireguardautotunnel.notification.TunnelNotificationLine
|
|
||||||
import com.zaneschepke.wireguardautotunnel.notification.TunnelNotificationService
|
|
||||||
import com.zaneschepke.wireguardautotunnel.service.tile.TunnelTileRefresher
|
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.state.DisplayTunnelState
|
|
||||||
import kotlinx.coroutines.flow.first
|
|
||||||
|
|
||||||
class AndroidApplicationProvider(
|
|
||||||
private val notificationService: NotificationService,
|
|
||||||
private val tunnelNotificationService: TunnelNotificationService,
|
|
||||||
private val tunnelRepository: TunnelRepository,
|
|
||||||
) : ApplicationProvider {
|
|
||||||
|
|
||||||
private val context: Context = notificationService.context
|
|
||||||
|
|
||||||
override fun refreshTile(context: Context) {
|
|
||||||
TunnelTileRefresher.refresh(context)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun createVpnConfigurePendingIntent(context: Context): PendingIntent {
|
|
||||||
return PendingIntent.getActivity(
|
|
||||||
context,
|
|
||||||
0,
|
|
||||||
Intent(context, MainActivity::class.java).apply {
|
|
||||||
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP)
|
|
||||||
},
|
|
||||||
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
override val vpnInitNotification: Notification
|
|
||||||
get() =
|
|
||||||
notificationService.createNotification(
|
|
||||||
channel = AndroidNotificationService.NotificationChannels.Tunnel.VPN,
|
|
||||||
title = context.getString(R.string.initializing),
|
|
||||||
onGoing = true,
|
|
||||||
groupKey = VPN_GROUP_KEY,
|
|
||||||
)
|
|
||||||
|
|
||||||
override val proxyInitNotification: Notification
|
|
||||||
get() =
|
|
||||||
notificationService.createNotification(
|
|
||||||
channel = AndroidNotificationService.NotificationChannels.Tunnel.Proxy,
|
|
||||||
title = context.getString(R.string.initializing),
|
|
||||||
onGoing = true,
|
|
||||||
groupKey = PROXY_GROUP_KEY,
|
|
||||||
)
|
|
||||||
|
|
||||||
override val vpnNotificationId: Int
|
|
||||||
get() = NotificationService.VPN_NOTIFICATION_ID
|
|
||||||
|
|
||||||
override val proxyNotificationId: Int
|
|
||||||
get() = NotificationService.PROXY_NOTIFICATION_ID
|
|
||||||
|
|
||||||
override suspend fun buildVpnPersistentNotification(
|
|
||||||
currentStatus: BackendStatus
|
|
||||||
): Notification {
|
|
||||||
val lines = computeVpnNotificationLines(currentStatus)
|
|
||||||
return tunnelNotificationService.buildVpnPersistentNotification(lines)
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun buildProxyPersistentNotification(
|
|
||||||
currentStatus: BackendStatus
|
|
||||||
): Notification {
|
|
||||||
val lines = computeProxyNotificationLines(currentStatus)
|
|
||||||
return tunnelNotificationService.buildProxyPersistentNotification(lines)
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun computeVpnNotificationLines(
|
|
||||||
status: BackendStatus
|
|
||||||
): Map<Int, TunnelNotificationLine> {
|
|
||||||
val activeTunnels = status.activeTunnels
|
|
||||||
val allTunnels = tunnelRepository.userTunnelsFlow.first()
|
|
||||||
return activeTunnels
|
|
||||||
.mapNotNull { (id, activeTunnel) ->
|
|
||||||
val mode = activeTunnel.mode ?: return@mapNotNull null
|
|
||||||
if (mode !is BackendMode.Vpn && mode !is BackendMode.Proxy.KillSwitchPrimary)
|
|
||||||
return@mapNotNull null
|
|
||||||
val tunnel = allTunnels.find { it.id == id } ?: return@mapNotNull null
|
|
||||||
val displayState = DisplayTunnelState.from(activeTunnel)
|
|
||||||
TunnelNotificationLine(id, tunnel.name, displayState)
|
|
||||||
}
|
|
||||||
.associateBy { it.id }
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun computeProxyNotificationLines(
|
|
||||||
status: BackendStatus
|
|
||||||
): Map<Int, TunnelNotificationLine> {
|
|
||||||
val activeTunnels = status.activeTunnels
|
|
||||||
val allTunnels = tunnelRepository.userTunnelsFlow.first()
|
|
||||||
return activeTunnels
|
|
||||||
.mapNotNull { (id, activeTunnel) ->
|
|
||||||
val mode = activeTunnel.mode ?: return@mapNotNull null
|
|
||||||
if (mode !is BackendMode.Proxy.Standard) return@mapNotNull null
|
|
||||||
val tunnel = allTunnels.find { it.id == id } ?: return@mapNotNull null
|
|
||||||
val displayState = DisplayTunnelState.from(activeTunnel)
|
|
||||||
TunnelNotificationLine(id, tunnel.name, displayState)
|
|
||||||
}
|
|
||||||
.associateBy { it.id }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+9
@@ -4,11 +4,14 @@ import com.zaneschepke.tunnel.Tunnel
|
|||||||
import com.zaneschepke.tunnel.backend.Backend
|
import com.zaneschepke.tunnel.backend.Backend
|
||||||
import com.zaneschepke.tunnel.model.BackendMode
|
import com.zaneschepke.tunnel.model.BackendMode
|
||||||
import com.zaneschepke.tunnel.state.BackendStatus
|
import com.zaneschepke.tunnel.state.BackendStatus
|
||||||
|
import com.zaneschepke.wireguardautotunnel.domain.events.BackendCoreException
|
||||||
|
import com.zaneschepke.wireguardautotunnel.domain.events.BackendMessage
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.model.LockdownSettings
|
import com.zaneschepke.wireguardautotunnel.domain.model.LockdownSettings
|
||||||
import kotlin.concurrent.atomics.ExperimentalAtomicApi
|
import kotlin.concurrent.atomics.ExperimentalAtomicApi
|
||||||
import kotlinx.coroutines.CoroutineDispatcher
|
import kotlinx.coroutines.CoroutineDispatcher
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
|
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||||
import kotlinx.coroutines.flow.SharingStarted
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.stateIn
|
import kotlinx.coroutines.flow.stateIn
|
||||||
@@ -49,4 +52,10 @@ class TunnelBackendProvider(
|
|||||||
override suspend fun disableLockDown(): Result<Unit> {
|
override suspend fun disableLockDown(): Result<Unit> {
|
||||||
return backend.disableKillSwitch()
|
return backend.disableKillSwitch()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
|
private val localErrorEvents = MutableSharedFlow<Pair<String?, BackendCoreException>>()
|
||||||
|
|
||||||
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
|
private val localMessageEvents = MutableSharedFlow<Pair<String?, BackendMessage>>()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,9 +6,9 @@ import androidx.work.ExistingPeriodicWorkPolicy
|
|||||||
import androidx.work.PeriodicWorkRequestBuilder
|
import androidx.work.PeriodicWorkRequestBuilder
|
||||||
import androidx.work.WorkManager
|
import androidx.work.WorkManager
|
||||||
import androidx.work.WorkerParameters
|
import androidx.work.WorkerParameters
|
||||||
|
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
|
||||||
|
import com.zaneschepke.wireguardautotunnel.core.service.autotunnel.AutoTunnelStateHolder
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.repository.AutoTunnelSettingsRepository
|
import com.zaneschepke.wireguardautotunnel.domain.repository.AutoTunnelSettingsRepository
|
||||||
import com.zaneschepke.wireguardautotunnel.service.ServiceManager
|
|
||||||
import com.zaneschepke.wireguardautotunnel.service.autotunnel.AutoTunnelStateHolder
|
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ import com.zaneschepke.wireguardautotunnel.data.entity.TunnelConfig
|
|||||||
DnsSettings::class,
|
DnsSettings::class,
|
||||||
LockdownSettings::class,
|
LockdownSettings::class,
|
||||||
],
|
],
|
||||||
version = 31,
|
version = 30,
|
||||||
autoMigrations =
|
autoMigrations =
|
||||||
[
|
[
|
||||||
AutoMigration(from = 1, to = 2),
|
AutoMigration(from = 1, to = 2),
|
||||||
@@ -63,7 +63,6 @@ import com.zaneschepke.wireguardautotunnel.data.entity.TunnelConfig
|
|||||||
AutoMigration(from = 26, to = 27, spec = GlobalsMigration::class),
|
AutoMigration(from = 26, to = 27, spec = GlobalsMigration::class),
|
||||||
AutoMigration(from = 27, to = 28, spec = DonationMigration::class),
|
AutoMigration(from = 27, to = 28, spec = DonationMigration::class),
|
||||||
AutoMigration(from = 29, to = 30, spec = SingleConfigMigration::class),
|
AutoMigration(from = 29, to = 30, spec = SingleConfigMigration::class),
|
||||||
AutoMigration(from = 30, to = 31),
|
|
||||||
],
|
],
|
||||||
exportSchema = true,
|
exportSchema = true,
|
||||||
)
|
)
|
||||||
|
|||||||
-3
@@ -18,7 +18,4 @@ interface AutoTunnelSettingsDao {
|
|||||||
|
|
||||||
@Query("UPDATE auto_tunnel_settings SET is_tunnel_enabled = :enabled")
|
@Query("UPDATE auto_tunnel_settings SET is_tunnel_enabled = :enabled")
|
||||||
suspend fun updateAutoTunnelEnabled(enabled: Boolean)
|
suspend fun updateAutoTunnelEnabled(enabled: Boolean)
|
||||||
|
|
||||||
@Query("UPDATE auto_tunnel_settings SET disable_on_captive_portal = :enabled")
|
|
||||||
suspend fun updateDisableOnCaptivePortal(enabled: Boolean)
|
|
||||||
}
|
}
|
||||||
|
|||||||
-2
@@ -27,6 +27,4 @@ data class AutoTunnelSettings(
|
|||||||
@ColumnInfo(name = "wifi_detection_method", defaultValue = "0")
|
@ColumnInfo(name = "wifi_detection_method", defaultValue = "0")
|
||||||
val wifiDetectionMethod: WifiDetectionMethod = WifiDetectionMethod.fromValue(0),
|
val wifiDetectionMethod: WifiDetectionMethod = WifiDetectionMethod.fromValue(0),
|
||||||
@ColumnInfo(name = "start_on_boot", defaultValue = "0") val startOnBoot: Boolean = false,
|
@ColumnInfo(name = "start_on_boot", defaultValue = "0") val startOnBoot: Boolean = false,
|
||||||
@ColumnInfo(name = "disable_on_captive_portal", defaultValue = "1")
|
|
||||||
val disableTunnelOnCaptivePortal: Boolean = true,
|
|
||||||
)
|
)
|
||||||
|
|||||||
-4
@@ -26,8 +26,4 @@ class RoomAutoTunnelSettingsRepository(private val autoTunnelSettingsDao: AutoTu
|
|||||||
override suspend fun updateAutoTunnelEnabled(enabled: Boolean) {
|
override suspend fun updateAutoTunnelEnabled(enabled: Boolean) {
|
||||||
autoTunnelSettingsDao.updateAutoTunnelEnabled(enabled)
|
autoTunnelSettingsDao.updateAutoTunnelEnabled(enabled)
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun updateDisableOnCaptivePortal(enabled: Boolean) {
|
|
||||||
autoTunnelSettingsDao.updateDisableOnCaptivePortal(enabled)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,14 +6,14 @@ import android.os.StrictMode
|
|||||||
import com.zaneschepke.logcatter.LogReader
|
import com.zaneschepke.logcatter.LogReader
|
||||||
import com.zaneschepke.logcatter.LogcatReader
|
import com.zaneschepke.logcatter.LogcatReader
|
||||||
import com.zaneschepke.wireguardautotunnel.BuildConfig
|
import com.zaneschepke.wireguardautotunnel.BuildConfig
|
||||||
|
import com.zaneschepke.wireguardautotunnel.core.notification.AndroidNotificationService
|
||||||
|
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationService
|
||||||
|
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
|
||||||
|
import com.zaneschepke.wireguardautotunnel.core.service.autotunnel.AutoTunnelStateHolder
|
||||||
import com.zaneschepke.wireguardautotunnel.core.shortcut.DynamicShortcutManager
|
import com.zaneschepke.wireguardautotunnel.core.shortcut.DynamicShortcutManager
|
||||||
import com.zaneschepke.wireguardautotunnel.core.shortcut.ShortcutManager
|
import com.zaneschepke.wireguardautotunnel.core.shortcut.ShortcutManager
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.repository.GlobalEffectRepository
|
import com.zaneschepke.wireguardautotunnel.domain.repository.GlobalEffectRepository
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.repository.SelectedTunnelsRepository
|
import com.zaneschepke.wireguardautotunnel.domain.repository.SelectedTunnelsRepository
|
||||||
import com.zaneschepke.wireguardautotunnel.notification.AndroidNotificationService
|
|
||||||
import com.zaneschepke.wireguardautotunnel.notification.NotificationService
|
|
||||||
import com.zaneschepke.wireguardautotunnel.service.ServiceManager
|
|
||||||
import com.zaneschepke.wireguardautotunnel.service.autotunnel.AutoTunnelStateHolder
|
|
||||||
import com.zaneschepke.wireguardautotunnel.util.FileUtils
|
import com.zaneschepke.wireguardautotunnel.util.FileUtils
|
||||||
import com.zaneschepke.wireguardautotunnel.util.network.NetworkUtils
|
import com.zaneschepke.wireguardautotunnel.util.network.NetworkUtils
|
||||||
import com.zaneschepke.wireguardautotunnel.viewmodel.AutoTunnelViewModel
|
import com.zaneschepke.wireguardautotunnel.viewmodel.AutoTunnelViewModel
|
||||||
|
|||||||
@@ -27,7 +27,6 @@ val coordinatorModule = module {
|
|||||||
get(),
|
get(),
|
||||||
get(),
|
get(),
|
||||||
get(),
|
get(),
|
||||||
get(),
|
|
||||||
get(named(Scope.APPLICATION)),
|
get(named(Scope.APPLICATION)),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,19 +1,25 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.di
|
package com.zaneschepke.wireguardautotunnel.di
|
||||||
|
|
||||||
|
import android.app.Notification
|
||||||
|
import android.content.Context
|
||||||
import com.zaneschepke.networkmonitor.AndroidNetworkMonitor
|
import com.zaneschepke.networkmonitor.AndroidNetworkMonitor
|
||||||
import com.zaneschepke.networkmonitor.NetworkMonitor
|
import com.zaneschepke.networkmonitor.NetworkMonitor
|
||||||
import com.zaneschepke.networkmonitor.StableNetworkEngine
|
import com.zaneschepke.networkmonitor.StableNetworkEngine
|
||||||
import com.zaneschepke.tunnel.ApplicationProvider
|
import com.zaneschepke.tunnel.NotificationProvider
|
||||||
import com.zaneschepke.tunnel.util.RootShell
|
import com.zaneschepke.tunnel.backend.RootShell
|
||||||
import com.zaneschepke.tunnel.util.RootShellException
|
import com.zaneschepke.tunnel.util.RootShellException
|
||||||
|
import com.zaneschepke.wireguardautotunnel.R
|
||||||
import com.zaneschepke.wireguardautotunnel.core.event.TunnelEventDispatcher
|
import com.zaneschepke.wireguardautotunnel.core.event.TunnelEventDispatcher
|
||||||
import com.zaneschepke.wireguardautotunnel.core.tunnel.AndroidApplicationProvider
|
import com.zaneschepke.wireguardautotunnel.core.notification.AndroidNotificationService.NotificationChannels
|
||||||
|
import com.zaneschepke.wireguardautotunnel.core.notification.AndroidTunnelNotificationService
|
||||||
|
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationService
|
||||||
|
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationService.Companion.PROXY_GROUP_KEY
|
||||||
|
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationService.Companion.VPN_GROUP_KEY
|
||||||
|
import com.zaneschepke.wireguardautotunnel.core.notification.TunnelNotificationService
|
||||||
|
import com.zaneschepke.wireguardautotunnel.core.service.tile.TunnelTileRefresher
|
||||||
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelBackendProvider
|
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelBackendProvider
|
||||||
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelProvider
|
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelProvider
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.repository.AutoTunnelSettingsRepository
|
import com.zaneschepke.wireguardautotunnel.domain.repository.AutoTunnelSettingsRepository
|
||||||
import com.zaneschepke.wireguardautotunnel.lifecyle.AppVisibilityObserver
|
|
||||||
import com.zaneschepke.wireguardautotunnel.notification.AndroidTunnelNotificationService
|
|
||||||
import com.zaneschepke.wireguardautotunnel.notification.TunnelNotificationService
|
|
||||||
import com.zaneschepke.wireguardautotunnel.util.extensions.to
|
import com.zaneschepke.wireguardautotunnel.util.extensions.to
|
||||||
import kotlin.time.Duration.Companion.milliseconds
|
import kotlin.time.Duration.Companion.milliseconds
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
@@ -30,15 +36,40 @@ import timber.log.Timber
|
|||||||
|
|
||||||
val tunnelBackendProviderModule = module {
|
val tunnelBackendProviderModule = module {
|
||||||
single<TunnelNotificationService> { AndroidTunnelNotificationService(get()) }
|
single<TunnelNotificationService> { AndroidTunnelNotificationService(get()) }
|
||||||
single { AppVisibilityObserver() }
|
|
||||||
singleOf(::TunnelEventDispatcher)
|
singleOf(::TunnelEventDispatcher)
|
||||||
|
|
||||||
single<ApplicationProvider> {
|
single<NotificationProvider> {
|
||||||
AndroidApplicationProvider(
|
val notificationService = get<NotificationService>()
|
||||||
notificationService = get(),
|
val context = androidContext()
|
||||||
tunnelNotificationService = get(),
|
object : NotificationProvider {
|
||||||
tunnelRepository = get(),
|
override val vpnInitNotification: Notification
|
||||||
)
|
get() =
|
||||||
|
notificationService.createNotification(
|
||||||
|
channel = NotificationChannels.Tunnel.VPN,
|
||||||
|
title = context.getString(R.string.initializing),
|
||||||
|
onGoing = true,
|
||||||
|
groupKey = VPN_GROUP_KEY,
|
||||||
|
)
|
||||||
|
|
||||||
|
override val proxyInitNotification: Notification
|
||||||
|
get() =
|
||||||
|
notificationService.createNotification(
|
||||||
|
channel = NotificationChannels.Tunnel.Proxy,
|
||||||
|
title = context.getString(R.string.initializing),
|
||||||
|
onGoing = true,
|
||||||
|
groupKey = PROXY_GROUP_KEY,
|
||||||
|
)
|
||||||
|
|
||||||
|
override val vpnNotificationId: Int
|
||||||
|
get() = NotificationService.VPN_NOTIFICATION_ID
|
||||||
|
|
||||||
|
override val proxyNotificationId: Int
|
||||||
|
get() = NotificationService.PROXY_NOTIFICATION_ID
|
||||||
|
|
||||||
|
override fun refreshTile(context: Context) {
|
||||||
|
TunnelTileRefresher.refresh(context)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
single {
|
single {
|
||||||
|
|||||||
@@ -7,6 +7,4 @@ sealed interface AutoTunnelEvent {
|
|||||||
data class Sync(val start: Set<TunnelConfig>, val stop: Set<Int>) : AutoTunnelEvent
|
data class Sync(val start: Set<TunnelConfig>, val stop: Set<Int>) : AutoTunnelEvent
|
||||||
|
|
||||||
data object DoNothing : AutoTunnelEvent
|
data object DoNothing : AutoTunnelEvent
|
||||||
|
|
||||||
data object StopAllDueToNoInternet : AutoTunnelEvent
|
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-7
@@ -4,12 +4,7 @@ import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelActionSource
|
|||||||
|
|
||||||
sealed interface TunnelActionEvent {
|
sealed interface TunnelActionEvent {
|
||||||
|
|
||||||
val source: TunnelActionSource
|
data class Started(val tunnelId: Int, val source: TunnelActionSource) : TunnelActionEvent
|
||||||
val tunnelId: Int
|
|
||||||
|
|
||||||
data class Started(override val tunnelId: Int, override val source: TunnelActionSource) :
|
data class Stopped(val tunnelId: Int, val source: TunnelActionSource) : TunnelActionEvent
|
||||||
TunnelActionEvent
|
|
||||||
|
|
||||||
data class Stopped(override val tunnelId: Int, override val source: TunnelActionSource) :
|
|
||||||
TunnelActionEvent
|
|
||||||
}
|
}
|
||||||
|
|||||||
-1
@@ -14,5 +14,4 @@ data class AutoTunnelSettings(
|
|||||||
val isTunnelOnUnsecureEnabled: Boolean = false,
|
val isTunnelOnUnsecureEnabled: Boolean = false,
|
||||||
val wifiDetectionMethod: WifiDetectionMethod = WifiDetectionMethod.fromValue(0),
|
val wifiDetectionMethod: WifiDetectionMethod = WifiDetectionMethod.fromValue(0),
|
||||||
val startOnBoot: Boolean = false,
|
val startOnBoot: Boolean = false,
|
||||||
val disableTunnelOnCaptivePortal: Boolean = true,
|
|
||||||
)
|
)
|
||||||
|
|||||||
-2
@@ -11,6 +11,4 @@ interface AutoTunnelSettingsRepository {
|
|||||||
suspend fun getAutoTunnelSettings(): AutoTunnelSettings
|
suspend fun getAutoTunnelSettings(): AutoTunnelSettings
|
||||||
|
|
||||||
suspend fun updateAutoTunnelEnabled(enabled: Boolean)
|
suspend fun updateAutoTunnelEnabled(enabled: Boolean)
|
||||||
|
|
||||||
suspend fun updateDisableOnCaptivePortal(enabled: Boolean)
|
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -7,7 +7,7 @@ import kotlinx.coroutines.flow.asSharedFlow
|
|||||||
class GlobalEffectRepository {
|
class GlobalEffectRepository {
|
||||||
|
|
||||||
private val _globalEffectFlow =
|
private val _globalEffectFlow =
|
||||||
MutableSharedFlow<GlobalSideEffect>(replay = 0, extraBufferCapacity = 0)
|
MutableSharedFlow<GlobalSideEffect>(replay = 0, extraBufferCapacity = 1)
|
||||||
val flow = _globalEffectFlow.asSharedFlow()
|
val flow = _globalEffectFlow.asSharedFlow()
|
||||||
|
|
||||||
suspend fun post(effect: GlobalSideEffect) {
|
suspend fun post(effect: GlobalSideEffect) {
|
||||||
|
|||||||
+4
-2
@@ -1,8 +1,8 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.domain.sideeffect
|
package com.zaneschepke.wireguardautotunnel.domain.sideeffect
|
||||||
|
|
||||||
import com.dokar.sonner.ToastType
|
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelMode
|
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelMode
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConfig
|
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConfig
|
||||||
|
import com.zaneschepke.wireguardautotunnel.ui.common.snackbar.SnackbarType
|
||||||
import com.zaneschepke.wireguardautotunnel.util.StringValue
|
import com.zaneschepke.wireguardautotunnel.util.StringValue
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
@@ -10,12 +10,14 @@ sealed class GlobalSideEffect {
|
|||||||
|
|
||||||
data class Snackbar(
|
data class Snackbar(
|
||||||
val message: StringValue,
|
val message: StringValue,
|
||||||
val type: ToastType,
|
val type: SnackbarType? = null,
|
||||||
val actionLabel: String? = null,
|
val actionLabel: String? = null,
|
||||||
val onAction: (() -> Unit)? = null,
|
val onAction: (() -> Unit)? = null,
|
||||||
val durationMs: Long? = null,
|
val durationMs: Long? = null,
|
||||||
) : GlobalSideEffect()
|
) : GlobalSideEffect()
|
||||||
|
|
||||||
|
data class Toast(val message: StringValue) : GlobalSideEffect()
|
||||||
|
|
||||||
data object PopBackStack : GlobalSideEffect()
|
data object PopBackStack : GlobalSideEffect()
|
||||||
|
|
||||||
data class LaunchUrl(val url: String) : GlobalSideEffect()
|
data class LaunchUrl(val url: String) : GlobalSideEffect()
|
||||||
|
|||||||
@@ -11,20 +11,16 @@ sealed class ActiveNetwork {
|
|||||||
|
|
||||||
data object Cellular : ActiveNetwork()
|
data object Cellular : ActiveNetwork()
|
||||||
|
|
||||||
data class Wifi(
|
data class Wifi(val ssid: String, val isSecure: Boolean?) : ActiveNetwork()
|
||||||
val ssid: String,
|
|
||||||
val isSecure: Boolean?,
|
|
||||||
val requiresCaptivePortalLogin: Boolean,
|
|
||||||
) : ActiveNetwork()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
data class NetworkState(
|
data class NetworkState(
|
||||||
val activeNetwork: ActiveNetwork = ActiveNetwork.Disconnected,
|
val activeNetwork: ActiveNetwork = ActiveNetwork.Disconnected,
|
||||||
val locationServicesEnabled: Boolean = false,
|
val locationServicesEnabled: Boolean = false,
|
||||||
val locationPermissionGranted: Boolean = false,
|
val locationPermissionGranted: Boolean = false,
|
||||||
// Has a network that can actually transfer data (not suspended)
|
) {
|
||||||
val hasUsableNetwork: Boolean = false,
|
fun hasInternet(): Boolean = activeNetwork !is ActiveNetwork.Disconnected
|
||||||
)
|
}
|
||||||
|
|
||||||
fun ConnectivityState.toDomain(): NetworkState {
|
fun ConnectivityState.toDomain(): NetworkState {
|
||||||
val domainNetwork: ActiveNetwork =
|
val domainNetwork: ActiveNetwork =
|
||||||
@@ -37,11 +33,7 @@ fun ConnectivityState.toDomain(): NetworkState {
|
|||||||
null -> null
|
null -> null
|
||||||
else -> true
|
else -> true
|
||||||
}
|
}
|
||||||
ActiveNetwork.Wifi(
|
ActiveNetwork.Wifi(ssid = network.ssid, isSecure = isSecure)
|
||||||
ssid = network.ssid,
|
|
||||||
isSecure = isSecure,
|
|
||||||
requiresCaptivePortalLogin(),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
is MonitorActiveNetwork.Cellular -> ActiveNetwork.Cellular
|
is MonitorActiveNetwork.Cellular -> ActiveNetwork.Cellular
|
||||||
is MonitorActiveNetwork.Ethernet -> ActiveNetwork.Ethernet
|
is MonitorActiveNetwork.Ethernet -> ActiveNetwork.Ethernet
|
||||||
@@ -52,6 +44,5 @@ fun ConnectivityState.toDomain(): NetworkState {
|
|||||||
activeNetwork = domainNetwork,
|
activeNetwork = domainNetwork,
|
||||||
locationPermissionGranted = this.locationPermissionsGranted,
|
locationPermissionGranted = this.locationPermissionsGranted,
|
||||||
locationServicesEnabled = this.locationServicesEnabled,
|
locationServicesEnabled = this.locationServicesEnabled,
|
||||||
hasUsableNetwork = hasUsableNetwork(),
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
-26
@@ -1,26 +0,0 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.lifecyle
|
|
||||||
|
|
||||||
import androidx.lifecycle.DefaultLifecycleObserver
|
|
||||||
import androidx.lifecycle.LifecycleOwner
|
|
||||||
import androidx.lifecycle.ProcessLifecycleOwner
|
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
|
||||||
|
|
||||||
class AppVisibilityObserver : DefaultLifecycleObserver {
|
|
||||||
|
|
||||||
private val _isForeground = MutableStateFlow(false)
|
|
||||||
val isForeground: StateFlow<Boolean> = _isForeground.asStateFlow()
|
|
||||||
|
|
||||||
init {
|
|
||||||
ProcessLifecycleOwner.get().lifecycle.addObserver(this)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onStart(owner: LifecycleOwner) {
|
|
||||||
_isForeground.value = true
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onStop(owner: LifecycleOwner) {
|
|
||||||
_isForeground.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-73
@@ -1,73 +0,0 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.ui.common.dialog
|
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
|
||||||
import androidx.compose.foundation.layout.Column
|
|
||||||
import androidx.compose.foundation.layout.Spacer
|
|
||||||
import androidx.compose.foundation.layout.height
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.res.stringResource
|
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import com.zaneschepke.wireguardautotunnel.R
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun LocalNetworkPermissionDialog(onDismiss: () -> Unit, onAttest: () -> Unit) {
|
|
||||||
InfoDialog(
|
|
||||||
onAttest = onAttest,
|
|
||||||
onDismiss = onDismiss,
|
|
||||||
title = stringResource(R.string.local_network_permission_title),
|
|
||||||
body = {
|
|
||||||
Column {
|
|
||||||
Text(
|
|
||||||
text = stringResource(R.string.local_network_permission_intro),
|
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
|
||||||
)
|
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
|
||||||
|
|
||||||
Text(
|
|
||||||
text = stringResource(R.string.local_network_permission_issues_intro),
|
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
|
||||||
fontWeight = FontWeight.Bold,
|
|
||||||
)
|
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
|
||||||
|
|
||||||
Column(verticalArrangement = Arrangement.spacedBy(4.dp)) {
|
|
||||||
Text(
|
|
||||||
text = stringResource(R.string.local_network_permission_feature_tunnels),
|
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
|
||||||
)
|
|
||||||
Text(
|
|
||||||
text = stringResource(R.string.local_network_permission_feature_autotunnel),
|
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
|
||||||
)
|
|
||||||
Text(
|
|
||||||
text = stringResource(R.string.local_network_permission_feature_proxy),
|
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
|
||||||
|
|
||||||
Text(
|
|
||||||
text = stringResource(R.string.local_network_permission_recommendation),
|
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
|
||||||
fontWeight = FontWeight.Bold,
|
|
||||||
)
|
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
|
||||||
|
|
||||||
Text(
|
|
||||||
text = stringResource(R.string.local_network_permission_nearby_devices),
|
|
||||||
style = MaterialTheme.typography.bodySmall,
|
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
confirmText = stringResource(R.string._continue),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
+104
@@ -0,0 +1,104 @@
|
|||||||
|
package com.zaneschepke.wireguardautotunnel.ui.common.snackbar
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.IntrinsicSize
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.foundation.layout.wrapContentHeight
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.outlined.Favorite
|
||||||
|
import androidx.compose.material.icons.rounded.Close
|
||||||
|
import androidx.compose.material.icons.rounded.Info
|
||||||
|
import androidx.compose.material.icons.rounded.Warning
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.IconButton
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Snackbar
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.AnnotatedString
|
||||||
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.zaneschepke.wireguardautotunnel.R
|
||||||
|
import com.zaneschepke.wireguardautotunnel.ui.LocalIsAndroidTV
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun CustomSnackBar(
|
||||||
|
message: AnnotatedString,
|
||||||
|
onDismiss: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
type: SnackbarType = SnackbarType.INFO,
|
||||||
|
containerColor: Color = MaterialTheme.colorScheme.surface,
|
||||||
|
) {
|
||||||
|
val isTv = LocalIsAndroidTV.current
|
||||||
|
val icon =
|
||||||
|
when (type) {
|
||||||
|
SnackbarType.INFO -> Icons.Rounded.Info
|
||||||
|
SnackbarType.WARNING -> Icons.Rounded.Warning
|
||||||
|
SnackbarType.THANK_YOU -> Icons.Outlined.Favorite
|
||||||
|
}
|
||||||
|
val iconDescription =
|
||||||
|
when (type) {
|
||||||
|
SnackbarType.INFO -> stringResource(R.string.info)
|
||||||
|
SnackbarType.WARNING -> stringResource(R.string.warning)
|
||||||
|
SnackbarType.THANK_YOU -> stringResource(R.string.thank_you)
|
||||||
|
}
|
||||||
|
|
||||||
|
Snackbar(
|
||||||
|
containerColor = containerColor,
|
||||||
|
modifier =
|
||||||
|
modifier
|
||||||
|
.wrapContentHeight(align = Alignment.Top)
|
||||||
|
.padding(horizontal = if (isTv) 48.dp else 16.dp),
|
||||||
|
shape = RoundedCornerShape(16.dp),
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier =
|
||||||
|
Modifier.fillMaxWidth()
|
||||||
|
.height(IntrinsicSize.Min)
|
||||||
|
.width(IntrinsicSize.Min)
|
||||||
|
.padding(vertical = 16.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth().weight(1f),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.Start,
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
icon,
|
||||||
|
contentDescription = iconDescription,
|
||||||
|
tint = MaterialTheme.colorScheme.onSurface,
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.width(16.dp))
|
||||||
|
Text(
|
||||||
|
text = message,
|
||||||
|
color = MaterialTheme.colorScheme.onSurface,
|
||||||
|
maxLines = 8,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Row {
|
||||||
|
IconButton(onClick = onDismiss, modifier = Modifier.size(24.dp)) {
|
||||||
|
Icon(
|
||||||
|
Icons.Rounded.Close,
|
||||||
|
contentDescription = stringResource(R.string.stop),
|
||||||
|
tint = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+76
@@ -0,0 +1,76 @@
|
|||||||
|
package com.zaneschepke.wireguardautotunnel.ui.common.snackbar
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import kotlinx.coroutines.channels.Channel
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun rememberCustomSnackbarState(): CustomSnackbarState {
|
||||||
|
return remember { CustomSnackbarState() }
|
||||||
|
}
|
||||||
|
|
||||||
|
class CustomSnackbarState {
|
||||||
|
private val _snackbars = Channel<SnackbarInfo>(Channel.BUFFERED)
|
||||||
|
val snackbars: Channel<SnackbarInfo> = _snackbars
|
||||||
|
|
||||||
|
private var currentSnackbar by mutableStateOf<SnackbarInfo?>(null)
|
||||||
|
private var isShowing by mutableStateOf(false)
|
||||||
|
|
||||||
|
fun showSnackbar(info: SnackbarInfo) {
|
||||||
|
_snackbars.trySend(info)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun dismissCurrent() {
|
||||||
|
currentSnackbar = null
|
||||||
|
isShowing = false
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun SnackbarHost(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
snackbar: @Composable (SnackbarInfo) -> Unit = { info ->
|
||||||
|
CustomSnackBar(
|
||||||
|
message = info.message,
|
||||||
|
type = info.type,
|
||||||
|
onDismiss = { dismissCurrent() },
|
||||||
|
modifier = Modifier,
|
||||||
|
containerColor = MaterialTheme.colorScheme.surface.copy(.1f),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
for (info in snackbars) {
|
||||||
|
currentSnackbar = info
|
||||||
|
isShowing = true
|
||||||
|
|
||||||
|
scope.launch {
|
||||||
|
delay(info.durationMs)
|
||||||
|
if (currentSnackbar?.id == info.id) {
|
||||||
|
dismissCurrent()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
while (isShowing && currentSnackbar?.id == info.id) {
|
||||||
|
delay(100)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
currentSnackbar?.let { info ->
|
||||||
|
if (isShowing) {
|
||||||
|
Box(modifier = modifier) { snackbar(info) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+16
@@ -0,0 +1,16 @@
|
|||||||
|
package com.zaneschepke.wireguardautotunnel.ui.common.snackbar
|
||||||
|
|
||||||
|
import androidx.compose.ui.text.AnnotatedString
|
||||||
|
|
||||||
|
enum class SnackbarType {
|
||||||
|
INFO,
|
||||||
|
WARNING,
|
||||||
|
THANK_YOU,
|
||||||
|
}
|
||||||
|
|
||||||
|
data class SnackbarInfo(
|
||||||
|
val message: AnnotatedString,
|
||||||
|
val type: SnackbarType = SnackbarType.INFO,
|
||||||
|
val durationMs: Long = 4000L,
|
||||||
|
val id: String = System.currentTimeMillis().toString(),
|
||||||
|
)
|
||||||
+33
-49
@@ -1,18 +1,13 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.ui.common.textbox
|
package com.zaneschepke.wireguardautotunnel.ui.common.textbox
|
||||||
|
|
||||||
import androidx.compose.foundation.background
|
|
||||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
import androidx.compose.foundation.layout.Box
|
|
||||||
import androidx.compose.foundation.layout.defaultMinSize
|
import androidx.compose.foundation.layout.defaultMinSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.offset
|
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
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.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
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
|
||||||
@@ -34,59 +29,48 @@ fun ConfigurationTextBox(
|
|||||||
leading: (@Composable () -> Unit)? = null,
|
leading: (@Composable () -> Unit)? = null,
|
||||||
trailing: (@Composable (Modifier) -> Unit)? = null,
|
trailing: (@Composable (Modifier) -> Unit)? = null,
|
||||||
supportingText: (@Composable () -> Unit)? = null,
|
supportingText: (@Composable () -> Unit)? = null,
|
||||||
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
|
interactionSource: MutableInteractionSource = MutableInteractionSource(),
|
||||||
visualTransformation: VisualTransformation = VisualTransformation.None,
|
visualTransformation: VisualTransformation = VisualTransformation.None,
|
||||||
enabled: Boolean = true,
|
enabled: Boolean = true,
|
||||||
readOnly: Boolean = false,
|
readOnly: Boolean = false,
|
||||||
singleLine: Boolean = true,
|
singleLine: Boolean = true,
|
||||||
) {
|
) {
|
||||||
Box(modifier = modifier.padding(top = 6.dp)) {
|
|
||||||
CustomTextField(
|
|
||||||
isError = isError,
|
|
||||||
textStyle =
|
|
||||||
MaterialTheme.typography.bodySmall.copy(
|
|
||||||
color = MaterialTheme.colorScheme.onSurface
|
|
||||||
),
|
|
||||||
modifier = Modifier.fillMaxWidth().defaultMinSize(minHeight = 48.dp),
|
|
||||||
value = value,
|
|
||||||
visualTransformation = visualTransformation,
|
|
||||||
singleLine = singleLine,
|
|
||||||
interactionSource = interactionSource,
|
|
||||||
onValueChange = onValueChange,
|
|
||||||
label = null, // Disable built in label
|
|
||||||
containerColor = MaterialTheme.colorScheme.surface,
|
|
||||||
placeholder = {
|
|
||||||
Text(
|
|
||||||
text = hint,
|
|
||||||
style = MaterialTheme.typography.bodySmall,
|
|
||||||
color = MaterialTheme.colorScheme.outline,
|
|
||||||
overflow = TextOverflow.Ellipsis,
|
|
||||||
maxLines = 1,
|
|
||||||
)
|
|
||||||
},
|
|
||||||
keyboardOptions = keyboardOptions,
|
|
||||||
keyboardActions = keyboardActions,
|
|
||||||
trailing = trailing,
|
|
||||||
supportingText = supportingText,
|
|
||||||
leading = leading,
|
|
||||||
readOnly = readOnly,
|
|
||||||
enabled = enabled,
|
|
||||||
)
|
|
||||||
|
|
||||||
// custom static label notch
|
CustomTextField(
|
||||||
if (label.isNotEmpty()) {
|
isError = isError,
|
||||||
|
textStyle =
|
||||||
|
MaterialTheme.typography.bodySmall.copy(color = MaterialTheme.colorScheme.onSurface),
|
||||||
|
modifier = modifier.fillMaxWidth().defaultMinSize(minHeight = 48.dp),
|
||||||
|
value = value,
|
||||||
|
visualTransformation = visualTransformation,
|
||||||
|
singleLine = singleLine,
|
||||||
|
interactionSource = interactionSource,
|
||||||
|
onValueChange = { onValueChange(it) },
|
||||||
|
label = {
|
||||||
Text(
|
Text(
|
||||||
text = label,
|
label,
|
||||||
color = MaterialTheme.colorScheme.onSurface,
|
color = MaterialTheme.colorScheme.onSurface,
|
||||||
style = MaterialTheme.typography.labelSmall,
|
style = MaterialTheme.typography.labelMedium,
|
||||||
overflow = TextOverflow.Ellipsis,
|
overflow = TextOverflow.Ellipsis,
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
modifier =
|
|
||||||
Modifier.padding(start = 12.dp)
|
|
||||||
.offset(y = (-8).dp)
|
|
||||||
.background(MaterialTheme.colorScheme.surface)
|
|
||||||
.padding(horizontal = 4.dp),
|
|
||||||
)
|
)
|
||||||
}
|
},
|
||||||
}
|
containerColor = MaterialTheme.colorScheme.surface,
|
||||||
|
placeholder = {
|
||||||
|
Text(
|
||||||
|
hint,
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = MaterialTheme.colorScheme.outline,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
maxLines = 1,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
keyboardOptions = keyboardOptions,
|
||||||
|
keyboardActions = keyboardActions,
|
||||||
|
trailing = trailing,
|
||||||
|
supportingText = supportingText,
|
||||||
|
leading = leading,
|
||||||
|
readOnly = readOnly,
|
||||||
|
enabled = enabled,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+15
-24
@@ -22,9 +22,7 @@ import androidx.compose.ui.focus.focusRequester
|
|||||||
import androidx.compose.ui.focus.onFocusChanged
|
import androidx.compose.ui.focus.onFocusChanged
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.SolidColor
|
import androidx.compose.ui.graphics.SolidColor
|
||||||
import androidx.compose.ui.text.TextRange
|
|
||||||
import androidx.compose.ui.text.TextStyle
|
import androidx.compose.ui.text.TextStyle
|
||||||
import androidx.compose.ui.text.input.TextFieldValue
|
|
||||||
import androidx.compose.ui.text.input.VisualTransformation
|
import androidx.compose.ui.text.input.VisualTransformation
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
||||||
@@ -35,7 +33,7 @@ fun CustomTextField(
|
|||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
textStyle: TextStyle =
|
textStyle: TextStyle =
|
||||||
MaterialTheme.typography.bodyLarge.copy(color = MaterialTheme.colorScheme.onSurface),
|
MaterialTheme.typography.bodyLarge.copy(color = MaterialTheme.colorScheme.onSurface),
|
||||||
label: @Composable (() -> Unit)? = null,
|
label: @Composable () -> Unit,
|
||||||
containerColor: Color,
|
containerColor: Color,
|
||||||
onValueChange: (value: String) -> Unit = {},
|
onValueChange: (value: String) -> Unit = {},
|
||||||
singleLine: Boolean = true,
|
singleLine: Boolean = true,
|
||||||
@@ -49,19 +47,10 @@ fun CustomTextField(
|
|||||||
readOnly: Boolean = false,
|
readOnly: Boolean = false,
|
||||||
enabled: Boolean = true,
|
enabled: Boolean = true,
|
||||||
visualTransformation: VisualTransformation = VisualTransformation.None,
|
visualTransformation: VisualTransformation = VisualTransformation.None,
|
||||||
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
|
interactionSource: MutableInteractionSource = MutableInteractionSource(),
|
||||||
) {
|
) {
|
||||||
|
val space = " "
|
||||||
var isFocused by remember { mutableStateOf(false) }
|
var isFocused by remember { mutableStateOf(false) }
|
||||||
var textFieldValueState by remember { mutableStateOf(TextFieldValue(text = value)) }
|
|
||||||
val textFieldValue =
|
|
||||||
remember(value, textFieldValueState) {
|
|
||||||
if (textFieldValueState.text == value) {
|
|
||||||
textFieldValueState
|
|
||||||
} else {
|
|
||||||
textFieldValueState.copy(text = value, selection = TextRange(value.length))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val cursorBrush =
|
val cursorBrush =
|
||||||
if (isFocused) SolidColor(MaterialTheme.colorScheme.primary)
|
if (isFocused) SolidColor(MaterialTheme.colorScheme.primary)
|
||||||
else SolidColor(Color.Transparent)
|
else SolidColor(Color.Transparent)
|
||||||
@@ -78,14 +67,9 @@ fun CustomTextField(
|
|||||||
}
|
}
|
||||||
|
|
||||||
BasicTextField(
|
BasicTextField(
|
||||||
value = textFieldValue,
|
value = value,
|
||||||
textStyle = effectiveTextStyle,
|
textStyle = effectiveTextStyle,
|
||||||
onValueChange = { newTextFieldValue ->
|
onValueChange = { onValueChange(it) },
|
||||||
textFieldValueState = newTextFieldValue
|
|
||||||
if (value != newTextFieldValue.text) {
|
|
||||||
onValueChange(newTextFieldValue.text)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
keyboardActions = keyboardActions,
|
keyboardActions = keyboardActions,
|
||||||
keyboardOptions = keyboardOptions,
|
keyboardOptions = keyboardOptions,
|
||||||
readOnly = readOnly,
|
readOnly = readOnly,
|
||||||
@@ -106,9 +90,15 @@ fun CustomTextField(
|
|||||||
visualTransformation = visualTransformation,
|
visualTransformation = visualTransformation,
|
||||||
) {
|
) {
|
||||||
OutlinedTextFieldDefaults.DecorationBox(
|
OutlinedTextFieldDefaults.DecorationBox(
|
||||||
value = value,
|
value = space + value,
|
||||||
innerTextField = it,
|
innerTextField = {
|
||||||
placeholder = placeholder,
|
if (value.isEmpty()) {
|
||||||
|
if (placeholder != null) {
|
||||||
|
placeholder()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
it.invoke()
|
||||||
|
},
|
||||||
contentPadding = OutlinedTextFieldDefaults.contentPadding(top = 14.dp, bottom = 14.dp),
|
contentPadding = OutlinedTextFieldDefaults.contentPadding(top = 14.dp, bottom = 14.dp),
|
||||||
leadingIcon = leading,
|
leadingIcon = leading,
|
||||||
trailingIcon =
|
trailingIcon =
|
||||||
@@ -151,6 +141,7 @@ fun CustomTextField(
|
|||||||
label = label,
|
label = label,
|
||||||
visualTransformation = visualTransformation,
|
visualTransformation = visualTransformation,
|
||||||
interactionSource = interactionSource,
|
interactionSource = interactionSource,
|
||||||
|
placeholder = placeholder,
|
||||||
container = {
|
container = {
|
||||||
OutlinedTextFieldDefaults.Container(
|
OutlinedTextFieldDefaults.Container(
|
||||||
enabled = enabled,
|
enabled = enabled,
|
||||||
|
|||||||
@@ -42,12 +42,7 @@ sealed class Route : NavKey {
|
|||||||
|
|
||||||
@Keep @Serializable data object Display : Route()
|
@Keep @Serializable data object Display : Route()
|
||||||
|
|
||||||
@Keep
|
@Keep @Serializable data object Tunnels : Route()
|
||||||
@Serializable
|
|
||||||
data object Tunnels : Route(), SecureRoute {
|
|
||||||
override val requiresProtection: Boolean
|
|
||||||
get() = true
|
|
||||||
}
|
|
||||||
|
|
||||||
@Keep @Serializable data class TunnelSettings(val id: Int) : Route()
|
@Keep @Serializable data class TunnelSettings(val id: Int) : Route()
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -250,7 +250,7 @@ fun currentRouteAsNavbarState(
|
|||||||
val global = route !is ConfigEdit
|
val global = route !is ConfigEdit
|
||||||
val tunnelName =
|
val tunnelName =
|
||||||
if (!global) globalState.tunnelNames[route.id]
|
if (!global) globalState.tunnelNames[route.id]
|
||||||
else context.getString(R.string.tunnel_configuration)
|
else context.getString(R.string.configuration_globals)
|
||||||
NavbarState(
|
NavbarState(
|
||||||
topLeading = { TvBackButton { navController.pop() } },
|
topLeading = { TvBackButton { navController.pop() } },
|
||||||
showBottomItems = true,
|
showBottomItems = true,
|
||||||
@@ -297,7 +297,7 @@ fun currentRouteAsNavbarState(
|
|||||||
is SplitTunnelGlobal -> {
|
is SplitTunnelGlobal -> {
|
||||||
val tunnelName =
|
val tunnelName =
|
||||||
if (route is SplitTunnel) globalState.tunnelNames[route.id]
|
if (route is SplitTunnel) globalState.tunnelNames[route.id]
|
||||||
else context.getString(R.string.splt_tunneling)
|
else context.getString(R.string.global_split_tunneling)
|
||||||
NavbarState(
|
NavbarState(
|
||||||
topLeading = { TvBackButton { navController.pop() } },
|
topLeading = { TvBackButton { navController.pop() } },
|
||||||
topTitle = tunnelName ?: "",
|
topTitle = tunnelName ?: "",
|
||||||
|
|||||||
+2
-5
@@ -302,9 +302,7 @@ fun AutoTunnelScreen(
|
|||||||
SurfaceRow(
|
SurfaceRow(
|
||||||
leading = { Icon(Icons.Outlined.PublicOff, contentDescription = null) },
|
leading = { Icon(Icons.Outlined.PublicOff, contentDescription = null) },
|
||||||
title = stringResource(R.string.stop_on_no_internet),
|
title = stringResource(R.string.stop_on_no_internet),
|
||||||
description = {
|
description = { DescriptionText(stringResource(R.string.stop_on_internet_loss)) },
|
||||||
DescriptionText(stringResource(R.string.stop_on_no_internet_desc))
|
|
||||||
},
|
|
||||||
trailing = {
|
trailing = {
|
||||||
ThemedSwitch(
|
ThemedSwitch(
|
||||||
checked = uiState.autoTunnelSettings.isStopOnNoInternetEnabled,
|
checked = uiState.autoTunnelSettings.isStopOnNoInternetEnabled,
|
||||||
@@ -320,7 +318,7 @@ fun AutoTunnelScreen(
|
|||||||
}
|
}
|
||||||
Column {
|
Column {
|
||||||
GroupLabel(
|
GroupLabel(
|
||||||
stringResource(R.string.automation),
|
stringResource(R.string.other),
|
||||||
modifier = Modifier.padding(horizontal = 16.dp),
|
modifier = Modifier.padding(horizontal = 16.dp),
|
||||||
)
|
)
|
||||||
SurfaceRow(
|
SurfaceRow(
|
||||||
@@ -332,7 +330,6 @@ fun AutoTunnelScreen(
|
|||||||
onClick = { viewModel.setStartAtBoot(it) },
|
onClick = { viewModel.setStartAtBoot(it) },
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
description = { DescriptionText(stringResource(R.string.start_on_boot_desc)) },
|
|
||||||
onClick = { viewModel.setStartAtBoot(!uiState.autoTunnelSettings.startOnBoot) },
|
onClick = { viewModel.setStartAtBoot(!uiState.autoTunnelSettings.startOnBoot) },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
-16
@@ -10,7 +10,6 @@ import androidx.compose.foundation.verticalScroll
|
|||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.outlined.Filter1
|
import androidx.compose.material.icons.outlined.Filter1
|
||||||
import androidx.compose.material.icons.outlined.Map
|
import androidx.compose.material.icons.outlined.Map
|
||||||
import androidx.compose.material.icons.outlined.PublicOff
|
|
||||||
import androidx.compose.material.icons.outlined.WifiFind
|
import androidx.compose.material.icons.outlined.WifiFind
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
@@ -192,21 +191,6 @@ fun WifiSettingsScreen(viewModel: AutoTunnelViewModel = koinViewModel()) {
|
|||||||
)
|
)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
SurfaceRow(
|
|
||||||
leading = { Icon(Icons.Outlined.PublicOff, contentDescription = null) },
|
|
||||||
title = stringResource(R.string.stop_while_captive_portal),
|
|
||||||
onClick = {
|
|
||||||
viewModel.setDisabledOnCaptivePortal(
|
|
||||||
!uiState.autoTunnelSettings.disableTunnelOnCaptivePortal
|
|
||||||
)
|
|
||||||
},
|
|
||||||
trailing = {
|
|
||||||
ThemedSwitch(
|
|
||||||
checked = uiState.autoTunnelSettings.disableTunnelOnCaptivePortal,
|
|
||||||
onClick = { viewModel.setDisabledOnCaptivePortal(it) },
|
|
||||||
)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
Column {
|
Column {
|
||||||
GroupLabel(stringResource(R.string.tunnels), Modifier.padding(horizontal = 16.dp))
|
GroupLabel(stringResource(R.string.tunnels), Modifier.padding(horizontal = 16.dp))
|
||||||
|
|||||||
+2
-9
@@ -9,7 +9,6 @@ import androidx.compose.runtime.mutableStateOf
|
|||||||
import androidx.compose.runtime.saveable.rememberSaveable
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import com.dokar.sonner.ToastType
|
|
||||||
import com.zaneschepke.wireguardautotunnel.R
|
import com.zaneschepke.wireguardautotunnel.R
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.LocalNavController
|
import com.zaneschepke.wireguardautotunnel.ui.LocalNavController
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.navigation.Route
|
import com.zaneschepke.wireguardautotunnel.ui.navigation.Route
|
||||||
@@ -46,17 +45,11 @@ fun PinLockScreen(sharedViewModel: SharedAppViewModel = koinActivityViewModel())
|
|||||||
textColor = MaterialTheme.colorScheme.onSurface,
|
textColor = MaterialTheme.colorScheme.onSurface,
|
||||||
onPinCorrect = { onPinCorrect() },
|
onPinCorrect = { onPinCorrect() },
|
||||||
onPinIncorrect = {
|
onPinIncorrect = {
|
||||||
sharedViewModel.showSnackMessage(
|
sharedViewModel.showToast(StringValue.StringResource(R.string.incorrect_pin))
|
||||||
StringValue.StringResource(R.string.incorrect_pin),
|
|
||||||
ToastType.Warning,
|
|
||||||
)
|
|
||||||
},
|
},
|
||||||
onPinCreated = {
|
onPinCreated = {
|
||||||
pinCreated = true
|
pinCreated = true
|
||||||
sharedViewModel.showSnackMessage(
|
sharedViewModel.showToast(StringValue.StringResource(R.string.pin_created))
|
||||||
StringValue.StringResource(R.string.pin_created),
|
|
||||||
ToastType.Success,
|
|
||||||
)
|
|
||||||
sharedViewModel.setPinLockEnabled(true)
|
sharedViewModel.setPinLockEnabled(true)
|
||||||
onPinCorrect()
|
onPinCorrect()
|
||||||
},
|
},
|
||||||
|
|||||||
+10
-51
@@ -38,7 +38,6 @@ import androidx.compose.ui.res.stringResource
|
|||||||
import androidx.compose.ui.res.vectorResource
|
import androidx.compose.ui.res.vectorResource
|
||||||
import androidx.compose.ui.text.intl.Locale
|
import androidx.compose.ui.text.intl.Locale
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import com.dokar.sonner.ToastType
|
|
||||||
import com.zaneschepke.wireguardautotunnel.MainActivity
|
import com.zaneschepke.wireguardautotunnel.MainActivity
|
||||||
import com.zaneschepke.wireguardautotunnel.R
|
import com.zaneschepke.wireguardautotunnel.R
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelMode
|
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelMode
|
||||||
@@ -52,12 +51,12 @@ import com.zaneschepke.wireguardautotunnel.ui.common.label.GroupLabel
|
|||||||
import com.zaneschepke.wireguardautotunnel.ui.common.text.DescriptionText
|
import com.zaneschepke.wireguardautotunnel.ui.common.text.DescriptionText
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.navigation.Route
|
import com.zaneschepke.wireguardautotunnel.ui.navigation.Route
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.components.BackupBottomSheet
|
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.components.BackupBottomSheet
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.components.BackupEncryptionDialog
|
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.proxy.compoents.AppModeBottomSheet
|
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.proxy.compoents.AppModeBottomSheet
|
||||||
import com.zaneschepke.wireguardautotunnel.util.StringValue
|
import com.zaneschepke.wireguardautotunnel.util.StringValue
|
||||||
import com.zaneschepke.wireguardautotunnel.util.extensions.asString
|
import com.zaneschepke.wireguardautotunnel.util.extensions.asString
|
||||||
import com.zaneschepke.wireguardautotunnel.util.extensions.asTitleString
|
import com.zaneschepke.wireguardautotunnel.util.extensions.asTitleString
|
||||||
import com.zaneschepke.wireguardautotunnel.util.extensions.capitalize
|
import com.zaneschepke.wireguardautotunnel.util.extensions.capitalize
|
||||||
|
import com.zaneschepke.wireguardautotunnel.util.extensions.showToast
|
||||||
import com.zaneschepke.wireguardautotunnel.viewmodel.SettingsViewModel
|
import com.zaneschepke.wireguardautotunnel.viewmodel.SettingsViewModel
|
||||||
import com.zaneschepke.wireguardautotunnel.viewmodel.SharedAppViewModel
|
import com.zaneschepke.wireguardautotunnel.viewmodel.SharedAppViewModel
|
||||||
import org.koin.androidx.compose.koinViewModel
|
import org.koin.androidx.compose.koinViewModel
|
||||||
@@ -89,9 +88,6 @@ fun SettingsScreen(
|
|||||||
}
|
}
|
||||||
|
|
||||||
var showBackupSheet by rememberSaveable { mutableStateOf(false) }
|
var showBackupSheet by rememberSaveable { mutableStateOf(false) }
|
||||||
var showEncryptionDialog by rememberSaveable { mutableStateOf(false) }
|
|
||||||
var isRestoreAction by remember { mutableStateOf(false) }
|
|
||||||
|
|
||||||
var showAppModeSheet by rememberSaveable { mutableStateOf(false) }
|
var showAppModeSheet by rememberSaveable { mutableStateOf(false) }
|
||||||
|
|
||||||
val appMode = uiState.settings.tunnelMode
|
val appMode = uiState.settings.tunnelMode
|
||||||
@@ -103,53 +99,19 @@ fun SettingsScreen(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun performBackupRestore(action: () -> Unit) {
|
fun performBackupRestore(action: () -> Unit) {
|
||||||
|
if (uiState.tunnelActive || globalUiState.isAutoTunnelActive)
|
||||||
|
return context.showToast(R.string.all_services_disabled)
|
||||||
showBackupSheet = false
|
showBackupSheet = false
|
||||||
if (uiState.tunnelActive || globalUiState.isAutoTunnelActive) {
|
|
||||||
sharedViewModel.showSnackMessage(
|
|
||||||
StringValue.StringResource(R.string.all_services_disabled),
|
|
||||||
ToastType.Warning,
|
|
||||||
)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
action()
|
action()
|
||||||
}
|
}
|
||||||
|
|
||||||
if (showBackupSheet) {
|
if (showBackupSheet)
|
||||||
BackupBottomSheet(
|
BackupBottomSheet(
|
||||||
onBackup = {
|
{ performBackupRestore { (context as? MainActivity)?.performBackup() } },
|
||||||
showBackupSheet = false
|
{ performBackupRestore { (context as? MainActivity)?.performRestore() } },
|
||||||
isRestoreAction = false
|
) {
|
||||||
showEncryptionDialog = true
|
showBackupSheet = false
|
||||||
},
|
}
|
||||||
onRestore = {
|
|
||||||
showBackupSheet = false
|
|
||||||
isRestoreAction = true
|
|
||||||
showEncryptionDialog = true
|
|
||||||
},
|
|
||||||
onDismiss = { showBackupSheet = false },
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (showEncryptionDialog) {
|
|
||||||
BackupEncryptionDialog(
|
|
||||||
isRestore = isRestoreAction,
|
|
||||||
onConfirm = { encrypt, password ->
|
|
||||||
showEncryptionDialog = false
|
|
||||||
|
|
||||||
if (isRestoreAction) {
|
|
||||||
performBackupRestore {
|
|
||||||
(context as? MainActivity)?.performRestore(encrypt, password)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
performBackupRestore {
|
|
||||||
(context as? MainActivity)?.performBackup(encrypt, password)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onDismiss = { showEncryptionDialog = false },
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (showAppModeSheet)
|
if (showAppModeSheet)
|
||||||
AppModeBottomSheet(sharedViewModel::setAppMode, uiState.settings.tunnelMode) {
|
AppModeBottomSheet(sharedViewModel::setAppMode, uiState.settings.tunnelMode) {
|
||||||
showAppModeSheet = false
|
showAppModeSheet = false
|
||||||
@@ -206,8 +168,7 @@ fun SettingsScreen(
|
|||||||
StringValue.StringResource(
|
StringValue.StringResource(
|
||||||
R.string.mode_disabled_template,
|
R.string.mode_disabled_template,
|
||||||
appMode.asString(context),
|
appMode.asString(context),
|
||||||
),
|
)
|
||||||
ToastType.Info,
|
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@@ -215,7 +176,6 @@ fun SettingsScreen(
|
|||||||
leading = { Icon(Icons.Outlined.Public, contentDescription = null) },
|
leading = { Icon(Icons.Outlined.Public, contentDescription = null) },
|
||||||
title = stringResource(R.string.tunnel_globals),
|
title = stringResource(R.string.tunnel_globals),
|
||||||
onClick = { navController.push(Route.TunnelGlobals) },
|
onClick = { navController.push(Route.TunnelGlobals) },
|
||||||
description = { DescriptionText(stringResource(R.string.tunnel_globals_desc)) },
|
|
||||||
)
|
)
|
||||||
SurfaceRow(
|
SurfaceRow(
|
||||||
leading = { Icon(Icons.Outlined.Terminal, contentDescription = null) },
|
leading = { Icon(Icons.Outlined.Terminal, contentDescription = null) },
|
||||||
@@ -272,7 +232,6 @@ fun SettingsScreen(
|
|||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
description = { DescriptionText(stringResource(R.string.local_logging_desc)) },
|
|
||||||
onClick = { navController.push(Route.Logs) },
|
onClick = { navController.push(Route.Logs) },
|
||||||
)
|
)
|
||||||
SurfaceRow(
|
SurfaceRow(
|
||||||
|
|||||||
-146
@@ -1,146 +0,0 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.ui.screens.settings.components
|
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.*
|
|
||||||
import androidx.compose.material.icons.Icons
|
|
||||||
import androidx.compose.material.icons.outlined.Visibility
|
|
||||||
import androidx.compose.material.icons.outlined.VisibilityOff
|
|
||||||
import androidx.compose.material3.Icon
|
|
||||||
import androidx.compose.material3.IconButton
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.runtime.*
|
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.res.stringResource
|
|
||||||
import androidx.compose.ui.text.input.PasswordVisualTransformation
|
|
||||||
import androidx.compose.ui.text.input.VisualTransformation
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import com.zaneschepke.wireguardautotunnel.R
|
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.common.button.ThemedSwitch
|
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.common.dialog.InfoDialog
|
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.common.textbox.CustomTextField
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun BackupEncryptionDialog(
|
|
||||||
isRestore: Boolean,
|
|
||||||
onConfirm: (encrypt: Boolean, password: String?) -> Unit,
|
|
||||||
onDismiss: () -> Unit,
|
|
||||||
) {
|
|
||||||
var encrypt by remember { mutableStateOf(false) }
|
|
||||||
var password by remember { mutableStateOf("") }
|
|
||||||
var confirmPassword by remember { mutableStateOf("") }
|
|
||||||
var showPasswordError by remember { mutableStateOf(false) }
|
|
||||||
var passwordVisible by remember { mutableStateOf(false) }
|
|
||||||
var confirmPasswordVisible by remember { mutableStateOf(false) }
|
|
||||||
|
|
||||||
InfoDialog(
|
|
||||||
title =
|
|
||||||
if (isRestore) {
|
|
||||||
stringResource(R.string.restore)
|
|
||||||
} else {
|
|
||||||
stringResource(R.string.backup)
|
|
||||||
},
|
|
||||||
confirmText =
|
|
||||||
if (isRestore) {
|
|
||||||
stringResource(R.string.restore)
|
|
||||||
} else {
|
|
||||||
stringResource(R.string.backup)
|
|
||||||
},
|
|
||||||
onAttest = {
|
|
||||||
if (!isRestore && encrypt && password != confirmPassword) {
|
|
||||||
showPasswordError = true
|
|
||||||
return@InfoDialog
|
|
||||||
}
|
|
||||||
if (encrypt && password.isBlank()) {
|
|
||||||
return@InfoDialog
|
|
||||||
}
|
|
||||||
|
|
||||||
val finalPassword = if (encrypt) password else null
|
|
||||||
onConfirm(encrypt, finalPassword)
|
|
||||||
},
|
|
||||||
onDismiss = onDismiss,
|
|
||||||
body = {
|
|
||||||
Column(
|
|
||||||
verticalArrangement = Arrangement.spacedBy(16.dp),
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
) {
|
|
||||||
Row(
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
) {
|
|
||||||
Text(stringResource(R.string.encrypted))
|
|
||||||
ThemedSwitch(checked = encrypt, onClick = { encrypt = it })
|
|
||||||
}
|
|
||||||
|
|
||||||
if (encrypt) {
|
|
||||||
CustomTextField(
|
|
||||||
value = password,
|
|
||||||
onValueChange = {
|
|
||||||
password = it
|
|
||||||
showPasswordError = false
|
|
||||||
},
|
|
||||||
containerColor = MaterialTheme.colorScheme.surface,
|
|
||||||
label = { Text(stringResource(R.string.password)) },
|
|
||||||
visualTransformation =
|
|
||||||
if (passwordVisible) VisualTransformation.None
|
|
||||||
else PasswordVisualTransformation(),
|
|
||||||
trailing = {
|
|
||||||
IconButton(onClick = { passwordVisible = !passwordVisible }) {
|
|
||||||
Icon(
|
|
||||||
imageVector =
|
|
||||||
if (passwordVisible) Icons.Outlined.VisibilityOff
|
|
||||||
else Icons.Outlined.Visibility,
|
|
||||||
contentDescription =
|
|
||||||
if (passwordVisible) stringResource(R.string.hide_password)
|
|
||||||
else stringResource(R.string.show_password),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
)
|
|
||||||
|
|
||||||
if (!isRestore) {
|
|
||||||
CustomTextField(
|
|
||||||
value = confirmPassword,
|
|
||||||
onValueChange = {
|
|
||||||
confirmPassword = it
|
|
||||||
showPasswordError = false
|
|
||||||
},
|
|
||||||
label = { Text(stringResource(R.string.confirm_password)) },
|
|
||||||
visualTransformation =
|
|
||||||
if (confirmPasswordVisible) VisualTransformation.None
|
|
||||||
else PasswordVisualTransformation(),
|
|
||||||
trailing = {
|
|
||||||
IconButton(
|
|
||||||
onClick = { confirmPasswordVisible = !confirmPasswordVisible }
|
|
||||||
) {
|
|
||||||
Icon(
|
|
||||||
imageVector =
|
|
||||||
if (confirmPasswordVisible) Icons.Outlined.VisibilityOff
|
|
||||||
else Icons.Outlined.Visibility,
|
|
||||||
contentDescription =
|
|
||||||
if (confirmPasswordVisible)
|
|
||||||
stringResource(R.string.hide_password)
|
|
||||||
else stringResource(R.string.show_password),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
containerColor = MaterialTheme.colorScheme.surface,
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
isError = showPasswordError,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (showPasswordError) {
|
|
||||||
Text(
|
|
||||||
text = stringResource(R.string.passwords_do_not_match),
|
|
||||||
color = MaterialTheme.colorScheme.error,
|
|
||||||
style = MaterialTheme.typography.bodySmall,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
+2
-2
@@ -56,7 +56,7 @@ fun TunnelGlobalsScreen(
|
|||||||
)
|
)
|
||||||
},
|
},
|
||||||
enabled = sharedUiState.tunnelMode != TunnelMode.PROXY,
|
enabled = sharedUiState.tunnelMode != TunnelMode.PROXY,
|
||||||
title = stringResource(R.string.splt_tunneling),
|
title = stringResource(R.string.global_split_tunneling),
|
||||||
trailing = { modifier ->
|
trailing = { modifier ->
|
||||||
SwitchWithDivider(
|
SwitchWithDivider(
|
||||||
checked = uiState.settings.isGlobalSplitTunnelEnabled,
|
checked = uiState.settings.isGlobalSplitTunnelEnabled,
|
||||||
@@ -82,7 +82,7 @@ fun TunnelGlobalsScreen(
|
|||||||
)
|
)
|
||||||
SurfaceRow(
|
SurfaceRow(
|
||||||
leading = { Icon(Icons.Outlined.Description, contentDescription = null) },
|
leading = { Icon(Icons.Outlined.Description, contentDescription = null) },
|
||||||
title = stringResource(R.string.tunnel_configuration),
|
title = stringResource(R.string.configuration_globals),
|
||||||
onClick = {
|
onClick = {
|
||||||
uiState.globalTunnelConfig?.let {
|
uiState.globalTunnelConfig?.let {
|
||||||
navController.push(Route.ConfigGlobal(id = it.id))
|
navController.push(Route.ConfigGlobal(id = it.id))
|
||||||
|
|||||||
+4
-6
@@ -11,7 +11,7 @@ import androidx.compose.foundation.verticalScroll
|
|||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.automirrored.outlined.Launch
|
import androidx.compose.material.icons.automirrored.outlined.Launch
|
||||||
import androidx.compose.material.icons.filled.AppShortcut
|
import androidx.compose.material.icons.filled.AppShortcut
|
||||||
import androidx.compose.material.icons.filled.SettingsRemote
|
import androidx.compose.material.icons.filled.SmartToy
|
||||||
import androidx.compose.material.icons.outlined.AdminPanelSettings
|
import androidx.compose.material.icons.outlined.AdminPanelSettings
|
||||||
import androidx.compose.material.icons.outlined.ContentCopy
|
import androidx.compose.material.icons.outlined.ContentCopy
|
||||||
import androidx.compose.material.icons.outlined.Key
|
import androidx.compose.material.icons.outlined.Key
|
||||||
@@ -133,22 +133,20 @@ fun AndroidIntegrationsScreen(viewModel: SettingsViewModel = koinViewModel()) {
|
|||||||
onClick = { viewModel.setShortcutsEnabled(it) },
|
onClick = { viewModel.setShortcutsEnabled(it) },
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
title = stringResource(R.string.app_shortcuts),
|
title = stringResource(R.string.enabled_app_shortcuts),
|
||||||
description = { DescriptionText(stringResource(R.string.app_shortcuts_desc)) },
|
|
||||||
onClick = {
|
onClick = {
|
||||||
viewModel.setShortcutsEnabled(!settingsState.settings.isShortcutsEnabled)
|
viewModel.setShortcutsEnabled(!settingsState.settings.isShortcutsEnabled)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
SurfaceRow(
|
SurfaceRow(
|
||||||
leading = { Icon(Icons.Filled.SettingsRemote, contentDescription = null) },
|
leading = { Icon(Icons.Filled.SmartToy, contentDescription = null) },
|
||||||
trailing = {
|
trailing = {
|
||||||
ThemedSwitch(
|
ThemedSwitch(
|
||||||
checked = settingsState.isRemoteEnabled,
|
checked = settingsState.isRemoteEnabled,
|
||||||
onClick = { viewModel.setRemoteEnabled(it) },
|
onClick = { viewModel.setRemoteEnabled(it) },
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
title = stringResource(R.string.remote_control),
|
title = stringResource(R.string.enable_remote_app_control),
|
||||||
description = { DescriptionText(stringResource(R.string.remote_control_desc)) },
|
|
||||||
onClick = { viewModel.setRemoteEnabled(!settingsState.isRemoteEnabled) },
|
onClick = { viewModel.setRemoteEnabled(!settingsState.isRemoteEnabled) },
|
||||||
)
|
)
|
||||||
AnimatedVisibility(settingsState.isRemoteEnabled) {
|
AnimatedVisibility(settingsState.isRemoteEnabled) {
|
||||||
|
|||||||
+2
-5
@@ -12,7 +12,6 @@ import androidx.compose.runtime.setValue
|
|||||||
import androidx.compose.runtime.snapshotFlow
|
import androidx.compose.runtime.snapshotFlow
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import com.dokar.sonner.ToastType
|
|
||||||
import com.zaneschepke.wireguardautotunnel.R
|
import com.zaneschepke.wireguardautotunnel.R
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.logs.components.LogList
|
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.logs.components.LogList
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.logs.components.LogsBottomSheet
|
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.logs.components.LogsBottomSheet
|
||||||
@@ -87,15 +86,13 @@ fun LogsScreen(
|
|||||||
},
|
},
|
||||||
onCanceled = {
|
onCanceled = {
|
||||||
sharedViewModel.showSnackMessage(
|
sharedViewModel.showSnackMessage(
|
||||||
StringValue.StringResource(R.string.export_canceled),
|
StringValue.StringResource(R.string.export_canceled)
|
||||||
ToastType.Warning,
|
|
||||||
)
|
)
|
||||||
showLogsSheet = false
|
showLogsSheet = false
|
||||||
},
|
},
|
||||||
onUnsupported = {
|
onUnsupported = {
|
||||||
sharedViewModel.showSnackMessage(
|
sharedViewModel.showSnackMessage(
|
||||||
StringValue.StringResource(R.string.export_unsupported),
|
StringValue.StringResource(R.string.export_unsupported)
|
||||||
ToastType.Warning,
|
|
||||||
)
|
)
|
||||||
showLogsSheet = false
|
showLogsSheet = false
|
||||||
},
|
},
|
||||||
|
|||||||
+1
-9
@@ -1,12 +1,10 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.ui.screens.settings.logs.components
|
package com.zaneschepke.wireguardautotunnel.ui.screens.settings.logs.components
|
||||||
|
|
||||||
import androidx.compose.foundation.gestures.Orientation
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.LazyListState
|
import androidx.compose.foundation.lazy.LazyListState
|
||||||
import androidx.compose.foundation.lazy.itemsIndexed
|
import androidx.compose.foundation.lazy.itemsIndexed
|
||||||
import androidx.compose.material3.scrollbar
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
@@ -20,13 +18,7 @@ fun LogList(
|
|||||||
) {
|
) {
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
state = lazyColumnListState,
|
state = lazyColumnListState,
|
||||||
modifier =
|
modifier = modifier.padding(horizontal = 12.dp),
|
||||||
modifier
|
|
||||||
.padding(horizontal = 12.dp)
|
|
||||||
.scrollbar(
|
|
||||||
state = lazyColumnListState.scrollIndicatorState,
|
|
||||||
orientation = Orientation.Vertical,
|
|
||||||
),
|
|
||||||
verticalArrangement = Arrangement.spacedBy(2.dp),
|
verticalArrangement = Arrangement.spacedBy(2.dp),
|
||||||
) {
|
) {
|
||||||
itemsIndexed(items = logs, key = { index, _ -> index }) { _, log -> LogItem(log = log) }
|
itemsIndexed(items = logs, key = { index, _ -> index }) { _, log -> LogItem(log = log) }
|
||||||
|
|||||||
-8
@@ -2,8 +2,6 @@ package com.zaneschepke.wireguardautotunnel.ui.screens.settings.proxy.compoents
|
|||||||
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.res.stringResource
|
|
||||||
import com.zaneschepke.wireguardautotunnel.R
|
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelMode
|
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelMode
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.common.sheet.CustomBottomSheet
|
import com.zaneschepke.wireguardautotunnel.ui.common.sheet.CustomBottomSheet
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.common.sheet.SheetOption
|
import com.zaneschepke.wireguardautotunnel.ui.common.sheet.SheetOption
|
||||||
@@ -29,12 +27,6 @@ fun AppModeBottomSheet(
|
|||||||
onDismiss()
|
onDismiss()
|
||||||
onAppModeChange(it)
|
onAppModeChange(it)
|
||||||
},
|
},
|
||||||
description =
|
|
||||||
when (it) {
|
|
||||||
TunnelMode.VPN -> stringResource(R.string.vpn_desc)
|
|
||||||
TunnelMode.PROXY -> stringResource(R.string.local_proxy_desc)
|
|
||||||
TunnelMode.LOCK_DOWN -> stringResource(R.string.lockdown_desc)
|
|
||||||
},
|
|
||||||
selected = tunnelMode == it,
|
selected = tunnelMode == it,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-6
@@ -19,7 +19,6 @@ import com.zaneschepke.wireguardautotunnel.R
|
|||||||
import com.zaneschepke.wireguardautotunnel.ui.LocalNavController
|
import com.zaneschepke.wireguardautotunnel.ui.LocalNavController
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.common.button.SurfaceRow
|
import com.zaneschepke.wireguardautotunnel.ui.common.button.SurfaceRow
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.common.button.ThemedSwitch
|
import com.zaneschepke.wireguardautotunnel.ui.common.button.ThemedSwitch
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.common.text.DescriptionText
|
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.navigation.Route
|
import com.zaneschepke.wireguardautotunnel.ui.navigation.Route
|
||||||
import com.zaneschepke.wireguardautotunnel.viewmodel.SharedAppViewModel
|
import com.zaneschepke.wireguardautotunnel.viewmodel.SharedAppViewModel
|
||||||
import org.koin.compose.viewmodel.koinActivityViewModel
|
import org.koin.compose.viewmodel.koinActivityViewModel
|
||||||
@@ -46,16 +45,13 @@ fun SecurityScreen(viewModel: SharedAppViewModel = koinActivityViewModel()) {
|
|||||||
onClick = { viewModel.setScreenRecordingSecurity(it) },
|
onClick = { viewModel.setScreenRecordingSecurity(it) },
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
description = {
|
|
||||||
DescriptionText(stringResource(R.string.screen_recording_protection_desc))
|
|
||||||
},
|
|
||||||
onClick = {
|
onClick = {
|
||||||
viewModel.setScreenRecordingSecurity(!uiState.isScreenRecordingProtectionEnabled)
|
viewModel.setScreenRecordingSecurity(!uiState.isScreenRecordingProtectionEnabled)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
SurfaceRow(
|
SurfaceRow(
|
||||||
leading = { Icon(Icons.Outlined.Pin, contentDescription = null) },
|
leading = { Icon(Icons.Outlined.Pin, contentDescription = null) },
|
||||||
title = stringResource(R.string.app_lock),
|
title = stringResource(R.string.enable_app_lock),
|
||||||
trailing = {
|
trailing = {
|
||||||
ThemedSwitch(
|
ThemedSwitch(
|
||||||
checked = uiState.pinLockEnabled,
|
checked = uiState.pinLockEnabled,
|
||||||
@@ -68,7 +64,6 @@ fun SecurityScreen(viewModel: SharedAppViewModel = koinActivityViewModel()) {
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
description = { DescriptionText(stringResource(R.string.app_lock_desc)) },
|
|
||||||
onClick = {
|
onClick = {
|
||||||
if (!uiState.pinLockEnabled) {
|
if (!uiState.pinLockEnabled) {
|
||||||
navController.push(Route.Lock)
|
navController.push(Route.Lock)
|
||||||
|
|||||||
+15
-67
@@ -26,7 +26,6 @@ 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
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
|
||||||
import androidx.compose.runtime.saveable.rememberSaveable
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
@@ -38,10 +37,8 @@ import androidx.compose.ui.platform.LocalContext
|
|||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.res.vectorResource
|
import androidx.compose.ui.res.vectorResource
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import com.dokar.sonner.ToastType
|
|
||||||
import com.zaneschepke.wireguardautotunnel.BuildConfig
|
import com.zaneschepke.wireguardautotunnel.BuildConfig
|
||||||
import com.zaneschepke.wireguardautotunnel.R
|
import com.zaneschepke.wireguardautotunnel.R
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.sideeffect.GlobalSideEffect
|
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.LocalIsAndroidTV
|
import com.zaneschepke.wireguardautotunnel.ui.LocalIsAndroidTV
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.LocalNavController
|
import com.zaneschepke.wireguardautotunnel.ui.LocalNavController
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.common.button.SurfaceRow
|
import com.zaneschepke.wireguardautotunnel.ui.common.button.SurfaceRow
|
||||||
@@ -52,13 +49,12 @@ import com.zaneschepke.wireguardautotunnel.ui.navigation.Route
|
|||||||
import com.zaneschepke.wireguardautotunnel.ui.screens.support.components.PermissionDialog
|
import com.zaneschepke.wireguardautotunnel.ui.screens.support.components.PermissionDialog
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.screens.support.components.UpdateDialog
|
import com.zaneschepke.wireguardautotunnel.ui.screens.support.components.UpdateDialog
|
||||||
import com.zaneschepke.wireguardautotunnel.util.Constants
|
import com.zaneschepke.wireguardautotunnel.util.Constants
|
||||||
import com.zaneschepke.wireguardautotunnel.util.StringValue
|
|
||||||
import com.zaneschepke.wireguardautotunnel.util.extensions.launchPlayStoreListing
|
import com.zaneschepke.wireguardautotunnel.util.extensions.launchPlayStoreListing
|
||||||
import com.zaneschepke.wireguardautotunnel.util.extensions.launchPlayStoreReview
|
import com.zaneschepke.wireguardautotunnel.util.extensions.launchPlayStoreReview
|
||||||
import com.zaneschepke.wireguardautotunnel.util.extensions.launchSupportEmail
|
import com.zaneschepke.wireguardautotunnel.util.extensions.launchSupportEmail
|
||||||
import com.zaneschepke.wireguardautotunnel.util.extensions.openWebUrl
|
import com.zaneschepke.wireguardautotunnel.util.extensions.openWebUrl
|
||||||
|
import com.zaneschepke.wireguardautotunnel.util.extensions.showToast
|
||||||
import com.zaneschepke.wireguardautotunnel.viewmodel.SupportViewModel
|
import com.zaneschepke.wireguardautotunnel.viewmodel.SupportViewModel
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import org.koin.androidx.compose.koinViewModel
|
import org.koin.androidx.compose.koinViewModel
|
||||||
import org.orbitmvi.orbit.compose.collectAsState
|
import org.orbitmvi.orbit.compose.collectAsState
|
||||||
|
|
||||||
@@ -67,24 +63,11 @@ fun SupportScreen(viewModel: SupportViewModel = koinViewModel()) {
|
|||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val navController = LocalNavController.current
|
val navController = LocalNavController.current
|
||||||
val isTv = LocalIsAndroidTV.current
|
val isTv = LocalIsAndroidTV.current
|
||||||
val scope = rememberCoroutineScope()
|
|
||||||
|
|
||||||
val supportState by viewModel.collectAsState()
|
val supportState by viewModel.collectAsState()
|
||||||
|
|
||||||
val clipboardManager = rememberClipboardHelper()
|
val clipboardManager = rememberClipboardHelper()
|
||||||
|
|
||||||
val issuesUrl = stringResource(R.string.github_url)
|
|
||||||
val izzyUrl = stringResource(R.string.fdroid_url)
|
|
||||||
val telegramUrl = stringResource(R.string.telegram_url)
|
|
||||||
val matrixUrl = stringResource(R.string.matrix_url)
|
|
||||||
val docsUrl = stringResource(R.string.docs_url)
|
|
||||||
val websiteUrl = stringResource(R.string.website_url)
|
|
||||||
val translationUrl = stringResource(R.string.translation_url)
|
|
||||||
val privacyPolicyUrl = stringResource(R.string.privacy_policy_url)
|
|
||||||
val playStoreUrl = "https://play.google.com/store/apps/details?id=${context.packageName}"
|
|
||||||
val playReviewsUrl =
|
|
||||||
"https://play.google.com/store/apps/details?id=${context.packageName}&showAllReviews=true"
|
|
||||||
|
|
||||||
val version = remember {
|
val version = remember {
|
||||||
"v${BuildConfig.VERSION_NAME +
|
"v${BuildConfig.VERSION_NAME +
|
||||||
if(BuildConfig.DEBUG) "-debug" else "" }"
|
if(BuildConfig.DEBUG) "-debug" else "" }"
|
||||||
@@ -112,19 +95,6 @@ fun SupportScreen(viewModel: SupportViewModel = koinViewModel()) {
|
|||||||
PermissionDialog(context = context, onDismiss = { showPermissionDialog = false })
|
PermissionDialog(context = context, onDismiss = { showPermissionDialog = false })
|
||||||
}
|
}
|
||||||
|
|
||||||
fun openWebUrl(url: String) {
|
|
||||||
context.openWebUrl(url).onFailure {
|
|
||||||
scope.launch {
|
|
||||||
viewModel.postSideEffect(
|
|
||||||
GlobalSideEffect.Snackbar(
|
|
||||||
StringValue.StringResource(R.string.no_browser_detected),
|
|
||||||
ToastType.Error,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier.fillMaxSize().verticalScroll(rememberScrollState()),
|
modifier = Modifier.fillMaxSize().verticalScroll(rememberScrollState()),
|
||||||
horizontalAlignment = Alignment.Start,
|
horizontalAlignment = Alignment.Start,
|
||||||
@@ -145,19 +115,19 @@ fun SupportScreen(viewModel: SupportViewModel = koinViewModel()) {
|
|||||||
)
|
)
|
||||||
SurfaceRow(
|
SurfaceRow(
|
||||||
stringResource(R.string.docs_description),
|
stringResource(R.string.docs_description),
|
||||||
onClick = { openWebUrl(docsUrl) },
|
onClick = { context.openWebUrl(context.getString(R.string.docs_url)) },
|
||||||
leading = { Icon(Icons.Outlined.Book, contentDescription = null) },
|
leading = { Icon(Icons.Outlined.Book, contentDescription = null) },
|
||||||
trailing = { Icon(Icons.AutoMirrored.Outlined.Launch, null) },
|
trailing = { Icon(Icons.AutoMirrored.Outlined.Launch, null) },
|
||||||
)
|
)
|
||||||
SurfaceRow(
|
SurfaceRow(
|
||||||
stringResource(R.string.website),
|
stringResource(R.string.website),
|
||||||
onClick = { openWebUrl(websiteUrl) },
|
onClick = { context.openWebUrl(context.getString(R.string.website_url)) },
|
||||||
leading = { Icon(Icons.Outlined.Web, contentDescription = null) },
|
leading = { Icon(Icons.Outlined.Web, contentDescription = null) },
|
||||||
trailing = { Icon(Icons.AutoMirrored.Outlined.Launch, null) },
|
trailing = { Icon(Icons.AutoMirrored.Outlined.Launch, null) },
|
||||||
)
|
)
|
||||||
SurfaceRow(
|
SurfaceRow(
|
||||||
stringResource(R.string.translation),
|
stringResource(R.string.translation),
|
||||||
onClick = { openWebUrl(translationUrl) },
|
onClick = { context.openWebUrl(context.getString(R.string.translation_url)) },
|
||||||
description = { DescriptionText(stringResource(R.string.help_translate)) },
|
description = { DescriptionText(stringResource(R.string.help_translate)) },
|
||||||
leading = { Icon(Icons.Outlined.Translate, contentDescription = null) },
|
leading = { Icon(Icons.Outlined.Translate, contentDescription = null) },
|
||||||
trailing = { Icon(Icons.AutoMirrored.Outlined.Launch, null) },
|
trailing = { Icon(Icons.AutoMirrored.Outlined.Launch, null) },
|
||||||
@@ -171,16 +141,14 @@ fun SupportScreen(viewModel: SupportViewModel = koinViewModel()) {
|
|||||||
leading = { Icon(Icons.Outlined.Policy, contentDescription = null) },
|
leading = { Icon(Icons.Outlined.Policy, contentDescription = null) },
|
||||||
title = stringResource(R.string.privacy_policy),
|
title = stringResource(R.string.privacy_policy),
|
||||||
trailing = { Icon(Icons.AutoMirrored.Outlined.Launch, null) },
|
trailing = { Icon(Icons.AutoMirrored.Outlined.Launch, null) },
|
||||||
onClick = { openWebUrl(privacyPolicyUrl) },
|
onClick = { context.openWebUrl(context.getString(R.string.privacy_policy_url)) },
|
||||||
)
|
)
|
||||||
if (BuildConfig.FLAVOR == Constants.GOOGLE_PLAY_FLAVOR) {
|
if (BuildConfig.FLAVOR == Constants.GOOGLE_PLAY_FLAVOR) {
|
||||||
SurfaceRow(
|
SurfaceRow(
|
||||||
leading = { Icon(Icons.Outlined.Reviews, contentDescription = null) },
|
leading = { Icon(Icons.Outlined.Reviews, contentDescription = null) },
|
||||||
title = stringResource(R.string.review),
|
title = stringResource(R.string.review),
|
||||||
trailing = { Icon(Icons.AutoMirrored.Outlined.Launch, null) },
|
trailing = { Icon(Icons.AutoMirrored.Outlined.Launch, null) },
|
||||||
onClick = {
|
onClick = { context.launchPlayStoreReview() },
|
||||||
context.launchPlayStoreReview().onFailure { openWebUrl(playReviewsUrl) }
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -199,7 +167,7 @@ fun SupportScreen(viewModel: SupportViewModel = koinViewModel()) {
|
|||||||
},
|
},
|
||||||
title = stringResource(R.string.join_matrix),
|
title = stringResource(R.string.join_matrix),
|
||||||
trailing = { Icon(Icons.AutoMirrored.Outlined.Launch, null) },
|
trailing = { Icon(Icons.AutoMirrored.Outlined.Launch, null) },
|
||||||
onClick = { openWebUrl(matrixUrl) },
|
onClick = { context.openWebUrl(context.getString(R.string.matrix_url)) },
|
||||||
)
|
)
|
||||||
SurfaceRow(
|
SurfaceRow(
|
||||||
leading = {
|
leading = {
|
||||||
@@ -211,7 +179,7 @@ fun SupportScreen(viewModel: SupportViewModel = koinViewModel()) {
|
|||||||
},
|
},
|
||||||
title = stringResource(R.string.join_telegram),
|
title = stringResource(R.string.join_telegram),
|
||||||
trailing = { Icon(Icons.AutoMirrored.Outlined.Launch, null) },
|
trailing = { Icon(Icons.AutoMirrored.Outlined.Launch, null) },
|
||||||
onClick = { openWebUrl(telegramUrl) },
|
onClick = { context.openWebUrl(context.getString(R.string.telegram_url)) },
|
||||||
)
|
)
|
||||||
SurfaceRow(
|
SurfaceRow(
|
||||||
leading = {
|
leading = {
|
||||||
@@ -223,24 +191,13 @@ fun SupportScreen(viewModel: SupportViewModel = koinViewModel()) {
|
|||||||
},
|
},
|
||||||
title = stringResource(R.string.open_issue),
|
title = stringResource(R.string.open_issue),
|
||||||
trailing = { Icon(Icons.AutoMirrored.Outlined.Launch, null) },
|
trailing = { Icon(Icons.AutoMirrored.Outlined.Launch, null) },
|
||||||
onClick = { openWebUrl(issuesUrl) },
|
onClick = { context.openWebUrl(context.getString(R.string.github_url)) },
|
||||||
)
|
)
|
||||||
SurfaceRow(
|
SurfaceRow(
|
||||||
leading = { Icon(Icons.Outlined.Mail, contentDescription = null) },
|
leading = { Icon(Icons.Outlined.Mail, contentDescription = null) },
|
||||||
title = stringResource(R.string.email_description),
|
title = stringResource(R.string.email_description),
|
||||||
trailing = { Icon(Icons.AutoMirrored.Outlined.Launch, null) },
|
trailing = { Icon(Icons.AutoMirrored.Outlined.Launch, null) },
|
||||||
onClick = {
|
onClick = { context.launchSupportEmail() },
|
||||||
context.launchSupportEmail().onFailure {
|
|
||||||
scope.launch {
|
|
||||||
viewModel.postSideEffect(
|
|
||||||
GlobalSideEffect.Snackbar(
|
|
||||||
StringValue.StringResource(R.string.no_email_detected),
|
|
||||||
ToastType.Error,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
Column {
|
Column {
|
||||||
@@ -265,21 +222,12 @@ fun SupportScreen(viewModel: SupportViewModel = koinViewModel()) {
|
|||||||
leading = { Icon(Icons.Outlined.InstallMobile, contentDescription = null) },
|
leading = { Icon(Icons.Outlined.InstallMobile, contentDescription = null) },
|
||||||
title = stringResource(R.string.check_for_update),
|
title = stringResource(R.string.check_for_update),
|
||||||
onClick = {
|
onClick = {
|
||||||
if (BuildConfig.DEBUG) {
|
if (BuildConfig.DEBUG)
|
||||||
scope.launch {
|
return@SurfaceRow context.showToast(R.string.update_check_unsupported)
|
||||||
viewModel.postSideEffect(
|
|
||||||
GlobalSideEffect.Snackbar(
|
|
||||||
StringValue.StringResource(R.string.update_check_unsupported),
|
|
||||||
ToastType.Warning,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
return@SurfaceRow
|
|
||||||
}
|
|
||||||
when (BuildConfig.FLAVOR) {
|
when (BuildConfig.FLAVOR) {
|
||||||
Constants.GOOGLE_PLAY_FLAVOR ->
|
Constants.GOOGLE_PLAY_FLAVOR -> context.launchPlayStoreListing()
|
||||||
context.launchPlayStoreListing().onFailure { openWebUrl(playStoreUrl) }
|
Constants.FDROID_FLAVOR ->
|
||||||
Constants.FDROID_FLAVOR -> openWebUrl(izzyUrl)
|
context.openWebUrl(context.getString(R.string.fdroid_url))
|
||||||
else -> viewModel.checkForStandaloneUpdate()
|
else -> viewModel.checkForStandaloneUpdate()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
+1
-10
@@ -1,12 +1,10 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.ui.screens.support.donate.crypto
|
package com.zaneschepke.wireguardautotunnel.ui.screens.support.donate.crypto
|
||||||
|
|
||||||
import androidx.compose.foundation.gestures.Orientation
|
|
||||||
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.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.foundation.rememberScrollState
|
||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.material3.scrollbar
|
|
||||||
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
|
||||||
@@ -15,17 +13,10 @@ import com.zaneschepke.wireguardautotunnel.ui.screens.support.donate.crypto.comp
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun AddressesScreen() {
|
fun AddressesScreen() {
|
||||||
val scrollState = rememberScrollState()
|
|
||||||
Column(
|
Column(
|
||||||
horizontalAlignment = Alignment.Start,
|
horizontalAlignment = Alignment.Start,
|
||||||
verticalArrangement = Arrangement.Top,
|
verticalArrangement = Arrangement.Top,
|
||||||
modifier =
|
modifier = Modifier.fillMaxSize().verticalScroll(rememberScrollState()),
|
||||||
Modifier.fillMaxSize()
|
|
||||||
.verticalScroll(scrollState)
|
|
||||||
.scrollbar(
|
|
||||||
state = scrollState.scrollIndicatorState,
|
|
||||||
orientation = Orientation.Vertical,
|
|
||||||
),
|
|
||||||
) {
|
) {
|
||||||
val clipboard = rememberClipboardHelper()
|
val clipboard = rememberClipboardHelper()
|
||||||
Address.allAddresses.forEach { AddressItem(it) { address -> clipboard.copy(address) } }
|
Address.allAddresses.forEach { AddressItem(it) { address -> clipboard.copy(address) } }
|
||||||
|
|||||||
+1
-13
@@ -2,7 +2,6 @@ package com.zaneschepke.wireguardautotunnel.ui.screens.support.license.component
|
|||||||
|
|
||||||
import LicenseFileEntry
|
import LicenseFileEntry
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.gestures.Orientation
|
|
||||||
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.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
@@ -10,13 +9,11 @@ import androidx.compose.foundation.layout.fillMaxSize
|
|||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.items
|
||||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.automirrored.outlined.Launch
|
import androidx.compose.material.icons.automirrored.outlined.Launch
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.scrollbar
|
|
||||||
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
|
||||||
@@ -27,17 +24,8 @@ import com.zaneschepke.wireguardautotunnel.util.extensions.openWebUrl
|
|||||||
@Composable
|
@Composable
|
||||||
fun LicenseList(licenses: List<LicenseFileEntry>) {
|
fun LicenseList(licenses: List<LicenseFileEntry>) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val lazyListState = rememberLazyListState()
|
|
||||||
|
|
||||||
LazyColumn(
|
LazyColumn(modifier = Modifier.fillMaxSize()) {
|
||||||
modifier =
|
|
||||||
Modifier.fillMaxSize()
|
|
||||||
.scrollbar(
|
|
||||||
state = lazyListState.scrollIndicatorState,
|
|
||||||
orientation = Orientation.Vertical,
|
|
||||||
),
|
|
||||||
state = lazyListState,
|
|
||||||
) {
|
|
||||||
items(licenses) { entry ->
|
items(licenses) { entry ->
|
||||||
Row(
|
Row(
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
|||||||
+7
-17
@@ -13,7 +13,6 @@ import androidx.compose.ui.Modifier
|
|||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import com.dokar.sonner.ToastType
|
|
||||||
import com.zaneschepke.wireguardautotunnel.R
|
import com.zaneschepke.wireguardautotunnel.R
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.LocalNavController
|
import com.zaneschepke.wireguardautotunnel.ui.LocalNavController
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.common.dialog.InfoDialog
|
import com.zaneschepke.wireguardautotunnel.ui.common.dialog.InfoDialog
|
||||||
@@ -51,15 +50,11 @@ fun TunnelsScreen(sharedViewModel: SharedAppViewModel = koinActivityViewModel())
|
|||||||
rememberFileExportLauncherForResult(
|
rememberFileExportLauncherForResult(
|
||||||
onSuccess = { uri -> sharedViewModel.exportSelectedTunnels(uri) },
|
onSuccess = { uri -> sharedViewModel.exportSelectedTunnels(uri) },
|
||||||
onCanceled = {
|
onCanceled = {
|
||||||
sharedViewModel.showSnackMessage(
|
sharedViewModel.showToast(StringValue.StringResource(R.string.export_canceled))
|
||||||
StringValue.StringResource(R.string.export_canceled),
|
|
||||||
ToastType.Warning,
|
|
||||||
)
|
|
||||||
},
|
},
|
||||||
onUnsupported = {
|
onUnsupported = {
|
||||||
sharedViewModel.showSnackMessage(
|
sharedViewModel.showSnackMessage(
|
||||||
StringValue.StringResource(R.string.export_unsupported),
|
StringValue.StringResource(R.string.export_unsupported)
|
||||||
ToastType.Warning,
|
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@@ -78,8 +73,7 @@ fun TunnelsScreen(sharedViewModel: SharedAppViewModel = koinActivityViewModel())
|
|||||||
selectedTunnelsExportLauncher.launch(fileName)
|
selectedTunnelsExportLauncher.launch(fileName)
|
||||||
} else {
|
} else {
|
||||||
sharedViewModel.showSnackMessage(
|
sharedViewModel.showSnackMessage(
|
||||||
StringValue.StringResource(R.string.error_no_file_explorer),
|
StringValue.StringResource(R.string.error_no_file_explorer)
|
||||||
ToastType.Error,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -93,8 +87,7 @@ fun TunnelsScreen(sharedViewModel: SharedAppViewModel = koinActivityViewModel())
|
|||||||
rememberFileImportLauncherForResult(
|
rememberFileImportLauncherForResult(
|
||||||
onNoFileExplorer = {
|
onNoFileExplorer = {
|
||||||
sharedViewModel.showSnackMessage(
|
sharedViewModel.showSnackMessage(
|
||||||
StringValue.StringResource(R.string.error_no_file_explorer),
|
StringValue.StringResource(R.string.error_no_file_explorer)
|
||||||
ToastType.Error,
|
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
onData = { data -> sharedViewModel.importFromUri(data) },
|
onData = { data -> sharedViewModel.importFromUri(data) },
|
||||||
@@ -108,15 +101,13 @@ fun TunnelsScreen(sharedViewModel: SharedAppViewModel = koinActivityViewModel())
|
|||||||
}
|
}
|
||||||
QRResult.QRMissingPermission -> {
|
QRResult.QRMissingPermission -> {
|
||||||
sharedViewModel.showSnackMessage(
|
sharedViewModel.showSnackMessage(
|
||||||
StringValue.StringResource(R.string.camera_permission_required),
|
StringValue.StringResource(R.string.camera_permission_required)
|
||||||
ToastType.Warning,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
is QRResult.QRSuccess -> {
|
is QRResult.QRSuccess -> {
|
||||||
result.content.rawValue?.let { sharedViewModel.importFromQr(it) }
|
result.content.rawValue?.let { sharedViewModel.importFromQr(it) }
|
||||||
?: sharedViewModel.showSnackMessage(
|
?: sharedViewModel.showSnackMessage(
|
||||||
StringValue.StringResource(R.string.config_error),
|
StringValue.StringResource(R.string.config_error)
|
||||||
ToastType.Error,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
QRResult.QRUserCanceled -> Unit
|
QRResult.QRUserCanceled -> Unit
|
||||||
@@ -128,8 +119,7 @@ fun TunnelsScreen(sharedViewModel: SharedAppViewModel = koinActivityViewModel())
|
|||||||
->
|
->
|
||||||
if (!isGranted) {
|
if (!isGranted) {
|
||||||
sharedViewModel.showSnackMessage(
|
sharedViewModel.showSnackMessage(
|
||||||
StringValue.StringResource(R.string.camera_permission_required),
|
StringValue.StringResource(R.string.camera_permission_required)
|
||||||
ToastType.Warning,
|
|
||||||
)
|
)
|
||||||
return@rememberLauncherForActivityResult
|
return@rememberLauncherForActivityResult
|
||||||
}
|
}
|
||||||
|
|||||||
-3
@@ -42,8 +42,5 @@ fun PeerStatisticsSection(peer: ActivePeer) {
|
|||||||
style = style,
|
style = style,
|
||||||
color = color,
|
color = color,
|
||||||
)
|
)
|
||||||
peer.endpoint?.let {
|
|
||||||
StatText(stringResource(R.string.endpoint_template, it), style = style, color = color)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -42,6 +42,6 @@ private fun TransferMetric(icon: ImageVector, text: String, style: TextStyle, co
|
|||||||
modifier = Modifier.size(12.dp),
|
modifier = Modifier.size(12.dp),
|
||||||
)
|
)
|
||||||
|
|
||||||
Text(text = text, style = style, color = color)
|
Text(text = text.lowercase(), style = style, color = color)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+4
-10
@@ -1,7 +1,6 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.ui.screens.tunnels.components
|
package com.zaneschepke.wireguardautotunnel.ui.screens.tunnels.components
|
||||||
|
|
||||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||||
import androidx.compose.foundation.gestures.Orientation
|
|
||||||
import androidx.compose.foundation.gestures.ScrollableDefaults
|
import androidx.compose.foundation.gestures.ScrollableDefaults
|
||||||
import androidx.compose.foundation.gestures.detectTapGestures
|
import androidx.compose.foundation.gestures.detectTapGestures
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
@@ -13,10 +12,8 @@ import androidx.compose.foundation.rememberOverscrollEffect
|
|||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.rounded.Circle
|
import androidx.compose.material.icons.rounded.Circle
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.scrollbar
|
|
||||||
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.remember
|
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
|
||||||
@@ -69,11 +66,7 @@ fun TunnelList(
|
|||||||
viewModel.clearSelectedTunnels()
|
viewModel.clearSelectedTunnels()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.overscroll(rememberOverscrollEffect())
|
.overscroll(rememberOverscrollEffect()),
|
||||||
.scrollbar(
|
|
||||||
state = lazyListState.scrollIndicatorState,
|
|
||||||
orientation = Orientation.Vertical,
|
|
||||||
),
|
|
||||||
state = lazyListState,
|
state = lazyListState,
|
||||||
userScrollEnabled = true,
|
userScrollEnabled = true,
|
||||||
reverseLayout = false,
|
reverseLayout = false,
|
||||||
@@ -94,7 +87,8 @@ fun TunnelList(
|
|||||||
uiState.backendStatus.activeTunnels[tunnel.id] ?: ActiveTunnel()
|
uiState.backendStatus.activeTunnels[tunnel.id] ?: ActiveTunnel()
|
||||||
}
|
}
|
||||||
|
|
||||||
val displayState = remember(activeTunnel) { DisplayTunnelState.from(activeTunnel) }
|
val displayState =
|
||||||
|
uiState.displayStates[tunnel.id] ?: DisplayTunnelState.from(activeTunnel)
|
||||||
|
|
||||||
val isRunning = uiState.backendStatus.activeTunnels.containsKey(tunnel.id)
|
val isRunning = uiState.backendStatus.activeTunnels.containsKey(tunnel.id)
|
||||||
|
|
||||||
@@ -113,7 +107,7 @@ fun TunnelList(
|
|||||||
Icon(
|
Icon(
|
||||||
Icons.Rounded.Circle,
|
Icons.Rounded.Circle,
|
||||||
contentDescription = stringResource(R.string.tunnel_monitoring),
|
contentDescription = stringResource(R.string.tunnel_monitoring),
|
||||||
tint = remember(displayState) { displayState.asColor() },
|
tint = displayState.asColor(),
|
||||||
modifier = Modifier.size(14.dp),
|
modifier = Modifier.size(14.dp),
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
|||||||
+29
-27
@@ -141,10 +141,6 @@ fun TunnelSettingsScreen(
|
|||||||
onClick = { navController.push(Route.IPv6(tunnel.id)) },
|
onClick = { navController.push(Route.IPv6(tunnel.id)) },
|
||||||
)
|
)
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||||
val meteredDisabled = sharedUiState.tunnelMode == TunnelMode.PROXY
|
|
||||||
val meteredTunnelDesc =
|
|
||||||
if (meteredDisabled) stringResource(R.string.unavailable_in_mode)
|
|
||||||
else stringResource(R.string.metered_tunnel_desc)
|
|
||||||
SurfaceRow(
|
SurfaceRow(
|
||||||
leading = {
|
leading = {
|
||||||
Icon(
|
Icon(
|
||||||
@@ -157,9 +153,15 @@ fun TunnelSettingsScreen(
|
|||||||
},
|
},
|
||||||
title = stringResource(R.string.metered_tunnel),
|
title = stringResource(R.string.metered_tunnel),
|
||||||
enabled = sharedUiState.tunnelMode != TunnelMode.PROXY,
|
enabled = sharedUiState.tunnelMode != TunnelMode.PROXY,
|
||||||
description = {
|
description =
|
||||||
DescriptionText(meteredTunnelDesc, disabled = meteredDisabled)
|
if (sharedUiState.tunnelMode == TunnelMode.PROXY) {
|
||||||
},
|
{
|
||||||
|
DescriptionText(
|
||||||
|
stringResource(R.string.unavailable_in_mode),
|
||||||
|
disabled = true,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else null,
|
||||||
trailing = {
|
trailing = {
|
||||||
ThemedSwitch(
|
ThemedSwitch(
|
||||||
checked = tunnel.isMetered,
|
checked = tunnel.isMetered,
|
||||||
@@ -170,26 +172,26 @@ fun TunnelSettingsScreen(
|
|||||||
onClick = { viewModel.onMetered(!tunnel.isMetered) },
|
onClick = { viewModel.onMetered(!tunnel.isMetered) },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
Column {
|
||||||
Column {
|
GroupLabel(
|
||||||
GroupLabel(
|
stringResource(R.string.automation),
|
||||||
stringResource(R.string.automation),
|
modifier = Modifier.padding(horizontal = 16.dp),
|
||||||
modifier = Modifier.padding(horizontal = 16.dp),
|
)
|
||||||
)
|
SurfaceRow(
|
||||||
SurfaceRow(
|
leading = { Icon(Icons.Outlined.Dns, contentDescription = null) },
|
||||||
leading = { Icon(Icons.Outlined.Dns, contentDescription = null) },
|
title = stringResource(R.string.ddns_auto_update),
|
||||||
title = stringResource(R.string.ddns_auto_update),
|
description = {
|
||||||
description = {
|
DescriptionText(stringResource(R.string.ddns_auto_update_description))
|
||||||
DescriptionText(stringResource(R.string.ddns_auto_update_description))
|
},
|
||||||
},
|
trailing = {
|
||||||
trailing = {
|
ThemedSwitch(
|
||||||
ThemedSwitch(
|
checked = tunnel.dynamicDnsEnabled,
|
||||||
checked = tunnel.dynamicDnsEnabled,
|
onClick = { viewModel.onDynamicDns(it) },
|
||||||
onClick = { viewModel.onDynamicDns(it) },
|
)
|
||||||
)
|
},
|
||||||
},
|
onClick = { viewModel.onDynamicDns(!tunnel.dynamicDnsEnabled) },
|
||||||
onClick = { viewModel.onDynamicDns(!tunnel.dynamicDnsEnabled) },
|
)
|
||||||
)
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-20
@@ -1,6 +1,5 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.ui.screens.tunnels.settings.config
|
package com.zaneschepke.wireguardautotunnel.ui.screens.tunnels.settings.config
|
||||||
|
|
||||||
import androidx.compose.foundation.gestures.Orientation
|
|
||||||
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.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
@@ -10,7 +9,6 @@ import androidx.compose.foundation.text.selection.SelectionContainer
|
|||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.scrollbar
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.derivedStateOf
|
import androidx.compose.runtime.derivedStateOf
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
@@ -28,16 +26,14 @@ import androidx.compose.ui.text.font.FontFamily
|
|||||||
import androidx.compose.ui.text.font.FontStyle
|
import androidx.compose.ui.text.font.FontStyle
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import com.dokar.sonner.ToastType
|
|
||||||
import com.zaneschepke.wireguardautotunnel.R
|
import com.zaneschepke.wireguardautotunnel.R
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.sideeffect.GlobalSideEffect
|
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.common.functions.rememberClipboardHelper
|
import com.zaneschepke.wireguardautotunnel.ui.common.functions.rememberClipboardHelper
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.screens.tunnels.settings.config.components.QrCodeDialog
|
import com.zaneschepke.wireguardautotunnel.ui.screens.tunnels.settings.config.components.QrCodeDialog
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.sideeffect.LocalSideEffect
|
import com.zaneschepke.wireguardautotunnel.ui.sideeffect.LocalSideEffect
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.theme.ConfigHeaderColor
|
import com.zaneschepke.wireguardautotunnel.ui.theme.ConfigHeaderColor
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.theme.ConfigKeyColor
|
import com.zaneschepke.wireguardautotunnel.ui.theme.ConfigKeyColor
|
||||||
import com.zaneschepke.wireguardautotunnel.util.StringValue
|
|
||||||
import com.zaneschepke.wireguardautotunnel.util.extensions.isTextTooLargeForQr
|
import com.zaneschepke.wireguardautotunnel.util.extensions.isTextTooLargeForQr
|
||||||
|
import com.zaneschepke.wireguardautotunnel.util.extensions.showToast
|
||||||
import com.zaneschepke.wireguardautotunnel.viewmodel.SharedAppViewModel
|
import com.zaneschepke.wireguardautotunnel.viewmodel.SharedAppViewModel
|
||||||
import com.zaneschepke.wireguardautotunnel.viewmodel.TunnelViewModel
|
import com.zaneschepke.wireguardautotunnel.viewmodel.TunnelViewModel
|
||||||
import org.koin.compose.viewmodel.koinActivityViewModel
|
import org.koin.compose.viewmodel.koinActivityViewModel
|
||||||
@@ -61,8 +57,6 @@ fun ConfigScreen(
|
|||||||
|
|
||||||
var showQrModal by rememberSaveable { mutableStateOf(false) }
|
var showQrModal by rememberSaveable { mutableStateOf(false) }
|
||||||
|
|
||||||
val scrollState = rememberScrollState()
|
|
||||||
|
|
||||||
val rawConfig by
|
val rawConfig by
|
||||||
remember(liveConfig, uiState.activeConfig, uiState.tunnel?.quickConfig) {
|
remember(liveConfig, uiState.activeConfig, uiState.tunnel?.quickConfig) {
|
||||||
derivedStateOf {
|
derivedStateOf {
|
||||||
@@ -78,12 +72,7 @@ fun ConfigScreen(
|
|||||||
when (sideEffect) {
|
when (sideEffect) {
|
||||||
is LocalSideEffect.Modal.QR -> {
|
is LocalSideEffect.Modal.QR -> {
|
||||||
if (tunnel.quickConfig.isTextTooLargeForQr()) {
|
if (tunnel.quickConfig.isTextTooLargeForQr()) {
|
||||||
sharedViewModel.postSideEffect(
|
context.showToast(R.string.text_too_large_for_qr)
|
||||||
GlobalSideEffect.Snackbar(
|
|
||||||
StringValue.StringResource(R.string.text_too_large_for_qr),
|
|
||||||
ToastType.Error,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
} else {
|
} else {
|
||||||
showQrModal = true
|
showQrModal = true
|
||||||
}
|
}
|
||||||
@@ -101,13 +90,7 @@ fun ConfigScreen(
|
|||||||
Column(
|
Column(
|
||||||
horizontalAlignment = Alignment.Start,
|
horizontalAlignment = Alignment.Start,
|
||||||
verticalArrangement = Arrangement.spacedBy(12.dp, Alignment.Top),
|
verticalArrangement = Arrangement.spacedBy(12.dp, Alignment.Top),
|
||||||
modifier =
|
modifier = Modifier.fillMaxSize().verticalScroll(rememberScrollState()),
|
||||||
Modifier.fillMaxSize()
|
|
||||||
.verticalScroll(scrollState)
|
|
||||||
.scrollbar(
|
|
||||||
state = scrollState.scrollIndicatorState,
|
|
||||||
orientation = Orientation.Vertical,
|
|
||||||
),
|
|
||||||
) {
|
) {
|
||||||
val displayText by
|
val displayText by
|
||||||
remember(rawConfig, showKeys) { derivedStateOf { maskSensitive(rawConfig, showKeys) } }
|
remember(rawConfig, showKeys) { derivedStateOf { maskSensitive(rawConfig, showKeys) } }
|
||||||
|
|||||||
+4
-14
@@ -1,6 +1,5 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.ui.screens.tunnels.settings.config.edit
|
package com.zaneschepke.wireguardautotunnel.ui.screens.tunnels.settings.config.edit
|
||||||
|
|
||||||
import androidx.compose.foundation.gestures.Orientation
|
|
||||||
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.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
@@ -13,7 +12,6 @@ import androidx.compose.material.icons.Icons
|
|||||||
import androidx.compose.material.icons.outlined.HdrAuto
|
import androidx.compose.material.icons.outlined.HdrAuto
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.scrollbar
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
@@ -51,12 +49,11 @@ fun ConfigEditScreen(
|
|||||||
val uiState by viewModel.collectAsState()
|
val uiState by viewModel.collectAsState()
|
||||||
|
|
||||||
if (uiState.isLoading) return
|
if (uiState.isLoading) return
|
||||||
|
|
||||||
val locale = Locale.current.platformLocale
|
val locale = Locale.current.platformLocale
|
||||||
|
|
||||||
var showSelectionDialog by rememberSaveable { mutableStateOf(false) }
|
var showSelectionDialog by rememberSaveable { mutableStateOf(false) }
|
||||||
|
|
||||||
val scrollState = rememberScrollState()
|
|
||||||
|
|
||||||
sharedViewModel.collectSideEffect { sideEffect ->
|
sharedViewModel.collectSideEffect { sideEffect ->
|
||||||
when (sideEffect) {
|
when (sideEffect) {
|
||||||
is LocalSideEffect.SaveChanges -> {
|
is LocalSideEffect.SaveChanges -> {
|
||||||
@@ -107,14 +104,7 @@ fun ConfigEditScreen(
|
|||||||
Column(
|
Column(
|
||||||
horizontalAlignment = Alignment.Start,
|
horizontalAlignment = Alignment.Start,
|
||||||
verticalArrangement = Arrangement.spacedBy(16.dp, Alignment.Top),
|
verticalArrangement = Arrangement.spacedBy(16.dp, Alignment.Top),
|
||||||
modifier =
|
modifier = Modifier.fillMaxSize().imePadding().verticalScroll(rememberScrollState()),
|
||||||
Modifier.fillMaxSize()
|
|
||||||
.imePadding()
|
|
||||||
.verticalScroll(scrollState)
|
|
||||||
.scrollbar(
|
|
||||||
state = scrollState.scrollIndicatorState,
|
|
||||||
orientation = Orientation.Vertical,
|
|
||||||
),
|
|
||||||
) {
|
) {
|
||||||
if (uiState.isGlobalConfig) {
|
if (uiState.isGlobalConfig) {
|
||||||
Column {
|
Column {
|
||||||
@@ -122,7 +112,7 @@ fun ConfigEditScreen(
|
|||||||
leading = {
|
leading = {
|
||||||
Icon(ImageVector.vectorResource(R.drawable.host), contentDescription = null)
|
Icon(ImageVector.vectorResource(R.drawable.host), contentDescription = null)
|
||||||
},
|
},
|
||||||
title = stringResource(R.string.dns_servers),
|
title = stringResource(R.string.global_dns_servers),
|
||||||
trailing = { modifier ->
|
trailing = { modifier ->
|
||||||
ThemedSwitch(
|
ThemedSwitch(
|
||||||
checked = uiState.globalSettings.dnsEnabled,
|
checked = uiState.globalSettings.dnsEnabled,
|
||||||
@@ -136,7 +126,7 @@ fun ConfigEditScreen(
|
|||||||
)
|
)
|
||||||
SurfaceRow(
|
SurfaceRow(
|
||||||
leading = { Icon(Icons.Outlined.HdrAuto, contentDescription = null) },
|
leading = { Icon(Icons.Outlined.HdrAuto, contentDescription = null) },
|
||||||
title = stringResource(R.string.amnezia_configuration),
|
title = stringResource(R.string.global_amnezia_configuration),
|
||||||
trailing = { modifier ->
|
trailing = { modifier ->
|
||||||
ThemedSwitch(
|
ThemedSwitch(
|
||||||
checked = uiState.globalSettings.amneziaEnabled,
|
checked = uiState.globalSettings.amneziaEnabled,
|
||||||
|
|||||||
+1
-7
@@ -1,6 +1,5 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.ui.screens.tunnels.sort
|
package com.zaneschepke.wireguardautotunnel.ui.screens.tunnels.sort
|
||||||
|
|
||||||
import androidx.compose.foundation.gestures.Orientation
|
|
||||||
import androidx.compose.foundation.gestures.ScrollableDefaults
|
import androidx.compose.foundation.gestures.ScrollableDefaults
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
@@ -18,7 +17,6 @@ import androidx.compose.material.icons.filled.ArrowUpward
|
|||||||
import androidx.compose.material.icons.filled.DragHandle
|
import androidx.compose.material.icons.filled.DragHandle
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.IconButton
|
import androidx.compose.material3.IconButton
|
||||||
import androidx.compose.material3.scrollbar
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
@@ -109,11 +107,7 @@ fun SortScreen(sharedViewModel: SharedAppViewModel = koinActivityViewModel()) {
|
|||||||
Modifier.pointerInput(Unit) {
|
Modifier.pointerInput(Unit) {
|
||||||
if (tunnelsUiState.tunnels.isEmpty()) return@pointerInput
|
if (tunnelsUiState.tunnels.isEmpty()) return@pointerInput
|
||||||
}
|
}
|
||||||
.overscroll(rememberOverscrollEffect())
|
.overscroll(rememberOverscrollEffect()),
|
||||||
.scrollbar(
|
|
||||||
state = lazyListState.scrollIndicatorState,
|
|
||||||
orientation = Orientation.Vertical,
|
|
||||||
),
|
|
||||||
state = lazyListState,
|
state = lazyListState,
|
||||||
userScrollEnabled = true,
|
userScrollEnabled = true,
|
||||||
reverseLayout = false,
|
reverseLayout = false,
|
||||||
|
|||||||
+51
-18
@@ -26,33 +26,38 @@ sealed class DisplayTunnelState {
|
|||||||
|
|
||||||
data object Connected : DisplayTunnelState()
|
data object Connected : DisplayTunnelState()
|
||||||
|
|
||||||
data object HandshakeFailure : DisplayTunnelState()
|
data object Degraded : DisplayTunnelState()
|
||||||
|
|
||||||
@StringRes
|
@StringRes
|
||||||
fun labelRes(): Int =
|
fun labelRes(): Int {
|
||||||
when (this) {
|
return when (this) {
|
||||||
Disconnected -> R.string.tunnel_state_disconnected
|
Disconnected -> R.string.tunnel_state_disconnected
|
||||||
Connecting,
|
Connecting -> R.string.tunnel_state_starting
|
||||||
EstablishingConnection -> R.string.tunnel_state_establishing_connection
|
|
||||||
ResolvingDns -> R.string.tunnel_state_resolving_dns
|
ResolvingDns -> R.string.tunnel_state_resolving_dns
|
||||||
|
EstablishingConnection -> R.string.tunnel_state_establishing_connection
|
||||||
Ready -> R.string.ready
|
Ready -> R.string.ready
|
||||||
Connected -> R.string.tunnel_state_connected
|
Connected -> R.string.tunnel_state_connected
|
||||||
HandshakeFailure -> R.string.tunnel_state_handshake_failure
|
Degraded -> R.string.tunnel_state_handshake_failure
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun asColor(): Color =
|
fun asLocalizedString(context: Context): String {
|
||||||
when (this) {
|
return context.getString(labelRes())
|
||||||
|
}
|
||||||
|
|
||||||
|
fun asColor(): Color {
|
||||||
|
return when (this) {
|
||||||
Disconnected -> CoolGray
|
Disconnected -> CoolGray
|
||||||
|
|
||||||
Connecting,
|
Connecting,
|
||||||
ResolvingDns,
|
ResolvingDns,
|
||||||
EstablishingConnection,
|
EstablishingConnection,
|
||||||
Ready -> Straw
|
Ready -> Straw
|
||||||
Connected -> SilverTree
|
|
||||||
HandshakeFailure -> AlertRed
|
|
||||||
}
|
|
||||||
|
|
||||||
fun asLocalizedString(context: Context): String {
|
Connected -> SilverTree
|
||||||
return context.getString(labelRes())
|
|
||||||
|
Degraded -> AlertRed
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
@@ -62,21 +67,49 @@ sealed class DisplayTunnelState {
|
|||||||
val mode = activeTunnel.mode
|
val mode = activeTunnel.mode
|
||||||
val isVpnStyle = mode is BackendMode.Vpn || mode is BackendMode.Proxy.KillSwitchPrimary
|
val isVpnStyle = mode is BackendMode.Vpn || mode is BackendMode.Proxy.KillSwitchPrimary
|
||||||
|
|
||||||
|
// Static peers bootstrap never goes to complete, treat none the same
|
||||||
|
val bootstrapPhaseDone =
|
||||||
|
bootstrap is BootstrapState.Complete || bootstrap is BootstrapState.None
|
||||||
|
|
||||||
return when {
|
return when {
|
||||||
transport is Tunnel.State.Down -> Disconnected
|
transport is Tunnel.State.Down -> Disconnected
|
||||||
bootstrap is BootstrapState.Failed -> HandshakeFailure
|
|
||||||
|
|
||||||
|
bootstrap is BootstrapState.Failed -> Degraded
|
||||||
|
|
||||||
|
// DNS resolution still in progress
|
||||||
bootstrap is BootstrapState.ResolvingDns ||
|
bootstrap is BootstrapState.ResolvingDns ||
|
||||||
bootstrap is BootstrapState.UpdatingPeers -> ResolvingDns
|
bootstrap is BootstrapState.UpdatingPeers -> ResolvingDns
|
||||||
|
|
||||||
transport is Tunnel.State.Up.Healthy -> Connected
|
transport is Tunnel.State.Up.Healthy -> Connected
|
||||||
|
|
||||||
transport is Tunnel.State.Up.HandshakeFailure -> HandshakeFailure
|
transport is Tunnel.State.Up.HandshakeFailure -> {
|
||||||
|
val age = System.currentTimeMillis() - activeTunnel.lastStateChangeMs
|
||||||
|
|
||||||
transport is Tunnel.State.Starting ->
|
if (age > 15_000L && bootstrapPhaseDone) {
|
||||||
if (isVpnStyle) EstablishingConnection else Ready
|
Degraded
|
||||||
|
} else if (isVpnStyle && bootstrapPhaseDone) {
|
||||||
|
EstablishingConnection
|
||||||
|
} else if (bootstrapPhaseDone) {
|
||||||
|
// For regular proxy mode, we go to ready once past bootstrap phase
|
||||||
|
Ready
|
||||||
|
} else {
|
||||||
|
Connecting
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
else -> if (isVpnStyle) EstablishingConnection else Ready
|
transport is Tunnel.State.Starting -> {
|
||||||
|
when {
|
||||||
|
bootstrapPhaseDone -> {
|
||||||
|
if (isVpnStyle) EstablishingConnection else Ready
|
||||||
|
}
|
||||||
|
else -> Connecting
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Final fallback after bootstrap phase is done
|
||||||
|
bootstrapPhaseDone -> if (isVpnStyle) EstablishingConnection else Ready
|
||||||
|
|
||||||
|
else -> Connecting
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +18,5 @@ data class GlobalAppUiState(
|
|||||||
val selectedTunnelCount: Int = 0,
|
val selectedTunnelCount: Int = 0,
|
||||||
val alreadyDonated: Boolean = false,
|
val alreadyDonated: Boolean = false,
|
||||||
val isPinVerified: Boolean = false,
|
val isPinVerified: Boolean = false,
|
||||||
val pendingWgImportUrl: String? = null,
|
|
||||||
val isScreenRecordingProtectionEnabled: Boolean = false,
|
val isScreenRecordingProtectionEnabled: Boolean = false,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -16,14 +16,14 @@ val ElectricTeal = Color(0xFF4DD0E1)
|
|||||||
// Status colors
|
// Status colors
|
||||||
val SilverTree = Color(0xFF6DB58B)
|
val SilverTree = Color(0xFF6DB58B)
|
||||||
val AlertRed = Color(0xFFCF6679)
|
val AlertRed = Color(0xFFCF6679)
|
||||||
|
|
||||||
val Straw = Color(0xFFD4C483)
|
val Straw = Color(0xFFD4C483)
|
||||||
|
|
||||||
val Disabled = CoolGray.copy(alpha = 0.4f)
|
val Disabled = CoolGray.copy(alpha = 0.4f)
|
||||||
|
|
||||||
// Other colors
|
// Config colors
|
||||||
val ConfigHeaderColor = Color(0xFFBB86FC)
|
val ConfigHeaderColor = Color(0xFFBB86FC)
|
||||||
val ConfigKeyColor = Color(0xFF03DAC5)
|
val ConfigKeyColor = Color(0xFF03DAC5)
|
||||||
val Heart = Color(0xFFDB61A2)
|
|
||||||
|
|
||||||
sealed class ThemeColors(
|
sealed class ThemeColors(
|
||||||
val background: Color,
|
val background: Color,
|
||||||
|
|||||||
@@ -6,11 +6,15 @@ object Constants {
|
|||||||
const val BASE_LOG_FILE_NAME = "wg_tunnel_logs"
|
const val BASE_LOG_FILE_NAME = "wg_tunnel_logs"
|
||||||
|
|
||||||
const val VPN_SETTINGS_PACKAGE = "android.net.vpn.SETTINGS"
|
const val VPN_SETTINGS_PACKAGE = "android.net.vpn.SETTINGS"
|
||||||
|
const val SYSTEM_EXEMPT_SERVICE_TYPE_ID = 1 shl 10
|
||||||
const val SPECIAL_USE_SERVICE_TYPE_ID = 1 shl 30
|
const val SPECIAL_USE_SERVICE_TYPE_ID = 1 shl 30
|
||||||
|
|
||||||
|
const val QR_CODE_NAME_PROPERTY = "# Name ="
|
||||||
|
|
||||||
const val FDROID_FLAVOR = "fdroid"
|
const val FDROID_FLAVOR = "fdroid"
|
||||||
const val GOOGLE_PLAY_FLAVOR = "google"
|
const val GOOGLE_PLAY_FLAVOR = "google"
|
||||||
const val STANDALONE_FLAVOR = "standalone"
|
const val STANDALONE_FLAVOR = "standalone"
|
||||||
|
const val RELEASE = "release"
|
||||||
|
|
||||||
const val BASE_RELEASE_URL = "https://github.com/wgtunnel/wgtunnel/releases/tag/"
|
const val BASE_RELEASE_URL = "https://github.com/wgtunnel/wgtunnel/releases/tag/"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -81,7 +81,7 @@ object DnsValidator {
|
|||||||
return Result.Valid
|
return Result.Valid
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun validateUdp(value: String): Result {
|
private fun validateUdp(value: String): DnsValidator.Result {
|
||||||
val parts = value.split(":")
|
val parts = value.split(":")
|
||||||
|
|
||||||
val host = parts.getOrNull(0)?.trim()
|
val host = parts.getOrNull(0)?.trim()
|
||||||
@@ -93,14 +93,14 @@ object DnsValidator {
|
|||||||
|
|
||||||
// basic IP/hostname sanity check
|
// basic IP/hostname sanity check
|
||||||
if (!isValidHostOrIp(host)) {
|
if (!isValidHostOrIp(host)) {
|
||||||
return Result.Invalid(DnsError.InvalidIpOrHost)
|
return DnsValidator.Result.Invalid(DnsError.InvalidIpOrHost)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (port !in 1..65535) {
|
if (port !in 1..65535) {
|
||||||
return Result.Invalid(DnsError.InvalidPort)
|
return DnsValidator.Result.Invalid(DnsError.InvalidPort)
|
||||||
}
|
}
|
||||||
|
|
||||||
return Result.Valid
|
return DnsValidator.Result.Valid
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun isValidHostOrIp(value: String): Boolean {
|
private fun isValidHostOrIp(value: String): Boolean {
|
||||||
|
|||||||
@@ -1,9 +1,22 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.util
|
package com.zaneschepke.wireguardautotunnel.util
|
||||||
|
|
||||||
import com.vdurmont.semver4j.Semver
|
import com.vdurmont.semver4j.Semver
|
||||||
|
import java.math.BigDecimal
|
||||||
|
import kotlin.math.pow
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
|
||||||
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()
|
||||||
|
|
||||||
|
fun bytesToMB(bytes: Long): BigDecimal {
|
||||||
|
return bytes.toBigDecimal().divide(BYTES_IN_MB.toBigDecimal())
|
||||||
|
}
|
||||||
|
|
||||||
|
fun isValidKey(key: String): Boolean {
|
||||||
|
return key.matches(keyValidationRegex)
|
||||||
|
}
|
||||||
|
|
||||||
fun generateRandomTunnelName(): String {
|
fun generateRandomTunnelName(): String {
|
||||||
return "tunnel${randomFive()}"
|
return "tunnel${randomFive()}"
|
||||||
|
|||||||
+108
-14
@@ -1,17 +1,27 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.util.extensions
|
package com.zaneschepke.wireguardautotunnel.util.extensions
|
||||||
|
|
||||||
|
import android.Manifest
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
|
import android.content.ComponentName
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.content.Context.POWER_SERVICE
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.pm.ApplicationInfo
|
import android.content.pm.ApplicationInfo
|
||||||
|
import android.content.pm.PackageInfo
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
|
import android.os.PowerManager
|
||||||
import android.provider.Settings
|
import android.provider.Settings
|
||||||
|
import android.service.quicksettings.TileService
|
||||||
|
import android.widget.Toast
|
||||||
import androidx.core.content.FileProvider
|
import androidx.core.content.FileProvider
|
||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
import com.zaneschepke.wireguardautotunnel.MainActivity
|
import com.zaneschepke.wireguardautotunnel.MainActivity
|
||||||
import com.zaneschepke.wireguardautotunnel.R
|
import com.zaneschepke.wireguardautotunnel.R
|
||||||
|
import com.zaneschepke.wireguardautotunnel.core.service.tile.AutoTunnelControlTile
|
||||||
|
import com.zaneschepke.wireguardautotunnel.core.service.tile.TunnelControlTile
|
||||||
|
import com.zaneschepke.wireguardautotunnel.ui.screens.tunnels.splittunnel.state.TunnelApp
|
||||||
import com.zaneschepke.wireguardautotunnel.util.Constants
|
import com.zaneschepke.wireguardautotunnel.util.Constants
|
||||||
import com.zaneschepke.wireguardautotunnel.util.FileUtils
|
import com.zaneschepke.wireguardautotunnel.util.FileUtils
|
||||||
import java.io.File
|
import java.io.File
|
||||||
@@ -20,11 +30,22 @@ import java.util.Locale
|
|||||||
import kotlin.system.exitProcess
|
import kotlin.system.exitProcess
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
|
||||||
fun Context.openWebUrl(url: String): Result<Unit> = runCatching {
|
fun Context.openWebUrl(url: String): Result<Unit> {
|
||||||
val webpage: Uri = url.toUri()
|
return kotlin
|
||||||
val intent =
|
.runCatching {
|
||||||
Intent(Intent.ACTION_VIEW, webpage).apply { addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) }
|
val webpage: Uri = url.toUri()
|
||||||
startActivity(intent)
|
val intent =
|
||||||
|
Intent(Intent.ACTION_VIEW, webpage).apply {
|
||||||
|
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||||
|
}
|
||||||
|
startActivity(intent)
|
||||||
|
}
|
||||||
|
.onFailure { showToast(R.string.no_browser_detected) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Context.isBatteryOptimizationsDisabled(): Boolean {
|
||||||
|
val pm = getSystemService(POWER_SERVICE) as PowerManager
|
||||||
|
return pm.isIgnoringBatteryOptimizations(packageName)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun Context.launchNotificationSettings() {
|
fun Context.launchNotificationSettings() {
|
||||||
@@ -73,7 +94,30 @@ fun Context.hasSAFSupport(mimeType: String): Boolean {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun Context.launchSupportEmail(): Result<Unit> = runCatching {
|
fun Context.launchShareFile(file: File) {
|
||||||
|
FileProvider.getUriForFile(this, getString(R.string.provider), file)
|
||||||
|
val shareIntent =
|
||||||
|
Intent().apply {
|
||||||
|
action = Intent.ACTION_SEND
|
||||||
|
type = FileUtils.ALL_FILE_TYPES
|
||||||
|
putExtra(Intent.EXTRA_STREAM, file)
|
||||||
|
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||||
|
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||||
|
}
|
||||||
|
val chooserIntent =
|
||||||
|
Intent.createChooser(shareIntent, "").apply { addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) }
|
||||||
|
this.startActivity(chooserIntent)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Context.showToast(resId: Int) {
|
||||||
|
Toast.makeText(this, this.getString(resId), Toast.LENGTH_LONG).show()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Context.showToast(message: String) {
|
||||||
|
Toast.makeText(this, message, Toast.LENGTH_LONG).show()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Context.launchSupportEmail() {
|
||||||
val intent =
|
val intent =
|
||||||
Intent(Intent.ACTION_SENDTO).apply {
|
Intent(Intent.ACTION_SENDTO).apply {
|
||||||
data = "mailto:".toUri()
|
data = "mailto:".toUri()
|
||||||
@@ -88,7 +132,7 @@ fun Context.launchSupportEmail(): Result<Unit> = runCatching {
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
throw IllegalStateException("No email client found")
|
showToast(R.string.no_email_detected)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -99,7 +143,7 @@ fun Context.isRunningOnTv(): Boolean {
|
|||||||
fun Context.launchVpnSettings(): Result<Unit> {
|
fun Context.launchVpnSettings(): Result<Unit> {
|
||||||
return kotlin.runCatching {
|
return kotlin.runCatching {
|
||||||
val intent =
|
val intent =
|
||||||
Intent(Constants.VPN_SETTINGS_PACKAGE).apply { flags = Intent.FLAG_ACTIVITY_NEW_TASK }
|
Intent(Constants.VPN_SETTINGS_PACKAGE).apply { setFlags(Intent.FLAG_ACTIVITY_NEW_TASK) }
|
||||||
startActivity(intent)
|
startActivity(intent)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -118,6 +162,14 @@ fun Context.launchLocationServicesSettings(): Result<Unit> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun Context.launchSettings(): Result<Unit> {
|
||||||
|
return kotlin.runCatching {
|
||||||
|
val intent =
|
||||||
|
Intent(Settings.ACTION_SETTINGS).apply { flags = Intent.FLAG_ACTIVITY_NEW_TASK }
|
||||||
|
startActivity(intent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun Context.launchAppSettings() {
|
fun Context.launchAppSettings() {
|
||||||
kotlin
|
kotlin
|
||||||
.runCatching {
|
.runCatching {
|
||||||
@@ -134,6 +186,48 @@ fun Context.launchAppSettings() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun Context.requestTunnelTileServiceStateUpdate() =
|
||||||
|
runCatching {
|
||||||
|
TileService.requestListeningState(
|
||||||
|
this,
|
||||||
|
ComponentName(this, TunnelControlTile::class.java),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.onFailure { Timber.w(it) }
|
||||||
|
|
||||||
|
fun Context.requestAutoTunnelTileServiceUpdate() =
|
||||||
|
runCatching {
|
||||||
|
TileService.requestListeningState(
|
||||||
|
this,
|
||||||
|
ComponentName(this, AutoTunnelControlTile::class.java),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.onFailure { Timber.w(it) }
|
||||||
|
|
||||||
|
fun Context.getAllInternetCapablePackages(): List<PackageInfo> {
|
||||||
|
val permissions = arrayOf(Manifest.permission.INTERNET)
|
||||||
|
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||||
|
packageManager.getPackagesHoldingPermissions(
|
||||||
|
permissions,
|
||||||
|
PackageManager.PackageInfoFlags.of(0L),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
packageManager.getPackagesHoldingPermissions(permissions, 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Context.getSplitTunnelApps(): List<TunnelApp> {
|
||||||
|
val packages = getAllInternetCapablePackages()
|
||||||
|
return packages
|
||||||
|
.filter { it.applicationInfo != null }
|
||||||
|
.map { pkg ->
|
||||||
|
TunnelApp(
|
||||||
|
packageManager.getApplicationLabel(pkg.applicationInfo!!).toString(),
|
||||||
|
pkg.packageName,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun Context.canInstallPackages(): Boolean {
|
fun Context.canInstallPackages(): Boolean {
|
||||||
return packageManager.canRequestPackageInstalls()
|
return packageManager.canRequestPackageInstalls()
|
||||||
}
|
}
|
||||||
@@ -158,9 +252,9 @@ fun Context.installApk(apkFile: File) {
|
|||||||
startActivity(intent)
|
startActivity(intent)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun Context.launchPlayStoreListing(): Result<Unit> = runCatching {
|
fun Context.launchPlayStoreListing() {
|
||||||
val intent =
|
val intent =
|
||||||
Intent(Intent.ACTION_VIEW, "market://details?id=$packageName".toUri()).apply {
|
Intent(Intent.ACTION_VIEW, Uri.parse("market://details?id=$packageName")).apply {
|
||||||
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||||
setPackage("com.android.vending")
|
setPackage("com.android.vending")
|
||||||
}
|
}
|
||||||
@@ -168,12 +262,12 @@ fun Context.launchPlayStoreListing(): Result<Unit> = runCatching {
|
|||||||
if (intent.resolveActivity(packageManager) != null) {
|
if (intent.resolveActivity(packageManager) != null) {
|
||||||
startActivity(intent)
|
startActivity(intent)
|
||||||
} else {
|
} else {
|
||||||
throw IllegalStateException("Play Store not found")
|
openWebUrl("https://play.google.com/store/apps/details?id=$packageName")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun Context.launchPlayStoreReview(): Result<Unit> = runCatching {
|
fun Context.launchPlayStoreReview() {
|
||||||
val uri = "market://details?id=$packageName&showAllReviews=true".toUri()
|
val uri = Uri.parse("market://details?id=$packageName&showAllReviews=true")
|
||||||
val intent =
|
val intent =
|
||||||
Intent(Intent.ACTION_VIEW, uri).apply {
|
Intent(Intent.ACTION_VIEW, uri).apply {
|
||||||
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||||
@@ -182,7 +276,7 @@ fun Context.launchPlayStoreReview(): Result<Unit> = runCatching {
|
|||||||
if (intent.resolveActivity(packageManager) != null) {
|
if (intent.resolveActivity(packageManager) != null) {
|
||||||
startActivity(intent)
|
startActivity(intent)
|
||||||
} else {
|
} else {
|
||||||
throw IllegalStateException("Play Store not found")
|
openWebUrl("https://play.google.com/store/apps/details?id=$packageName&showAllReviews=true")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+8
@@ -0,0 +1,8 @@
|
|||||||
|
package com.zaneschepke.wireguardautotunnel.util.extensions
|
||||||
|
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
|
|
||||||
|
fun <K, V> Flow<Map<K, V>>.distinctByKeys(): Flow<Map<K, V>> {
|
||||||
|
return distinctUntilChanged { old, new -> old.keys == new.keys }
|
||||||
|
}
|
||||||
@@ -18,6 +18,10 @@ fun <T, R : Comparable<R>> List<T>.isSortedBy(selector: (T) -> R): Boolean {
|
|||||||
return zipWithNext().all { (a, b) -> selector(a) <= selector(b) }
|
return zipWithNext().all { (a, b) -> selector(a) <= selector(b) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun Int.toMillis(): Long {
|
||||||
|
return this * 1_000L
|
||||||
|
}
|
||||||
|
|
||||||
fun Double.round(decimals: Int): Double {
|
fun Double.round(decimals: Int): Double {
|
||||||
val factor = 10.0.pow(decimals)
|
val factor = 10.0.pow(decimals)
|
||||||
return (this * factor).roundToInt() / factor
|
return (this * factor).roundToInt() / factor
|
||||||
|
|||||||
-13
@@ -1,13 +0,0 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.util.extensions
|
|
||||||
|
|
||||||
import io.ktor.client.statement.HttpResponse
|
|
||||||
import io.ktor.client.statement.bodyAsText
|
|
||||||
|
|
||||||
suspend fun HttpResponse.isHtmlResponse(): Boolean {
|
|
||||||
val contentType = headers["Content-Type"] ?: ""
|
|
||||||
if (contentType.contains("text/html", ignoreCase = true)) return true
|
|
||||||
|
|
||||||
val bodyStart = bodyAsText().trimStart()
|
|
||||||
return bodyStart.startsWith("<!DOCTYPE", ignoreCase = true) ||
|
|
||||||
bodyStart.startsWith("<html", ignoreCase = true)
|
|
||||||
}
|
|
||||||
+1
-1
@@ -61,7 +61,7 @@ fun TunnelMode.asTitleString(context: Context): String {
|
|||||||
fun TunnelMode.asString(context: Context): String {
|
fun TunnelMode.asString(context: Context): String {
|
||||||
return when (this) {
|
return when (this) {
|
||||||
TunnelMode.VPN -> context.getString(R.string.vpn)
|
TunnelMode.VPN -> context.getString(R.string.vpn)
|
||||||
TunnelMode.PROXY -> context.getString(R.string.local_proxy)
|
TunnelMode.PROXY -> context.getString(R.string.proxy)
|
||||||
TunnelMode.LOCK_DOWN -> context.getString(R.string.lockdown)
|
TunnelMode.LOCK_DOWN -> context.getString(R.string.lockdown)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
-23
@@ -1,23 +0,0 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.util.permission
|
|
||||||
|
|
||||||
import android.Manifest
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.pm.PackageManager
|
|
||||||
import android.os.Build
|
|
||||||
import androidx.core.content.ContextCompat
|
|
||||||
|
|
||||||
object LocalNetworkPermissionHelper {
|
|
||||||
|
|
||||||
fun shouldRequestPermission(): Boolean {
|
|
||||||
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.CINNAMON_BUN
|
|
||||||
}
|
|
||||||
|
|
||||||
fun isPermissionGranted(context: Context): Boolean {
|
|
||||||
return if (shouldRequestPermission()) {
|
|
||||||
ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_LOCAL_NETWORK) ==
|
|
||||||
PackageManager.PERMISSION_GRANTED
|
|
||||||
} else {
|
|
||||||
true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+10
-29
@@ -2,12 +2,13 @@ package com.zaneschepke.wireguardautotunnel.viewmodel
|
|||||||
|
|
||||||
import androidx.core.content.PermissionChecker.PERMISSION_GRANTED
|
import androidx.core.content.PermissionChecker.PERMISSION_GRANTED
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import com.dokar.sonner.ToastType
|
|
||||||
import com.zaneschepke.networkmonitor.NetworkMonitor
|
import com.zaneschepke.networkmonitor.NetworkMonitor
|
||||||
import com.zaneschepke.networkmonitor.StableNetworkEngine
|
import com.zaneschepke.networkmonitor.StableNetworkEngine
|
||||||
import com.zaneschepke.tunnel.util.RootShell
|
import com.zaneschepke.tunnel.backend.RootShell
|
||||||
import com.zaneschepke.wireguardautotunnel.R
|
import com.zaneschepke.wireguardautotunnel.R
|
||||||
import com.zaneschepke.wireguardautotunnel.core.orchestration.AutoTunnelCoordinator
|
import com.zaneschepke.wireguardautotunnel.core.orchestration.AutoTunnelCoordinator
|
||||||
|
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
|
||||||
|
import com.zaneschepke.wireguardautotunnel.core.service.autotunnel.AutoTunnelStateHolder
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelMode
|
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelMode
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.enums.WifiDetectionMethod
|
import com.zaneschepke.wireguardautotunnel.domain.enums.WifiDetectionMethod
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConfig
|
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConfig
|
||||||
@@ -15,8 +16,6 @@ import com.zaneschepke.wireguardautotunnel.domain.repository.AutoTunnelSettingsR
|
|||||||
import com.zaneschepke.wireguardautotunnel.domain.repository.GlobalEffectRepository
|
import com.zaneschepke.wireguardautotunnel.domain.repository.GlobalEffectRepository
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository
|
import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.sideeffect.GlobalSideEffect
|
import com.zaneschepke.wireguardautotunnel.domain.sideeffect.GlobalSideEffect
|
||||||
import com.zaneschepke.wireguardautotunnel.service.ServiceManager
|
|
||||||
import com.zaneschepke.wireguardautotunnel.service.autotunnel.AutoTunnelStateHolder
|
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.state.AutoTunnelUiState
|
import com.zaneschepke.wireguardautotunnel.ui.state.AutoTunnelUiState
|
||||||
import com.zaneschepke.wireguardautotunnel.util.StringValue
|
import com.zaneschepke.wireguardautotunnel.util.StringValue
|
||||||
import kotlinx.coroutines.flow.combine
|
import kotlinx.coroutines.flow.combine
|
||||||
@@ -100,10 +99,7 @@ class AutoTunnelViewModel(
|
|||||||
val trimmed = name.trim()
|
val trimmed = name.trim()
|
||||||
if (state.autoTunnelSettings.trustedNetworkSSIDs.contains(name)) {
|
if (state.autoTunnelSettings.trustedNetworkSSIDs.contains(name)) {
|
||||||
return@intent postSideEffect(
|
return@intent postSideEffect(
|
||||||
GlobalSideEffect.Snackbar(
|
GlobalSideEffect.Snackbar(StringValue.StringResource(R.string.error_ssid_exists))
|
||||||
StringValue.StringResource(R.string.error_ssid_exists),
|
|
||||||
ToastType.Error,
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
setTrustedNetworkNames(
|
setTrustedNetworkNames(
|
||||||
@@ -145,10 +141,6 @@ class AutoTunnelViewModel(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setDisabledOnCaptivePortal(enabled: Boolean) = intent {
|
|
||||||
autoTunnelRepository.updateDisableOnCaptivePortal(enabled)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun removeTunnelNetwork(tunnel: TunnelConfig, ssid: String) = intent {
|
fun removeTunnelNetwork(tunnel: TunnelConfig, ssid: String) = intent {
|
||||||
tunnelsRepository.save(
|
tunnelsRepository.save(
|
||||||
tunnel.copy(
|
tunnel.copy(
|
||||||
@@ -161,19 +153,11 @@ class AutoTunnelViewModel(
|
|||||||
when (method) {
|
when (method) {
|
||||||
WifiDetectionMethod.ROOT -> {
|
WifiDetectionMethod.ROOT -> {
|
||||||
val accepted = RootShell.requestRootPermission()
|
val accepted = RootShell.requestRootPermission()
|
||||||
if (!accepted)
|
val message =
|
||||||
return@intent postSideEffect(
|
if (!accepted) StringValue.StringResource(R.string.error_root_denied)
|
||||||
GlobalSideEffect.Snackbar(
|
else StringValue.StringResource(R.string.root_accepted)
|
||||||
StringValue.StringResource(R.string.error_root_denied),
|
postSideEffect(GlobalSideEffect.Snackbar(message))
|
||||||
ToastType.Error,
|
if (!accepted) return@intent
|
||||||
)
|
|
||||||
)
|
|
||||||
postSideEffect(
|
|
||||||
GlobalSideEffect.Snackbar(
|
|
||||||
StringValue.StringResource(R.string.root_accepted),
|
|
||||||
ToastType.Success,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
WifiDetectionMethod.SHIZUKU -> {
|
WifiDetectionMethod.SHIZUKU -> {
|
||||||
requestShizuku()
|
requestShizuku()
|
||||||
@@ -204,10 +188,7 @@ class AutoTunnelViewModel(
|
|||||||
)
|
)
|
||||||
} catch (_: Exception) {
|
} catch (_: Exception) {
|
||||||
postSideEffect(
|
postSideEffect(
|
||||||
GlobalSideEffect.Snackbar(
|
GlobalSideEffect.Snackbar(StringValue.StringResource(R.string.shizuku_not_detected))
|
||||||
StringValue.StringResource(R.string.shizuku_not_detected),
|
|
||||||
ToastType.Error,
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+6
-13
@@ -1,7 +1,6 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.viewmodel
|
package com.zaneschepke.wireguardautotunnel.viewmodel
|
||||||
|
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import com.dokar.sonner.ToastType
|
|
||||||
import com.zaneschepke.wireguardautotunnel.R
|
import com.zaneschepke.wireguardautotunnel.R
|
||||||
import com.zaneschepke.wireguardautotunnel.core.orchestration.TunnelCoordinator
|
import com.zaneschepke.wireguardautotunnel.core.orchestration.TunnelCoordinator
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.enums.MimicMode
|
import com.zaneschepke.wireguardautotunnel.domain.enums.MimicMode
|
||||||
@@ -94,6 +93,7 @@ class ConfigEditViewModel(
|
|||||||
tunnel = tunnel,
|
tunnel = tunnel,
|
||||||
tunnels = tunnels,
|
tunnels = tunnels,
|
||||||
isRunning = isRunning,
|
isRunning = isRunning,
|
||||||
|
globalSettings = globalSettings,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -106,10 +106,7 @@ class ConfigEditViewModel(
|
|||||||
if (state.isTunnelNameTaken) {
|
if (state.isTunnelNameTaken) {
|
||||||
|
|
||||||
postSideEffect(
|
postSideEffect(
|
||||||
GlobalSideEffect.Snackbar(
|
GlobalSideEffect.Snackbar(StringValue.StringResource(R.string.tunnel_name_taken))
|
||||||
StringValue.StringResource(R.string.tunnel_name_taken),
|
|
||||||
ToastType.Error,
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
return@intent
|
return@intent
|
||||||
@@ -117,10 +114,7 @@ class ConfigEditViewModel(
|
|||||||
|
|
||||||
if (state.draft.tunnelName.isBlank()) {
|
if (state.draft.tunnelName.isBlank()) {
|
||||||
postSideEffect(
|
postSideEffect(
|
||||||
GlobalSideEffect.Snackbar(
|
GlobalSideEffect.Snackbar(StringValue.StringResource(R.string.name_error_empty))
|
||||||
StringValue.StringResource(R.string.name_error_empty),
|
|
||||||
ToastType.Error,
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
return@intent
|
return@intent
|
||||||
}
|
}
|
||||||
@@ -154,9 +148,8 @@ class ConfigEditViewModel(
|
|||||||
}
|
}
|
||||||
|
|
||||||
postSideEffect(
|
postSideEffect(
|
||||||
GlobalSideEffect.Snackbar(
|
GlobalSideEffect.Toast(
|
||||||
StringValue.StringResource(R.string.config_changes_saved),
|
StringValue.StringResource(R.string.config_changes_saved)
|
||||||
ToastType.Success,
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -172,7 +165,7 @@ class ConfigEditViewModel(
|
|||||||
else -> StringValue.StringResource(R.string.unknown_error)
|
else -> StringValue.StringResource(R.string.unknown_error)
|
||||||
}
|
}
|
||||||
|
|
||||||
postSideEffect(GlobalSideEffect.Snackbar(message, ToastType.Error))
|
postSideEffect(GlobalSideEffect.Snackbar(message))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.viewmodel
|
package com.zaneschepke.wireguardautotunnel.viewmodel
|
||||||
|
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import com.dokar.sonner.ToastType
|
|
||||||
import com.zaneschepke.networkmonitor.NetworkMonitor
|
import com.zaneschepke.networkmonitor.NetworkMonitor
|
||||||
import com.zaneschepke.wireguardautotunnel.R
|
import com.zaneschepke.wireguardautotunnel.R
|
||||||
import com.zaneschepke.wireguardautotunnel.core.orchestration.DnsSettingsCoordinator
|
import com.zaneschepke.wireguardautotunnel.core.orchestration.DnsSettingsCoordinator
|
||||||
@@ -10,6 +9,7 @@ import com.zaneschepke.wireguardautotunnel.domain.repository.DnsSettingsReposito
|
|||||||
import com.zaneschepke.wireguardautotunnel.domain.repository.GlobalEffectRepository
|
import com.zaneschepke.wireguardautotunnel.domain.repository.GlobalEffectRepository
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository
|
import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.sideeffect.GlobalSideEffect
|
import com.zaneschepke.wireguardautotunnel.domain.sideeffect.GlobalSideEffect
|
||||||
|
import com.zaneschepke.wireguardautotunnel.ui.common.snackbar.SnackbarType
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.state.DnsUiState
|
import com.zaneschepke.wireguardautotunnel.ui.state.DnsUiState
|
||||||
import com.zaneschepke.wireguardautotunnel.util.DnsValidator
|
import com.zaneschepke.wireguardautotunnel.util.DnsValidator
|
||||||
import com.zaneschepke.wireguardautotunnel.util.StringValue
|
import com.zaneschepke.wireguardautotunnel.util.StringValue
|
||||||
@@ -70,7 +70,7 @@ class DnsViewModel(
|
|||||||
postSideEffect(
|
postSideEffect(
|
||||||
GlobalSideEffect.Snackbar(
|
GlobalSideEffect.Snackbar(
|
||||||
StringValue.StringResource(result.error.labelRes()),
|
StringValue.StringResource(result.error.labelRes()),
|
||||||
type = ToastType.Error,
|
type = SnackbarType.WARNING,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
return@intent
|
return@intent
|
||||||
@@ -87,10 +87,7 @@ class DnsViewModel(
|
|||||||
|
|
||||||
postSideEffect(GlobalSideEffect.PopBackStack)
|
postSideEffect(GlobalSideEffect.PopBackStack)
|
||||||
postSideEffect(
|
postSideEffect(
|
||||||
GlobalSideEffect.Snackbar(
|
GlobalSideEffect.Toast(StringValue.StringResource(R.string.config_changes_saved))
|
||||||
StringValue.StringResource(R.string.config_changes_saved),
|
|
||||||
ToastType.Success,
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+1
-5
@@ -1,7 +1,6 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.viewmodel
|
package com.zaneschepke.wireguardautotunnel.viewmodel
|
||||||
|
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import com.dokar.sonner.ToastType
|
|
||||||
import com.zaneschepke.wireguardautotunnel.R
|
import com.zaneschepke.wireguardautotunnel.R
|
||||||
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelProvider
|
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelProvider
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.repository.GlobalEffectRepository
|
import com.zaneschepke.wireguardautotunnel.domain.repository.GlobalEffectRepository
|
||||||
@@ -53,10 +52,7 @@ class LockdownViewModel(
|
|||||||
|
|
||||||
postSideEffect(GlobalSideEffect.PopBackStack)
|
postSideEffect(GlobalSideEffect.PopBackStack)
|
||||||
postSideEffect(
|
postSideEffect(
|
||||||
GlobalSideEffect.Snackbar(
|
GlobalSideEffect.Toast(StringValue.StringResource(R.string.config_changes_saved))
|
||||||
StringValue.StringResource(R.string.config_changes_saved),
|
|
||||||
ToastType.Success,
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package com.zaneschepke.wireguardautotunnel.viewmodel
|
|||||||
|
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import com.dokar.sonner.ToastType
|
|
||||||
import com.zaneschepke.logcatter.LogReader
|
import com.zaneschepke.logcatter.LogReader
|
||||||
import com.zaneschepke.wireguardautotunnel.BuildConfig
|
import com.zaneschepke.wireguardautotunnel.BuildConfig
|
||||||
import com.zaneschepke.wireguardautotunnel.R
|
import com.zaneschepke.wireguardautotunnel.R
|
||||||
@@ -62,10 +61,7 @@ class LoggerViewModel(
|
|||||||
fun exportLogs(uri: Uri?) = intent {
|
fun exportLogs(uri: Uri?) = intent {
|
||||||
if (uri == null) {
|
if (uri == null) {
|
||||||
postSideEffect(
|
postSideEffect(
|
||||||
GlobalSideEffect.Snackbar(
|
GlobalSideEffect.Toast(StringValue.StringResource(R.string.export_unsupported))
|
||||||
StringValue.StringResource(R.string.export_unsupported),
|
|
||||||
ToastType.Warning,
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
return@intent
|
return@intent
|
||||||
}
|
}
|
||||||
@@ -80,12 +76,11 @@ class LoggerViewModel(
|
|||||||
Timber.e(action)
|
Timber.e(action)
|
||||||
intent {
|
intent {
|
||||||
postSideEffect(
|
postSideEffect(
|
||||||
GlobalSideEffect.Snackbar(
|
GlobalSideEffect.Toast(
|
||||||
StringValue.StringResource(
|
StringValue.StringResource(
|
||||||
R.string.export_failed,
|
R.string.export_failed,
|
||||||
": ${action.localizedMessage}",
|
": ${action.localizedMessage}",
|
||||||
),
|
)
|
||||||
ToastType.Error,
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+17
-27
@@ -1,7 +1,6 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.viewmodel
|
package com.zaneschepke.wireguardautotunnel.viewmodel
|
||||||
|
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import com.dokar.sonner.ToastType
|
|
||||||
import com.zaneschepke.wireguardautotunnel.R
|
import com.zaneschepke.wireguardautotunnel.R
|
||||||
import com.zaneschepke.wireguardautotunnel.core.orchestration.TunnelCoordinator
|
import com.zaneschepke.wireguardautotunnel.core.orchestration.TunnelCoordinator
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.repository.GlobalEffectRepository
|
import com.zaneschepke.wireguardautotunnel.domain.repository.GlobalEffectRepository
|
||||||
@@ -28,26 +27,21 @@ class ProxySettingsViewModel(
|
|||||||
combine(tunnelCoordinator.backendStatus, proxySettingsRepository.flow) {
|
combine(tunnelCoordinator.backendStatus, proxySettingsRepository.flow) {
|
||||||
backendStatus,
|
backendStatus,
|
||||||
settings ->
|
settings ->
|
||||||
if (state.isLoading) {
|
ProxySettingsUiState(
|
||||||
ProxySettingsUiState(
|
proxySettings = settings,
|
||||||
proxySettings = settings,
|
backendStatus = backendStatus,
|
||||||
backendStatus = backendStatus,
|
isLoading = false,
|
||||||
isLoading = false,
|
socks5Enabled = settings.socks5ProxyEnabled,
|
||||||
socks5Enabled = settings.socks5ProxyEnabled,
|
httpEnabled = settings.httpProxyEnabled,
|
||||||
httpEnabled = settings.httpProxyEnabled,
|
socksBindAddress = settings.socks5ProxyBindAddress ?: "",
|
||||||
socksBindAddress = settings.socks5ProxyBindAddress ?: "",
|
httpBindAddress = settings.httpProxyBindAddress ?: "",
|
||||||
httpBindAddress = settings.httpProxyBindAddress ?: "",
|
proxyUsername = settings.proxyUsername ?: "",
|
||||||
proxyUsername = settings.proxyUsername ?: "",
|
proxyPassword = settings.proxyPassword ?: "",
|
||||||
proxyPassword = settings.proxyPassword ?: "",
|
)
|
||||||
)
|
|
||||||
} else {
|
|
||||||
state.copy(backendStatus = backendStatus)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
.collect { reduce { it } }
|
.collect { reduce { it } }
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO add a dialog requesting restart if any tunnels active
|
|
||||||
fun save() = intent {
|
fun save() = intent {
|
||||||
reduce { state.copy(showSaveModal = false) }
|
reduce { state.copy(showSaveModal = false) }
|
||||||
|
|
||||||
@@ -80,8 +74,7 @@ class ProxySettingsViewModel(
|
|||||||
if (socksPort == null || httpPort == null || socksPort == httpPort) {
|
if (socksPort == null || httpPort == null || socksPort == httpPort) {
|
||||||
return@intent postSideEffect(
|
return@intent postSideEffect(
|
||||||
GlobalSideEffect.Snackbar(
|
GlobalSideEffect.Snackbar(
|
||||||
StringValue.StringResource(R.string.ports_must_differ),
|
StringValue.StringResource(R.string.ports_must_differ)
|
||||||
ToastType.Error,
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -98,21 +91,18 @@ class ProxySettingsViewModel(
|
|||||||
|
|
||||||
if (updated.proxyPassword?.any { it.isWhitespace() } == true) {
|
if (updated.proxyPassword?.any { it.isWhitespace() } == true) {
|
||||||
postSideEffect(
|
postSideEffect(
|
||||||
GlobalSideEffect.Snackbar(
|
GlobalSideEffect.Snackbar(StringValue.StringResource(R.string.password_no_spaces))
|
||||||
StringValue.StringResource(R.string.password_no_spaces),
|
|
||||||
ToastType.Error,
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
return@intent reduce { state.copy(isPasswordError = true) }
|
return@intent reduce { state.copy(isPasswordError = true) }
|
||||||
}
|
}
|
||||||
|
|
||||||
proxySettingsRepository.upsert(updated)
|
proxySettingsRepository.upsert(updated)
|
||||||
|
|
||||||
|
tunnelCoordinator.toggleTunnels()
|
||||||
|
tunnelCoordinator.toggleTunnels()
|
||||||
|
|
||||||
postSideEffect(
|
postSideEffect(
|
||||||
GlobalSideEffect.Snackbar(
|
GlobalSideEffect.Snackbar(StringValue.StringResource(R.string.config_changes_saved))
|
||||||
StringValue.StringResource(R.string.config_changes_saved),
|
|
||||||
ToastType.Success,
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
postSideEffect(GlobalSideEffect.PopBackStack)
|
postSideEffect(GlobalSideEffect.PopBackStack)
|
||||||
}
|
}
|
||||||
|
|||||||
+6
-15
@@ -1,8 +1,7 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.viewmodel
|
package com.zaneschepke.wireguardautotunnel.viewmodel
|
||||||
|
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import com.dokar.sonner.ToastType
|
import com.zaneschepke.tunnel.backend.RootShell
|
||||||
import com.zaneschepke.tunnel.util.RootShell
|
|
||||||
import com.zaneschepke.wireguardautotunnel.R
|
import com.zaneschepke.wireguardautotunnel.R
|
||||||
import com.zaneschepke.wireguardautotunnel.core.orchestration.TunnelCoordinator
|
import com.zaneschepke.wireguardautotunnel.core.orchestration.TunnelCoordinator
|
||||||
import com.zaneschepke.wireguardautotunnel.core.shortcut.ShortcutManager
|
import com.zaneschepke.wireguardautotunnel.core.shortcut.ShortcutManager
|
||||||
@@ -97,19 +96,11 @@ class SettingsViewModel(
|
|||||||
fun setTunnelScriptedEnabled(to: Boolean) = intent {
|
fun setTunnelScriptedEnabled(to: Boolean) = intent {
|
||||||
if (to) {
|
if (to) {
|
||||||
val accepted = RootShell.requestRootPermission()
|
val accepted = RootShell.requestRootPermission()
|
||||||
if (!accepted)
|
val message =
|
||||||
return@intent postSideEffect(
|
if (!accepted) StringValue.StringResource(R.string.error_root_denied)
|
||||||
GlobalSideEffect.Snackbar(
|
else StringValue.StringResource(R.string.root_accepted)
|
||||||
StringValue.StringResource(R.string.error_root_denied),
|
postSideEffect(GlobalSideEffect.Snackbar(message))
|
||||||
ToastType.Error,
|
if (!accepted) return@intent
|
||||||
)
|
|
||||||
)
|
|
||||||
postSideEffect(
|
|
||||||
GlobalSideEffect.Snackbar(
|
|
||||||
StringValue.StringResource(R.string.root_accepted),
|
|
||||||
ToastType.Success,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
settingsRepository.upsert(state.settings.copy(tunnelScriptingEnabled = to))
|
settingsRepository.upsert(state.settings.copy(tunnelScriptingEnabled = to))
|
||||||
}
|
}
|
||||||
|
|||||||
+31
-70
@@ -3,10 +3,11 @@ package com.zaneschepke.wireguardautotunnel.viewmodel
|
|||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import com.dokar.sonner.ToastType
|
|
||||||
import com.zaneschepke.wireguardautotunnel.R
|
import com.zaneschepke.wireguardautotunnel.R
|
||||||
import com.zaneschepke.wireguardautotunnel.core.orchestration.TunnelCoordinator
|
import com.zaneschepke.wireguardautotunnel.core.orchestration.TunnelCoordinator
|
||||||
import com.zaneschepke.wireguardautotunnel.core.orchestration.TunnelModeCoordinator
|
import com.zaneschepke.wireguardautotunnel.core.orchestration.TunnelModeCoordinator
|
||||||
|
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
|
||||||
|
import com.zaneschepke.wireguardautotunnel.core.service.autotunnel.AutoTunnelStateHolder
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelMode
|
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelMode
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConfig
|
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConfig
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.repository.AppStateRepository
|
import com.zaneschepke.wireguardautotunnel.domain.repository.AppStateRepository
|
||||||
@@ -16,10 +17,7 @@ import com.zaneschepke.wireguardautotunnel.domain.repository.SelectedTunnelsRepo
|
|||||||
import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository
|
import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.sideeffect.GlobalSideEffect
|
import com.zaneschepke.wireguardautotunnel.domain.sideeffect.GlobalSideEffect
|
||||||
import com.zaneschepke.wireguardautotunnel.parser.ConfigParseException
|
import com.zaneschepke.wireguardautotunnel.parser.ConfigParseException
|
||||||
import com.zaneschepke.wireguardautotunnel.service.ServiceManager
|
|
||||||
import com.zaneschepke.wireguardautotunnel.service.autotunnel.AutoTunnelStateHolder
|
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.sideeffect.LocalSideEffect
|
import com.zaneschepke.wireguardautotunnel.ui.sideeffect.LocalSideEffect
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.state.DisplayTunnelState
|
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.state.GlobalAppUiState
|
import com.zaneschepke.wireguardautotunnel.ui.state.GlobalAppUiState
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.state.TunnelsUiState
|
import com.zaneschepke.wireguardautotunnel.ui.state.TunnelsUiState
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.theme.Theme
|
import com.zaneschepke.wireguardautotunnel.ui.theme.Theme
|
||||||
@@ -29,7 +27,6 @@ import com.zaneschepke.wireguardautotunnel.util.StringValue
|
|||||||
import com.zaneschepke.wireguardautotunnel.util.extensions.QuickConfig
|
import com.zaneschepke.wireguardautotunnel.util.extensions.QuickConfig
|
||||||
import com.zaneschepke.wireguardautotunnel.util.extensions.TunnelName
|
import com.zaneschepke.wireguardautotunnel.util.extensions.TunnelName
|
||||||
import com.zaneschepke.wireguardautotunnel.util.extensions.asStringValue
|
import com.zaneschepke.wireguardautotunnel.util.extensions.asStringValue
|
||||||
import com.zaneschepke.wireguardautotunnel.util.extensions.isHtmlResponse
|
|
||||||
import com.zaneschepke.wireguardautotunnel.util.extensions.saveTunnelsUniquely
|
import com.zaneschepke.wireguardautotunnel.util.extensions.saveTunnelsUniquely
|
||||||
import com.zaneschepke.wireguardautotunnel.util.network.NetworkUtils
|
import com.zaneschepke.wireguardautotunnel.util.network.NetworkUtils
|
||||||
import io.ktor.client.HttpClient
|
import io.ktor.client.HttpClient
|
||||||
@@ -74,16 +71,10 @@ class SharedAppViewModel(
|
|||||||
tunnelRepository.userTunnelsFlow,
|
tunnelRepository.userTunnelsFlow,
|
||||||
tunnelCoordinator.backendStatus,
|
tunnelCoordinator.backendStatus,
|
||||||
selectedTunnelsRepository.flow,
|
selectedTunnelsRepository.flow,
|
||||||
) { tunnels, backendStatus, selectedTuns ->
|
tunnelCoordinator.tunnelDisplayStates,
|
||||||
val sortedTunnels = tunnels.sortedBy { it.position }
|
) { tunnels, backendStatus, selectedTuns, displayStates ->
|
||||||
|
|
||||||
val displayStates =
|
|
||||||
backendStatus.activeTunnels.mapValues { (_, activeTunnel) ->
|
|
||||||
DisplayTunnelState.from(activeTunnel)
|
|
||||||
}
|
|
||||||
|
|
||||||
TunnelsUiState(
|
TunnelsUiState(
|
||||||
tunnels = sortedTunnels,
|
tunnels = tunnels,
|
||||||
backendStatus = backendStatus,
|
backendStatus = backendStatus,
|
||||||
displayStates = displayStates,
|
displayStates = displayStates,
|
||||||
selectedTunnels = selectedTuns,
|
selectedTunnels = selectedTuns,
|
||||||
@@ -179,10 +170,6 @@ class SharedAppViewModel(
|
|||||||
appStateRepository.setShouldShowDonationSnackbar(to)
|
appStateRepository.setShouldShowDonationSnackbar(to)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun showSnackMessage(message: StringValue, type: ToastType) = intent {
|
|
||||||
postGlobalSideEffect(GlobalSideEffect.Snackbar(message, type))
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun postSideEffect(globalSideEffect: GlobalSideEffect) {
|
suspend fun postSideEffect(globalSideEffect: GlobalSideEffect) {
|
||||||
globalEffectRepository.post(globalSideEffect)
|
globalEffectRepository.post(globalSideEffect)
|
||||||
}
|
}
|
||||||
@@ -193,6 +180,12 @@ class SharedAppViewModel(
|
|||||||
globalEffectRepository.post(sideEffect)
|
globalEffectRepository.post(sideEffect)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun showSnackMessage(message: StringValue) = intent {
|
||||||
|
postGlobalSideEffect(GlobalSideEffect.Snackbar(message))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun showToast(message: StringValue) = intent { postSideEffect(GlobalSideEffect.Toast(message)) }
|
||||||
|
|
||||||
fun disableBatteryOptimizationsShown() = intent {
|
fun disableBatteryOptimizationsShown() = intent {
|
||||||
appStateRepository.setBatteryOptimizationDisableShown(true)
|
appStateRepository.setBatteryOptimizationDisableShown(true)
|
||||||
}
|
}
|
||||||
@@ -200,20 +193,14 @@ class SharedAppViewModel(
|
|||||||
fun saveSortChanges(tunnels: List<TunnelConfig>) = intent {
|
fun saveSortChanges(tunnels: List<TunnelConfig>) = intent {
|
||||||
tunnelRepository.saveAll(tunnels.mapIndexed { index, conf -> conf.copy(position = index) })
|
tunnelRepository.saveAll(tunnels.mapIndexed { index, conf -> conf.copy(position = index) })
|
||||||
postSideEffect(
|
postSideEffect(
|
||||||
GlobalSideEffect.Snackbar(
|
GlobalSideEffect.Snackbar(StringValue.StringResource(R.string.config_changes_saved))
|
||||||
StringValue.StringResource(R.string.config_changes_saved),
|
|
||||||
ToastType.Success,
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
postSideEffect(GlobalSideEffect.PopBackStack)
|
postSideEffect(GlobalSideEffect.PopBackStack)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun sortByLatency(tunnels: List<TunnelConfig>) = intent {
|
fun sortByLatency(tunnels: List<TunnelConfig>) = intent {
|
||||||
postSideEffect(
|
postSideEffect(
|
||||||
GlobalSideEffect.Snackbar(
|
GlobalSideEffect.Snackbar(StringValue.StringResource(R.string.pinging_servers))
|
||||||
StringValue.StringResource(R.string.pinging_servers),
|
|
||||||
ToastType.Info,
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
val sortedResult =
|
val sortedResult =
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
@@ -255,17 +242,12 @@ class SharedAppViewModel(
|
|||||||
TunnelConfig.tunnelConfFromQuick(config, name)
|
TunnelConfig.tunnelConfFromQuick(config, name)
|
||||||
}
|
}
|
||||||
tunnelRepository.saveTunnelsUniquely(tunnelConfigs, state.tunnelNames.map { it.value })
|
tunnelRepository.saveTunnelsUniquely(tunnelConfigs, state.tunnelNames.map { it.value })
|
||||||
} catch (e: Exception) {
|
} catch (_: IOException) {
|
||||||
if (e is ConfigParseException) {
|
postSideEffect(
|
||||||
postSideEffect(GlobalSideEffect.Snackbar(e.asStringValue(), ToastType.Error))
|
GlobalSideEffect.Snackbar(StringValue.StringResource(R.string.read_failed))
|
||||||
} else {
|
)
|
||||||
postSideEffect(
|
} catch (e: ConfigParseException) {
|
||||||
GlobalSideEffect.Snackbar(
|
postSideEffect(GlobalSideEffect.Snackbar(e.asStringValue()))
|
||||||
StringValue.StringResource(R.string.config_error),
|
|
||||||
ToastType.Error,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -275,38 +257,22 @@ class SharedAppViewModel(
|
|||||||
|
|
||||||
fun importFromQr(conf: String) = intent { importFromClipboard(conf) }
|
fun importFromQr(conf: String) = intent { importFromClipboard(conf) }
|
||||||
|
|
||||||
fun promptWgImport(url: String) = intent { reduce { state.copy(pendingWgImportUrl = url) } }
|
|
||||||
|
|
||||||
fun dismissWgImport() = intent { reduce { state.copy(pendingWgImportUrl = null) } }
|
|
||||||
|
|
||||||
fun importFromUrl(url: String) = intent {
|
fun importFromUrl(url: String) = intent {
|
||||||
reduce { state.copy(pendingWgImportUrl = null) }
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
httpClient.prepareGet(url).execute { response ->
|
httpClient.prepareGet(url).execute { response ->
|
||||||
if (response.status.value !in 200..299) {
|
if (response.status.value in 200..299) {
|
||||||
throw IOException("Server returned error: ${response.status.value}")
|
val body = response.bodyAsText()
|
||||||
}
|
importFromClipboard(body)
|
||||||
|
} else {
|
||||||
if (response.isHtmlResponse()) {
|
throw IOException(
|
||||||
postSideEffect(
|
"Failed to download file with error status: ${response.status.value}"
|
||||||
GlobalSideEffect.Snackbar(
|
|
||||||
StringValue.StringResource(R.string.error_invalid_config_url),
|
|
||||||
ToastType.Error,
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
return@execute
|
|
||||||
}
|
}
|
||||||
val body = response.bodyAsText()
|
|
||||||
importFromClipboard(body)
|
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Timber.e(e)
|
Timber.e(e)
|
||||||
postSideEffect(
|
postSideEffect(
|
||||||
GlobalSideEffect.Snackbar(
|
GlobalSideEffect.Toast(StringValue.StringResource(R.string.error_download_failed))
|
||||||
StringValue.StringResource(R.string.error_download_failed),
|
|
||||||
ToastType.Error,
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -321,7 +287,7 @@ class SharedAppViewModel(
|
|||||||
is IOException -> StringValue.StringResource(R.string.error_download_failed)
|
is IOException -> StringValue.StringResource(R.string.error_download_failed)
|
||||||
else -> StringValue.StringResource(R.string.error_file_extension)
|
else -> StringValue.StringResource(R.string.error_file_extension)
|
||||||
}
|
}
|
||||||
postSideEffect(GlobalSideEffect.Snackbar(message, ToastType.Error))
|
postSideEffect(GlobalSideEffect.Toast(message))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -353,8 +319,7 @@ class SharedAppViewModel(
|
|||||||
if (selectedTuns.any { activeTunIds?.contains(it.id) == true })
|
if (selectedTuns.any { activeTunIds?.contains(it.id) == true })
|
||||||
return@intent postSideEffect(
|
return@intent postSideEffect(
|
||||||
GlobalSideEffect.Snackbar(
|
GlobalSideEffect.Snackbar(
|
||||||
StringValue.StringResource(R.string.delete_active_message),
|
StringValue.StringResource(R.string.delete_active_message)
|
||||||
ToastType.Error,
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
tunnelRepository.delete(selectedTuns)
|
tunnelRepository.delete(selectedTuns)
|
||||||
@@ -377,12 +342,11 @@ class SharedAppViewModel(
|
|||||||
val onFailure = { action: Throwable ->
|
val onFailure = { action: Throwable ->
|
||||||
intent {
|
intent {
|
||||||
postSideEffect(
|
postSideEffect(
|
||||||
GlobalSideEffect.Snackbar(
|
GlobalSideEffect.Toast(
|
||||||
StringValue.StringResource(
|
StringValue.StringResource(
|
||||||
R.string.export_failed,
|
R.string.export_failed,
|
||||||
": ${action.localizedMessage}",
|
": ${action.localizedMessage}",
|
||||||
),
|
)
|
||||||
ToastType.Error,
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -399,10 +363,7 @@ class SharedAppViewModel(
|
|||||||
if (it.exists()) it.delete()
|
if (it.exists()) it.delete()
|
||||||
}
|
}
|
||||||
postSideEffect(
|
postSideEffect(
|
||||||
GlobalSideEffect.Snackbar(
|
GlobalSideEffect.Snackbar(StringValue.StringResource(R.string.export_success))
|
||||||
StringValue.StringResource(R.string.export_success),
|
|
||||||
ToastType.Success,
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
clearSelectedTunnels()
|
clearSelectedTunnels()
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-8
@@ -1,7 +1,6 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.viewmodel
|
package com.zaneschepke.wireguardautotunnel.viewmodel
|
||||||
|
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import com.dokar.sonner.ToastType
|
|
||||||
import com.zaneschepke.wireguardautotunnel.R
|
import com.zaneschepke.wireguardautotunnel.R
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.repository.GlobalEffectRepository
|
import com.zaneschepke.wireguardautotunnel.domain.repository.GlobalEffectRepository
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.repository.InstalledPackageRepository
|
import com.zaneschepke.wireguardautotunnel.domain.repository.InstalledPackageRepository
|
||||||
@@ -89,14 +88,9 @@ class SplitTunnelViewModel(
|
|||||||
editableInterface.copy(includedApplications = included, excludedApplications = excluded)
|
editableInterface.copy(includedApplications = included, excludedApplications = excluded)
|
||||||
val updatedProxyConfig = editableConfig.copy(`interface` = updatedInterface)
|
val updatedProxyConfig = editableConfig.copy(`interface` = updatedInterface)
|
||||||
val updatedConfig = updatedProxyConfig.buildConfig()
|
val updatedConfig = updatedProxyConfig.buildConfig()
|
||||||
tunnelRepository.save(
|
tunnelRepository.save(tunnel.copy(quickConfig = updatedConfig.asQuickString()))
|
||||||
tunnel.copy(quickConfig = updatedConfig.withName(tunnel.name).asQuickString())
|
|
||||||
)
|
|
||||||
postSideEffect(
|
postSideEffect(
|
||||||
GlobalSideEffect.Snackbar(
|
GlobalSideEffect.Snackbar(StringValue.StringResource(R.string.config_changes_saved))
|
||||||
StringValue.StringResource(R.string.config_changes_saved),
|
|
||||||
ToastType.Success,
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
postSideEffect(GlobalSideEffect.PopBackStack)
|
postSideEffect(GlobalSideEffect.PopBackStack)
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user